[libcontrolsfx-java] 01/02: New upstream version 8.40.12

Andreas Tille tille at debian.org
Sat May 20 20:40:50 UTC 2017


This is an automated email from the git hooks/post-receive script.

tille pushed a commit to branch master
in repository libcontrolsfx-java.

commit 18b25cfc07969ac76e6e77a11620631dd6482771
Author: Andreas Tille <tille at debian.org>
Date:   Sat May 20 22:22:52 2017 +0200

    New upstream version 8.40.12
---
 .classpath                                         |    9 +
 .hg_archival.txt                                   |    4 +
 .hgeol                                             |    2 +
 .hgignore                                          |   18 +
 .hgtags                                            |   18 +
 .project                                           |   43 +
 bitbucket-pipelines.yml                            |   12 +
 build.gradle                                       |   70 +
 controlsfx-build.properties                        |   12 +
 controlsfx-samples/.classpath                      |   11 +
 controlsfx-samples/.project                        |   19 +
 controlsfx-samples/build.gradle                    |   41 +
 .../eclipse/ControlsFX Samples.launch              |   24 +
 .../src/deploy/package/shortcut-128.png            |  Bin 0 -> 19897 bytes
 .../src/deploy/package/volume-128.png              |  Bin 0 -> 19897 bytes
 .../main/java/org/controlsfx/ControlsFXSample.java |   62 +
 .../java/org/controlsfx/ControlsFXSampler.java     |   51 +
 .../java/org/controlsfx/samples/HelloBorders.java  |  214 +++
 .../org/controlsfx/samples/HelloDecorator.java     |  168 ++
 .../org/controlsfx/samples/HelloGlyphFont.java     |  170 ++
 .../java/org/controlsfx/samples/HelloGridView.java |  142 ++
 .../controlsfx/samples/HelloHiddenSidesPane.java   |  124 ++
 .../controlsfx/samples/HelloHyperlinkLabel.java    |   98 +
 .../org/controlsfx/samples/HelloInfoOverlay.java   |  115 ++
 .../controlsfx/samples/HelloListSelectionView.java |  131 ++
 .../org/controlsfx/samples/HelloMaskerPane.java    |  130 ++
 .../controlsfx/samples/HelloMasterDetailPane.java  |  119 ++
 .../controlsfx/samples/HelloNotificationPane.java  |  143 ++
 .../org/controlsfx/samples/HelloNotifications.java |  368 ++++
 .../controlsfx/samples/HelloPlusMinusSlider.java   |  119 ++
 .../java/org/controlsfx/samples/HelloPopOver.java  |  368 ++++
 .../controlsfx/samples/HelloPrefixSelection.java   |  142 ++
 .../org/controlsfx/samples/HelloPropertySheet.java |  260 +++
 .../org/controlsfx/samples/HelloRangeSlider.java   |  184 ++
 .../java/org/controlsfx/samples/HelloRating.java   |  117 ++
 .../org/controlsfx/samples/HelloSnapshotView.java  |  545 ++++++
 .../controlsfx/samples/HelloSpreadsheetView.java   |  716 ++++++++
 .../org/controlsfx/samples/HelloStatusBar.java     |  161 ++
 .../controlsfx/samples/HelloTaskProgressView.java  |  206 +++
 .../org/controlsfx/samples/HelloToggleSwitch.java  |  108 ++
 .../org/controlsfx/samples/HelloValidation.java    |  250 +++
 .../main/java/org/controlsfx/samples/Utils.java    |   33 +
 .../samples/actions/HelloActionGroup.java          |  204 +++
 .../samples/actions/HelloActionProxy.java          |  224 +++
 .../samples/actions/HelloCustomAction.java         |   60 +
 .../samples/actions/HelloCustomActionFactory.java  |   47 +
 .../samples/button/HelloBreadCrumbBar.java         |  144 ++
 .../controlsfx/samples/button/HelloButtonBar.java  |  198 ++
 .../samples/button/HelloSegmentedButton.java       |  147 ++
 .../samples/checked/HelloCheckComboBox.java        |  239 +++
 .../samples/checked/HelloCheckListView.java        |  167 ++
 .../samples/checked/HelloCheckTreeView.java        |  171 ++
 .../controlsfx/samples/dialogs/HelloDialogs.java   |  742 ++++++++
 .../controlsfx/samples/propertysheet/Address.java  |  117 ++
 .../samples/propertysheet/AddressBeanInfo.java     |   73 +
 .../propertysheet/CustomPropertyDescriptor.java    |   63 +
 .../samples/propertysheet/PopupPropertyEditor.java |  196 ++
 .../samples/propertysheet/SampleBean.java          |  106 ++
 .../samples/propertysheet/SampleBeanBeanInfo.java  |   79 +
 .../tablefilter/ConcurrentTableFilterTest.java     |  113 ++
 .../org/controlsfx/samples/tablefilter/Flight.java |   56 +
 .../samples/tablefilter/FlightTable.java           |  139 ++
 .../samples/tablefilter/LargeTableFilterTest.java  |   97 +
 .../samples/tablefilter/TableFilterMemoryTest.java |   86 +
 .../samples/tableview/HelloCustomTableMenu.java    |   80 +
 .../tableview/HelloSwingTableModelSample.java      |   79 +
 .../samples/tableview/HelloTableRowExpander.java   |  194 ++
 .../samples/textfields/HelloAutoComplete.java      |  154 ++
 .../samples/textfields/HelloTextFields.java        |  144 ++
 .../META-INF/services/fxsampler.FXSamplerProject   |    1 +
 .../org/controlsfx/samples/ControlsFX.png          |  Bin 0 -> 34049 bytes
 .../org/controlsfx/samples/ammunationLogo.JPG      |  Bin 0 -> 9769 bytes
 .../org/controlsfx/samples/apertureLogo.png        |  Bin 0 -> 2818 bytes
 .../main/resources/org/controlsfx/samples/bar.png  |  Bin 0 -> 168 bytes
 .../org/controlsfx/samples/controlsfx-logo.png     |  Bin 0 -> 13319 bytes
 .../org/controlsfx/samples/decorations.css         |    7 +
 .../org/controlsfx/samples/dialogs/login.png       |  Bin 0 -> 3724 bytes
 .../resources/org/controlsfx/samples/duke_wave.png |  Bin 0 -> 36659 bytes
 .../org/controlsfx/samples/exclamation.png         |  Bin 0 -> 1179 bytes
 .../resources/org/controlsfx/samples/flowers.png   |  Bin 0 -> 68778 bytes
 .../resources/org/controlsfx/samples/icomoon.ttf   |  Bin 0 -> 67960 bytes
 .../org/controlsfx/samples/information.png         |  Bin 0 -> 778 bytes
 .../samples/notification-pane-warning.png          |  Bin 0 -> 1402 bytes
 .../org/controlsfx/samples/nukaColaLogo.png        |  Bin 0 -> 13488 bytes
 .../org/controlsfx/samples/paynsprayLogo.jpg       |  Bin 0 -> 28186 bytes
 .../org/controlsfx/samples/raptureLogo.png         |  Bin 0 -> 31855 bytes
 .../org/controlsfx/samples/security-low.png        |  Bin 0 -> 669 bytes
 .../org/controlsfx/samples/spreadsheetSample.css   |  113 ++
 .../org/controlsfx/samples/toggleSwitchSample.css  |    9 +
 .../org/controlsfx/samples/umbrellacorporation.png |  Bin 0 -> 19224 bytes
 .../org/controlsfx/samples/validation.css          |    7 +
 controlsfx/.classpath                              |    8 +
 controlsfx/.project                                |   19 +
 controlsfx/build.gradle                            |  144 ++
 controlsfx/src/main/docs/ControlsFX.png            |  Bin 0 -> 34049 bytes
 .../docs/org/controlsfx/control/ToggleSwitch.png   |  Bin 0 -> 1611 bytes
 .../control/action/actionGroup-contextmenu.png     |  Bin 0 -> 2789 bytes
 .../control/action/actionGroup-menubar.png         |  Bin 0 -> 973 bytes
 .../control/action/actionGroup-toolbar.png         |  Bin 0 -> 1629 bytes
 .../docs/org/controlsfx/control/breadCrumbBar.png  |  Bin 0 -> 2176 bytes
 .../org/controlsfx/control/buttonBar-linux.png     |  Bin 0 -> 4374 bytes
 .../docs/org/controlsfx/control/buttonBar-mac.png  |  Bin 0 -> 4271 bytes
 .../org/controlsfx/control/buttonBar-windows.png   |  Bin 0 -> 4329 bytes
 .../docs/org/controlsfx/control/checkComboBox.png  |  Bin 0 -> 8759 bytes
 .../docs/org/controlsfx/control/checkListView.png  |  Bin 0 -> 9776 bytes
 .../docs/org/controlsfx/control/checkTreeView.png  |  Bin 0 -> 3972 bytes
 .../main/docs/org/controlsfx/control/gridView.png  |  Bin 0 -> 7801 bytes
 .../org/controlsfx/control/hiddenSidesPane.png     |  Bin 0 -> 15394 bytes
 .../docs/org/controlsfx/control/hyperlinkLabel.PNG |  Bin 0 -> 1523 bytes
 .../docs/org/controlsfx/control/infoOverlay.png    |  Bin 0 -> 27055 bytes
 .../org/controlsfx/control/list-selection-view.png |  Bin 0 -> 44610 bytes
 .../org/controlsfx/control/masterDetailPane.png    |  Bin 0 -> 11410 bytes
 .../control/notication-pane-dark-bottom.png        |  Bin 0 -> 8589 bytes
 .../control/notication-pane-dark-top.png           |  Bin 0 -> 9109 bytes
 .../control/notication-pane-light-bottom.png       |  Bin 0 -> 8260 bytes
 .../control/notication-pane-light-top.png          |  Bin 0 -> 8785 bytes
 .../docs/org/controlsfx/control/notifications.png  |  Bin 0 -> 10786 bytes
 .../org/controlsfx/control/plus-minus-slider.png   |  Bin 0 -> 5784 bytes
 .../org/controlsfx/control/popover-accordion.png   |  Bin 0 -> 79946 bytes
 .../org/controlsfx/control/popover-detached.png    |  Bin 0 -> 12077 bytes
 .../main/docs/org/controlsfx/control/popover.png   |  Bin 0 -> 11711 bytes
 .../docs/org/controlsfx/control/propertySheet.PNG  |  Bin 0 -> 16824 bytes
 .../controlsfx/control/rangeSlider-horizontal.png  |  Bin 0 -> 2055 bytes
 .../controlsfx/control/rangeSlider-vertical.png    |  Bin 0 -> 2633 bytes
 .../org/controlsfx/control/rating-horizontal.png   |  Bin 0 -> 5226 bytes
 .../docs/org/controlsfx/control/rating-partial.png |  Bin 0 -> 3702 bytes
 .../org/controlsfx/control/rating-vertical.png     |  Bin 0 -> 3633 bytes
 .../org/controlsfx/control/segmentedButton.png     |  Bin 0 -> 10968 bytes
 .../docs/org/controlsfx/control/snapshotView.png   |  Bin 0 -> 37083 bytes
 .../controlsfx/control/spreadsheet/dateEditor.png  |  Bin 0 -> 15265 bytes
 .../controlsfx/control/spreadsheet/dateFormat.PNG  |  Bin 0 -> 6291 bytes
 .../control/spreadsheet/doubleEditor.png           |  Bin 0 -> 2649 bytes
 .../control/spreadsheet/editorScheme.png           |  Bin 0 -> 123298 bytes
 .../controlsfx/control/spreadsheet/fixedColumn.png |  Bin 0 -> 107021 bytes
 .../control/spreadsheet/graphicNodeToCell.png      |  Bin 0 -> 43209 bytes
 .../controlsfx/control/spreadsheet/listEditor.png  |  Bin 0 -> 6189 bytes
 .../org/controlsfx/control/spreadsheet/pickers.PNG |  Bin 0 -> 8821 bytes
 .../controlsfx/control/spreadsheet/spanType.png    |  Bin 0 -> 22386 bytes
 .../control/spreadsheet/spreadsheetView.png        |  Bin 0 -> 90821 bytes
 .../controlsfx/control/spreadsheet/textEditor.png  |  Bin 0 -> 2442 bytes
 .../control/spreadsheet/triangleCell.PNG           |  Bin 0 -> 3628 bytes
 .../org/controlsfx/control/statusbar-items.png     |  Bin 0 -> 2379 bytes
 .../org/controlsfx/control/statusbar-progress.png  |  Bin 0 -> 1434 bytes
 .../main/docs/org/controlsfx/control/statusbar.png |  Bin 0 -> 570 bytes
 .../docs/org/controlsfx/control/task-monitor.png   |  Bin 0 -> 28078 bytes
 .../control/textfield/autoCompletion.png           |  Bin 0 -> 3906 bytes
 .../control/textfield/customTextField.png          |  Bin 0 -> 12700 bytes
 .../main/docs/org/controlsfx/control/wizard.png    |  Bin 0 -> 9754 bytes
 .../dialog/dialog-choicebox-masthead.png           |  Bin 0 -> 10390 bytes
 .../dialog/dialog-choicebox-no-masthead.png        |  Bin 0 -> 9659 bytes
 .../dialog/dialog-commandlink-masthead.png         |  Bin 0 -> 23647 bytes
 .../dialog/dialog-commandlink-no-masthead.png      |  Bin 0 -> 22567 bytes
 .../dialog/dialog-confirmation-masthead.png        |  Bin 0 -> 11992 bytes
 .../dialog/dialog-confirmation-no-masthead.png     |  Bin 0 -> 11166 bytes
 .../controlsfx/dialog/dialog-error-masthead.png    |  Bin 0 -> 11897 bytes
 .../controlsfx/dialog/dialog-error-no-masthead.png |  Bin 0 -> 8426 bytes
 .../dialog/dialog-exception-expanded-masthead.png  |  Bin 0 -> 22389 bytes
 .../dialog-exception-expanded-no-masthead.png      |  Bin 0 -> 21746 bytes
 .../dialog/dialog-exception-masthead.png           |  Bin 0 -> 10754 bytes
 .../dialog/dialog-exception-new-window.png         |  Bin 0 -> 19592 bytes
 .../dialog/dialog-exception-no-masthead.png        |  Bin 0 -> 9721 bytes
 .../org/controlsfx/dialog/dialog-font-selector.png |  Bin 0 -> 13362 bytes
 .../dialog/dialog-information-masthead.png         |  Bin 0 -> 10413 bytes
 .../dialog/dialog-information-no-masthead.png      |  Bin 0 -> 9280 bytes
 .../org/controlsfx/dialog/dialog-login-sample.png  |  Bin 0 -> 9279 bytes
 .../docs/org/controlsfx/dialog/dialog-overview.png |  Bin 0 -> 39835 bytes
 .../dialog-progress-with-progress-message.png      |  Bin 0 -> 8713 bytes
 .../docs/org/controlsfx/dialog/dialog-progress.png |  Bin 0 -> 7513 bytes
 .../dialog/dialog-style/cross-platform.png         |  Bin 0 -> 12655 bytes
 .../dialog/dialog-style/linux-native-titlebar.png  |  Bin 0 -> 18602 bytes
 .../dialog-style/linux-undecorated-dialog.png      |  Bin 0 -> 16485 bytes
 .../dialog/dialog-style/mac-native-titlebar.png    |  Bin 0 -> 41982 bytes
 .../windows-8-lightweight-cross-platform.png       |  Bin 0 -> 16045 bytes
 .../windows-8-lightweight-undecorated.png          |  Bin 0 -> 29734 bytes
 .../dialog-style/windows-8-native-titlebar.png     |  Bin 0 -> 12223 bytes
 .../dialog/dialog-text-input-masthead.png          |  Bin 0 -> 10676 bytes
 .../dialog/dialog-text-input-no-masthead.png       |  Bin 0 -> 9554 bytes
 .../controlsfx/dialog/dialog-warning-masthead.png  |  Bin 0 -> 7753 bytes
 .../dialog/dialog-warning-no-masthead.png          |  Bin 0 -> 6563 bytes
 .../docs/org/controlsfx/glyphfont/glyphFont.png    |  Bin 0 -> 8758 bytes
 .../org/controlsfx/tools/borders-etchedBorder.png  |  Bin 0 -> 1456 bytes
 .../org/controlsfx/tools/borders-lineBorder.png    |  Bin 0 -> 1400 bytes
 .../docs/org/controlsfx/tools/borders-twoLines.png |  Bin 0 -> 1766 bytes
 .../decoration/CompoundValidationDecoration.png    |  Bin 0 -> 43465 bytes
 .../GraphicValidationDecorationWithTooltip.png     |  Bin 0 -> 39240 bytes
 .../decoration/StyleClassValidationDecoration.png  |  Bin 0 -> 37455 bytes
 controlsfx/src/main/docs/overview.html             |   20 +
 .../src/main/java/impl/build/transifex/JSON.java   |  140 ++
 .../main/java/impl/build/transifex/Transifex.java  |  179 ++
 .../main/java/impl/org/controlsfx/ImplUtils.java   |  149 ++
 .../AutoCompletionTextFieldBinding.java            |  161 ++
 .../autocompletion/SuggestionProvider.java         |  199 ++
 .../controlsfx/behavior/RangeSliderBehavior.java   |  339 ++++
 .../org/controlsfx/behavior/RatingBehavior.java    |   41 +
 .../controlsfx/behavior/SnapshotViewBehavior.java  |  783 ++++++++
 .../impl/org/controlsfx/i18n/Localization.java     |  125 ++
 .../i18n/SimpleLocalizedStringProperty.java        |   58 +
 .../java/impl/org/controlsfx/i18n/Translation.java |   74 +
 .../impl/org/controlsfx/i18n/Translations.java     |  129 ++
 .../org/controlsfx/skin/AutoCompletePopup.java     |  233 +++
 .../org/controlsfx/skin/AutoCompletePopupSkin.java |  115 ++
 .../org/controlsfx/skin/BreadCrumbBarSkin.java     |  381 ++++
 .../org/controlsfx/skin/CheckComboBoxSkin.java     |  193 ++
 .../org/controlsfx/skin/CustomTextFieldSkin.java   |  156 ++
 .../impl/org/controlsfx/skin/DecorationPane.java   |  122 ++
 .../controlsfx/skin/ExpandableTableRowSkin.java    |  109 ++
 .../impl/org/controlsfx/skin/GridCellSkin.java     |   43 +
 .../java/impl/org/controlsfx/skin/GridRow.java     |  104 ++
 .../java/impl/org/controlsfx/skin/GridRowSkin.java |  179 ++
 .../impl/org/controlsfx/skin/GridViewSkin.java     |  225 +++
 .../org/controlsfx/skin/HiddenSidesPaneSkin.java   |  336 ++++
 .../org/controlsfx/skin/HyperlinkLabelSkin.java    |  156 ++
 .../impl/org/controlsfx/skin/InfoOverlaySkin.java  |  248 +++
 .../org/controlsfx/skin/ListSelectionViewSkin.java |  474 +++++
 .../impl/org/controlsfx/skin/MaskerPaneSkin.java   |   79 +
 .../org/controlsfx/skin/MasterDetailPaneSkin.java  |  473 +++++
 .../impl/org/controlsfx/skin/NotificationBar.java  |  309 ++++
 .../org/controlsfx/skin/NotificationPaneSkin.java  |  201 +++
 .../org/controlsfx/skin/PlusMinusSliderSkin.java   |  160 ++
 .../java/impl/org/controlsfx/skin/PopOverSkin.java |  717 ++++++++
 .../org/controlsfx/skin/PropertySheetSkin.java     |  350 ++++
 .../impl/org/controlsfx/skin/RangeSliderSkin.java  |  580 ++++++
 .../java/impl/org/controlsfx/skin/RatingSkin.java  |  329 ++++
 .../org/controlsfx/skin/SegmentedButtonSkin.java   |  117 ++
 .../impl/org/controlsfx/skin/SnapshotViewSkin.java |  566 ++++++
 .../impl/org/controlsfx/skin/StatusBarSkin.java    |  100 ++
 .../org/controlsfx/skin/TaskProgressViewSkin.java  |  167 ++
 .../impl/org/controlsfx/skin/ToggleSwitchSkin.java |  242 +++
 .../impl/org/controlsfx/spreadsheet/CellView.java  |  651 +++++++
 .../org/controlsfx/spreadsheet/CellViewSkin.java   |  224 +++
 .../controlsfx/spreadsheet/FocusModelListener.java |  142 ++
 .../org/controlsfx/spreadsheet/GridCellEditor.java |  270 +++
 .../impl/org/controlsfx/spreadsheet/GridRow.java   |  161 ++
 .../org/controlsfx/spreadsheet/GridRowSkin.java    |  615 +++++++
 .../controlsfx/spreadsheet/GridViewBehavior.java   |  509 ++++++
 .../org/controlsfx/spreadsheet/GridViewSkin.java   | 1137 ++++++++++++
 .../controlsfx/spreadsheet/GridVirtualFlow.java    |  426 +++++
 .../controlsfx/spreadsheet/HorizontalHeader.java   |  349 ++++
 .../spreadsheet/HorizontalHeaderColumn.java        |  149 ++
 .../controlsfx/spreadsheet/HorizontalPicker.java   |  152 ++
 .../controlsfx/spreadsheet/RectangleSelection.java |  436 +++++
 .../spreadsheet/SelectedCellsMapTemp.java          |  205 +++
 .../spreadsheet/SpreadsheetGridView.java           |   72 +
 .../controlsfx/spreadsheet/SpreadsheetHandle.java  |   44 +
 .../spreadsheet/TableViewSpanSelectionModel.java   |  912 ++++++++++
 .../org/controlsfx/spreadsheet/VerticalHeader.java |  675 +++++++
 .../impl/org/controlsfx/table/ColumnFilter.java    |  308 ++++
 .../impl/org/controlsfx/table/DupeCounter.java     |   52 +
 .../impl/org/controlsfx/table/FilterPanel.java     |  275 +++
 .../impl/org/controlsfx/table/FilterValue.java     |   68 +
 .../java/impl/org/controlsfx/tools/MathTools.java  |   96 +
 .../tools/PrefixSelectionCustomizer.java           |  221 +++
 .../tools/rectangle/CoordinatePosition.java        |   84 +
 .../tools/rectangle/CoordinatePositions.java       |  222 +++
 .../org/controlsfx/tools/rectangle/Edge2D.java     |  249 +++
 .../controlsfx/tools/rectangle/Rectangles2D.java   |  796 ++++++++
 .../AbstractBeginEndCheckingChangeStrategy.java    |  149 ++
 .../change/AbstractFixedEdgeChangeStrategy.java    |  137 ++
 .../change/AbstractFixedPointChangeStrategy.java   |  139 ++
 .../AbstractPreviousRectangleChangeStrategy.java   |   77 +
 .../AbstractRatioRespectingChangeStrategy.java     |   86 +
 .../tools/rectangle/change/MoveChangeStrategy.java |  166 ++
 .../tools/rectangle/change/NewChangeStrategy.java  |   82 +
 .../change/Rectangle2DChangeStrategy.java          |   73 +
 .../rectangle/change/ToEastChangeStrategy.java     |  104 ++
 .../rectangle/change/ToNorthChangeStrategy.java    |  104 ++
 .../change/ToNortheastChangeStrategy.java          |   80 +
 .../change/ToNorthwestChangeStrategy.java          |   80 +
 .../rectangle/change/ToSouthChangeStrategy.java    |  104 ++
 .../change/ToSoutheastChangeStrategy.java          |   80 +
 .../change/ToSouthwestChangeStrategy.java          |   82 +
 .../rectangle/change/ToWestChangeStrategy.java     |  104 ++
 .../org/controlsfx/version/VersionChecker.java     |  222 +++
 .../java/org/controlsfx/control/BreadCrumbBar.java |  343 ++++
 .../controlsfx/control/CheckBitSetModelBase.java   |  315 ++++
 .../java/org/controlsfx/control/CheckComboBox.java |  297 +++
 .../java/org/controlsfx/control/CheckListView.java |  264 +++
 .../java/org/controlsfx/control/CheckModel.java    |   66 +
 .../java/org/controlsfx/control/CheckTreeView.java |  327 ++++
 .../org/controlsfx/control/ControlsFXControl.java  |   63 +
 .../main/java/org/controlsfx/control/GridCell.java |  125 ++
 .../main/java/org/controlsfx/control/GridView.java |  560 ++++++
 .../org/controlsfx/control/HiddenSidesPane.java    |  426 +++++
 .../org/controlsfx/control/HyperlinkLabel.java     |  211 +++
 .../org/controlsfx/control/IndexedCheckModel.java  |   68 +
 .../java/org/controlsfx/control/InfoOverlay.java   |  220 +++
 .../org/controlsfx/control/ListSelectionView.java  |  370 ++++
 .../java/org/controlsfx/control/MaskerPane.java    |  117 ++
 .../org/controlsfx/control/MasterDetailPane.java   |  386 ++++
 .../org/controlsfx/control/NotificationPane.java   |  630 +++++++
 .../java/org/controlsfx/control/Notifications.java |  645 +++++++
 .../org/controlsfx/control/PlusMinusSlider.java    |  345 ++++
 .../main/java/org/controlsfx/control/PopOver.java  | 1011 +++++++++++
 .../control/PrefixSelectionChoiceBox.java          |   77 +
 .../control/PrefixSelectionComboBox.java           |   81 +
 .../java/org/controlsfx/control/PropertySheet.java |  452 +++++
 .../java/org/controlsfx/control/RangeSlider.java   | 1102 ++++++++++++
 .../main/java/org/controlsfx/control/Rating.java   |  317 ++++
 .../org/controlsfx/control/SegmentedButton.java    |  241 +++
 .../java/org/controlsfx/control/SnapshotView.java  | 1733 ++++++++++++++++++
 .../java/org/controlsfx/control/StatusBar.java     |  211 +++
 .../org/controlsfx/control/TaskProgressView.java   |  158 ++
 .../java/org/controlsfx/control/ToggleSwitch.java  |  171 ++
 .../java/org/controlsfx/control/action/Action.java |  430 +++++
 .../org/controlsfx/control/action/ActionCheck.java |   21 +
 .../org/controlsfx/control/action/ActionGroup.java |  167 ++
 .../org/controlsfx/control/action/ActionMap.java   |  224 +++
 .../org/controlsfx/control/action/ActionProxy.java |  138 ++
 .../org/controlsfx/control/action/ActionUtils.java |  890 +++++++++
 .../controlsfx/control/action/AnnotatedAction.java |  115 ++
 .../control/action/AnnotatedActionFactory.java     |   46 +
 .../control/action/AnnotatedCheckAction.java       |   19 +
 .../control/action/DefaultActionFactory.java       |  114 ++
 .../controlsfx/control/action/package-info.java    |    7 +
 .../org/controlsfx/control/cell/ColorGridCell.java |   82 +
 .../org/controlsfx/control/cell/ImageGridCell.java |   85 +
 .../controlsfx/control/cell/MediaImageCell.java    |  106 ++
 .../org/controlsfx/control/cell/package-info.java  |    9 +
 .../controlsfx/control/decoration/Decoration.java  |   92 +
 .../controlsfx/control/decoration/Decorator.java   |  241 +++
 .../control/decoration/GraphicDecoration.java      |  193 ++
 .../control/decoration/StyleClassDecoration.java   |   81 +
 .../control/decoration/package-info.java           |    5 +
 .../java/org/controlsfx/control/package-info.java  |    5 +
 .../org/controlsfx/control/spreadsheet/Grid.java   |  200 +++
 .../controlsfx/control/spreadsheet/GridBase.java   |  414 +++++
 .../controlsfx/control/spreadsheet/GridChange.java |  126 ++
 .../org/controlsfx/control/spreadsheet/Picker.java |   93 +
 .../control/spreadsheet/SpreadsheetCell.java       |  333 ++++
 .../control/spreadsheet/SpreadsheetCellBase.java   |  643 +++++++
 .../control/spreadsheet/SpreadsheetCellEditor.java |  926 ++++++++++
 .../control/spreadsheet/SpreadsheetCellType.java   |  858 +++++++++
 .../control/spreadsheet/SpreadsheetColumn.java     |  372 ++++
 .../control/spreadsheet/SpreadsheetView.java       | 1897 ++++++++++++++++++++
 .../spreadsheet/SpreadsheetViewSelectionModel.java |  237 +++
 .../spreadsheet/StringConverterWithFormat.java     |   77 +
 .../control/spreadsheet/package-info.java          |    5 +
 .../org/controlsfx/control/table/TableFilter.java  |  225 +++
 .../control/table/TableRowExpanderColumn.java      |  325 ++++
 .../controlsfx/control/table/TableViewUtils.java   |  138 ++
 .../control/table/model/JavaFXTableModel.java      |   47 +
 .../control/table/model/JavaFXTableModels.java     |  100 ++
 .../control/table/model/TableModelRow.java         |   60 +
 .../control/table/model/TableModelTableView.java   |   66 +
 .../table/model/TableModelValueFactory.java        |   55 +
 .../control/textfield/AutoCompletionBinding.java   |  558 ++++++
 .../control/textfield/CustomPasswordField.java     |  171 ++
 .../control/textfield/CustomTextField.java         |  178 ++
 .../controlsfx/control/textfield/TextFields.java   |  190 ++
 .../controlsfx/control/textfield/package-info.java |    4 +
 .../org/controlsfx/dialog/CommandLinksDialog.java  |  298 +++
 .../java/org/controlsfx/dialog/DialogUtils.java    |   61 +
 .../org/controlsfx/dialog/ExceptionDialog.java     |   84 +
 .../org/controlsfx/dialog/FontSelectorDialog.java  |  367 ++++
 .../java/org/controlsfx/dialog/LoginDialog.java    |  152 ++
 .../java/org/controlsfx/dialog/ProgressDialog.java |  215 +++
 .../main/java/org/controlsfx/dialog/Wizard.java    |  737 ++++++++
 .../java/org/controlsfx/dialog/WizardPane.java     |   64 +
 .../java/org/controlsfx/dialog/package-info.java   |    5 +
 .../java/org/controlsfx/glyphfont/FontAwesome.java |  723 ++++++++
 .../main/java/org/controlsfx/glyphfont/Glyph.java  |  350 ++++
 .../java/org/controlsfx/glyphfont/GlyphFont.java   |  245 +++
 .../controlsfx/glyphfont/GlyphFontRegistry.java    |  127 ++
 .../org/controlsfx/glyphfont/INamedCharacter.java  |   44 +
 .../org/controlsfx/glyphfont/package-info.java     |    5 +
 .../java/org/controlsfx/property/BeanProperty.java |  208 +++
 .../org/controlsfx/property/BeanPropertyUtils.java |   96 +
 .../property/editor/AbstractObjectField.java       |   91 +
 .../property/editor/AbstractPropertyEditor.java    |  139 ++
 .../editor/DefaultPropertyEditorFactory.java       |  115 ++
 .../org/controlsfx/property/editor/Editors.java    |  232 +++
 .../controlsfx/property/editor/NumericField.java   |  150 ++
 .../controlsfx/property/editor/PropertyEditor.java |   57 +
 .../controlsfx/property/editor/package-info.java   |    5 +
 .../java/org/controlsfx/property/package-info.java |    5 +
 .../main/java/org/controlsfx/tools/Borders.java    |  807 +++++++++
 .../java/org/controlsfx/tools/Duplicatable.java    |   42 +
 .../main/java/org/controlsfx/tools/Platform.java   |   84 +
 .../main/java/org/controlsfx/tools/SVGLoader.java  |  227 +++
 .../src/main/java/org/controlsfx/tools/Utils.java  |  113 ++
 .../java/org/controlsfx/tools/ValueExtractor.java  |  170 ++
 .../java/org/controlsfx/tools/package-info.java    |    4 +
 .../java/org/controlsfx/validation/Severity.java   |   37 +
 .../validation/SimpleValidationMessage.java        |   95 +
 .../controlsfx/validation/ValidationMessage.java   |   91 +
 .../controlsfx/validation/ValidationResult.java    |  276 +++
 .../controlsfx/validation/ValidationSupport.java   |  329 ++++
 .../java/org/controlsfx/validation/Validator.java  |  158 ++
 .../decoration/AbstractValidationDecoration.java   |  105 ++
 .../decoration/CompoundValidationDecoration.java   |   93 +
 .../decoration/GraphicValidationDecoration.java    |  127 ++
 .../decoration/StyleClassValidationDecoration.java |   80 +
 .../decoration/ValidationDecoration.java           |   59 +
 .../validation/decoration/package-info.java        |    4 +
 .../org/controlsfx/validation/package-info.java    |    5 +
 .../services/org.controlsfx.glyphfont.GlyphFont    |    1 +
 .../src/main/resources/controlsfx.properties       |   73 +
 .../control/validation/decoration-error.png        |  Bin 0 -> 3158 bytes
 .../control/validation/decoration-warning.png      |  Bin 0 -> 3120 bytes
 .../control/validation/required-indicator.png      |  Bin 0 -> 2955 bytes
 .../resources/impl/org/controlsfx/table/filter.png |  Bin 0 -> 4700 bytes
 .../impl/org/controlsfx/table/no_filter.png        |  Bin 0 -> 7021 bytes
 .../impl/org/controlsfx/table/tablefilter.css      |    7 +
 .../org/controlsfx/control/breadcrumbbar.css       |    3 +
 .../resources/org/controlsfx/control/collapse.png  |  Bin 0 -> 912 bytes
 .../resources/org/controlsfx/control/expand.png    |  Bin 0 -> 933 bytes
 .../org/controlsfx/control/format-indent-more.png  |  Bin 0 -> 532 bytes
 .../control/format-line-spacing-triple.png         |  Bin 0 -> 499 bytes
 .../resources/org/controlsfx/control/gridview.css  |    7 +
 .../org/controlsfx/control/info-overlay.css        |   15 +
 .../org/controlsfx/control/listselectionview.css   |   13 +
 .../org/controlsfx/control/maskerpane.css          |   17 +
 .../org/controlsfx/control/masterdetailpane.css    |   14 +
 .../org/controlsfx/control/notificationpane.css    |  103 ++
 .../org/controlsfx/control/notificationpopup.css   |   79 +
 .../org/controlsfx/control/open-editor.png         |  Bin 0 -> 238 bytes
 .../org/controlsfx/control/plusminusslider.css     |   57 +
 .../resources/org/controlsfx/control/popover.css   |   36 +
 .../org/controlsfx/control/propertysheet.css       |   15 +
 .../org/controlsfx/control/rangeslider.css         |   86 +
 .../resources/org/controlsfx/control/rating.css    |   15 +
 .../org/controlsfx/control/segmentedbutton.css     |   79 +
 .../org/controlsfx/control/selected-star.png       |  Bin 0 -> 2118 bytes
 .../org/controlsfx/control/snapshot-view.css       |    4 +
 .../org/controlsfx/control/spreadsheet/comment.png |  Bin 0 -> 673 bytes
 .../control/spreadsheet/copySpreadsheetView.png    |  Bin 0 -> 518 bytes
 .../control/spreadsheet/pasteSpreadsheetView.png   |  Bin 0 -> 639 bytes
 .../org/controlsfx/control/spreadsheet/picker.png  |  Bin 0 -> 382 bytes
 .../control/spreadsheet/pinSpreadsheetView.png     |  Bin 0 -> 525 bytes
 .../controlsfx/control/spreadsheet/spreadsheet.css |  150 ++
 .../resources/org/controlsfx/control/statusbar.css |    6 +
 .../org/controlsfx/control/taskprogressview.css    |   53 +
 .../control/textfield/autocompletion.css           |   31 +
 .../control/textfield/customtextfield.css          |   89 +
 .../org/controlsfx/control/toggleswitch.css        |   51 +
 .../org/controlsfx/control/unselected-star.png     |  Bin 0 -> 2252 bytes
 .../org/controlsfx/dialog/arrow-green-right.png    |  Bin 0 -> 3526 bytes
 .../org/controlsfx/dialog/commandlink.css          |   71 +
 .../org/controlsfx/dialog/dialog-confirm.png       |  Bin 0 -> 3901 bytes
 .../org/controlsfx/dialog/dialog-error.png         |  Bin 0 -> 2561 bytes
 .../org/controlsfx/dialog/dialog-information.png   |  Bin 0 -> 3594 bytes
 .../org/controlsfx/dialog/dialog-warning.png       |  Bin 0 -> 2443 bytes
 .../resources/org/controlsfx/dialog/dialogs.css    |   13 +
 .../org/controlsfx/dialog/fewer-details.png        |  Bin 0 -> 933 bytes
 .../resources/org/controlsfx/dialog/license.txt    |    7 +
 .../main/resources/org/controlsfx/dialog/lock.png  |  Bin 0 -> 242 bytes
 .../org/controlsfx/dialog/more-details.png         |  Bin 0 -> 912 bytes
 .../main/resources/org/controlsfx/dialog/user.png  |  Bin 0 -> 420 bytes
 .../org/controlsfx/dialog/wizard-page.png          |  Bin 0 -> 5830 bytes
 .../resources/org/controlsfx/dialog/wizard.css     |    3 +
 .../org/controlsfx/glyphfont/glyphfont.css         |   16 +
 .../org/controlsfx/control/CheckTreeViewTest.java  |   82 +
 .../control/spreadsheet/GridBaseTest.java          |  232 +++
 .../control/spreadsheet/JavaFXThreadingRule.java   |  116 ++
 .../control/spreadsheet/SpreadsheetViewTest.java   |  401 +++++
 doRelease.bat                                      |   76 +
 fxsampler/.classpath                               |    7 +
 fxsampler/.hgignore                                |   11 +
 fxsampler/.project                                 |   19 +
 fxsampler/build.gradle                             |   46 +
 fxsampler/src/main/java/fxsampler/FXSampler.java   |  466 +++++
 .../java/fxsampler/FXSamplerConfiguration.java     |    5 +
 .../src/main/java/fxsampler/FXSamplerProject.java  |   24 +
 fxsampler/src/main/java/fxsampler/Sample.java      |   93 +
 fxsampler/src/main/java/fxsampler/SampleBase.java  |  145 ++
 .../src/main/java/fxsampler/model/EmptySample.java |   60 +
 .../src/main/java/fxsampler/model/Project.java     |   84 +
 .../src/main/java/fxsampler/model/SampleTree.java  |  141 ++
 .../src/main/java/fxsampler/model/WelcomePage.java |   56 +
 .../main/java/fxsampler/util/SampleScanner.java    |  280 +++
 .../src/main/resources/fxsampler/fxsampler.css     |   39 +
 .../main/resources/fxsampler/util/CssTemplate.html |   26 +
 .../fxsampler/util/SourceCodeTemplate.html         |   26 +
 gradle/wrapper/gradle-wrapper.jar                  |  Bin 0 -> 46707 bytes
 gradle/wrapper/gradle-wrapper.properties           |    6 +
 gradlew                                            |  164 ++
 gradlew.bat                                        |   90 +
 javafx.plugin                                      |   36 +
 license.txt                                        |   26 +
 mavenPublish.gradle                                |  118 ++
 readme.md                                          |   29 +
 settings.gradle                                    |    1 +
 482 files changed, 65384 insertions(+)

diff --git a/.classpath b/.classpath
new file mode 100644
index 0000000..60ac6f9
--- /dev/null
+++ b/.classpath
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+	<classpathentry exported="true" kind="con" path="org.springsource.ide.eclipse.gradle.classpathcontainer"/>
+	<classpathentry kind="con" path="org.springsource.ide.eclipse.gradle.dsld.classpathcontainer"/>
+	<classpathentry kind="con" path="GROOVY_DSL_SUPPORT"/>
+	<classpathentry kind="con" path="GROOVY_SUPPORT"/>
+	<classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/.hg_archival.txt b/.hg_archival.txt
new file mode 100644
index 0000000..31129c4
--- /dev/null
+++ b/.hg_archival.txt
@@ -0,0 +1,4 @@
+repo: 2201b18d26a2d604cc7ef5c5067b1725b2e573d7
+node: 580696f836e3bfc7b9b6789305045cb363a054fd
+branch: default
+tag: 8.40.12
diff --git a/.hgeol b/.hgeol
new file mode 100644
index 0000000..4e22577
--- /dev/null
+++ b/.hgeol
@@ -0,0 +1,2 @@
+[repository]
+native = CRLF
\ No newline at end of file
diff --git a/.hgignore b/.hgignore
new file mode 100644
index 0000000..fd1082e
--- /dev/null
+++ b/.hgignore
@@ -0,0 +1,18 @@
+syntax: glob
+*.class
+*.iml
+.DS_Store
+.nb-gradle-properties
+controlsfx/build/*
+controlsfx-samples/build/*
+fxsampler/build/*
+.idea/*
+.gradle/*
+.nb-gradle/*
+bin/*
+doc/*
+.settings/*
+build/*
+lib/*
+controlsfx/classes/*
+gradle.properties
diff --git a/.hgtags b/.hgtags
new file mode 100644
index 0000000..35e7776
--- /dev/null
+++ b/.hgtags
@@ -0,0 +1,18 @@
+75093ba3c5b190cf6e32028abe8956481cc3f70f 8.0.0-dev-preview-1
+4126a1a38b741b380036a9426dbea6a6d8542123 8.0.0-dev-preview-2
+229a8fdc71d72bea191283a010ae2dbbeb85dc9a 8.0.0
+72de760aca86ae3ae1b06ef49f3662c54a5feb35 8.0.1
+f24c2e539df07853fc9d482de2650bd7a5a74fab 8.0.2-dev-preview-1
+1167c609e548a902718da94ec3639ed8c2b10a36 8.0.2-dev-preview-2
+
+49fbd100695b2208c70462d8afd01287fced2d52 8.0.2
+f804169e6636ba7e5d10b940024f9e9e0e31f210 8.0.4
+8e5e0b9b4cde512f8958e626cdc7533ba172684f 8.0.3
+0ba5652c7ff882647e8f169a8a58e277be5652e6 8.0.5
+6ce54c6bf82c3eb91426c2557bdd9612bcb96e48 8.0.6
+371bca0e905536ca6742d181fdade07258260659 8.0.6_20
+086f829f8156899d93dbbfcc8e1ad655e4a95064 8.20.7
+47d19341dd3cc108195efda6073ba056c9490672 8.20.8
+c04b084e1b202a3b7174a0147171225c9852892f 8.40.9
+48fad0295886030b0732ee9ea648f49e9c267681 8.40.10
+2c5a9d8ce3aaf4dfc121a29d4641acd1a1254d76 8.40.11
diff --git a/.project b/.project
new file mode 100644
index 0000000..b338c48
--- /dev/null
+++ b/.project
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>controlsfx</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.springsource.ide.eclipse.gradle.core.nature</nature>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+		<nature>org.eclipse.jdt.groovy.core.groovyNature</nature>
+	</natures>
+	<filteredResources>
+		<filter>
+			<id>1408918714062</id>
+			<name></name>
+			<type>26</type>
+			<matcher>
+				<id>org.eclipse.ui.ide.orFilterMatcher</id>
+				<arguments>
+					<matcher>
+						<id>org.eclipse.ui.ide.multiFilter</id>
+						<arguments>1.0-projectRelativePath-equals-true-false-controlsfx-samples</arguments>
+					</matcher>
+					<matcher>
+						<id>org.eclipse.ui.ide.multiFilter</id>
+						<arguments>1.0-projectRelativePath-equals-true-false-controlsfx</arguments>
+					</matcher>
+					<matcher>
+						<id>org.eclipse.ui.ide.multiFilter</id>
+						<arguments>1.0-projectRelativePath-equals-true-false-fxsampler</arguments>
+					</matcher>
+				</arguments>
+			</matcher>
+		</filter>
+	</filteredResources>
+</projectDescription>
diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml
new file mode 100644
index 0000000..37f2124
--- /dev/null
+++ b/bitbucket-pipelines.yml
@@ -0,0 +1,12 @@
+# This is a sample build configuration for Gradle.
+# Only use spaces to indent your .yml configuration.
+# -----
+# You can specify a custom docker image from Dockerhub as your build environment.
+image: cogniteev/oracle-java:java8
+
+pipelines:
+  default:
+    - step:
+        script: # Modify the commands below to build your repository.
+          - chmod +x gradlew
+          - ./gradlew build -x test
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..f6b41f1
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,70 @@
+subprojects {
+
+    apply plugin: 'java'
+    apply plugin: 'eclipse'
+    apply plugin: 'idea'
+    apply plugin: 'osgi'
+    apply plugin: 'maven'
+    apply from  : '../mavenPublish.gradle'
+
+    Properties cfg = new Properties()
+    cfg.load(new FileInputStream("$rootDir/controlsfx-build.properties"))
+	
+    ext {
+      artifact_suffix       = cfg.artifact_suffix
+      specification_title   = cfg.controlsfx_specification_title  //'Java 8u20'
+      specification_version = cfg.controlsfx_specification_version //'8.20.7'
+      controlsfx_name       = 'controlsfx'
+      fxsampler_name        = 'fxsampler'
+      fxsampler_version     = cfg.fxsampler_specification_version + artifact_suffix
+      fxsampler_mainClass   = 'fxsampler.FXSampler'
+    }
+
+
+    group = 'org.controlsfx'
+    version = specification_version + artifact_suffix
+
+    sourceCompatibility = '1.8'
+    targetCompatibility = '1.8'
+
+    repositories {
+       mavenCentral()
+    }
+
+    dependencies {
+       testCompile 'junit:junit:[4,)'
+    }
+
+    test {
+       testLogging {
+        // Show that tests are run in the command-line output
+           events 'started', 'passed'
+       }
+    }
+
+    task wrapper(type: Wrapper) {
+        gradleVersion = '2.0'
+    }
+
+    compileJava {  
+        options.encoding = "UTF-8"
+    }
+
+    task sourceJar(type: Jar) {
+        from sourceSets.main.java
+        from sourceSets.main.resources
+        classifier = 'sources'
+    }
+
+    task javadocJar(type: Jar) {
+        dependsOn javadoc
+        from javadoc.destinationDir
+        classifier = 'javadoc'
+    }
+
+    artifacts {
+        archives sourceJar
+        archives javadocJar
+    }
+
+}
diff --git a/controlsfx-build.properties b/controlsfx-build.properties
new file mode 100644
index 0000000..0864d97
--- /dev/null
+++ b/controlsfx-build.properties
@@ -0,0 +1,12 @@
+controlsfx_group=org.controlsfx
+
+# The name of Java version required by ControlsFX.
+controlsfx_specification_title=Java Version 8 Update 40
+
+# The minimum required JavaFX version for ControlsFX.
+# Set it to String obtained by VersionInfo.getVersion() method.
+controlsfx_specification_version=8.40.12
+
+fxsampler_specification_version=1.0.9
+
+artifact_suffix=-SNAPSHOT
diff --git a/controlsfx-samples/.classpath b/controlsfx-samples/.classpath
new file mode 100644
index 0000000..1948596
--- /dev/null
+++ b/controlsfx-samples/.classpath
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="src" path="src/deploy/package"/>
+	<classpathentry kind="src" path="src/main/java"/>
+	<classpathentry kind="src" path="src/main/resources"/>
+	<classpathentry exported="true" kind="src" path="/controlsfx-controlsfx"/>
+	<classpathentry exported="true" kind="src" path="/fxsampler"/>
+	<classpathentry exported="true" kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+	<classpathentry exported="true" kind="con" path="org.springsource.ide.eclipse.gradle.classpathcontainer"/>
+	<classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/controlsfx-samples/.project b/controlsfx-samples/.project
new file mode 100644
index 0000000..9919449
--- /dev/null
+++ b/controlsfx-samples/.project
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>controlsfx-samples</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.springsource.ide.eclipse.gradle.core.nature</nature>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+		<nature>org.eclipse.jdt.groovy.core.groovyNature</nature>
+	</natures>
+</projectDescription>
diff --git a/controlsfx-samples/build.gradle b/controlsfx-samples/build.gradle
new file mode 100644
index 0000000..fe97d28
--- /dev/null
+++ b/controlsfx-samples/build.gradle
@@ -0,0 +1,41 @@
+import org.apache.tools.ant.filters.*
+apply plugin: 'application'
+
+applicationName = 'FXSampler'
+mainClassName = fxsampler_mainClass
+
+configurations {
+    jdk
+}
+
+sourceSets {
+    main {
+        compileClasspath += configurations.jdk
+    }
+}
+
+dependencies {
+
+    compile project(':controlsfx')
+    compile project(':fxsampler')
+
+    compile group + ':' + fxsampler_name +':'  + fxsampler_version
+    compile group + ':' + controlsfx_name + ':' + version
+
+    try {
+        jdk files(jfxrtJar)
+    } catch (MissingPropertyException pne) {
+        // javafx plugin will provide in this case
+    }
+}
+
+jar {
+    manifest {
+        attributes  'Implementation-Title':     'ControlsFX-Samples',
+                    'Implementation-Version':   project.version,
+                    'Class-Path':               configurations.compile.collect { it.getName() }.join(' '),
+                    'Main-Class':               fxsampler_mainClass
+    }
+    from sourceSets.main.allJava
+}
+
diff --git a/controlsfx-samples/eclipse/ControlsFX Samples.launch b/controlsfx-samples/eclipse/ControlsFX Samples.launch
new file mode 100644
index 0000000..b2fffa0
--- /dev/null
+++ b/controlsfx-samples/eclipse/ControlsFX Samples.launch	
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<launchConfiguration type="org.eclipse.jdt.launching.localJavaApplication">
+<stringAttribute key="bad_container_name" value="\controlsfx-samples\ecl"/>
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
+<listEntry value="/controlsfx-samples"/>
+</listAttribute>
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
+<listEntry value="4"/>
+</listAttribute>
+<listAttribute key="org.eclipse.debug.ui.favoriteGroups">
+<listEntry value="org.eclipse.debug.ui.launchGroup.run"/>
+</listAttribute>
+<listAttribute key="org.eclipse.jdt.launching.CLASSPATH">
+<listEntry value="<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<runtimeClasspathEntry containerPath="org.springsource.ide.eclipse.gradle.classpathcontainer" javaProject="controlsfx-controlsfx" path="2" type="4"/>
"/>
+<listEntry value="<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<runtimeClasspathEntry containerPath="GROOVY_DSL_SUPPORT" javaProject="controlsfx-controlsfx" path="2" type="4"/>
"/>
+<listEntry value="<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<runtimeClasspathEntry containerPath="org.eclipse.jdt.launching.JRE_CONTAINER" javaProject="controlsfx-samples" path="1" type="4"/>
"/>
+<listEntry value="<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<runtimeClasspathEntry containerPath="org.springsource.ide.eclipse.gradle.classpathcontainer" javaProject="controlsfx" path="2" type="4"/>
"/>
+<listEntry value="<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<runtimeClasspathEntry path="3" projectName="controlsfx-controlsfx" type="1"/>
"/>
+<listEntry value="<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<runtimeClasspathEntry id="org.eclipse.jdt.launching.classpathentry.defaultClasspath">
<memento exportedEntriesOnly="false" project="controlsfx-samples"/>
</runtimeClasspathEntry>
"/>
+</listAttribute>
+<booleanAttribute key="org.eclipse.jdt.launching.DEFAULT_CLASSPATH" value="false"/>
+<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="fxsampler.FXSampler"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="controlsfx-samples"/>
+</launchConfiguration>
diff --git a/controlsfx-samples/src/deploy/package/shortcut-128.png b/controlsfx-samples/src/deploy/package/shortcut-128.png
new file mode 100644
index 0000000..d886de4
Binary files /dev/null and b/controlsfx-samples/src/deploy/package/shortcut-128.png differ
diff --git a/controlsfx-samples/src/deploy/package/volume-128.png b/controlsfx-samples/src/deploy/package/volume-128.png
new file mode 100644
index 0000000..d886de4
Binary files /dev/null and b/controlsfx-samples/src/deploy/package/volume-128.png differ
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/ControlsFXSample.java b/controlsfx-samples/src/main/java/org/controlsfx/ControlsFXSample.java
new file mode 100644
index 0000000..02c1aa0
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/ControlsFXSample.java
@@ -0,0 +1,62 @@
+package org.controlsfx;
+
+import java.io.InputStream;
+import java.util.jar.Attributes;
+import java.util.jar.Manifest;
+
+import org.controlsfx.samples.Utils;
+
+import fxsampler.SampleBase;
+
+public abstract class ControlsFXSample extends SampleBase {
+
+	private static final ProjectInfo projectInfo = new ProjectInfo();
+	
+	@Override
+	public String getProjectName() {
+		return "ControlsFX";
+	}
+
+	@Override
+	public String getProjectVersion() {
+		return projectInfo.getVersion();
+	}
+	
+	@Override public String getSampleSourceURL() {
+	    return Utils.JAVADOC_BASE + "samples-src/" + getClass().getName().replace('.','/') + ".java";
+	}
+	
+	@Override
+	public String getControlStylesheetURL() {
+		return null;
+	}
+	
+	private static class ProjectInfo {
+		
+		private String version;
+		
+		
+		public ProjectInfo() {
+			
+			InputStream s = getClass().getClassLoader().getResourceAsStream(
+					"META-INF/MANIFEST.MF");
+			
+			try {
+				Manifest manifest = new Manifest(s);
+				Attributes attr = manifest.getMainAttributes();
+				version = attr.getValue("Implementation-Version");
+			} catch (Throwable e) {
+				System.out.println("Unable to load project version for ControlsFX "
+				        + "samples project as the manifest file can't be read "
+				        + "or the Implementation-Version attribute is unavailable.");
+				version = "";
+			}
+		}
+		
+		public String getVersion() {
+			return version;
+		}
+	}
+
+}
+
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/ControlsFXSampler.java b/controlsfx-samples/src/main/java/org/controlsfx/ControlsFXSampler.java
new file mode 100644
index 0000000..5af951a
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/ControlsFXSampler.java
@@ -0,0 +1,51 @@
+package org.controlsfx;
+
+import javafx.scene.control.Label;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.StackPane;
+import javafx.scene.layout.VBox;
+import fxsampler.FXSampler;
+import fxsampler.FXSamplerProject;
+import fxsampler.model.WelcomePage;
+
+public class ControlsFXSampler implements FXSamplerProject {
+
+    /** {@inheritDoc} */
+    @Override public String getProjectName() {
+        return "ControlsFX";
+    }
+    
+    /** {@inheritDoc} */
+    @Override public String getSampleBasePackage() {
+        return "org.controlsfx.samples";
+    }
+    
+    /** {@inheritDoc} */
+    @Override public WelcomePage getWelcomePage() {
+        VBox vBox = new VBox();
+        ImageView imgView = new ImageView();
+        imgView.setStyle("-fx-image: url('org/controlsfx/samples/ControlsFX.png');");
+        StackPane pane = new StackPane();
+        pane.setPrefHeight(207);
+        pane.setStyle("-fx-background-image: url('org/controlsfx/samples/bar.png');"
+                + "-fx-background-repeat: repeat-x;");
+        pane.getChildren().add(imgView);
+        Label label = new Label();
+        label.setWrapText(true);
+        StringBuilder desc = new StringBuilder();
+        desc.append("ControlsFX is an open source project for JavaFX that aims ");
+        desc.append("to provide really high quality UI controls and other tools to ");
+        desc.append("complement the core JavaFX distribution.");
+        desc.append("\n\n");
+        desc.append("Explore the available UI controls by clicking on the options to the left.");
+        label.setText(desc.toString());
+        label.setStyle("-fx-font-size: 1.5em; -fx-padding: 20 0 0 5;");
+        vBox.getChildren().addAll(pane, label);
+        WelcomePage wPage = new WelcomePage("Welcome to Controls FX!", vBox);
+        return wPage;
+    }
+    
+    public static void main(String[] args) {
+        FXSampler.main(args);
+    }
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloBorders.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloBorders.java
new file mode 100644
index 0000000..c009068
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloBorders.java
@@ -0,0 +1,214 @@
+/**
+ * Copyright (c) 2013, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples;
+
+import javafx.scene.Node;
+import javafx.scene.control.Button;
+import javafx.scene.layout.Pane;
+import javafx.stage.Stage;
+
+import org.controlsfx.ControlsFXSample;
+import org.controlsfx.tools.Borders;
+
+public class HelloBorders extends ControlsFXSample {
+    
+    public static void main(String[] args) {
+        launch(args);
+    }
+    
+    @Override public String getSampleName() {
+        return "Borders";
+    }
+    
+    @Override public String getJavaDocURL() {
+        return Utils.JAVADOC_BASE + "org/controlsfx/tools/Borders.html";
+    }
+    
+    @Override public boolean isVisible() {
+        return true;
+    }
+    
+    @Override public Node getPanel(Stage stage) {
+        Pane root = new Pane();
+        
+        Button button = new Button("Hello World!");
+        Node wrappedButton = Borders.wrap(button)
+//                                                 .emptyBorder()
+//                                                     .padding(20)
+//                                                     .build()
+                                                 .lineBorder()
+                                                     .title("Line")
+//                                                     .color(Color.GREEN)
+//                                                     .thickness(1, 0, 0, 0)
+                                                     .thickness(1)
+                                                     .radius(0, 5, 5, 0)
+                                                     .build()
+                                                 .emptyBorder()
+                                                     .padding(20)
+                                                     .build()
+                                                 .etchedBorder()
+                                                     .title("Etched")
+                                                     .build()
+                                                 .emptyBorder()
+                                                     .padding(20)
+                                                     .build()
+                                                 .build();
+        
+        
+        root.getChildren().add(wrappedButton);
+        
+        return root;
+    }
+    
+    @Override public String getSampleDescription() {
+        return "A utility class that allows you to wrap JavaFX Nodes with a border, "
+                + "in a way somewhat analogous to the Swing BorderFactory (although "
+                + "with less options as a lot of what the Swing BorderFactory offers "
+                + "resulted in ugly borders!)."
+                + "\n\nThe Borders class provides a fluent API for specifying the "
+                + "properties of each border. It is possible to create multiple "
+                + "borders around a Node simply by continuing to call additional "
+                + "methods before you call the final build() method. To use the "
+                + "Borders class, you simply call wrap(Node), passing in the Node "
+                + "you wish to wrap the border(s) around.";
+    }
+    
+    @Override
+    public Node getControlPanel() {
+
+        return null;
+        
+        // TODO
+//        // current borders
+//        ListView<String> currentBordersListView = new ListView<String>();
+//        currentBordersListView.setPrefHeight(100);
+//        Node borderedListView = Borders.wrap(currentBordersListView)
+//                .etchedBorder()
+//                    .title("Current Borders:")
+//                    .build()
+//                .emptyBorder()
+//                    .padding(5)
+//                    .build()
+//                .build();
+//        
+//        
+//        // add new borders
+//        Tab lineBorderTab = buildLineBorderTab();
+//        
+//        Tab etchedBorderTab = new Tab("Etched");
+//        Tab emptyBorderTab = new Tab("Empty");
+//        
+//        TabPane tabPane = new TabPane();
+//        tabPane.getStyleClass().add(TabPane.STYLE_CLASS_FLOATING);
+//        tabPane.setTabClosingPolicy(TabClosingPolicy.UNAVAILABLE);
+//        tabPane.setMaxHeight(Double.MAX_VALUE);
+//        tabPane.getTabs().addAll(lineBorderTab, etchedBorderTab, emptyBorderTab);
+//        Region borderedTabPane = (Region) Borders.wrap(tabPane)
+//            .lineBorder()
+//                .thickness(1, 0, 0, 0)
+//                .title("Add a Border:")
+//                .build()
+////            .emptyBorder()
+////                .padding(5, 0, 0, 0)
+////                .build()
+//            .build();
+//        borderedTabPane.setMaxHeight(Double.MAX_VALUE);
+//        
+//        VBox vbox = new VBox(borderedListView, borderedTabPane);
+//        vbox.setMaxHeight(Double.MAX_VALUE);
+//        vbox.setSpacing(10);
+//        
+//        StackPane stackPane = new StackPane(tabPane);
+//        stackPane.setMaxHeight(Double.MAX_VALUE);
+//        
+//        return stackPane;
+    }
+
+//    private Tab buildLineBorderTab() {
+//        PropertySheet lineBorderPropertySheet = new PropertySheet();
+//        lineBorderPropertySheet.setModeSwitcherVisible(false);
+//        lineBorderPropertySheet.setSearchBoxVisible(false);
+//        lineBorderPropertySheet.setMaxHeight(Double.MAX_VALUE);
+//        
+//        Item titleProperty = new BorderItem("Title");
+//        lineBorderPropertySheet.getItems().add(titleProperty);
+//        
+//        Item colorProperty = new BorderItem("Color", Color.class);
+//        lineBorderPropertySheet.getItems().add(colorProperty);
+//        
+//        Item radiusProperty = new BorderItem("Radius", Number.class);
+//        lineBorderPropertySheet.getItems().add(radiusProperty);
+//        
+//        Item thicknessProperty = new BorderItem("Thickness", Number.class);
+//        lineBorderPropertySheet.getItems().add(thicknessProperty);
+//        
+//        Tab tab = new Tab("Line");
+//        tab.setContent(lineBorderPropertySheet);
+//        return tab;
+//    }
+    
+//    private static class BorderItem implements Item {
+//        private final String displayText;
+//        private final Class<?> type;
+//        
+//        public BorderItem(String displayText) {
+//            this(displayText, null);
+//        }
+//        
+//        public BorderItem(String displayText, Class<?> type) {
+//            this.displayText = displayText;
+//            this.type = type == null ? String.class : type;
+//        }
+//
+//        @Override public Class<?> getType() {
+//            return type;
+//        }
+//
+//        @Override public String getCategory() {
+//            return null;
+//        }
+//
+//        @Override public String getName() {
+//            return displayText;
+//        }
+//
+//        @Override public String getDescription() {
+//            return null;
+//        }
+//
+//        @Override public Object getValue() {
+//            // TODO Auto-generated method stub
+//            return null;
+//        }
+//
+//        @Override public void setValue(Object value) {
+//            // TODO Auto-generated method stub
+//            
+//        }
+//        
+//    }
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloDecorator.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloDecorator.java
new file mode 100644
index 0000000..641bbf1
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloDecorator.java
@@ -0,0 +1,168 @@
+/**
+ * Copyright (c) 2013, 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples;
+
+import static org.controlsfx.control.decoration.Decorator.addDecoration;
+import static org.controlsfx.control.decoration.Decorator.removeAllDecorations;
+import javafx.application.Platform;
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.FXCollections;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.Node;
+import javafx.scene.control.ChoiceBox;
+import javafx.scene.control.Label;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.control.TextField;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.VBox;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Rectangle;
+import javafx.stage.Stage;
+
+import org.controlsfx.ControlsFXSample;
+import org.controlsfx.control.decoration.GraphicDecoration;
+import org.controlsfx.control.decoration.StyleClassDecoration;
+
+public class HelloDecorator extends ControlsFXSample {
+    
+    private final TextField field = new TextField();
+    
+    @Override public String getSampleName() {
+        return "Decorations";
+    }
+    
+    @Override public String getJavaDocURL() {
+        return Utils.JAVADOC_BASE + "org/controlsfx/control/decoration/Decorator.html";
+    }
+    
+    @Override public Node getPanel(final Stage stage) {
+        VBox root = new VBox(10);
+        root.setPadding(new Insets(10, 10, 10, 10));
+        root.setMaxHeight(Double.MAX_VALUE);
+        root.getChildren().addAll(field);
+        
+        // for the sake of this sample we have to install a custom css file to
+        // style the sample - but we can't do this until the scene is set on the
+        // pane
+        root.sceneProperty().addListener(new InvalidationListener() {
+            @Override public void invalidated(Observable o) {
+                if (root.getScene() != null) {
+                    Platform.runLater(() -> {
+                        root.getScene().getStylesheets().add(HelloDecorator.class.getResource("decorations.css").toExternalForm());
+                    });
+                }
+            }
+        });
+        
+        
+        ScrollPane scrollPane = new ScrollPane(root);
+        return scrollPane;
+    }
+    
+    @Override
+    public Node getControlPanel() {
+        GridPane grid = new GridPane();
+        grid.setVgap(10);
+        grid.setHgap(10);
+        grid.setPadding(new Insets(30, 30, 0, 30));
+        
+        int row = 0;
+        
+        // --- show decorations
+        Label showDecorationsLabel = new Label("Show decorations: ");
+        showDecorationsLabel.getStyleClass().add("property");
+        grid.add(showDecorationsLabel, 0, row);
+        ChoiceBox<String> decorationTypeBox = new ChoiceBox<>(FXCollections.observableArrayList("None", "Node", "CSS", "Node + CSS", "Image"));
+        decorationTypeBox.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<String>() {
+            @Override public void changed(ObservableValue<? extends String> o, String old, String newItem) {
+                removeAllDecorations(field);
+                switch (newItem) {
+                    case "None": break;
+                    case "Node": {
+                        addDecoration(field, new GraphicDecoration(createDecoratorNode(Color.RED),Pos.TOP_LEFT));
+                        addDecoration(field, new GraphicDecoration(createDecoratorNode(Color.RED),Pos.TOP_CENTER));
+                        addDecoration(field, new GraphicDecoration(createDecoratorNode(Color.RED),Pos.TOP_RIGHT));
+                        addDecoration(field, new GraphicDecoration(createDecoratorNode(Color.GREEN),Pos.CENTER_LEFT));
+                        addDecoration(field, new GraphicDecoration(createDecoratorNode(Color.GREEN),Pos.CENTER));
+                        addDecoration(field, new GraphicDecoration(createDecoratorNode(Color.GREEN),Pos.CENTER_RIGHT));
+                        addDecoration(field, new GraphicDecoration(createDecoratorNode(Color.BLUE),Pos.BOTTOM_LEFT));
+                        addDecoration(field, new GraphicDecoration(createDecoratorNode(Color.BLUE),Pos.BOTTOM_CENTER));
+                        addDecoration(field, new GraphicDecoration(createDecoratorNode(Color.BLUE),Pos.BOTTOM_RIGHT));
+                        break;
+                    }
+                    case "CSS": {
+                        addDecoration(field, new StyleClassDecoration("warning"));
+                        break;
+                    }
+                    case "Node + CSS": {
+                        addDecoration(field, new GraphicDecoration(createDecoratorNode(Color.GREEN),Pos.CENTER_RIGHT));
+                        addDecoration(field, new StyleClassDecoration("success"));
+                        break;
+                    }
+                    case "Image": {
+                        addDecoration(field, new GraphicDecoration(createImageNode(),Pos.BOTTOM_LEFT));
+                        break;
+                    }
+                }
+            }
+        });
+        grid.add(decorationTypeBox, 1, row++);
+        
+//        // --- Toggle text field visibility
+//        Label showTextFieldLabel = new Label("TextField visible: ");
+//        showTextFieldLabel.getStyleClass().add("property");
+//        grid.add(showTextFieldLabel, 0, row);
+//        ToggleButton fieldVisibleBtn = new ToggleButton("Press");
+//        fieldVisibleBtn.setSelected(true);
+//        field.visibleProperty().bindBidirectional(fieldVisibleBtn.selectedProperty());
+//        grid.add(fieldVisibleBtn, 1, row++);
+        
+        return grid;
+    }
+    
+    private Node createDecoratorNode(Color color) {
+    	Rectangle d = new Rectangle(7, 7);
+        d.setFill(color);
+        return d;
+    }
+    
+    private Node createImageNode() {
+        Image image = new Image("/org/controlsfx/samples/security-low.png");
+        ImageView imageView = new ImageView(image);
+        return imageView;
+    }
+    
+    public static void main(String[] args) {
+        launch(args);
+    } 
+}
\ No newline at end of file
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloGlyphFont.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloGlyphFont.java
new file mode 100644
index 0000000..ccf79f6
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloGlyphFont.java
@@ -0,0 +1,170 @@
+/**
+ * Copyright (c) 2013, 2014 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples;
+
+import javafx.collections.FXCollections;
+import javafx.geometry.Insets;
+import javafx.scene.Node;
+import javafx.scene.control.Button;
+import javafx.scene.control.ContentDisplay;
+import javafx.scene.control.Label;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.control.Tab;
+import javafx.scene.control.TabPane;
+import javafx.scene.control.ToolBar;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.VBox;
+import javafx.scene.paint.Color;
+import javafx.stage.Stage;
+
+import org.controlsfx.ControlsFXSample;
+import org.controlsfx.control.GridCell;
+import org.controlsfx.control.GridView;
+import org.controlsfx.glyphfont.FontAwesome;
+import org.controlsfx.glyphfont.Glyph;
+import org.controlsfx.glyphfont.GlyphFont;
+import org.controlsfx.glyphfont.GlyphFontRegistry;
+
+public class HelloGlyphFont extends ControlsFXSample {
+
+    static {
+        // Register a custom default font
+        GlyphFontRegistry.register("icomoon", HelloGlyphFont.class.getResourceAsStream("icomoon.ttf") , 16);
+    }
+
+
+    private GlyphFont fontAwesome = GlyphFontRegistry.font("FontAwesome");
+    private GlyphFont icoMoon = GlyphFontRegistry.font("icomoon");
+
+    //	private static char FAW_TRASH = '\uf014';
+    private static char FAW_GEAR  = '\uf013';
+//	private static char FAW_STAR  = '\uf005';
+
+    private static char IM_BOLD        = '\ue027';
+    private static char IM_UNDERSCORED = '\ue02b';
+    private static char IM_ITALIC      = '\ue13e';
+
+
+
+    @Override
+    public String getSampleName() {
+        return "Glyph Font";
+    }
+
+    @Override
+    public String getJavaDocURL() {
+        return Utils.JAVADOC_BASE + "org/controlsfx/glyphfont/GlyphFont.html";
+    }
+
+    @Override
+    public Node getPanel(final Stage stage) {
+
+        VBox root = new VBox(10);
+
+        root.setPadding(new Insets(10, 10, 10, 10));
+        root.setMaxHeight(Double.MAX_VALUE);
+        Label title = new Label("Using FontAwesome(CDN)");
+        root.getChildren().add(title);
+        ToolBar toolbar = new ToolBar(
+
+                // There are many ways how you can define a Glyph:
+
+                new Button("", new Glyph("FontAwesome", "TRASH_ALT")),              // Use the Glyph-class with a icon name
+                new Button("", new Glyph("FontAwesome", FontAwesome.Glyph.STAR)),   // Use the Glyph-class with a known enum value
+                new Button("", Glyph.create("FontAwesome|BUG")),                    // Use the static Glyph-class create protocol
+                new Button("", fontAwesome.create("REBEL")),                        // Use the font-instance with a name
+                new Button("", fontAwesome.create(FontAwesome.Glyph.SMILE_ALT)),    // Use the font-instance with a enum
+                new Button("", fontAwesome.create(FAW_GEAR).color(Color.RED))       // Use the font-instance with a unicode char
+        );
+        root.getChildren().add(toolbar);
+        title = new Label("Using IcoMoon (Local)");
+        root.getChildren().add(title);
+
+        Glyph effectGlyph = icoMoon.create(IM_UNDERSCORED)
+                .color(Color.BLUE)
+                .size(48)
+                .useHoverEffect();
+
+        Glyph effectGlyph2 = icoMoon.create(IM_UNDERSCORED)
+                .color(Color.BLUE)
+                .size(48)
+                .useGradientEffect().useHoverEffect();
+
+        toolbar = new ToolBar(
+
+                // Since we have a custom font without named characters,
+                // we have to use unicode character codes for the icons:
+
+                new Button("", icoMoon.create(IM_BOLD).size(16)),
+                new Button("", icoMoon.create(IM_UNDERSCORED).color(Color.GREEN).size(32)),
+                new Button("", icoMoon.create(IM_ITALIC).size(48)),
+                new Button("", effectGlyph),
+                new Button("", effectGlyph2));
+        root.getChildren().add(toolbar);
+        
+        GridPane fontDemo = new GridPane();
+        fontDemo.setHgap(5);
+        fontDemo.setVgap(5);
+        int maxColumns = 10;
+        int col = 0;
+        int row = 0;
+        
+        for ( FontAwesome.Glyph glyph:  FontAwesome.Glyph.values() ){
+        	Color randomColor = new Color( Math.random(), Math.random(), Math.random(), 1);
+        	Glyph graphic = Glyph.create( "FontAwesome|" + glyph.name()).sizeFactor(2).color(randomColor).useGradientEffect();
+        	Button button = new Button(glyph.name(), graphic);
+        	button.setContentDisplay(ContentDisplay.TOP);
+        	button.setMaxWidth(Double.MAX_VALUE);
+        	col = col % maxColumns + 1;
+        	if ( col == 1 ) row++;
+        	fontDemo.add( button, col, row);
+        	GridPane.setFillHeight(button, true);
+        	GridPane.setFillWidth(button, true);
+        }
+        
+        ScrollPane scroller = new ScrollPane(fontDemo);
+        scroller.setFitToWidth(true);
+        
+        TabPane tabs = new TabPane();
+        Tab tab = new Tab("FontAwesome Glyph Demo");
+        tab.setContent(scroller);
+        tabs.getTabs().add(tab);
+
+        
+        root.getChildren().add(tabs);
+        VBox.setVgrow(tabs, Priority.ALWAYS);
+        
+        return root;
+
+    }
+    
+
+    public static void main(String[] args) {
+        launch(args);
+    }
+}
\ No newline at end of file
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloGridView.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloGridView.java
new file mode 100644
index 0000000..968e0fe
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloGridView.java
@@ -0,0 +1,142 @@
+/**
+ * Copyright (c) 2013, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples;
+
+import java.util.Random;
+
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.event.ActionEvent;
+import javafx.scene.Node;
+import javafx.scene.control.ToolBar;
+import javafx.scene.image.Image;
+import javafx.scene.layout.VBox;
+import javafx.scene.paint.Color;
+import javafx.stage.Stage;
+import javafx.util.Callback;
+
+import org.controlsfx.ControlsFXSample;
+import org.controlsfx.control.GridCell;
+import org.controlsfx.control.GridView;
+import org.controlsfx.control.SegmentedButton;
+import org.controlsfx.control.action.Action;
+import org.controlsfx.control.action.ActionUtils;
+import org.controlsfx.control.cell.ColorGridCell;
+import org.controlsfx.control.cell.ImageGridCell;
+
+public class HelloGridView extends ControlsFXSample {
+
+    private GridView<?> myGrid;
+    private final VBox root = new VBox();
+    
+    public static void main(String[] args) {
+        launch();
+    }
+    
+    @Override public String getSampleName() {
+        return "GridView";
+    }
+    
+    @Override public String getJavaDocURL() {
+        return Utils.JAVADOC_BASE + "org/controlsfx/control/GridView.html";
+    }
+    
+    
+    @Override
+    public String getControlStylesheetURL() {
+    	return "/org/controlsfx/control/gridview.css";
+    }
+    
+    private GridView<?> getColorGrid() {
+        final ObservableList<Color> list = FXCollections.<Color>observableArrayList();
+        
+        GridView<Color> colorGrid = new GridView<>(list);
+        
+        colorGrid.setCellFactory(new Callback<GridView<Color>, GridCell<Color>>() {
+            @Override public GridCell<Color> call(GridView<Color> arg0) {
+                return new ColorGridCell();
+            }
+        });
+        Random r = new Random(System.currentTimeMillis());
+        for(int i = 0; i < 500; i++) {
+            list.add(new Color(r.nextDouble(), r.nextDouble(), r.nextDouble(), 1.0));
+        }
+        return colorGrid;
+    }
+    
+    private GridView<?> getImageGrid( final boolean preserveImageProperties ) {
+        
+        final Image image = new Image("/org/controlsfx/samples/flowers.png", 200, 0, true, true);
+        final ObservableList<Image> list = FXCollections.<Image>observableArrayList();
+        
+        GridView<Image> colorGrid = new GridView<>(list);
+        
+        colorGrid.setCellFactory(new Callback<GridView<Image>, GridCell<Image>>() {
+            @Override public GridCell<Image> call(GridView<Image> arg0) {
+                return new ImageGridCell(preserveImageProperties);
+            }
+        });
+        for(int i = 0; i < 50; i++) {
+            list.add(image);
+        }
+        return colorGrid;
+    }    
+    
+    
+    @Override public Node getPanel(Stage stage) {
+        SegmentedButton selector = ActionUtils.createSegmentedButton(
+            new ActionShowGrid("Colors", getColorGrid()),
+            new ActionShowGrid("Images", getImageGrid(false)),
+            new ActionShowGrid("Images (preserve properties)", getImageGrid(true))
+        );
+        root.getChildren().clear();
+        root.getChildren().add(new ToolBar(selector));
+        selector.getButtons().get(0).fire();
+        return root;
+    }
+    
+    class ActionShowGrid extends Action {
+
+        GridView<?> grid;
+        
+        public ActionShowGrid(String text, GridView<?> grid) {
+            super(text);
+            this.grid = grid;
+            setEventHandler(this::handleAction);
+        }
+
+        private void handleAction(ActionEvent ae) {
+            if ( myGrid != null ) {
+                root.getChildren().remove(myGrid);
+            }
+            myGrid = grid;
+            root.getChildren().add(myGrid);
+        }
+        
+    }
+    
+}
\ No newline at end of file
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloHiddenSidesPane.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloHiddenSidesPane.java
new file mode 100644
index 0000000..216b60c
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloHiddenSidesPane.java
@@ -0,0 +1,124 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples;
+
+import javafx.event.EventHandler;
+import javafx.geometry.Pos;
+import javafx.geometry.Side;
+import javafx.scene.Node;
+import javafx.scene.control.Label;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.StackPane;
+import javafx.stage.Stage;
+
+import org.controlsfx.ControlsFXSample;
+import org.controlsfx.control.HiddenSidesPane;
+
+public class HelloHiddenSidesPane extends ControlsFXSample {
+
+    @Override
+    public String getSampleName() {
+        return "Hidden Sides Pane";
+    }
+
+    @Override
+    public String getJavaDocURL() {
+        return Utils.JAVADOC_BASE
+                + "org/controlsfx/control/HiddenSidesPane.html";
+    }
+
+    @Override
+    public String getSampleDescription() {
+        return "Hidden nodes will appear when moving the mouse cursor close to the edge of the content node. "
+                + "They disappear again when the mouse cursor exits them. In this example a hidden node "
+                + "can be pinned (and unpinned) by clicking on it so that it stays visible all the time.";
+    }
+
+    @Override
+    public Node getPanel(Stage stage) {
+        StackPane stackPane = new StackPane();
+        stackPane.setStyle("-fx-padding: 30");
+
+        HiddenSidesPane pane = new HiddenSidesPane();
+
+        Label content = new Label("Content Node");
+        content.setStyle("-fx-background-color: white; -fx-border-color: black;");
+        content.setAlignment(Pos.CENTER);
+        content.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
+
+        pane.setContent(content);
+
+        SideNode top = new SideNode("Top", Side.TOP, pane);
+        top.setStyle("-fx-background-color: rgba(0,255,0,.25);");
+        pane.setTop(top);
+
+        SideNode right = new SideNode("Right", Side.RIGHT, pane);
+        right.setStyle("-fx-background-color: rgba(0,0, 255,.25);");
+        pane.setRight(right);
+
+        SideNode bottom = new SideNode("Bottom", Side.BOTTOM, pane);
+        bottom.setStyle("-fx-background-color: rgba(255,255,0,.25);");
+        pane.setBottom(bottom);
+
+        SideNode left = new SideNode("Left", Side.LEFT, pane);
+        left.setStyle("-fx-background-color: rgba(255,0,0,.25);");
+        pane.setLeft(left);
+
+        stackPane.getChildren().add(pane);
+
+        return stackPane;
+    }
+
+    class SideNode extends Label {
+
+        public SideNode(final String text, final Side side,
+                final HiddenSidesPane pane) {
+
+            super(text + " (Click to pin / unpin)");
+
+            setAlignment(Pos.CENTER);
+            setPrefSize(200, 200);
+
+            setOnMouseClicked(new EventHandler<MouseEvent>() {
+                @Override
+                public void handle(MouseEvent event) {
+                    if (pane.getPinnedSide() != null) {
+                        setText(text + " (unpinned)");
+                        pane.setPinnedSide(null);
+                    } else {
+                        setText(text + " (pinned)");
+                        pane.setPinnedSide(side);
+                    }
+                }
+            });
+        }
+    }
+
+    public static void main(String[] args) {
+        launch(args);
+    }
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloHyperlinkLabel.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloHyperlinkLabel.java
new file mode 100644
index 0000000..746bc32
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloHyperlinkLabel.java
@@ -0,0 +1,98 @@
+/**
+ * Copyright (c) 2013, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples;
+
+import javafx.beans.binding.StringBinding;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.geometry.Insets;
+import javafx.scene.Node;
+import javafx.scene.control.Hyperlink;
+import javafx.scene.control.TextField;
+import javafx.scene.layout.VBox;
+import javafx.stage.Stage;
+
+import org.controlsfx.ControlsFXSample;
+import org.controlsfx.control.HyperlinkLabel;
+
+public class HelloHyperlinkLabel extends ControlsFXSample {
+    
+    private HyperlinkLabel label;
+    
+    public static void main(String[] args) {
+        launch(args);
+    }
+    
+    @Override public String getSampleName() {
+        return "Hyperlink Label";
+    }
+    
+    @Override public String getJavaDocURL() {
+        return Utils.JAVADOC_BASE + "org/controlsfx/control/HyperlinkLabel.html";
+    }
+    
+    @Override public Node getPanel(Stage stage) {
+        VBox root = new VBox(20);
+        root.setPadding(new Insets(30, 30, 30, 30));
+        
+        final TextField textToShowField = new TextField();
+        textToShowField.setMaxWidth(Double.MAX_VALUE);
+        textToShowField.setPromptText("Type text in here to display - use [] to indicate a hyperlink - e.g. [hello]");
+        root.getChildren().add(textToShowField);
+        
+        final TextField selectedLinkField = new TextField();
+        selectedLinkField.setMaxWidth(Double.MAX_VALUE);
+        selectedLinkField.setEditable(false);
+        selectedLinkField.setPromptText("Click a link - I'll show you which one you clicked :-)");
+        root.getChildren().add(selectedLinkField);
+        
+        label = new HyperlinkLabel();
+        label.textProperty().bind(new StringBinding() {
+            {
+                bind(textToShowField.textProperty());
+            }
+            
+            @Override protected String computeValue() {
+                final String str = textToShowField.getText();
+                if (str == null || str.isEmpty()) {
+                    return "Hello [world]! I [wonder] what hyperlink [you] [will] [click]";
+                }
+                return str;
+            }
+        });
+        label.setOnAction(new EventHandler<ActionEvent>() {
+            @Override public void handle(ActionEvent event) {
+                Hyperlink link = (Hyperlink)event.getSource();
+                final String str = link == null ? "" : "You clicked on '" + link.getText() + "'";
+                selectedLinkField.setText(str);
+            }
+        });
+        root.getChildren().add(label);
+        
+        return root;
+    }
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloInfoOverlay.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloInfoOverlay.java
new file mode 100644
index 0000000..a4290aa
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloInfoOverlay.java
@@ -0,0 +1,115 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples;
+
+import javafx.geometry.Insets;
+import javafx.scene.Node;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.Label;
+import javafx.scene.control.Slider;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.StackPane;
+import javafx.stage.Stage;
+
+import org.controlsfx.ControlsFXSample;
+import org.controlsfx.control.InfoOverlay;
+
+public class HelloInfoOverlay extends ControlsFXSample {
+
+    private InfoOverlay infoOverlay;
+    private Slider fitHeightSlider = new Slider(250, 800, 400);
+    
+    public static void main(String[] args) {
+        launch(args);
+    }
+    
+    @Override public String getSampleName() {
+        return "InfoOverlay";
+    }
+    
+    @Override public String getJavaDocURL() {
+        return Utils.JAVADOC_BASE + "org/controlsfx/control/InfoOverlay.html";
+    }
+    
+    
+    @Override
+    public String getControlStylesheetURL() {
+    	return "/org/controlsfx/control/info-overlay.css";
+    }
+    
+    @Override public String getSampleDescription() {
+        return "A simple UI control that allows for an information popup to be "
+                + "displayed over a node to describe it in further detail. In "
+                + "some ways, it can be thought of as a always visible tooltip "
+                + "(although by default it is collapsed so only the first line is "
+                + "shown - clicking on it will expand it to show all text).";
+    }
+    
+    @Override public Node getPanel(Stage stage) {
+        String imageUrl = getClass().getResource("duke_wave.png").toExternalForm();
+        ImageView image = new ImageView(imageUrl);
+        image.fitHeightProperty().bind(fitHeightSlider.valueProperty());
+        image.setPreserveRatio(true);
+
+        String info = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " +
+                "Nam tortor felis, pulvinar in scelerisque cursus, pulvinar at ante. " +
+                "Nulla consequat congue lectus in sodales.";
+
+        infoOverlay = new InfoOverlay(image, info);
+        
+        StackPane stackPane = new StackPane(infoOverlay);
+        
+        return stackPane;
+    }
+    
+    @Override public Node getControlPanel() {
+        final GridPane grid = new GridPane();
+        grid.setHgap(5);
+        grid.setVgap(5);
+        grid.setPadding(new Insets(5, 5, 5, 5));
+        
+        int row = 0;
+
+        // fit height
+        Label imageHeightLabel = new Label("Image Size: ");
+        imageHeightLabel.getStyleClass().add("property");
+        grid.add(imageHeightLabel, 0, row);
+        grid.add(fitHeightSlider, 1, row++);
+        
+        // show on hover
+        Label showOnHoverLabel = new Label("Show overlay on hover: ");
+        showOnHoverLabel.getStyleClass().add("property");
+        grid.add(showOnHoverLabel, 0, row);
+        CheckBox showOnHoverChk = new CheckBox();
+        showOnHoverChk.setSelected(true);
+        infoOverlay.showOnHoverProperty().bind(showOnHoverChk.selectedProperty());
+        grid.add(showOnHoverChk, 1, row++);
+        
+        return grid;
+    }
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloListSelectionView.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloListSelectionView.java
new file mode 100644
index 0000000..a59f497
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloListSelectionView.java
@@ -0,0 +1,131 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples;
+
+import javafx.collections.FXCollections;
+import javafx.geometry.Insets;
+import javafx.geometry.Orientation;
+import javafx.geometry.Pos;
+import javafx.scene.Node;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.ChoiceBox;
+import javafx.scene.control.ListCell;
+import javafx.scene.control.Tooltip;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.VBox;
+import javafx.scene.text.Font;
+import javafx.scene.text.FontPosture;
+import javafx.scene.text.FontWeight;
+import javafx.stage.Stage;
+
+import org.controlsfx.ControlsFXSample;
+import org.controlsfx.control.ListSelectionView;
+
+public class HelloListSelectionView extends ControlsFXSample {
+
+    private ListSelectionView<String> view;
+
+    @Override
+    public String getSampleName() {
+        return "List Selection View";
+    }
+
+    @Override
+    public Node getPanel(Stage stage) {
+        view = new ListSelectionView<>();
+        view.getSourceItems()
+                .addAll("Katja", "Dirk", "Philip", "Jule", "Armin");
+
+        GridPane pane = new GridPane();
+        pane.add(view, 0, 0);
+        pane.setAlignment(Pos.CENTER);
+
+        return pane;
+    }
+
+    @Override
+    public Node getControlPanel() {
+        VBox root = new VBox(20);
+        root.setPadding(new Insets(30, 30, 30, 30));
+        
+        CheckBox useCellFactory = new CheckBox("Use cell factory");
+        useCellFactory.setOnAction(evt -> {
+            if (useCellFactory.isSelected()) {
+                view.setCellFactory(listView -> {
+                    ListCell<String> cell = new ListCell<String>() {
+                        @Override
+                        public void updateItem(String item, boolean empty) {
+                            super.updateItem(item, empty);
+
+                            if (empty) {
+                                setText(null);
+                                setGraphic(null);
+                            } else {
+                                setText(item == null ? "null" : item);
+                                setGraphic(null);
+                            }
+                        }
+                    };
+                    cell.setFont(Font.font("Arial", FontWeight.BOLD,
+                            FontPosture.ITALIC, 18));
+                    return cell;
+                });
+            } else {
+                view.setCellFactory(null);
+            }
+        });
+
+        ChoiceBox<Orientation> orientation = new ChoiceBox<>(FXCollections.observableArrayList(Orientation.values()));
+        orientation.setTooltip(new Tooltip("The orientation of ListSelectionView"));
+        
+        orientation.getSelectionModel().select(Orientation.HORIZONTAL);
+        view.orientationProperty().bind(orientation.getSelectionModel().selectedItemProperty());
+        
+        root.getChildren().addAll(useCellFactory, orientation);
+        
+        return root;
+    }
+
+    @Override
+    public String getSampleDescription() {
+        return "A control used to let the user select multiple values from a "
+                + "list of available values. Selected values are moved into a "
+                + "second list that is showing the current selection. Items can "
+                + "be moved by double clicking on them or by first selecting "
+                + "them and then pressing one of the buttons in the center.";
+    }
+
+    @Override
+    public String getJavaDocURL() {
+        return Utils.JAVADOC_BASE
+                + "org/controlsfx/control/ListSelectionView.html";
+    }
+
+    public static void main(String[] args) {
+        launch(args);
+    }
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloMaskerPane.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloMaskerPane.java
new file mode 100644
index 0000000..f9d7498
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloMaskerPane.java
@@ -0,0 +1,130 @@
+/**
+ * Copyright (c) 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.controlsfx.samples;
+
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.Node;
+import javafx.scene.control.*;
+import javafx.scene.layout.*;
+import javafx.stage.Stage;
+import org.controlsfx.ControlsFXSample;
+import org.controlsfx.control.MaskerPane;
+import org.controlsfx.control.SegmentedButton;
+
+public class HelloMaskerPane extends ControlsFXSample {
+
+    private MaskerPane masker = new MaskerPane();
+
+    public static void main(String[] args) { launch(args); }
+
+    @Override public String getSampleName() { return "MaskerPane"; } //$NON-NLS-1$
+
+    @Override public String getJavaDocURL() { return Utils.JAVADOC_BASE + "org/controlsfx/control/MaskerPane.html"; } //$NON-NLS-1$
+
+    @Override public String getControlStylesheetURL() { return "/org/controlsfx/control/maskerpane.css"; } //$NON-NLS-1$
+
+    @Override
+    public Node getPanel(Stage stage) {
+        BorderPane root = new BorderPane();
+        root.setPadding(new Insets(10));
+
+        root.setTop(getSwitcher());
+        root.setCenter(getBody());
+
+        return root;
+    }
+
+    private Node getSwitcher() {
+        Label label = new Label("Masker Visibility:");
+        ToggleButton onButton = new ToggleButton("On");
+        onButton.setSelected(false);
+        ToggleButton offButton = new ToggleButton("Off");
+        offButton.setSelected(false);
+
+        SegmentedButton segmentedButton = new SegmentedButton(onButton, offButton);
+        segmentedButton.getToggleGroup().selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
+            if (newValue.equals(onButton)) {
+                masker.setVisible(true);
+            } else if (newValue.equals(offButton)) {
+                masker.setVisible(false);
+            }
+        });
+
+        Label progressLabel = new Label("Progress:");
+        Slider progressSlider = new Slider(0, 1, 0);
+        progressSlider.valueProperty().bindBidirectional(masker.progressProperty());
+
+        Label progressVisible = new Label("Progress Visible:");
+        CheckBox checkBox = new CheckBox();
+        checkBox.selectedProperty().bindBidirectional(masker.progressVisibleProperty());
+
+        Label text = new Label("Text:");
+        TextField textField = new TextField();
+        textField.textProperty().bindBidirectional(masker.textProperty());
+
+        HBox hBox = new HBox(label, segmentedButton, progressLabel, progressSlider, progressVisible, checkBox, text, textField);
+        hBox.setSpacing(10);
+        hBox.setAlignment(Pos.CENTER_LEFT);
+
+        return hBox;
+    }
+
+    private Node getBody() {
+        StackPane body = new StackPane();
+        body.setPadding(new Insets(10, 0, 0, 0));
+
+        VBox vBox = new VBox();
+        vBox.setSpacing(10);
+        vBox.setPadding(new Insets(20));
+
+        Label description = new Label("This is an example form where you may wish to block user interaction for a short period, possibly after " +
+                "selecting 'Submit'. This allows for client-server communication to finish, and the user is notified that the application is not " +
+                "frozen.\n\n" +
+                "While the masker is visible, the form elements underneath are effectively disabled (that is, the user cannot interact with them).");
+        description.setWrapText(true);
+        description.setPadding(new Insets(0, 0, 20, 0));
+
+        HBox hBox = new HBox(new Label("Username:"), new TextField());
+        hBox.setSpacing(10);
+        hBox.setAlignment(Pos.CENTER_LEFT);
+
+        HBox hBox1 = new HBox(new Label("Password:"), new TextField());
+        hBox1.setSpacing(10);
+        hBox1.setAlignment(Pos.CENTER_LEFT);
+
+        HBox hBox2 = new HBox(new Button("Submit"));
+        hBox2.setAlignment(Pos.CENTER_LEFT);
+
+        vBox.getChildren().addAll(description, hBox, hBox1, hBox2);
+
+        body.getChildren().addAll(vBox, masker);
+
+        return body;
+    }
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloMasterDetailPane.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloMasterDetailPane.java
new file mode 100644
index 0000000..f8542c9
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloMasterDetailPane.java
@@ -0,0 +1,119 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples;
+
+import javafx.application.Application;
+import javafx.geometry.Insets;
+import javafx.geometry.Side;
+import javafx.scene.Node;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.Label;
+import javafx.scene.layout.GridPane;
+import javafx.stage.Stage;
+
+import org.controlsfx.ControlsFXSample;
+import org.controlsfx.control.MasterDetailPane;
+
+public class HelloMasterDetailPane extends ControlsFXSample {
+
+    @Override
+    public String getJavaDocURL() {
+        return Utils.JAVADOC_BASE
+                + "org/controlsfx/control/MasterDetailPane.html";
+    }
+    
+    
+    @Override
+    public String getControlStylesheetURL() {
+    	return "/org/controlsfx/control/masterdetailpane.css";
+    }
+
+    private MasterDetailPane masterDetailPane;
+
+    @Override
+    public Node getPanel(Stage stage) {
+        masterDetailPane = new MasterDetailPane(Side.BOTTOM);
+        masterDetailPane.setShowDetailNode(true);
+        
+        return masterDetailPane;
+    }
+
+    @Override
+    public Node getControlPanel() {
+        GridPane grid = new GridPane();
+        grid.setVgap(10);
+        grid.setHgap(10);
+        grid.setPadding(new Insets(30, 30, 0, 30));
+
+        int row = 0;
+
+        // show details
+        Label lblShowDetail = new Label("Show details: ");
+        lblShowDetail.getStyleClass().add("property");
+        grid.add(lblShowDetail, 0, row);
+        CheckBox chkShowDetails = new CheckBox();
+        grid.add(chkShowDetails, 1, row++);
+        chkShowDetails.selectedProperty().bindBidirectional(masterDetailPane.showDetailNodeProperty());
+        
+     
+        // animated
+        Label lblAnimated = new Label("Animated: ");
+        lblAnimated.getStyleClass().add("property");
+        grid.add(lblAnimated, 0, row);
+        CheckBox chkAnimated = new CheckBox();
+        grid.add(chkAnimated, 1, row++);
+        chkAnimated.selectedProperty().bindBidirectional(masterDetailPane.animatedProperty());
+
+
+        // side
+        Label lblSide = new Label("Side: ");
+        lblSide.getStyleClass().add("property");
+        grid.add(lblSide, 0, row);
+        ComboBox<Side> positionBox = new ComboBox<>();
+        positionBox.getItems().addAll(Side.values());
+        grid.add(positionBox, 1, row++);
+        positionBox.setValue(masterDetailPane.getDetailSide());
+        masterDetailPane.detailSideProperty().bind(positionBox.valueProperty());
+
+        return grid;
+    }
+
+    @Override
+    public String getSampleDescription() {
+        return "A control used to display a master node and a detail node. The detail can be shown / hidden at the top, the bottom, to the left or to the right.";
+    }
+
+    public static void main(String[] args) {
+        Application.launch(args);
+    }
+
+    @Override
+    public String getSampleName() {
+        return "Master Detail Pane";
+    }
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloNotificationPane.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloNotificationPane.java
new file mode 100644
index 0000000..26607fa
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloNotificationPane.java
@@ -0,0 +1,143 @@
+/**
+ * Copyright (c) 2013, 2014 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples;
+
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.geometry.Insets;
+import javafx.scene.Node;
+import javafx.scene.control.Button;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.TextField;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.VBox;
+import javafx.stage.Stage;
+
+import org.controlsfx.ControlsFXSample;
+import org.controlsfx.control.NotificationPane;
+import org.controlsfx.control.action.Action;
+
+public class HelloNotificationPane extends ControlsFXSample {
+    
+    private NotificationPane notificationPane;
+    private CheckBox cbUseDarkTheme;
+    private CheckBox cbHideCloseBtn;
+    private TextField textField;
+    
+    public static void main(String[] args) {
+        launch(args);
+    }
+    
+    @Override public String getSampleName() {
+        return "Notification Pane";
+    }
+    
+    @Override public String getJavaDocURL() {
+        return Utils.JAVADOC_BASE + "org/controlsfx/control/NotificationPane.html";
+    }
+    
+    
+    @Override
+    public String getControlStylesheetURL() {
+    	return "/org/controlsfx/control/notificationpane.css";
+    }
+    
+    @Override public Node getPanel(Stage stage) {
+        notificationPane = new NotificationPane();
+        
+        String imagePath = HelloNotificationPane.class.getResource("notification-pane-warning.png").toExternalForm();
+        ImageView image = new ImageView(imagePath);
+        notificationPane.setGraphic(image);
+        
+        notificationPane.getActions().addAll(new Action("Sync", ae -> {
+                // do sync
+                
+                // then hide...
+                notificationPane.hide();
+        }));
+        
+        Button showBtn = new Button("Show / Hide");
+        showBtn.setOnAction(new EventHandler<ActionEvent>() {
+            @Override public void handle(ActionEvent arg0) {
+                if (notificationPane.isShowing()) {
+                    notificationPane.hide();
+                } else {
+                    notificationPane.show();
+                }
+            }
+        });
+        
+        CheckBox cbSlideFromTop = new CheckBox("Slide from top");
+        cbSlideFromTop.setSelected(true);
+        notificationPane.showFromTopProperty().bind(cbSlideFromTop.selectedProperty());
+        
+        cbUseDarkTheme = new CheckBox("Use dark theme");
+        cbUseDarkTheme.setSelected(false);
+        cbUseDarkTheme.setOnAction(new EventHandler<ActionEvent>() {
+            @Override public void handle(ActionEvent arg0) {
+                updateBar();
+            }
+        });
+        
+        cbHideCloseBtn = new CheckBox("Hide close button");
+        cbHideCloseBtn.setSelected(false);
+        cbHideCloseBtn.setOnAction(new EventHandler<ActionEvent>() {
+            @Override public void handle(ActionEvent arg0) {
+                notificationPane.setCloseButtonVisible(!cbHideCloseBtn.isSelected());
+            }
+        });
+        
+        textField = new TextField();
+        textField.setPromptText("Type text to display and press Enter");
+        textField.setOnAction(new EventHandler<ActionEvent>() {
+            @Override public void handle(ActionEvent arg0) {
+                notificationPane.show(textField.getText());
+            }
+        });
+        
+        VBox root = new VBox(10);
+        root.setPadding(new Insets(50, 0, 0, 10));
+        root.getChildren().addAll(showBtn, cbSlideFromTop, cbUseDarkTheme, cbHideCloseBtn, textField);
+        
+        notificationPane.setContent(root);
+        updateBar();
+        
+        return notificationPane;
+    }
+    
+    private void updateBar() {
+        boolean useDarkTheme = cbUseDarkTheme.isSelected();
+        
+        if (useDarkTheme) {
+            notificationPane.setText("Hello World! Using the dark theme");
+            notificationPane.getStyleClass().add(NotificationPane.STYLE_CLASS_DARK);
+        } else {
+            notificationPane.setText("Hello World! Using the light theme");
+            notificationPane.getStyleClass().remove(NotificationPane.STYLE_CLASS_DARK);
+        }
+    }
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloNotifications.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloNotifications.java
new file mode 100644
index 0000000..df29b5a
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloNotifications.java
@@ -0,0 +1,368 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples;
+
+import java.util.Random;
+
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.Node;
+import javafx.scene.control.Button;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.ChoiceBox;
+import javafx.scene.control.Label;
+import javafx.scene.control.SelectionModel;
+import javafx.scene.control.Slider;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.Priority;
+import javafx.scene.paint.Color;
+import javafx.stage.Stage;
+import javafx.util.Callback;
+import javafx.util.Duration;
+
+import org.controlsfx.ControlsFXSample;
+import org.controlsfx.control.GridCell;
+import org.controlsfx.control.GridView;
+import org.controlsfx.control.Notifications;
+import org.controlsfx.control.cell.ColorGridCell;
+
+public class HelloNotifications extends ControlsFXSample {
+    
+    private static final Image SMALL_GRAPHIC = 
+            new Image(HelloNotificationPane.class.getResource("controlsfx-logo.png").toExternalForm());
+    
+    private Stage stage;
+    private Pane pane;
+    
+    private int count = 0;
+    
+    private CheckBox showTitleChkBox;
+    private CheckBox showCloseButtonChkBox;
+    private CheckBox darkStyleChkBox;
+    private CheckBox ownerChkBox;
+    private Slider fadeDelaySlider;
+    protected String graphicMode = "";
+    
+    public static void main(String[] args) {
+        launch(args);
+    }
+    
+    @Override public String getSampleName() {
+        return "Notifications";
+    }
+    
+    @Override public String getJavaDocURL() {
+        return Utils.JAVADOC_BASE + "org/controlsfx/control/Notifications.html";
+    }
+    
+    
+    @Override
+    public String getControlStylesheetURL() {
+    	return "/org/controlsfx/control/notificationpopup.css";
+    }
+    
+    @Override public Node getPanel(Stage stage) {
+        this.stage = stage;
+        
+        pane = new Pane() {
+            @Override protected void layoutChildren() {
+                super.layoutChildren();
+                updatePane();
+            }
+        };
+        createPaneChildren();
+        updatePane();
+        pane.setPadding(new Insets(10));
+        
+        return pane;
+    }
+    
+    private void createPaneChildren() {
+        Button topLeftBtn = new Button("Top-left\nnotification");
+        topLeftBtn.setOnAction(new EventHandler<ActionEvent>() {
+            @Override public void handle(ActionEvent e) {
+                notification(Pos.TOP_LEFT);
+            }
+        });
+        
+        Button topCenterBtn = new Button("Top-center\nnotification");
+        topCenterBtn.setOnAction(new EventHandler<ActionEvent>() {
+            @Override public void handle(ActionEvent e) {
+                notification(Pos.TOP_CENTER);
+            }
+        });
+        
+        Button topRightBtn = new Button("Top-right\nnotification");
+        topRightBtn.setOnAction(new EventHandler<ActionEvent>() {
+            @Override public void handle(ActionEvent e) {
+                notification(Pos.TOP_RIGHT);
+            }
+        });
+        
+        Button centerLeftBtn = new Button("Center-left\nNotification");
+        centerLeftBtn.setOnAction(new EventHandler<ActionEvent>() {
+            @Override public void handle(ActionEvent e) {
+                notification(Pos.CENTER_LEFT);
+            }
+        });
+        
+        Button centerBtn = new Button("Center\nnotification");
+        centerBtn.setOnAction(new EventHandler<ActionEvent>() {
+            @Override public void handle(ActionEvent e) {
+                notification(Pos.CENTER);
+            }
+        });
+        
+        Button centerRightBtn = new Button("Center-right\nNotification");
+        centerRightBtn.setOnAction(new EventHandler<ActionEvent>() {
+            @Override public void handle(ActionEvent e) {
+                notification(Pos.CENTER_RIGHT);
+            }
+        });
+        
+        Button bottomLeftBtn = new Button("Bottom-left\nNotification");
+        bottomLeftBtn.setOnAction(new EventHandler<ActionEvent>() {
+            @Override public void handle(ActionEvent e) {
+                notification(Pos.BOTTOM_LEFT);
+            }
+        });
+        
+        Button bottomCenterBtn = new Button("Bottom-center\nnotification");
+        bottomCenterBtn.setOnAction(new EventHandler<ActionEvent>() {
+            @Override public void handle(ActionEvent e) {
+                notification(Pos.BOTTOM_CENTER);
+            }
+        });
+        
+        Button bottomRightBtn = new Button("Bottom-right\nNotification");
+        bottomRightBtn.setOnAction(new EventHandler<ActionEvent>() {
+            @Override public void handle(ActionEvent e) {
+                notification(Pos.BOTTOM_RIGHT);
+            }
+        });
+        
+        pane.getChildren().addAll(topLeftBtn,    topCenterBtn,    topRightBtn,
+                                  centerLeftBtn, centerBtn,       centerRightBtn,
+                                  bottomLeftBtn, bottomCenterBtn, bottomRightBtn);
+    }
+    
+    private void updatePane() {
+        final double paneWidth = pane.getWidth();
+        final double paneHeight = pane.getHeight();
+        
+        final double halfWidth = paneWidth / 2.0;
+        final double halfHeight = paneHeight / 2.0;
+        
+        int row = 0;
+        int col = 0;
+        
+        for (Node node : pane.getChildren()) {
+            final double nodeWidth = node.prefWidth(-1);
+            final double nodeHeight = node.prefHeight(-1);
+            
+            double layoutX = col == 0 ? 0 : 
+                             col == 1 ? halfWidth - nodeWidth / 2.0 :
+                           /*col == 2*/ paneWidth - nodeWidth;
+            
+            double layoutY = row == 0 ? 0 : 
+                             row == 1 ? halfHeight - nodeHeight / 2.0 :
+                           /*row == 2*/ paneHeight - nodeHeight;
+            
+            node.setLayoutX(layoutX);
+            node.setLayoutY(layoutY);
+            
+            col++;
+            if (col == 3) {
+                row++;
+                col = 0;
+            }
+        }
+    }
+
+    @Override public String getSampleDescription() {
+        return "Unlike the NotificationPane, the Notifications class is designed to"
+                + "show popup warnings outside your application.";
+    }
+    
+    @Override public Node getControlPanel() {
+        GridPane grid = new GridPane();
+        grid.setVgap(10);
+        grid.setHgap(10);
+        grid.setPadding(new Insets(30, 30, 0, 30));
+        
+        int row = 0;
+        
+        // --- show title
+        Label showTitleLabel = new Label("Show Title: ");
+        showTitleLabel.getStyleClass().add("property");
+        grid.add(showTitleLabel, 0, row);
+        showTitleChkBox = new CheckBox();
+        showTitleChkBox.setSelected(true);
+        grid.add(showTitleChkBox, 1, row++);
+        
+        // --- show close button
+        Label showCloseButtonLabel = new Label("Show Close Button: ");
+        showCloseButtonLabel.getStyleClass().add("property");
+        grid.add(showCloseButtonLabel, 0, row);
+        showCloseButtonChkBox = new CheckBox();
+        showCloseButtonChkBox.setSelected(true);
+        grid.add(showCloseButtonChkBox, 1, row++);
+        
+        // --- dark style
+        Label darkStyleLabel = new Label("Use Dark Style: ");
+        darkStyleLabel.getStyleClass().add("property");
+        grid.add(darkStyleLabel, 0, row);
+        darkStyleChkBox = new CheckBox();
+        grid.add(darkStyleChkBox, 1, row++);
+        
+        // --- owner
+        Label owner = new Label("Set Owner: ");
+        owner.getStyleClass().add("property");
+        grid.add(owner, 0, row);
+        ownerChkBox = new CheckBox();
+        grid.add(ownerChkBox, 1, row++);
+        
+        // --- graphic
+        Label graphicLabel = new Label("Graphic Options: ");
+        graphicLabel.getStyleClass().add("property");
+        grid.add(graphicLabel, 0, row);
+        final ChoiceBox<String> graphicOptions = new ChoiceBox<>(
+                FXCollections.observableArrayList(
+                        "No graphic", 
+                        "Warning graphic",
+                        "Information graphic",
+                        "Confirm graphic",
+                        "Error graphic",
+                        "Custom graphic",
+                        "Total-replacement graphic"));
+        graphicOptions.setMaxWidth(Double.MAX_VALUE);
+        GridPane.setHgrow(graphicOptions, Priority.ALWAYS);
+        final SelectionModel<String> sm = graphicOptions.getSelectionModel();
+        sm.selectedItemProperty().addListener(new InvalidationListener() {
+            @Override public void invalidated(Observable o) {
+                graphicMode = sm.getSelectedItem();
+            }
+        });
+        sm.select(1);
+        grid.add(graphicOptions, 1, row++);
+        
+        // --- fade duration
+        Label fadeDurationLabel = new Label("Fade delay (seconds): ");
+        fadeDurationLabel.getStyleClass().add("property");
+        grid.add(fadeDurationLabel, 0, row);
+        fadeDelaySlider = new Slider(1, 20, 5);
+        fadeDelaySlider.setShowTickMarks(true);
+        fadeDelaySlider.setMaxWidth(Double.MAX_VALUE);
+        GridPane.setHgrow(fadeDelaySlider, Priority.ALWAYS);
+        grid.add(fadeDelaySlider, 1, row++);
+        
+        
+        return grid;
+    }
+    
+    private void notification(Pos pos) {
+        String text = "Hello World " + (count++) + "!";
+        
+        Node graphic = null;
+        switch (graphicMode) {
+            default:
+            case "No graphic": 
+            case "Warning graphic":
+            case "Information graphic":
+            case "Confirm graphic":
+            case "Error graphic":
+                break;
+            case "Custom graphic":
+                graphic = new ImageView(SMALL_GRAPHIC);
+                break;
+            case "Total-replacement graphic": 
+                text = null;
+                graphic = buildTotalReplacementGraphic();
+                break;
+        }
+        
+        Notifications notificationBuilder = Notifications.create()
+                .title(showTitleChkBox.isSelected() ? "Title Text" : "")
+                .text(text)
+                .graphic(graphic)
+                .hideAfter(Duration.seconds(fadeDelaySlider.getValue()))
+                .position(pos)
+                .onAction(new EventHandler<ActionEvent>() {
+                    @Override public void handle(ActionEvent arg0) {
+                        System.out.println("Notification clicked on!");
+                    }
+                });
+        
+        if(ownerChkBox.isSelected()){
+            notificationBuilder.owner(stage);
+        }
+        
+        if (! showCloseButtonChkBox.isSelected()) {
+            notificationBuilder.hideCloseButton();
+        }
+        
+        if (darkStyleChkBox.isSelected()) {
+            notificationBuilder.darkStyle();
+        }
+        
+        switch (graphicMode) {
+            case "Warning graphic":     notificationBuilder.showWarning(); break;
+            case "Information graphic": notificationBuilder.showInformation(); break;
+            case "Confirm graphic":     notificationBuilder.showConfirm(); break;
+            case "Error graphic":       notificationBuilder.showError(); break;
+            default: notificationBuilder.show(); 
+        }
+    }
+    
+    private Node buildTotalReplacementGraphic() {
+        final ObservableList<Color> list = FXCollections.<Color>observableArrayList();
+
+        GridView<Color> colorGrid = new GridView<>(list);
+        colorGrid.setPrefSize(300, 300);
+        colorGrid.setMaxSize(300, 300);
+
+        colorGrid.setCellFactory(new Callback<GridView<Color>, GridCell<Color>>() {
+            @Override public GridCell<Color> call(GridView<Color> arg0) {
+                return new ColorGridCell();
+            }
+        });
+        Random r = new Random(System.currentTimeMillis());
+        for(int i = 0; i < 500; i++) {
+            list.add(new Color(r.nextDouble(), r.nextDouble(), r.nextDouble(), 1.0));
+        }
+        return colorGrid;
+    }
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloPlusMinusSlider.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloPlusMinusSlider.java
new file mode 100644
index 0000000..92fb121
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloPlusMinusSlider.java
@@ -0,0 +1,119 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples;
+
+import javafx.application.Application;
+import javafx.event.EventHandler;
+import javafx.geometry.Orientation;
+import javafx.scene.Group;
+import javafx.scene.Node;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.Label;
+import javafx.scene.layout.VBox;
+import javafx.stage.Stage;
+
+import org.controlsfx.ControlsFXSample;
+import org.controlsfx.control.PlusMinusSlider;
+import org.controlsfx.control.PlusMinusSlider.PlusMinusEvent;
+
+public class HelloPlusMinusSlider extends ControlsFXSample {
+
+    private PlusMinusSlider plusMinusSlider = new PlusMinusSlider();
+
+    @Override
+    public Node getPanel(Stage stage) {
+        Group group = new Group();
+
+        VBox vBox = new VBox();
+        vBox.setMinWidth(500);
+        vBox.setSpacing(20);
+        vBox.setStyle("-fx-padding: 40;");
+
+        group.getChildren().add(vBox);
+
+        vBox.getChildren().add(plusMinusSlider);
+
+        final Label counterLabel = new Label();
+        vBox.getChildren().add(counterLabel);
+
+        final Label valueLabel = new Label();
+        vBox.getChildren().add(valueLabel);
+
+        plusMinusSlider.setOnValueChanged(new EventHandler<PlusMinusEvent>() {
+            long counter = 1;
+
+            @Override
+            public void handle(PlusMinusEvent event) {
+                counterLabel.setText("Event #" + counter);
+                valueLabel.setText("Value = " + event.getValue());
+                counter++;
+            }
+        });
+
+        return group;
+    }
+
+    @Override
+    public Node getControlPanel() {
+        ComboBox<Orientation> box = new ComboBox<>();
+        box.getItems().addAll(Orientation.values());
+        box.setValue(plusMinusSlider.getOrientation());
+        plusMinusSlider.orientationProperty().bind(box.valueProperty());
+        return box;
+    }
+
+    public static void main(String[] args) {
+        Application.launch(args);
+    }
+
+    @Override
+    public String getSampleName() {
+        return "PlusMinusSlider";
+    }
+
+    @Override
+    public String getJavaDocURL() {
+        return Utils.JAVADOC_BASE
+                + "org/controlsfx/control/PlusMinusSlider.html";
+    }
+    
+    
+    @Override
+    public String getControlStylesheetURL() {
+    	return "/org/controlsfx/control/plusminusslider.css";
+    }
+
+    @Override
+    public String getSampleDescription() {
+        return "A slider-like control used to fire value events "
+                + "with values in the range of -1 and +1. "
+                + "The slider thumb jumps back to the zero "
+                + "position when the user lets go of the mouse. "
+                + "Possible use case: scrolling through a lot of data "
+                + "at different speeds.";
+    }
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloPopOver.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloPopOver.java
new file mode 100644
index 0000000..9f1af48
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloPopOver.java
@@ -0,0 +1,368 @@
+/**
+ * Copyright (c) 2013, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples;
+
+import java.text.NumberFormat;
+
+import javafx.application.Application;
+import javafx.beans.binding.Bindings;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.event.EventHandler;
+import javafx.geometry.HPos;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.Group;
+import javafx.scene.Node;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.Label;
+import javafx.scene.control.Slider;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.input.ScrollEvent;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.StackPane;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Circle;
+import javafx.scene.shape.Line;
+import javafx.scene.shape.Rectangle;
+import javafx.scene.text.TextAlignment;
+import javafx.stage.Stage;
+import javafx.util.Duration;
+
+import org.controlsfx.ControlsFXSample;
+import org.controlsfx.control.PopOver;
+import org.controlsfx.control.PopOver.ArrowLocation;
+
+public class HelloPopOver extends ControlsFXSample {
+
+    private PopOver popOver;
+
+    private DoubleProperty masterArrowSize;
+    private DoubleProperty masterArrowIndent;
+    private DoubleProperty masterCornerRadius;
+    private ObjectProperty<ArrowLocation> masterArrowLocation;
+    private BooleanProperty masterHeaderAlwaysVisible;
+
+    private double targetX;
+    private double targetY;
+
+    private CheckBox detached;
+
+    private CheckBox detachable;
+
+    private CheckBox autoPosition;
+
+    private CheckBox headerAlwaysVisible;
+
+    private CheckBox animated;
+
+    private Circle circle;
+
+    private Line line1;
+
+    private Line line2;
+
+    @Override
+    public Node getPanel(Stage stage) {
+        Group group = new Group();
+
+        final Rectangle rect = new Rectangle();
+        rect.setStroke(Color.BLACK);
+        rect.setFill(Color.CORAL);
+        rect.setWidth(220);
+        rect.setHeight(220);
+        group.getChildren().add(rect);
+
+        circle = new Circle();
+        circle.setStroke(Color.BLACK);
+        circle.setFill(Color.WHITE);
+        group.getChildren().add(circle);
+
+        line1 = new Line();
+        line1.setFill(Color.BLACK);
+        group.getChildren().add(line1);
+
+        line2 = new Line();
+        line2.setFill(Color.BLACK);
+        group.getChildren().add(line2);
+
+        /*
+         * These master properties are only needed for this demo as we want to
+         * make sure that the settings done by the user via the demo controls
+         * will be applied to all popovers that are currently visible (this
+         * includes the detached ones).
+         */
+        masterArrowSize = new SimpleDoubleProperty(12);
+        masterArrowIndent = new SimpleDoubleProperty(12);
+        masterCornerRadius = new SimpleDoubleProperty(6);
+        masterArrowLocation = new SimpleObjectProperty<>(ArrowLocation.LEFT_TOP);
+        masterHeaderAlwaysVisible = new SimpleBooleanProperty(false);
+
+        rect.setOnScroll(new EventHandler<ScrollEvent>() {
+            @Override
+            public void handle(ScrollEvent evt) {
+                double delta = evt.getDeltaY();
+                rect.setWidth(Math.max(100,
+                        Math.min(500, rect.getWidth() + delta)));
+                rect.setHeight(Math.max(100,
+                        Math.min(500, rect.getHeight() + delta)));
+            }
+        });
+
+        rect.setOnMouseClicked(new EventHandler<MouseEvent>() {
+            @Override
+            public void handle(MouseEvent evt) {
+                if (popOver != null && !popOver.isDetached()) {
+                    popOver.hide();
+                }
+
+                if (evt.getClickCount() == 2) {
+                    if (popOver != null && popOver.isShowing()) {
+                        popOver.hide(Duration.ZERO);
+                    }
+
+                    targetX = evt.getScreenX();
+                    targetY = evt.getScreenY();
+
+                    popOver = createPopOver();
+
+                    double size = 3;
+                    line1.setStartX(evt.getX() - size);
+                    line1.setStartY(evt.getY() - size);
+                    line1.setEndX(evt.getX() + size);
+                    line1.setEndY(evt.getY() + size);
+
+                    line2.setStartX(evt.getX() + size);
+                    line2.setStartY(evt.getY() - size);
+                    line2.setEndX(evt.getX() - size);
+                    line2.setEndY(evt.getY() + size);
+
+                    circle.setCenterX(evt.getX());
+                    circle.setCenterY(evt.getY());
+                    circle.setRadius(size * 3);
+
+                    if (autoPosition.isSelected()) {
+                        popOver.show(rect);
+                    } else {
+                        popOver.show(rect, targetX, targetY);
+                    }
+                }
+            }
+        });
+
+        StackPane stackPane = new StackPane();
+        stackPane.getChildren().add(group);
+        Label label = new Label("Double Click for PopOver. Scroll for resize.");
+        label.setWrapText(true);
+        label.setAlignment(Pos.CENTER);
+        label.setTextAlignment(TextAlignment.CENTER);
+        label.requestFocus();
+        label.maxWidthProperty().bind(rect.widthProperty());
+
+        stackPane.getChildren().add(label);
+        BorderPane.setMargin(stackPane, new Insets(10));
+
+        BorderPane borderPane = new BorderPane();
+        borderPane.setCenter(stackPane);
+
+        return borderPane;
+    }
+
+    @Override
+    public Node getControlPanel() {
+        Slider arrowSize = new Slider(0, 50, masterArrowSize.getValue());
+        masterArrowSize.bind(arrowSize.valueProperty());
+        GridPane.setFillWidth(arrowSize, true);
+
+        Slider arrowIndent = new Slider(0, 30, masterArrowIndent.getValue());
+        masterArrowIndent.bind(arrowIndent.valueProperty());
+        GridPane.setFillWidth(arrowIndent, true);
+
+        Slider cornerRadius = new Slider(0, 32, masterCornerRadius.getValue());
+        masterCornerRadius.bind(cornerRadius.valueProperty());
+        GridPane.setFillWidth(cornerRadius, true);
+
+        GridPane controls = new GridPane();
+        controls.setHgap(10);
+        controls.setVgap(10);
+
+        BorderPane.setMargin(controls, new Insets(10));
+        BorderPane.setAlignment(controls, Pos.BOTTOM_CENTER);
+
+        Label arrowSizeLabel = new Label("Arrow Size:");
+        GridPane.setHalignment(arrowSizeLabel, HPos.RIGHT);
+        controls.add(arrowSizeLabel, 0, 0);
+        controls.add(arrowSize, 1, 0);
+
+        Label arrowIndentLabel = new Label("Arrow Indent:");
+        GridPane.setHalignment(arrowIndentLabel, HPos.RIGHT);
+        controls.add(arrowIndentLabel, 0, 1);
+        controls.add(arrowIndent, 1, 1);
+
+        Label cornerRadiusLabel = new Label("Corner Radius:");
+        GridPane.setHalignment(cornerRadiusLabel, HPos.RIGHT);
+        controls.add(cornerRadiusLabel, 0, 2);
+        controls.add(cornerRadius, 1, 2);
+
+        final Label arrowSizeValue = new Label();
+        controls.add(arrowSizeValue, 2, 0);
+
+        final Label arrowIndentValue = new Label();
+        controls.add(arrowIndentValue, 2, 1);
+
+        final Label cornerRadiusValue = new Label();
+        controls.add(cornerRadiusValue, 2, 2);
+
+        arrowSize.valueProperty().addListener(new ChangeListener<Number>() {
+            @Override
+            public void changed(ObservableValue<? extends Number> value,
+                    Number oldSize, Number newSize) {
+                arrowSizeValue.setText(NumberFormat.getIntegerInstance()
+                        .format(newSize));
+            }
+        });
+
+        arrowIndent.valueProperty().addListener(new ChangeListener<Number>() {
+            @Override
+            public void changed(ObservableValue<? extends Number> value,
+                    Number oldSize, Number newSize) {
+                arrowIndentValue.setText(NumberFormat.getIntegerInstance()
+                        .format(newSize));
+            }
+        });
+
+        cornerRadius.valueProperty().addListener(new ChangeListener<Number>() {
+            @Override
+            public void changed(ObservableValue<? extends Number> value,
+                    Number oldSize, Number newSize) {
+                cornerRadiusValue.setText(NumberFormat.getIntegerInstance()
+                        .format(newSize));
+            }
+        });
+
+        Label arrowLocationLabel = new Label("Arrow Location:");
+        GridPane.setHalignment(arrowLocationLabel, HPos.RIGHT);
+        controls.add(arrowLocationLabel, 0, 3);
+
+        ComboBox<ArrowLocation> locationBox = new ComboBox<>();
+        locationBox.getItems().addAll(ArrowLocation.values());
+        locationBox.setValue(ArrowLocation.TOP_CENTER);
+        Bindings.bindBidirectional(masterArrowLocation,
+                locationBox.valueProperty());
+        controls.add(locationBox, 1, 3);
+
+        detachable = new CheckBox("Detachable");
+        detachable.setSelected(true);
+        controls.add(detachable, 0, 4);
+        GridPane.setColumnSpan(detachable, 2);
+
+        detached = new CheckBox("Initially detached");
+        controls.add(detached, 0, 5);
+        GridPane.setColumnSpan(detached, 2);
+
+        autoPosition = new CheckBox("Auto Position");
+        controls.add(autoPosition, 0, 6);
+        GridPane.setColumnSpan(autoPosition, 2);
+
+        autoPosition.setOnAction(evt -> {
+            if (popOver != null) {
+                popOver.hide();
+            }
+        });
+
+        headerAlwaysVisible = new CheckBox("Header Always Visible");
+        controls.add(headerAlwaysVisible, 0, 7);
+        GridPane.setColumnSpan(headerAlwaysVisible, 2);
+
+        masterHeaderAlwaysVisible.bind(headerAlwaysVisible.selectedProperty());
+
+        animated = new CheckBox("Animated");
+        animated.setSelected(true);
+        controls.add(animated, 0, 8);
+        GridPane.setColumnSpan(animated, 2);
+
+        circle.visibleProperty().bind(
+                Bindings.not(autoPosition.selectedProperty()));
+        line1.visibleProperty().bind(
+                Bindings.not(autoPosition.selectedProperty()));
+        line2.visibleProperty().bind(
+                Bindings.not(autoPosition.selectedProperty()));
+
+        return controls;
+    }
+
+    private PopOver createPopOver() {
+        PopOver popOver = new PopOver();
+        popOver.setDetachable(detachable.isSelected());
+        popOver.setDetached(detached.isSelected());
+        popOver.arrowSizeProperty().bind(masterArrowSize);
+        popOver.arrowIndentProperty().bind(masterArrowIndent);
+        popOver.arrowLocationProperty().bind(masterArrowLocation);
+        popOver.cornerRadiusProperty().bind(masterCornerRadius);
+        popOver.headerAlwaysVisibleProperty().bind(masterHeaderAlwaysVisible);
+        popOver.setAnimated(animated.isSelected());
+        return popOver;
+    }
+
+    public PopOver getPopOver() {
+        return popOver;
+    }
+
+    public static void main(String[] args) {
+        Application.launch(args);
+    }
+
+    @Override
+    public String getSampleName() {
+        return "PopOver";
+    }
+
+    @Override
+    public String getJavaDocURL() {
+        return Utils.JAVADOC_BASE + "org/controlsfx/control/PopOver.html";
+    }
+
+    @Override
+    public String getSampleDescription() {
+        return "An implementation of a pop over control as used by Apple for its iCal application. A pop over allows"
+                + " the user to see and edit an objects properties. The pop over gets displayed in its own popup window and"
+                + " can be torn off in order to create several instances of it.";
+    }
+
+    @Override
+    public String getControlStylesheetURL() {
+    	return "/org/controlsfx/control/popover.css";
+    }
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloPrefixSelection.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloPrefixSelection.java
new file mode 100644
index 0000000..aabb012
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloPrefixSelection.java
@@ -0,0 +1,142 @@
+/**
+ * Copyright (c) 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples;
+
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.geometry.Insets;
+import javafx.scene.Node;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.Label;
+import javafx.scene.layout.GridPane;
+import javafx.stage.Stage;
+
+import org.controlsfx.ControlsFXSample;
+import org.controlsfx.control.PrefixSelectionChoiceBox;
+import org.controlsfx.control.PrefixSelectionComboBox;
+
+public class HelloPrefixSelection extends ControlsFXSample {
+    
+    public static void main(String[] args) {
+        launch(args);
+    }
+    
+    @Override public String getSampleName() {
+        return "Prefix Selection ComboBox/ChoiceBox";
+    }
+    
+    @Override public String getJavaDocURL() {
+        return Utils.JAVADOC_BASE + "org/controlsfx/tools/PrefixSelectionCustomizer.html";
+    }
+    
+    @Override public Node getPanel(Stage stage) {
+        GridPane grid = new GridPane();
+        grid.setVgap(12);
+        grid.setHgap(12);
+        grid.setPadding(new Insets(24));
+        
+        ObservableList<String> stringList = FXCollections.observableArrayList("1111", "2222", "Aaaaa", "Abbbb", "Abccc", "Abcdd", "Abcde", "Bbbb", "bbbb", "Cccc", "Dddd", "Eeee", "Ffff", "gggg", "hhhh", "3333");
+
+        grid.add(new Label("ChoiceBox<String>"), 0, 0);
+        PrefixSelectionChoiceBox<String> choice1 = new PrefixSelectionChoiceBox<>();
+        choice1.setItems(stringList);
+        choice1.setMaxWidth(Double.MAX_VALUE);
+        grid.add(choice1, 1, 0);
+        
+        grid.add(new Label("ComboBox<String>"), 0, 1);
+        PrefixSelectionComboBox<String> combo1 = new PrefixSelectionComboBox<>();
+        combo1.setItems(stringList);
+        combo1.setMaxWidth(Double.MAX_VALUE);
+        grid.add(combo1, 1, 1);
+        CheckBox cb1 = new CheckBox("Make ComboBox editable");
+        cb1.selectedProperty().bindBidirectional(combo1.editableProperty());
+        grid.add(cb1, 2, 1);
+        
+        ObservableList<Person> personList = FXCollections.observableArrayList(
+                new Person("Jack Nicholson"),
+                new Person("Marlon Brando"), 
+                new Person("Robert De Niro"), 
+                new Person("Al Pacino"),
+                new Person("Daniel Day-Lewis"),
+                new Person("Dustin Hoffman"),
+                new Person("Tom Hanks"),
+                new Person("Anthony Hopkins"),
+                new Person("Paul Newman"),
+                new Person("Denzel Washington"),
+                new Person("Spencer Tracy"),
+                new Person("Laurence Olivier"),
+                new Person("Jack Lemmon"),
+                new Person("Jeff Bridges"),
+                new Person("James Stewart"),
+                new Person("Sean Penn"),
+                new Person("Michael Caine"),
+                new Person("Morgan Freeman"),
+                new Person("Robert Duvall"),
+                new Person("Gene Hackman"),
+                new Person("Clint Eastwood"),
+                new Person("Gregory Peck"),
+                new Person("Robin Williams"),
+                new Person("Ben Kingsley"),
+                new Person("Philip Seymour Hoffman")
+        );
+        
+        grid.add(new Label("ChoiceBox<Person>"), 0, 3);
+        PrefixSelectionChoiceBox<Person> choice2 = new PrefixSelectionChoiceBox<>();
+        choice2.setItems(personList);
+        choice2.setMaxWidth(Double.MAX_VALUE);
+        grid.add(choice2, 1, 3);
+        
+        grid.add(new Label("ComboBox<Person>"), 0, 4);
+        PrefixSelectionComboBox<Person> combo2 = new PrefixSelectionComboBox<>();
+        combo2.setItems(personList);
+        combo2.setMaxWidth(Double.MAX_VALUE);
+        grid.add(combo2, 1, 4);
+        CheckBox cb2 = new CheckBox("Make ComboBox editable");
+        cb2.selectedProperty().bindBidirectional(combo2.editableProperty());
+        grid.add(cb2, 2, 4);
+        
+        return grid;
+    }
+    
+    @Override public String getSampleDescription() {
+        return "This utility class can be used to customize a ChoiceBox or ComboBox"
+                + " and enable the prefix selection feature. This will enable the user to type letters or"
+                + " digits on the keyboard and die ChoiceBox or ComboBox will attempt to"
+                + " select the first item it can find with a matching prefix.";
+    }
+    
+    private static class Person {
+        public String name;
+        public Person(String string) {
+            name = string;
+        }
+        @Override
+        public String toString() {
+            return name;
+        }
+    }
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloPropertySheet.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloPropertySheet.java
new file mode 100644
index 0000000..549f903
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloPropertySheet.java
@@ -0,0 +1,260 @@
+/**
+ * Copyright (c) 2014, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples;
+
+import java.time.LocalDate;
+import java.time.Month;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Optional;
+import javafx.beans.value.ObservableValue;
+
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.concurrent.Service;
+import javafx.concurrent.Task;
+import javafx.concurrent.WorkerStateEvent;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.scene.Node;
+import javafx.scene.control.Button;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.TextField;
+import javafx.scene.layout.VBox;
+import javafx.stage.Stage;
+
+import org.controlsfx.ControlsFXSample;
+import org.controlsfx.control.PropertySheet;
+import org.controlsfx.control.PropertySheet.Item;
+import org.controlsfx.control.PropertySheet.Mode;
+import org.controlsfx.control.SegmentedButton;
+import org.controlsfx.control.action.Action;
+import org.controlsfx.control.action.ActionUtils;
+import org.controlsfx.property.BeanProperty;
+import org.controlsfx.property.BeanPropertyUtils;
+import org.controlsfx.samples.propertysheet.CustomPropertyDescriptor;
+import org.controlsfx.samples.propertysheet.SampleBean;
+
+public class HelloPropertySheet extends ControlsFXSample {
+
+    private static Map<String, Object> customDataMap = new LinkedHashMap<>();
+
+    static {
+        customDataMap.put("1. Name#First Name", "Jonathan");
+        customDataMap.put("1. Name#Last Name", "Giles");
+        customDataMap.put("1. Name#Birthday", LocalDate.of(1985, Month.JANUARY, 12));
+        customDataMap.put("2. Billing Address#Address 1", "");
+        customDataMap.put("2. Billing Address#Address 2", "");
+        customDataMap.put("2. Billing Address#City", "");
+        customDataMap.put("2. Billing Address#State", "");
+        customDataMap.put("2. Billing Address#Zip", "");
+        customDataMap.put("3. Phone#Home", "123-123-1234");
+        customDataMap.put("3. Phone#Mobile", "234-234-2345");
+        customDataMap.put("3. Phone#Work", "");
+    }
+
+    private PropertySheet propertySheet = new PropertySheet();
+
+    public static void main(String[] args) {
+        launch();
+    }
+
+    @Override
+    public String getSampleName() {
+        return "Property Sheet";
+    }
+
+    @Override
+    public String getSampleDescription() {
+        return "The PropertySheet control is useful when you want to present a number"
+                + " of properties to a user for them to edit.";
+    }
+
+    @Override
+    public String getJavaDocURL() {
+        return Utils.JAVADOC_BASE + "org/controlsfx/control/PropertySheet.html";
+    }
+    
+    
+    @Override
+    public String getControlStylesheetURL() {
+    	return "/org/controlsfx/control/propertysheet.css";
+    }
+
+    class CustomPropertyItem implements Item {
+
+        private String key;
+        private String category, name;
+
+        public CustomPropertyItem(String key) {
+            this.key = key;
+            String[] skey = key.split("#");
+            category = skey[0];
+            name = skey[1];
+        }
+
+        @Override
+        public Class<?> getType() {
+            return customDataMap.get(key).getClass();
+        }
+
+        @Override
+        public String getCategory() {
+            return category;
+        }
+
+        @Override
+        public String getName() {
+            return name;
+        }
+
+        @Override
+        public String getDescription() {
+            return null;
+        }
+
+        @Override
+        public Object getValue() {
+            return customDataMap.get(key);
+        }
+
+        @Override
+        public void setValue(Object value) {
+            customDataMap.put(key, value);
+        }
+
+        @Override
+        public Optional<ObservableValue<? extends Object>> getObservableValue() {
+            return Optional.empty();
+        }
+
+    }
+
+    class ActionShowInPropertySheet extends Action {
+
+        private Object bean;
+
+        public ActionShowInPropertySheet(String title, Object bean) {
+            super(title);
+            setEventHandler(this::handleAction);
+            this.bean = bean;
+        }
+
+        private ObservableList<Item> getCustomModelProperties() {
+            ObservableList<Item> list = FXCollections.observableArrayList();
+            for (String key : customDataMap.keySet()) {
+                list.add(new CustomPropertyItem(key));
+            }
+            return list;
+        }
+
+        private void handleAction(ActionEvent ae) {
+
+            // retrieving bean properties may take some time
+            // so we have to put it on separate thread to keep UI responsive
+            Service<?> service = new Service<ObservableList<Item>>() {
+
+                @Override
+                protected Task<ObservableList<Item>> createTask() {
+                    return new Task<ObservableList<Item>>() {
+                        @Override
+                        protected ObservableList<Item> call() throws Exception {
+                            return bean == null ? getCustomModelProperties() : BeanPropertyUtils.getProperties(bean);
+                        }
+                    };
+                }
+
+            };
+            service.setOnSucceeded(new EventHandler<WorkerStateEvent>() {
+
+                @SuppressWarnings("unchecked")
+                @Override
+                public void handle(WorkerStateEvent e) {
+                    if (bean instanceof SampleBean) {
+                        for (Item i : (ObservableList<Item>) e.getSource().getValue()) {
+                            if (i instanceof BeanProperty && ((BeanProperty) i).getPropertyDescriptor() instanceof CustomPropertyDescriptor) {
+                                BeanProperty bi = (BeanProperty) i;
+                                bi.setEditable(((CustomPropertyDescriptor) bi.getPropertyDescriptor()).isEditable());
+                            }
+                        }
+                    }
+                    propertySheet.getItems().setAll((ObservableList<Item>) e.getSource().getValue());
+                }
+            });
+            service.start();
+
+        }
+
+    }
+
+    @Override
+    public Node getPanel(Stage stage) {
+        return propertySheet;
+    }
+
+    @Override
+    public Node getControlPanel() {
+        VBox infoPane = new VBox(10);
+
+        Button button = new Button("Title");
+        TextField textField = new TextField();
+        SampleBean sampleBean = new SampleBean();
+
+        SegmentedButton segmentedButton = ActionUtils.createSegmentedButton(
+                new ActionShowInPropertySheet("Bean: Button", button),
+                new ActionShowInPropertySheet("Bean: TextField", textField),
+                new ActionShowInPropertySheet("Custom Model", null),
+                new ActionShowInPropertySheet("Custom BeanInfo", sampleBean)
+        );
+        segmentedButton.getStyleClass().add(SegmentedButton.STYLE_CLASS_DARK);
+        segmentedButton.getButtons().get(0).fire();
+
+        CheckBox toolbarModeVisible = new CheckBox("Show Mode Buttons");
+        toolbarModeVisible.selectedProperty().bindBidirectional(propertySheet.modeSwitcherVisibleProperty());
+
+        CheckBox toolbarSearchVisible = new CheckBox("Show Search Field");
+        toolbarSearchVisible.selectedProperty().bindBidirectional(propertySheet.searchBoxVisibleProperty());
+
+        infoPane.getChildren().add(toolbarModeVisible);
+        infoPane.getChildren().add(toolbarSearchVisible);
+        infoPane.getChildren().add(segmentedButton);
+        infoPane.getChildren().add(button);
+        infoPane.getChildren().add(textField);
+
+        return infoPane;
+    }
+
+    class ActionModeChange extends Action {
+
+        public ActionModeChange(String title, Mode mode) {
+            super(title);
+            setEventHandler(ae -> propertySheet.modeProperty().set(mode));
+        }
+
+    }
+
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloRangeSlider.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloRangeSlider.java
new file mode 100644
index 0000000..23b3a88
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloRangeSlider.java
@@ -0,0 +1,184 @@
+/**
+ * Copyright (c) 2013, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples;
+
+import javafx.geometry.Insets;
+import javafx.geometry.Orientation;
+import javafx.scene.Node;
+import javafx.scene.control.TextField;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.VBox;
+import javafx.stage.Stage;
+import javafx.util.StringConverter;
+
+import org.controlsfx.ControlsFXSample;
+import org.controlsfx.control.RangeSlider;
+
+public class HelloRangeSlider extends ControlsFXSample {
+
+    public static void main(String[] args) {
+        launch(args);
+    }
+
+    @Override public String getSampleName() {
+        return "RangeSlider";
+    }
+
+    @Override public String getJavaDocURL() {
+        return Utils.JAVADOC_BASE + "org/controlsfx/control/RangeSlider.html";
+    }
+
+    
+    @Override
+    public String getControlStylesheetURL() {
+        return "/org/controlsfx/control/rangeslider.css";
+    }
+
+    @Override public Node getPanel(Stage stage) {
+        VBox root = new VBox(15);
+
+        Region horizontalRangeSlider = createHorizontalSlider();
+        Region verticalRangeSlider = createVerticalSlider();
+        Region labelRangeSlider = createLabelSlider();
+        root.getChildren().addAll(horizontalRangeSlider, verticalRangeSlider, labelRangeSlider );
+
+        return root;
+    }
+
+    @Override public String getSampleDescription() {
+        return "The Slider control in JavaFX is great for selecting a single "
+                + "value between a min and max value, but it isn't so great for "
+                + "letting users select a range - that's where RangeSlider comes in!";
+    }
+
+    Region createLabelSlider() {
+        final RangeSlider hSlider = new RangeSlider(0, 100, 10, 90);
+        hSlider.setShowTickMarks(true);
+        hSlider.setShowTickLabels(true);
+        hSlider.setLabelFormatter(new StringConverter<Number>() {
+
+            @Override
+            public String toString(Number object) {
+                switch (object.intValue()) {
+                    case 0:
+                        return "very low";
+                    case 25:
+                        return "low";
+                    case 50:
+                        return "middle";
+                    case 75:
+                        return "high";
+                    case 100:
+                        return "very high";
+                }
+                return object.toString();
+            }
+
+            @Override
+            public Number fromString(String string) {
+                return Double.valueOf(string);
+            }
+        });
+        hSlider.setBlockIncrement(10);
+        hSlider.setPrefWidth(300);
+
+        HBox box = new HBox(10);
+        box.getChildren().addAll(hSlider);
+        box.setPadding(new Insets(20,0,0,20));
+        box.setFillHeight(false);
+
+        return box;
+    }
+
+    Region createHorizontalSlider() {
+        final TextField minField = new TextField();
+        minField.setPrefColumnCount(5);
+        final TextField maxField = new TextField();
+        maxField.setPrefColumnCount(5);
+
+        final RangeSlider hSlider = new RangeSlider(0, 100, 10, 90);
+        hSlider.setShowTickMarks(true);
+        hSlider.setShowTickLabels(true);
+        hSlider.setBlockIncrement(10);
+        hSlider.setPrefWidth(200);
+
+        minField.setText("" + hSlider.getLowValue());
+        maxField.setText("" + hSlider.getHighValue());
+
+        minField.setEditable(false);
+        minField.setPromptText("Min");
+
+        maxField.setEditable(false);
+        maxField.setPromptText("Max");
+
+        minField.textProperty().bind(hSlider.lowValueProperty().asString("%.2f"));
+        maxField.textProperty().bind(hSlider.highValueProperty().asString("%.2f"));
+
+        HBox box = new HBox(10);
+        box.getChildren().addAll(minField, hSlider, maxField);
+        box.setPadding(new Insets(20,0,0,20));
+        box.setFillHeight(false);
+
+        return box;
+    }
+
+    
+    Region createVerticalSlider() {
+        final TextField minField = new TextField();
+        minField.setPrefColumnCount(5);
+        final TextField maxField = new TextField();
+        maxField.setPrefColumnCount(5);
+
+        final RangeSlider vSlider = new RangeSlider(0, 200, 30, 150);
+        vSlider.setOrientation(Orientation.VERTICAL);
+        vSlider.setPrefHeight(200);
+        vSlider.setBlockIncrement(10);
+        vSlider.setShowTickMarks(true);
+        vSlider.setShowTickLabels(true);
+
+        minField.setText("" + vSlider.getLowValue());
+        maxField.setText("" + vSlider.getHighValue());
+
+        minField.setEditable(false);
+        minField.setPromptText("Min");
+
+        maxField.setEditable(false);
+        maxField.setPromptText("Max");
+
+        minField.textProperty().bind(vSlider.lowValueProperty().asString("%.2f"));
+        maxField.textProperty().bind(vSlider.highValueProperty().asString("%.2f"));
+
+        VBox box = new VBox(10);
+        box.setPadding(new Insets(0,0,0, 20));
+//        box.setAlignment(Pos.CENTER);
+        box.setFillWidth(false);
+        box.getChildren().addAll(maxField, vSlider, minField);
+        return box;
+    }
+
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloRating.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloRating.java
new file mode 100644
index 0000000..26674e7
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloRating.java
@@ -0,0 +1,117 @@
+/**
+ * Copyright (c) 2013, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples;
+
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.FXCollections;
+import javafx.geometry.Insets;
+import javafx.geometry.Orientation;
+import javafx.scene.Node;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.ChoiceBox;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.VBox;
+import javafx.stage.Stage;
+
+import org.controlsfx.ControlsFXSample;
+import org.controlsfx.control.Rating;
+
+public class HelloRating extends ControlsFXSample {
+    
+    private Rating rating;
+    
+    public static void main(String[] args) {
+        launch(args);
+    }
+    
+    @Override public String getSampleName() {
+        return "Rating";
+    }
+    
+    @Override public String getJavaDocURL() {
+        return Utils.JAVADOC_BASE + "org/controlsfx/control/Rating.html";
+    }
+    
+    
+    @Override
+    public String getControlStylesheetURL() {
+    	return "/org/controlsfx/control/rating.css";
+    }
+    
+    @Override public String getSampleDescription() {
+        return "TODO";
+    }
+    
+    @Override public Node getPanel(Stage stage) {
+        VBox root = new VBox(20);
+        root.setPadding(new Insets(30, 30, 30, 30));
+        rating = new Rating();
+        root.getChildren().addAll(rating);
+        
+        rating.ratingProperty().addListener(new ChangeListener<Number>() {
+            @Override public void changed(ObservableValue<? extends Number> ov, Number t, Number t1) {
+                System.out.println("Rating = " + t1);
+            }
+        });
+        
+        return root;
+    }
+    
+    @Override public Node getControlPanel() {
+        VBox root = new VBox(20);
+        root.setPadding(new Insets(30, 30, 30, 30));
+        
+        // controls, row 1
+        HBox controls_row1 = new HBox(5);
+        ChoiceBox<Orientation> orientation = new ChoiceBox<Orientation>(FXCollections.observableArrayList(Orientation.values()));
+        orientation.getSelectionModel().select(Orientation.HORIZONTAL);
+        rating.orientationProperty().bind(orientation.getSelectionModel().selectedItemProperty());
+        
+        ChoiceBox<Double> ratingValue = new ChoiceBox<Double>(FXCollections.observableArrayList(0D, 1D, 2D, 3D, 4D, 5D, 6D, 7D, 8D, 9D, 10D));
+        ratingValue.getSelectionModel().select(rating.getRating());
+//        rating.ratingProperty().bind(ratingValue.getSelectionModel().selectedItemProperty());
+        
+        ChoiceBox<Integer> maxValue = new ChoiceBox<Integer>(FXCollections.observableArrayList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
+        maxValue.getSelectionModel().select(rating.getMax());
+        rating.maxProperty().bind(maxValue.getSelectionModel().selectedItemProperty());
+        
+        controls_row1.getChildren().addAll(orientation, ratingValue, maxValue);
+        
+        // controls, row 2
+        CheckBox partialRating = new CheckBox("Allow partial ratings");
+        partialRating.selectedProperty().bindBidirectional(rating.partialRatingProperty());
+        
+        // controls, row 3
+        CheckBox updateOnHover = new CheckBox("Update rating on hover");
+        updateOnHover.selectedProperty().bindBidirectional(rating.updateOnHoverProperty());
+        
+        root.getChildren().addAll(controls_row1, partialRating, updateOnHover);
+        
+        return root;
+    }
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloSnapshotView.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloSnapshotView.java
new file mode 100644
index 0000000..0089d1b
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloSnapshotView.java
@@ -0,0 +1,545 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples;
+
+import java.text.DecimalFormat;
+import java.text.ParseException;
+import java.util.Random;
+
+import javafx.animation.Animation;
+import javafx.animation.AnimationTimer;
+import javafx.animation.RotateTransition;
+import javafx.application.Application;
+import javafx.beans.binding.Bindings;
+import javafx.beans.property.IntegerProperty;
+import javafx.beans.property.SimpleIntegerProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.FXCollections;
+import javafx.geometry.Insets;
+import javafx.geometry.Rectangle2D;
+import javafx.scene.Node;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.ChoiceBox;
+import javafx.scene.control.ColorPicker;
+import javafx.scene.control.Label;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.control.SelectionModel;
+import javafx.scene.control.Slider;
+import javafx.scene.control.TextField;
+import javafx.scene.control.TitledPane;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.VBox;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Rectangle;
+import javafx.stage.Stage;
+import javafx.util.Duration;
+import javafx.util.StringConverter;
+
+import org.controlsfx.ControlsFXSample;
+import org.controlsfx.control.SnapshotView;
+import org.controlsfx.control.SnapshotView.Boundary;
+
+/**
+ * Demonstrates the {@link SnapshotView}.
+ */
+ at SuppressWarnings("nls")
+public class HelloSnapshotView extends ControlsFXSample {
+
+    /* ************************************************************************
+     *                                                                         *
+     * Attributes & Properties                                                 *
+     *                                                                         *
+     **************************************************************************/
+
+    // STATIC
+
+    /**
+     * The format used to display all numbers in the text fields.
+     */
+    private static final DecimalFormat zeroDpFormat = new DecimalFormat("0");
+    private static final DecimalFormat twoDpFormat = new DecimalFormat("0.00");
+
+    // INSTANCE
+
+    /**
+     * The displayed nodes.
+     */
+    private final Node[] nodes;
+
+    /**
+     * The names of the displayed nodes.
+     */
+    private final String[] nodeNames = new String[] {
+            "ImageView",
+            "Fitted ImageView",
+            "Transformed ImageView",
+            "Rotating Node",
+            "Null Node",
+    };
+
+    /**
+     * The images displayed by the image views.
+     */
+    private final Image[] images;
+
+    /**
+     * The names of the displayed nodes.
+     */
+    private final String[] imageNames = new String[] {
+            "ControlsFX",
+            "Java's Duke",
+            "Null Image",
+    };
+
+    private final IntegerProperty selectedImageIndex = new SimpleIntegerProperty();
+
+    /**
+     * The demonstrated view.
+     */
+    private final SnapshotView snapshotView = new SnapshotView();
+
+    /* ************************************************************************
+     *                                                                         *
+     * Construction                                                            *
+     *                                                                         *
+     **************************************************************************/
+
+    public HelloSnapshotView() {
+        images = loadImages();
+        nodes = createNodes();
+    }
+
+    /* ************************************************************************
+     *                                                                         *
+     * Displayed Controls                                                      *
+     *                                                                         *
+     **************************************************************************/
+
+    @Override
+    public Node getPanel(Stage stage) {
+        snapshotView.setNode(nodes[0]);
+        return snapshotView;
+    }
+
+    /**
+     * Loads the displayed images.
+     * 
+     * @return an array of {@link Image image}s
+     */
+    private static Image[] loadImages() {
+        Image controlsFX = new Image(HelloSnapshotView.class.getResource("ControlsFX.png").toExternalForm());
+        Image duke = new Image(HelloSnapshotView.class.getResource("duke_wave.png").toExternalForm());
+        return new Image[] { controlsFX, duke, null };
+    }
+
+    /**
+     * Creates the nodes used by the snapshot view.
+     * 
+     * @return an array of {@link Node node}s
+     */
+    private Node[] createNodes() {
+        // regular image view
+        ImageView imageView = new ImageView(images[0]);
+        imageView.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> increaseImageIndex());
+
+        // fitted image view
+        ImageView fittedImageView = new ImageView(images[0]);
+        fittedImageView.setPreserveRatio(true);
+        fittedImageView.fitWidthProperty().bind(snapshotView.widthProperty());
+        fittedImageView.fitHeightProperty().bind(snapshotView.heightProperty());
+        fittedImageView.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> increaseImageIndex());
+
+        // transformed image view
+        ImageView transformedImageView = new ImageView(images[0]);
+        transformedImageView.setScaleX(0.5);
+        transformedImageView.setRotate(45);
+        transformedImageView.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> increaseImageIndex());
+
+        // rotating rectangle
+        Rectangle rotatingRect = new Rectangle(200, 300, Color.GREEN);
+        rotatingRect.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
+            Random r = new Random();
+            Color newColor = new Color(r.nextDouble(), r.nextDouble(), r.nextDouble(), r.nextDouble());
+            rotatingRect.setFill(newColor);
+        });
+        RotateTransition rotator = new RotateTransition(Duration.seconds(3), rotatingRect);
+        rotator.setAutoReverse(true);
+        rotator.setByAngle(360);
+        rotator.setCycleCount(Animation.INDEFINITE);
+        rotator.play();
+
+        return new Node[] {
+                new Pane(imageView), new Pane(fittedImageView), new Pane(transformedImageView),
+                new Pane(rotatingRect), null
+        };
+    }
+
+    private void increaseImageIndex() {
+        int currentImageIndex = selectedImageIndex.get();
+        // set the next index but leave out the null image (which is assumed to be last in the array)
+        int nextImageIndex = (currentImageIndex + 1) % (images.length - 1);
+        selectedImageIndex.set(nextImageIndex);
+    }
+
+    @Override
+    public Node getControlPanel() {
+        return new VBox(10,
+                createNodeControl(), createSettingsControl(),
+                createVisualizationControl(), createSelectionControl(), createSnapshotImageView());
+    }
+
+    /**
+     * @return a control for all the node related properties
+     */
+    private Node createNodeControl() {
+        GridPane grid = new GridPane();
+        grid.setVgap(5);
+        grid.setHgap(5);
+        grid.setPadding(new Insets(5));
+
+        int row = 0;
+
+        // --- node
+        final Label nodeTypeLabel = new Label("Node type: ");
+        nodeTypeLabel.getStyleClass().add("property");
+        grid.add(nodeTypeLabel, 0, row);
+
+        final ChoiceBox<String> nodeOptions = new ChoiceBox<>(FXCollections.observableArrayList(nodeNames));
+        final SelectionModel<String> nodeSelectionModel = nodeOptions.getSelectionModel();
+        nodeSelectionModel.selectedIndexProperty().addListener(
+                (o, oldNodeIndex, newNodeIndex) -> snapshotView.setNode(nodes[newNodeIndex.intValue()]));
+        nodeSelectionModel.select(0);
+
+        nodeOptions.setMaxWidth(Double.MAX_VALUE);
+        GridPane.setHgrow(nodeOptions, Priority.ALWAYS);
+        grid.add(nodeOptions, 1, row++);
+
+        // --- image
+        final Label imageLabel = new Label("Image: ");
+        imageLabel.getStyleClass().add("property");
+        grid.add(imageLabel, 0, row);
+
+        final ChoiceBox<String> imageOptions = new ChoiceBox<>(FXCollections.observableArrayList(imageNames));
+        // disable the box if no image view is shown
+        imageOptions.disableProperty().bind(Bindings.equal(3, nodeSelectionModel.selectedIndexProperty()));
+        // bind 'selectedImageIndex' and the box' selection model together
+        final SelectionModel<String> imageSelectionModel = imageOptions.getSelectionModel();
+        imageSelectionModel.selectedIndexProperty().addListener(
+                (o, oldIndex, newIndex) -> selectedImageIndex.set(newIndex.intValue()));
+        selectedImageIndex.addListener((o, oldIndex, newIndex) -> {
+            imageSelectionModel.clearAndSelect(newIndex.intValue());
+            setImageForAllViews(newIndex.intValue());
+        });
+        imageSelectionModel.select(0);
+
+        imageOptions.setMaxWidth(Double.MAX_VALUE);
+        GridPane.setHgrow(imageOptions, Priority.ALWAYS);
+        grid.add(imageOptions, 1, row++);
+
+        return new TitledPane("Node", grid);
+    }
+
+    private void setImageForAllViews(int index) {
+        Image image = images[index];
+        for (int i = 0; i < 3; i++) {
+            setImageForView(i, image);
+        }
+    }
+
+    private void setImageForView(int imageViewIndex, Image image) {
+        Pane containingPane = (Pane) nodes[imageViewIndex];
+        ImageView view = (ImageView) containingPane.getChildren().get(0);
+        view.setImage(image);
+    }
+
+    /**
+     * @return a control for all the view related properties
+     */
+    private Node createSettingsControl() {
+        GridPane grid = new GridPane();
+        grid.setVgap(5);
+        grid.setHgap(5);
+        grid.setPadding(new Insets(5));
+
+        int row = 0;
+
+        // selection active
+        CheckBox selectionActive = new CheckBox();
+        selectionActive.selectedProperty().bindBidirectional(snapshotView.selectionActiveProperty());
+        selectionActive.disableProperty().bind(snapshotView.selectionActivityManagedProperty());
+        grid.addRow(row++, new Label("Active:"), selectionActive);
+
+        // selection managed
+        CheckBox selectionActivityManaged = new CheckBox();
+        selectionActivityManaged.selectedProperty().bindBidirectional(snapshotView.selectionActivityManagedProperty());
+        grid.addRow(row++, new Label("Activity Managed:"), selectionActivityManaged);
+
+        // selection mouse transparent
+        CheckBox selectionMouseTransparent = new CheckBox();
+        selectionMouseTransparent.selectedProperty().bindBidirectional(
+                snapshotView.selectionMouseTransparentProperty());
+        grid.addRow(row++, new Label("Mouse Transparent:"), selectionMouseTransparent);
+
+        // --- fixed ratio
+        Label fixedRatioLabel = new Label("Fixed selection ratio: ");
+        fixedRatioLabel.getStyleClass().add("property");
+        grid.add(fixedRatioLabel, 0, row);
+        CheckBox ratioFixed = new CheckBox();
+        ratioFixed.selectedProperty().bindBidirectional(snapshotView.selectionRatioFixedProperty());
+        grid.add(ratioFixed, 1, row++);
+
+        // --- ratio
+        Label ratioLabel = new Label("Fixed ratio: ");
+        ratioLabel.getStyleClass().add("property");
+        grid.add(ratioLabel, 0, row);
+        TextField ratioTextField = new TextField();
+        ratioTextField.textProperty().bindBidirectional(snapshotView.fixedSelectionRatioProperty(),
+                new StringConverter<Number>() {
+                    @Override
+                    public Number fromString(String value) {
+                        try {
+                            return twoDpFormat.parse(value);
+                        } catch (ParseException e) {
+                            return 1;
+                        }
+                    }
+
+                    @Override
+                    public String toString(Number value) {
+                        return twoDpFormat.format(value);
+                    }
+                });
+        grid.add(ratioTextField, 1, row++);
+
+        // --- selection area boundary
+        final Label selectionBoundaryLabel = new Label("Selection Area Boundary: ");
+        selectionBoundaryLabel.getStyleClass().add("property");
+        grid.add(selectionBoundaryLabel, 0, row);
+
+        final ChoiceBox<Boundary> selectionBoundaryOptions = new ChoiceBox<>(
+                FXCollections.observableArrayList(Boundary.CONTROL, Boundary.NODE));
+        selectionBoundaryOptions.getSelectionModel().select(Boundary.CONTROL);
+        snapshotView.selectionAreaBoundaryProperty().bind(
+                selectionBoundaryOptions.getSelectionModel().selectedItemProperty());
+
+        selectionBoundaryOptions.setMaxWidth(Double.MAX_VALUE);
+        GridPane.setHgrow(selectionBoundaryOptions, Priority.ALWAYS);
+        grid.add(selectionBoundaryOptions, 1, row++);
+
+        // --- unselected area boundary
+        final Label unselectedBoundaryLabel = new Label("Unselected Area Boundary: ");
+        unselectedBoundaryLabel.getStyleClass().add("property");
+        grid.add(unselectedBoundaryLabel, 0, row);
+
+        final ChoiceBox<Boundary> unselectedBoundaryOptions = new ChoiceBox<>(
+                FXCollections.observableArrayList(Boundary.CONTROL, Boundary.NODE));
+        unselectedBoundaryOptions.getSelectionModel().select(Boundary.CONTROL);
+        snapshotView.unselectedAreaBoundaryProperty().bind(
+                unselectedBoundaryOptions.getSelectionModel().selectedItemProperty());
+
+        unselectedBoundaryOptions.setMaxWidth(Double.MAX_VALUE);
+        GridPane.setHgrow(unselectedBoundaryOptions, Priority.ALWAYS);
+        grid.add(unselectedBoundaryOptions, 1, row++);
+
+        return new TitledPane("Selection Settings", grid);
+    }
+
+    /**
+     * @return a control for all the visualization related properties
+     */
+    private Node createVisualizationControl() {
+        GridPane grid = new GridPane();
+        grid.setVgap(5);
+        grid.setHgap(5);
+        grid.setPadding(new Insets(5));
+
+        int row = 0;
+
+        // selection fill color
+        ColorPicker selectionFillPicker = new ColorPicker((Color) snapshotView.getSelectionAreaFill());
+        snapshotView.selectionAreaFillProperty().bind(selectionFillPicker.valueProperty());
+        grid.addRow(row++, new Label("Fill Color:"), selectionFillPicker);
+
+        // selection border color
+        ColorPicker selectionBorderPaintPicker = new ColorPicker((Color) snapshotView.getSelectionBorderPaint());
+        snapshotView.selectionBorderPaintProperty().bind(selectionBorderPaintPicker.valueProperty());
+        grid.addRow(row++, new Label("Stroke Color:"), selectionBorderPaintPicker);
+
+        // selection border width
+        Slider selectionStrokeWidth = new Slider(0, 25, snapshotView.getSelectionBorderWidth());
+        snapshotView.selectionBorderWidthProperty().bindBidirectional(selectionStrokeWidth.valueProperty());
+        grid.addRow(row++, new Label("Stroke Width:"), selectionStrokeWidth);
+
+        // unselected area fill color
+        ColorPicker unselectedAreaFillPicker = new ColorPicker((Color) snapshotView.getUnselectedAreaFill());
+        snapshotView.unselectedAreaFillProperty().bind(unselectedAreaFillPicker.valueProperty());
+        grid.addRow(row++, new Label("Outer Color:"), unselectedAreaFillPicker);
+
+        return new TitledPane("Visualization Settings", grid);
+    }
+
+    /**
+     * @return a control for all the selection related properties
+     */
+    private Node createSelectionControl() {
+        // upper left
+        TextField upperLeftX = new TextField();
+        upperLeftX.setPrefColumnCount(3);
+        upperLeftX.setEditable(false);
+        TextField upperLeftY = new TextField();
+        upperLeftY.setPrefColumnCount(3);
+        upperLeftY.setEditable(false);
+
+        // lower right
+        TextField lowerRightX = new TextField();
+        lowerRightX.setPrefColumnCount(3);
+        lowerRightX.setEditable(false);
+        TextField lowerRightY = new TextField();
+        lowerRightY.setPrefColumnCount(3);
+        lowerRightY.setEditable(false);
+
+        // size
+        TextField width = new TextField();
+        width.setPrefColumnCount(3);
+        width.setEditable(false);
+        TextField height = new TextField();
+        height.setPrefColumnCount(3);
+        height.setEditable(false);
+        TextField ratio = new TextField();
+        ratio.setPrefColumnCount(3);
+
+        // set up the binding
+        snapshotView.selectionProperty().addListener(new ChangeListener<Rectangle2D>() {
+            @Override
+            public void changed(
+                    ObservableValue<? extends Rectangle2D> observable, Rectangle2D oldValue, Rectangle2D newValue) {
+                if (newValue == null) {
+                    upperLeftX.setText("");
+                    upperLeftY.setText("");
+                    lowerRightX.setText("");
+                    lowerRightY.setText("");
+                    width.setText("");
+                    height.setText("");
+                    ratio.setText("");
+                } else {
+                    upperLeftX.setText(zeroDpFormat.format(newValue.getMinX()));
+                    upperLeftY.setText(zeroDpFormat.format(newValue.getMinY()));
+                    lowerRightX.setText(zeroDpFormat.format(newValue.getMaxX()));
+                    lowerRightY.setText(zeroDpFormat.format(newValue.getMaxY()));
+                    width.setText(zeroDpFormat.format(newValue.getWidth()));
+                    height.setText(zeroDpFormat.format(newValue.getHeight()));
+                    ratio.setText(twoDpFormat.format(newValue.getWidth() / newValue.getHeight()));
+                }
+            }
+        });
+
+        // put it all together
+        GridPane grid = new GridPane();
+        grid.setVgap(5);
+        grid.setHgap(5);
+        grid.setPadding(new Insets(5));
+
+        int row = 0;
+
+        grid.addRow(row++, new Label("Upper Left Corner:"), upperLeftX, new Label("/"), upperLeftY);
+        grid.addRow(row++, new Label("Lower Right Corner:"), lowerRightX, new Label("/"), lowerRightY);
+        grid.addRow(row++, new Label("Size (Ratio):"), width, new Label("x"), height, new Label(" ("), ratio,
+                new Label(")"));
+
+        // selection changing
+        CheckBox selectionChanging = new CheckBox();
+        selectionChanging.setDisable(true);
+        selectionChanging.selectedProperty().bind(snapshotView.selectionChangingProperty());
+        grid.addRow(row++, new Label("Selection Changing:"), selectionChanging);
+
+        return new TitledPane("Selection Stats", grid);
+    }
+
+    /**
+     * @return a control which displays the current snapshot
+     */
+    private Node createSnapshotImageView() {
+        final ImageView snapshotImageView = new ImageView();
+
+        // display snapshots which are constantly taken
+        AnimationTimer timer = new AnimationTimer() {
+            @Override
+            public void handle(long timestamp) {
+                Image snapshot = null;
+                if (snapshotView.getNode() != null && snapshotView.hasSelection()) {
+                    snapshot = snapshotView.createSnapshot();
+                }
+                snapshotImageView.setImage(snapshot);
+            }
+        };
+        timer.start();
+
+        ScrollPane scrollPane = new ScrollPane();
+        scrollPane.setContent(snapshotImageView);
+        return new TitledPane("Snapshot", scrollPane);
+    }
+
+    /* ************************************************************************
+     *                                                                         *
+     * Boilerplate                                                             *
+     *                                                                         *
+     **************************************************************************/
+
+    public static void main(String[] args) {
+        Application.launch(args);
+    }
+
+    @Override
+    public String getSampleName() {
+        return "SnapshotView";
+    }
+
+    @Override
+    public String getJavaDocURL() {
+        return Utils.JAVADOC_BASE
+                + "org/controlsfx/control/SnapshotView.html";
+    }
+
+    @Override
+    public String getControlStylesheetURL() {
+        return "/org/controlsfx/control/snapshot-view.css";
+    }
+
+    @Override
+    public String getSampleDescription() {
+        return "A control which allows the user to select a rectangular area of the displayed node. " +
+                "The selection's ratio can be fixed so that the user can only make selections with that ratio. " +
+                "The method 'createSnapshot()' returns an Image of the selected area. " +
+                "The displayed node can be interacted with if the selection is set to be mouse transparent. ";
+    }
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloSpreadsheetView.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloSpreadsheetView.java
new file mode 100644
index 0000000..b642c04
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloSpreadsheetView.java
@@ -0,0 +1,716 @@
+/**
+ * Copyright (c) 2013, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples;
+
+import java.awt.Desktop;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.geometry.Insets;
+import javafx.scene.Node;
+import javafx.scene.control.Alert;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.Hyperlink;
+import javafx.scene.control.Label;
+import javafx.scene.control.SelectionMode;
+import javafx.scene.control.Slider;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.StackPane;
+import javafx.stage.Stage;
+import org.controlsfx.ControlsFXSample;
+import org.controlsfx.control.spreadsheet.GridBase;
+import org.controlsfx.control.spreadsheet.Picker;
+import org.controlsfx.control.spreadsheet.SpreadsheetCell;
+import org.controlsfx.control.spreadsheet.SpreadsheetCellBase;
+import org.controlsfx.control.spreadsheet.SpreadsheetCellType;
+import org.controlsfx.control.spreadsheet.SpreadsheetView;
+
+/**
+ *
+ * Build the UI and launch the Application
+ */
+public class HelloSpreadsheetView extends ControlsFXSample {
+
+    public static void main(String[] args) {
+        launch(args);
+    }
+
+    private SpreadsheetView spreadSheetView;
+    private StackPane centerPane;
+    private final CheckBox rowHeader = new CheckBox();
+    private final CheckBox columnHeader = new CheckBox();
+    private final CheckBox selectionMode = new CheckBox();
+    private final CheckBox editable = new CheckBox();
+
+    /**
+     * List for custom cells
+     */
+    private final List<String> companiesList = Arrays.asList("", "ControlsFX", "Aperture Science",
+            "Rapture", "Ammu-Nation", "Nuka-Cola", "Pay'N'Spray", "Umbrella Corporation");
+
+    private final List<String> countryList = Arrays.asList("China", "France", "New Zealand",
+            "United States", "Germany", "Canada");
+
+    private final List<String> logoList = Arrays.asList("", "ControlsFX.png", "apertureLogo.png",
+            "raptureLogo.png", "ammunationLogo.JPG", "nukaColaLogo.png", "paynsprayLogo.jpg", "umbrellacorporation.png");
+
+    private final List<String> webSiteList = Arrays.asList("", "http://fxexperience.com/controlsfx/",
+            "http://aperturescience.com/", "", "http://fr.gta.wikia.com/wiki/Ammu-Nation",
+            "http://e-shop.nuka-cola.eu/", "http://fr.gta.wikia.com/wiki/Pay_%27n%27_Spray",
+            "http://www.umbrellacorporation.net/");
+
+    @Override
+    public String getSampleName() {
+        return "SpreadsheetView";
+    }
+
+    @Override
+    public String getSampleDescription() {
+        return "The SpreadsheetView is a control similar to the JavaFX TableView control "
+                + "but with different functionalities and use cases. The aim is to have a "
+                + "powerful grid where data can be written and retrieved.\n\n"
+                + "Here you have an example where some information about fictive "
+                + "companies are displayed. They have different type and format.\n\n"
+                + "After that, some random generated cells are displayed with some span.\n\n"
+                + "Don't forget to right-click on headers and cells to discover some features.";
+    }
+
+    @Override
+    public String getControlStylesheetURL() {
+        return "/org/controlsfx/samples/spreadsheetSample.css";
+    }
+
+    @Override
+    public Node getPanel(Stage stage) {
+        centerPane = new StackPane();
+
+        int rowCount = 31; //Will be re-calculated after if incorrect.
+        int columnCount = 8;
+
+        GridBase grid = new GridBase(rowCount, columnCount);
+        grid.setRowHeightCallback(new GridBase.MapBasedRowHeightFactory(generateRowHeight()));
+        buildGrid(grid);
+
+        spreadSheetView = new SpreadsheetView(grid);
+        spreadSheetView.setShowRowHeader(rowHeader.isSelected());
+        spreadSheetView.setShowColumnHeader(columnHeader.isSelected());
+        spreadSheetView.setEditable(editable.isSelected());
+        spreadSheetView.getSelectionModel().setSelectionMode(selectionMode.isSelected() ? SelectionMode.MULTIPLE : SelectionMode.SINGLE);
+
+        generatePickers();
+
+        spreadSheetView.getFixedRows().add(0);
+        spreadSheetView.getColumns().get(0).setFixed(true);
+        spreadSheetView.getColumns().get(1).setPrefWidth(250);
+        centerPane.getChildren().setAll(spreadSheetView);
+
+        spreadSheetView.getStylesheets().add(getClass().getResource("spreadsheetSample.css").toExternalForm());
+        return centerPane;
+    }
+
+    @Override
+    public Node getControlPanel() {
+        return buildCommonControlGrid();
+    }
+
+    @Override
+    public String getJavaDocURL() {
+        return Utils.JAVADOC_BASE + "org/controlsfx/control/spreadsheet/SpreadsheetView.html";
+    }
+
+    /**
+     * Add some pickers into the SpreadsheetView in order to give some
+     * information.
+     */
+    private void generatePickers() {
+        spreadSheetView.getRowPickers().put(0, new Picker() {
+
+            @Override
+            public void onClick() {
+                Alert alert = new Alert(Alert.AlertType.INFORMATION);
+                alert.setContentText("This row contains several fictive companies. "
+                        + "The cells are not editable.\n"
+                        + "A custom tooltip is applied for the first cell.");
+                alert.show();
+            }
+        });
+
+        spreadSheetView.getRowPickers().put(1, new Picker() {
+
+            @Override
+            public void onClick() {
+                Alert alert = new Alert(Alert.AlertType.INFORMATION);
+                alert.setContentText("This row contains cells that can only show a list.");
+                alert.show();
+            }
+        });
+
+        spreadSheetView.getRowPickers().put(2, new Picker() {
+
+            @Override
+            public void onClick() {
+                Alert alert = new Alert(Alert.AlertType.INFORMATION);
+                alert.setContentText("This row contains cells that display some dates.");
+                alert.show();
+            }
+        });
+
+        spreadSheetView.getRowPickers().put(3, new Picker() {
+
+            @Override
+            public void onClick() {
+                Alert alert = new Alert(Alert.AlertType.INFORMATION);
+                alert.setContentText("This row contains some Images displaying logos of the companies.");
+                alert.show();
+            }
+        });
+
+        spreadSheetView.getRowPickers().put(4, new Picker() {
+
+            @Override
+            public void onClick() {
+                Alert alert = new Alert(Alert.AlertType.INFORMATION);
+                alert.setContentText("This row contains Double editable cells. "
+                        + "Except for ControlsFX compagny where it's a String.");
+                alert.show();
+            }
+        });
+        spreadSheetView.getRowPickers().put(5, new Picker("picker-label", "picker-label-exclamation") {
+
+            @Override
+            public void onClick() {
+                Alert alert = new Alert(Alert.AlertType.INFORMATION);
+                alert.setContentText("This row contains Double editable cells with "
+                        + "a special format (%). Some cells also have "
+                        + "a little icon next to their value.");
+                alert.show();
+            }
+        });
+
+        spreadSheetView.getColumnPickers().put(0, new Picker("picker-label", "picker-label-security") {
+
+            @Override
+            public void onClick() {
+                Alert alert = new Alert(Alert.AlertType.INFORMATION);
+                alert.setContentText("Each cell of this column (except for the "
+                        + "separator in the middle) has a particular css "
+                        + "class for changing its color.\n");
+                alert.show();
+            }
+        });
+    }
+
+    /**
+     * Specify a custom row height.
+     *
+     * @return
+     */
+    private Map<Integer, Double> generateRowHeight() {
+        Map<Integer, Double> rowHeight = new HashMap<>();
+        rowHeight.put(1, 100.0);
+        return rowHeight;
+    }
+
+    /**
+     * Randomly generate a {@link SpreadsheetCell}.
+     */
+    private SpreadsheetCell generateCell(int row, int column, int rowSpan, int colSpan) {
+        SpreadsheetCell cell;
+        List<String> cityList = Arrays.asList("Shanghai", "Paris", "New York City", "Bangkok",
+                "Singapore", "Johannesburg", "Berlin", "Wellington", "London", "Montreal");
+        final double random = Math.random();
+        if (random < 0.25) {
+            cell = SpreadsheetCellType.LIST(countryList).createCell(row, column, rowSpan, colSpan,
+                    countryList.get((int) (Math.random() * 6)));
+        } else if (random >= 0.25 && random < 0.5) {
+            cell = SpreadsheetCellType.STRING.createCell(row, column, rowSpan, colSpan,
+                    cityList.get((int) (Math.random() * 10)));
+        } else if (random >= 0.5 && random < 0.75) {
+            cell = generateNumberCell(row, column, rowSpan, colSpan);
+        } else {
+            cell = generateDateCell(row, column, rowSpan, colSpan);
+        }
+
+        // Styling for preview
+        if (row % 5 == 0) {
+            cell.getStyleClass().add("five_rows");
+        }
+        return cell;
+    }
+
+    /**
+     * Generate a Date Cell with a random format.
+     *
+     * @param row
+     * @param column
+     * @param rowSpan
+     * @param colSpan
+     * @return
+     */
+    private SpreadsheetCell generateDateCell(int row, int column, int rowSpan, int colSpan) {
+        SpreadsheetCell cell = SpreadsheetCellType.DATE.createCell(row, column, rowSpan, colSpan, LocalDate.now()
+                .plusDays((int) (Math.random() * 10)));
+        final double random = Math.random();
+        if (random < 0.25) {
+            cell.setFormat("EEEE d");
+        } else if (random < 0.5) {
+            cell.setFormat("dd/MM :YY");
+        } else {
+            cell.setFormat("dd/MM/YYYY");
+        }
+        return cell;
+    }
+
+    /**
+     * Generate a Number Cell with a random format.
+     *
+     * @param row
+     * @param column
+     * @param rowSpan
+     * @param colSpan
+     * @return
+     */
+    private SpreadsheetCell generateNumberCell(int row, int column, int rowSpan, int colSpan) {
+        final double random = Math.random();
+        SpreadsheetCell cell;
+        if (random < 0.3) {
+            cell = SpreadsheetCellType.INTEGER.createCell(row, column, rowSpan, colSpan,
+                    Math.round((float) Math.random() * 100));
+        } else {
+            cell = SpreadsheetCellType.DOUBLE.createCell(row, column, rowSpan, colSpan,
+                    (double) Math.round((Math.random() * 100) * 100) / 100);
+            final double randomFormat = Math.random();
+            if (randomFormat < 0.25) {
+                cell.setFormat("#,##0.00" + "\u20AC");
+            } else if (randomFormat < 0.5) {
+                cell.setFormat("0.###E0 km/h");
+            } else {
+                cell.setFormat("0.###E0");
+            }
+        }
+        return cell;
+    }
+
+    /**
+     * Generate a Double Cell
+     *
+     * @param row
+     * @param column
+     * @param rowSpan
+     * @param colSpan
+     * @return
+     */
+    private SpreadsheetCell generateDoubleCell(int row, int column, int rowSpan, int colSpan) {
+        final double random = Math.random();
+        SpreadsheetCell cell;
+        cell = SpreadsheetCellType.DOUBLE.createCell(row, column, rowSpan, colSpan,
+                (double) Math.round((random * 100) * 100) / 100);
+        return cell;
+    }
+
+    /**
+     * Return a List of SpreadsheetCell displaying the companies.
+     *
+     * @param grid
+     * @param row
+     * @return
+     */
+    private ObservableList<SpreadsheetCell> getCompanies(GridBase grid, int row) {
+
+        final ObservableList<SpreadsheetCell> companies = FXCollections.observableArrayList();
+
+        SpreadsheetCell cell = SpreadsheetCellType.STRING.createCell(row, 0, 1, 1, "Company : ");
+        ((SpreadsheetCellBase) cell).setTooltip("This cell displays a custom toolTip.");
+        cell.setEditable(false);
+        companies.add(cell);
+
+        for (int column = 1; column < grid.getColumnCount(); ++column) {
+            cell = SpreadsheetCellType.STRING.createCell(row, column, 1, 1,
+                    companiesList.get(column));
+            cell.setEditable(false);
+            cell.getStyleClass().add("compagny");
+            companies.add(cell);
+        }
+
+        return companies;
+    }
+
+    /**
+     * Return a row with some countries.
+     *
+     * @param grid
+     * @param row
+     * @return
+     */
+    private ObservableList<SpreadsheetCell> getCountries(GridBase grid, int row) {
+
+        final ObservableList<SpreadsheetCell> countries = FXCollections.observableArrayList();
+
+        SpreadsheetCell cell = SpreadsheetCellType.STRING.createCell(row, 0, 1, 1, "Countries");
+        cell.setEditable(false);
+        cell.getStyleClass().add("first-cell");
+        countries.add(cell);
+
+        for (int column = 1; column < grid.getColumnCount(); ++column) {
+            cell = SpreadsheetCellType.LIST(countryList).createCell(row, column, 1, 1,
+                    countryList.get((int) (Math.random() * 6)));
+            countries.add(cell);
+        }
+        return countries;
+    }
+
+    /**
+     * Return a row with some dates.
+     *
+     * @param grid
+     * @param row
+     * @return
+     */
+    private ObservableList<SpreadsheetCell> getStartDate(GridBase grid, int row) {
+
+        final ObservableList<SpreadsheetCell> startDate = FXCollections.observableArrayList();
+
+        SpreadsheetCell cell = SpreadsheetCellType.STRING.createCell(row, 0, 1, 1, "Start day");
+        cell.setEditable(false);
+        cell.getStyleClass().add("first-cell");
+        startDate.add(cell);
+
+        for (int column = 1; column < grid.getColumnCount(); ++column) {
+            startDate.add(generateDateCell(row, column, 1, 1));
+        }
+        return startDate;
+    }
+
+    /**
+     * Return a row with some Images.
+     *
+     * @param grid
+     * @param row
+     * @return
+     */
+    private ObservableList<SpreadsheetCell> getLogos(GridBase grid, int row) {
+
+        final ObservableList<SpreadsheetCell> logos = FXCollections.observableArrayList();
+
+        SpreadsheetCell cell = SpreadsheetCellType.STRING.createCell(row, 0, 1, 1, "Logo");
+        cell.setEditable(false);
+        cell.getStyleClass().add("first-cell");
+        logos.add(cell);
+
+        for (int column = 1; column < grid.getColumnCount(); ++column) {
+            cell = SpreadsheetCellType.STRING.createCell(row, column, 1, 1, null);
+            cell.setGraphic(new ImageView(new Image(getClass().getResourceAsStream(logoList.get(column)))));
+            cell.getStyleClass().add("logo");
+            cell.setEditable(false);
+            logos.add(cell);
+        }
+        return logos;
+    }
+
+    /**
+     * Return a row with Double.
+     *
+     * @param grid
+     * @param row
+     * @return
+     */
+    private ObservableList<SpreadsheetCell> getIncome(GridBase grid, int row) {
+
+        final ObservableList<SpreadsheetCell> incomes = FXCollections.observableArrayList();
+
+        SpreadsheetCell cell = SpreadsheetCellType.STRING.createCell(row, 0, 1, 1, "Income");
+        cell.setEditable(false);
+        cell.getStyleClass().add("first-cell");
+        incomes.add(cell);
+
+        SpreadsheetCell cell2 = SpreadsheetCellType.STRING.createCell(row, 1, 1, 1, "It's over 9000!");
+        incomes.add(cell2);
+
+        for (int column = 2; column < grid.getColumnCount(); ++column) {
+            cell = generateDoubleCell(row, column, 1, 1);
+            cell.setFormat("#,##0.00" + "\u20AC");
+
+            incomes.add(cell);
+        }
+        return incomes;
+    }
+
+    /**
+     * Return a row with Double.
+     *
+     * @param grid
+     * @param row
+     * @return
+     */
+    private ObservableList<SpreadsheetCell> getIncrease(GridBase grid, int row) {
+
+        final ObservableList<SpreadsheetCell> increase = FXCollections.observableArrayList();
+
+        SpreadsheetCell cell = SpreadsheetCellType.STRING.createCell(row, 0, 1, 1, "Increase");
+        cell.setEditable(false);
+        cell.getStyleClass().add("first-cell");
+        increase.add(cell);
+
+        for (int column = 1; column < grid.getColumnCount(); ++column) {
+            cell = SpreadsheetCellType.DOUBLE.createCell(row, column, 1, 1, (double) Math.random());
+            if (column % 2 == 1) {
+                cell.setGraphic(new ImageView(new Image(getClass().getResourceAsStream("exclamation.png"))));
+            }
+            cell.setFormat("#" + "%");
+            increase.add(cell);
+        }
+        return increase;
+    }
+
+    /**
+     * Return a List with Integer.
+     *
+     * @param grid
+     * @param row
+     * @return
+     */
+    private ObservableList<SpreadsheetCell> getEmployees(GridBase grid, int row) {
+
+        final ObservableList<SpreadsheetCell> employees = FXCollections.observableArrayList();
+
+        SpreadsheetCell cell = SpreadsheetCellType.STRING.createCell(row, 0, 1, 1, "Number of employees");
+        cell.setEditable(false);
+        cell.getStyleClass().add("first-cell");
+        employees.add(cell);
+
+        for (int column = 1; column < grid.getColumnCount(); ++column) {
+            cell = SpreadsheetCellType.INTEGER.createCell(row, column, 1, 1,
+                    Math.round((float) Math.random() * 10));
+            employees.add(cell);
+        }
+        return employees;
+    }
+
+    /**
+     * Return a row with some URL.
+     *
+     * @param grid
+     * @param row
+     * @return
+     */
+    private ObservableList<SpreadsheetCell> getWebSite(GridBase grid, int row) {
+
+        final ObservableList<SpreadsheetCell> employees = FXCollections.observableArrayList();
+
+        SpreadsheetCell cell = SpreadsheetCellType.STRING.createCell(row, 0, 1, 1, "WebSite ");
+        cell.setEditable(false);
+        cell.getStyleClass().add("first-cell");
+        employees.add(cell);
+
+        for (int column = 1; column < grid.getColumnCount(); ++column) {
+            cell = SpreadsheetCellType.STRING.createCell(row, column, 1, 1, null);
+            Hyperlink link = new Hyperlink(webSiteList.get(column));
+            Desktop desktop = Desktop.isDesktopSupported() ? Desktop.getDesktop() : null;
+            URI uri;
+            try {
+                uri = new URI(link.getText());
+                if (desktop != null && desktop.isSupported(Desktop.Action.BROWSE)) {
+                    link.setOnAction(new EventHandler<ActionEvent>() {
+                        @Override
+                        public void handle(ActionEvent t) {
+                            try {
+                                desktop.browse(uri);
+                            } catch (IOException ex) {
+                            }
+                        }
+                    });
+                }
+            } catch (URISyntaxException ex) {
+            }
+            cell.setGraphic(link);
+            cell.setEditable(false);
+            employees.add(cell);
+        }
+        return employees;
+    }
+
+    /**
+     * Return a row with blank non editable cell.
+     *
+     * @param grid
+     * @param row
+     * @return
+     */
+    private ObservableList<SpreadsheetCell> getSeparator(GridBase grid, int row) {
+
+        final ObservableList<SpreadsheetCell> separator = FXCollections.observableArrayList();
+
+        for (int column = 0; column < grid.getColumnCount(); ++column) {
+            SpreadsheetCell cell = SpreadsheetCellType.STRING.createCell(row, column, 1, 1, "");
+            cell.setEditable(false);
+            cell.getStyleClass().add("separator");
+            separator.add(cell);
+        }
+        return separator;
+    }
+
+    /**
+     * Build the grid.
+     *
+     * @param grid
+     */
+    private void buildGrid(GridBase grid) {
+        ArrayList<ObservableList<SpreadsheetCell>> rows = new ArrayList<>(grid.getRowCount());
+
+        int rowIndex = 0;
+        rows.add(getCompanies(grid, rowIndex++));
+        rows.add(getCountries(grid, rowIndex++));
+        rows.add(getStartDate(grid, rowIndex++));
+        rows.add(getLogos(grid, rowIndex++));
+        rows.add(getIncome(grid, rowIndex++));
+        rows.add(getIncrease(grid, rowIndex++));
+        rows.add(getEmployees(grid, rowIndex++));
+        rows.add(getWebSite(grid, rowIndex++));
+
+        //Separators
+        rows.add(getSeparator(grid, rowIndex++));
+        rows.add(getSeparator(grid, rowIndex++));
+        rows.add(getSeparator(grid, rowIndex++));
+
+        for (int i = rowIndex; i < rowIndex + 20; ++i) {
+            final ObservableList<SpreadsheetCell> randomRow = FXCollections.observableArrayList();
+
+            SpreadsheetCell cell = SpreadsheetCellType.STRING.createCell(i, 0, 1, 1, "Random " + (i + 1));
+            cell.getStyleClass().add("first-cell");
+            randomRow.add(cell);
+
+            for (int column = 1; column < grid.getColumnCount(); column++) {
+                randomRow.add(generateCell(i, column, 1, 1));
+            }
+            rows.add(randomRow);
+        }
+        grid.setRows(rows);
+
+        grid.getRows().get(15).get(1).getStyleClass().add("span");
+        grid.spanRow(2, 15, 1);
+        grid.spanColumn(2, 15, 1);
+
+        grid.getRows().get(18).get(1).getStyleClass().add("span");
+        grid.spanColumn(4, 18, 1);
+
+        grid.getRows().get(19).get(1).getStyleClass().add("span");
+        grid.spanRow(3, 19, 1);
+    }
+
+    /**
+     * Build a common control Grid with some options on the left to control the
+     * SpreadsheetViewInternal
+     *
+     * @param gridType
+     *
+     * @param spreadsheetView
+     * @return
+     */
+    private GridPane buildCommonControlGrid() {
+        final GridPane grid = new GridPane();
+        grid.setHgap(5);
+        grid.setVgap(5);
+        grid.setPadding(new Insets(5, 5, 5, 5));
+
+        int row = 0;
+
+        // row header
+        Label rowHeaderLabel = new Label("Row header: ");
+        rowHeaderLabel.getStyleClass().add("property");
+        grid.add(rowHeaderLabel, 0, row);
+        rowHeader.setSelected(true);
+        spreadSheetView.setShowRowHeader(true);
+        grid.add(rowHeader, 1, row++);
+        rowHeader.selectedProperty().addListener(new ChangeListener<Boolean>() {
+            @Override
+            public void changed(ObservableValue<? extends Boolean> arg0, Boolean arg1, Boolean arg2) {
+                spreadSheetView.setShowRowHeader(arg2);
+            }
+        });
+
+        // column header
+        Label columnHeaderLabel = new Label("Column header: ");
+        columnHeaderLabel.getStyleClass().add("property");
+        grid.add(columnHeaderLabel, 0, row);
+        columnHeader.setSelected(true);
+        spreadSheetView.setShowColumnHeader(true);
+        grid.add(columnHeader, 1, row++);
+        columnHeader.selectedProperty().addListener(new ChangeListener<Boolean>() {
+            @Override
+            public void changed(ObservableValue<? extends Boolean> arg0, Boolean arg1, Boolean arg2) {
+                spreadSheetView.setShowColumnHeader(arg2);
+            }
+        });
+
+        // editable
+        Label editableLabel = new Label("Editable: ");
+        editableLabel.getStyleClass().add("property");
+        grid.add(editableLabel, 0, row);
+        editable.setSelected(true);
+        spreadSheetView.setEditable(true);
+        grid.add(editable, 1, row++);
+        spreadSheetView.editableProperty().bind(editable.selectedProperty());
+
+        //Row Header width
+        Label rowHeaderWidth = new Label("Row header width: ");
+        rowHeaderWidth.getStyleClass().add("property");
+        grid.add(rowHeaderWidth, 0, row);
+        Slider slider = new Slider(15, 100, 30);
+        spreadSheetView.rowHeaderWidthProperty().bind(slider.valueProperty());
+        grid.add(slider, 1, row++);
+
+        // Multiple Selection
+        Label selectionModeLabel = new Label("Multiple selection: ");
+        selectionModeLabel.getStyleClass().add("property");
+        grid.add(selectionModeLabel, 0, row);
+        selectionMode.setSelected(true);
+        grid.add(selectionMode, 1, row++);
+        selectionMode.selectedProperty().addListener(new ChangeListener<Boolean>() {
+            @Override
+            public void changed(ObservableValue<? extends Boolean> arg0, Boolean arg1, Boolean isSelected) {
+                spreadSheetView.getSelectionModel().clearSelection();
+                spreadSheetView.getSelectionModel().setSelectionMode(isSelected ? SelectionMode.MULTIPLE : SelectionMode.SINGLE);
+            }
+        });
+
+        return grid;
+    }
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloStatusBar.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloStatusBar.java
new file mode 100644
index 0000000..2cb6506
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloStatusBar.java
@@ -0,0 +1,161 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples;
+
+import static javafx.geometry.Orientation.VERTICAL;
+import javafx.concurrent.Task;
+import javafx.geometry.Insets;
+import javafx.scene.Node;
+import javafx.scene.control.Button;
+import javafx.scene.control.Separator;
+import javafx.scene.control.TextField;
+import javafx.scene.layout.Background;
+import javafx.scene.layout.BackgroundFill;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.CornerRadii;
+import javafx.scene.layout.VBox;
+import javafx.scene.paint.Color;
+import javafx.stage.Stage;
+
+import org.controlsfx.ControlsFXSample;
+import org.controlsfx.control.StatusBar;
+
+public class HelloStatusBar extends ControlsFXSample {
+    private StatusBar statusBar;
+
+    public static void main(String[] args) {
+        launch(args);
+    }
+
+    @Override public String getSampleName() {
+        return "StatusBar";
+    }
+
+    @Override public String getJavaDocURL() {
+        return Utils.JAVADOC_BASE + "org/controlsfx/control/StatusBar.html";
+    }
+
+    @Override public Node getPanel(Stage stage) {
+        statusBar = new StatusBar();
+
+        BorderPane borderPane = new BorderPane();
+        borderPane.setBottom(statusBar);
+
+        return borderPane;
+    }
+
+    @Override public String getSampleDescription() {
+        return "The StatusBar control can be used to display various application-specific status fields. This "
+                + "can be plain text, the progress of a long running task, or any other type of information.";
+    }
+
+    @Override public Node getControlPanel() {
+        VBox box = new VBox();
+        box.setSpacing(10);
+
+        TextField statusTextField = new TextField();
+        statusTextField.setPromptText("Status Text");
+        statusTextField.textProperty().bindBidirectional(statusBar.textProperty());
+
+        box.getChildren().add(statusTextField);
+
+        Button simulateTask = new Button("Start Task");
+        simulateTask.setOnAction(evt -> startTask());
+        box.getChildren().add(simulateTask);
+
+        Button addLeftItem = new Button("Add Left Item");
+        addLeftItem.setOnAction(evt -> addItem(true));
+        box.getChildren().add(addLeftItem);
+
+        Button addLeftSeparator = new Button("Add Left Separator");
+        addLeftSeparator.setOnAction(evt -> addSeparator(true));
+        box.getChildren().add(addLeftSeparator);
+
+        Button addRightItem = new Button("Add Right Item");
+        addRightItem.setOnAction(evt -> addItem(false));
+        box.getChildren().add(addRightItem);
+
+        Button addRightSeparator = new Button("Add Right Separator");
+        addRightSeparator.setOnAction(evt -> addSeparator(false));
+        box.getChildren().add(addRightSeparator);
+
+        return box;
+    }
+
+    private int itemCounter;
+
+    private void addItem(boolean left) {
+        itemCounter++;
+        Button button = new Button(Integer.toString(itemCounter));
+        button.setBackground(new Background(new BackgroundFill(Color.ORANGE,
+                new CornerRadii(2), new Insets(4))));
+        if (left) {
+            statusBar.getLeftItems().add(button);
+        } else {
+            statusBar.getRightItems().add(button);
+        }
+    }
+
+    private void addSeparator(boolean left) {
+        if (left) {
+            statusBar.getLeftItems().add(new Separator(VERTICAL));
+        } else {
+            statusBar.getRightItems().add(new Separator(VERTICAL));
+        }
+    }
+
+    private void startTask() {
+        Task<Void> task = new Task<Void>() {
+            @Override protected Void call() throws Exception {
+                updateMessage("First we sleep ....");
+
+                Thread.sleep(2500);
+
+                int max = 100000000;
+                for (int i = 0; i < max; i++) {
+                    updateMessage("Message " + i);
+                    updateProgress(i, max);
+                }
+
+                updateProgress(0, 0);
+                done();
+                return null;
+            }
+        };
+        
+        statusBar.textProperty().bind(task.messageProperty());
+        statusBar.progressProperty().bind(task.progressProperty());
+        
+        // remove bindings again
+        task.setOnSucceeded(event -> {
+            statusBar.textProperty().unbind();    
+            statusBar.progressProperty().unbind();
+        });
+
+        new Thread(task).start();
+    }
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloTaskProgressView.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloTaskProgressView.java
new file mode 100644
index 0000000..520f33a
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloTaskProgressView.java
@@ -0,0 +1,206 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples;
+
+import java.util.ArrayList;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import javafx.concurrent.Task;
+import javafx.geometry.Pos;
+import javafx.scene.Node;
+import javafx.scene.control.Button;
+import javafx.scene.control.CheckBox;
+import javafx.scene.effect.DropShadow;
+import javafx.scene.layout.StackPane;
+import javafx.scene.layout.VBox;
+import javafx.scene.paint.Color;
+import javafx.stage.Stage;
+import javafx.util.Callback;
+
+import org.controlsfx.ControlsFXSample;
+import org.controlsfx.control.TaskProgressView;
+import org.controlsfx.glyphfont.FontAwesome;
+import org.controlsfx.glyphfont.FontAwesome.Glyph;
+
+public class HelloTaskProgressView extends ControlsFXSample {
+
+    private ExecutorService executorService = Executors.newCachedThreadPool();
+
+    private TaskProgressView<MyTask> taskProgressView;
+
+    private FontAwesome fontAwesome = new FontAwesome();
+
+    private Callback<MyTask, Node> factory;
+
+    public static void main(String[] args) {
+        launch(args);
+    }
+
+    @Override
+    public String getSampleName() {
+        return "TaskProgressView";
+    }
+
+    @Override
+    public String getJavaDocURL() {
+        return Utils.JAVADOC_BASE
+                + "org/controlsfx/control/TaskProgressView.html";
+    }
+
+    @Override
+    public Node getPanel(Stage stage) {
+        taskProgressView = new TaskProgressView<MyTask>();
+
+        factory = task -> {
+
+            org.controlsfx.glyphfont.Glyph result = null;
+            switch (task.getType()) {
+            case TYPE1:
+                result = fontAwesome.create(Glyph.MOBILE_PHONE).size(24)
+                        .color(Color.RED);
+                break;
+            case TYPE2:
+                result = fontAwesome.create(Glyph.COMPASS).size(24)
+                        .color(Color.GREEN);
+                break;
+            case TYPE3:
+                result = fontAwesome.create(Glyph.APPLE).size(24)
+                        .color(Color.BLUE);
+                break;
+            default:
+            }
+
+            if (result != null) {
+                result.setEffect(new DropShadow(8, Color.GRAY));
+                result.setAlignment(Pos.CENTER);
+
+                /*
+                 * We have to make sure all glyps have the same size. Otherwise
+                 * the progress cells will not be aligned properly.
+                 */
+                result.setPrefSize(24, 24);
+            }
+
+            return result;
+        };
+
+        StackPane stackPane = new StackPane();
+        stackPane.setStyle("-fx-border-color: black; -fx-border-insets: 40;");
+        stackPane.getChildren().add(taskProgressView);
+        StackPane.setAlignment(taskProgressView, Pos.CENTER);
+
+        return stackPane;
+    }
+
+    @Override
+    public String getSampleDescription() {
+        return "The task progress view lists running tasks and displays their progress and status. "
+                + "The view can have an optional title, a 'cancel all' button and a task counter.";
+    }
+
+    @Override
+    public Node getControlPanel() {
+        VBox box = new VBox();
+        box.setSpacing(10);
+
+        Button startTask = new Button("Start Task");
+        startTask.setOnAction(evt -> startTask());
+        box.getChildren().add(startTask);
+
+        CheckBox useFactory = new CheckBox("Use Graphics Factory");
+        useFactory.setOnAction(evt -> {
+            /*
+             * Cancel all tasks before changing the factory.
+             */
+            (new ArrayList<>(taskProgressView.getTasks())).forEach(task -> task.cancel());
+            if (useFactory.isSelected()) {
+                taskProgressView.setGraphicFactory(factory);
+            } else {
+                taskProgressView.setGraphicFactory(null);
+            }
+        });
+
+        box.getChildren().add(useFactory);
+
+        return box;
+    }
+
+    private int taskCounter;
+
+    private void startTask() {
+        taskCounter++;
+
+        MyTask task = new MyTask("Task #" + taskCounter);
+
+        // add to the UI
+        taskProgressView.getTasks().add(task);
+
+        // execute task
+        executorService.submit(task);
+    }
+
+    enum TaskType {
+        TYPE1, TYPE2, TYPE3;
+    }
+
+    class MyTask extends Task<Void> {
+        private TaskType type;
+
+        public MyTask(String title) {
+            updateTitle(title);
+
+            type = TaskType.values()[(int) (Math.random() * 3)];
+        }
+
+        public TaskType getType() {
+            return type;
+        }
+
+        @Override
+        protected Void call() throws Exception {
+
+            if (Math.random() < .3) {
+                updateMessage("First we sleep ....");
+                Thread.sleep(2500);
+            }
+
+            int max = 10000000;
+            for (int i = 0; i < max; i++) {
+                if (isCancelled()) {
+                    break;
+                }
+                updateMessage("Message " + i);
+                updateProgress(i, max);
+            }
+
+            updateProgress(0, 0);
+            done();
+            return null;
+        }
+    }
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloToggleSwitch.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloToggleSwitch.java
new file mode 100644
index 0000000..4fbd1df
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloToggleSwitch.java
@@ -0,0 +1,108 @@
+/**
+ * Copyright (c) 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.controlsfx.samples;
+
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Node;
+import javafx.scene.Parent;
+import javafx.scene.Scene;
+import javafx.scene.control.Label;
+import javafx.scene.layout.AnchorPane;
+import javafx.stage.Stage;
+import org.controlsfx.ControlsFXSample;
+import org.controlsfx.control.ToggleSwitch;
+
+import java.io.IOException;
+
+public class HelloToggleSwitch extends ControlsFXSample
+{
+    static final String RESOURCE = "ToggleSwitch.fxml";
+
+    @Override
+    public String getSampleName()
+    {
+        return "ToggleSwitch";
+    }
+
+    @Override
+    public Node getPanel(Stage stage)
+    {
+        AnchorPane anchorPane = new AnchorPane();
+        anchorPane.setPrefHeight(316);
+        anchorPane.setPrefWidth(444);
+
+        Label headerLabel = new Label("Toggle Switch");
+        headerLabel.getStyleClass().add("header");
+        headerLabel.setLayoutX(44);
+        headerLabel.setLayoutY(34);
+
+        Label itemTitle1 = new Label("Normal unselected");
+        itemTitle1.getStyleClass().add("item-title");
+        itemTitle1.setLayoutX(70);
+        itemTitle1.setLayoutY(145);
+
+        ToggleSwitch toggleSwitch1 = new ToggleSwitch("Off");
+        toggleSwitch1.setLayoutX(70);
+        toggleSwitch1.setLayoutY(168);
+
+        Label itemTitle2 = new Label("Disabled");
+        itemTitle2.getStyleClass().add("item-title");
+        itemTitle2.setLayoutX(271);
+        itemTitle2.setLayoutY(145);
+
+        ToggleSwitch toggleSwitch2 = new ToggleSwitch("Off");
+        toggleSwitch2.setLayoutX(271);
+        toggleSwitch2.setLayoutY(168);
+        toggleSwitch2.setDisable(true);
+
+        Label itemTitle3 = new Label("Normal selected");
+        itemTitle3.getStyleClass().add("item-title");
+        itemTitle3.setLayoutX(70);
+        itemTitle3.setLayoutY(227);
+
+        ToggleSwitch toggleSwitch3 = new ToggleSwitch("On");
+        toggleSwitch3.setLayoutX(70);
+        toggleSwitch3.setLayoutY(250);
+        toggleSwitch3.setSelected(true);
+
+        anchorPane.getChildren().addAll(headerLabel, itemTitle1, toggleSwitch1, itemTitle2, toggleSwitch2, itemTitle3, toggleSwitch3);
+
+        anchorPane.getStylesheets().add(getClass().getResource("toggleSwitchSample.css").toExternalForm());
+        return anchorPane;
+    }
+
+    @Override
+    public String getJavaDocURL()
+    {
+        return Utils.JAVADOC_BASE + "org/controlsfx/control/ToggleSwitch.html";
+    }
+
+    public static void main(String[] args) {
+        launch(args);
+    }
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloValidation.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloValidation.java
new file mode 100644
index 0000000..323f4f2
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/HelloValidation.java
@@ -0,0 +1,250 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples;
+
+import java.time.LocalDate;
+import java.util.Arrays;
+
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.geometry.Insets;
+import javafx.scene.Node;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.ChoiceBox;
+import javafx.scene.control.ColorPicker;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.Control;
+import javafx.scene.control.DatePicker;
+import javafx.scene.control.Label;
+import javafx.scene.control.ListCell;
+import javafx.scene.control.ListView;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.control.Slider;
+import javafx.scene.control.TextField;
+import javafx.scene.control.ToggleButton;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.Priority;
+import javafx.scene.paint.Color;
+import javafx.stage.Stage;
+import javafx.util.Callback;
+
+import org.controlsfx.ControlsFXSample;
+import org.controlsfx.validation.ValidationResult;
+import org.controlsfx.validation.ValidationSupport;
+import org.controlsfx.validation.Validator;
+import org.controlsfx.validation.decoration.CompoundValidationDecoration;
+import org.controlsfx.validation.decoration.GraphicValidationDecoration;
+import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
+import org.controlsfx.validation.decoration.ValidationDecoration;
+
+public class HelloValidation extends ControlsFXSample {
+
+    TextField textField = new TextField();
+
+	
+    @Override public String getSampleName() {
+        return "Component Validation";
+    }
+
+    @Override public String getJavaDocURL() {
+        return Utils.JAVADOC_BASE + "org/controlsfx/validation/ValidationSupport.html";
+    }
+
+    @Override public String getSampleDescription() {
+        return "Component Validation";
+    }
+
+    ValidationSupport validationSupport = new ValidationSupport();
+
+    @Override public Node getPanel(final Stage stage) {
+        GridPane root = new GridPane();
+        root.setVgap(10);
+        root.setHgap(10);
+        root.setPadding(new Insets(30, 30, 0, 30));
+
+        root.sceneProperty().addListener(new InvalidationListener() {
+            @Override public void invalidated(Observable o) {
+                if (root.getScene() != null) {
+                    root.getScene().getStylesheets().add(HelloDecorator.class.getResource("validation.css").toExternalForm());
+                }
+            }
+        });
+
+
+        //        final ListView<ValidationMessage> messageList = new ListView<>();
+        //        validationSupport.validationResultProperty().addListener( (o, oldValue, validationResult) -> {
+        //        	messageList.getItems().setAll(validationResult.getMessages());
+        //        }
+        //        );
+
+
+        int row = 0;
+
+        // text field
+        validationSupport.registerValidator(textField, Validator.createEmptyValidator("Text is required"));
+        root.add(new Label("TextField"), 0, row);
+        root.add(textField, 1, row);
+        GridPane.setHgrow(textField, Priority.ALWAYS);
+
+        //combobox
+        row++;
+        ComboBox<String> combobox = new ComboBox<String>();
+        combobox.getItems().addAll("Item A", "Item B", "Item C");
+        validationSupport.registerValidator(combobox, Validator.createEmptyValidator( "ComboBox Selection required"));
+
+        root.add(new Label("ComboBox"), 0, row);
+        root.add(combobox, 1, row);
+        GridPane.setHgrow(combobox, Priority.ALWAYS);
+
+        //choicebox
+        row++;
+        ChoiceBox<String> choiceBox = new ChoiceBox<String>();
+        choiceBox.getItems().addAll("Item A", "Item B", "Item C");
+        validationSupport.registerValidator(choiceBox, Validator.createEmptyValidator("ChoiceBox Selection required"));
+
+        root.add(new Label("ChoiceBox"), 0, row);
+        root.add(choiceBox, 1, row);
+        GridPane.setHgrow(combobox, Priority.ALWAYS);
+
+        //checkbox
+        row++;
+        CheckBox checkBox = new CheckBox();
+        validationSupport.registerValidator(checkBox, (Control c, Boolean newValue) -> 
+            ValidationResult.fromErrorIf( c, "Checkbox should be checked", !newValue)
+        );
+        root.add(new Label("CheckBox"), 0, row);
+        root.add(checkBox, 1, row);
+        GridPane.setHgrow(checkBox, Priority.ALWAYS);
+
+        //slider
+        row++;
+        Slider slider =  new Slider(-50d, 50d, -10d);
+        slider.setShowTickLabels(true);
+        validationSupport.registerValidator(slider, (Control c, Double newValue) -> 
+            ValidationResult.fromErrorIf( slider, "Slider value should be > 0",  newValue <= 0 ));
+
+        root.add(new Label("Slider"), 0, row);
+        root.add(slider, 1, row);
+        GridPane.setHgrow(checkBox, Priority.ALWAYS);
+
+        // color picker
+        row++;
+        ColorPicker colorPicker =  new ColorPicker(Color.RED);
+        validationSupport.registerValidator(colorPicker, 
+            Validator.createEqualsValidator("Color should be WHITE or BLACK", Arrays.asList(Color.WHITE, Color.BLACK)));
+
+        root.add(new Label("Color Picker"), 0, row);
+        root.add(colorPicker, 1, row);
+        GridPane.setHgrow(checkBox, Priority.ALWAYS);
+
+        // date picker
+        row++;
+        DatePicker datePicker =  new DatePicker();
+        validationSupport.registerValidator(datePicker, false, (Control c, LocalDate newValue) -> 
+            ValidationResult.fromWarningIf( datePicker, "The date should be today", !LocalDate.now().equals(newValue)));
+
+        root.add(new Label("Date Picker"), 0, row);
+        root.add(datePicker, 1, row);
+        GridPane.setHgrow(checkBox, Priority.ALWAYS);
+
+        //        // validation results
+        //        row++;
+        //        TitledPane pane = new TitledPane("Validation Results", messageList);
+        //        pane.setCollapsible(false);
+        //        root.add(pane, 0, row, 2, 1);
+        //        GridPane.setHgrow(pane, Priority.ALWAYS);
+
+        //root.setTop(grid);
+        ScrollPane scrollPane = new ScrollPane(root);
+        return scrollPane;
+    }
+
+
+    @Override public Node getControlPanel() {
+        GridPane grid = new GridPane();
+        grid.setVgap(10);
+        grid.setHgap(10);
+        grid.setPadding(new Insets(30, 30, 0, 30));
+
+        ValidationDecoration iconDecorator = new GraphicValidationDecoration();
+        ValidationDecoration cssDecorator = new StyleClassValidationDecoration();
+        ValidationDecoration compoundDecorator = new CompoundValidationDecoration(cssDecorator, iconDecorator);
+
+        int row = 0;
+        
+        // --- validation decorator
+        Callback<ListView<ValidationDecoration>, ListCell<ValidationDecoration>> cellFactory = listView -> new ListCell<ValidationDecoration>() {
+            @Override protected void updateItem(ValidationDecoration decorator, boolean empty) {
+                super.updateItem(decorator, empty);
+
+                if (empty) {
+                    setText("");
+                } else {
+                    if (decorator instanceof StyleClassValidationDecoration) {
+                        setText("Style Class Validation Decorator");
+                    } else if (decorator instanceof GraphicValidationDecoration) {
+                        setText("Graphic Validation Decorator");
+                    } else if (decorator instanceof CompoundValidationDecoration) {
+                        setText("Compound Validation Decorator");
+                    } else {
+                        setText("Unknown decorator type!");
+                    }
+                }
+            }
+        };
+        ComboBox<ValidationDecoration> decoratorBox = new ComboBox<>();
+        decoratorBox.getItems().addAll(iconDecorator, cssDecorator, compoundDecorator);
+        decoratorBox.setCellFactory(cellFactory);
+        decoratorBox.setButtonCell(cellFactory.call(null));
+        decoratorBox.getSelectionModel().selectedItemProperty().addListener((o,old,decorator) ->
+        	validationSupport.setValidationDecorator(decorator));
+        decoratorBox.getSelectionModel().select(0);
+
+        Label validationDecoratorLabel = new Label("Validation Decorator: ");
+        validationDecoratorLabel.getStyleClass().add("property");
+        grid.add(validationDecoratorLabel, 0, row);
+        grid.add(decoratorBox, 1, row);
+        GridPane.setHgrow(decoratorBox, Priority.ALWAYS);
+        
+        row++;
+        ToggleButton btnToggleRequired = new ToggleButton("Toggle TextField required status");
+        btnToggleRequired.setSelected(ValidationSupport.isRequired(textField));
+        btnToggleRequired.setOnAction(e -> {
+//            boolean required = ValidationSupport.isRequired(textField);
+            System.out.println("Is required: " + btnToggleRequired.isSelected());
+        	ValidationSupport.setRequired(textField, btnToggleRequired.isSelected()); 
+        });
+        grid.add(btnToggleRequired, 1, row, 1, 1);
+
+        return grid;
+    }
+
+    public static void main(String[] args) {
+        launch(args);
+    }
+
+}
\ No newline at end of file
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/Utils.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/Utils.java
new file mode 100644
index 0000000..f93a442
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/Utils.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) 2013, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples;
+
+public class Utils {
+    private Utils() { } 
+    
+    public static final String JAVADOC_BASE = "http://docs.controlsfx.org/";
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/actions/HelloActionGroup.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/actions/HelloActionGroup.java
new file mode 100644
index 0000000..a93b44f
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/actions/HelloActionGroup.java
@@ -0,0 +1,204 @@
+/**
+ * Copyright (c) 2013, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples.actions;
+
+import javafx.beans.property.BooleanProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.event.ActionEvent;
+import javafx.geometry.Insets;
+import javafx.scene.Node;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.Control;
+import javafx.scene.control.Label;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.VBox;
+import javafx.stage.Stage;
+import org.controlsfx.ControlsFXSample;
+import org.controlsfx.control.action.Action;
+import org.controlsfx.control.action.ActionCheck;
+import org.controlsfx.control.action.ActionGroup;
+import org.controlsfx.control.action.ActionUtils;
+import org.controlsfx.control.action.ActionUtils.ActionTextBehavior;
+import org.controlsfx.samples.Utils;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import static org.controlsfx.control.action.ActionUtils.ACTION_SEPARATOR;
+import static org.controlsfx.control.action.ActionUtils.ACTION_SPAN;
+
+public class HelloActionGroup extends ControlsFXSample {
+    
+    private static final ImageView image = new ImageView( new Image("/org/controlsfx/samples/security-low.png"));
+    
+    private Collection<? extends Action> actions = Arrays.asList(
+        new ActionGroup("Group 1", image, new DummyAction("Action 1.1", image), 
+                                          new CheckDummyAction("Action 1.2") ),
+        new ActionGroup("Group 2", image, new DummyAction("Action 2.1"), 
+                                          ACTION_SEPARATOR,
+                                          new ActionGroup("Action 2.2", new DummyAction("Action 2.2.1"), 
+                                                                        new CheckDummyAction("Action 2.2.2")),
+                                          new DummyAction("Action 2.3") ),
+        ACTION_SPAN,
+        ACTION_SEPARATOR,
+        new CheckDummyAction("Action 3", image),
+        new ActionGroup("Group 4",  image, new DummyAction("Action 4.1", image), 
+                                           new CheckDummyAction("Action 4.2"))
+    );
+    
+    private static class DummyAction extends Action {
+        public DummyAction(String name, Node image) {
+            super(name);
+            setGraphic(image);
+            setEventHandler(ae -> String.format("Action '%s' is executed", getText()) );
+        }
+        
+        public DummyAction( String name ) {
+            super(name);
+        }
+
+        @Override public String toString() {
+            return getText();
+        }
+    }
+
+    @ActionCheck
+    private static class CheckDummyAction extends Action {
+        public CheckDummyAction(String name, Node image) {
+            super(name);
+            setGraphic(image);
+            setEventHandler(ae -> String.format("Action '%s' is executed", getText()) );
+        }
+
+        public CheckDummyAction( String name ) {
+            super(name);
+        }
+
+        @Override public String toString() {
+            return getText();
+        }
+    }
+    
+    private ObservableList<Action> flatten( Collection<? extends Action> actions, ObservableList<Action> dest ) {
+        for (Action a : actions) {
+           if ( a == null || a == ActionUtils.ACTION_SEPARATOR ) continue;
+           dest.add(a); 
+           if ( a instanceof ActionGroup ) {
+               flatten( ((ActionGroup)a).getActions(), dest);
+           }
+        }
+        
+        return dest;
+    }
+    
+    @Override public String getSampleName() {
+        return "Action Group";
+    }
+    
+    @Override public String getJavaDocURL() {
+        return Utils.JAVADOC_BASE + "org/controlsfx/control/action/ActionGroup.html";
+    }
+    
+    @Override public String getSampleDescription() {
+        return "MenuBar, ToolBar and ContextMenu presented here are effortlessly built out of the same action tree. " +
+                "Action properties can be dynamically changed, triggering changes in all related controls";
+    }
+    
+    @Override public Node getControlPanel() {
+        GridPane grid = new GridPane();
+        grid.setVgap(10);
+        grid.setHgap(10);
+        grid.setPadding(new Insets(30, 30, 0, 30));
+
+        int row = 0;
+
+        // Dynamically enable/disable action
+        Label lblAddCrumb = new Label("Dynamically enable/disable action: ");
+        lblAddCrumb.getStyleClass().add("property");
+        grid.add(lblAddCrumb, 0, row);
+        final ComboBox<Action> cbActions = new ComboBox<>(flatten( actions, FXCollections.<Action>observableArrayList()));
+        cbActions.getSelectionModel().select(0);
+        grid.add(cbActions, 1, row);
+        Action toggleAction = new Action("Enable/Disable") {
+            { setEventHandler(this::handleAction); }
+
+            private void handleAction(ActionEvent ae) {
+               Action action = cbActions.getSelectionModel().getSelectedItem();
+               if ( action != null ) {
+                   BooleanProperty p = action.disabledProperty();
+                   p.set(!p.get());
+               }
+            }
+        };
+        grid.add(ActionUtils.createButton(toggleAction), 2, row++);
+        
+        return grid;
+    }
+    
+    @Override public Node getPanel(final Stage stage) {
+        VBox root = new VBox(10);
+        root.setPadding(new Insets(10, 10, 10, 10));
+        root.setMaxHeight(Double.MAX_VALUE);
+        
+        Insets topMargin = new Insets(7, 7, 0, 7);
+        Insets margin = new Insets(0, 7, 7, 7);
+        
+        addWithMargin(root, new Label("MenuBar:"), topMargin ).setStyle("-fx-font-weight: bold;");
+        addWithMargin(root, ActionUtils.createMenuBar(actions), margin);
+
+        addWithMargin(root,new Label("ToolBar (with text on controls):"), topMargin).setStyle("-fx-font-weight: bold;");
+        addWithMargin(root, ActionUtils.createToolBar(actions, ActionTextBehavior.SHOW), margin);
+
+        addWithMargin(root,new Label("ToolBar (no text on controls):"), topMargin).setStyle("-fx-font-weight: bold;");
+        addWithMargin(root, ActionUtils.createToolBar(actions, ActionTextBehavior.HIDE), margin);
+        
+        addWithMargin(root, new Label("ContextMenu:"), topMargin).setStyle("-fx-font-weight: bold;");
+        Label context = new Label("Right-click to see the context menu");
+        addWithMargin(root,context, margin);
+        context.setContextMenu(ActionUtils.createContextMenu(actions)); 
+        context.setStyle("-fx-background-color: #E0E0E0 ;-fx-border-color: black;-fx-border-style: dotted");
+        context.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
+        VBox.setVgrow(context, Priority.ALWAYS);
+        VBox.setVgrow(root, Priority.ALWAYS);
+        
+        return root;
+    }
+    
+    private Control addWithMargin( VBox parent, Control control, Insets insets) {
+    	parent.getChildren().add(control);
+    	VBox.setMargin(control, insets);
+    	return control;
+    }
+     
+    public static void main(String[] args) {
+        launch(args);
+    }
+}
\ No newline at end of file
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/actions/HelloActionProxy.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/actions/HelloActionProxy.java
new file mode 100644
index 0000000..68c9d01
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/actions/HelloActionProxy.java
@@ -0,0 +1,224 @@
+/**
+ * Copyright (c) 2013, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples.actions;
+
+import javafx.beans.property.BooleanProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.event.ActionEvent;
+import javafx.geometry.Insets;
+import javafx.scene.Node;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.Control;
+import javafx.scene.control.Label;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.VBox;
+import javafx.stage.Stage;
+import org.controlsfx.ControlsFXSample;
+import org.controlsfx.control.action.*;
+import org.controlsfx.control.action.ActionUtils.ActionTextBehavior;
+import org.controlsfx.samples.Utils;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import static org.controlsfx.control.action.ActionMap.action;
+import static org.controlsfx.control.action.ActionMap.actions;
+import static org.controlsfx.control.action.ActionUtils.ACTION_SEPARATOR;
+import static org.controlsfx.control.action.ActionUtils.ACTION_SPAN;
+
+public class HelloActionProxy extends ControlsFXSample {
+    
+    private static final String imagePath = "/org/controlsfx/samples/security-low.png";
+    private static final ImageView image = new ImageView(new Image(imagePath));
+    
+    private Collection<? extends Action> actions;
+    
+    public HelloActionProxy() {
+		ActionMap.register(this);
+		actions = Arrays.asList(
+	        new ActionGroup("Group 1", image, actions("action11","action12") ),
+			new ActionGroup("Group 2", image, actions("action21","---","action22", "action221","action222","action23") ),
+            ACTION_SPAN,
+            ACTION_SEPARATOR,
+			action("action3"),
+			new ActionGroup("Group 4",  image, actions("action41","action42"))
+		);
+	}
+    
+    @ActionProxy(text="Action 1.1", graphic=imagePath, accelerator="ctrl+shift+T")
+    private void action11() {
+    	 System.out.println( "Action 1.1 is executed");
+    }
+
+    @ActionCheck
+    @ActionProxy(text="Action 1.2", graphic="http://icons.iconarchive.com/icons/custom-icon-design/mini-3/16/teacher-male-icon.png") 
+    private void action12() {
+    	 System.out.println( "Action 1.2 is executed");
+    }
+    
+    @ActionProxy(text="Action 2.1", graphic=imagePath, factory="org.controlsfx.samples.actions.HelloCustomActionFactory")
+    private void action21() {
+    	 System.out.println( "Action 2.1 is executed (and used a custom action factory)");
+    }
+    
+    @ActionProxy(text="Action 2.2", graphic=imagePath)
+    private void action22( ActionEvent evt ) {
+    	 System.out.println( "Action 2.2 is executed (and received an ActionEvent)");
+    }
+    
+    @ActionProxy(text="Action 2.2.1", graphic=imagePath)
+    private void action221( ActionEvent evt, Action action ) {
+    	 System.out.println( "Action 2.2.1 is executed (and received both an ActionEvent and an Action)");
+    }
+    
+    @ActionProxy(text="Action 2.2.2", graphic=imagePath)
+    private void action222() {
+    	 System.out.println( "Action 2.2.2 is executed");
+    }   
+    
+    @ActionProxy(text="Action 2.3", graphic=imagePath)
+    private void action23() {
+    	 System.out.println( "Action 2.3 is executed");
+    }
+
+    @ActionCheck
+    @ActionProxy(text="Action 3", graphic="font>FontAwesome|STAR")
+    private void action3() {
+    	 System.out.println( "Action 3 is executed");
+    }    
+    
+    @ActionProxy(text="Action 4.1", graphic=imagePath)
+    private void action41() {
+    	 System.out.println( "Action 4.1 is executed");
+    }
+    
+    @ActionProxy(text="Action 4.2", graphic=imagePath)
+    private void action42() {
+    	 System.out.println( "Action 4.2 is executed");
+    }    
+    
+    private ObservableList<Action> flatten( Collection<? extends Action> actions, ObservableList<Action> dest ) {
+        
+        for (Action a : actions) {
+           if ( a == null || a == ActionUtils.ACTION_SEPARATOR ) continue;
+           dest.add(a); 
+           if ( a instanceof ActionGroup ) {
+               flatten( ((ActionGroup)a).getActions(), dest);
+           }
+        }
+        
+        return dest;
+    }
+    
+    
+    @Override public String getSampleName() {
+        return "Action Proxy";
+    }
+    
+    @Override public String getJavaDocURL() {
+        return Utils.JAVADOC_BASE + "org/controlsfx/control/action/ActionProxy.html";
+    }
+    
+    @Override public String getSampleDescription() {
+        return "MenuBar, ToolBar and ContextMenu presented here are effortlessly built out of the same action tree. " +
+                "Action properties can be dynamically changed, triggering changes in all related controls";
+    }
+    
+    @Override public Node getControlPanel() {
+        GridPane grid = new GridPane();
+        grid.setVgap(10);
+        grid.setHgap(10);
+        grid.setPadding(new Insets(30, 30, 0, 30));
+
+        int row = 0;
+
+        // Dynamically enable/disable action
+        Label lblAddCrumb = new Label("Dynamically enable/disable action: ");
+        lblAddCrumb.getStyleClass().add("property");
+        grid.add(lblAddCrumb, 0, row);
+        final ComboBox<Action> cbActions = new ComboBox<>(flatten( actions, FXCollections.<Action>observableArrayList()));
+        cbActions.getSelectionModel().select(0);
+        grid.add(cbActions, 1, row);
+        Action toggleAction = new Action("Enable/Disable") {
+            { setEventHandler(this::handleAction); }
+
+            private void handleAction(ActionEvent ae) {
+               Action action = cbActions.getSelectionModel().getSelectedItem();
+               if ( action != null ) {
+                   BooleanProperty p = action.disabledProperty();
+                   p.set(!p.get());
+               }
+            }
+        };
+        grid.add(ActionUtils.createButton(toggleAction), 2, row++);
+        
+        return grid;
+    }
+    
+    @Override public Node getPanel(final Stage stage) {
+        VBox root = new VBox(10);
+        root.setPadding(new Insets(10, 10, 10, 10));
+        root.setMaxHeight(Double.MAX_VALUE);
+        
+        Insets topMargin = new Insets(7, 7, 0, 7);
+        Insets margin = new Insets(0, 7, 7, 7);
+        
+        addWithMargin(root, new Label("MenuBar:"), topMargin ).setStyle("-fx-font-weight: bold;");
+        addWithMargin(root, ActionUtils.createMenuBar(actions), margin);
+
+        addWithMargin(root,new Label("ToolBar (with text on controls):"), topMargin).setStyle("-fx-font-weight: bold;");
+        addWithMargin(root, ActionUtils.createToolBar(actions, ActionTextBehavior.SHOW), margin);
+
+        addWithMargin(root,new Label("ToolBar (no text on controls):"), topMargin).setStyle("-fx-font-weight: bold;");
+        addWithMargin(root, ActionUtils.createToolBar(actions, ActionTextBehavior.HIDE), margin);
+        
+        addWithMargin(root, new Label("ContextMenu:"), topMargin).setStyle("-fx-font-weight: bold;");
+        Label context = new Label("Right-click to see the context menu");
+        addWithMargin(root,context, margin);
+        context.setContextMenu(ActionUtils.createContextMenu(actions)); 
+        context.setStyle("-fx-background-color: #E0E0E0 ;-fx-border-color: black;-fx-border-style: dotted");
+        context.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
+        VBox.setVgrow(context, Priority.ALWAYS);
+        VBox.setVgrow(root, Priority.ALWAYS);
+        
+        return root;
+    }
+    
+    private Control addWithMargin( VBox parent, Control control, Insets insets) {
+    	parent.getChildren().add(control);
+    	VBox.setMargin(control, insets);
+    	return control;
+    }
+     
+    public static void main(String[] args) {
+        launch(args);
+    }
+}
\ No newline at end of file
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/actions/HelloCustomAction.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/actions/HelloCustomAction.java
new file mode 100644
index 0000000..3d906bc
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/actions/HelloCustomAction.java
@@ -0,0 +1,60 @@
+/**
+ * Copyright (c) 2013, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples.actions;
+
+import java.lang.reflect.Method;
+import javafx.event.ActionEvent;
+import org.controlsfx.control.action.AnnotatedAction;
+
+
+
+public class HelloCustomAction extends AnnotatedAction {
+
+    public HelloCustomAction( String text, Method method, Object target ) {
+        super( text, method, target );
+    }
+
+
+    @Override
+    protected void handleAction( ActionEvent ae ) {
+        // ... add custom logic before handling the action
+        System.out.println( "About to handle action" );
+        
+        super.handleAction( ae );
+        
+        // ... add custom logic after handling the action
+        System.out.println( "Finished handling action" );
+    }
+
+
+    @Override
+    protected void handleActionException( ActionEvent ae, Throwable ex ) {
+        // ... do something custom with an exception that was thrown during action handling
+        System.err.println( "Shouldn't you be doing something more than just printing the stack trace?" );
+        ex.printStackTrace();
+    }
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/actions/HelloCustomActionFactory.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/actions/HelloCustomActionFactory.java
new file mode 100644
index 0000000..a6ebbc9
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/actions/HelloCustomActionFactory.java
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2013, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples.actions;
+
+import java.lang.reflect.Method;
+import org.controlsfx.control.action.ActionProxy;
+import org.controlsfx.control.action.AnnotatedAction;
+import org.controlsfx.control.action.DefaultActionFactory;
+
+/**
+ *
+ */
+public class HelloCustomActionFactory extends DefaultActionFactory {
+
+    @Override
+    public AnnotatedAction createAction( ActionProxy annotation, Method method, Object target ) {
+        AnnotatedAction action = new HelloCustomAction( annotation.text(), method, target );
+        
+        configureAction( annotation, action );
+        
+        return action;
+    }
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/button/HelloBreadCrumbBar.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/button/HelloBreadCrumbBar.java
new file mode 100644
index 0000000..103ed3b
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/button/HelloBreadCrumbBar.java
@@ -0,0 +1,144 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples.button;
+
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.geometry.Insets;
+import javafx.scene.Node;
+import javafx.scene.control.Button;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.Label;
+import javafx.scene.control.TreeItem;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.VBox;
+import javafx.stage.Stage;
+
+import org.controlsfx.ControlsFXSample;
+import org.controlsfx.control.BreadCrumbBar;
+import org.controlsfx.control.BreadCrumbBar.BreadCrumbActionEvent;
+import org.controlsfx.samples.Utils;
+
+public class HelloBreadCrumbBar extends ControlsFXSample {
+
+    private BreadCrumbBar<String> sampleBreadCrumbBar;
+    private final Label selectedCrumbLbl = new Label();
+    
+    private int newCrumbCount = 0;
+
+    @Override public String getSampleName() {
+        return "BreadCrumbBar";
+    }
+
+    @Override public String getJavaDocURL() {
+        return Utils.JAVADOC_BASE + "org/controlsfx/control/BreadCrumbBar.html";
+    }
+
+    @Override public String getSampleDescription() {
+        return "The BreadCrumbBar provides an easy way to navigate hirarchical structures " +
+                "such as file systems.";
+    }
+
+    @Override public Node getPanel(final Stage stage) {
+        VBox root = new VBox(10);
+        root.setPadding(new Insets(10));
+
+        sampleBreadCrumbBar = new BreadCrumbBar<>();
+        resetModel();
+
+        root.getChildren().add(sampleBreadCrumbBar);
+        BorderPane.setMargin(sampleBreadCrumbBar, new Insets(20));
+
+        sampleBreadCrumbBar.setOnCrumbAction(new EventHandler<BreadCrumbBar.BreadCrumbActionEvent<String>>() {
+            @Override public void handle(BreadCrumbActionEvent<String> bae) {
+                selectedCrumbLbl.setText("You just clicked on '" + bae.getSelectedCrumb() + "'!");
+            }
+        });
+        
+        root.getChildren().add(selectedCrumbLbl);
+
+        return root;
+    }
+
+    @Override public Node getControlPanel() {
+        GridPane grid = new GridPane();
+        grid.setVgap(10);
+        grid.setHgap(10);
+        grid.setPadding(new Insets(30, 30, 0, 30));
+
+        int row = 0;
+
+        // add crumb
+        Label lblAddCrumb = new Label("Add crumb: ");
+        lblAddCrumb.getStyleClass().add("property");
+        grid.add(lblAddCrumb, 0, row);
+        Button btnAddCrumb = new Button("Press");
+        btnAddCrumb.setOnAction(new EventHandler<ActionEvent>() {
+            @Override public void handle(ActionEvent ae) {
+                // Construct a new leaf node and append it to the previous leaf
+                TreeItem<String> leaf = new TreeItem<>("New Crumb #" + newCrumbCount++);
+                sampleBreadCrumbBar.getSelectedCrumb().getChildren().add(leaf);
+                sampleBreadCrumbBar.setSelectedCrumb(leaf);
+            }
+        });
+        grid.add(btnAddCrumb, 1, row++);
+        
+        // reset
+        Label lblReset = new Label("Reset model: ");
+        lblReset.getStyleClass().add("property");
+        grid.add(lblReset, 0, row);
+        Button btnReset = new Button("Press");
+        btnReset.setOnAction(new EventHandler<ActionEvent>() {
+            @Override public void handle(ActionEvent ae) {
+                resetModel();
+            }
+        });
+        grid.add(btnReset, 1, row++);
+        
+        // auto navigation
+        Label lblAutoNavigation = new Label("Enable auto navigation: ");
+        lblAutoNavigation.getStyleClass().add("property");
+        grid.add(lblAutoNavigation, 0, row);
+        CheckBox chkAutoNav = new CheckBox();
+        chkAutoNav.selectedProperty().bindBidirectional(sampleBreadCrumbBar.autoNavigationEnabledProperty());
+        grid.add(chkAutoNav, 1, row++);
+
+        return grid;
+    }
+    
+    private void resetModel() {
+        TreeItem<String> model = BreadCrumbBar.buildTreeModel("Hello", "World", "This", "is", "cool");
+        sampleBreadCrumbBar.setSelectedCrumb(model);
+    }
+
+
+    public static void main(String[] args) {
+        launch(args);
+    }
+
+}
\ No newline at end of file
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/button/HelloButtonBar.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/button/HelloButtonBar.java
new file mode 100644
index 0000000..3c915e8
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/button/HelloButtonBar.java
@@ -0,0 +1,198 @@
+/**
+ * Copyright (c) 2013, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples.button;
+
+import javafx.beans.binding.StringBinding;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.FXCollections;
+import javafx.geometry.Insets;
+import javafx.scene.Node;
+import javafx.scene.control.Button;
+import javafx.scene.control.ButtonBar;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.Label;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.control.Slider;
+import javafx.scene.control.ToggleButton;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.VBox;
+import javafx.stage.Stage;
+
+import org.controlsfx.ControlsFXSample;
+import org.controlsfx.control.SegmentedButton;
+import org.controlsfx.samples.Utils;
+
+public class HelloButtonBar extends ControlsFXSample {
+    
+    private ButtonBar buttonBar;
+
+    @Override public String getSampleName() {
+        return "ButtonBar";
+    }
+    
+    @Override public String getJavaDocURL() {
+        return Utils.JAVADOC_BASE + "org/controlsfx/control/ButtonBar.html";
+    }
+    
+    @Override public String getSampleDescription() {
+        return "The ButtonBar allows for buttons to be positioned" +
+                " in a way that is OS-specific (or in any way that suits your use case." +
+                " For example, try toggling the OS toggle buttons below (note, you'll want " +
+                "to increase the width of this window first!)";
+    }
+    
+    @Override public Node getPanel(final Stage stage) {
+        VBox root = new VBox(10);
+        root.setPadding(new Insets(10, 10, 10, 10));
+        root.setMaxHeight(Double.MAX_VALUE);
+        
+        buttonBar = new ButtonBar();
+
+        // spacer to push button bar to bottom
+        Region spacer = new Region();
+        VBox.setVgrow(spacer, Priority.ALWAYS);
+        root.getChildren().add(spacer);
+        
+        // create button bar
+        buttonBar.getButtons().addAll(
+            createButton("OK", ButtonBar.ButtonData.OK_DONE),
+            createButton("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE),
+            createButton("Left 1", ButtonBar.ButtonData.LEFT),
+            createButton("Left 2", ButtonBar.ButtonData.LEFT),
+            createButton("Left 3", ButtonBar.ButtonData.LEFT),
+            createButton("Right 1", ButtonBar.ButtonData.RIGHT),
+            createButton("Unknown 1", ButtonBar.ButtonData.OTHER),
+            createButton("Help(R)", ButtonBar.ButtonData.HELP),
+            createButton("Help(L)", ButtonBar.ButtonData.HELP_2),
+            createButton("Unknown 2 xxxxxxxxxx", ButtonBar.ButtonData.OTHER),
+            createButton("Yes", ButtonBar.ButtonData.YES),
+            createButton("No", ButtonBar.ButtonData.NO),
+            createButton("Next", ButtonBar.ButtonData.NEXT_FORWARD),
+            createButton("Unknown 3", ButtonBar.ButtonData.OTHER),
+            createButton("Back", ButtonBar.ButtonData.BACK_PREVIOUS),
+            createButton("Right 2", ButtonBar.ButtonData.RIGHT),
+            createButton("Finish", ButtonBar.ButtonData.FINISH),
+            createButton("Right 3", ButtonBar.ButtonData.RIGHT),
+            createButton("Apply", ButtonBar.ButtonData.APPLY)
+        );
+        
+        // put the ButtonBar inside a ScrollPane so that the user can scroll horizontally
+        // when the button width is large
+        ScrollPane sp = new ScrollPane(buttonBar);
+        sp.setStyle("-fx-background-color: -fx-background; -fx-background-insets: 0");
+        
+        root.getChildren().add(sp);
+        VBox.setVgrow(sp, Priority.ALWAYS);
+        
+        return root;
+    }
+    
+    @Override public Node getControlPanel() {
+        GridPane grid = new GridPane();
+        grid.setVgap(10);
+        grid.setHgap(10);
+        grid.setPadding(new Insets(30, 30, 0, 30));
+        
+        int row = 0;
+
+        // operating system button order
+        Label osChoiceLabel = new Label("Operating system button order: ");
+        osChoiceLabel.getStyleClass().add("property");
+        grid.add(osChoiceLabel, 0, 0);
+        final ToggleButton windowsBtn = createToggle("Windows", buttonBar, ButtonBar.BUTTON_ORDER_WINDOWS);
+        final ToggleButton macBtn = createToggle("Mac OS", buttonBar, ButtonBar.BUTTON_ORDER_MAC_OS);
+        final ToggleButton linuxBtn = createToggle("Linux", buttonBar, ButtonBar.BUTTON_ORDER_LINUX);
+        windowsBtn.selectedProperty().set(true);
+        SegmentedButton operatingSystem = new SegmentedButton(
+                FXCollections.observableArrayList(windowsBtn, macBtn, linuxBtn));
+        grid.add(operatingSystem, 1, row);
+        row++;
+        
+        // uniform size
+        Label uniformSizeLabel = new Label("Set all buttons to a uniform size: ");
+        uniformSizeLabel.getStyleClass().add("property");
+        grid.add(uniformSizeLabel, 0, row);
+        final CheckBox uniformButtonBtn = new CheckBox();
+        uniformButtonBtn.selectedProperty().addListener(new ChangeListener<Boolean>() {
+
+            @Override
+            public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean isUniform) {
+                for(Node button :buttonBar.getButtons()){
+                    ButtonBar.setButtonUniformSize(button, isUniform);
+                }
+            }
+        });
+//        uniformButtonBtn.selectedProperty().bindBidirectional( buttonBar.buttonUniformSizeProperty());
+        grid.add(uniformButtonBtn, 1, row);
+        row++;
+        
+        // minimum size slider / label
+        Label minSizeLabel = new Label("Button min size: ");
+        minSizeLabel.getStyleClass().add("property");
+        grid.add(minSizeLabel, 0, row);
+        
+        final Slider minSizeSlider = new Slider(0, 200, 0);
+        Label pixelCountLabel = new Label();
+        pixelCountLabel.textProperty().bind(new StringBinding() {
+            { bind(minSizeSlider.valueProperty()); }
+            
+            @Override protected String computeValue() {
+                return (int)minSizeSlider.getValue() + "px";
+            }
+        });
+        HBox minSizeBox = new HBox(10, minSizeSlider, pixelCountLabel);
+        buttonBar.buttonMinWidthProperty().bind(minSizeSlider.valueProperty());
+        grid.add(minSizeBox, 1, row);
+        row++;
+        
+        return grid;
+    }
+    
+    private ToggleButton createToggle( final String caption, final ButtonBar buttonBar, final String buttonBarOrder ) {
+        final ToggleButton btn = new ToggleButton(caption);
+        btn.selectedProperty().addListener(new ChangeListener<Boolean>() {
+            @Override public void changed(ObservableValue<? extends Boolean> arg0, Boolean oldValue, Boolean newValue) {
+                if ( newValue) buttonBar.setButtonOrder(buttonBarOrder);
+            }});
+        return btn;
+    }
+     
+    private Button createButton( String title, ButtonBar.ButtonData buttonData) {
+        Button button = new Button(title);
+        ButtonBar.setButtonData(button, buttonData);
+        return button;
+    }
+    
+    public static void main(String[] args) {
+        launch(args);
+    }
+    
+}
\ No newline at end of file
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/button/HelloSegmentedButton.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/button/HelloSegmentedButton.java
new file mode 100644
index 0000000..7ebc3c5
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/button/HelloSegmentedButton.java
@@ -0,0 +1,147 @@
+/**
+ * Copyright (c) 2013, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples.button;
+
+import javafx.geometry.Insets;
+import javafx.scene.Node;
+import javafx.scene.control.Label;
+import javafx.scene.control.ToggleButton;
+import javafx.scene.control.ToggleGroup;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.HBox;
+import javafx.stage.Stage;
+
+import org.controlsfx.ControlsFXSample;
+import org.controlsfx.control.SegmentedButton;
+import org.controlsfx.samples.Utils;
+
+public class HelloSegmentedButton extends ControlsFXSample {
+    
+    @Override public String getSampleName() {
+        return "SegmentedButton";
+    }
+    
+    @Override public String getJavaDocURL() {
+        return Utils.JAVADOC_BASE + "org/controlsfx/control/SegmentedButton.html";
+    }
+    
+    
+    @Override
+    public String getControlStylesheetURL() {
+    	return "/org/controlsfx/control/segmentedbutton.css";
+    }
+    
+    @Override public Node getPanel(Stage stage) {
+        GridPane grid = new GridPane();
+        grid.setVgap(10);
+        grid.setHgap(10);
+        grid.setPadding(new Insets(30, 30, 0, 30));
+        
+        // without segmented button
+        grid.add(new Label("Without SegmentedButton (with 10px spacing): "), 0, 0);
+        
+        ToggleButton without_b1 = new ToggleButton("day"); 
+        ToggleButton without_b2 = new ToggleButton("week");
+        ToggleButton without_b3 = new ToggleButton("month");
+        ToggleButton without_b4 = new ToggleButton("year");
+        
+        final ToggleGroup group = new ToggleGroup();
+        group.getToggles().addAll(without_b1, without_b2, without_b3, without_b4);
+        
+        HBox toggleButtons = new HBox(without_b1, without_b2, without_b3, without_b4);
+        toggleButtons.setSpacing(10);
+        grid.add(toggleButtons, 1, 0);
+        
+        
+        // Using modena segmented button
+        grid.add(new Label("With SegmentedButton (with default (modena) styling): "), 0, 1);
+        
+        ToggleButton modena_b1 = new ToggleButton("day");
+        ToggleButton modena_b2 = new ToggleButton("week");
+        ToggleButton modena_b3 = new ToggleButton("month");
+        ToggleButton modena_b4 = new ToggleButton("year");
+        SegmentedButton segmentedButton_modena = new SegmentedButton(modena_b1, modena_b2, modena_b3, modena_b4); 
+        grid.add(segmentedButton_modena, 1, 1);
+        
+        
+        // with segmented button and dark styling
+        grid.add(new Label("With SegmentedButton (using dark styling): "), 0, 2);
+        
+        ToggleButton dark_b1 = new ToggleButton("day");
+        ToggleButton dark_b2 = new ToggleButton("week");
+        ToggleButton dark_b3 = new ToggleButton("month");
+        ToggleButton dark_b4 = new ToggleButton("year");
+        
+        SegmentedButton segmentedButton_dark = new SegmentedButton(dark_b1, dark_b2, dark_b3, dark_b4);   
+        segmentedButton_dark.getStyleClass().add(SegmentedButton.STYLE_CLASS_DARK);
+        grid.add(segmentedButton_dark, 1, 2);
+        
+        
+        // without toggle group
+        grid.add(new Label("SegmentedButton without a ToggleGroup (multiple selection): "), 0, 3);
+        
+        ToggleButton nogrp_b1 = new ToggleButton("day");
+        ToggleButton nogrp_b2 = new ToggleButton("week");
+        ToggleButton nogrp_b3 = new ToggleButton("month");
+        ToggleButton nogrp_b4 = new ToggleButton("year");
+        
+        SegmentedButton segmentedButton_nogrp = new SegmentedButton(nogrp_b1, nogrp_b2, nogrp_b3, nogrp_b4);
+        segmentedButton_nogrp.setToggleGroup(null);
+        grid.add(segmentedButton_nogrp, 1, 3);
+        
+        
+        // combined toggle group
+        grid.add(new Label("SegmentedButtons with a combined ToggleGroup: "), 0, 4);
+        ToggleGroup combgrp_grp = new ToggleGroup();
+        
+        ToggleButton combgrp_a_b1 = new ToggleButton("day");
+        ToggleButton combgrp_a_b2 = new ToggleButton("week");
+        ToggleButton combgrp_a_b3 = new ToggleButton("month");
+        ToggleButton combgrp_a_b4 = new ToggleButton("year");
+        
+        SegmentedButton segmentedButton_combgrp_a = new SegmentedButton(combgrp_a_b1, combgrp_a_b2, combgrp_a_b3, combgrp_a_b4);
+        segmentedButton_combgrp_a.setToggleGroup(combgrp_grp);
+        
+        ToggleButton combgrp_b_b1 = new ToggleButton("hour");
+        ToggleButton combgrp_b_b2 = new ToggleButton("minute");
+        ToggleButton combgrp_b_b3 = new ToggleButton("second");
+        
+        SegmentedButton segmentedButton_combgrp_b = new SegmentedButton(combgrp_b_b1, combgrp_b_b2, combgrp_b_b3);
+        segmentedButton_combgrp_b.setToggleGroup(combgrp_grp);
+        
+        HBox combgrp_pane = new HBox(5);
+        combgrp_pane.getChildren().addAll(segmentedButton_combgrp_a, segmentedButton_combgrp_b);
+        grid.add(combgrp_pane, 1, 4);
+        
+
+        return grid;
+    }
+    
+    public static void main(String[] args) {
+        launch(args);
+    }
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/checked/HelloCheckComboBox.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/checked/HelloCheckComboBox.java
new file mode 100644
index 0000000..8019dc0
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/checked/HelloCheckComboBox.java
@@ -0,0 +1,239 @@
+/**
+ * Copyright (c) 2013, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples.checked;
+
+import javafx.beans.binding.Bindings;
+import javafx.beans.property.ReadOnlyStringProperty;
+import javafx.beans.property.ReadOnlyStringWrapper;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.geometry.Insets;
+import javafx.scene.Node;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.Label;
+import javafx.scene.layout.GridPane;
+import javafx.stage.Stage;
+import javafx.util.StringConverter;
+
+import org.controlsfx.ControlsFXSample;
+import org.controlsfx.control.CheckComboBox;
+import org.controlsfx.control.IndexedCheckModel;
+import org.controlsfx.samples.Utils;
+
+public class HelloCheckComboBox extends ControlsFXSample {
+    
+    private final Label checkedItemsLabel = new Label();
+    private CheckComboBox<String> checkComboBox;
+    
+    @Override public String getSampleName() {
+        return "CheckComboBox";
+    }
+    
+    @Override public String getJavaDocURL() {
+        return Utils.JAVADOC_BASE + "org/controlsfx/control/CheckComboBox.html";
+    }
+    
+    @Override public String getSampleDescription() {
+        return "A simple UI control that makes it possible to select zero or "
+                + "more items within a ComboBox without the need to set a custom "
+                + "cell factory or manually create boolean properties for each "
+                + "row - simply use the check model property to request the "
+                + "current selection state.";
+    }
+    
+    @Override public Node getPanel(Stage stage) {
+        GridPane grid = new GridPane();
+        grid.setVgap(10);
+        grid.setHgap(10);
+        grid.setPadding(new Insets(30, 30, 0, 30));
+        
+        int row = 0;
+        
+        final ObservableList<String> strings = FXCollections.observableArrayList();
+        for (int i = 0; i <= 100; i++) {
+            strings.add("Item " + i);
+        }
+
+        // normal ComboBox
+        grid.add(new Label("Normal ComboBox: "), 0, row);
+        grid.add(new ComboBox<String>(strings), 1, row++);
+        
+        // CheckComboBox
+        checkComboBox = new CheckComboBox<String>(strings);
+        checkComboBox.getCheckModel().getCheckedItems().addListener(new ListChangeListener<String>() {
+            @Override public void onChanged(ListChangeListener.Change<? extends String> change) {
+                updateText(checkedItemsLabel, change.getList());
+                
+                while (change.next()) {
+                    System.out.println("============================================");
+                    System.out.println("Change: " + change);
+                    System.out.println("Added sublist " + change.getAddedSubList());
+                    System.out.println("Removed sublist " + change.getRemoved());
+                    System.out.println("List " + change.getList());
+                    System.out.println("Added " + change.wasAdded() + " Permutated " + change.wasPermutated() + " Removed " + change.wasRemoved() + " Replaced "
+                            + change.wasReplaced() + " Updated " + change.wasUpdated());
+                    System.out.println("============================================");
+                }
+            }
+        });
+        grid.add(new Label("CheckComboBox: "), 0, row);
+        grid.add(checkComboBox, 1, row++);
+        
+        CheckComboBox<Person> checkComboBox2 = new CheckComboBox<Person>(Person.createDemoList());
+        checkComboBox2.setConverter(new StringConverter<Person>() {
+            @Override
+            public String toString(Person object) {
+                return object.getFullName();
+            }
+            @Override
+            public Person fromString(String string) {
+                return null;
+            }
+        });
+        grid.add(new Label("CheckComboBox with data objects: "), 0, row);
+        grid.add(checkComboBox2, 1, row++);
+        
+        return grid;
+    }
+    
+    @Override public Node getControlPanel() {
+        GridPane grid = new GridPane();
+        grid.setVgap(10);
+        grid.setHgap(10);
+        grid.setPadding(new Insets(30, 30, 0, 30));
+        
+        int row = 0;
+        
+        Label label1 = new Label("Checked items: ");
+        label1.getStyleClass().add("property");
+        grid.add(label1, 0, 0);
+        grid.add(checkedItemsLabel, 1, row++);
+        updateText(checkedItemsLabel, null);
+        
+        Label checkItem2Label = new Label("Check 'Item 2': ");
+        checkItem2Label.getStyleClass().add("property");
+        grid.add(checkItem2Label, 0, row);
+        final CheckBox checkItem2Btn = new CheckBox();
+        checkItem2Btn.setOnAction(new EventHandler<ActionEvent>() {
+            @Override public void handle(ActionEvent e) {
+                IndexedCheckModel<String> cm = checkComboBox.getCheckModel();
+                if (cm.isChecked(2)) {
+                    cm.clearCheck(2);
+                } else {
+                    cm.check(2);
+                }
+            }
+        });
+        grid.add(checkItem2Btn, 1, row++);
+        
+        return grid;
+    }
+    
+    protected void updateText(Label label, ObservableList<? extends String> list) {
+        final StringBuilder sb = new StringBuilder();
+        
+        if (list != null) {
+            for (int i = 0, max = list.size(); i < max; i++) {
+                sb.append(list.get(i));
+                if (i < max - 1) {
+                    sb.append(", ");
+                }
+            }
+        }
+        
+        final String str = sb.toString();
+        label.setText(str.isEmpty() ? "<empty>" : str);
+    }
+    
+    public static void main(String[] args) {
+        launch(args);
+    }
+    
+}
+
+class Person {
+    private StringProperty firstname = new SimpleStringProperty();
+    private StringProperty lastname = new SimpleStringProperty();
+    private ReadOnlyStringWrapper fullName = new ReadOnlyStringWrapper();
+    
+    public Person(String firstname, String lastname) {
+        this.firstname.set(firstname);
+        this.lastname.set(lastname);
+        fullName.bind(Bindings.concat(firstname, " ", lastname));
+    }
+    
+    public static final ObservableList<Person> createDemoList() {
+        final ObservableList<Person> result = FXCollections.observableArrayList();
+        result.add(new Person("Paul", "McCartney"));
+        result.add(new Person("Andrew Lloyd", "Webber"));
+        result.add(new Person("Herb", "Alpert"));
+        result.add(new Person("Emilio", "Estefan"));
+        result.add(new Person("Bernie", "Taupin"));
+        result.add(new Person("Elton", "John"));
+        result.add(new Person("Mick", "Jagger"));
+        result.add(new Person("Keith", "Richerds"));
+        return result;
+    }
+
+    public final StringProperty firstnameProperty() {
+        return this.firstname;
+    }
+
+    public final java.lang.String getFirstname() {
+        return this.firstnameProperty().get();
+    }
+
+    public final void setFirstname(final String firstname) {
+        this.firstnameProperty().set(firstname);
+    }
+
+    public final StringProperty lastnameProperty() {
+        return this.lastname;
+    }
+
+    public final String getLastname() {
+        return this.lastnameProperty().get();
+    }
+
+    public final void setLastname(final String lastname) {
+        this.lastnameProperty().set(lastname);
+    }
+
+    public final ReadOnlyStringProperty fullNameProperty() {
+        return this.fullName.getReadOnlyProperty();
+    }
+
+    public final String getFullName() {
+        return this.fullNameProperty().get();
+    }
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/checked/HelloCheckListView.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/checked/HelloCheckListView.java
new file mode 100644
index 0000000..6b8f3b3
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/checked/HelloCheckListView.java
@@ -0,0 +1,167 @@
+/**
+ * Copyright (c) 2013, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples.checked;
+
+import javafx.collections.FXCollections;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.geometry.Insets;
+import javafx.scene.Node;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.Label;
+import javafx.scene.control.SelectionMode;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.StackPane;
+import javafx.stage.Stage;
+
+import org.controlsfx.ControlsFXSample;
+import org.controlsfx.control.CheckListView;
+import org.controlsfx.control.IndexedCheckModel;
+import org.controlsfx.samples.Utils;
+
+public class HelloCheckListView extends ControlsFXSample {
+    
+    private final Label checkedItemsLabel = new Label();
+    private final Label selectedItemsLabel = new Label();
+    
+    private CheckListView<String> checkListView;
+    
+    @Override public String getSampleName() {
+        return "CheckListView";
+    }
+    
+    @Override public String getJavaDocURL() {
+        return Utils.JAVADOC_BASE + "org/controlsfx/control/CheckListView.html";
+    }
+    
+    @Override public String getSampleDescription() {
+        return "A simple UI control that makes it possible to select zero or "
+                + "more items within a ListView without the need to set a custom "
+                + "cell factory or manually create boolean properties for each "
+                + "row - simply use the check model property to request the "
+                + "current selection state.";
+    }
+    
+    @Override public Node getPanel(Stage stage) {
+        final ObservableList<String> strings = FXCollections.observableArrayList();
+        for (int i = 0; i <= 100; i++) {
+            strings.add("Item " + i);
+        }
+        
+        // CheckListView
+        checkListView = new CheckListView<>(strings);
+        checkListView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
+        checkListView.getSelectionModel().getSelectedItems().addListener(new ListChangeListener<String>() {
+            @Override public void onChanged(ListChangeListener.Change<? extends String> c) {
+                updateText(selectedItemsLabel, c.getList());
+            }
+        });
+        checkListView.getCheckModel().getCheckedItems().addListener(new ListChangeListener<String>() {
+            @Override public void onChanged(ListChangeListener.Change<? extends String> change) {
+                updateText(checkedItemsLabel, change.getList());
+                
+                while (change.next()) {
+                    System.out.println("============================================");
+                    System.out.println("Change: " + change);
+                    System.out.println("Added sublist " + change.getAddedSubList());
+                    System.out.println("Removed sublist " + change.getRemoved());
+                    System.out.println("List " + change.getList());
+                    System.out.println("Added " + change.wasAdded() + " Permutated " + change.wasPermutated() + " Removed " + change.wasRemoved() + " Replaced "
+                            + change.wasReplaced() + " Updated " + change.wasUpdated());
+                    System.out.println("============================================");
+                }
+            }
+        });
+        
+        StackPane stackPane = new StackPane(checkListView);
+        stackPane.setPadding(new Insets(30));
+        return stackPane;
+    }
+    
+    @Override public Node getControlPanel() {
+        GridPane grid = new GridPane();
+        grid.setVgap(10);
+        grid.setHgap(10);
+        grid.setPadding(new Insets(30, 30, 0, 30));
+        
+        int row = 0;
+        
+        Label label1 = new Label("Checked items: ");
+        label1.getStyleClass().add("property");
+        grid.add(label1, 0, 0);
+        grid.add(checkedItemsLabel, 1, row++);
+        updateText(checkedItemsLabel, null);
+        
+        Label label2 = new Label("Selected items: ");
+        label2.getStyleClass().add("property");
+        grid.add(label2, 0, 1);
+        grid.add(selectedItemsLabel, 1, row++);
+        updateText(selectedItemsLabel, null);
+        
+        
+        Label checkItem2Label = new Label("Check 'Item 2': ");
+        checkItem2Label.getStyleClass().add("property");
+        grid.add(checkItem2Label, 0, row);
+        final CheckBox checkItem2Btn = new CheckBox();
+        checkItem2Btn.setOnAction(new EventHandler<ActionEvent>() {
+            @Override public void handle(ActionEvent e) {
+                IndexedCheckModel<String> cm = checkListView.getCheckModel();
+                if (cm.isChecked(2)) {
+                    cm.clearCheck(2);
+                } else {
+                    cm.check(2);
+                }
+            }
+        });
+        grid.add(checkItem2Btn, 1, row++);
+        
+        
+        return grid;
+    }
+    
+    protected void updateText(Label label, ObservableList<? extends String> list) {
+        final StringBuilder sb = new StringBuilder();
+        
+        if (list != null) {
+            for (int i = 0, max = list.size(); i < max; i++) {
+                sb.append(list.get(i));
+                if (i < max - 1) {
+                    sb.append(", ");
+                }
+            }
+        }
+        
+        final String str = sb.toString();
+        label.setText(str.isEmpty() ? "<empty>" : str);
+    }
+    
+    public static void main(String[] args) {
+        launch(args);
+    }
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/checked/HelloCheckTreeView.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/checked/HelloCheckTreeView.java
new file mode 100644
index 0000000..58ee12b
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/checked/HelloCheckTreeView.java
@@ -0,0 +1,171 @@
+/**
+ * Copyright (c) 2013, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples.checked;
+
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.geometry.Insets;
+import javafx.scene.Node;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.CheckBoxTreeItem;
+import javafx.scene.control.Label;
+import javafx.scene.control.SelectionMode;
+import javafx.scene.control.TreeItem;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.StackPane;
+import javafx.stage.Stage;
+
+import org.controlsfx.ControlsFXSample;
+import org.controlsfx.control.CheckModel;
+import org.controlsfx.control.IndexedCheckModel;
+import org.controlsfx.control.CheckTreeView;
+import org.controlsfx.samples.Utils;
+
+public class HelloCheckTreeView extends ControlsFXSample {
+    
+    private final Label checkedItemsLabel = new Label();
+    private final Label selectedItemsLabel = new Label();
+    
+    private CheckTreeView<String> checkTreeView;
+    
+    private CheckBoxTreeItem<String> treeItem_Jonathan = new CheckBoxTreeItem<>("Jonathan");
+    private CheckBoxTreeItem<String> treeItem_Eugene = new CheckBoxTreeItem<>("Eugene");
+    private CheckBoxTreeItem<String> treeItem_Henry = new CheckBoxTreeItem<>("Henry");
+    private CheckBoxTreeItem<String> treeItem_Samir = new CheckBoxTreeItem<>("Samir");
+
+    @Override public String getSampleName() {
+        return "CheckTreeView";
+    }
+    
+    @Override public String getJavaDocURL() {
+        return Utils.JAVADOC_BASE + "org/controlsfx/control/CheckTreeView.html";
+    }
+    
+    @Override public String getSampleDescription() {
+        return "A simple UI control that makes it possible to select zero or "
+                + "more items within a TreeView without the need to set a custom "
+                + "cell factory or manually create boolean properties for each "
+                + "row - simply use the check model property to request the "
+                + "current selection state.";
+    }
+    
+    @SuppressWarnings("unchecked")
+    @Override public Node getPanel(Stage stage) {
+        CheckBoxTreeItem<String> root = new CheckBoxTreeItem<String>("Root");
+        root.setExpanded(true);
+        root.getChildren().addAll(
+                treeItem_Jonathan,
+                treeItem_Eugene,
+                treeItem_Henry,
+                treeItem_Samir);
+        
+        // lets check Eugene to make sure that it shows up in the tree
+        treeItem_Eugene.setSelected(true);
+        
+        // CheckListView
+        checkTreeView = new CheckTreeView<>(root);
+        checkTreeView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
+        checkTreeView.getSelectionModel().getSelectedItems().addListener(new ListChangeListener<TreeItem<String>>() {
+            @Override public void onChanged(ListChangeListener.Change<? extends TreeItem<String>> c) {
+                updateText(selectedItemsLabel, c.getList());
+            }
+        });
+        
+        checkTreeView.getCheckModel().getCheckedItems().addListener(new ListChangeListener<TreeItem<String>>() {
+            @Override public void onChanged(ListChangeListener.Change<? extends TreeItem<String>> change) {
+                updateText(checkedItemsLabel, change.getList());
+                
+                while (change.next()) {
+                    System.out.println("============================================");
+                    System.out.println("Change: " + change);
+                    System.out.println("Added sublist " + change.getAddedSubList());
+                    System.out.println("Removed sublist " + change.getRemoved());
+                    System.out.println("List " + change.getList());
+                    System.out.println("Added " + change.wasAdded() + " Permutated " + change.wasPermutated() + " Removed " + change.wasRemoved() + " Replaced "
+                            + change.wasReplaced() + " Updated " + change.wasUpdated());
+                    System.out.println("============================================");
+                }
+            }
+        });
+        
+        StackPane stackPane = new StackPane(checkTreeView);
+        stackPane.setPadding(new Insets(30));
+        return stackPane;
+    }
+    
+    @Override public Node getControlPanel() {
+        GridPane grid = new GridPane();
+        grid.setVgap(10);
+        grid.setHgap(10);
+        grid.setPadding(new Insets(30, 30, 0, 30));
+        
+        int row = 0;
+        
+        Label label1 = new Label("Checked items: ");
+        label1.getStyleClass().add("property");
+        grid.add(label1, 0, row);
+        grid.add(checkedItemsLabel, 1, row++);
+        updateText(checkedItemsLabel, checkTreeView.getCheckModel().getCheckedItems());
+        
+        Label label2 = new Label("Selected items: ");
+        label2.getStyleClass().add("property");
+        grid.add(label2, 0, row);
+        grid.add(selectedItemsLabel, 1, row++);
+        updateText(selectedItemsLabel, checkTreeView.getSelectionModel().getSelectedItems());
+        
+        Label checkItem2Label = new Label("Check 'Jonathan': ");
+        checkItem2Label.getStyleClass().add("property");
+        grid.add(checkItem2Label, 0, row);
+        final CheckBox checkItem2Btn = new CheckBox();
+        checkItem2Btn.selectedProperty().bindBidirectional(treeItem_Jonathan.selectedProperty());
+        grid.add(checkItem2Btn, 1, row++);
+        
+        return grid;
+    }
+    
+    protected void updateText(Label label, ObservableList<? extends TreeItem<String>> list) {
+        final StringBuilder sb = new StringBuilder();
+        
+        if (list != null) {
+            for (int i = 0, max = list.size(); i < max; i++) {
+                sb.append(list.get(i).getValue());
+                if (i < max - 1) {
+                    sb.append(", ");
+                }
+            }
+        }
+        
+        final String str = sb.toString();
+        label.setText(str.isEmpty() ? "<empty>" : str);
+    }
+    
+    public static void main(String[] args) {
+        launch(args);
+    }
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/dialogs/HelloDialogs.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/dialogs/HelloDialogs.java
new file mode 100644
index 0000000..7c4e4c8
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/dialogs/HelloDialogs.java
@@ -0,0 +1,742 @@
+/**
+ * Copyright (c) 2013, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples.dialogs;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+import javafx.application.Application;
+import javafx.application.Platform;
+import javafx.collections.FXCollections;
+import javafx.concurrent.Task;
+import javafx.event.ActionEvent;
+import javafx.geometry.Insets;
+import javafx.scene.Node;
+import javafx.scene.control.Alert;
+import javafx.scene.control.Alert.AlertType;
+import javafx.scene.control.Button;
+import javafx.scene.control.ButtonBar.ButtonData;
+import javafx.scene.control.ButtonType;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.ChoiceDialog;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.Dialog;
+import javafx.scene.control.Label;
+import javafx.scene.control.TextField;
+import javafx.scene.control.TextInputDialog;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.VBox;
+import javafx.scene.text.Font;
+import javafx.stage.Modality;
+import javafx.stage.Stage;
+import javafx.stage.StageStyle;
+import javafx.stage.Window;
+
+import org.controlsfx.ControlsFXSample;
+import org.controlsfx.dialog.CommandLinksDialog;
+import org.controlsfx.dialog.CommandLinksDialog.CommandLinksButtonType;
+import org.controlsfx.dialog.WizardPane;
+import org.controlsfx.dialog.ExceptionDialog;
+import org.controlsfx.dialog.FontSelectorDialog;
+import org.controlsfx.dialog.LoginDialog;
+import org.controlsfx.dialog.ProgressDialog;
+import org.controlsfx.dialog.Wizard;
+import org.controlsfx.dialog.Wizard.LinearFlow;
+import org.controlsfx.validation.ValidationSupport;
+import org.controlsfx.validation.Validator;
+
+public class HelloDialogs extends ControlsFXSample {
+    
+	@Override
+	public String getSampleName() {
+		return "Dialogs";
+	}
+
+	@Override
+	public String getJavaDocURL() {
+//		return Utils.JAVADOC_BASE + "org/controlsfx/dialog/Dialogs.html";
+	    return null;
+	}
+
+	@Override
+	public String getSampleDescription() {
+		return "";
+	}
+
+	private final ComboBox<StageStyle> styleCombobox = new ComboBox<>();
+    private final ComboBox<Modality> modalityCombobox = new ComboBox<>();
+    private final CheckBox cbUseBlocking = new CheckBox();
+    private final CheckBox cbCloseDialogAutomatically = new CheckBox();
+    private final CheckBox cbShowMasthead = new CheckBox();
+    private final CheckBox cbSetOwner = new CheckBox();
+    private final CheckBox cbCustomGraphic = new CheckBox();
+    
+    private Stage stage;
+
+    @Override
+    public Node getPanel(Stage stage) {
+        this.stage = stage;
+
+        GridPane grid = new GridPane();
+        grid.setPadding(new Insets(10, 10, 10, 10));
+        grid.setHgap(10);
+        grid.setVgap(10);
+
+        int row = 0;
+        
+        Label javafxDialogs = new Label("JavaFX Dialogs:");
+        javafxDialogs.setFont(Font.font(25));
+        grid.add(javafxDialogs, 0, row++, 2, 1);
+
+        // *******************************************************************
+        // Information Dialog
+        // *******************************************************************
+
+        grid.add(createLabel("Information Dialog: "), 0, row);
+
+        final Button Hyperlink2 = new Button("Show");
+        Hyperlink2.setOnAction( (ActionEvent e) -> {
+        		
+                Alert dlg = createAlert(AlertType.INFORMATION);
+                dlg.setTitle("Custom title");
+                String optionalMasthead = "Wouldn't this be nice?";
+                dlg.getDialogPane().setContentText("A collection of pre-built JavaFX dialogs?\nSeems like a great idea to me...");
+                configureSampleDialog(dlg, optionalMasthead);
+                
+                // lets get some output when events happen
+                dlg.setOnShowing(evt -> System.out.println(evt));
+                dlg.setOnShown(evt -> System.out.println(evt));
+                dlg.setOnHiding(evt -> System.out.println(evt));
+                dlg.setOnHidden(evt -> System.out.println(evt));
+                
+//              dlg.setOnCloseRequest(evt -> evt.consume());
+                
+                showDialog(dlg);
+        });
+
+        final Button Hyperlink2a = new Button("2 x Buttons (no cancel)");
+        Hyperlink2a.setOnAction( (ActionEvent e) -> {
+                Alert dlg = createAlert(AlertType.INFORMATION);
+                dlg.setTitle("Custom title");
+                String optionalMasthead = "Wouldn't this be nice?";
+                dlg.getDialogPane().setContentText("A collection of pre-built JavaFX dialogs?\nSeems like a great idea to me...");
+                configureSampleDialog(dlg, optionalMasthead);
+                dlg.getButtonTypes().add(ButtonType.NEXT);
+
+//              dlg.setOnCloseRequest(evt -> evt.consume());
+
+                showDialog(dlg);
+            
+        });
+
+        grid.add(new HBox(10, Hyperlink2, Hyperlink2a), 1, row);
+
+        row++;
+
+        // *******************************************************************
+        // Confirmation Dialog
+        // *******************************************************************
+
+        grid.add(createLabel("Confirmation Dialog: "), 0, row);
+
+        final CheckBox cbShowCancel = new CheckBox("Show Cancel Button");
+        cbShowCancel.setSelected(true);
+
+        final Button Hyperlink3 = new Button("Show");
+        Hyperlink3.setOnAction(e -> {
+            Alert dlg = createAlert(AlertType.CONFIRMATION);
+            dlg.setTitle("You do want dialogs right?");
+            String optionalMasthead = "Just Checkin'";
+            dlg.getDialogPane().setContentText("I was a bit worried that you might not want them, so I wanted to double check.");
+
+            if (!cbShowCancel.isSelected()) {
+                dlg.getDialogPane().getButtonTypes().remove(ButtonType.CANCEL);
+            }
+
+            configureSampleDialog(dlg, optionalMasthead);
+            showDialog(dlg);
+        });
+        grid.add(new HBox(10, Hyperlink3, cbShowCancel), 1, row);
+
+        row++;
+
+        // *******************************************************************
+        // Warning Dialog
+        // *******************************************************************
+
+        grid.add(createLabel("Warning Dialog: "), 0, row);
+
+        final Button Hyperlink6a = new Button("Show");
+        Hyperlink6a.setOnAction(e -> {
+            Alert dlg = createAlert(AlertType.WARNING);
+            dlg.setTitle("I'm warning you!");
+            String optionalMasthead = "This is a warning";
+            dlg.getDialogPane().setContentText("I'm glad I didn't need to use this...");
+            configureSampleDialog(dlg, optionalMasthead);
+            showDialog(dlg);
+        });
+        grid.add(new HBox(10, Hyperlink6a), 1, row);
+
+        row++;
+
+        // *******************************************************************
+        // Error Dialog
+        // *******************************************************************
+
+        grid.add(createLabel("Error Dialog: "), 0, row);
+
+        final Button Hyperlink7a = new Button("Show");
+        Hyperlink7a.setOnAction(e -> {
+            Alert dlg = createAlert(AlertType.ERROR);
+            dlg.setTitle("It looks like you're making a bad decision");
+            String optionalMasthead = "Exception Encountered";
+            dlg.getDialogPane().setContentText("Better change your mind - this is really your last chance! (Even longer text that should probably wrap)");
+            configureSampleDialog(dlg, optionalMasthead);
+            showDialog(dlg);
+        });
+        grid.add(new HBox(10, Hyperlink7a), 1, row);
+
+        row++;
+
+
+        // *******************************************************************
+        // Input Dialog (with header)
+        // *******************************************************************
+
+        grid.add(createLabel("Input Dialog: "), 0, row);
+
+        final Button Hyperlink8 = new Button("TextField");
+        Hyperlink8.setOnAction(e -> {
+            TextInputDialog dlg = new TextInputDialog("");
+            dlg.setTitle("Name Check");
+            String optionalMasthead = "Please type in your name";
+            dlg.getDialogPane().setContentText("What is your name?");
+            configureSampleDialog(dlg, optionalMasthead);
+            showDialog(dlg);
+        });
+
+        final Button Hyperlink9 = new Button("Initial Value Set");
+        Hyperlink9.setOnAction(e -> {
+            TextInputDialog dlg = new TextInputDialog("Jonathan");
+            dlg.setTitle("Name Guess");
+            String optionalMasthead = "Name Guess";
+            dlg.getDialogPane().setContentText("Pick a name?");
+            configureSampleDialog(dlg, optionalMasthead);
+            showDialog(dlg);
+        });
+
+        final Button Hyperlink10 = new Button("Set Choices (< 10)");
+        Hyperlink10.setOnAction(e -> {
+            ChoiceDialog<String> dlg = new ChoiceDialog<>("Jonathan",
+                                                          "Matthew", "Jonathan", "Ian", "Sue", "Hannah");
+            dlg.setTitle("Name Guess");
+            String optionalMasthead = "Name Guess";
+            dlg.getDialogPane().setContentText("Pick a name?");
+            configureSampleDialog(dlg, optionalMasthead);
+            showDialog(dlg);
+        });
+
+        final Button Hyperlink11 = new Button("Set Choices (>= 10)");
+        Hyperlink11.setOnAction(e -> {
+            ChoiceDialog<String> dlg = new ChoiceDialog<>("Jonathan",
+                                                          "Matthew", "Jonathan", "Ian", "Sue",
+                                                          "Hannah", "Julia", "Denise", "Stephan",
+                                                          "Sarah", "Ron", "Ingrid");
+            dlg.setTitle("Name Guess");
+            String optionalMasthead = "Name Guess";
+            dlg.getDialogPane().setContentText("Pick a name?");
+            configureSampleDialog(dlg, optionalMasthead);
+            showDialog(dlg);
+        });
+
+        grid.add(new HBox(10, Hyperlink8, Hyperlink9, Hyperlink10, Hyperlink11), 1, row);
+        row++;
+        
+        
+        
+        
+        
+        
+        // ---------  ControlsFX-specific Dialogs
+        
+        Label controlsfxDialogs = new Label("ControlsFX Dialogs:");
+        controlsfxDialogs.setFont(Font.font(25));
+        grid.add(controlsfxDialogs, 0, row++, 2, 1);
+        
+
+        // *******************************************************************
+        // Command links
+        // *******************************************************************
+
+        grid.add(createLabel("Pre-built dialogs: "), 0, row);
+        final Button Hyperlink12 = new Button("Command Links");
+        Hyperlink12.setOnAction(e -> {
+            List<CommandLinksButtonType> links = Arrays
+                    .asList(new CommandLinksButtonType(
+                            "Add a network that is in the range of this computer",
+                            "This shows you a list of networks that are currently available and lets you connect to one.", false),
+                            new CommandLinksButtonType(
+                            "Manually create a network profile",
+                            "This creates a new network profile or locates an existing one and saves it on your computer",
+                             true /*default*/),
+                             new CommandLinksButtonType("Create an ad hoc network",
+                            "This creates a temporary network for sharing files or and Internet connection", false));
+
+            CommandLinksDialog dlg = new CommandLinksDialog(links);
+            dlg.setTitle("Manually connect to wireless network");
+            String optionalMasthead = "Manually connect to wireless network";
+            dlg.getDialogPane().setContentText("How do you want to add a network?");
+            configureSampleDialog(dlg, optionalMasthead);
+            showDialog(dlg);
+        });
+
+        final Button Hyperlink12a = new Button("Font Selector");
+        Hyperlink12a.setOnAction(e -> {
+            FontSelectorDialog dlg = new FontSelectorDialog(null);
+            configureSampleDialog(dlg, "Please select a font!");
+            showDialog(dlg);
+        });
+        
+        final Button Hyperlink12b = new Button("Progress");
+        Hyperlink12b.setOnAction((ActionEvent e) -> {
+                Task<Object> worker = new Task<Object>() {
+                    @Override
+                    protected Object call() throws Exception {
+                        for (int i = 0; i <= 100; i++) {
+                            updateProgress(i, 99);
+                            updateMessage("progress: " + i);
+                            System.out.println("progress: " + i);
+                            Thread.sleep(100);
+                        }
+                        return null;
+                    }
+                };
+
+                ProgressDialog dlg = new ProgressDialog(worker);
+                configureSampleDialog(dlg, "");
+
+                Thread th = new Thread(worker);
+                th.setDaemon(true);
+                th.start();
+        });
+        
+        final Button Hyperlink12c = new Button("Login");
+        Hyperlink12c.setOnAction((ActionEvent e) -> {
+                LoginDialog dlg = new LoginDialog(null, null);
+                configureSampleDialog(dlg, "");
+                showDialog(dlg);
+        });
+        
+        final Button Hyperlink12d = new Button("Exception");
+        Hyperlink12d.setOnAction((ActionEvent e) -> {
+                ExceptionDialog dlg = new ExceptionDialog(new Exception("ControlsFX is _too_ awesome!"));
+                configureSampleDialog(dlg, "");
+                showDialog(dlg);
+        });
+
+        grid.add(new HBox(10, Hyperlink12, Hyperlink12a, Hyperlink12b, Hyperlink12c, Hyperlink12d), 1, row);
+        row++;
+
+        
+        // *******************************************************************
+        // wizards
+        // *******************************************************************
+
+        grid.add(createLabel("Wizard: "), 0, row);
+        final Button Hyperlink15a = new Button("Linear Wizard");
+        Hyperlink15a.setOnAction(e -> showLinearWizard());
+        
+        final Button Hyperlink15b = new Button("Branching Wizard");
+        Hyperlink15b.setOnAction(e -> showBranchingWizard());
+        
+        final Button Hyperlink15c = new Button("Validated Linear Wizard");
+        Hyperlink15c.setOnAction(e -> showValidatedLinearWizard());
+        
+        grid.add(new HBox(10, Hyperlink15a, Hyperlink15b, Hyperlink15c), 1, row++);
+        
+        return grid;
+    }
+
+    private Alert createAlert(AlertType type) {
+        Window owner = cbSetOwner.isSelected() ? stage : null;
+        Alert dlg = new Alert(type, "");
+        dlg.initModality(modalityCombobox.getValue());
+        dlg.initOwner(owner);
+        return dlg;
+    }
+    
+    private void configureSampleDialog(Dialog<?> dlg, String header) {
+        Window owner = cbSetOwner.isSelected() ? stage : null;
+        if (header != null && cbShowMasthead.isSelected()) {
+            dlg.getDialogPane().setHeaderText(header);
+        }
+        
+        if (cbCustomGraphic.isSelected()) {
+            dlg.getDialogPane().setGraphic(new ImageView(new Image(getClass().getResource("../controlsfx-logo.png").toExternalForm())));
+        }
+        
+        dlg.initStyle(styleCombobox.getValue());
+        dlg.initOwner(owner);
+    }
+    
+    private void showDialog(Dialog<?> dlg) {
+        Window owner = cbSetOwner.isSelected() ? stage : null;
+        if (cbCloseDialogAutomatically.isSelected()) {
+            new Thread(() -> {
+                try {
+                    Thread.sleep(2000);
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+                System.out.println("Attempting to close dialog now...");
+                Platform.runLater(() -> dlg.close());
+            }).start();
+        }
+        dlg.initOwner(owner);
+
+        if (cbUseBlocking.isSelected()) {
+            dlg.showAndWait().ifPresent(result -> System.out.println("Result is " + result));
+        } else {
+            dlg.show();
+            dlg.resultProperty().addListener(o -> System.out.println("Result is: " + dlg.getResult()));
+            System.out.println("This println is _after_ the show method - we're non-blocking!");
+        }
+    }
+
+    @Override public Node getControlPanel() {
+        GridPane grid = new GridPane();
+        grid.setVgap(10);
+        grid.setHgap(10);
+        grid.setPadding(new Insets(30, 30, 0, 30));
+
+        int row = 0;
+
+        // stage style
+        grid.add(createLabel("Style: ", "property"), 0, row);
+        styleCombobox.getItems().setAll(StageStyle.values());
+        styleCombobox.setValue(styleCombobox.getItems().get(0));
+        grid.add(styleCombobox, 1, row);
+        row++;
+
+        // modality
+        grid.add(createLabel("Modality: ", "property"), 0, row);
+        modalityCombobox.getItems().setAll(Modality.values());
+        modalityCombobox.setValue(modalityCombobox.getItems().get(Modality.values().length-1));
+        grid.add(modalityCombobox, 1, row);
+        row++;
+        
+        // use blocking
+        cbUseBlocking.setSelected(true);
+        grid.add(createLabel("Use blocking: ", "property"), 0, row);
+        grid.add(cbUseBlocking, 1, row);
+        row++;
+
+        // close dialog automatically
+        grid.add(createLabel("Close dialog after 2000ms: ", "property"), 0, row);
+        grid.add(cbCloseDialogAutomatically, 1, row);
+        row++;
+
+        // show header
+        grid.add(createLabel("Show custom header text: ", "property"), 0, row);
+        grid.add(cbShowMasthead, 1, row);
+        row++;
+
+        // set owner
+        grid.add(createLabel("Set dialog owner: ", "property"), 0, row);
+        grid.add(cbSetOwner, 1, row);
+        row++;
+        
+        // custom graphic
+        grid.add(createLabel("Use custom graphic: ", "property"), 0, row);
+        grid.add(cbCustomGraphic, 1, row);
+        row++;
+
+        return grid;
+    }
+    
+//    private CommandLinksButtonType buildCommandLink( String text, String comment, boolean isDefault ) {
+//            return new CommandLinksButtonType(text, comment, isDefault);
+//    }
+
+
+    public static void main(String[] args) {
+        Application.launch(args);
+    }
+
+    private Node createLabel(String text, String... styleclass) {
+        Label label = new Label(text);
+
+        if (styleclass == null || styleclass.length == 0) {
+            label.setFont(Font.font(13));
+        } else {
+            label.getStyleClass().addAll(styleclass);
+        }
+        return label;
+    }
+
+    private void showLinearWizard() {
+        Window owner = cbSetOwner.isSelected() ? stage : null;
+        // define pages to show
+        Wizard wizard = new Wizard(owner);
+        wizard.setTitle("Linear Wizard");
+        
+        // --- page 1
+        int row = 0;
+
+        GridPane page1Grid = new GridPane();
+        page1Grid.setVgap(10);
+        page1Grid.setHgap(10);
+
+        page1Grid.add(new Label("First Name:"), 0, row);
+        TextField txFirstName = createTextField("firstName");
+//        wizard.getValidationSupport().registerValidator(txFirstName, Validator.createEmptyValidator("First Name is mandatory"));  
+        page1Grid.add(txFirstName, 1, row++);
+
+        page1Grid.add(new Label("Last Name:"), 0, row);
+        TextField txLastName = createTextField("lastName");
+//        wizard.getValidationSupport().registerValidator(txLastName, Validator.createEmptyValidator("Last Name is mandatory"));
+        page1Grid.add(txLastName, 1, row);
+
+        WizardPane page1 = new WizardPane();
+        page1.setHeaderText("Please Enter Your Details");
+        page1.setContent(page1Grid);
+
+
+        // --- page 2
+        final WizardPane page2 = new WizardPane() {
+            @Override public void onEnteringPage(Wizard wizard) {
+                String firstName = (String) wizard.getSettings().get("firstName");
+                String lastName = (String) wizard.getSettings().get("lastName");
+
+                setContentText("Welcome, " + firstName + " " + lastName + "! Let's add some newlines!\n\n\n\n\n\n\nHello World!");
+            }
+        };
+        page2.setHeaderText("Thanks For Your Details!");
+
+
+        // --- page 3
+        WizardPane page3 = new WizardPane();
+        page3.setHeaderText("Goodbye!");
+        page3.setContentText("Page 3, with extra 'help' button!");
+        
+        ButtonType helpDialogButton = new ButtonType("Help", ButtonData.HELP_2);
+        page3.getButtonTypes().add(helpDialogButton);
+        Button helpButton = (Button) page3.lookupButton(helpDialogButton);
+        helpButton.addEventFilter(ActionEvent.ACTION, actionEvent -> {
+            actionEvent.consume(); // stop hello.dialog from closing
+            System.out.println("Help clicked!");
+        });
+                
+                
+
+        // create wizard
+        wizard.setFlow(new LinearFlow(page1, page2, page3));
+        
+        System.out.println("page1: " + page1);
+        System.out.println("page2: " + page2);
+        System.out.println("page3: " + page3);
+
+        // show wizard and wait for response
+        wizard.showAndWait().ifPresent(result -> {
+            if (result == ButtonType.FINISH) {
+                System.out.println("Wizard finished, settings: " + wizard.getSettings());
+            }
+        });
+    }
+    
+    private void showBranchingWizard() {
+        Window owner = cbSetOwner.isSelected() ? stage : null;
+        // define pages to show.
+        // Because page1 references page2, we need to declare page2 first.
+        final WizardPane page2 = new WizardPane();
+        page2.setContentText("Page 2");
+
+        final CheckBox checkBox = new CheckBox("Skip the second page");
+        checkBox.setId("skip-page-2");
+        VBox vbox = new VBox(10, new Label("Page 1"), checkBox);
+        final WizardPane page1 = new WizardPane() {
+            // when we exit page 1, we will check the state of the 'skip page 2'
+            // checkbox, and if it is true, we will remove page 2 from the pages list
+            @Override public void onExitingPage(Wizard wizard) {
+//                List<WizardPage> pages = wizard.getPages();
+//                if (checkBox.isSelected()) {
+//                    pages.remove(page2);
+//                } else {
+//                    if (! pages.contains(page2)) {
+//                        pages.add(1, page2);
+//                    }
+//                }
+            }
+        };
+        page1.setContent(vbox);
+
+        final WizardPane page3 = new WizardPane();
+        page3.setContentText("Page 3");
+
+        // create wizard
+        Wizard wizard = new Wizard(owner);
+        wizard.setTitle("Branching Wizard");
+        Wizard.Flow branchingFlow = new Wizard.Flow() {
+
+            @Override
+            public Optional<WizardPane> advance(WizardPane currentPage) {
+                return Optional.of(getNext(currentPage));
+            }
+
+            @Override
+            public boolean canAdvance(WizardPane currentPage) {
+                return currentPage != page3;
+            }
+            
+            private WizardPane getNext(WizardPane currentPage) {
+                if ( currentPage == null ) {
+                    return page1;
+                } else if ( currentPage == page1) {
+                    return checkBox.isSelected()? page3: page2;
+                } else {
+                    return page3;
+                }
+            }
+            
+        };
+        
+        //wizard.setFlow( new LinearWizardFlow( page1, page2, page3));
+        wizard.setFlow( branchingFlow);
+
+        // show wizard
+        wizard.showAndWait().ifPresent(result -> {
+            if (result == ButtonType.FINISH) {
+                System.out.println("Wizard finished, settings: " + wizard.getSettings());
+            }
+        });
+    }
+    
+    private void showValidatedLinearWizard() {
+        Window owner = cbSetOwner.isSelected() ? stage : null;
+        Wizard wizard = new Wizard(owner);
+        wizard.setTitle("Validated Linear Wizard");
+        
+        // Page 1
+        WizardPane page1 = new WizardPane() {
+            ValidationSupport vs = new ValidationSupport();
+            {
+                vs.initInitialDecoration();
+                
+                int row = 0;
+
+                GridPane page1Grid = new GridPane();
+                page1Grid.setVgap(10);
+                page1Grid.setHgap(10);
+
+                page1Grid.add(new Label("Username:"), 0, row);
+                TextField txUsername = createTextField("username");
+                vs.registerValidator(txUsername, Validator.createEmptyValidator("EMPTY!"));
+                page1Grid.add(txUsername, 1, row++);
+
+                page1Grid.add(new Label("Full Name:"), 0, row);
+                TextField txFullName = createTextField("fullName");
+                page1Grid.add(txFullName, 1, row);
+                
+                setContent(page1Grid);
+            }
+            
+            @Override
+            public void onEnteringPage(Wizard wizard) {
+                wizard.invalidProperty().unbind();
+                wizard.invalidProperty().bind(vs.invalidProperty());
+            }
+        };
+        
+        // Page 2
+
+        WizardPane page2 = new WizardPane() {
+            ValidationSupport vs = new ValidationSupport();
+            {
+                vs.initInitialDecoration();
+                
+                int row = 0;
+
+                GridPane page2Grid = new GridPane();
+                page2Grid.setVgap(10);
+                page2Grid.setHgap(10);
+
+                page2Grid.add(new Label("ControlsFX is:"), 0, row);
+                ComboBox<String> cbControlsFX = createComboBox("controlsfx");
+                cbControlsFX.setItems(FXCollections.observableArrayList("Cool", "Great"));
+                vs.registerValidator(cbControlsFX, Validator.createEmptyValidator("EMPTY!"));
+                page2Grid.add(cbControlsFX, 1, row++);
+
+                page2Grid.add(new Label("Where have you heard of it?:"), 0, row);
+                TextField txWhere = createTextField("where");
+                vs.registerValidator(txWhere, Validator.createEmptyValidator("EMPTY!"));
+                page2Grid.add(txWhere, 1, row++);
+
+                page2Grid.add(new Label("Free text:"), 0, row);
+                TextField txFreeText = createTextField("freetext");
+                page2Grid.add(txFreeText, 1, row);
+                
+                setContent(page2Grid);
+            }
+            
+            @Override
+            public void onEnteringPage(Wizard wizard) {
+                wizard.invalidProperty().unbind();
+                wizard.invalidProperty().bind(vs.invalidProperty());
+            }
+        };
+
+        // create wizard
+        wizard.setFlow(new LinearFlow(page1, page2));
+
+        // show wizard and wait for response
+        wizard.showAndWait().ifPresent(result -> {
+            if (result == ButtonType.FINISH) {
+                System.out.println("Wizard finished, settings: " + wizard.getSettings());
+            }
+        });
+    }
+    
+    private TextField createTextField(String id) {
+        TextField textField = new TextField();
+        textField.setId(id);
+        GridPane.setHgrow(textField, Priority.ALWAYS);
+        return textField;
+    }
+    
+    private ComboBox<String> createComboBox(String id) {
+        ComboBox<String> comboBox = new ComboBox<>();
+        comboBox.setId(id);
+        GridPane.setHgrow(comboBox, Priority.ALWAYS);
+        return comboBox;
+    }
+
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/propertysheet/Address.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/propertysheet/Address.java
new file mode 100644
index 0000000..d6940bd
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/propertysheet/Address.java
@@ -0,0 +1,117 @@
+/**
+ * Copyright (c) 2014, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples.propertysheet;
+
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+
+public class Address {
+
+    private String addressLine;
+    private String suburb;
+    private final StringProperty state = new SimpleStringProperty();
+    private final StringProperty postcode = new SimpleStringProperty();
+
+    public Address() {
+    }
+
+    /**
+     * @return the addressLine
+     */
+    public String getAddressLine() {
+        return addressLine;
+    }
+
+    /**
+     * @param addressLine the addressLine to set
+     */
+    public void setAddressLine(String addressLine) {
+        this.addressLine = addressLine;
+    }
+
+    /**
+     * @return the suburb
+     */
+    public String getSuburb() {
+        return suburb;
+    }
+
+    /**
+     * @param suburb the suburb to set
+     */
+    public void setSuburb(String suburb) {
+        this.suburb = suburb;
+    }
+
+    /**
+     * @return the state
+     */
+    public String getState() {
+        return state.get();
+    }
+
+    /**
+     * @param state the state to set
+     */
+    public void setState(String state) {
+        this.state.set(state);
+    }
+    
+    /**
+     * @return Property that contains the state.
+     */
+    public StringProperty stateProperty() {
+        return state;
+    }
+    
+    /**
+     * @return the postcode
+     */
+    public String getPostcode() {
+        return postcode.get();
+    }
+
+    /**
+     * @param postcode the postcode to set
+     */
+    public void setPostcode(String postcode) {
+        this.postcode.set(postcode);
+    }
+    
+    /**
+     * @return Property that contains the postcode.
+     */
+    public StringProperty postcodeProperty() {
+        return postcode;
+    }
+    
+    @Override
+    public String toString() {
+        return addressLine + " " + suburb + " " + state.get() + " " + postcode.get();
+    }
+
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/propertysheet/AddressBeanInfo.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/propertysheet/AddressBeanInfo.java
new file mode 100644
index 0000000..8641db2
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/propertysheet/AddressBeanInfo.java
@@ -0,0 +1,73 @@
+/**
+ * Copyright (c) 2014, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples.propertysheet;
+
+import java.beans.BeanDescriptor;
+import java.beans.IntrospectionException;
+import java.beans.PropertyDescriptor;
+import java.beans.SimpleBeanInfo;
+
+public class AddressBeanInfo extends SimpleBeanInfo {
+
+    private static final BeanDescriptor beanDescriptor = new BeanDescriptor(AddressBeanInfo.class);
+    private static PropertyDescriptor[] propDescriptors;
+
+    static {
+        beanDescriptor.setDisplayName("Address Bean");
+    }
+
+    @Override
+    public BeanDescriptor getBeanDescriptor() {
+        return beanDescriptor;
+    }
+
+    @Override
+    public int getDefaultPropertyIndex() {
+        return 0;
+    }
+
+    @Override
+    public PropertyDescriptor[] getPropertyDescriptors() {
+        if (propDescriptors == null) {
+            propDescriptors = new PropertyDescriptor[4];
+            try {
+                propDescriptors[0] = new PropertyDescriptor("addressLine", Address.class);
+                propDescriptors[0].setDisplayName("Address Line 1");
+                propDescriptors[1] = new PropertyDescriptor("suburb", Address.class);
+                propDescriptors[1].setDisplayName("Suburb");
+                propDescriptors[2] = new PropertyDescriptor("state", Address.class);
+                propDescriptors[2].setDisplayName("State");
+                propDescriptors[3] = new PropertyDescriptor("postcode", Address.class);
+                propDescriptors[3].setDisplayName("Postcode");
+            } catch (IntrospectionException ex) {
+                ex.printStackTrace();
+            }
+        }
+        return propDescriptors;
+    }
+
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/propertysheet/CustomPropertyDescriptor.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/propertysheet/CustomPropertyDescriptor.java
new file mode 100644
index 0000000..8340971
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/propertysheet/CustomPropertyDescriptor.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples.propertysheet;
+
+import java.beans.IntrospectionException;
+import java.beans.PropertyDescriptor;
+import java.lang.reflect.Method;
+
+public class CustomPropertyDescriptor extends PropertyDescriptor {
+
+    private boolean editable = true;
+
+    public CustomPropertyDescriptor(String propertyName, Class<?> beanClass) throws IntrospectionException {
+        super(propertyName, beanClass);
+    }
+
+    public CustomPropertyDescriptor(String propertyName, Class<?> beanClass, String readMethodName, String writeMethodName) throws IntrospectionException {
+        super(propertyName, beanClass, readMethodName, writeMethodName);
+    }
+
+    public CustomPropertyDescriptor(String propertyName, Method readMethod, Method writeMethod) throws IntrospectionException {
+        super(propertyName, readMethod, writeMethod);
+    }
+
+    /**
+     * @return the editable
+     */
+    public boolean isEditable() {
+        return editable;
+    }
+
+    /**
+     * @param editable the editable to set
+     */
+    public void setEditable(boolean editable) {
+        this.editable = editable;
+    }
+
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/propertysheet/PopupPropertyEditor.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/propertysheet/PopupPropertyEditor.java
new file mode 100644
index 0000000..c3e3810
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/propertysheet/PopupPropertyEditor.java
@@ -0,0 +1,196 @@
+/**
+ * Copyright (c) 2014, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples.propertysheet;
+
+import java.util.Optional;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.ObservableList;
+import javafx.concurrent.Service;
+import javafx.concurrent.Task;
+import javafx.concurrent.WorkerStateEvent;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.geometry.Pos;
+import javafx.scene.Node;
+import javafx.scene.control.Alert;
+import javafx.scene.control.Button;
+import javafx.scene.control.ButtonBar;
+import javafx.scene.control.ButtonType;
+import javafx.scene.layout.BorderPane;
+
+import org.controlsfx.control.PropertySheet;
+import org.controlsfx.property.BeanProperty;
+import org.controlsfx.property.BeanPropertyUtils;
+import org.controlsfx.property.editor.PropertyEditor;
+
+public class PopupPropertyEditor<T> implements PropertyEditor<T> {
+
+    private final Button btnEditor;
+    private final PropertySheet.Item item;
+    private final ObjectProperty<T> value = new SimpleObjectProperty<>();
+
+    public PopupPropertyEditor(PropertySheet.Item item) {
+        this.item = item;
+        if (item.getValue() != null) {
+            btnEditor = new Button(item.getValue().toString());
+            value.set((T) item.getValue());
+        } else {
+            btnEditor = new Button("<empty>");
+        }
+        btnEditor.setAlignment(Pos.CENTER_LEFT);
+        btnEditor.setOnAction((ActionEvent event) -> {
+            displayPopupEditor();
+        });
+    }
+
+    private void displayPopupEditor() {
+        PopupPropertySheet<T> sheet = new PopupPropertySheet<>(item, this);
+        sheet.setPrefWidth(500);
+        Alert alert = new Alert(Alert.AlertType.NONE);
+//        alert.setWidth(700);
+//        alert.setResizable(true);
+        alert.setResizable(false);
+        alert.getDialogPane().setContent(sheet);
+        alert.setTitle("Popup Property Editor");
+        ButtonType saveButton = new ButtonType("Save", ButtonBar.ButtonData.OK_DONE);
+        ButtonType testButton = new ButtonType("Change Postcode", ButtonBar.ButtonData.OTHER);
+        alert.getButtonTypes().addAll(ButtonType.CANCEL, saveButton, testButton);
+        
+        final Button btTest = (Button) alert.getDialogPane().lookupButton(testButton);
+        btTest.addEventFilter(ActionEvent.ACTION, event -> {
+            Address addr = null;
+            if (item.getValue() != null && item.getValue() instanceof Address) {
+                addr = (Address) item.getValue();
+            } else if (sheet.getBean() != null && sheet.getBean() instanceof Address) {
+                addr = (Address) sheet.getBean();
+            }
+            if (addr != null) {
+                int pc = (int) (Math.random() * 8000);
+                addr.setPostcode(Integer.toString(pc));
+            }
+            event.consume();
+        });
+        
+        Optional<ButtonType> response = alert.showAndWait();
+
+        if (response.isPresent() && saveButton.equals(response.get())) {
+            item.setValue(sheet.getBean());
+            btnEditor.setText(sheet.getBean().toString());
+        }
+    }
+
+    @Override
+    public Node getEditor() {
+        return btnEditor;
+    }
+
+    @Override
+    public T getValue() {
+        return value.get();
+    }
+
+    @Override
+    public void setValue(T t) {
+        value.set(t);
+        if (t != null) {
+            btnEditor.setText(t.toString());
+        }
+    }
+
+    private class PopupPropertySheet<T> extends BorderPane {
+
+        private final PropertyEditor<T> owner;
+        private final PropertySheet sheet;
+        private final PropertySheet.Item item;
+        private T bean;
+
+        public PopupPropertySheet(PropertySheet.Item item, PropertyEditor<T> owner) {
+
+            this.item = item;
+            this.owner = owner;
+            sheet = new PropertySheet();
+            setCenter(sheet);
+//            installButtons();
+            setMinHeight(500);
+
+            initSheet();
+
+        }
+
+        public T getBean() {
+            return bean;
+        }
+
+        private void initSheet() {
+            if (item.getValue() == null) {
+
+                bean = null;
+                try {
+                    bean = (T) item.getType().newInstance();
+                } catch (InstantiationException | IllegalAccessException ex) {
+                    ex.printStackTrace();
+                    return;
+                }
+                if (bean == null) {
+                    return;
+                }
+            } else {
+                bean = (T) item.getValue();
+            }
+
+            Service<?> service = new Service<ObservableList<PropertySheet.Item>>() {
+                @Override
+                protected Task<ObservableList<PropertySheet.Item>> createTask() {
+                    return new Task<ObservableList<PropertySheet.Item>>() {
+                        @Override
+                        protected ObservableList<PropertySheet.Item> call() throws Exception {
+                            return BeanPropertyUtils.getProperties(bean);
+                        }
+                    };
+                }
+
+            };
+            service.setOnSucceeded(new EventHandler<WorkerStateEvent>() {
+                @SuppressWarnings("unchecked")
+                @Override
+                public void handle(WorkerStateEvent e) {
+                    for (PropertySheet.Item i : (ObservableList<PropertySheet.Item>) e.getSource().getValue()) {
+                        if (i instanceof BeanProperty && ((BeanProperty) i).getPropertyDescriptor() instanceof CustomPropertyDescriptor) {
+                            BeanProperty bi = (BeanProperty) i;
+                            bi.setEditable(((CustomPropertyDescriptor) bi.getPropertyDescriptor()).isEditable());
+                        }
+                    }
+                    sheet.getItems().setAll((ObservableList<PropertySheet.Item>) e.getSource().getValue());
+                }
+            });
+            service.start();
+
+        }
+    }
+
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/propertysheet/SampleBean.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/propertysheet/SampleBean.java
new file mode 100644
index 0000000..009e184
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/propertysheet/SampleBean.java
@@ -0,0 +1,106 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples.propertysheet;
+
+import java.util.UUID;
+
+public class SampleBean {
+
+    private String id = UUID.randomUUID().toString();
+    private String firstName;
+    private String lastName;
+    private Address address;
+    private String hiddenValue;
+
+    public SampleBean() {
+    }
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    /**
+     * @return the firstName
+     */
+    public String getFirstName() {
+        return firstName;
+    }
+
+    /**
+     * @param firstName the firstName to set
+     */
+    public void setFirstName(String firstName) {
+        this.firstName = firstName;
+    }
+
+    /**
+     * @return the lastName
+     */
+    public String getLastName() {
+        return lastName;
+    }
+
+    /**
+     * @param lastName the lastName to set
+     */
+    public void setLastName(String lastName) {
+        this.lastName = lastName;
+    }
+
+    /**
+     * @return the address
+     */
+    public Address getAddress() {
+        return address;
+    }
+
+    /**
+     * @param address the address to set
+     */
+    public void setAddress(Address address) {
+        this.address = address;
+    }
+
+    /**
+     * @return the hiddenValue
+     */
+    public String getHiddenValue() {
+        return hiddenValue;
+    }
+
+    /**
+     * @param hiddenValue the hiddenValue to set
+     */
+    public void setHiddenValue(String hiddenValue) {
+        this.hiddenValue = hiddenValue;
+    }
+
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/propertysheet/SampleBeanBeanInfo.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/propertysheet/SampleBeanBeanInfo.java
new file mode 100644
index 0000000..556c433
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/propertysheet/SampleBeanBeanInfo.java
@@ -0,0 +1,79 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples.propertysheet;
+
+import java.beans.BeanDescriptor;
+import java.beans.IntrospectionException;
+import java.beans.PropertyDescriptor;
+import java.beans.SimpleBeanInfo;
+
+public class SampleBeanBeanInfo extends SimpleBeanInfo {
+
+    private static final BeanDescriptor beanDescriptor = new BeanDescriptor(SampleBeanBeanInfo.class);
+    private static PropertyDescriptor[] propDescriptors;
+
+    static {
+        beanDescriptor.setDisplayName("Sample Bean");
+    }
+
+    @Override
+    public BeanDescriptor getBeanDescriptor() {
+        return beanDescriptor;
+    }
+
+    @Override
+    public int getDefaultPropertyIndex() {
+        return 0;
+    }
+
+    @Override
+    public PropertyDescriptor[] getPropertyDescriptors() {
+        if (propDescriptors == null) {
+            propDescriptors = new PropertyDescriptor[5];
+            try {
+                CustomPropertyDescriptor cdp = new CustomPropertyDescriptor("id", SampleBean.class, "getId", "setId");
+                cdp.setDisplayName("Id");
+                cdp.setEditable(false);
+                propDescriptors[0] = cdp;
+                propDescriptors[1] = new PropertyDescriptor("firstName", SampleBean.class, "getFirstName", "setFirstName");
+                propDescriptors[1].setDisplayName("First Name");
+                propDescriptors[2] = new PropertyDescriptor("lastName", SampleBean.class, "getLastName", "setLastName");
+                propDescriptors[2].setDisplayName("Last Name");
+                propDescriptors[3] = new PropertyDescriptor("address", SampleBean.class, "getAddress", "setAddress");
+                propDescriptors[3].setDisplayName("Address");
+                propDescriptors[3].setPropertyEditorClass(PopupPropertyEditor.class);
+                propDescriptors[4] = new PropertyDescriptor("hiddenValue", SampleBean.class, "getHiddenValue", "setHiddenValue");
+                propDescriptors[4].setDisplayName("Hidden Value");
+                propDescriptors[4].setHidden(true);
+            } catch (IntrospectionException ex) {
+                ex.printStackTrace();
+            }
+        }
+        return propDescriptors;
+    }
+
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/tablefilter/ConcurrentTableFilterTest.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/tablefilter/ConcurrentTableFilterTest.java
new file mode 100644
index 0000000..462fde4
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/tablefilter/ConcurrentTableFilterTest.java
@@ -0,0 +1,113 @@
+package org.controlsfx.samples.tablefilter;
+
+import javafx.application.Application;
+import javafx.application.Platform;
+import javafx.beans.property.Property;
+import javafx.beans.property.ReadOnlyObjectWrapper;
+import javafx.beans.property.SimpleIntegerProperty;
+import javafx.collections.FXCollections;
+import javafx.scene.Scene;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableView;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.Priority;
+import javafx.stage.Stage;
+import org.controlsfx.control.table.TableFilter;
+
+import java.util.Random;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.stream.IntStream;
+
+public final class ConcurrentTableFilterTest extends Application {
+
+    private static final ExecutorService exec = Executors.newFixedThreadPool(5);
+
+    @Override
+    public void start(Stage primaryStage) throws Exception {
+
+        TableView<DataItem> tableView = new TableView<>();
+
+        tableView.setItems(FXCollections.observableArrayList());
+        IntStream.range(0,500).mapToObj(i -> new DataItem()).forEach(d -> tableView.getItems().add(d));
+
+        TableColumn<DataItem,Integer> smallInt = new TableColumn<>("Small Int");
+        smallInt.setCellValueFactory(cb -> new ReadOnlyObjectWrapper<>(cb.getValue().getSmallIntValue()));
+
+        TableColumn<DataItem,Integer> largeInt = new TableColumn<>("Large Int");
+        largeInt.setCellValueFactory(cb -> new ReadOnlyObjectWrapper<>(cb.getValue().getLargeIntValue()));
+
+        TableColumn<DataItem,String> randomLetter = new TableColumn<>("Letter");
+        randomLetter.setCellValueFactory(cb -> new ReadOnlyObjectWrapper<>(cb.getValue().getRandomLetter()));
+
+        TableColumn<DataItem,Number> concurrentNumber = new TableColumn<>("Concurrent Number");
+        concurrentNumber.setCellValueFactory(cb ->
+            cb.getValue().getConcurrentNumber()
+        );
+
+        tableView.getColumns().addAll(smallInt, largeInt, randomLetter, concurrentNumber);
+
+        Platform.runLater(() -> new TableFilter<>(tableView));
+
+        GridPane grp = new GridPane();
+
+        GridPane.setFillHeight(tableView, true);
+        GridPane.setFillWidth(tableView, true);
+        GridPane.setHgrow(tableView, Priority.ALWAYS);
+        GridPane.setVgrow(tableView, Priority.ALWAYS);
+        grp.getChildren().add(tableView);
+
+        Scene scene = new Scene(grp);
+
+        primaryStage.setScene(scene);
+
+        primaryStage.show();
+
+    }
+
+    @Override
+    public void stop() throws Exception {
+        exec.shutdown();
+    }
+
+    private static final class DataItem {
+
+        private final int smallIntValue = new Random().nextInt(100);
+        private final int largeIntValue = new Random().nextInt(10000);
+        private final String randomLetter = String.valueOf((char) (new Random().nextInt(26) + 'a'));
+
+        private final Property<Number> concurrentNumber = new SimpleIntegerProperty();
+
+        private DataItem() {
+            exec.execute(() -> {
+                try {
+                    Thread.sleep(1000);
+                } catch (Exception e) {
+                    e.printStackTrace();
+                }
+                Platform.runLater(() -> {
+                    concurrentNumber.setValue(new Random().nextInt(10000));
+                });
+            });
+        }
+        public int getLargeIntValue() {
+            return largeIntValue;
+        }
+        public int getSmallIntValue() {
+            return smallIntValue;
+        }
+        public String getRandomLetter() {
+            return randomLetter;
+        }
+        public Property<Number> getConcurrentNumber() {
+            return concurrentNumber;
+        }
+
+
+    }
+
+    public static void main(String[] args) {
+        Application.launch(args);
+    }
+
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/tablefilter/Flight.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/tablefilter/Flight.java
new file mode 100644
index 0000000..6e92d4f
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/tablefilter/Flight.java
@@ -0,0 +1,56 @@
+package org.controlsfx.samples.tablefilter;
+
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+
+import java.time.LocalDate;
+
+
+public final class Flight {
+
+    private final int flightNumber;
+    private final String orig;
+    private final String dest;
+    private final LocalDate departureTime;
+    private final int mileaage;
+    private final BooleanProperty cancelledInd = new SimpleBooleanProperty(false);
+    private final StringProperty gateNumber = new SimpleStringProperty();
+
+    public Flight(int flightNumber, String orig, String dest, LocalDate departureTime, int mileage, String gateNumber) {
+        this.flightNumber = flightNumber;
+        this.orig = orig;
+        this.dest = dest;
+        this.departureTime = departureTime;
+        this.mileaage = mileage;
+        this.gateNumber.set(gateNumber);
+    }
+
+    public int getFlightNumber() {
+        return flightNumber;
+    }
+
+    public String getOrig() {
+        return orig;
+    }
+
+    public String getDest() {
+        return dest;
+    }
+
+    public LocalDate getDepartureDate() {
+        return departureTime;
+    }
+
+    public int getMileaage() {
+        return mileaage;
+    }
+
+    public BooleanProperty getCancelledProperty() {
+        return cancelledInd;
+    }
+    public StringProperty getGateNumber() {
+        return gateNumber;
+    }
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/tablefilter/FlightTable.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/tablefilter/FlightTable.java
new file mode 100644
index 0000000..b635d23
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/tablefilter/FlightTable.java
@@ -0,0 +1,139 @@
+package org.controlsfx.samples.tablefilter;
+
+
+import javafx.application.Application;
+import javafx.beans.property.ReadOnlyObjectWrapper;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.collections.transformation.SortedList;
+import javafx.geometry.Insets;
+import javafx.scene.Scene;
+import javafx.scene.control.Button;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableView;
+import javafx.scene.control.cell.TextFieldTableCell;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.VBox;
+import javafx.stage.Stage;
+import org.controlsfx.control.table.TableFilter;
+
+import java.time.LocalDate;
+
+public final class FlightTable extends Application {
+
+    private final ObservableList<Flight> flights = FXCollections.observableArrayList(
+            new Flight(567,"ABQ","DAL", LocalDate.of(2015,5,8), 642,"23"),
+            new Flight(234,"ABQ","DAL", LocalDate.of(2015,5,9), 642, "13"),
+            new Flight(756,"ABQ","DAL", LocalDate.of(2015,5,11), 642, "9"),
+            new Flight(268,"ABQ","DAL", LocalDate.of(2015,5,13), 642, "2"),
+
+            new Flight(567,"DAL","HOU", LocalDate.of(2015,5,8), 244, "A5"),
+            new Flight(239,"DAL","HOU", LocalDate.of(2015,5,14), 244, "B4"),
+            new Flight(5923,"DAL","HOU", LocalDate.of(2015,5,17), 244, "C3"),
+            new Flight(2389,"DAL","HOU", LocalDate.of(2015,5,6), 244, null),
+
+            new Flight(287,"SEA","PHX", LocalDate.of(2015,5,8), 1100, null),
+            new Flight(875,"SEA","PHX", LocalDate.of(2015,5,16), 1100,  "12"),
+            new Flight(4288,"SEA","PHX", LocalDate.of(2015,5,9), 1100,  "19")
+    );
+
+    private final ObservableList<Flight> hiddenFlights = FXCollections.observableArrayList(
+            new Flight(567,"BWI","MCO", LocalDate.of(2015,7,9), 898, "45"),
+            new Flight(234,"MDW","PDX", LocalDate.of(2015,7,12), 2118, "B9"),
+            new Flight(411,"SAN","JFK", LocalDate.of(2015,7,19), 2077, null)
+    );
+
+    private final TableView<Flight> table = new TableView<>();
+
+    @Override
+    public void start(Stage primaryStage) throws Exception {
+        primaryStage.setTitle("Flight Table");
+        BorderPane borderPane = new BorderPane();
+        Scene scene = new Scene(borderPane, 800, 600);
+
+        SortedList<Flight> sortedList = new SortedList<>(flights);
+        table.setItems(sortedList);
+        sortedList.comparatorProperty().bind(table.comparatorProperty());
+
+        table.setEditable(true);
+        TableColumn<Flight, Integer> flightNumCol = new TableColumn<>("FLIGHT NUM");
+        flightNumCol.setCellValueFactory(cellData -> new ReadOnlyObjectWrapper<>(cellData.getValue().getFlightNumber()));
+        table.getColumns().add(flightNumCol);
+
+        TableColumn itinerary = new TableColumn("ITINERARY");
+
+        TableColumn<Flight,String> origCol = new TableColumn<>("ORIG");
+        origCol.setCellValueFactory(cellData -> new ReadOnlyObjectWrapper<>(cellData.getValue().getOrig()));
+
+        TableColumn<Flight,String> destCol = new TableColumn<>("DEST");
+        destCol.setCellValueFactory(cellData -> new ReadOnlyObjectWrapper<>(cellData.getValue().getDest()));
+
+        itinerary.getColumns().addAll(origCol,destCol);
+        table.getColumns().add(itinerary);
+
+        TableColumn<Flight,LocalDate> depDateCol = new TableColumn<>("DEP DATE");
+        depDateCol.setCellValueFactory(cellData -> new ReadOnlyObjectWrapper<>(cellData.getValue().getDepartureDate()));
+        table.getColumns().add(depDateCol);
+
+        TableColumn<Flight,Integer> mileage =  new TableColumn<>("MILEAGE");
+        mileage.setCellValueFactory(cellData -> new ReadOnlyObjectWrapper<>(cellData.getValue().getMileaage()));
+        table.getColumns().add(mileage);
+
+        TableColumn<Flight,Boolean> cancelledInd =  new TableColumn<>("CANCELLED");
+        cancelledInd.setCellValueFactory(cellData -> cellData.getValue().getCancelledProperty());
+        cancelledInd.setEditable(true);
+        table.getColumns().add(cancelledInd);
+
+        TableColumn<Flight,String> gateNumber = new TableColumn<>("GATE NO.");
+        gateNumber.setCellValueFactory(cellData -> cellData.getValue().getGateNumber());
+        gateNumber.setCellFactory(TextFieldTableCell.forTableColumn());
+        gateNumber.setEditable(true);
+        table.getColumns().add(gateNumber);
+
+        TableFilter<Flight> tableFilter = TableFilter.forTableView(table).lazy(true).apply();
+
+        table.setEditable(true);
+        tableFilter.setSearchStrategy((input,target) -> {
+            try {
+                return target.matches(input);
+            } catch (Exception e) {
+                return false;
+            }
+        });
+
+        tableFilter.unSelectAllValues(origCol);
+        tableFilter.selectValue(origCol,"ABQ");
+        tableFilter.executeFilter();
+
+        borderPane.setCenter(table);
+
+        Button addFlightButton = new Button("APPEND");
+        addFlightButton.setOnAction(e -> {
+            if (hiddenFlights.size() > 0) {
+                flights.add(hiddenFlights.get(0));
+                hiddenFlights.remove(0);
+            }
+        });
+
+        Button removeFlightButton = new Button("REMOVE");
+        removeFlightButton.setOnAction(e -> {
+            if (flights.size() > 0) {
+                hiddenFlights.add(flights.get(0));
+                flights.remove(0);
+            }
+        });
+
+        VBox buttonPane = new VBox();
+        buttonPane.setPadding(new Insets(5));
+
+        buttonPane.getChildren().addAll(addFlightButton, removeFlightButton);
+
+        borderPane.setRight(buttonPane);
+        primaryStage.setScene(scene);
+
+        primaryStage.show();
+    }
+    public static void main(String[] args) {
+        Application.launch(args);
+    }
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/tablefilter/LargeTableFilterTest.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/tablefilter/LargeTableFilterTest.java
new file mode 100644
index 0000000..0dc30c2
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/tablefilter/LargeTableFilterTest.java
@@ -0,0 +1,97 @@
+package org.controlsfx.samples.tablefilter;
+
+import javafx.application.Application;
+import javafx.beans.property.ReadOnlyObjectWrapper;
+import javafx.collections.FXCollections;
+import javafx.scene.Scene;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableView;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.Priority;
+import javafx.stage.Stage;
+import org.controlsfx.control.table.TableFilter;
+
+import java.util.Random;
+import java.util.UUID;
+import java.util.stream.IntStream;
+
+public final class LargeTableFilterTest extends Application {
+
+    @Override
+    public void start(Stage primaryStage) throws Exception {
+
+        TableView<DataItem> tableView = new TableView<>();
+
+        tableView.setItems(FXCollections.observableArrayList());
+        IntStream.range(0,20000).mapToObj(i -> new DataItem()).forEach(d -> tableView.getItems().add(d));
+
+        TableColumn<DataItem,Integer> smallInt = new TableColumn<>("Small Int");
+        smallInt.setCellValueFactory(cb -> new ReadOnlyObjectWrapper<>(cb.getValue().getSmallIntValue()));
+
+        TableColumn<DataItem,Integer> largeInt = new TableColumn<>("Large Int");
+        largeInt.setCellValueFactory(cb -> new ReadOnlyObjectWrapper<>(cb.getValue().getLargeIntValue()));
+
+        TableColumn<DataItem,String> randomLetter = new TableColumn<>("Letter");
+        randomLetter.setCellValueFactory(cb -> new ReadOnlyObjectWrapper<>(cb.getValue().getRandomLetter()));
+
+        TableColumn randomStrings = new TableColumn("Random Strings");
+
+        TableColumn<DataItem,String> randomString1 = new TableColumn<>("AlphaNum 1");
+        randomString1.setCellValueFactory(cb -> new ReadOnlyObjectWrapper<>(cb.getValue().getRandomStr1()));
+
+        TableColumn<DataItem,String> randomString2 = new TableColumn<>("AlphaNum 2");
+        randomString2.setCellValueFactory(cb -> new ReadOnlyObjectWrapper<>(cb.getValue().getRandomStr2()));
+
+        randomStrings.getColumns().addAll(randomString1,randomString2);
+
+        tableView.getColumns().addAll(smallInt, largeInt, randomLetter, randomStrings);
+
+        TableFilter.forTableView(tableView).lazy(false).apply();
+
+        GridPane grp = new GridPane();
+
+        GridPane.setFillHeight(tableView, true);
+        GridPane.setFillWidth(tableView, true);
+        GridPane.setHgrow(tableView, Priority.ALWAYS);
+        GridPane.setVgrow(tableView, Priority.ALWAYS);
+        grp.getChildren().add(tableView);
+
+        Scene scene = new Scene(grp);
+
+        primaryStage.setScene(scene);
+
+        primaryStage.show();
+
+    }
+
+    private static final class DataItem {
+
+        private final int smallIntValue = new Random().nextInt(100);
+        private final int largeIntValue = new Random().nextInt(10000);
+        private final String randomLetter = String.valueOf((char) (new Random().nextInt(26) + 'a'));
+
+        private final String randomStr1 = UUID.randomUUID().toString().replaceAll("-","");
+        private final String randomStr2 = UUID.randomUUID().toString().replaceAll("-","");
+
+        public int getLargeIntValue() {
+            return largeIntValue;
+        }
+        public int getSmallIntValue() {
+            return smallIntValue;
+        }
+        public String getRandomLetter() {
+            return randomLetter;
+        }
+        public String getRandomStr1() {
+            return randomStr1;
+        }
+        public String getRandomStr2() {
+            return randomStr2;
+        }
+    }
+
+    public static void main(String[] args) {
+        Application.launch(args);
+    }
+
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/tablefilter/TableFilterMemoryTest.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/tablefilter/TableFilterMemoryTest.java
new file mode 100644
index 0000000..ab1508e
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/tablefilter/TableFilterMemoryTest.java
@@ -0,0 +1,86 @@
+package org.controlsfx.samples.tablefilter;
+
+import javafx.application.Application;
+import javafx.beans.property.ReadOnlyStringWrapper;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.scene.Scene;
+import javafx.scene.control.Button;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableView;
+import javafx.scene.layout.BorderPane;
+import javafx.stage.Stage;
+import org.controlsfx.control.table.TableFilter;
+
+import java.util.Random;
+
+/**
+ * This test class helps investigate memory usage of TableFilters while created multiple times for the same table.
+ * Inspection of memory is not part of the class and should be done externally.
+ */
+public class TableFilterMemoryTest extends Application {
+
+    static final int ColCount = 9;
+    static final Random rng = new Random(0);
+    private final BorderPane root = new BorderPane();
+    private final TableView<Row> table = new TableView<>();
+
+    private int rangeStart = 1;
+
+    public TableFilterMemoryTest() {
+        root.setCenter(table);
+
+        Button btn = new Button("Reset");
+        btn.setOnAction(e -> resetTable());
+
+        root.setTop(btn);
+
+        // Add columns
+        char letter = 'A';
+        for (int i = 0; i < 9; i++) {
+            TableColumn<Row, String> tc = new TableColumn<>("Column " + letter);
+            int index = i;
+            tc.setCellValueFactory(cdf -> new ReadOnlyStringWrapper(cdf.getValue().data[index]));
+            table.getColumns().add(tc);
+            letter++;
+        }
+    }
+
+    private void resetTable() {
+        table.setItems(null);
+
+        // Suggest to perform garbage collection
+        System.gc();
+
+        // Add new data
+        ObservableList<Row> ans = FXCollections.observableArrayList();
+        for (int i = 0; i < 1024; i++) {
+            ans.add(new Row());
+        }
+        table.setItems(ans);
+
+        // Reset the filter
+        TableFilter.forTableView(table).lazy(false).apply();
+
+        rangeStart += 10;
+    }
+
+    @Override
+    public void start(Stage primaryStage) throws Exception {
+        Scene scene = new Scene(root, 800, 600);
+        primaryStage.setTitle("TableFilter");
+        primaryStage.setScene(scene);
+        primaryStage.show();
+    }
+
+    class Row {
+        public final String[] data;
+
+        public Row() {
+            data = new String[ColCount];
+            for (int i = 0; i < ColCount; i++) {
+                data[i] = String.format("%d", rangeStart + rng.nextInt(10));
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/tableview/HelloCustomTableMenu.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/tableview/HelloCustomTableMenu.java
new file mode 100644
index 0000000..4ff5468
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/tableview/HelloCustomTableMenu.java
@@ -0,0 +1,80 @@
+///**
+// * Copyright (c) 2014, ControlsFX
+// * All rights reserved.
+// *
+// * Redistribution and use in source and binary forms, with or without
+// * modification, are permitted provided that the following conditions are met:
+// *     * Redistributions of source code must retain the above copyright
+// * notice, this list of conditions and the following disclaimer.
+// *     * Redistributions in binary form must reproduce the above copyright
+// * notice, this list of conditions and the following disclaimer in the
+// * documentation and/or other materials provided with the distribution.
+// *     * Neither the name of ControlsFX, any associated website, nor the
+// * names of its contributors may be used to endorse or promote products
+// * derived from this software without specific prior written permission.
+// *
+// * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+// * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+// * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+// * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+// * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+// * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+// * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+// * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+// */
+//package org.controlsfx.samples.tableview;
+//
+//import javafx.beans.property.ReadOnlyStringWrapper;
+//import javafx.scene.Node;
+//import javafx.scene.control.MenuItem;
+//import javafx.scene.control.SeparatorMenuItem;
+//import javafx.scene.control.TableColumn;
+//import javafx.scene.control.TableView;
+//import javafx.scene.layout.VBox;
+//import javafx.stage.Stage;
+//
+//import org.controlsfx.ControlsFXSample;
+//import org.controlsfx.control.table.TableMenuButtonAccessor;
+//import org.controlsfx.samples.Utils;
+//
+///**
+// *
+// */
+//public class HelloCustomTableMenu extends ControlsFXSample {
+//    
+//    @Override public String getSampleName() {
+//        return "Custom TableMenuButton";
+//    }
+//    
+//    @Override public String getJavaDocURL() {
+//        return Utils.JAVADOC_BASE + "org/controlsfx/control/table/TableMenuButtonAccessor.html";
+//    }
+//    
+//    @Override public Node getPanel(final Stage stage) {
+//        // boring - setting up TableView
+//        TableView<String> tableView = new TableView<>();
+//        tableView.getItems().addAll("Jonathan", "Julia", "Henry");
+//        
+//        TableColumn<String, String> names = new TableColumn<>("Name");
+//        names.setCellValueFactory(cdf -> new ReadOnlyStringWrapper(cdf.getValue()));
+//        
+//        tableView.getColumns().add(names);
+//        
+//        VBox root = new VBox();
+//        root.getChildren().add(tableView);
+//        
+//        // This is where it gets interesting - we modify the context menu
+//        tableView.setTableMenuButtonVisible(true);
+//        TableMenuButtonAccessor.modifyTableMenu(tableView, menu -> {
+//            menu.getItems().addAll(new SeparatorMenuItem(), new MenuItem("Hello World!"));
+//        });
+//        
+//        return root;
+//    }
+//    
+//    public static void main(String[] args) {
+//        launch(args);
+//    } 
+//}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/tableview/HelloSwingTableModelSample.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/tableview/HelloSwingTableModelSample.java
new file mode 100644
index 0000000..b6e29d2
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/tableview/HelloSwingTableModelSample.java
@@ -0,0 +1,79 @@
+///**
+// * Copyright (c) 2014, ControlsFX
+// * All rights reserved.
+// *
+// * Redistribution and use in source and binary forms, with or without
+// * modification, are permitted provided that the following conditions are met:
+// *     * Redistributions of source code must retain the above copyright
+// * notice, this list of conditions and the following disclaimer.
+// *     * Redistributions in binary form must reproduce the above copyright
+// * notice, this list of conditions and the following disclaimer in the
+// * documentation and/or other materials provided with the distribution.
+// *     * Neither the name of ControlsFX, any associated website, nor the
+// * names of its contributors may be used to endorse or promote products
+// * derived from this software without specific prior written permission.
+// *
+// * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+// * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+// * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+// * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+// * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+// * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+// * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+// * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+// */
+//package org.controlsfx.samples.tableview;
+//
+//import javafx.scene.Node;
+//import javafx.scene.layout.VBox;
+//import javafx.stage.Stage;
+//
+//import javax.swing.table.DefaultTableModel;
+//import javax.swing.table.TableModel;
+//
+//import org.controlsfx.ControlsFXSample;
+//import org.controlsfx.control.table.model.JavaFXTableModels;
+//import org.controlsfx.control.table.model.TableModelTableView;
+//import org.controlsfx.samples.Utils;
+//
+//// TODO sorting doesn't work due to readonlyunbacked list
+//public class HelloSwingTableModelSample extends ControlsFXSample {
+//
+//    @Override public String getSampleName() {
+//        return "Swing TableModel TableView";
+//    }
+//    
+//    @Override public String getJavaDocURL() {
+//        return Utils.JAVADOC_BASE + "org/controlsfx/control/table/TableModelTableView.html";
+//    }
+//    
+//    @Override public Node getPanel(final Stage stage) {
+//        TableModel swingTableModel = new DefaultTableModel(
+//            new Object[][] { /* Data: row, column */
+//                { "1", "2", "3" }, 
+//                { "4", "5", "6" },
+//                { "7", "8", "9" },
+//                { "10", "11", "12" },
+//            }, 
+//            new String[] {  /* Column names */
+//                "Column 1", "Column 2", "Column 3"
+//            }
+//        );
+//        
+//        TableModelTableView<String> tableView = new TableModelTableView<>(JavaFXTableModels.wrap(swingTableModel));
+//        
+////        tableView.getSelectionModel().selectedItemProperty().addListener((o, oldRow, newRow) -> {
+////            System.out.println("Old row: " + oldRow + ", new row: " + newRow);
+////        });
+//        
+//        VBox root = new VBox();
+//        root.getChildren().add(tableView);
+//        return root;
+//    }
+//    
+//    public static void main(String[] args) {
+//        launch(args);
+//    } 
+//}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/tableview/HelloTableRowExpander.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/tableview/HelloTableRowExpander.java
new file mode 100644
index 0000000..1a62a08
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/tableview/HelloTableRowExpander.java
@@ -0,0 +1,194 @@
+/**
+ * Copyright (c) 2016 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples.tableview;
+
+import javafx.beans.property.SimpleIntegerProperty;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.geometry.Insets;
+import javafx.scene.Node;
+import javafx.scene.control.*;
+import javafx.scene.control.cell.PropertyValueFactory;
+import javafx.scene.layout.GridPane;
+import javafx.stage.Stage;
+import org.controlsfx.ControlsFXSample;
+import org.controlsfx.control.table.TableRowExpanderColumn;
+
+public class HelloTableRowExpander extends ControlsFXSample {
+    @Override
+    public String getSampleName() {
+        return "TableRowExpanderColumn";
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public Node getPanel(Stage stage) {
+        TableView<Customer> tableView = new TableView<>();
+        TableRowExpanderColumn<Customer> expanderColumn = new TableRowExpanderColumn<>(this::createEditor);
+
+        TableColumn<Customer, Integer> idColumn = new TableColumn<>("ID");
+        idColumn.setCellValueFactory(new PropertyValueFactory<>("id"));
+        
+        TableColumn<Customer, String> nameColumn = new TableColumn<>("Name");
+        nameColumn.setCellValueFactory(new PropertyValueFactory<>("name"));
+        
+        TableColumn<Customer, String> emailColumn = new TableColumn<>("Email");
+        emailColumn.setCellValueFactory(new PropertyValueFactory<>("email"));
+
+        tableView.getColumns().addAll(expanderColumn, idColumn, nameColumn, emailColumn);
+
+        tableView.setItems(getCustomers());
+
+        return tableView;
+    }
+
+    private GridPane createEditor(TableRowExpanderColumn.TableRowDataFeatures<Customer> param) {
+        GridPane editor = new GridPane();
+        editor.setPadding(new Insets(10));
+        editor.setHgap(10);
+        editor.setVgap(5);
+
+        Customer customer = param.getValue();
+
+        TextField nameField = new TextField(customer.getName());
+        TextField emailField = new TextField(customer.getEmail());
+
+        editor.addRow(0, new Label("Name"), nameField);
+        editor.addRow(1, new Label("Email"), emailField);
+
+        Button saveButton = new Button("Save");
+        saveButton.setOnAction(event -> {
+            customer.setName(nameField.getText());
+            customer.setEmail(emailField.getText());
+            param.toggleExpanded();
+        });
+
+        Button cancelButton = new Button("Cancel");
+        cancelButton.setOnAction(event -> param.toggleExpanded());
+
+        editor.addRow(2, saveButton, cancelButton);
+
+        return editor;
+    }
+
+    @Override
+    public String getJavaDocURL() {
+        return "org/controlsfx/control/table/TableRowExpanderColumn.html";
+    }
+
+    @Override
+    public String getSampleDescription() {
+        return "An extension to TableView which lets the user expand a table row to reveal a custom "
+                + "editor right below the cells of the current row. "
+                + "Any arbitrary Node can be used as the expanded row editor and there is an API to "
+                + "toggle the expanded state of each row. "
+                + "The toggle button can be customized by providing setting a custom cellFactory "
+                + "for the TableRowExpanderColumn.";
+    }
+
+    private ObservableList<Customer> getCustomers() {
+        return FXCollections.observableArrayList(
+                new Customer(1, "Samantha Stuart", "samantha.stuart at contoso.com"),
+                new Customer(2, "Tom Marks", "tom.marks at contoso.com"),
+                new Customer(3, "Stuart Gills", "stuart.gills at contoso.com"),
+                new Customer(4, "Nicole Williams", "nicole.williams at contoso.com")
+        );
+    }
+
+    public static void main(String[] args) {
+        launch(args);
+    }
+
+    public static class Customer {
+        public SimpleIntegerProperty idProperty = new SimpleIntegerProperty(this, "id");
+        public SimpleStringProperty nameProperty = new SimpleStringProperty(this, "name");
+        public SimpleStringProperty emailProperty = new SimpleStringProperty(this, "email");
+
+        public Customer(Integer id, String name, String email) {
+            setId(id);
+            setName(name);
+            setEmail(email);
+        }
+
+        public Integer getId() {
+            return idProperty.get();
+        }
+
+        public SimpleIntegerProperty idProperty() {
+            return idProperty;
+        }
+
+        public void setId(int id) {
+            this.idProperty.set(id);
+        }
+
+        public String getName() {
+            return nameProperty.get();
+        }
+
+        public SimpleStringProperty nameProperty() {
+            return nameProperty;
+        }
+
+        public void setName(String name) {
+            this.nameProperty.set(name);
+        }
+
+        public String getEmail() {
+            return emailProperty.get();
+        }
+
+        public SimpleStringProperty emailProperty() {
+            return emailProperty;
+        }
+
+        public void setEmail(String email) {
+            this.emailProperty.set(email);
+        }
+
+        @Override
+        public String toString() {
+            return getName();
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+
+            Customer customer = (Customer) o;
+
+            return getId() != null ? getId().equals(customer.getId()) : customer.getId() == null;
+        }
+
+        @Override
+        public int hashCode() {
+            return getId() != null ? getId().hashCode() : 0;
+        }
+    }
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/textfields/HelloAutoComplete.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/textfields/HelloAutoComplete.java
new file mode 100644
index 0000000..3b6be40
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/textfields/HelloAutoComplete.java
@@ -0,0 +1,154 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples.textfields;
+
+import javafx.event.EventHandler;
+import javafx.geometry.Insets;
+import javafx.scene.Node;
+import javafx.scene.control.Label;
+import javafx.scene.control.TextField;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.Priority;
+import javafx.stage.Stage;
+import org.controlsfx.ControlsFXSample;
+import org.controlsfx.control.textfield.AutoCompletionBinding;
+import org.controlsfx.control.textfield.TextFields;
+import org.controlsfx.samples.Utils;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+public class HelloAutoComplete extends ControlsFXSample {
+
+    private AutoCompletionBinding<String> autoCompletionBinding;
+    private String[] _possibleSuggestions = {"Hey", "Hello", "Hello World", "Apple", "Cool", "Costa", "Cola", "Coca Cola"};
+    private Set<String> possibleSuggestions = new HashSet<>(Arrays.asList(_possibleSuggestions));
+    
+    private TextField learningTextField;
+
+    @Override public String getSampleName() {
+        return "AutoComplete";
+    }
+
+    @Override public String getJavaDocURL() {
+        return Utils.JAVADOC_BASE + "org/controlsfx/control/textfield/TextFields.html";
+    }
+
+    @Override public String getSampleDescription() {
+        return "AutoComplete helps a user with suggestions to type faster, " 
+                + "but does not limit the user from entering alternative text."
+                + "\n\n"
+                + "The textfields have been primed with the following words:\n"
+                + "\"Hey\", \"Hello\", \"Hello World\", \"Apple\", \"Cool\", "
+                + "\"Costa\", \"Cola\", \"Coca Cola\""
+                + "\n\n"
+                + "The 'Learning TextField' will add whatever words are typed "
+                + "to the auto-complete popup, as long as you press Enter once "
+                + "you've finished typing the word.";
+    }
+
+    @Override public Node getPanel(final Stage stage) {
+
+        BorderPane root = new BorderPane();
+
+        GridPane grid = new GridPane();
+        grid.setVgap(10);
+        grid.setHgap(10);
+        grid.setPadding(new Insets(30, 30, 0, 30));
+
+        //
+        // TextField with static auto-complete functionality
+        //
+        TextField textField = new TextField();
+
+        TextFields.bindAutoCompletion(
+                textField,
+                "Hey", "Hello", "Hello World", "Apple", "Cool", "Costa", "Cola", "Coca Cola");
+
+        grid.add(new Label("Auto-complete Text"), 0, 0);
+        grid.add(textField, 1, 0);
+        GridPane.setHgrow(textField, Priority.ALWAYS);
+
+
+        //
+        // TextField with learning auto-complete functionality
+        // Learn the word when user presses ENTER
+        //
+        learningTextField = new TextField();
+        autoCompletionBinding = TextFields.bindAutoCompletion(learningTextField, possibleSuggestions);
+        learningTextField.setOnKeyPressed(new EventHandler<KeyEvent>() {
+            @Override
+            public void handle(KeyEvent ke) {
+                switch (ke.getCode()) {
+                case ENTER:
+                    autoCompletionLearnWord(learningTextField.getText().trim());
+                    break;
+                default:
+                    break;
+                }
+            }
+        });
+
+        grid.add(new Label("Learning TextField"), 0, 1);
+        grid.add(learningTextField, 1, 1);
+        GridPane.setHgrow(learningTextField, Priority.ALWAYS);
+
+        root.setTop(grid);
+        return root;
+    }
+
+    private void autoCompletionLearnWord(String newWord){
+        possibleSuggestions.add(newWord);
+        
+        // we dispose the old binding and recreate a new binding
+        if (autoCompletionBinding != null) {
+            autoCompletionBinding.dispose();
+        }
+        autoCompletionBinding = TextFields.bindAutoCompletion(learningTextField, possibleSuggestions);
+    }
+
+    @Override public Node getControlPanel() {
+        GridPane grid = new GridPane();
+        grid.setVgap(10);
+        grid.setHgap(10);
+        grid.setPadding(new Insets(30, 30, 0, 30));
+
+        // TODO Add customization example controls
+
+
+        return grid;
+    }
+
+
+    public static void main(String[] args) {
+        launch(args);
+    }
+
+}
diff --git a/controlsfx-samples/src/main/java/org/controlsfx/samples/textfields/HelloTextFields.java b/controlsfx-samples/src/main/java/org/controlsfx/samples/textfields/HelloTextFields.java
new file mode 100644
index 0000000..99934b8
--- /dev/null
+++ b/controlsfx-samples/src/main/java/org/controlsfx/samples/textfields/HelloTextFields.java
@@ -0,0 +1,144 @@
+/**
+ * Copyright (c) 2013, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.samples.textfields;
+
+import javafx.geometry.HPos;
+import javafx.geometry.Insets;
+import javafx.scene.Node;
+import javafx.scene.control.Label;
+import javafx.scene.control.PasswordField;
+import javafx.scene.control.TextField;
+import javafx.scene.control.ToggleButton;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.GridPane;
+import javafx.scene.text.Font;
+import javafx.stage.Stage;
+
+import org.controlsfx.ControlsFXSample;
+import org.controlsfx.control.textfield.CustomPasswordField;
+import org.controlsfx.control.textfield.CustomTextField;
+import org.controlsfx.control.textfield.TextFields;
+import org.controlsfx.samples.Utils;
+
+public class HelloTextFields extends ControlsFXSample {
+    
+    private static final Image image = new Image("/org/controlsfx/samples/security-low.png");
+    
+    @Override public String getSampleName() {
+        return "TextFields";
+    }
+    
+    @Override public String getJavaDocURL() {
+        return Utils.JAVADOC_BASE + "org/controlsfx/control/textfield/TextFields.html";
+    }
+    
+    @Override public Node getPanel(Stage stage) {
+        GridPane grid = new GridPane();
+        grid.setVgap(10);
+        grid.setHgap(10);
+        grid.setPadding(new Insets(30, 30, 0, 30));
+        
+        int row = 0;
+        
+        // TextField and PasswordField labels
+        Label textFieldLabel = new Label("TextField");
+        textFieldLabel.setFont(Font.font(24));
+        GridPane.setHalignment(textFieldLabel, HPos.CENTER);
+        Label passwordFieldLabel = new Label("PasswordField");
+        passwordFieldLabel.setFont(Font.font(24));
+        GridPane.setHalignment(passwordFieldLabel, HPos.CENTER);
+        grid.add(textFieldLabel, 1, row);
+        grid.add(passwordFieldLabel, 2, row);
+        row++;
+
+        // normal TextField / PasswordField
+        grid.add(new Label("Normal TextField / PasswordField: "), 0, row);
+        grid.add(new TextField(), 1, row);
+        grid.add(new PasswordField(), 2, row++);
+        
+        // Clearable*Field
+        grid.add(new Label("Clearable*Field: "), 0, row);
+        TextField clearableTextField = TextFields.createClearableTextField();
+        PasswordField clearablePasswordField = TextFields.createClearablePasswordField();
+		ToggleButton btToggle = new ToggleButton("Enable/Disable");
+		ToggleButton btEditable = new ToggleButton("Toggle Editable");
+		clearableTextField.disableProperty().bind(btToggle.selectedProperty());
+		clearablePasswordField.disableProperty().bind(btToggle.selectedProperty());
+		clearableTextField.editableProperty().bindBidirectional(btEditable.selectedProperty());
+		clearablePasswordField.editableProperty().bindBidirectional(btEditable.selectedProperty());
+		btEditable.setSelected(true);
+		grid.add(clearableTextField, 1, row);
+		grid.add(clearablePasswordField, 2, row);
+		grid.add(btEditable, 3, row);
+		grid.add(btToggle, 4, row++);
+        
+        // Custom*Field
+        grid.add(new Label("Custom*Field (no additional nodes): "), 0, row);
+        grid.add(new CustomTextField(), 1, row);
+        grid.add(new CustomPasswordField(), 2, row++);
+        
+        // Custom*Field (w/ right node)
+        grid.add(new Label("Custom*Field (w/ right node): "), 0, row);
+        CustomTextField customTextField1 = new CustomTextField();
+        customTextField1.setRight(new ImageView(image));
+        grid.add(customTextField1, 1, row);
+        
+        CustomPasswordField customPasswordField1 = new CustomPasswordField();
+        customPasswordField1.setRight(new ImageView(image));
+        grid.add(customPasswordField1, 2, row++);
+        
+        // Custom*Field (w/ left node)
+        grid.add(new Label("Custom*Field (w/ left node): "), 0, row);
+        CustomTextField customTextField2 = new CustomTextField();
+        customTextField2.setLeft(new ImageView(image));
+        grid.add(customTextField2, 1, row);
+        
+        CustomPasswordField customPasswordField2 = new CustomPasswordField();
+        customPasswordField2.setLeft(new ImageView(image));
+        grid.add(customPasswordField2, 2, row++);
+        
+        // Custom*Field (w/ left + right node)
+        grid.add(new Label("Custom*Field (w/ left + right node): "), 0, row);
+        CustomTextField customTextField3 = new CustomTextField();
+        customTextField3.setLeft(new ImageView(image));
+        customTextField3.setRight(new ImageView(image));
+        grid.add(customTextField3, 1, row);
+        
+        CustomPasswordField customPasswordField3 = new CustomPasswordField();
+        customPasswordField3.setLeft(new ImageView(image));
+        customPasswordField3.setRight(new ImageView(image));
+        grid.add(customPasswordField3, 2, row++);
+        
+        return grid;
+    }
+    
+    public static void main(String[] args) {
+        launch(args);
+    }
+    
+}
diff --git a/controlsfx-samples/src/main/resources/META-INF/services/fxsampler.FXSamplerProject b/controlsfx-samples/src/main/resources/META-INF/services/fxsampler.FXSamplerProject
new file mode 100644
index 0000000..c70dc76
--- /dev/null
+++ b/controlsfx-samples/src/main/resources/META-INF/services/fxsampler.FXSamplerProject
@@ -0,0 +1 @@
+org.controlsfx.ControlsFXSampler
\ No newline at end of file
diff --git a/controlsfx-samples/src/main/resources/org/controlsfx/samples/ControlsFX.png b/controlsfx-samples/src/main/resources/org/controlsfx/samples/ControlsFX.png
new file mode 100644
index 0000000..b33c42f
Binary files /dev/null and b/controlsfx-samples/src/main/resources/org/controlsfx/samples/ControlsFX.png differ
diff --git a/controlsfx-samples/src/main/resources/org/controlsfx/samples/ammunationLogo.JPG b/controlsfx-samples/src/main/resources/org/controlsfx/samples/ammunationLogo.JPG
new file mode 100644
index 0000000..6800639
Binary files /dev/null and b/controlsfx-samples/src/main/resources/org/controlsfx/samples/ammunationLogo.JPG differ
diff --git a/controlsfx-samples/src/main/resources/org/controlsfx/samples/apertureLogo.png b/controlsfx-samples/src/main/resources/org/controlsfx/samples/apertureLogo.png
new file mode 100644
index 0000000..dd3fdd0
Binary files /dev/null and b/controlsfx-samples/src/main/resources/org/controlsfx/samples/apertureLogo.png differ
diff --git a/controlsfx-samples/src/main/resources/org/controlsfx/samples/bar.png b/controlsfx-samples/src/main/resources/org/controlsfx/samples/bar.png
new file mode 100644
index 0000000..04f89b6
Binary files /dev/null and b/controlsfx-samples/src/main/resources/org/controlsfx/samples/bar.png differ
diff --git a/controlsfx-samples/src/main/resources/org/controlsfx/samples/controlsfx-logo.png b/controlsfx-samples/src/main/resources/org/controlsfx/samples/controlsfx-logo.png
new file mode 100644
index 0000000..c96761e
Binary files /dev/null and b/controlsfx-samples/src/main/resources/org/controlsfx/samples/controlsfx-logo.png differ
diff --git a/controlsfx-samples/src/main/resources/org/controlsfx/samples/decorations.css b/controlsfx-samples/src/main/resources/org/controlsfx/samples/decorations.css
new file mode 100644
index 0000000..09fc439
--- /dev/null
+++ b/controlsfx-samples/src/main/resources/org/controlsfx/samples/decorations.css
@@ -0,0 +1,7 @@
+.warning {
+    -fx-background-color: #FF000055;
+}
+
+.success {
+    -fx-background-color: #00FF0055;
+}
\ No newline at end of file
diff --git a/controlsfx-samples/src/main/resources/org/controlsfx/samples/dialogs/login.png b/controlsfx-samples/src/main/resources/org/controlsfx/samples/dialogs/login.png
new file mode 100644
index 0000000..4e37d55
Binary files /dev/null and b/controlsfx-samples/src/main/resources/org/controlsfx/samples/dialogs/login.png differ
diff --git a/controlsfx-samples/src/main/resources/org/controlsfx/samples/duke_wave.png b/controlsfx-samples/src/main/resources/org/controlsfx/samples/duke_wave.png
new file mode 100644
index 0000000..a182ef4
Binary files /dev/null and b/controlsfx-samples/src/main/resources/org/controlsfx/samples/duke_wave.png differ
diff --git a/controlsfx-samples/src/main/resources/org/controlsfx/samples/exclamation.png b/controlsfx-samples/src/main/resources/org/controlsfx/samples/exclamation.png
new file mode 100644
index 0000000..e286ae2
Binary files /dev/null and b/controlsfx-samples/src/main/resources/org/controlsfx/samples/exclamation.png differ
diff --git a/controlsfx-samples/src/main/resources/org/controlsfx/samples/flowers.png b/controlsfx-samples/src/main/resources/org/controlsfx/samples/flowers.png
new file mode 100644
index 0000000..326c3fd
Binary files /dev/null and b/controlsfx-samples/src/main/resources/org/controlsfx/samples/flowers.png differ
diff --git a/controlsfx-samples/src/main/resources/org/controlsfx/samples/icomoon.ttf b/controlsfx-samples/src/main/resources/org/controlsfx/samples/icomoon.ttf
new file mode 100644
index 0000000..6d64241
Binary files /dev/null and b/controlsfx-samples/src/main/resources/org/controlsfx/samples/icomoon.ttf differ
diff --git a/controlsfx-samples/src/main/resources/org/controlsfx/samples/information.png b/controlsfx-samples/src/main/resources/org/controlsfx/samples/information.png
new file mode 100644
index 0000000..12cd1ae
Binary files /dev/null and b/controlsfx-samples/src/main/resources/org/controlsfx/samples/information.png differ
diff --git a/controlsfx-samples/src/main/resources/org/controlsfx/samples/notification-pane-warning.png b/controlsfx-samples/src/main/resources/org/controlsfx/samples/notification-pane-warning.png
new file mode 100644
index 0000000..eee1126
Binary files /dev/null and b/controlsfx-samples/src/main/resources/org/controlsfx/samples/notification-pane-warning.png differ
diff --git a/controlsfx-samples/src/main/resources/org/controlsfx/samples/nukaColaLogo.png b/controlsfx-samples/src/main/resources/org/controlsfx/samples/nukaColaLogo.png
new file mode 100644
index 0000000..034b2b4
Binary files /dev/null and b/controlsfx-samples/src/main/resources/org/controlsfx/samples/nukaColaLogo.png differ
diff --git a/controlsfx-samples/src/main/resources/org/controlsfx/samples/paynsprayLogo.jpg b/controlsfx-samples/src/main/resources/org/controlsfx/samples/paynsprayLogo.jpg
new file mode 100644
index 0000000..9687aba
Binary files /dev/null and b/controlsfx-samples/src/main/resources/org/controlsfx/samples/paynsprayLogo.jpg differ
diff --git a/controlsfx-samples/src/main/resources/org/controlsfx/samples/raptureLogo.png b/controlsfx-samples/src/main/resources/org/controlsfx/samples/raptureLogo.png
new file mode 100644
index 0000000..672af58
Binary files /dev/null and b/controlsfx-samples/src/main/resources/org/controlsfx/samples/raptureLogo.png differ
diff --git a/controlsfx-samples/src/main/resources/org/controlsfx/samples/security-low.png b/controlsfx-samples/src/main/resources/org/controlsfx/samples/security-low.png
new file mode 100644
index 0000000..6e8e42c
Binary files /dev/null and b/controlsfx-samples/src/main/resources/org/controlsfx/samples/security-low.png differ
diff --git a/controlsfx-samples/src/main/resources/org/controlsfx/samples/spreadsheetSample.css b/controlsfx-samples/src/main/resources/org/controlsfx/samples/spreadsheetSample.css
new file mode 100644
index 0000000..f14afc3
--- /dev/null
+++ b/controlsfx-samples/src/main/resources/org/controlsfx/samples/spreadsheetSample.css
@@ -0,0 +1,113 @@
+
+/* FIRST CELL */
+.spreadsheet-cell.first-cell:filled:selected ,
+.spreadsheet-cell.first-cell:filled:focused:selected,
+.spreadsheet-cell.first-cell:filled:focused:selected:hover {
+    -fx-background-color: #8cb1ff;
+}
+.spreadsheet-cell.first-cell:hover {
+    -fx-background-color: #988490;
+    -fx-background-insets: 0, 0 0 1 0;
+}
+
+.spreadsheet-cell.first-cell{
+    -fx-background-color:-fx-table-cell-border-color,#77ABD6 ;
+    -fx-background-insets: 0, 0 1 1 0;
+    -fx-alignment: CENTER_LEFT;
+}
+
+/* SEPARATOR */
+.spreadsheet-cell.separator{
+    -fx-background-color: white;
+}
+
+.spreadsheet-cell.separator:selected{
+    -fx-background-color: #8cb1ff;
+}
+
+/* COMPAGNIES */
+.spreadsheet-cell.compagny:filled:selected ,
+.spreadsheet-cell.compagny:filled:focused:selected,
+.spreadsheet-cell.compagny:filled:focused:selected:hover {
+    -fx-background-color: #8cb1ff;
+    -fx-text-fill: -fx-selection-bar-text;
+}
+.spreadsheet-cell.compagny:hover,
+.spreadsheet-cell.compagny:filled:focused {
+    -fx-background-color: #988490;
+    -fx-text-fill: -fx-text-inner-color;
+    -fx-background-insets: 0, 0 0 1 0;
+}
+
+.spreadsheet-cell.compagny{
+    -fx-background-color: -fx-table-cell-border-color, #ABC8E2;
+    -fx-background-insets: 0, 0 1 1 0;
+    -fx-alignment: center;
+}
+
+/** LOGO CELL **/
+
+.spreadsheet-cell.logo{
+    -fx-background-color: white;
+    -fx-alignment: center;
+}
+.spreadsheet-cell.logo:hover{
+    -fx-background-color: #988490;
+}
+.spreadsheet-cell.logo:selected{
+    -fx-background-color: #8cb1ff;
+    -fx-alignment: center;
+}
+
+/* CELL EVERY 5 ROW */
+.spreadsheet-cell.five_rows:filled:selected ,
+.spreadsheet-cell.five_rows:filled:focused:selected,
+.spreadsheet-cell.five_rows:filled:focused:selected:hover {
+    -fx-background-color: #8cb1ff ;
+    -fx-text-fill: -fx-selection-bar-text;
+}
+.spreadsheet-cell.five_rows:hover,
+.spreadsheet-cell.five_rows:filled:focused {
+    -fx-background-color: #988490;
+    -fx-text-fill: -fx-text-inner-color;
+    -fx-background-insets: 0, 0 0 1 0;
+}
+
+.spreadsheet-cell.five_rows{
+    -fx-background-color: -fx-table-cell-border-color, #ccffff;
+    -fx-background-insets: 0, 0 1 1 0;
+    -fx-alignment: center;
+}
+
+/* CELL SPANNING */
+.spreadsheet-cell.span{
+    -fx-alignment: center;
+    -fx-border-color : #3B0405;
+    -fx-border-width : 1;
+}
+.spreadsheet-cell.span:hover,
+.spreadsheet-cell.span:selected{
+    -fx-alignment: center;
+}
+
+/* PICKERS */
+.picker-label{
+    -fx-graphic: url("information.png"); 
+    -fx-background-color: white;
+    -fx-padding: 0 0 0 0;
+    -fx-alignment: center;
+}
+
+.picker-label-exclamation{
+    -fx-graphic: url("exclamation.png"); 
+    -fx-background-color: transparent;
+    -fx-padding: 0 0 0 0;
+    -fx-alignment: center;
+}
+
+.picker-label-security{
+    -fx-graphic: url("security-low.png"); 
+    -fx-background-color: transparent;
+    -fx-padding: 0 0 0 0;
+    -fx-alignment: center;
+}
\ No newline at end of file
diff --git a/controlsfx-samples/src/main/resources/org/controlsfx/samples/toggleSwitchSample.css b/controlsfx-samples/src/main/resources/org/controlsfx/samples/toggleSwitchSample.css
new file mode 100644
index 0000000..bb50b64
--- /dev/null
+++ b/controlsfx-samples/src/main/resources/org/controlsfx/samples/toggleSwitchSample.css
@@ -0,0 +1,9 @@
+.header{
+    -fx-font-size: 3.5em;
+    -fx-text-fill: black;
+}
+
+.item-title{
+    -fx-font-size: 1em;
+    -fx-text-fill: black;
+}
\ No newline at end of file
diff --git a/controlsfx-samples/src/main/resources/org/controlsfx/samples/umbrellacorporation.png b/controlsfx-samples/src/main/resources/org/controlsfx/samples/umbrellacorporation.png
new file mode 100644
index 0000000..03616c1
Binary files /dev/null and b/controlsfx-samples/src/main/resources/org/controlsfx/samples/umbrellacorporation.png differ
diff --git a/controlsfx-samples/src/main/resources/org/controlsfx/samples/validation.css b/controlsfx-samples/src/main/resources/org/controlsfx/samples/validation.css
new file mode 100644
index 0000000..9eb7a4d
--- /dev/null
+++ b/controlsfx-samples/src/main/resources/org/controlsfx/samples/validation.css
@@ -0,0 +1,7 @@
+.error {
+    -fx-effect: dropshadow(three-pass-box, darkred, 7, 0, 0, 0);
+}
+
+.warning {
+    -fx-effect: dropshadow(three-pass-box, gold, 14, 0, 0, 0);
+}
\ No newline at end of file
diff --git a/controlsfx/.classpath b/controlsfx/.classpath
new file mode 100644
index 0000000..d09d017
--- /dev/null
+++ b/controlsfx/.classpath
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="src" path="src/main/java"/>
+	<classpathentry kind="src" path="src/main/resources"/>
+	<classpathentry exported="true" kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+	<classpathentry exported="true" kind="con" path="org.springsource.ide.eclipse.gradle.classpathcontainer"/>
+	<classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/controlsfx/.project b/controlsfx/.project
new file mode 100644
index 0000000..3d2f8b4
--- /dev/null
+++ b/controlsfx/.project
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>controlsfx</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.springsource.ide.eclipse.gradle.core.nature</nature>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+		<nature>org.eclipse.jdt.groovy.core.groovyNature</nature>
+	</natures>
+</projectDescription>
diff --git a/controlsfx/build.gradle b/controlsfx/build.gradle
new file mode 100644
index 0000000..f5ed7ab
--- /dev/null
+++ b/controlsfx/build.gradle
@@ -0,0 +1,144 @@
+import org.apache.tools.ant.filters.EscapeUnicode
+
+dependencies {
+    try {
+        jdk files(jfxrtJar)
+    } catch (MissingPropertyException pne) {
+        // javafx plugin will provide in this case
+    }
+}
+
+ext {
+    transifex_username = ""
+    transifex_password = ""
+}
+
+configurations {
+    jdk
+    
+    if (project.hasProperty("transifex.username")) {
+        transifex_username = project.property("transifex.username")
+    }
+    if (project.hasProperty("transifex.password")) {
+        transifex_password = project.getProperty("transifex.password")
+    }
+}
+
+sourceSets {
+    main {
+        compileClasspath += configurations.jdk
+    }
+}
+
+
+task downloadTranslations << {
+    description = "Download translations from Transifex"
+    
+    if (transifex_username.equals("") || transifex_password.equals("")) {
+        logger.warn "----------------------------------------------------------"
+        logger.warn "Cannot determine Transifex Username/Password."
+        logger.warn "If you want to build ControlsFX with its translations then"
+        logger.warn "you need to create a Transifex account and set"
+        logger.warn "transifex.username & transifex.password properties in your" 
+        logger.warn "gradle.properties file." 
+        logger.warn "----------------------------------------------------------"
+    } else {
+        javaexec {
+            main = 'impl.build.transifex.Transifex'
+            classpath = sourceSets.main.runtimeClasspath
+            systemProperty 'transifex.username', transifex_username
+            systemProperty 'transifex.password', transifex_password
+        }
+    }
+}
+
+processResources.finalizedBy(downloadTranslations)
+
+task compileCSS << {
+    ant.delete (includeEmptyDirs: 'true') { 
+        fileset(dir: file("build/resources/main"), includes: "**/*.bss") 
+    }
+    javaexec {
+        main = "com.sun.javafx.tools.packager.Main"
+        classpath = files("${System.properties['java.home']}/../lib/ant-javafx.jar")
+        args = [ "-createbss",
+            "-srcdir", "src/main/resources",
+            "-outdir", "build/resources/main"
+        ]
+    }
+}
+
+processResources.finalizedBy(compileCSS)
+
+javadoc {
+    exclude 'impl/*'
+    //failOnError = true
+    classpath = project.sourceSets.main.runtimeClasspath + configurations.jdk
+    
+    options.windowTitle("ControlsFX Project ${version}")
+    options.links("http://docs.oracle.com/javase/8/docs/api/");
+    options.links("http://docs.oracle.com/javase/8/javafx/api/");
+    options.addBooleanOption("Xdoclint:none").setValue(true);
+    options.addBooleanOption("javafx").setValue(true);
+    options.overview("${projectDir}/src/main/docs/overview.html");
+    
+    // All doc-files are located in src/main/docs because Gradle's javadoc doesn't copy
+    // over the doc-files if they are embedded with the sources. I find this arrangement
+    // somewhat cleaner anyway (never was a fan of mixing javadoc files with the sources)
+    doLast {
+        copy {
+            from "src/main/docs"
+            into "$buildDir/docs/javadoc"
+        }
+    }
+}
+
+jar {
+    //exclude '**/16/*'
+    exclude '**/32/*'
+    exclude '**/64/*'
+    exclude '**/128/*'
+    exclude '**/oxygen/svg/*'
+    exclude '**/impl/build/**'
+
+    manifest { // the manifest of the default jar is of type OsgiManifest
+        attributes (\
+            'Specification-Title': specification_title,\
+            'Specification-Version': specification_version,\
+            'Implementation-Title': 'ControlsFX',\
+            'Implementation-Version': version,\
+            'Bundle-Name': 'ControlsFX'
+        )
+        instruction 'Bundle-Description', 'High quality UI controls and other tools to complement the core JavaFX distribution'        
+	instruction 'Import-Package',
+                '!org.controlsfx*',
+                '*'
+
+        instruction 'Export-Package',
+                '!impl.org.controlsfx.*',
+                'org.controlsfx.*'
+    }
+}
+
+task native2ascii(type:Copy) {
+    // Files are downloaded with extension utf8.
+    // Here they are unicode escaped then renamed .properties...
+    from ("$buildDir/resources/main") {
+        include('**/controlsfx_*.utf8') 
+        filesMatching("controlsfx_*.utf8") {
+            println "  native2ascii: $name"
+            filter(EscapeUnicode)
+            name = name[0..-6] + '.properties'
+        } 
+    } into "$buildDir/resources/main"
+}
+
+task deleteUtf8Files(type: Delete) {
+    delete fileTree(dir: "$buildDir/resources/main", include: "**/controlsfx_*.utf8")
+}
+
+downloadTranslations.finalizedBy(native2ascii)
+native2ascii.finalizedBy(deleteUtf8Files)
+
+//jar.dependsOn(native2ascii)
+//jar.dependsOn(deleteUtf8Files)
diff --git a/controlsfx/src/main/docs/ControlsFX.png b/controlsfx/src/main/docs/ControlsFX.png
new file mode 100644
index 0000000..b33c42f
Binary files /dev/null and b/controlsfx/src/main/docs/ControlsFX.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/ToggleSwitch.png b/controlsfx/src/main/docs/org/controlsfx/control/ToggleSwitch.png
new file mode 100644
index 0000000..3ff5cbb
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/ToggleSwitch.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/action/actionGroup-contextmenu.png b/controlsfx/src/main/docs/org/controlsfx/control/action/actionGroup-contextmenu.png
new file mode 100644
index 0000000..da9db8f
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/action/actionGroup-contextmenu.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/action/actionGroup-menubar.png b/controlsfx/src/main/docs/org/controlsfx/control/action/actionGroup-menubar.png
new file mode 100644
index 0000000..6b65f08
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/action/actionGroup-menubar.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/action/actionGroup-toolbar.png b/controlsfx/src/main/docs/org/controlsfx/control/action/actionGroup-toolbar.png
new file mode 100644
index 0000000..866885c
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/action/actionGroup-toolbar.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/breadCrumbBar.png b/controlsfx/src/main/docs/org/controlsfx/control/breadCrumbBar.png
new file mode 100644
index 0000000..5233b1c
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/breadCrumbBar.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/buttonBar-linux.png b/controlsfx/src/main/docs/org/controlsfx/control/buttonBar-linux.png
new file mode 100644
index 0000000..b7154d5
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/buttonBar-linux.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/buttonBar-mac.png b/controlsfx/src/main/docs/org/controlsfx/control/buttonBar-mac.png
new file mode 100644
index 0000000..34dfbfa
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/buttonBar-mac.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/buttonBar-windows.png b/controlsfx/src/main/docs/org/controlsfx/control/buttonBar-windows.png
new file mode 100644
index 0000000..c5db4fd
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/buttonBar-windows.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/checkComboBox.png b/controlsfx/src/main/docs/org/controlsfx/control/checkComboBox.png
new file mode 100644
index 0000000..ee8a9b7
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/checkComboBox.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/checkListView.png b/controlsfx/src/main/docs/org/controlsfx/control/checkListView.png
new file mode 100644
index 0000000..6b615f0
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/checkListView.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/checkTreeView.png b/controlsfx/src/main/docs/org/controlsfx/control/checkTreeView.png
new file mode 100644
index 0000000..aa1c629
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/checkTreeView.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/gridView.png b/controlsfx/src/main/docs/org/controlsfx/control/gridView.png
new file mode 100644
index 0000000..8400531
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/gridView.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/hiddenSidesPane.png b/controlsfx/src/main/docs/org/controlsfx/control/hiddenSidesPane.png
new file mode 100644
index 0000000..d267c91
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/hiddenSidesPane.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/hyperlinkLabel.PNG b/controlsfx/src/main/docs/org/controlsfx/control/hyperlinkLabel.PNG
new file mode 100644
index 0000000..460c4a4
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/hyperlinkLabel.PNG differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/infoOverlay.png b/controlsfx/src/main/docs/org/controlsfx/control/infoOverlay.png
new file mode 100644
index 0000000..ded23da
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/infoOverlay.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/list-selection-view.png b/controlsfx/src/main/docs/org/controlsfx/control/list-selection-view.png
new file mode 100644
index 0000000..7c0ecda
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/list-selection-view.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/masterDetailPane.png b/controlsfx/src/main/docs/org/controlsfx/control/masterDetailPane.png
new file mode 100644
index 0000000..08f2b60
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/masterDetailPane.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/notication-pane-dark-bottom.png b/controlsfx/src/main/docs/org/controlsfx/control/notication-pane-dark-bottom.png
new file mode 100644
index 0000000..8477a93
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/notication-pane-dark-bottom.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/notication-pane-dark-top.png b/controlsfx/src/main/docs/org/controlsfx/control/notication-pane-dark-top.png
new file mode 100644
index 0000000..fd4b723
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/notication-pane-dark-top.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/notication-pane-light-bottom.png b/controlsfx/src/main/docs/org/controlsfx/control/notication-pane-light-bottom.png
new file mode 100644
index 0000000..fedebe7
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/notication-pane-light-bottom.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/notication-pane-light-top.png b/controlsfx/src/main/docs/org/controlsfx/control/notication-pane-light-top.png
new file mode 100644
index 0000000..fc6263a
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/notication-pane-light-top.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/notifications.png b/controlsfx/src/main/docs/org/controlsfx/control/notifications.png
new file mode 100644
index 0000000..98c691b
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/notifications.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/plus-minus-slider.png b/controlsfx/src/main/docs/org/controlsfx/control/plus-minus-slider.png
new file mode 100644
index 0000000..9119a78
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/plus-minus-slider.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/popover-accordion.png b/controlsfx/src/main/docs/org/controlsfx/control/popover-accordion.png
new file mode 100644
index 0000000..7e44d2f
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/popover-accordion.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/popover-detached.png b/controlsfx/src/main/docs/org/controlsfx/control/popover-detached.png
new file mode 100644
index 0000000..af69d1f
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/popover-detached.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/popover.png b/controlsfx/src/main/docs/org/controlsfx/control/popover.png
new file mode 100644
index 0000000..a0f6d35
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/popover.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/propertySheet.PNG b/controlsfx/src/main/docs/org/controlsfx/control/propertySheet.PNG
new file mode 100644
index 0000000..b868051
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/propertySheet.PNG differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/rangeSlider-horizontal.png b/controlsfx/src/main/docs/org/controlsfx/control/rangeSlider-horizontal.png
new file mode 100644
index 0000000..59c3139
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/rangeSlider-horizontal.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/rangeSlider-vertical.png b/controlsfx/src/main/docs/org/controlsfx/control/rangeSlider-vertical.png
new file mode 100644
index 0000000..f35946e
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/rangeSlider-vertical.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/rating-horizontal.png b/controlsfx/src/main/docs/org/controlsfx/control/rating-horizontal.png
new file mode 100644
index 0000000..2b9af33
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/rating-horizontal.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/rating-partial.png b/controlsfx/src/main/docs/org/controlsfx/control/rating-partial.png
new file mode 100644
index 0000000..417359d
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/rating-partial.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/rating-vertical.png b/controlsfx/src/main/docs/org/controlsfx/control/rating-vertical.png
new file mode 100644
index 0000000..89f47cb
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/rating-vertical.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/segmentedButton.png b/controlsfx/src/main/docs/org/controlsfx/control/segmentedButton.png
new file mode 100644
index 0000000..fd37c15
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/segmentedButton.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/snapshotView.png b/controlsfx/src/main/docs/org/controlsfx/control/snapshotView.png
new file mode 100644
index 0000000..e3e9135
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/snapshotView.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/spreadsheet/dateEditor.png b/controlsfx/src/main/docs/org/controlsfx/control/spreadsheet/dateEditor.png
new file mode 100644
index 0000000..865e0e9
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/spreadsheet/dateEditor.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/spreadsheet/dateFormat.PNG b/controlsfx/src/main/docs/org/controlsfx/control/spreadsheet/dateFormat.PNG
new file mode 100644
index 0000000..03d8ad6
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/spreadsheet/dateFormat.PNG differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/spreadsheet/doubleEditor.png b/controlsfx/src/main/docs/org/controlsfx/control/spreadsheet/doubleEditor.png
new file mode 100644
index 0000000..f7c191d
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/spreadsheet/doubleEditor.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/spreadsheet/editorScheme.png b/controlsfx/src/main/docs/org/controlsfx/control/spreadsheet/editorScheme.png
new file mode 100644
index 0000000..6a9f801
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/spreadsheet/editorScheme.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/spreadsheet/fixedColumn.png b/controlsfx/src/main/docs/org/controlsfx/control/spreadsheet/fixedColumn.png
new file mode 100644
index 0000000..4a45e56
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/spreadsheet/fixedColumn.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/spreadsheet/graphicNodeToCell.png b/controlsfx/src/main/docs/org/controlsfx/control/spreadsheet/graphicNodeToCell.png
new file mode 100644
index 0000000..438b253
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/spreadsheet/graphicNodeToCell.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/spreadsheet/listEditor.png b/controlsfx/src/main/docs/org/controlsfx/control/spreadsheet/listEditor.png
new file mode 100644
index 0000000..d3c04a9
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/spreadsheet/listEditor.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/spreadsheet/pickers.PNG b/controlsfx/src/main/docs/org/controlsfx/control/spreadsheet/pickers.PNG
new file mode 100644
index 0000000..7b5a378
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/spreadsheet/pickers.PNG differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/spreadsheet/spanType.png b/controlsfx/src/main/docs/org/controlsfx/control/spreadsheet/spanType.png
new file mode 100644
index 0000000..e67beff
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/spreadsheet/spanType.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/spreadsheet/spreadsheetView.png b/controlsfx/src/main/docs/org/controlsfx/control/spreadsheet/spreadsheetView.png
new file mode 100644
index 0000000..45cc9fb
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/spreadsheet/spreadsheetView.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/spreadsheet/textEditor.png b/controlsfx/src/main/docs/org/controlsfx/control/spreadsheet/textEditor.png
new file mode 100644
index 0000000..99d1eaa
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/spreadsheet/textEditor.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/spreadsheet/triangleCell.PNG b/controlsfx/src/main/docs/org/controlsfx/control/spreadsheet/triangleCell.PNG
new file mode 100644
index 0000000..7096cfc
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/spreadsheet/triangleCell.PNG differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/statusbar-items.png b/controlsfx/src/main/docs/org/controlsfx/control/statusbar-items.png
new file mode 100644
index 0000000..1b42595
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/statusbar-items.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/statusbar-progress.png b/controlsfx/src/main/docs/org/controlsfx/control/statusbar-progress.png
new file mode 100644
index 0000000..8d3f5a0
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/statusbar-progress.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/statusbar.png b/controlsfx/src/main/docs/org/controlsfx/control/statusbar.png
new file mode 100644
index 0000000..61b4828
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/statusbar.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/task-monitor.png b/controlsfx/src/main/docs/org/controlsfx/control/task-monitor.png
new file mode 100644
index 0000000..e7ba29e
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/task-monitor.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/textfield/autoCompletion.png b/controlsfx/src/main/docs/org/controlsfx/control/textfield/autoCompletion.png
new file mode 100644
index 0000000..e6ef04d
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/textfield/autoCompletion.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/textfield/customTextField.png b/controlsfx/src/main/docs/org/controlsfx/control/textfield/customTextField.png
new file mode 100644
index 0000000..fce2285
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/textfield/customTextField.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/control/wizard.png b/controlsfx/src/main/docs/org/controlsfx/control/wizard.png
new file mode 100644
index 0000000..5f09be8
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/control/wizard.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-choicebox-masthead.png b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-choicebox-masthead.png
new file mode 100644
index 0000000..844c2f7
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-choicebox-masthead.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-choicebox-no-masthead.png b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-choicebox-no-masthead.png
new file mode 100644
index 0000000..badc588
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-choicebox-no-masthead.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-commandlink-masthead.png b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-commandlink-masthead.png
new file mode 100644
index 0000000..9f074b6
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-commandlink-masthead.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-commandlink-no-masthead.png b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-commandlink-no-masthead.png
new file mode 100644
index 0000000..6460596
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-commandlink-no-masthead.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-confirmation-masthead.png b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-confirmation-masthead.png
new file mode 100644
index 0000000..f3c2c53
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-confirmation-masthead.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-confirmation-no-masthead.png b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-confirmation-no-masthead.png
new file mode 100644
index 0000000..52ad693
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-confirmation-no-masthead.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-error-masthead.png b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-error-masthead.png
new file mode 100644
index 0000000..bae849b
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-error-masthead.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-error-no-masthead.png b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-error-no-masthead.png
new file mode 100644
index 0000000..082e40d
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-error-no-masthead.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-exception-expanded-masthead.png b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-exception-expanded-masthead.png
new file mode 100644
index 0000000..b991657
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-exception-expanded-masthead.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-exception-expanded-no-masthead.png b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-exception-expanded-no-masthead.png
new file mode 100644
index 0000000..ac8f847
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-exception-expanded-no-masthead.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-exception-masthead.png b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-exception-masthead.png
new file mode 100644
index 0000000..7b79860
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-exception-masthead.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-exception-new-window.png b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-exception-new-window.png
new file mode 100644
index 0000000..c1273b7
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-exception-new-window.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-exception-no-masthead.png b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-exception-no-masthead.png
new file mode 100644
index 0000000..0aa6058
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-exception-no-masthead.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-font-selector.png b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-font-selector.png
new file mode 100644
index 0000000..7bb8c1b
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-font-selector.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-information-masthead.png b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-information-masthead.png
new file mode 100644
index 0000000..5bf47cc
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-information-masthead.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-information-no-masthead.png b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-information-no-masthead.png
new file mode 100644
index 0000000..7741bbd
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-information-no-masthead.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-login-sample.png b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-login-sample.png
new file mode 100644
index 0000000..ba3f9be
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-login-sample.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-overview.png b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-overview.png
new file mode 100644
index 0000000..2a1dcbe
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-overview.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-progress-with-progress-message.png b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-progress-with-progress-message.png
new file mode 100644
index 0000000..bb6d71d
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-progress-with-progress-message.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-progress.png b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-progress.png
new file mode 100644
index 0000000..7393293
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-progress.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-style/cross-platform.png b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-style/cross-platform.png
new file mode 100644
index 0000000..186c493
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-style/cross-platform.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-style/linux-native-titlebar.png b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-style/linux-native-titlebar.png
new file mode 100644
index 0000000..25d4fb0
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-style/linux-native-titlebar.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-style/linux-undecorated-dialog.png b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-style/linux-undecorated-dialog.png
new file mode 100644
index 0000000..84779e3
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-style/linux-undecorated-dialog.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-style/mac-native-titlebar.png b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-style/mac-native-titlebar.png
new file mode 100644
index 0000000..fb345c2
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-style/mac-native-titlebar.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-style/windows-8-lightweight-cross-platform.png b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-style/windows-8-lightweight-cross-platform.png
new file mode 100644
index 0000000..11d041c
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-style/windows-8-lightweight-cross-platform.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-style/windows-8-lightweight-undecorated.png b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-style/windows-8-lightweight-undecorated.png
new file mode 100644
index 0000000..1876b4d
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-style/windows-8-lightweight-undecorated.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-style/windows-8-native-titlebar.png b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-style/windows-8-native-titlebar.png
new file mode 100644
index 0000000..c0bbdd6
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-style/windows-8-native-titlebar.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-text-input-masthead.png b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-text-input-masthead.png
new file mode 100644
index 0000000..9ad3756
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-text-input-masthead.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-text-input-no-masthead.png b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-text-input-no-masthead.png
new file mode 100644
index 0000000..3ae4264
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-text-input-no-masthead.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-warning-masthead.png b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-warning-masthead.png
new file mode 100644
index 0000000..02bba84
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-warning-masthead.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-warning-no-masthead.png b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-warning-no-masthead.png
new file mode 100644
index 0000000..e466e05
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/dialog/dialog-warning-no-masthead.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/glyphfont/glyphFont.png b/controlsfx/src/main/docs/org/controlsfx/glyphfont/glyphFont.png
new file mode 100644
index 0000000..ed8b0b4
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/glyphfont/glyphFont.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/tools/borders-etchedBorder.png b/controlsfx/src/main/docs/org/controlsfx/tools/borders-etchedBorder.png
new file mode 100644
index 0000000..800b7b1
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/tools/borders-etchedBorder.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/tools/borders-lineBorder.png b/controlsfx/src/main/docs/org/controlsfx/tools/borders-lineBorder.png
new file mode 100644
index 0000000..e2a7837
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/tools/borders-lineBorder.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/tools/borders-twoLines.png b/controlsfx/src/main/docs/org/controlsfx/tools/borders-twoLines.png
new file mode 100644
index 0000000..2f00fe5
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/tools/borders-twoLines.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/validation/decoration/CompoundValidationDecoration.png b/controlsfx/src/main/docs/org/controlsfx/validation/decoration/CompoundValidationDecoration.png
new file mode 100644
index 0000000..93a2c5d
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/validation/decoration/CompoundValidationDecoration.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/validation/decoration/GraphicValidationDecorationWithTooltip.png b/controlsfx/src/main/docs/org/controlsfx/validation/decoration/GraphicValidationDecorationWithTooltip.png
new file mode 100644
index 0000000..abe6923
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/validation/decoration/GraphicValidationDecorationWithTooltip.png differ
diff --git a/controlsfx/src/main/docs/org/controlsfx/validation/decoration/StyleClassValidationDecoration.png b/controlsfx/src/main/docs/org/controlsfx/validation/decoration/StyleClassValidationDecoration.png
new file mode 100644
index 0000000..8d8017d
Binary files /dev/null and b/controlsfx/src/main/docs/org/controlsfx/validation/decoration/StyleClassValidationDecoration.png differ
diff --git a/controlsfx/src/main/docs/overview.html b/controlsfx/src/main/docs/overview.html
new file mode 100644
index 0000000..e089d96
--- /dev/null
+++ b/controlsfx/src/main/docs/overview.html
@@ -0,0 +1,20 @@
+<html>
+  <head>
+    <title>ControlsFX Overview</title>
+  </head>
+  <body>
+   
+  <p>Welcome to the JavaDoc for the <a href="http://www.controlsfx.org">ControlsFX</a> project! We've really poured our hearts into this documentation to make it a great one-stop shop for learning how to use ControlsFX - refer to the links at the end of this page for other useful sites related to ControlsFX. 
+  
+  <ul>
+    <li><a href="http://www.controlsfx.org">ControlsFX Homepage</a></li>
+    <li><a href="http://fxexperience.com/controlsfx/features/">ControlsFX Features Overview</a></li>
+    <li><a href="http://code.controlsfx.org">Code Repo</a></li>
+    <li><a href="http://groups.controlsfx.org">Mailing List</a></li>
+    <li><a href="http://issues.controlsfx.org">Bug / Feature Tracker</a></li>
+  </ul>
+  </p>
+  
+  <center><img width="656" height="207" src="ControlsFX.png"></center>
+  </body>
+</html>
\ No newline at end of file
diff --git a/controlsfx/src/main/java/impl/build/transifex/JSON.java b/controlsfx/src/main/java/impl/build/transifex/JSON.java
new file mode 100644
index 0000000..044bf30
--- /dev/null
+++ b/controlsfx/src/main/java/impl/build/transifex/JSON.java
@@ -0,0 +1,140 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.build.transifex;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+ 
+class JSON {
+    private static final Pattern PAT_INTEGER = Pattern.compile("[-+]?[0-9]+|0[Xx][0-9]+"); //$NON-NLS-1$
+    private static final Pattern PAT_DOUBLE = Pattern.compile("[+-]?[0-9]+([Ee][+-]?[0-9]+)?|[+-]?[0-9]*\\.[0-9]*([Ee][+-]?[0-9]+)?"); //$NON-NLS-1$
+    private static final Pattern PAT_STRING = Pattern.compile("\"([^\\\\]+\\\\[\"'\\\\])*[^\"]*\"|'([^\\\\]+\\\\[\"'\\\\])*[^']*'"); //$NON-NLS-1$
+    private static final Pattern PAT_BOOL = Pattern.compile("(true)|(false)"); //$NON-NLS-1$
+
+    private static Object parse(String s, int[] start, Matcher integerMatcher, Matcher doubleMatcher, Matcher stringMatcher, Matcher booleanMatcher) {
+        char[] c = s.toCharArray();
+        skipSpace(s, start);
+        if (c[start[0]] == '[') {
+            start[0]++;
+            ArrayList<Object> a = new ArrayList<>();
+            if (c[start[0]] == ']') {
+                start[0]++;
+                return a;
+            }
+            while (true) {
+                a.add(parse(s, start, integerMatcher, doubleMatcher, stringMatcher, booleanMatcher));
+                boolean crlf = skipSpace(s, start);
+                char p = c[start[0]];
+                if (p == ']') {
+                    start[0]++;
+                    return a;
+                }
+                if (p == ',')
+                    start[0]++;
+                else if (!crlf)
+                    throw new IllegalStateException(", or ] expected"); //$NON-NLS-1$
+            }
+        } else if (c[start[0]] == '{') {
+            start[0]++;
+            HashMap<String, Object> a = new HashMap<>();
+            while (true) {
+                String field = (String) parse(s, start, integerMatcher, doubleMatcher, stringMatcher, booleanMatcher);
+                boolean crlf = skipSpace(s, start);
+                if (c[start[0]] == ':') {
+                    start[0]++;
+                    a.put(field, parse(s, start, integerMatcher, doubleMatcher, stringMatcher, booleanMatcher));
+                    crlf = skipSpace(s, start);
+                } else
+                    a.put(field, ""); //$NON-NLS-1$
+                char p = c[start[0]];
+                if (p == '}') {
+                    start[0]++;
+                    return a;
+                }
+                if (p == ',')
+                    start[0]++;
+                else if (!crlf) {
+                    start[0]++;
+//                    throw new IllegalStateException(", or } expected at " + start[0]); //$NON-NLS-1$
+                }
+            }
+        }
+        if (integerMatcher.find(start[0])) {
+            String substring = match(start, s, integerMatcher);
+            if (substring != null) return Integer.valueOf(substring);
+        }
+        if (doubleMatcher.find(start[0])) {
+            String substring = match(start, s, doubleMatcher);
+            if (substring != null) return Double.valueOf(substring);
+        }
+        if (stringMatcher.find(start[0])) {
+            String substring = match(start, s, stringMatcher);
+            if (substring != null) return substring.substring(1, substring.length() - 1);
+        }
+        if (booleanMatcher.find(start[0])) {
+            String substring = match(start, s, booleanMatcher);
+            if (substring != null) return Boolean.valueOf(substring);
+        }
+//        throw new IllegalStateException("unexpected end of data"); //$NON-NLS-1$
+        return null;
+    }
+
+    private static String match(int[] start, String s, Matcher matcher) {
+        int ms = matcher.start();
+        int me = matcher.end();
+        if (start[0] == ms) {
+            start[0] = me;
+            return s.substring(ms, me);
+        }
+        return null;
+    }
+
+    public static boolean skipSpace(String s, int[] start) {
+        boolean ret = false;
+        while (true) {
+            char c = s.charAt(start[0]);
+            boolean crlf = (c == '\r') || (c == '\n');
+            if ((c != ' ') && !crlf)
+                break;
+            if (crlf)
+                ret = true;
+            start[0]++;
+        }
+        return ret;
+    }
+
+    @SuppressWarnings("unchecked")
+	public static <T> T parse(String json) {
+        Matcher integerMatcher = PAT_INTEGER.matcher(json);
+        Matcher doubleMatcher = PAT_DOUBLE.matcher(json);
+        Matcher stringMatcher = PAT_STRING.matcher(json);
+        Matcher booleanMatcher = PAT_BOOL.matcher(json);
+        return (T) parse(json, new int[]{0}, integerMatcher, doubleMatcher, stringMatcher, booleanMatcher);
+    }
+}
\ No newline at end of file
diff --git a/controlsfx/src/main/java/impl/build/transifex/Transifex.java b/controlsfx/src/main/java/impl/build/transifex/Transifex.java
new file mode 100644
index 0000000..37e4696
--- /dev/null
+++ b/controlsfx/src/main/java/impl/build/transifex/Transifex.java
@@ -0,0 +1,179 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.build.transifex;
+import java.io.BufferedReader;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.channels.Channels;
+import java.nio.channels.ReadableByteChannel;
+import java.util.Base64;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+public class Transifex {
+    
+    private static final String CHARSET             = "ISO-8859-1"; //$NON-NLS-1$
+    private static final String FILE_NAME           = "controlsfx_%1s.utf8"; //$NON-NLS-1$
+    private static final String NEW_LINE            = System.getProperty("line.separator"); //$NON-NLS-1$
+
+    private static final String BASE_URI            = "https://www.transifex.com/api/2/"; //$NON-NLS-1$
+    private static final String PROJECT_PATH        = BASE_URI + "project/controlsfx/resource/controlsfx-core"; // list simple project details //$NON-NLS-1$
+    private static final String PROJECT_DETAILS     = BASE_URI + "project/controlsfx/resource/controlsfx-core?details"; // list all project details //$NON-NLS-1$
+    private static final String LIST_TRANSLATIONS   = BASE_URI + "project/controlsfx/languages/"; // list all translations //$NON-NLS-1$
+    private static final String GET_TRANSLATION     = BASE_URI + "project/controlsfx/resource/controlsfx-core/translation/%1s?file"; // gets a translation for one language //$NON-NLS-1$
+    private static final String TRANSLATION_STATS   = BASE_URI + "project/controlsfx/resource/controlsfx-core/stats/%1s/"; // gets a translation for one language //$NON-NLS-1$
+
+    private static final String USERNAME            = System.getProperty("transifex.username"); //$NON-NLS-1$
+    private static final String PASSWORD            = System.getProperty("transifex.password"); //$NON-NLS-1$
+    private static final boolean FILTER_INCOMPLETE_TRANSLATIONS = Boolean.parseBoolean(System.getProperty("transifex.filterIncompleteTranslations", "true"));
+
+    public static void main(String[] args) throws FileNotFoundException, UnsupportedEncodingException {
+        new Transifex().doTransifexCheck();
+    }
+        
+    @SuppressWarnings("unchecked")
+    private void doTransifexCheck() {
+        System.out.println("=== Starting Transifex Check ==="); //$NON-NLS-1$
+        
+        if (USERNAME == null || PASSWORD == null || USERNAME.isEmpty() || PASSWORD.isEmpty()) {
+            System.out.println("  transifex.username and transifex.password system properties must be specified"); //$NON-NLS-1$
+            return;
+        }
+        
+        System.out.println("  Filtering out incomplete translations: " + FILTER_INCOMPLETE_TRANSLATIONS);
+        
+        Map<String,Object> projectDetails = JSON.parse(transifexRequest(PROJECT_DETAILS));
+        List<Map<String, String>> availableLanguages = (List<Map<String, String>>) projectDetails.get("available_languages");
+        
+        // main loop
+        availableLanguages.parallelStream()
+                .map(map -> map.get("code")) //$NON-NLS-1$
+                .filter(this::filterOutIncompleteTranslations)
+                .forEach(this::downloadTranslation);
+        
+        System.out.println("Transifex Check Complete"); //$NON-NLS-1$
+    }
+    
+    private String transifexRequest(String request, Object... args) {
+        Function<InputStream, String> consumer = inputStream -> {
+            StringBuilder response = new StringBuilder(); 
+            try(BufferedReader rd = new BufferedReader(new InputStreamReader(inputStream)) ) {
+                String line;
+                while((line = rd.readLine()) != null) {
+                    response.append(line);
+                    response.append(NEW_LINE);
+                }
+            } catch (IOException e) {
+                e.printStackTrace();
+            } 
+
+            return response.toString();
+        };
+        return performTransifexTask(consumer, request, args);
+    }
+    
+    private static <T> T performTransifexTask(Function<InputStream, T> consumer, String request, Object... args) {
+        request = String.format(request, args);
+        
+        URL url;
+        HttpURLConnection connection = null;  
+        try {
+            url = new URL(request);
+            connection = (HttpURLConnection)url.openConnection();
+            connection.setRequestMethod("GET"); //$NON-NLS-1$
+            connection.setUseCaches(false);
+            connection.setDoInput(true);
+            
+            // pass in username / password
+            String encoded = Base64.getEncoder().encodeToString((USERNAME+":"+PASSWORD).getBytes()); //$NON-NLS-1$
+            connection.setRequestProperty("Authorization", "Basic "+encoded); //$NON-NLS-1$ //$NON-NLS-2$
+            connection.setRequestProperty("Accept-Charset", CHARSET);  //$NON-NLS-1$
+            
+            return consumer.apply(connection.getInputStream());
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            if (connection != null) {
+                connection.disconnect();
+            }
+        }
+        
+        return null;
+    }
+    
+    private boolean filterOutIncompleteTranslations(String languageCode) {
+        // filter out any translation that does not have 100% completion and reviewed state.
+        // Returns a Map, for example:
+        // { 
+        //     untranslated_entities=8, 
+        //     last_commiter=eryzhikov, 
+        //     translated_entities=34, 
+        //     untranslated_words=16, 
+        //     translated_words=57, 
+        //     last_update=2014-09-12 08:44:33, 
+        //     reviewed_percentage=69%, 
+        //     reviewed=29, 
+        //     completed=80%
+        // }
+        Map<String, String> map = JSON.parse(transifexRequest(TRANSLATION_STATS, languageCode));
+        String completed = map.getOrDefault("completed", "0%"); //$NON-NLS-1$ //$NON-NLS-2$
+        String reviewed = map.getOrDefault("reviewed_percentage", "0%"); //$NON-NLS-1$ //$NON-NLS-2$
+        boolean isAccepted = completed.equals("100%") && reviewed.equals("100%"); //$NON-NLS-1$ //$NON-NLS-2$
+        
+        System.out.println("  Reviewing translation '" + languageCode + "'" +  //$NON-NLS-1$ //$NON-NLS-2$
+                "\tcompletion: " + completed +  //$NON-NLS-1$
+                ",\treviewed: " + reviewed +  //$NON-NLS-1$
+                "\t-> TRANSLATION" + (isAccepted ? " ACCEPTED" : " REJECTED")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+        
+        return isAccepted || !FILTER_INCOMPLETE_TRANSLATIONS;
+    }
+    
+    private void downloadTranslation(String languageCode) {
+        // Now we download the translations of the completed languages
+        System.out.println("\tDownloading translation file..."); //$NON-NLS-1$
+        
+        Function<InputStream, Void> consumer = inputStream -> {
+            final String outputFile = "build/resources/main/" + String.format(FILE_NAME, languageCode); //$NON-NLS-1$
+            
+            ReadableByteChannel rbc = Channels.newChannel(inputStream);
+            try (FileOutputStream fos = new FileOutputStream(outputFile)) {
+                fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+            return null;
+        };
+        performTransifexTask(consumer, GET_TRANSLATION, languageCode);
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/ImplUtils.java b/controlsfx/src/main/java/impl/org/controlsfx/ImplUtils.java
new file mode 100644
index 0000000..08bbbee
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/ImplUtils.java
@@ -0,0 +1,149 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx;
+
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.List;
+
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.scene.Group;
+import javafx.scene.Node;
+import javafx.scene.Parent;
+import javafx.scene.Scene;
+import javafx.scene.control.Control;
+import javafx.scene.control.Skin;
+import javafx.scene.control.SkinBase;
+import javafx.scene.layout.Pane;
+
+public class ImplUtils {
+
+    private ImplUtils() {
+        // no-op
+    }
+    
+    public static void injectAsRootPane(Scene scene, Parent injectedParent, boolean useReflection) {
+        Parent originalParent = scene.getRoot();
+        scene.setRoot(injectedParent);
+        
+        if (originalParent != null) {
+            getChildren(injectedParent, useReflection).add(0, originalParent);
+            
+            // copy in layout properties, etc, so that the dialogStack displays
+            // properly in (hopefully) whatever layout the owner node is in
+            injectedParent.getProperties().putAll(originalParent.getProperties());
+        }
+    }
+    
+    // parent is where we want to inject the injectedParent. We then need to
+    // set the child of the injectedParent to include parent.
+    // The end result is that we've forced in the injectedParent node above parent.
+    public static void injectPane(Parent parent, Parent injectedParent, boolean useReflection) {
+        if (parent == null) {
+            throw new IllegalArgumentException("parent can not be null"); //$NON-NLS-1$
+        }
+        
+        List<Node> ownerParentChildren = getChildren(parent.getParent(), useReflection);
+        
+        // we've got the children list, now we need to insert a temporary
+        // layout container holding our dialogs and opaque layer / effect
+        // in place of the owner (the owner will become a child of the dialog
+        // stack)
+        int ownerPos = ownerParentChildren.indexOf(parent);
+        ownerParentChildren.remove(ownerPos);
+        ownerParentChildren.add(ownerPos, injectedParent);
+        
+        // now we install the parent as a child of the injectedParent
+        getChildren(injectedParent, useReflection).add(0, parent);
+        
+        // copy in layout properties, etc, so that the dialogStack displays
+        // properly in (hopefully) whatever layout the owner node is in
+        injectedParent.getProperties().putAll(parent.getProperties());
+    }
+    
+    public static void stripRootPane(Scene scene, Parent originalParent, boolean useReflection) {
+        Parent oldParent = scene.getRoot();
+        getChildren(oldParent, useReflection).remove(originalParent);
+        originalParent.getStyleClass().remove("root"); //$NON-NLS-1$
+        scene.setRoot(originalParent);        
+    }
+    
+    public static List<Node> getChildren(Node n, boolean useReflection) {
+        return n instanceof Parent ? getChildren((Parent)n, useReflection) : Collections.emptyList();
+    }
+    
+    public static List<Node> getChildren(Parent p, boolean useReflection) {
+        ObservableList<Node> children = null;
+        
+        // previously we used reflection immediately, now we try to avoid reflection
+        // by checking the type of the Parent. Still not great...
+        if (p instanceof Pane) {
+            // This should cover the majority of layout containers, including
+            // AnchorPane, FlowPane, GridPane, HBox, Pane, StackPane, TilePane, VBox
+            children = ((Pane)p).getChildren();
+        } else if (p instanceof Group) {
+            children = ((Group)p).getChildren();
+        } else if (p instanceof Control) {
+            Control c = (Control) p;
+            Skin<?> s = c.getSkin();
+            children = s instanceof SkinBase ? ((SkinBase<?>)s).getChildren() : getChildrenReflectively(p);
+        } else if (useReflection) {
+            // we really want to avoid using this!!!!
+            children = getChildrenReflectively(p);
+        }
+        
+        if (children == null) {
+            throw new RuntimeException("Unable to get children for Parent of type " + p.getClass() +  //$NON-NLS-1$
+                                       ". useReflection is set to " + useReflection); //$NON-NLS-1$
+        }
+        
+        return children == null ? FXCollections.emptyObservableList() : children;
+    }
+    
+    @SuppressWarnings("unchecked")
+	public static ObservableList<Node> getChildrenReflectively(Parent p) {
+        ObservableList<Node> children = null;
+        
+        try {
+            Method getChildrenMethod = Parent.class.getDeclaredMethod("getChildren"); //$NON-NLS-1$
+            
+            if (getChildrenMethod != null) {
+                if (! getChildrenMethod.isAccessible()) {
+                    getChildrenMethod.setAccessible(true);
+                }
+                children = (ObservableList<Node>) getChildrenMethod.invoke(p);
+            } else {
+                // uh oh, trouble
+            }
+        } catch (ReflectiveOperationException | IllegalArgumentException e) {
+            throw new RuntimeException("Unable to get children for Parent of type " + p.getClass(), e); //$NON-NLS-1$
+        }
+        
+        return children;
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/autocompletion/AutoCompletionTextFieldBinding.java b/controlsfx/src/main/java/impl/org/controlsfx/autocompletion/AutoCompletionTextFieldBinding.java
new file mode 100644
index 0000000..67416a6
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/autocompletion/AutoCompletionTextFieldBinding.java
@@ -0,0 +1,161 @@
+/**
+ * Copyright (c) 2014, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.autocompletion;
+
+import java.util.Collection;
+
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.scene.control.TextField;
+import javafx.util.Callback;
+import javafx.util.StringConverter;
+
+import org.controlsfx.control.textfield.AutoCompletionBinding;
+
+/**
+ * Represents a binding between a text field and a auto-completion popup
+ *
+ * @param <T>
+ */
+public class AutoCompletionTextFieldBinding<T>  extends AutoCompletionBinding<T>{
+
+    /***************************************************************************
+     *                                                                         *
+     * Static properties and methods                                           *
+     *                                                                         *
+     **************************************************************************/
+    
+    private static <T> StringConverter<T> defaultStringConverter() {
+        return new StringConverter<T>() {
+            @Override public String toString(T t) {
+                return t == null ? null : t.toString();
+            }
+            @SuppressWarnings("unchecked")
+			@Override public T fromString(String string) {
+                return (T) string;
+            }
+        };
+    }
+
+    /***************************************************************************
+     *                                                                         *
+     * Private fields                                                          *
+     *                                                                         *
+     **************************************************************************/
+	
+    /**
+     * String converter to be used to convert suggestions to strings.
+     */
+	private StringConverter<T> converter;
+
+	
+    /***************************************************************************
+     *                                                                         *
+     * Constructors                                                            *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * Creates a new auto-completion binding between the given textField 
+     * and the given suggestion provider.
+     * 
+     * @param textField
+     * @param suggestionProvider
+     */
+    public AutoCompletionTextFieldBinding(final TextField textField,
+            Callback<ISuggestionRequest, Collection<T>> suggestionProvider) {
+    	
+        this(textField, suggestionProvider, AutoCompletionTextFieldBinding
+    			.<T>defaultStringConverter());
+    }
+
+    /**
+     * Creates a new auto-completion binding between the given textField 
+     * and the given suggestion provider.
+     * 
+     * @param textField
+     * @param suggestionProvider
+     */
+    public AutoCompletionTextFieldBinding(final TextField textField,
+            Callback<ISuggestionRequest, Collection<T>> suggestionProvider,
+            final StringConverter<T> converter) {
+    	
+        super(textField, suggestionProvider, converter);
+        this.converter = converter; 
+
+        getCompletionTarget().textProperty().addListener(textChangeListener);
+        getCompletionTarget().focusedProperty().addListener(focusChangedListener);
+    }
+
+
+    /***************************************************************************
+     *                                                                         *
+     * Public API                                                              *
+     *                                                                         *
+     **************************************************************************/
+
+    /** {@inheritDoc} */
+    @Override public TextField getCompletionTarget(){
+        return (TextField)super.getCompletionTarget();
+    }
+
+    /** {@inheritDoc} */
+    @Override public void dispose(){
+        getCompletionTarget().textProperty().removeListener(textChangeListener);
+        getCompletionTarget().focusedProperty().removeListener(focusChangedListener);
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void completeUserInput(T completion){
+    	String newText = converter.toString(completion);     	
+        getCompletionTarget().setText(newText);
+        getCompletionTarget().positionCaret(newText.length());
+    }
+
+
+    /***************************************************************************
+     *                                                                         *
+     * Event Listeners                                                         *
+     *                                                                         *
+     **************************************************************************/
+
+
+    private final ChangeListener<String> textChangeListener = new ChangeListener<String>() {
+        @Override public void changed(ObservableValue<? extends String> obs, String oldText, String newText) {
+            if (getCompletionTarget().isFocused()) {
+                setUserInput(newText);
+            }
+        }
+    };
+
+    private final ChangeListener<Boolean> focusChangedListener = new ChangeListener<Boolean>() {
+        @Override public void changed(ObservableValue<? extends Boolean> obs, Boolean oldFocused, Boolean newFocused) {
+            if(newFocused == false)
+                hidePopup();
+        }
+    };
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/autocompletion/SuggestionProvider.java b/controlsfx/src/main/java/impl/org/controlsfx/autocompletion/SuggestionProvider.java
new file mode 100644
index 0000000..5c17803
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/autocompletion/SuggestionProvider.java
@@ -0,0 +1,199 @@
+/**
+ * Copyright (c) 2014, 2016 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.autocompletion;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import javafx.util.Callback;
+
+import org.controlsfx.control.textfield.AutoCompletionBinding.ISuggestionRequest;
+
+/**
+ * This is a simple implementation of a generic suggestion provider callback.
+ * The complexity of suggestion generation is O(n) where n is the number of possible suggestions.
+ * 
+ * @param <T> Type of suggestions
+ */
+public abstract class SuggestionProvider<T> implements Callback<ISuggestionRequest, Collection<T>>{
+
+    private final List<T> possibleSuggestions = new ArrayList<>();
+    private final Object possibleSuggestionsLock = new Object();
+
+
+    /**
+     * Add the given new possible suggestions to this  SuggestionProvider
+     * @param newPossible
+     */
+    public void addPossibleSuggestions(@SuppressWarnings("unchecked") T... newPossible){     
+        addPossibleSuggestions(Arrays.asList(newPossible));
+    }
+
+    /**
+     * Add the given new possible suggestions to this  SuggestionProvider
+     * @param newPossible
+     */
+    public void addPossibleSuggestions(Collection<T> newPossible){
+        synchronized (possibleSuggestionsLock) {
+            possibleSuggestions.addAll(newPossible);
+        }
+    }
+
+    /**
+     * Remove all current possible suggestions
+     */
+    public void clearSuggestions(){
+        synchronized (possibleSuggestionsLock) {
+            possibleSuggestions.clear();
+        }
+    }
+
+    @Override
+    public final Collection<T> call(final ISuggestionRequest request) {
+        List<T> suggestions = new ArrayList<>();
+        if(!request.getUserText().isEmpty()){
+            synchronized (possibleSuggestionsLock) {
+                for (T possibleSuggestion : possibleSuggestions) {
+                    if(isMatch(possibleSuggestion, request)){
+                        suggestions.add(possibleSuggestion);
+                    }
+                }
+            }
+            Collections.sort(suggestions, getComparator());
+        }
+        return suggestions;
+    }
+
+    /**
+     * Get the comparator to order the suggestions
+     * @return
+     */
+    protected abstract Comparator<T> getComparator();
+
+    /**
+     * Check the given possible suggestion is a match (is a valid suggestion)
+     * @param suggestion
+     * @param request
+     * @return
+     */
+    protected abstract boolean isMatch(T suggestion, ISuggestionRequest request);
+
+
+    /***************************************************************************
+     *                                                                         *
+     * Static methods                                                          *
+     *                                                                         *
+     **************************************************************************/
+
+
+    /**
+     * Create a default suggestion provider based on the toString() method of the generic objects
+     * @param possibleSuggestions All possible suggestions
+     * @return
+     */
+    public static <T> SuggestionProvider<T> create(Collection<T> possibleSuggestions){
+        return create(null, possibleSuggestions);
+    }
+
+    /**
+     * Create a default suggestion provider based on the toString() method of the generic objects
+     * using the provided stringConverter
+     * 
+     * @param stringConverter A stringConverter which converts generic T into a string
+     * @param possibleSuggestions All possible suggestions
+     * @return
+     */
+    public static <T> SuggestionProvider<T> create(Callback<T, String> stringConverter, Collection<T> possibleSuggestions){
+        SuggestionProviderString<T> suggestionProvider = new SuggestionProviderString<>(stringConverter);
+        suggestionProvider.addPossibleSuggestions(possibleSuggestions);
+        return suggestionProvider;
+    }
+
+
+
+    /***************************************************************************
+     *                                                                         *
+     * Default implementations                                                 *
+     *                                                                         *
+     **************************************************************************/
+
+
+    /**
+     * This is a simple string based suggestion provider.
+     * All generic suggestions T are turned into strings for processing.
+     * 
+     */
+    private static class SuggestionProviderString<T> extends SuggestionProvider<T> {
+
+        private Callback<T, String> stringConverter;
+
+        private final Comparator<T> stringComparator = new Comparator<T>() {
+            @Override
+            public int compare(T o1, T o2) {
+                String o1str = stringConverter.call(o1);
+                String o2str = stringConverter.call(o2);
+                return o1str.compareTo(o2str);
+            }
+        };
+
+        /**
+         * Create a new SuggestionProviderString
+         * @param stringConverter
+         */
+        public SuggestionProviderString(Callback<T, String> stringConverter){
+            this.stringConverter = stringConverter;
+
+            // In case no stringConverter was provided, use the default strategy
+            if(this.stringConverter == null){
+                this.stringConverter = new Callback<T, String>() {
+                    @Override
+                    public String call(T obj) {
+                        return obj != null ? obj.toString() : ""; //$NON-NLS-1$
+                    }
+                };
+            }
+        }
+
+        /**{@inheritDoc}*/
+        @Override
+        protected Comparator<T> getComparator() {
+            return stringComparator;
+        }
+
+        /**{@inheritDoc}*/
+        @Override
+        protected boolean isMatch(T suggestion, ISuggestionRequest request) {
+            String userTextLower = request.getUserText().toLowerCase();
+            String suggestionStr = suggestion.toString().toLowerCase();
+            return suggestionStr.contains(userTextLower);
+        }
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/behavior/RangeSliderBehavior.java b/controlsfx/src/main/java/impl/org/controlsfx/behavior/RangeSliderBehavior.java
new file mode 100644
index 0000000..e0f1665
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/behavior/RangeSliderBehavior.java
@@ -0,0 +1,339 @@
+/**
+ * Copyright (c) 2013, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.behavior;
+
+import static javafx.scene.input.KeyCode.DOWN;
+import static javafx.scene.input.KeyCode.END;
+import static javafx.scene.input.KeyCode.F4;
+import static javafx.scene.input.KeyCode.HOME;
+import static javafx.scene.input.KeyCode.KP_DOWN;
+import static javafx.scene.input.KeyCode.KP_LEFT;
+import static javafx.scene.input.KeyCode.KP_RIGHT;
+import static javafx.scene.input.KeyCode.KP_UP;
+import static javafx.scene.input.KeyCode.LEFT;
+import static javafx.scene.input.KeyCode.RIGHT;
+import static javafx.scene.input.KeyCode.UP;
+import static javafx.scene.input.KeyEvent.KEY_RELEASED;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javafx.event.EventType;
+import javafx.geometry.Orientation;
+import javafx.scene.control.Control;
+import javafx.scene.control.Skin;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.input.MouseEvent;
+import javafx.util.Callback;
+
+import org.controlsfx.control.RangeSlider;
+import org.controlsfx.tools.Utils;
+
+import com.sun.javafx.scene.control.behavior.BehaviorBase;
+import com.sun.javafx.scene.control.behavior.KeyBinding;
+import com.sun.javafx.scene.control.behavior.OrientedKeyBinding;
+
+public class RangeSliderBehavior extends BehaviorBase<RangeSlider> {
+    
+     /**************************************************************************
+     *                          Setup KeyBindings                             *
+     *                                                                        *
+     * We manually specify the focus traversal keys because Slider has        *
+     * different usage for up/down arrow keys.                                *
+     *************************************************************************/
+    private static final List<KeyBinding> RANGESLIDER_BINDINGS = new ArrayList<>();
+    static {
+        RANGESLIDER_BINDINGS.add(new KeyBinding(F4, "TraverseDebug").alt().ctrl().shift()); //$NON-NLS-1$
+
+        RANGESLIDER_BINDINGS.add(new RangeSliderKeyBinding(LEFT, "DecrementValue")); //$NON-NLS-1$
+        RANGESLIDER_BINDINGS.add(new RangeSliderKeyBinding(KP_LEFT, "DecrementValue")); //$NON-NLS-1$
+        RANGESLIDER_BINDINGS.add(new RangeSliderKeyBinding(UP, "IncrementValue").vertical()); //$NON-NLS-1$
+        RANGESLIDER_BINDINGS.add(new RangeSliderKeyBinding(KP_UP, "IncrementValue").vertical()); //$NON-NLS-1$
+        RANGESLIDER_BINDINGS.add(new RangeSliderKeyBinding(RIGHT, "IncrementValue")); //$NON-NLS-1$
+        RANGESLIDER_BINDINGS.add(new RangeSliderKeyBinding(KP_RIGHT, "IncrementValue")); //$NON-NLS-1$
+        RANGESLIDER_BINDINGS.add(new RangeSliderKeyBinding(DOWN, "DecrementValue").vertical()); //$NON-NLS-1$
+        RANGESLIDER_BINDINGS.add(new RangeSliderKeyBinding(KP_DOWN, "DecrementValue").vertical()); //$NON-NLS-1$
+
+        RANGESLIDER_BINDINGS.add(new RangeSliderKeyBinding(LEFT, "TraverseLeft").vertical()); //$NON-NLS-1$
+        RANGESLIDER_BINDINGS.add(new RangeSliderKeyBinding(KP_LEFT, "TraverseLeft").vertical()); //$NON-NLS-1$
+        RANGESLIDER_BINDINGS.add(new RangeSliderKeyBinding(UP, "TraverseUp")); //$NON-NLS-1$
+        RANGESLIDER_BINDINGS.add(new RangeSliderKeyBinding(KP_UP, "TraverseUp")); //$NON-NLS-1$
+        RANGESLIDER_BINDINGS.add(new RangeSliderKeyBinding(RIGHT, "TraverseRight").vertical()); //$NON-NLS-1$
+        RANGESLIDER_BINDINGS.add(new RangeSliderKeyBinding(KP_RIGHT, "TraverseRight").vertical()); //$NON-NLS-1$
+        RANGESLIDER_BINDINGS.add(new RangeSliderKeyBinding(DOWN, "TraverseDown")); //$NON-NLS-1$
+        RANGESLIDER_BINDINGS.add(new RangeSliderKeyBinding(KP_DOWN, "TraverseDown")); //$NON-NLS-1$
+
+        RANGESLIDER_BINDINGS.add(new KeyBinding(HOME, KEY_RELEASED, "Home")); //$NON-NLS-1$
+        RANGESLIDER_BINDINGS.add(new KeyBinding(END, KEY_RELEASED, "End")); //$NON-NLS-1$
+    }
+    
+    public RangeSliderBehavior(RangeSlider slider) {
+        super(slider, RANGESLIDER_BINDINGS);
+    }
+
+    @Override protected void callAction(String s) {
+        if ("Home".equals(s) || "Home2".equals(s)) home(); //$NON-NLS-1$ //$NON-NLS-2$
+        else if ("End".equals(s) || "End2".equals(s)) end(); //$NON-NLS-1$ //$NON-NLS-2$
+        else if ("IncrementValue".equals(s) || "IncrementValue2".equals(s)) incrementValue(); //$NON-NLS-1$ //$NON-NLS-2$
+        else if ("DecrementValue".equals(s) || "DecrementValue2".equals(s)) decrementValue(); //$NON-NLS-1$ //$NON-NLS-2$
+        else super.callAction(s);
+    }
+     
+    /**************************************************************************
+     *                         State and Functions                            *
+     *************************************************************************/
+
+    private Callback<Void, FocusedChild> selectedValue;
+    public void setSelectedValue(Callback<Void, FocusedChild> c) {
+        selectedValue = c;
+    }
+    /**
+     * Invoked by the RangeSlider {@link Skin} implementation whenever a mouse press
+     * occurs on the "track" of the slider. This will cause the thumb to be
+     * moved by some amount.
+     *
+     * @param position The mouse position on track with 0.0 being beginning of
+     *        track and 1.0 being the end
+     */
+    public void trackPress(MouseEvent e, double position) {
+        // determine the percentage of the way between min and max
+        // represented by this mouse event
+        final RangeSlider rangeSlider = getControl();
+        // If not already focused, request focus
+        if (!rangeSlider.isFocused()) {
+            rangeSlider.requestFocus();
+        }
+        if (selectedValue != null) {
+            double newPosition;
+            if (rangeSlider.getOrientation().equals(Orientation.HORIZONTAL)) {
+                newPosition = position * (rangeSlider.getMax() - rangeSlider.getMin()) + rangeSlider.getMin();
+            } else {
+                newPosition = (1 - position) * (rangeSlider.getMax() - rangeSlider.getMin()) + rangeSlider.getMin();
+            }
+
+            /**
+             * If the position is inferior to the current LowValue, this means
+             * the user clicked on the track to move the low thumb. If not, then
+             * it means the user wanted to move the high thumb.
+             */
+            if (newPosition < rangeSlider.getLowValue()) {
+                rangeSlider.adjustLowValue(newPosition);
+            } else {
+                rangeSlider.adjustHighValue(newPosition);
+            }
+        }
+    }
+
+    /**
+     */
+    public void trackRelease(MouseEvent e, double position) {
+    }
+    
+     /**
+     * @param position The mouse position on track with 0.0 being beginning of
+      *       track and 1.0 being the end
+     */
+    public void lowThumbPressed(MouseEvent e, double position) {
+        // If not already focused, request focus
+        final RangeSlider rangeSlider = getControl();
+        if (!rangeSlider.isFocused())  rangeSlider.requestFocus();
+        rangeSlider.setLowValueChanging(true);
+    }
+
+    /**
+     * @param position The mouse position on track with 0.0 being beginning of
+     *        track and 1.0 being the end
+     */
+    public void lowThumbDragged(MouseEvent e, double position) {
+        final RangeSlider rangeSlider = getControl();
+        double newValue = Utils.clamp(rangeSlider.getMin(), 
+                (position * (rangeSlider.getMax() - rangeSlider.getMin())) + rangeSlider.getMin(), 
+                rangeSlider.getMax());
+        rangeSlider.setLowValue(newValue);
+    }
+    
+    /**
+     * When lowThumb is released lowValueChanging should be set to false.
+     */
+    public void lowThumbReleased(MouseEvent e) {
+        final RangeSlider rangeSlider = getControl();
+        rangeSlider.setLowValueChanging(false);
+        // RT-15207 When snapToTicks is true, slider value calculated in drag
+        // is then snapped to the nearest tick on mouse release.
+        if (rangeSlider.isSnapToTicks()) {
+            rangeSlider.setLowValue(snapValueToTicks(rangeSlider.getLowValue()));
+        }
+    }
+    
+    void home() {
+        RangeSlider slider = (RangeSlider) getControl();
+        slider.adjustHighValue(slider.getMin());
+    }
+
+    void decrementValue() {
+        RangeSlider slider = (RangeSlider) getControl();
+        if (selectedValue != null) {
+            if (selectedValue.call(null) == FocusedChild.HIGH_THUMB) {
+                if (slider.isSnapToTicks())
+                    slider.adjustHighValue(slider.getHighValue() - computeIncrement());
+                else
+                    slider.decrementHighValue();
+            } else {
+                if (slider.isSnapToTicks())
+                    slider.adjustLowValue(slider.getLowValue() - computeIncrement());
+                else
+                    slider.decrementLowValue();
+            }
+        }
+    }
+
+    void end() {
+        RangeSlider slider = (RangeSlider) getControl();
+        slider.adjustHighValue(slider.getMax());
+    }
+
+    void incrementValue() {
+        RangeSlider slider = (RangeSlider) getControl();
+        if (selectedValue != null) {
+            if (selectedValue.call(null) == FocusedChild.HIGH_THUMB) {
+                if (slider.isSnapToTicks())
+                    slider.adjustHighValue(slider.getHighValue() + computeIncrement());
+                else
+                    slider.incrementHighValue();
+            } else {
+                if (slider.isSnapToTicks())
+                    slider.adjustLowValue(slider.getLowValue() + computeIncrement());
+                else
+                    slider.incrementLowValue();
+            }
+        }
+        
+    }
+
+    double computeIncrement() {
+        RangeSlider rangeSlider = (RangeSlider) getControl();
+        double d = 0.0D;
+        if (rangeSlider.getMinorTickCount() != 0)
+            d = rangeSlider.getMajorTickUnit() / (double) (Math.max(rangeSlider.getMinorTickCount(), 0) + 1);
+        else
+            d = rangeSlider.getMajorTickUnit();
+        if (rangeSlider.getBlockIncrement() > 0.0D && rangeSlider.getBlockIncrement() < d)
+            return d;
+        else
+            return rangeSlider.getBlockIncrement();
+    }
+
+    private double snapValueToTicks(double d) {
+        RangeSlider rangeSlider = (RangeSlider) getControl();
+        double d1 = d;
+        double d2 = 0.0D;
+        if (rangeSlider.getMinorTickCount() != 0)
+            d2 = rangeSlider.getMajorTickUnit() / (double) (Math.max(rangeSlider.getMinorTickCount(), 0) + 1);
+        else
+            d2 = rangeSlider.getMajorTickUnit();
+        int i = (int) ((d1 - rangeSlider.getMin()) / d2);
+        double d3 = (double) i * d2 + rangeSlider.getMin();
+        double d4 = (double) (i + 1) * d2 + rangeSlider.getMin();
+        d1 = Utils.nearest(d3, d1, d4);
+        return Utils.clamp(rangeSlider.getMin(), d1, rangeSlider.getMax());
+    }
+
+    // when high thumb is released, highValueChanging is set to false.
+    public void highThumbReleased(MouseEvent e) {
+        RangeSlider slider = (RangeSlider) getControl();
+        slider.setHighValueChanging(false);
+        if (slider.isSnapToTicks())
+            slider.setHighValue(snapValueToTicks(slider.getHighValue()));
+    }
+
+    public void highThumbPressed(MouseEvent e, double position) {
+        RangeSlider slider = (RangeSlider) getControl();
+        if (!slider.isFocused())
+            slider.requestFocus();
+        slider.setHighValueChanging(true);
+    }
+
+    public void highThumbDragged(MouseEvent e, double position) {
+        RangeSlider slider = (RangeSlider) getControl();
+        slider.setHighValue(Utils.clamp(slider.getMin(), position * (slider.getMax() - slider.getMin()) + slider.getMin(), slider.getMax()));
+    }
+    
+    public void moveRange(double position) {
+        RangeSlider slider = (RangeSlider) getControl();
+        final double min = slider.getMin();
+        final double max = slider.getMax();
+        final double lowValue = slider.getLowValue();
+        final double newLowValue = Utils.clamp(min, lowValue + position *(max-min) / 
+                (slider.getOrientation() == Orientation.HORIZONTAL? slider.getWidth(): slider.getHeight()), max);
+        final double highValue = slider.getHighValue();
+        final double newHighValue = Utils.clamp(min, highValue + position*(max-min) / 
+                (slider.getOrientation() == Orientation.HORIZONTAL? slider.getWidth(): slider.getHeight()), max);
+        
+        if (newLowValue <= min || newHighValue >= max) return;
+        slider.setLowValueChanging(true);
+        slider.setHighValueChanging(true);
+        slider.setLowValue(newLowValue);
+        slider.setHighValue(newHighValue);        
+    }
+    
+      public void confirmRange() {
+        RangeSlider slider = (RangeSlider) getControl();
+
+        slider.setLowValueChanging(false);
+        if (slider.isSnapToTicks()) {
+            slider.setLowValue(snapValueToTicks(slider.getLowValue()));
+        }
+        slider.setHighValueChanging(false);
+        if (slider.isSnapToTicks()) {
+            slider.setHighValue(snapValueToTicks(slider.getHighValue()));
+        }
+
+    }
+    
+    public static class RangeSliderKeyBinding extends OrientedKeyBinding {
+        public RangeSliderKeyBinding(KeyCode code, String action) {
+            super(code, action);
+        }
+
+        public RangeSliderKeyBinding(KeyCode code, EventType<KeyEvent> type, String action) {
+            super(code, type, action);
+        }
+
+        public @Override boolean getVertical(Control control) {
+            return ((RangeSlider)control).getOrientation() == Orientation.VERTICAL;
+        }
+    }
+     
+    public enum FocusedChild {
+        LOW_THUMB,
+        HIGH_THUMB,
+        RANGE_BAR,
+        NONE
+    }
+}
+
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/behavior/RatingBehavior.java b/controlsfx/src/main/java/impl/org/controlsfx/behavior/RatingBehavior.java
new file mode 100644
index 0000000..284d299
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/behavior/RatingBehavior.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2013, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.behavior;
+
+import java.util.Collections;
+
+import org.controlsfx.control.Rating;
+
+import com.sun.javafx.scene.control.behavior.BehaviorBase;
+import com.sun.javafx.scene.control.behavior.KeyBinding;
+
+public class RatingBehavior extends BehaviorBase<Rating> {
+
+    public RatingBehavior(Rating control) {
+        super(control, Collections.<KeyBinding> emptyList());
+    }
+}
\ No newline at end of file
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/behavior/SnapshotViewBehavior.java b/controlsfx/src/main/java/impl/org/controlsfx/behavior/SnapshotViewBehavior.java
new file mode 100644
index 0000000..33456ec
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/behavior/SnapshotViewBehavior.java
@@ -0,0 +1,783 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.behavior;
+
+import impl.org.controlsfx.tools.rectangle.CoordinatePosition;
+import impl.org.controlsfx.tools.rectangle.CoordinatePositions;
+import impl.org.controlsfx.tools.rectangle.Rectangles2D;
+import impl.org.controlsfx.tools.rectangle.change.MoveChangeStrategy;
+import impl.org.controlsfx.tools.rectangle.change.NewChangeStrategy;
+import impl.org.controlsfx.tools.rectangle.change.Rectangle2DChangeStrategy;
+import impl.org.controlsfx.tools.rectangle.change.ToEastChangeStrategy;
+import impl.org.controlsfx.tools.rectangle.change.ToNorthChangeStrategy;
+import impl.org.controlsfx.tools.rectangle.change.ToNortheastChangeStrategy;
+import impl.org.controlsfx.tools.rectangle.change.ToNorthwestChangeStrategy;
+import impl.org.controlsfx.tools.rectangle.change.ToSouthChangeStrategy;
+import impl.org.controlsfx.tools.rectangle.change.ToSoutheastChangeStrategy;
+import impl.org.controlsfx.tools.rectangle.change.ToSouthwestChangeStrategy;
+import impl.org.controlsfx.tools.rectangle.change.ToWestChangeStrategy;
+
+import java.util.ArrayList;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+import javafx.event.EventType;
+import javafx.geometry.Bounds;
+import javafx.geometry.Point2D;
+import javafx.geometry.Rectangle2D;
+import javafx.scene.Cursor;
+import javafx.scene.Node;
+import javafx.scene.input.MouseButton;
+import javafx.scene.input.MouseEvent;
+
+import org.controlsfx.control.SnapshotView;
+import org.controlsfx.control.SnapshotView.Boundary;
+
+import com.sun.javafx.scene.control.behavior.BehaviorBase;
+import com.sun.javafx.scene.control.behavior.KeyBinding;
+
+/**
+ * The behavior for the {@link SnapshotView}. It is concerned with creating and changing selections according to mouse
+ * events handed to {@link #handleMouseEvent(MouseEvent) handleMouseEvents}.
+ */
+public class SnapshotViewBehavior extends BehaviorBase<SnapshotView> {
+
+    /**
+     * The percentage of the control's node's width/height used as a tolerance for determining whether the cursor is on
+     * an edge of the selection.
+     */
+    private static final double RELATIVE_EDGE_TOLERANCE = 0.015;
+
+    /* ************************************************************************
+     *                                                                         *
+     * Attributes                                                              *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * The current selection change; might be {@code null}.
+     */
+    private SelectionChange selectionChange;
+
+    /**
+     * A function which sets the {@link SnapshotView#selectionChangingProperty() selectionChanging} property to the
+     * given value.
+     */
+    private final Consumer<Boolean> setSelectionChanging;
+
+    /* ************************************************************************
+     *                                                                         *
+     * Constructor                                                             *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * Creates a new behavior for the specified {@link SnapshotView}.
+     * 
+     * @param snapshotView
+     *            the control which this behavior will control
+     */
+    public SnapshotViewBehavior(SnapshotView snapshotView) {
+        super(snapshotView, new ArrayList<KeyBinding>());
+        this.setSelectionChanging = createSetSelectionChanging();
+    }
+
+    /**
+     * Creates a function which sets the applied boolean to {@link SnapshotView#selectionChangingProperty()}.
+     * 
+     * @return a Boolean {@link Consumer}
+     */
+    private Consumer<Boolean> createSetSelectionChanging() {
+        return changing -> getControl().getProperties().put(SnapshotView.SELECTION_CHANGING_PROPERTY_KEY, changing);
+    }
+
+    /* ************************************************************************
+     *                                                                         *
+     * Events                                                                  *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * Handles the specified mouse event (possibly by creating/changing/removing a selection) and returns the matching
+     * cursor.
+     * 
+     * @param mouseEvent
+     *            the handled {@link MouseEvent}; must not be {@code null}
+     * @return the cursor which will be used for this event
+     */
+    public Cursor handleMouseEvent(MouseEvent mouseEvent) {
+        Objects.requireNonNull(mouseEvent, "The argument 'mouseEvent' must not be null."); //$NON-NLS-1$
+
+        EventType<? extends MouseEvent> eventType = mouseEvent.getEventType();
+        SelectionEvent selectionEvent = createSelectionEvent(mouseEvent);
+
+        if (eventType == MouseEvent.MOUSE_MOVED) {
+            return getCursor(selectionEvent);
+        }
+        if (eventType == MouseEvent.MOUSE_PRESSED) {
+            return handleMousePressedEvent(selectionEvent);
+        }
+        if (eventType == MouseEvent.MOUSE_DRAGGED) {
+            return handleMouseDraggedEvent(selectionEvent);
+        }
+        if (eventType == MouseEvent.MOUSE_RELEASED) {
+            return handleMouseReleasedEvent(selectionEvent);
+        }
+
+        return Cursor.DEFAULT;
+    }
+
+    // TRANSFORM MOUSE EVENT TO SELECTION EVENT
+
+    /**
+     * Creates a selection event for the specified mouse event
+     * 
+     * @param mouseEvent
+     *            the {@link MouseEvent} for which the selection event will be created
+     * @return the {@link SelectionEvent} for the specified mouse event
+     */
+    private SelectionEvent createSelectionEvent(MouseEvent mouseEvent) {
+        Point2D point = new Point2D(mouseEvent.getX(), mouseEvent.getY());
+        Rectangle2D selectionBounds = createBoundsForCurrentBoundary();
+        CoordinatePosition position = computePosition(point);
+        return new SelectionEvent(mouseEvent, point, selectionBounds, position);
+    }
+
+    /**
+     * Returns the bounds according to the current {@link SnapshotView#selectionAreaBoundaryProperty()
+     * selectionAreaBoundary}.
+     * 
+     * @return the bounds as a {@link Rectangle2D}
+     */
+    private Rectangle2D createBoundsForCurrentBoundary() {
+        Boundary boundary = getControl().getSelectionAreaBoundary();
+        switch (boundary) {
+        case CONTROL:
+            return new Rectangle2D(0, 0, getControlWidth(), getControlHeight());
+        case NODE:
+            boolean nodeExists = getNode() != null;
+            if (nodeExists) {
+                Bounds nodeBounds = getNode().getBoundsInParent();
+                return Rectangles2D.fromBounds(nodeBounds);
+            } else {
+                return Rectangle2D.EMPTY;
+            }
+        default:
+            throw new IllegalArgumentException("The boundary " + boundary + " is not fully implemented."); //$NON-NLS-1$ //$NON-NLS-2$
+        }
+    }
+
+    /**
+     * Returns the position of the specified point relative to a possible selection.
+     * 
+     * @param point
+     *            the point (in the node's preferred coordinates) whose position will be computed
+     * 
+     * @return the {@link CoordinatePosition} the event's point has relative to the control's current selection; if the
+     *         selection is inactive this always returns {@link CoordinatePosition#OUT_OF_RECTANGLE}.
+     */
+    private CoordinatePosition computePosition(Point2D point) {
+        boolean noSelection = !getControl().hasSelection() || !getControl().isSelectionActive();
+        boolean controlHasNoSpace = getControlWidth() == 0 || getControlHeight() == 0;
+        if (noSelection || controlHasNoSpace) {
+            return CoordinatePosition.OUT_OF_RECTANGLE;
+        }
+
+        double tolerance = computeTolerance();
+        return computePosition(getSelection(), point, tolerance);
+    }
+
+    /**
+     * Computes the tolerance which is used to determine whether the cursor is on an edge.
+     * 
+     * @return the absolute tolerance
+     */
+    private double computeTolerance() {
+        double controlMeanLength = Math.sqrt(getControlWidth() * getControlHeight());
+        return RELATIVE_EDGE_TOLERANCE * controlMeanLength;
+    }
+
+    /**
+     * Returns the position of the specified point relative to the specified selection with the specified tolerance.
+     * 
+     * @param selection
+     *            the selection relative to which the point's position will be computed; as a {@link Rectangle2D}
+     * @param point
+     *            the {@link Point2D} whose position will be computed
+     * @param tolerance
+     *            the absolute tolerance used to determine whether the point is on an edge
+     * 
+     * @return the {@link CoordinatePosition} the event's point has relative to the control's current selection; if the
+     *         selection is inactive this always returns {@link CoordinatePosition#OUT_OF_RECTANGLE}.
+     */
+    private static CoordinatePosition computePosition(Rectangle2D selection, Point2D point, double tolerance) {
+        CoordinatePosition onEdge = CoordinatePositions.onEdges(selection, point, tolerance);
+        if (onEdge != null) {
+            return onEdge;
+        } else {
+            return CoordinatePositions.inRectangle(selection, point);
+        }
+    }
+
+    // HANDLE SELECTION EVENTS
+
+    /**
+     * Handles {@link MouseEvent#MOUSE_PRESSED} events by creating a new {@link #selectionChange} and beginning the
+     * change.
+     * 
+     * @param selectionEvent
+     *            the handled {@link SelectionEvent}
+     * @return the cursor which will be used while the selection changes
+     */
+    private Cursor handleMousePressedEvent(SelectionEvent selectionEvent) {
+        if (selectionEvent.isPointInSelectionBounds()) {
+            // get all necessary information to create a selection change
+            Cursor cursor = getCursor(selectionEvent);
+            Rectangle2DChangeStrategy selectionChangeStrategy = getChangeStrategy(selectionEvent);
+            boolean deactivateSelectionIfClick = willDeactivateSelectionIfClick(selectionEvent);
+
+            // create and begin the selection change
+            selectionChange = new SelectionChangeByStrategy(
+                    getControl(), setSelectionChanging, selectionChangeStrategy, cursor, deactivateSelectionIfClick);
+            selectionChange.beginSelectionChange(selectionEvent.getPoint());
+        } else {
+            // if the mouse is outside the legal bounds, the selection will not actually change
+            selectionChange = NoSelectionChange.INSTANCE;
+        }
+
+        return selectionChange.getCursor();
+    }
+    /**
+     * Handles {@link MouseEvent#MOUSE_DRAGGED} events by continuing the current {@link #selectionChange}.
+     * 
+     * @param selectionEvent
+     *            the handled {@link SelectionEvent}
+     * @return the cursor which will be used while the selection changes
+     */
+    private Cursor handleMouseDraggedEvent(SelectionEvent selectionEvent) {
+        selectionChange.continueSelectionChange(selectionEvent.getPoint());
+        return selectionChange.getCursor();
+    }
+
+    /**
+     * Handles {@link MouseEvent#MOUSE_RELEASED} events by ending the current {@link #selectionChange} and setting it to
+     * {@code null}.
+     * 
+     * @param selectionEvent
+     *            the handled {@link SelectionEvent}
+     * @return the cursor which will be used after the selection change ends
+     */
+    private Cursor handleMouseReleasedEvent(SelectionEvent selectionEvent) {
+        // end and deactivate the selection change
+        selectionChange.endSelectionChange(selectionEvent.getPoint());
+        selectionChange = null;
+
+        return getCursor(selectionEvent);
+    }
+
+    // CURSOR AND SELECTION CHANGE
+
+    /**
+     * Returns the cursor which will be used for the specified selection event.
+     * 
+     * @param selectionEvent
+     *            the {@link SelectionEvent} to check
+     * @return the {@link Cursor} which will be used for the event
+     */
+    private Cursor getCursor(SelectionEvent selectionEvent) {
+        // show the default cursor if the mouse is out of the selection bounds
+        if (!selectionEvent.isPointInSelectionBounds()) {
+            return getRegularCursor();
+        }
+
+        // otherwise pick a cursor from the relative position
+        switch (selectionEvent.getPosition()) {
+        case IN_RECTANGLE:
+            return Cursor.MOVE;
+        case OUT_OF_RECTANGLE:
+            return getRegularCursor();
+        case NORTH_EDGE:
+            return Cursor.N_RESIZE;
+        case NORTHEAST_EDGE:
+            return Cursor.NE_RESIZE;
+        case EAST_EDGE:
+            return Cursor.E_RESIZE;
+        case SOUTHEAST_EDGE:
+            return Cursor.SE_RESIZE;
+        case SOUTH_EDGE:
+            return Cursor.S_RESIZE;
+        case SOUTHWEST_EDGE:
+            return Cursor.SW_RESIZE;
+        case WEST_EDGE:
+            return Cursor.W_RESIZE;
+        case NORTHWEST_EDGE:
+            return Cursor.NW_RESIZE;
+        default:
+            throw new IllegalArgumentException("The position " + selectionEvent.getPosition() //$NON-NLS-1$
+                    + " is not fully implemented."); //$NON-NLS-1$
+        }
+    }
+
+    /**
+     * @return the cursor from the {@link #getControl() control's} current {@link SnapshotView#cursorProperty() cursor}
+     */
+    private Cursor getRegularCursor() {
+        return getControl().getCursor();
+    }
+
+    /**
+     * Returns the selection change strategy based on the specified selection event, which must be a
+     * {@link MouseEvent#MOUSE_PRESSED MOUSE_PRESSED} event.
+     * 
+     * @param selectionEvent
+     *            the {@link SelectionEvent} which will be checked
+     * @return the {@link Rectangle2DChangeStrategy} which will be executed based on the selection event
+     * @throws IllegalArgumentException
+     *             if {@link SelectionEvent#getMouseEvent()} is not of type {@link MouseEvent#MOUSE_PRESSED}.
+     */
+    private Rectangle2DChangeStrategy getChangeStrategy(SelectionEvent selectionEvent) {
+        boolean mousePressed = selectionEvent.getMouseEvent().getEventType() == MouseEvent.MOUSE_PRESSED;
+        if (!mousePressed) {
+            throw new IllegalArgumentException();
+        }
+
+        Rectangle2D selectionBounds = selectionEvent.getSelectionBounds();
+
+        switch (selectionEvent.getPosition()) {
+        case IN_RECTANGLE:
+            return new MoveChangeStrategy(getSelection(), selectionBounds);
+        case OUT_OF_RECTANGLE:
+            return new NewChangeStrategy(
+                    isSelectionRatioFixed(), getSelectionRatio(), selectionBounds);
+        case NORTH_EDGE:
+            return new ToNorthChangeStrategy(
+                    getSelection(), isSelectionRatioFixed(), getSelectionRatio(), selectionBounds);
+        case NORTHEAST_EDGE:
+            return new ToNortheastChangeStrategy(
+                    getSelection(), isSelectionRatioFixed(), getSelectionRatio(), selectionBounds);
+        case EAST_EDGE:
+            return new ToEastChangeStrategy(
+                    getSelection(), isSelectionRatioFixed(), getSelectionRatio(), selectionBounds);
+        case SOUTHEAST_EDGE:
+            return new ToSoutheastChangeStrategy(
+                    getSelection(), isSelectionRatioFixed(), getSelectionRatio(), selectionBounds);
+        case SOUTH_EDGE:
+            return new ToSouthChangeStrategy(
+                    getSelection(), isSelectionRatioFixed(), getSelectionRatio(), selectionBounds);
+        case SOUTHWEST_EDGE:
+            return new ToSouthwestChangeStrategy(
+                    getSelection(), isSelectionRatioFixed(), getSelectionRatio(), selectionBounds);
+        case WEST_EDGE:
+            return new ToWestChangeStrategy(
+                    getSelection(), isSelectionRatioFixed(), getSelectionRatio(), selectionBounds);
+        case NORTHWEST_EDGE:
+            return new ToNorthwestChangeStrategy(
+                    getSelection(), isSelectionRatioFixed(), getSelectionRatio(), selectionBounds);
+        default:
+            throw new IllegalArgumentException("The position " + selectionEvent.getPosition() //$NON-NLS-1$
+                    + " is not fully implemented."); //$NON-NLS-1$
+        }
+    }
+
+    /**
+     * Checks whether the selection will be deactivated if the mouse is clicked at the {@link SelectionEvent}.
+     * 
+     * @param selectionEvent
+     *            the selection event which will be checked
+     * @return {@code true} if the selection event is such that the selection will be deactivated if the mouse is only
+     *         clicked
+     */
+    private static boolean willDeactivateSelectionIfClick(SelectionEvent selectionEvent) {
+        boolean rightClick = selectionEvent.getMouseEvent().getButton() == MouseButton.SECONDARY;
+        boolean outOfAreaClick = selectionEvent.getPosition() == CoordinatePosition.OUT_OF_RECTANGLE;
+
+        return rightClick || outOfAreaClick;
+    }
+
+    /* ************************************************************************
+     *                                                                         *
+     * Usability Access Functions to SnapshotView Properties                   *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * The control's width.
+     * 
+     * @return {@link SnapshotView#getWidth()}
+     */
+    private double getControlWidth() {
+        return getControl().getWidth();
+    }
+
+    /**
+     * The control's height.
+     * 
+     * @return {@link SnapshotView#getHeight()}
+     */
+    private double getControlHeight() {
+        return getControl().getHeight();
+    }
+
+    /**
+     * The currently displayed node.
+     * 
+     * @return {@link SnapshotView#getNode()}
+     */
+    private Node getNode() {
+        return getControl().getNode();
+    }
+
+    /**
+     * The current selection.
+     * 
+     * @return {@link SnapshotView#getSelection()}
+     */
+    private Rectangle2D getSelection() {
+        return getControl().getSelection();
+    }
+    /**
+     * Indicates whether the current selection has a fixed ratio.
+     * 
+     * @return {@link SnapshotView#isSelectionRatioFixed()}
+     */
+    private boolean isSelectionRatioFixed() {
+        return getControl().isSelectionRatioFixed();
+    }
+
+    /**
+     * The current selection's fixed ratio.
+     * 
+     * @return {@link SnapshotView#getFixedSelectionRatio()}
+     */
+    private double getSelectionRatio() {
+        return getControl().getFixedSelectionRatio();
+    }
+
+    /* ************************************************************************
+     *                                                                         *
+     * Inner Classes                                                           *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * A selection event encapsulates a {@link MouseEvent} and adds some additional information like the coordinates
+     * relative to the node's preferred size and its position relative to a selection.
+     */
+    private static class SelectionEvent {
+
+        /**
+         * The {@link MouseEvent} for which this selection event was created.
+         */
+        private final MouseEvent mouseEvent;
+
+        /**
+         * The coordinates of the mouse event as a {@link Point2D}.
+         */
+        private final Point2D point;
+
+        /**
+         * The {@link Rectangle2D} within which any new selection must be contained.
+         */
+        private final Rectangle2D selectionBounds;
+
+        /**
+         * The {@link #point}'s position relative to a possible selection.
+         */
+        private final CoordinatePosition position;
+
+        /**
+         * Creates a new selection event with the specified arguments.
+         * 
+         * @param mouseEvent
+         *            the {@link MouseEvent} for which this selection event is created
+         * @param point
+         *            the coordinates of the mouse event as a {@link Point2D}
+         * @param selectionBounds
+         *            the {@link Rectangle2D} within which any new selection must be contained
+         * @param position
+         *            the point's position relative to a possible selection
+         */
+        public SelectionEvent(
+                MouseEvent mouseEvent, Point2D point, Rectangle2D selectionBounds, CoordinatePosition position) {
+
+            this.mouseEvent = mouseEvent;
+            this.point = point;
+            this.selectionBounds = selectionBounds;
+            this.position = position;
+        }
+
+        /**
+         * @return the mouse event for which this selection event was created
+         */
+        public MouseEvent getMouseEvent() {
+            return mouseEvent;
+        }
+
+        /**
+         * @return the coordinates of the mouse event in the nodes' preferred coordinates
+         */
+        public Point2D getPoint() {
+            return point;
+        }
+
+        /**
+         * @return the {@link Rectangle2D} within which any new selection must be contained
+         */
+        public Rectangle2D getSelectionBounds() {
+            return selectionBounds;
+        }
+
+        /**
+         * @return {@code true} if the {@link #getSelectionBounds() selectionBounds} contains the {@link #getPoint()
+         *         point}; otherwise {@code false}
+         */
+        public boolean isPointInSelectionBounds() {
+            return selectionBounds.contains(point);
+        }
+
+        /**
+         * @return the {@link #getPoint() point}'s position relative to a possible selection.
+         */
+        public CoordinatePosition getPosition() {
+            return position;
+        }
+
+    }
+
+    /**
+     * Handles the actual change of a selection when the mouse is pressed, dragged and released.
+     */
+    private static interface SelectionChange {
+
+        /**
+         * Begins the selection change at the specified point.
+         * 
+         * @param point
+         *            the starting point of the selection change
+         */
+        public abstract void beginSelectionChange(Point2D point);
+
+        /**
+         * Continues the selection change to the specified point.
+         * 
+         * @param point
+         *            the next point of this selection change
+         */
+        public abstract void continueSelectionChange(Point2D point);
+
+        /**
+         * Ends the selection change at the specified point.
+         * 
+         * @param point
+         *            the final point of this selection change
+         */
+        public abstract void endSelectionChange(Point2D point);
+
+        /**
+         * The cursor for this selection change.
+         * 
+         * @return the cursor for this selection change
+         */
+        public abstract Cursor getCursor();
+
+    }
+
+    /**
+     * Implementation of {@link SelectionChange} which does not actually change anything.
+     */
+    private static class NoSelectionChange implements SelectionChange {
+
+        /**
+         * The singleton instance.
+         */
+        public static final NoSelectionChange INSTANCE = new NoSelectionChange();
+
+        /**
+         * Private constructor for singleton.
+         */
+        private NoSelectionChange() {
+            // nothing to do
+        }
+
+        @Override
+        public void beginSelectionChange(Point2D point) {
+            // nothing to do
+        }
+
+        @Override
+        public void continueSelectionChange(Point2D point) {
+            // nothing to do
+        }
+
+        @Override
+        public void endSelectionChange(Point2D point) {
+            // nothing to do
+        }
+
+        @Override
+        public Cursor getCursor() {
+            return Cursor.DEFAULT;
+        }
+
+    }
+
+    /**
+     * Executes the changes from a {@link Rectangle2DChangeStrategy} on a {@link SnapshotView}'s
+     * {@link SnapshotView#selectionProperty() selection} property. This includes to check whether the mouse moved from
+     * the change's start to end and to possibly deactivate the selection if not.
+     */
+    private static class SelectionChangeByStrategy implements SelectionChange {
+
+        // Attributes
+
+        /**
+         * The snapshot view whose selection will be changed.
+         */
+        private final SnapshotView snapshotView;
+
+        /**
+         * A function which sets the {@link SnapshotView#selectionChangingProperty() selectionChanging} property to the
+         * given value.
+         */
+        private final Consumer<Boolean> setSelectionChanging;
+
+        /**
+         * The executed change strategy.
+         */
+        private final Rectangle2DChangeStrategy selectionChangeStrategy;
+
+        /**
+         * The cursor during the selection change.
+         */
+        private final Cursor cursor;
+
+        /**
+         * Indicates if the selection will be deactivated if the mouse is only clicked (e.g. does not move between start
+         * and end).
+         */
+        private final boolean deactivateSelectionIfClick;
+
+        /**
+         * The change's starting point. Used to check whether the mouse moved.
+         */
+        private Point2D startingPoint;
+
+        /**
+         * Set to true as soon as the mouse moved away from the starting point.
+         */
+        private boolean mouseMoved;
+
+        // Constructor
+
+        /**
+         * Creates a new selection change for the specified {@link SnapshotView} using the specified
+         * {@link Rectangle2DChangeStrategy}.
+         * 
+         * @param snapshotView
+         *            the {@link SnapshotView} whose selection will be changed
+         * @param setSelectionChanging
+         *            a function which sets the {@link SnapshotView#selectionChangingProperty() selectionChanging}
+         *            property to the given value
+         * @param selectionChangeStrategy
+         *            the {@link Rectangle2DChangeStrategy} used to change the selection
+         * @param cursor
+         *            the {@link Cursor} used during the selection change
+         * @param deactivateSelectionIfClick
+         *            indicates whether the selection will be deactivated if the change is only a click
+         */
+        public SelectionChangeByStrategy(
+                SnapshotView snapshotView, Consumer<Boolean> setSelectionChanging,
+                Rectangle2DChangeStrategy selectionChangeStrategy, Cursor cursor, boolean deactivateSelectionIfClick) {
+
+            this.snapshotView = snapshotView;
+            this.setSelectionChanging = setSelectionChanging;
+            this.selectionChangeStrategy = selectionChangeStrategy;
+            this.cursor = cursor;
+            this.deactivateSelectionIfClick = deactivateSelectionIfClick;
+        }
+
+        // Selection Change
+
+        @Override
+        public void beginSelectionChange(Point2D point) {
+            startingPoint = point;
+            setSelectionChanging.accept(true);
+
+            Rectangle2D newSelection = selectionChangeStrategy.beginChange(point);
+            snapshotView.setSelection(newSelection);
+        }
+
+        @Override
+        public void continueSelectionChange(Point2D point) {
+            updateMouseMoved(point);
+
+            Rectangle2D newSelection = selectionChangeStrategy.continueChange(point);
+            snapshotView.setSelection(newSelection);
+        }
+
+        @Override
+        public void endSelectionChange(Point2D point) {
+            updateMouseMoved(point);
+
+            Rectangle2D newSelection = selectionChangeStrategy.endChange(point);
+            snapshotView.setSelection(newSelection);
+
+            boolean deactivateSelection = deactivateSelectionIfClick && !mouseMoved;
+            if (deactivateSelection) {
+                snapshotView.setSelection(null);
+            }
+            setSelectionChanging.accept(false);
+        }
+
+        /**
+         * Updates {@link #mouseMoved} by checking whether the specified point is different from the
+         * {@link #startingPoint}.
+         * 
+         * @param point
+         *            the point which will be compared to the {@link #startingPoint}
+         */
+        private void updateMouseMoved(Point2D point) {
+            // if the mouse already moved, do nothing
+            if (mouseMoved) {
+                return;
+            }
+
+            // if the mouse did not move yet, check whether it did now
+            boolean mouseMovedNow = !startingPoint.equals(point);
+            mouseMoved = mouseMovedNow;
+        }
+
+        // Attribute Access
+
+        @Override
+        public Cursor getCursor() {
+            return cursor;
+        }
+
+    }
+
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/i18n/Localization.java b/controlsfx/src/main/java/impl/org/controlsfx/i18n/Localization.java
new file mode 100644
index 0000000..59c134c
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/i18n/Localization.java
@@ -0,0 +1,125 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.i18n;
+
+import java.util.Locale;
+import java.util.MissingResourceException;
+import java.util.ResourceBundle;
+
+public class Localization {
+
+	private Localization() {
+	}
+
+	public static final String KEY_PREFIX = "@@"; //$NON-NLS-1$
+
+	private static final String LOCALE_BUNDLE_NAME = "controlsfx"; //$NON-NLS-1$
+	private static Locale locale = null;
+
+	/**
+	 * Returns the Locale object that is associated with ControlsFX.
+	 * 
+	 * @return the global ControlsFX locale
+	 */
+	public static final Locale getLocale() {
+		// following allows us to have a "dynamic" locale based on OS/JDK
+		return locale == null ? Locale.getDefault() : locale;
+	}
+
+	/**
+	 * Sets locale which will be used as ControlsFX locale
+	 * 
+	 * @param newLocale
+	 *            null is allowed and will be interpreted as default locale
+	 */
+	public static final void setLocale(final Locale newLocale) {
+		locale = newLocale;
+	}
+
+	private static Locale resourceBundleLocale = null; // has to be null initially
+	private static ResourceBundle resourceBundle = null;
+
+	private static synchronized final ResourceBundle getLocaleBundle() {
+
+		Locale currentLocale = getLocale();
+		if (!currentLocale.equals(resourceBundleLocale)) {
+			resourceBundleLocale = currentLocale;
+			resourceBundle = ResourceBundle.getBundle(LOCALE_BUNDLE_NAME,
+					resourceBundleLocale, Localization.class.getClassLoader());
+		}
+		return resourceBundle;
+
+	}
+
+	/**
+	 * Returns a string localized using currently set locale
+	 * 
+	 * @param key resource bundle key
+	 * @return localized text or formatted key if not found
+	 */
+	public static final String getString(final String key) {
+		try {
+			return getLocaleBundle().getString(key);
+		} catch (MissingResourceException ex) {
+			return String.format("<%s>", key); //$NON-NLS-1$
+		}
+	}
+
+	/**
+	 * Converts text to localization key,
+	 * currently by prepending it with the KEY_PREFIX
+	 * 
+	 * @param text
+	 * @return localization key
+	 */
+	public static final String asKey(String text) {
+		return KEY_PREFIX + text;
+	}
+
+	/**
+	 * Checks if the text is a localization key
+	 * 
+	 * @param text
+	 * @return true if text is a localization key
+	 */
+	public static final boolean isKey(String text) {
+		return text != null && text.startsWith(KEY_PREFIX);
+	}
+
+	/**
+	 * Tries to localize the text. If the text is a localization key - and attempt will be made to 
+	 * use it for localization, otherwise the text is returned as is
+	 * 
+	 * @param text
+	 * @return
+	 */
+	public static String localize(String text) {
+		return isKey(text) ? getString(text.substring(KEY_PREFIX.length())
+				.trim()) : text;
+	}
+
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/i18n/SimpleLocalizedStringProperty.java b/controlsfx/src/main/java/impl/org/controlsfx/i18n/SimpleLocalizedStringProperty.java
new file mode 100644
index 0000000..44ce996
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/i18n/SimpleLocalizedStringProperty.java
@@ -0,0 +1,58 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.i18n;
+
+import javafx.beans.property.SimpleStringProperty;
+
+/**
+ * A special implementation of string property which assumes that its content may be a key and 
+ * attempts to get localized text resource base on it. 
+ * 
+ * It is intended for internal use only and will not work for bidirectional binding. 
+ */
+public class SimpleLocalizedStringProperty extends SimpleStringProperty {
+
+	public SimpleLocalizedStringProperty() {
+	}
+
+	public SimpleLocalizedStringProperty(String initialValue) {
+		super(initialValue);
+	}
+
+	public SimpleLocalizedStringProperty(Object bean, String name) {
+		super(bean, name);
+	}
+
+	public SimpleLocalizedStringProperty(Object bean, String name,
+			String initialValue) {
+		super(bean, name, initialValue);
+	}
+
+	@Override public String getValue() {
+		return Localization.localize(super.getValue());
+	}
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/i18n/Translation.java b/controlsfx/src/main/java/impl/org/controlsfx/i18n/Translation.java
new file mode 100644
index 0000000..a972bb4
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/i18n/Translation.java
@@ -0,0 +1,74 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.i18n;
+
+import java.nio.file.Path;
+import java.util.Locale;
+
+public class Translation implements Comparable<Translation> {
+
+    private final String localeString;
+    private final Locale locale;
+    private final Path path;
+    
+    public Translation(String locale, Path path) {
+        this.localeString = locale;
+        this.path = path;
+        
+        String[] split = localeString.split("_"); //$NON-NLS-1$
+        if (split.length == 1) {
+            this.locale = new Locale(localeString);
+        } else if (split.length == 2) {
+            this.locale = new Locale(split[0], split[1]);
+        } else if (split.length == 3) {
+            this.locale = new Locale(split[0], split[1], split[2]);
+        } else {
+            throw new IllegalArgumentException("Unknown locale string '" + locale + "'"); //$NON-NLS-1$ //$NON-NLS-2$
+        }
+    }
+    
+    public final String getLocaleString() {
+        return localeString;
+    }
+    
+    public final Locale getLocale() {
+        return locale;
+    }
+    
+    public final Path getPath() {
+        return path;
+    }
+    
+    @Override public String toString() {
+        return localeString;
+    }
+    
+    @Override public int compareTo(Translation o) {
+        if (o == null) return 1;
+        return localeString.compareTo(o.localeString);
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/i18n/Translations.java b/controlsfx/src/main/java/impl/org/controlsfx/i18n/Translations.java
new file mode 100644
index 0000000..ab90355
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/i18n/Translations.java
@@ -0,0 +1,129 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.i18n;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.DirectoryIteratorException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+public class Translations {
+
+    private static List<Translation> translations = new ArrayList<>();
+
+    static {
+        // firstly try to read from the controlsfx jar
+        File file = new File(Translations.class.getProtectionDomain().getCodeSource().getLocation().getPath());
+        if (file.getName().endsWith(".jar")) { //$NON-NLS-1$
+            Path jarFile = file.toPath();
+            try (FileSystem fs = FileSystems.newFileSystem(jarFile, null)) {
+                fs.getRootDirectories().forEach(path -> loadFrom(path));
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+        
+        // look in src directory
+        if (translations.isEmpty()) {
+            // try to read the files from the local filesystem (good for when ControlsFX
+            // is being run from within a developers IDE)
+            Path srcDir = new File("src/main/resources").toPath(); //$NON-NLS-1$
+            loadFrom(srcDir);
+        }
+        
+        // look in bin directory
+        if (translations.isEmpty()) {
+            Path binDir = new File("bin").toPath(); //$NON-NLS-1$
+            loadFrom(binDir);
+        }
+        
+        // look in bin directory an alternative way (good for when running 
+        // controlsfx-samples)
+        if (translations.isEmpty()) {
+            if (file.getAbsolutePath().endsWith("controlsfx" + File.separator + "bin")) { //$NON-NLS-1$ //$NON-NLS-2$
+                loadFrom(file.toPath());
+            }
+        }
+        
+        Collections.sort(translations);
+    }
+    
+    private static void loadFrom(Path rootPath) {
+        try (DirectoryStream<Path> stream = Files.newDirectoryStream(rootPath)) {
+            for (Path path : stream) {
+                String filename = path.getFileName().toString();
+
+                if (! filename.startsWith("controlsfx") && ! filename.endsWith(".properties")) { //$NON-NLS-1$ //$NON-NLS-2$
+                    continue;
+                }
+
+                if ("controlsfx.properties".equals(filename)) { //$NON-NLS-1$
+                    translations.add(new Translation("en", path)); //$NON-NLS-1$
+                } else if (filename.contains("_")) { //$NON-NLS-1$
+                    String locale = filename.substring(11, filename.indexOf(".properties")); //$NON-NLS-1$
+                    translations.add(new Translation(locale, path));
+                } else {
+                    throw new IllegalStateException("Unknown translation file '" + path + "'."); //$NON-NLS-1$ //$NON-NLS-2$
+                }
+
+            }
+        } catch (IOException | DirectoryIteratorException x) {
+            // no-op
+        }
+    }
+
+    private Translations() {
+        // no-op
+    }
+    
+    public static Optional<Translation> getTranslation(String localeString) {
+        for (Translation t : translations) {
+            if (localeString.equals(t.getLocaleString())) {
+                return Optional.of(t);
+            }
+        }
+        return Optional.empty();
+    }
+
+    public static List<Translation> getAllTranslations() {
+        return translations;
+    }
+
+    public static List<Locale> getAllTranslationLocales() {
+        return translations.stream().map((Translation t) -> t.getLocale()).collect(Collectors.toList());
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/skin/AutoCompletePopup.java b/controlsfx/src/main/java/impl/org/controlsfx/skin/AutoCompletePopup.java
new file mode 100644
index 0000000..e242d02
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/skin/AutoCompletePopup.java
@@ -0,0 +1,233 @@
+/**
+ * Copyright (c) 2014, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.skin;
+
+
+import com.sun.javafx.event.EventHandlerManager;
+import javafx.beans.property.IntegerProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.ObjectPropertyBase;
+import javafx.beans.property.SimpleIntegerProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.event.Event;
+import javafx.event.EventDispatchChain;
+import javafx.event.EventHandler;
+import javafx.event.EventType;
+import javafx.scene.Node;
+import javafx.scene.control.PopupControl;
+import javafx.scene.control.Skin;
+import javafx.stage.Window;
+import javafx.util.StringConverter;
+
+/**
+ * The auto-complete-popup provides an list of available suggestions in order
+ * to complete current user input.
+ */
+public class AutoCompletePopup<T> extends PopupControl{
+
+    /***************************************************************************
+     *                                                                         *
+     * Private fields                                                          *
+     *                                                                         *
+     **************************************************************************/
+
+    private final static int TITLE_HEIGHT = 28; // HACK: Hard-coded title-bar height
+    private final ObservableList<T> suggestions = FXCollections.observableArrayList();
+    private StringConverter<T> converter;
+    /**
+     * The maximum number of rows to be visible in the popup when it is
+     * showing. By default this value is 10, but this can be changed to increase
+     * or decrease the height of the popup.
+     */
+    private IntegerProperty visibleRowCount = new SimpleIntegerProperty(this, "visibleRowCount", 10);
+
+    /***************************************************************************
+     *                                                                         *
+     * Inner classes                                                           *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * Represents an Event which is fired when the user has selected a suggestion
+     * for auto-complete
+     *
+     * @param <TE>
+     */
+    @SuppressWarnings("serial")
+    public static class SuggestionEvent<TE> extends Event {
+        @SuppressWarnings("rawtypes")
+        public static final EventType<SuggestionEvent> SUGGESTION = new EventType<>("SUGGESTION"); //$NON-NLS-1$
+
+        private final TE suggestion;
+
+        public SuggestionEvent(TE suggestion) {
+            super(SUGGESTION);
+            this.suggestion = suggestion;
+        }
+
+        /**
+         * Returns the suggestion which was chosen by the user
+         * @return
+         */
+        public TE getSuggestion() {
+            return suggestion;
+        }
+    }
+
+
+    /***************************************************************************
+     *                                                                         *
+     * Constructors                                                            *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * Creates a new AutoCompletePopup
+     */
+    public AutoCompletePopup(){
+        this.setAutoFix(true);
+        this.setAutoHide(true);
+        this.setHideOnEscape(true);
+
+        getStyleClass().add(DEFAULT_STYLE_CLASS);
+    }
+
+
+    /***************************************************************************
+     *                                                                         *
+     * Public API                                                              *
+     *                                                                         *
+     **************************************************************************/
+
+
+    /**
+     * Get the suggestions presented by this AutoCompletePopup
+     * @return
+     */
+    public ObservableList<T> getSuggestions() {
+        return suggestions;
+    }
+
+    /**
+     * Show this popup right below the given Node
+     * @param node
+     */
+    public void show(Node node){
+
+        if(node.getScene() == null || node.getScene().getWindow() == null)
+            throw new IllegalStateException("Can not show popup. The node must be attached to a scene/window."); //$NON-NLS-1$
+
+        if(isShowing()){
+            return;
+        }
+        
+        Window parent = node.getScene().getWindow();
+        this.show(
+                parent,
+                parent.getX() + node.localToScene(0, 0).getX() +
+                node.getScene().getX(),
+                parent.getY() + node.localToScene(0, 0).getY() +
+                node.getScene().getY() + TITLE_HEIGHT);
+
+    }
+
+    /**
+     * Set the string converter used to turn a generic suggestion into a string
+     */
+    public void setConverter(StringConverter<T> converter) {
+		this.converter = converter;
+	}
+    
+    /**
+     * Get the string converter used to turn a generic suggestion into a string
+     */
+	public StringConverter<T> getConverter() {
+		return converter;
+	}
+
+    public final void setVisibleRowCount(int value) {
+        visibleRowCount.set(value);
+    }
+
+    public final int getVisibleRowCount() {
+        return visibleRowCount.get();
+    }
+
+    public final IntegerProperty visibleRowCountProperty() {
+        return visibleRowCount;
+    }
+
+    /***************************************************************************
+     *                                                                         *
+     * Properties                                                              *
+     *                                                                         *
+     **************************************************************************/
+
+
+    private final EventHandlerManager eventHandlerManager = new EventHandlerManager(this);
+
+    public final ObjectProperty<EventHandler<SuggestionEvent<T>>> onSuggestionProperty() { return onSuggestion; }
+    public final void setOnSuggestion(EventHandler<SuggestionEvent<T>> value) { onSuggestionProperty().set(value); }
+    public final EventHandler<SuggestionEvent<T>> getOnSuggestion() { return onSuggestionProperty().get(); }
+    private ObjectProperty<EventHandler<SuggestionEvent<T>>> onSuggestion = new ObjectPropertyBase<EventHandler<SuggestionEvent<T>>>() {
+        @SuppressWarnings({ "rawtypes", "unchecked" })
+        @Override protected void invalidated() {
+            eventHandlerManager.setEventHandler(SuggestionEvent.SUGGESTION, (EventHandler<SuggestionEvent>)(Object)get());
+        }
+
+        @Override
+        public Object getBean() {
+            return AutoCompletePopup.this;
+        }
+
+        @Override
+        public String getName() {
+            return "onSuggestion"; //$NON-NLS-1$
+        }
+    };
+
+    /**{@inheritDoc}*/
+    @Override public EventDispatchChain buildEventDispatchChain(EventDispatchChain tail) {
+        return super.buildEventDispatchChain(tail).append(eventHandlerManager);
+    } 
+
+
+    /***************************************************************************
+     *                                                                         *
+     * Stylesheet Handling                                                     *
+     *                                                                         *
+     **************************************************************************/
+
+    public static final String DEFAULT_STYLE_CLASS = "auto-complete-popup"; //$NON-NLS-1$
+
+    @Override
+    protected Skin<?> createDefaultSkin() {
+        return new AutoCompletePopupSkin<>(this);
+    }
+
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/skin/AutoCompletePopupSkin.java b/controlsfx/src/main/java/impl/org/controlsfx/skin/AutoCompletePopupSkin.java
new file mode 100644
index 0000000..0edfac6
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/skin/AutoCompletePopupSkin.java
@@ -0,0 +1,115 @@
+/**
+ * Copyright (c) 2014, 2016 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.skin;
+
+import javafx.beans.binding.Bindings;
+import javafx.event.Event;
+import javafx.scene.Node;
+import javafx.scene.control.ListView;
+import javafx.scene.control.Skin;
+import javafx.scene.control.cell.TextFieldListCell;
+import javafx.scene.input.MouseButton;
+import org.controlsfx.control.textfield.AutoCompletionBinding;
+
+
+public class AutoCompletePopupSkin<T> implements Skin<AutoCompletePopup<T>> {
+
+    private final AutoCompletePopup<T> control;
+    private final ListView<T> suggestionList;
+    final int LIST_CELL_HEIGHT = 24;
+
+    public AutoCompletePopupSkin(AutoCompletePopup<T> control){
+        this.control = control;
+        suggestionList = new ListView<>(control.getSuggestions());
+
+        suggestionList.getStyleClass().add(AutoCompletePopup.DEFAULT_STYLE_CLASS);
+
+        suggestionList.getStylesheets().add(AutoCompletionBinding.class
+        		.getResource("autocompletion.css").toExternalForm()); //$NON-NLS-1$
+        /**
+         * Here we bind the prefHeightProperty to the minimum height between the
+         * max visible rows and the current items list. We also add an arbitrary
+         * 5 number because when we have only one item we have the vertical
+         * scrollBar showing for no reason.
+         */
+        suggestionList.prefHeightProperty().bind(
+                Bindings.min(control.visibleRowCountProperty(), Bindings.size(suggestionList.getItems()))
+                .multiply(LIST_CELL_HEIGHT).add(18));
+        suggestionList.setCellFactory(TextFieldListCell.forListView(control.getConverter()));
+        
+        //Allowing the user to control ListView width.
+        suggestionList.prefWidthProperty().bind(control.prefWidthProperty());
+        suggestionList.maxWidthProperty().bind(control.maxWidthProperty());
+        suggestionList.minWidthProperty().bind(control.minWidthProperty());
+        registerEventListener();
+    }
+
+    private void registerEventListener(){
+        suggestionList.setOnMouseClicked(me -> {
+            if (me.getButton() == MouseButton.PRIMARY){
+                onSuggestionChoosen(suggestionList.getSelectionModel().getSelectedItem());
+            }
+        });
+
+
+        suggestionList.setOnKeyPressed(ke -> {
+            switch (ke.getCode()) {
+                case ENTER:
+                    onSuggestionChoosen(suggestionList.getSelectionModel().getSelectedItem());
+                    break;
+                case ESCAPE:
+                    if (control.isHideOnEscape()) {
+                        control.hide();
+                    }
+                    break;
+                default:
+                    break;
+            }
+        });
+    }
+
+    private void onSuggestionChoosen(T suggestion){
+        if(suggestion != null) {
+            Event.fireEvent(control, new AutoCompletePopup.SuggestionEvent<>(suggestion));
+        }
+    }
+
+
+    @Override
+    public Node getNode() {
+        return suggestionList;
+    }
+
+    @Override
+    public AutoCompletePopup<T> getSkinnable() {
+        return control;
+    }
+
+    @Override
+    public void dispose() {
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/skin/BreadCrumbBarSkin.java b/controlsfx/src/main/java/impl/org/controlsfx/skin/BreadCrumbBarSkin.java
new file mode 100644
index 0000000..1bd8f98
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/skin/BreadCrumbBarSkin.java
@@ -0,0 +1,381 @@
+/**
+ * Copyright (c) 2014, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.skin;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.event.Event;
+import javafx.event.EventHandler;
+import javafx.scene.Node;
+import javafx.scene.control.Button;
+import javafx.scene.control.TreeItem;
+import javafx.scene.control.TreeItem.TreeModificationEvent;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.ArcTo;
+import javafx.scene.shape.ClosePath;
+import javafx.scene.shape.HLineTo;
+import javafx.scene.shape.LineTo;
+import javafx.scene.shape.MoveTo;
+import javafx.scene.shape.Path;
+import javafx.util.Callback;
+
+import org.controlsfx.control.BreadCrumbBar;
+import org.controlsfx.control.BreadCrumbBar.BreadCrumbActionEvent;
+
+import com.sun.javafx.scene.control.behavior.BehaviorBase;
+import com.sun.javafx.scene.control.behavior.KeyBinding;
+import com.sun.javafx.scene.control.skin.BehaviorSkinBase;
+import com.sun.javafx.scene.traversal.Algorithm;
+import com.sun.javafx.scene.traversal.Direction;
+import com.sun.javafx.scene.traversal.ParentTraversalEngine;
+import com.sun.javafx.scene.traversal.TraversalContext;
+
+/**
+ * Basic Skin implementation for the {@link BreadCrumbBar}
+ *
+ * @param <T>
+ */
+public class BreadCrumbBarSkin<T> extends BehaviorSkinBase<BreadCrumbBar<T>, BehaviorBase<BreadCrumbBar<T>>> {
+        
+    private static final String STYLE_CLASS_FIRST = "first"; //$NON-NLS-1$
+
+    public BreadCrumbBarSkin(final BreadCrumbBar<T> control) {
+        super(control, new BehaviorBase<>(control, Collections.<KeyBinding> emptyList()));
+        control.selectedCrumbProperty().addListener(selectedPathChangeListener);
+        updateSelectedPath(getSkinnable().selectedCrumbProperty().get(), null);
+        fixFocusTraversal();
+    }
+
+    // https://bitbucket.org/controlsfx/controlsfx/issue/453/breadcrumbbar-keyboard-focus-traversal-is
+    // ContainerTabOrder will fail with LEFT/RIGHT navigation, since the buttons in bread crumb overlap
+    private void fixFocusTraversal() {
+
+        ParentTraversalEngine engine = new ParentTraversalEngine(getSkinnable(), new Algorithm() {
+
+            @Override
+            public Node select(Node owner, Direction dir, TraversalContext context) {
+                Node node = null;
+                int idx = getChildren().indexOf(owner);
+                switch(dir) {
+                    case NEXT:
+                    case NEXT_IN_LINE:
+                    case RIGHT:
+                        if (idx < getChildren().size() - 1) {
+                            node = getChildren().get(idx+1);
+                        }
+                    break;
+                    case PREVIOUS:
+                    case LEFT:
+                        if (idx > 0) {
+                            node = getChildren().get(idx-1);
+                        }
+                        break;
+                }
+                return node;
+            }
+
+            @Override
+            public Node selectFirst(TraversalContext context) {
+                Node first = null;
+                if (!getChildren().isEmpty()) {
+                    first = getChildren().get(0);
+                }
+                return first;
+            }
+
+            @Override
+            public Node selectLast(TraversalContext context) {
+                Node last = null;
+                if (!getChildren().isEmpty()) {
+                    last = getChildren().get(getChildren().size()-1);
+                }
+                return last;
+            }
+        });
+        engine.setOverriddenFocusTraversability(false);
+        getSkinnable().setImpl_traversalEngine(engine);
+
+    }
+
+    private final ChangeListener<TreeItem<T>> selectedPathChangeListener =
+            (obs, oldItem, newItem) -> updateSelectedPath(newItem, oldItem);
+
+    private void updateSelectedPath(TreeItem<T> newTarget, TreeItem<T> oldTarget) {
+        if(oldTarget != null){
+            // remove old listener
+            oldTarget.removeEventHandler(
+                    TreeItem.childrenModificationEvent(), treeChildrenModifiedHandler);
+        }
+        if(newTarget != null){
+            // add new listener
+            newTarget.addEventHandler(TreeItem.childrenModificationEvent(), treeChildrenModifiedHandler);
+        }
+        updateBreadCrumbs();
+    }
+
+
+    private final EventHandler<TreeModificationEvent<Object>> treeChildrenModifiedHandler = 
+            args -> updateBreadCrumbs();
+
+
+    private void updateBreadCrumbs() {
+        final BreadCrumbBar<T> buttonBar = getSkinnable();
+        final TreeItem<T> pathTarget = buttonBar.getSelectedCrumb();
+        final Callback<TreeItem<T>, Button> factory = buttonBar.getCrumbFactory();
+
+        getChildren().clear();
+
+        if(pathTarget != null){
+            List<TreeItem<T>> crumbs = constructFlatPath(pathTarget);
+
+            for (int i=0; i < crumbs.size(); i++) {
+                Button crumb = createCrumb(factory, crumbs.get(i));
+                crumb.setMnemonicParsing(false);
+                if (i == 0) {
+                    if (! crumb.getStyleClass().contains(STYLE_CLASS_FIRST)) {
+                        crumb.getStyleClass().add(STYLE_CLASS_FIRST);
+                    }
+                } else {
+                    crumb.getStyleClass().remove(STYLE_CLASS_FIRST);
+                }
+
+                getChildren().add(crumb);
+            }
+        }
+    }
+    
+    @Override protected void layoutChildren(double x, double y, double w, double h) {
+        for (int i = 0; i < getChildren().size(); i++) {
+            Node n = getChildren().get(i);
+            
+            double nw = snapSize(n.prefWidth(h));
+            double nh = snapSize(n.prefHeight(-1));
+            
+            if (i > 0) {
+                // We have to position the bread crumbs slightly overlapping
+                double ins = n instanceof BreadCrumbButton ?  ((BreadCrumbButton)n).getArrowWidth() : 0;
+                x = snapPosition(x - ins);
+            }
+
+            n.resize(nw, nh);
+            n.relocate(x, y);
+            x += nw;
+        }
+    }
+
+    /**
+     * Construct a flat list for the crumbs
+     * @param bottomMost The crumb node at the end of the path
+     * @return
+     */
+    private List<TreeItem<T>> constructFlatPath(TreeItem<T> bottomMost){
+        List<TreeItem<T>> path = new ArrayList<>();
+
+        TreeItem<T> current = bottomMost;
+        do {
+            path.add(current);
+            current = current.getParent();
+        } while (current != null);
+
+        Collections.reverse(path);
+        return path;
+    }
+
+    private Button createCrumb(
+            final Callback<TreeItem<T>, Button> factory,
+            final TreeItem<T> selectedCrumb) {
+
+        Button crumb = factory.call(selectedCrumb);
+        
+        crumb.getStyleClass().add("crumb"); //$NON-NLS-1$
+
+        // We want all buttons to have the same height
+        // so we bind their preferred height to the enclosing container
+//        crumb.prefHeightProperty().bind(getSkinnable().heightProperty());
+
+        // listen to the action event of each bread crumb
+        crumb.setOnAction(ae -> onBreadCrumbAction(selectedCrumb));
+
+        return crumb;
+    }
+
+    /**
+     * Occurs when a bread crumb gets the action event
+     * 
+     * @param crumbModel The crumb which received the action event
+     */
+    protected void onBreadCrumbAction(final TreeItem<T> crumbModel){
+        final BreadCrumbBar<T> breadCrumbBar = getSkinnable();
+
+        // fire the composite event in the breadCrumbBar
+        Event.fireEvent(breadCrumbBar, new BreadCrumbActionEvent<>(crumbModel));
+
+        // navigate to the clicked crumb
+        if(breadCrumbBar.isAutoNavigationEnabled()){
+            breadCrumbBar.setSelectedCrumb(crumbModel);
+        }
+    }
+
+    
+    
+    
+    /**
+     * Represents a BreadCrumb Button
+     * 
+     * <pre>
+     * ----------
+     *  \         \
+     *  /         /
+     * ----------
+     * </pre>
+     * 
+     * 
+     */
+    public static class BreadCrumbButton extends Button {
+
+        private final ObjectProperty<Boolean> first = new SimpleObjectProperty<>(this, "first"); //$NON-NLS-1$
+
+        private final double arrowWidth = 5;
+        private final double arrowHeight = 20;
+
+        /**
+         * Create a BreadCrumbButton
+         * 
+         * @param text Buttons text
+         */
+        public BreadCrumbButton(String text){
+            this(text, null);
+        }
+
+        /**
+         * Create a BreadCrumbButton
+         * @param text Buttons text
+         * @param gfx Gfx of the Button
+         */
+        public BreadCrumbButton(String text, Node gfx){
+            super(text, gfx);
+            first.set(false);
+
+            getStyleClass().addListener(new InvalidationListener() {
+                @Override public void invalidated(Observable arg0) {
+                    updateShape();
+                }
+            });
+
+            updateShape();
+        }
+
+        private void updateShape(){
+            this.setShape(createButtonShape());
+        }
+
+
+        /**
+         * Gets the crumb arrow with
+         * @return
+         */
+        public double getArrowWidth(){
+            return arrowWidth;
+        }
+
+        /**
+         * Create an arrow path
+         * 
+         * Based upon Uwe / Andy Till code snippet found here:
+         * @see http://ustesis.wordpress.com/2013/11/04/implementing-breadcrumbs-in-javafx/
+         * @return
+         */
+        private Path createButtonShape(){
+            // build the following shape (or home without left arrow)
+
+            //   --------
+            //  \         \
+            //  /         /
+            //   --------
+            Path path = new Path();
+
+            // begin in the upper left corner
+            MoveTo e1 = new MoveTo(0, 0);
+            path.getElements().add(e1);
+
+            // draw a horizontal line that defines the width of the shape
+            HLineTo e2 = new HLineTo();
+            // bind the width of the shape to the width of the button
+            e2.xProperty().bind(this.widthProperty().subtract(arrowWidth));
+            path.getElements().add(e2);
+
+            // draw upper part of right arrow
+            LineTo e3 = new LineTo();
+            // the x endpoint of this line depends on the x property of line e2
+            e3.xProperty().bind(e2.xProperty().add(arrowWidth));
+            e3.setY(arrowHeight / 2.0);
+            path.getElements().add(e3);
+
+            // draw lower part of right arrow
+            LineTo e4 = new LineTo();
+            // the x endpoint of this line depends on the x property of line e2
+            e4.xProperty().bind(e2.xProperty());
+            e4.setY(arrowHeight);
+            path.getElements().add(e4);
+
+            // draw lower horizontal line
+            HLineTo e5 = new HLineTo(0);
+            path.getElements().add(e5);
+
+            if(! getStyleClass().contains(STYLE_CLASS_FIRST)){
+                // draw lower part of left arrow
+                // we simply can omit it for the first Button
+                LineTo e6 = new LineTo(arrowWidth, arrowHeight / 2.0);
+                path.getElements().add(e6);
+            }else{
+                // draw an arc for the first bread crumb
+                ArcTo arcTo = new ArcTo();
+                arcTo.setSweepFlag(true);
+                arcTo.setX(0);
+                arcTo.setY(0);
+                arcTo.setRadiusX(15.0f);
+                arcTo.setRadiusY(15.0f);
+                path.getElements().add(arcTo);
+            }
+
+            // close path
+            ClosePath e7 = new ClosePath();
+            path.getElements().add(e7);
+            // this is a dummy color to fill the shape, it won't be visible
+            path.setFill(Color.BLACK);
+            
+            return path;
+        }
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/skin/CheckComboBoxSkin.java b/controlsfx/src/main/java/impl/org/controlsfx/skin/CheckComboBoxSkin.java
new file mode 100644
index 0000000..1e7d1f7
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/skin/CheckComboBoxSkin.java
@@ -0,0 +1,193 @@
+/**
+ * Copyright (c) 2013, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.skin;
+
+import java.util.Collections;
+
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.ListCell;
+import javafx.scene.control.ListView;
+import javafx.scene.control.cell.CheckBoxListCell;
+import javafx.util.Callback;
+
+import org.controlsfx.control.CheckComboBox;
+
+import com.sun.javafx.scene.control.ReadOnlyUnbackedObservableList;
+import com.sun.javafx.scene.control.behavior.BehaviorBase;
+import com.sun.javafx.scene.control.behavior.KeyBinding;
+import com.sun.javafx.scene.control.skin.BehaviorSkinBase;
+import com.sun.javafx.scene.control.skin.ComboBoxListViewSkin;
+
+public class CheckComboBoxSkin<T> extends BehaviorSkinBase<CheckComboBox<T>, BehaviorBase<CheckComboBox<T>>> {
+    
+    /**************************************************************************
+     * 
+     * Static fields
+     * 
+     **************************************************************************/
+
+    
+    
+    /**************************************************************************
+     * 
+     * fields
+     * 
+     **************************************************************************/
+    
+    // visuals
+    private final ComboBox<T> comboBox;
+    private final ListCell<T> buttonCell;
+    
+    // data
+    private final CheckComboBox<T> control;
+    private final ObservableList<T> items;
+    private final ReadOnlyUnbackedObservableList<Integer> selectedIndices;
+    private final ReadOnlyUnbackedObservableList<T> selectedItems;
+    
+    
+    /**************************************************************************
+     * 
+     * Constructors
+     * 
+     **************************************************************************/
+
+    @SuppressWarnings("unchecked")
+    public CheckComboBoxSkin(final CheckComboBox<T> control) {
+        super(control, new BehaviorBase<>(control, Collections.<KeyBinding> emptyList()));
+        
+        this.control = control;
+        this.items = control.getItems();
+        
+        selectedIndices = (ReadOnlyUnbackedObservableList<Integer>) control.getCheckModel().getCheckedIndices();
+        selectedItems = (ReadOnlyUnbackedObservableList<T>) control.getCheckModel().getCheckedItems();
+        
+        comboBox = new ComboBox<T>(items) {
+            @Override protected javafx.scene.control.Skin<?> createDefaultSkin() {
+                return new ComboBoxListViewSkin<T>(this) {
+                    // overridden to prevent the popup from disappearing
+                    @Override protected boolean isHideOnClickEnabled() {
+                        return false;
+                    }
+                };
+            }
+        };
+        comboBox.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
+        
+        // installs a custom CheckBoxListCell cell factory
+        comboBox.setCellFactory(new Callback<ListView<T>, ListCell<T>>() {
+            @Override public ListCell<T> call(ListView<T> listView) {
+                CheckBoxListCell<T> result = new CheckBoxListCell<>(item -> control.getItemBooleanProperty(item));
+                result.converterProperty().bind(control.converterProperty());
+                return result;
+            };
+        });
+        
+        // we render the selection into a custom button cell, so that it can 
+        // be pretty printed (e.g. 'Item 1, Item 2, Item 10').
+        buttonCell = new ListCell<T>() {
+            @Override protected void updateItem(T item, boolean empty) {
+                // we ignore whatever item is selected, instead choosing
+                // to display the selected item text using commas to separate
+                // each item
+                setText(buildString());
+            }
+        };
+        comboBox.setButtonCell(buttonCell);
+        comboBox.setValue((T)buildString());
+        
+        // The zero is a dummy value - it just has to be legally within the bounds of the
+        // item count for the CheckComboBox items list.
+        selectedIndices.addListener((ListChangeListener<Integer>) c -> buttonCell.updateIndex(0));
+        
+        getChildren().add(comboBox);
+    }
+    
+    
+    /**************************************************************************
+     * 
+     * Overriding public API
+     * 
+     **************************************************************************/
+    
+    @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return comboBox.minWidth(height);
+    }
+
+    @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return comboBox.minHeight(width);
+    }
+    
+    @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return comboBox.prefWidth(height);
+    }
+
+    @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return comboBox.prefHeight(width);
+    }
+    
+    @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return getSkinnable().prefWidth(height);
+    }
+
+    @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return getSkinnable().prefHeight(width);
+    }
+    
+    
+    
+    /**************************************************************************
+     * 
+     * Implementation
+     * 
+     **************************************************************************/
+    
+    private String buildString() {
+        final StringBuilder sb = new StringBuilder();
+        for (int i = 0, max = selectedItems.size(); i < max; i++) {
+            T item = selectedItems.get(i);
+            if (control.getConverter() == null) {
+                sb.append(item);
+            } else {
+                sb.append(control.getConverter().toString(item));
+            }
+            if (i < max - 1) {
+                sb.append(", "); //$NON-NLS-1$
+            }
+        }
+        return sb.toString();
+    }
+    
+    
+    /**************************************************************************
+     * 
+     * Support classes / enums
+     * 
+     **************************************************************************/
+    
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/skin/CustomTextFieldSkin.java b/controlsfx/src/main/java/impl/org/controlsfx/skin/CustomTextFieldSkin.java
new file mode 100644
index 0000000..00aa8f6
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/skin/CustomTextFieldSkin.java
@@ -0,0 +1,156 @@
+/**
+ * Copyright (c) 2013, 2016 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.skin;
+
+import javafx.beans.property.ObjectProperty;
+import javafx.css.PseudoClass;
+import javafx.geometry.Pos;
+import javafx.scene.Node;
+import javafx.scene.control.TextField;
+import javafx.scene.layout.StackPane;
+
+import com.sun.javafx.scene.control.behavior.TextFieldBehavior;
+import com.sun.javafx.scene.control.skin.TextFieldSkin;
+import com.sun.javafx.scene.text.HitInfo;
+
+public abstract class CustomTextFieldSkin extends TextFieldSkin {
+        
+    private static final PseudoClass HAS_NO_SIDE_NODE = PseudoClass.getPseudoClass("no-side-nodes"); //$NON-NLS-1$
+    private static final PseudoClass HAS_LEFT_NODE = PseudoClass.getPseudoClass("left-node-visible"); //$NON-NLS-1$
+    private static final PseudoClass HAS_RIGHT_NODE = PseudoClass.getPseudoClass("right-node-visible"); //$NON-NLS-1$
+    
+    private Node left;
+    private StackPane leftPane;
+    private Node right;
+    private StackPane rightPane;
+    
+    private final TextField control;
+    
+    public CustomTextFieldSkin(final TextField control) {
+        super(control, new TextFieldBehavior(control));
+        
+        this.control = control;
+        updateChildren();
+        
+        registerChangeListener(leftProperty(), "LEFT_NODE"); //$NON-NLS-1$
+        registerChangeListener(rightProperty(), "RIGHT_NODE"); //$NON-NLS-1$
+        registerChangeListener(control.focusedProperty(), "FOCUSED"); //$NON-NLS-1$
+    }
+    
+    public abstract ObjectProperty<Node> leftProperty();
+    public abstract ObjectProperty<Node> rightProperty();
+    
+    @Override protected void handleControlPropertyChanged(String p) {
+        super.handleControlPropertyChanged(p);
+        
+        if (p == "LEFT_NODE" || p == "RIGHT_NODE") { //$NON-NLS-1$ //$NON-NLS-2$
+            updateChildren();
+        }
+    }
+    
+    private void updateChildren() {
+        Node newLeft = leftProperty().get();
+        if (newLeft != null) {
+            getChildren().remove(leftPane);
+            leftPane = new StackPane(newLeft);
+            leftPane.setAlignment(Pos.CENTER_LEFT);
+            leftPane.getStyleClass().add("left-pane"); //$NON-NLS-1$
+            getChildren().add(leftPane);
+            left = newLeft;
+        }
+        
+        Node newRight = rightProperty().get();
+        if (newRight != null) {
+            getChildren().remove(rightPane);
+            rightPane = new StackPane(newRight);
+            rightPane.setAlignment(Pos.CENTER_RIGHT);
+            rightPane.getStyleClass().add("right-pane"); //$NON-NLS-1$
+            getChildren().add(rightPane);
+            right = newRight;
+        }
+        
+        control.pseudoClassStateChanged(HAS_LEFT_NODE, left != null);
+        control.pseudoClassStateChanged(HAS_RIGHT_NODE, right != null);
+        control.pseudoClassStateChanged(HAS_NO_SIDE_NODE, left == null && right == null);
+    }
+    
+    @Override protected void layoutChildren(double x, double y, double w, double h) {
+        final double fullHeight = h + snappedTopInset() + snappedBottomInset();
+        
+        final double leftWidth = leftPane == null ? 0.0 : snapSize(leftPane.prefWidth(fullHeight));
+        final double rightWidth = rightPane == null ? 0.0 : snapSize(rightPane.prefWidth(fullHeight));
+        
+        final double textFieldStartX = snapPosition(x) + snapSize(leftWidth);
+        final double textFieldWidth = w - snapSize(leftWidth) - snapSize(rightWidth);
+        
+        super.layoutChildren(textFieldStartX, 0, textFieldWidth, fullHeight);
+
+        if (leftPane != null) {
+            final double leftStartX = 0;
+            leftPane.resizeRelocate(leftStartX, 0, leftWidth, fullHeight);
+        }
+        
+        if (rightPane != null) {
+            final double rightStartX = rightPane == null ? 0.0 : w - rightWidth + snappedLeftInset();
+            rightPane.resizeRelocate(rightStartX, 0, rightWidth, fullHeight);
+        }
+    }
+    
+    @Override
+    public HitInfo getIndex(double x, double y) {
+        /**
+         * This resolves https://bitbucket.org/controlsfx/controlsfx/issue/476
+         * when we have a left Node and the click point is badly returned
+         * because we weren't considering the shift induced by the leftPane.
+         */
+        final double leftWidth = leftPane == null ? 0.0 : snapSize(leftPane.prefWidth(getSkinnable().getHeight()));
+        return super.getIndex(x - leftWidth, y);
+    }
+    
+    @Override
+    protected double computePrefWidth(double h, double topInset, double rightInset, double bottomInset, double leftInset) {
+        final double pw = super.computePrefWidth(h, topInset, rightInset, bottomInset, leftInset);
+        final double leftWidth = leftPane == null ? 0.0 : snapSize(leftPane.prefWidth(h));
+        final double rightWidth = rightPane == null ? 0.0 : snapSize(rightPane.prefWidth(h));
+    
+        return pw + leftWidth + rightWidth;
+    }
+    
+    @Override
+    protected double computePrefHeight(double w, double topInset, double rightInset, double bottomInset, double leftInset) {
+        final double ph = super.computePrefHeight(w, topInset, rightInset, bottomInset, leftInset);
+        final double leftHeight = leftPane == null ? 0.0 : snapSize(leftPane.prefHeight(-1));
+        final double rightHeight = rightPane == null ? 0.0 : snapSize(rightPane.prefHeight(-1));
+        
+        return Math.max(ph, Math.max(leftHeight, rightHeight));
+    }
+//    
+//    @Override
+//    protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+//        return computePrefWidth(height, topInset, rightInset, bottomInset, leftInset);
+//}
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/skin/DecorationPane.java b/controlsfx/src/main/java/impl/org/controlsfx/skin/DecorationPane.java
new file mode 100644
index 0000000..eb8cf87
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/skin/DecorationPane.java
@@ -0,0 +1,122 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.skin;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.WeakHashMap;
+
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.geometry.Pos;
+import javafx.scene.Node;
+import javafx.scene.layout.StackPane;
+
+import org.controlsfx.control.decoration.Decoration;
+import org.controlsfx.control.decoration.Decorator;
+
+public class DecorationPane extends StackPane {
+    
+    // maps from a node to a list of its decoration nodes
+    private final Map<Node, List<Node>> nodeDecorationMap = new WeakHashMap<>();
+    
+    ChangeListener<Boolean> visibilityListener = new ChangeListener<Boolean>() {
+        @Override public void changed(ObservableValue<? extends Boolean> o, Boolean wasVisible, Boolean isVisible) {
+            BooleanProperty p = (BooleanProperty)o;
+            Node n = (Node) p.getBean();
+            
+            removeAllDecorationsOnNode(n, Decorator.getDecorations(n));
+            Decorator.removeAllDecorations(n);
+        }
+    };
+
+    public DecorationPane() {
+        // Make DecorationPane transparent
+        setBackground(null);
+    }
+        
+    public void setRoot(Node root) {
+        getChildren().setAll(root);
+    }
+    
+    public void updateDecorationsOnNode(Node targetNode, List<Decoration> added, List<Decoration> removed) {
+        removeAllDecorationsOnNode(targetNode, removed);
+        addAllDecorationsOnNode(targetNode, added);
+    }
+
+    private void showDecoration(Node targetNode, Decoration decoration) {
+        Node decorationNode = decoration.applyDecoration(targetNode);
+        if (decorationNode != null) {
+            List<Node> decorationNodes = nodeDecorationMap.get(targetNode);
+            if (decorationNodes == null) {
+                decorationNodes = new ArrayList<>();
+                nodeDecorationMap.put(targetNode, decorationNodes);
+            }
+            decorationNodes.add(decorationNode);
+            
+            if (!getChildren().contains(decorationNode)) {
+                getChildren().add(decorationNode);
+                StackPane.setAlignment(decorationNode, Pos.TOP_LEFT); // TODO support for all positions.
+            }
+        }
+        
+        targetNode.visibleProperty().addListener(visibilityListener);
+    }
+
+    private void removeAllDecorationsOnNode(Node targetNode, List<Decoration> decorations) {
+        if (decorations == null || targetNode == null) return;
+        
+        // We need to do two things: 
+        // 1) Remove the decoration node (if it exists) from the nodeDecorationMap
+        //    for the targetNode, if it exists.
+        List<Node> decorationNodes = nodeDecorationMap.remove(targetNode);
+        if (decorationNodes != null) {
+            for (Node decorationNode : decorationNodes) {
+                boolean success = getChildren().remove(decorationNode);
+                if (! success) {
+                    throw new IllegalStateException("Could not remove decoration " +  //$NON-NLS-1$
+                            decorationNode + " from decoration pane children list: " +  //$NON-NLS-1$
+                            getChildren());
+                }
+            }
+        }
+        
+        // 2) Tell the decoration to remove itself from the target node (if necessary)
+        for (Decoration decoration : decorations) {
+            decoration.removeDecoration(targetNode);
+        }
+    }
+    
+    private void addAllDecorationsOnNode(Node targetNode, List<Decoration> decorations) {
+        if (decorations == null) return;
+        for (Decoration decoration : decorations) {
+            showDecoration(targetNode, decoration);
+        }
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/skin/ExpandableTableRowSkin.java b/controlsfx/src/main/java/impl/org/controlsfx/skin/ExpandableTableRowSkin.java
new file mode 100644
index 0000000..a8b5f83
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/skin/ExpandableTableRowSkin.java
@@ -0,0 +1,109 @@
+/**
+ * Copyright (c) 2016 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.skin;
+
+import com.sun.javafx.scene.control.skin.TableRowSkin;
+import javafx.scene.Node;
+import javafx.scene.control.TableRow;
+import org.controlsfx.control.table.TableRowExpanderColumn;
+
+/**
+ * This skin is installed when you assign a {@link org.controlsfx.control.table.TableRowExpanderColumn} to a TableView.
+ * The skin will render the expanded node produced by the
+ * {@link org.controlsfx.control.table.TableRowExpanderColumn#expandedNodeCallback} whenever the expanded state is
+ * changed to true for a certain row.
+ *
+ * @param <S> The type of items in the TableRow
+ */
+public class ExpandableTableRowSkin<S> extends TableRowSkin<S> {
+    private final TableRow<S> tableRow;
+    private TableRowExpanderColumn<S> expander;
+    private Double tableRowPrefHeight = -1D;
+
+    /**
+     * Create the ExpandableTableRowSkin and listen to changes for the item this table row represents. When the
+     * item is changed, the old expanded node, if any, is removed from the children list of the TableRow.
+     *
+     * @param tableRow The table row to apply this skin for
+     * @param expander The expander column, used to retrieve the expanded node when this row is expanded
+     */
+    public ExpandableTableRowSkin(TableRow<S> tableRow, TableRowExpanderColumn<S> expander) {
+        super(tableRow);
+        this.tableRow = tableRow;
+        this.expander = expander;
+        tableRow.itemProperty().addListener((observable, oldValue, newValue) -> {
+            if (oldValue != null) {
+                Node expandedNode = this.expander.getExpandedNode(oldValue);
+                if (expandedNode != null) getChildren().remove(expandedNode);
+            }
+        });
+    }
+
+    /**
+     * Create the expanded content node that should represent the current table row.
+     *
+     * If the expanded content node is not currently in the children list of the TableRow it is automatically added.
+     *
+     * @return The expanded content Node
+     */
+    private Node getContent() {
+        Node node = expander.getOrCreateExpandedNode(tableRow);
+        if (!getChildren().contains(node)) getChildren().add(node);
+        return node;
+    }
+
+    /**
+     * Check if the current node is expanded. This is done by checking that there is an item for the current row,
+     * and that the expanded property for the row is true.
+     *
+     * @return A boolean indicating the expanded state of this row
+     */
+    private Boolean isExpanded() {
+        return getSkinnable().getItem() != null && expander.getCellData(getSkinnable().getIndex());
+    }
+
+    /**
+     * Add the preferred height of the expanded Node whenever the expanded flag is true.
+     *
+     * @return The preferred height of the TableRow, appended with the preferred height of the expanded node
+     * if this row is currently expanded.
+     */
+    @Override
+    protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+        tableRowPrefHeight = super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
+        return isExpanded() ? tableRowPrefHeight + getContent().prefHeight(width) : tableRowPrefHeight;
+    }
+
+    /**
+     * Lay out the columns of the TableRow, then add the expanded content node below if this row is currently expanded.
+     */
+    @Override
+    protected void layoutChildren(double x, double y, double w, double h) {
+        super.layoutChildren(x, y, w, h);
+        if (isExpanded()) getContent().resizeRelocate(0.0, tableRowPrefHeight, w, h - tableRowPrefHeight);
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/skin/GridCellSkin.java b/controlsfx/src/main/java/impl/org/controlsfx/skin/GridCellSkin.java
new file mode 100644
index 0000000..db10ea7
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/skin/GridCellSkin.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2013, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.skin;
+
+import java.util.Collections;
+
+import org.controlsfx.control.GridCell;
+
+import com.sun.javafx.scene.control.behavior.BehaviorBase;
+import com.sun.javafx.scene.control.behavior.KeyBinding;
+import com.sun.javafx.scene.control.skin.CellSkinBase;
+
+public class GridCellSkin<T> extends CellSkinBase<GridCell<T>, BehaviorBase<GridCell<T>>> {
+
+    public GridCellSkin(GridCell<T> control) {
+        super(control, new BehaviorBase<>(control, Collections.<KeyBinding> emptyList()));
+    }
+
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/skin/GridRow.java b/controlsfx/src/main/java/impl/org/controlsfx/skin/GridRow.java
new file mode 100644
index 0000000..548ee8e
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/skin/GridRow.java
@@ -0,0 +1,104 @@
+/**
+ * Copyright (c) 2013, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.skin;
+
+import org.controlsfx.control.GridView;
+
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.scene.control.IndexedCell;
+import javafx.scene.control.Skin;
+
+/**
+ * A GridRow is a container for {@link GridCell}, and represents a single
+ * row inside a {@link GridView}.
+ */
+class GridRow<T> extends IndexedCell<T>{
+
+
+    /**************************************************************************
+     * 
+     * Constructors
+     * 
+     **************************************************************************/	
+
+    /**
+     * 
+     */
+    public GridRow() {
+        super();
+        getStyleClass().add("grid-row"); //$NON-NLS-1$
+
+        // we need to do this (or something similar) to allow for mouse wheel
+        // scrolling, as the GridRow has to report that it is non-empty (which
+        // is the second argument going into updateItem).
+        indexProperty().addListener(new InvalidationListener() {
+            @Override public void invalidated(Observable observable) {
+                updateItem(null, getIndex() == -1);
+            }
+        });
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override protected Skin<?> createDefaultSkin() {
+        return new GridRowSkin<>(this);
+    }
+
+
+
+    /**************************************************************************
+     * 
+     * Properties
+     * 
+     **************************************************************************/
+
+    /**
+     * The {@link GridView} that this GridRow exists within.
+     */
+    public SimpleObjectProperty<GridView<T>> gridViewProperty() {
+        return gridView;
+    }
+    private final SimpleObjectProperty<GridView<T>> gridView = 
+            new SimpleObjectProperty<>(this, "gridView"); //$NON-NLS-1$
+    
+    /**
+     * Sets the {@link GridView} that this GridRow exists within.
+     */
+    public final void updateGridView(GridView<T> gridView) {
+        this.gridView.set(gridView);
+    }
+    
+    /**
+     * Returns the {@link GridView} that this GridRow exists within.
+     */
+    public GridView<T> getGridView() {
+        return gridView.get();
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/skin/GridRowSkin.java b/controlsfx/src/main/java/impl/org/controlsfx/skin/GridRowSkin.java
new file mode 100644
index 0000000..2b03097
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/skin/GridRowSkin.java
@@ -0,0 +1,179 @@
+/**
+ * Copyright (c) 2013, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.skin;
+
+import java.util.Collections;
+
+import javafx.scene.Node;
+
+import org.controlsfx.control.GridCell;
+import org.controlsfx.control.GridView;
+
+import com.sun.javafx.scene.control.behavior.BehaviorBase;
+import com.sun.javafx.scene.control.behavior.KeyBinding;
+import com.sun.javafx.scene.control.skin.CellSkinBase;
+
+public class GridRowSkin<T> extends CellSkinBase<GridRow<T>, BehaviorBase<GridRow<T>>> {
+
+    public GridRowSkin(GridRow<T> control) {
+        super(control, new BehaviorBase<>(control, Collections.<KeyBinding> emptyList()));
+
+        // Remove any children before creating cells (by default a LabeledText exist and we don't need it)
+        getChildren().clear();
+        updateCells();
+        
+        registerChangeListener(getSkinnable().indexProperty(), "INDEX"); //$NON-NLS-1$
+        registerChangeListener(getSkinnable().widthProperty(), "WIDTH"); //$NON-NLS-1$
+        registerChangeListener(getSkinnable().heightProperty(), "HEIGHT"); //$NON-NLS-1$
+    }
+    
+    @Override protected void handleControlPropertyChanged(String p) {
+        super.handleControlPropertyChanged(p);
+        
+        if ("INDEX".equals(p)) { //$NON-NLS-1$
+            updateCells();
+        } else if ("WIDTH".equals(p)) { //$NON-NLS-1$
+            updateCells();
+        } else if ("HEIGHT".equals(p)) { //$NON-NLS-1$
+            updateCells();
+        }
+    }
+
+    /**
+     *  Returns a cell element at a desired index
+     *  @param index The index of the wanted cell element
+     *  @return Cell element if exist else null
+     */
+    @SuppressWarnings("unchecked")
+	public GridCell<T> getCellAtIndex(int index) {
+        if( index < getChildren().size() ) {
+            return (GridCell<T>)getChildren().get(index);
+        }
+        return null;
+    }
+        
+    /**
+     *  Update all cells
+     *  <p>Cells are only created when needed and re-used when possible.</p>
+     */
+    public void updateCells() {
+        int rowIndex = getSkinnable().getIndex();
+        if (rowIndex >= 0) {
+            GridView<T> gridView = getSkinnable().getGridView();
+            int maxCellsInRow = ((GridViewSkin<?>)gridView.getSkin()).computeMaxCellsInRow();
+            int totalCellsInGrid = gridView.getItems().size();
+            int startCellIndex = rowIndex * maxCellsInRow;
+            int endCellIndex = startCellIndex + maxCellsInRow - 1;
+            int cacheIndex = 0;
+
+            for (int cellIndex = startCellIndex; cellIndex <= endCellIndex; cellIndex++, cacheIndex++) {
+                if (cellIndex < totalCellsInGrid) {
+                    // Check if we can re-use a cell at this index or create a new one
+                    GridCell<T> cell = getCellAtIndex(cacheIndex);
+                    if( cell == null ) {
+                        cell = createCell();
+                        getChildren().add(cell);
+                    }
+                    cell.updateIndex(-1);
+                    cell.updateIndex(cellIndex);
+                }
+                // we are going out of bounds -> exist the loop
+                else { break; }
+            }
+            
+            // In case we are re-using a row that previously had more cells than
+            // this one, we need to remove the extra cells that remain
+            getChildren().remove(cacheIndex, getChildren().size());
+        }
+    }
+
+    private GridCell<T> createCell() {
+        GridView<T> gridView = getSkinnable().gridViewProperty().get();
+        GridCell<T> cell;
+        if (gridView.getCellFactory() != null) {
+            cell = gridView.getCellFactory().call(gridView);
+        } else {
+            cell = createDefaultCellImpl();
+        }
+        cell.updateGridView(gridView);
+        return cell;
+    }
+
+    private GridCell<T> createDefaultCellImpl() {
+        return new GridCell<T>() {
+            @Override protected void updateItem(T item, boolean empty) {
+                super.updateItem(item, empty);
+                if(empty) {
+                    setText(""); //$NON-NLS-1$
+                } else {
+                    setText(item.toString());
+                }
+            }
+        };
+    }
+    
+    @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
+    }
+
+    @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return Double.MAX_VALUE;
+    }
+
+    @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+        GridView<T> gv = getSkinnable().gridViewProperty().get();
+        return gv.getCellHeight() + gv.getVerticalCellSpacing() * 2;
+    }
+
+    @Override protected void layoutChildren(double x, double y, double w, double h) {
+//        double currentWidth = getSkinnable().getWidth();
+        double cellWidth = getSkinnable().gridViewProperty().get().getCellWidth();
+        double cellHeight = getSkinnable().gridViewProperty().get().getCellHeight();
+        double horizontalCellSpacing = getSkinnable().gridViewProperty().get().getHorizontalCellSpacing();
+        double verticalCellSpacing = getSkinnable().gridViewProperty().get().getVerticalCellSpacing();
+
+        double xPos = 0;
+        double yPos = 0;
+
+        // This has been commented out as I removed the API from GridView until
+        // a use case was created.
+//        HPos currentHorizontalAlignment = getSkinnable().gridViewProperty().get().getHorizontalAlignment();
+//        if (currentHorizontalAlignment != null) {
+//            if (currentHorizontalAlignment.equals(HPos.CENTER)) {
+//                xPos = (currentWidth % computeCellWidth()) / 2;
+//            } else if (currentHorizontalAlignment.equals(HPos.RIGHT)) {
+//                xPos = currentWidth % computeCellWidth();
+//            }
+//        }
+
+        for (Node child : getChildren()) {
+            child.relocate(xPos + horizontalCellSpacing, yPos + verticalCellSpacing);
+            child.resize(cellWidth, cellHeight);
+            xPos = xPos + horizontalCellSpacing + cellWidth + horizontalCellSpacing;
+        }
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/skin/GridViewSkin.java b/controlsfx/src/main/java/impl/org/controlsfx/skin/GridViewSkin.java
new file mode 100644
index 0000000..b21a24f
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/skin/GridViewSkin.java
@@ -0,0 +1,225 @@
+/**
+ * Copyright (c) 2013, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.skin;
+
+import java.util.Collections;
+
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import javafx.collections.WeakListChangeListener;
+import javafx.util.Callback;
+
+import org.controlsfx.control.GridView;
+
+import com.sun.javafx.scene.control.behavior.BehaviorBase;
+import com.sun.javafx.scene.control.behavior.KeyBinding;
+import com.sun.javafx.scene.control.skin.VirtualContainerBase;
+import com.sun.javafx.scene.control.skin.VirtualFlow;
+
+public class GridViewSkin<T> extends VirtualContainerBase<GridView<T>, BehaviorBase<GridView<T>>, GridRow<T>> {
+    
+    private final ListChangeListener<T> gridViewItemsListener = new ListChangeListener<T>() {
+        @Override public void onChanged(ListChangeListener.Change<? extends T> change) {
+            updateRowCount();
+            getSkinnable().requestLayout();
+        }
+    };
+
+    private final WeakListChangeListener<T> weakGridViewItemsListener = new WeakListChangeListener<>(gridViewItemsListener);
+
+    @SuppressWarnings("rawtypes")
+    public GridViewSkin(GridView<T> control) {
+        super(control, new BehaviorBase<>(control, Collections.<KeyBinding>emptyList()));
+        
+        updateGridViewItems();
+
+        flow.setId("virtual-flow"); //$NON-NLS-1$
+        flow.setPannable(false);
+        flow.setVertical(true);
+        flow.setFocusTraversable(getSkinnable().isFocusTraversable());
+        flow.setCreateCell(new Callback<VirtualFlow, GridRow<T>>() {
+            @Override public GridRow<T> call(VirtualFlow flow) {
+                return GridViewSkin.this.createCell();
+            }
+        });
+        getChildren().add(flow);
+
+        updateRowCount();
+
+        // Register listeners
+        registerChangeListener(control.itemsProperty(), "ITEMS"); //$NON-NLS-1$
+        registerChangeListener(control.cellFactoryProperty(), "CELL_FACTORY"); //$NON-NLS-1$
+        registerChangeListener(control.parentProperty(), "PARENT"); //$NON-NLS-1$
+        registerChangeListener(control.cellHeightProperty(), "CELL_HEIGHT"); //$NON-NLS-1$
+        registerChangeListener(control.cellWidthProperty(), "CELL_WIDTH"); //$NON-NLS-1$
+        registerChangeListener(control.horizontalCellSpacingProperty(), "HORIZONZAL_CELL_SPACING"); //$NON-NLS-1$
+        registerChangeListener(control.verticalCellSpacingProperty(), "VERTICAL_CELL_SPACING"); //$NON-NLS-1$
+        registerChangeListener(control.widthProperty(), "WIDTH_PROPERTY"); //$NON-NLS-1$
+        registerChangeListener(control.heightProperty(), "HEIGHT_PROPERTY"); //$NON-NLS-1$
+    }
+
+    @Override protected void handleControlPropertyChanged(String p) {
+        super.handleControlPropertyChanged(p);
+        if (p == "ITEMS") { //$NON-NLS-1$
+            updateGridViewItems();
+        } else if (p == "CELL_FACTORY") { //$NON-NLS-1$
+            flow.recreateCells();
+        } else if (p == "CELL_HEIGHT") { //$NON-NLS-1$
+            flow.recreateCells();
+        } else if (p == "CELL_WIDTH") { //$NON-NLS-1$
+            updateRowCount();
+            flow.recreateCells();
+        } else if (p == "HORIZONZAL_CELL_SPACING") { //$NON-NLS-1$
+            updateRowCount();
+            flow.recreateCells();
+        } else if (p == "VERTICAL_CELL_SPACING") { //$NON-NLS-1$
+            flow.recreateCells();
+        } else if (p == "PARENT") { //$NON-NLS-1$
+            if (getSkinnable().getParent() != null && getSkinnable().isVisible()) {
+                getSkinnable().requestLayout();
+            }
+        } else if (p == "WIDTH_PROPERTY" || p == "HEIGHT_PROPERTY") { //$NON-NLS-1$ //$NON-NLS-2$
+            updateRowCount();
+        }
+    }
+
+    public void updateGridViewItems() {
+        if (getSkinnable().getItems() != null) {
+            getSkinnable().getItems().removeListener(weakGridViewItemsListener);
+        }
+
+        if (getSkinnable().getItems() != null) {
+            getSkinnable().getItems().addListener(weakGridViewItemsListener);
+        }
+
+        updateRowCount();
+        flow.recreateCells();
+        getSkinnable().requestLayout();
+    }
+
+    @Override protected void updateRowCount() {
+        if (flow == null)
+            return;
+
+        int oldCount = flow.getCellCount();
+        int newCount = getItemCount();
+        
+        if (newCount != oldCount) {
+            flow.setCellCount(newCount);
+            flow.rebuildCells();
+        } else {
+            flow.reconfigureCells();
+        }
+        updateRows(newCount);
+    }
+
+    @Override protected void layoutChildren(double x, double y, double w, double h) {
+        double x1 = getSkinnable().getInsets().getLeft();
+        double y1 = getSkinnable().getInsets().getTop();
+        double w1 = getSkinnable().getWidth() - (getSkinnable().getInsets().getLeft() + getSkinnable().getInsets().getRight());
+        double h1 = getSkinnable().getHeight() - (getSkinnable().getInsets().getTop() + getSkinnable().getInsets().getBottom());
+
+        flow.resizeRelocate(x1, y1, w1, h1);
+    }
+
+    @Override public GridRow<T> createCell() {
+        GridRow<T> row = new GridRow<>();
+        row.updateGridView(getSkinnable());
+        return row;
+    }
+
+    /**
+     *  Returns the number of row needed to display the whole set of cells
+     *  @return GridView row count
+     */
+    @Override public int getItemCount() {
+        final ObservableList<?> items = getSkinnable().getItems();
+        // Fix for #98 : int division should be cast to get the result as
+        // double and ceiled to get the max int of it (as we are looking for
+        // the max number of necessary row)
+        return items == null ? 0 : (int)Math.ceil((double)items.size() / computeMaxCellsInRow());
+    }
+
+    /**
+     *  Returns the max number of cell per row
+     *  @return Max cell number per row 
+     */
+    public int computeMaxCellsInRow() {
+        return Math.max((int) Math.floor(computeRowWidth() / computeCellWidth()), 1);
+    }
+
+    /**
+     *  Returns the width of a row
+     *  (should be GridView.width - GridView.Scrollbar.width)
+     *  @return Computed width of a row 
+     */
+    protected double computeRowWidth() {
+        // Fix for #98 : width calculation should take the scrollbar size
+        // into account
+        
+        // TODO: need to figure out how to get the real scrollbar width and
+        // replace the 18 value
+        return getSkinnable().getWidth() - 18;
+    }
+
+    /**
+     *  Returns the width of a cell
+     *  @return Computed width of a cell 
+     */
+    protected double computeCellWidth() {
+        return getSkinnable().cellWidthProperty().doubleValue() + (getSkinnable().horizontalCellSpacingProperty().doubleValue() * 2);
+    }
+
+    protected void updateRows(int rowCount) {
+        for (int i = 0; i < rowCount; i++) {
+            GridRow<T> row = flow.getVisibleCell(i);
+            if (row != null) {
+                // FIXME hacky - need to better understand what this is about
+                row.updateIndex(-1);
+                row.updateIndex(i);
+            }
+        }
+    }
+
+    protected boolean areRowsVisible() {
+        if (flow == null)
+            return false;
+
+        if (flow.getFirstVisibleCell() == null)
+            return false;
+
+        if (flow.getLastVisibleCell() == null)
+            return false;
+
+        return true;
+    }
+    
+    @Override protected double computeMinHeight(double height, double topInset, double rightInset, double bottomInset,
+            double leftInset) {
+        return 0;
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/skin/HiddenSidesPaneSkin.java b/controlsfx/src/main/java/impl/org/controlsfx/skin/HiddenSidesPaneSkin.java
new file mode 100644
index 0000000..bd040bd
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/skin/HiddenSidesPaneSkin.java
@@ -0,0 +1,336 @@
+/**
+ * Copyright (c) 2013, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.skin;
+
+import javafx.animation.Animation.Status;
+import javafx.animation.KeyFrame;
+import javafx.animation.KeyValue;
+import javafx.animation.Timeline;
+import javafx.beans.InvalidationListener;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.event.EventHandler;
+import javafx.geometry.Side;
+import javafx.scene.Node;
+import javafx.scene.control.SkinBase;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.StackPane;
+import javafx.scene.shape.Rectangle;
+import javafx.util.Duration;
+
+import org.controlsfx.control.HiddenSidesPane;
+
+public class HiddenSidesPaneSkin extends SkinBase<HiddenSidesPane> {
+
+    private final StackPane stackPane;
+    private final EventHandler<MouseEvent> exitedHandler;
+    private boolean mousePressed;
+
+    public HiddenSidesPaneSkin(HiddenSidesPane pane) {
+        super(pane);
+
+        exitedHandler = event -> {
+            if (isMouseEnabled() && getSkinnable().getPinnedSide() == null
+                    && !mousePressed) {
+                hide();
+            }
+        };
+
+        stackPane = new StackPane();
+        getChildren().add(stackPane);
+        updateStackPane();
+
+        InvalidationListener rebuildListener = observable -> updateStackPane();
+        pane.contentProperty().addListener(rebuildListener);
+        pane.topProperty().addListener(rebuildListener);
+        pane.rightProperty().addListener(rebuildListener);
+        pane.bottomProperty().addListener(rebuildListener);
+        pane.leftProperty().addListener(rebuildListener);
+
+        pane.addEventFilter(MouseEvent.MOUSE_MOVED, event -> {
+            if (isMouseEnabled() && getSkinnable().getPinnedSide() == null) {
+                Side side = getSide(event);
+                if (side != null) {
+                    show(side);
+                } else if (isMouseMovedOutsideSides(event)) {
+                    hide();
+                }
+            }
+        });
+
+        pane.addEventFilter(MouseEvent.MOUSE_EXITED, exitedHandler);
+
+        pane.addEventFilter(MouseEvent.MOUSE_PRESSED,
+                event -> mousePressed = true);
+
+        pane.addEventFilter(MouseEvent.MOUSE_RELEASED, event -> {
+            mousePressed = false;
+
+            if (isMouseEnabled() && getSkinnable().getPinnedSide() == null) {
+                Side side = getSide(event);
+                if (side != null) {
+                    show(side);
+                } else {
+                    hide();
+                }
+            }
+        });
+
+        for (Side side : Side.values()) {
+            visibility[side.ordinal()] = new SimpleDoubleProperty(0);
+            visibility[side.ordinal()].addListener(observable -> getSkinnable()
+                    .requestLayout());
+        }
+
+        Side pinnedSide = getSkinnable().getPinnedSide();
+        if (pinnedSide != null) {
+        	show(pinnedSide);
+        }
+
+        pane.pinnedSideProperty().addListener(
+                observable -> show(getSkinnable().getPinnedSide()));
+
+        Rectangle clip = new Rectangle();
+        clip.setX(0);
+        clip.setY(0);
+        clip.widthProperty().bind(getSkinnable().widthProperty());
+        clip.heightProperty().bind(getSkinnable().heightProperty());
+
+        getSkinnable().setClip(clip);
+    }
+
+    private boolean isMouseMovedOutsideSides(MouseEvent event) {
+        if (getSkinnable().getLeft() != null
+                && getSkinnable().getLeft().getBoundsInParent()
+                        .contains(event.getX(), event.getY())) {
+            return false;
+        }
+
+        if (getSkinnable().getTop() != null
+                && getSkinnable().getTop().getBoundsInParent()
+                        .contains(event.getX(), event.getY())) {
+            return false;
+        }
+
+        if (getSkinnable().getRight() != null
+                && getSkinnable().getRight().getBoundsInParent()
+                        .contains(event.getX(), event.getY())) {
+            return false;
+        }
+
+        if (getSkinnable().getBottom() != null
+                && getSkinnable().getBottom().getBoundsInParent()
+                        .contains(event.getX(), event.getY())) {
+            return false;
+        }
+
+        return true;
+    }
+
+    private boolean isMouseEnabled() {
+        return getSkinnable().getTriggerDistance() > 0;
+    }
+
+    private Side getSide(MouseEvent evt) {
+        if (stackPane.getBoundsInLocal().contains(evt.getX(), evt.getY())) {
+            double trigger = getSkinnable().getTriggerDistance();
+            if (evt.getX() <= trigger) {
+                return Side.LEFT;
+            } else if (evt.getX() > getSkinnable().getWidth() - trigger) {
+                return Side.RIGHT;
+            } else if (evt.getY() <= trigger) {
+                return Side.TOP;
+            } else if (evt.getY() > getSkinnable().getHeight() - trigger) {
+                return Side.BOTTOM;
+            }
+        }
+
+        return null;
+    }
+
+    private DoubleProperty[] visibility = new SimpleDoubleProperty[Side
+            .values().length];
+
+    private Timeline showTimeline;
+
+    private void show(Side side) {
+        if (hideTimeline != null) {
+            hideTimeline.stop();
+        }
+
+        if (showTimeline != null && showTimeline.getStatus() == Status.RUNNING) {
+            return;
+        }
+
+        KeyValue[] keyValues = new KeyValue[Side.values().length];
+        for (Side s : Side.values()) {
+            keyValues[s.ordinal()] = new KeyValue(visibility[s.ordinal()],
+                    s.equals(side) ? 1 : 0);
+        }
+
+        Duration delay = getSkinnable().getAnimationDelay() != null ? getSkinnable()
+                .getAnimationDelay() : Duration.millis(300);
+        Duration duration = getSkinnable().getAnimationDuration() != null ? getSkinnable()
+                .getAnimationDuration() : Duration.millis(200);
+
+        KeyFrame keyFrame = new KeyFrame(duration, keyValues);
+        showTimeline = new Timeline(keyFrame);
+        showTimeline.setDelay(delay);
+        showTimeline.play();
+    }
+
+    private Timeline hideTimeline;
+
+    private void hide() {
+        if (showTimeline != null) {
+            showTimeline.stop();
+        }
+
+        if (hideTimeline != null && hideTimeline.getStatus() == Status.RUNNING) {
+            return;
+        }
+
+        boolean sideVisible = false;
+        for (Side side : Side.values()) {
+            if (visibility[side.ordinal()].get() > 0) {
+                sideVisible = true;
+                break;
+            }
+        }
+
+        // nothing to do here
+        if (!sideVisible) {
+            return;
+        }
+
+        KeyValue[] keyValues = new KeyValue[Side.values().length];
+        for (Side side : Side.values()) {
+            keyValues[side.ordinal()] = new KeyValue(
+                    visibility[side.ordinal()], 0);
+        }
+
+        Duration delay = getSkinnable().getAnimationDelay() != null ? getSkinnable()
+                .getAnimationDelay() : Duration.millis(300);
+        Duration duration = getSkinnable().getAnimationDuration() != null ? getSkinnable()
+                .getAnimationDuration() : Duration.millis(200);
+
+        KeyFrame keyFrame = new KeyFrame(duration, keyValues);
+        hideTimeline = new Timeline(keyFrame);
+        hideTimeline.setDelay(delay);
+        hideTimeline.play();
+    }
+
+    private void updateStackPane() {
+        stackPane.getChildren().clear();
+
+        if (getSkinnable().getContent() != null) {
+            stackPane.getChildren().add(getSkinnable().getContent());
+        }
+        if (getSkinnable().getTop() != null) {
+            stackPane.getChildren().add(getSkinnable().getTop());
+            getSkinnable().getTop().setManaged(false);
+            getSkinnable().getTop().removeEventFilter(MouseEvent.MOUSE_EXITED,
+                    exitedHandler);
+            getSkinnable().getTop().addEventFilter(MouseEvent.MOUSE_EXITED,
+                    exitedHandler);
+        }
+        if (getSkinnable().getRight() != null) {
+            stackPane.getChildren().add(getSkinnable().getRight());
+            getSkinnable().getRight().setManaged(false);
+            getSkinnable().getRight().removeEventFilter(
+                    MouseEvent.MOUSE_EXITED, exitedHandler);
+            getSkinnable().getRight().addEventFilter(MouseEvent.MOUSE_EXITED,
+                    exitedHandler);
+        }
+        if (getSkinnable().getBottom() != null) {
+            stackPane.getChildren().add(getSkinnable().getBottom());
+            getSkinnable().getBottom().setManaged(false);
+            getSkinnable().getBottom().removeEventFilter(
+                    MouseEvent.MOUSE_EXITED, exitedHandler);
+            getSkinnable().getBottom().addEventFilter(MouseEvent.MOUSE_EXITED,
+                    exitedHandler);
+        }
+        if (getSkinnable().getLeft() != null) {
+            stackPane.getChildren().add(getSkinnable().getLeft());
+            getSkinnable().getLeft().setManaged(false);
+            getSkinnable().getLeft().removeEventFilter(MouseEvent.MOUSE_EXITED,
+                    exitedHandler);
+            getSkinnable().getLeft().addEventFilter(MouseEvent.MOUSE_EXITED,
+                    exitedHandler);
+        }
+    }
+
+    @Override
+    protected void layoutChildren(double contentX, double contentY,
+            double contentWidth, double contentHeight) {
+
+        /*
+         * Layout the stackpane in a normal way (equals
+         * "lay out the content node", the only managed node)
+         */
+        super.layoutChildren(contentX, contentY, contentWidth, contentHeight);
+
+        // layout the unmanaged side nodes
+
+        Node bottom = getSkinnable().getBottom();
+        if (bottom != null) {
+            double prefHeight = bottom.prefHeight(-1);
+            double offset = prefHeight
+                    * visibility[Side.BOTTOM.ordinal()].get();
+            bottom.resizeRelocate(contentX, contentY + contentHeight - offset,
+                    contentWidth, prefHeight);
+            bottom.setVisible(visibility[Side.BOTTOM.ordinal()].get() > 0);
+        }
+
+        Node left = getSkinnable().getLeft();
+        if (left != null) {
+            double prefWidth = left.prefWidth(-1);
+            double offset = prefWidth * visibility[Side.LEFT.ordinal()].get();
+            left.resizeRelocate(contentX - (prefWidth - offset), contentY,
+                    prefWidth, contentHeight);
+            left.setVisible(visibility[Side.LEFT.ordinal()].get() > 0);
+        }
+
+        Node right = getSkinnable().getRight();
+        if (right != null) {
+            double prefWidth = right.prefWidth(-1);
+            double offset = prefWidth * visibility[Side.RIGHT.ordinal()].get();
+            right.resizeRelocate(contentX + contentWidth - offset, contentY,
+                    prefWidth, contentHeight);
+            right.setVisible(visibility[Side.RIGHT.ordinal()].get() > 0);
+        }
+
+        Node top = getSkinnable().getTop();
+        if (top != null) {
+            double prefHeight = top.prefHeight(-1);
+            double offset = prefHeight * visibility[Side.TOP.ordinal()].get();
+            top.resizeRelocate(contentX, contentY - (prefHeight - offset),
+                    contentWidth, prefHeight);
+            top.setVisible(visibility[Side.TOP.ordinal()].get() > 0);
+        }
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/skin/HyperlinkLabelSkin.java b/controlsfx/src/main/java/impl/org/controlsfx/skin/HyperlinkLabelSkin.java
new file mode 100644
index 0000000..10e6681
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/skin/HyperlinkLabelSkin.java
@@ -0,0 +1,156 @@
+/**
+ * Copyright (c) 2013, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.skin;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.geometry.Insets;
+import javafx.scene.Node;
+import javafx.scene.control.Hyperlink;
+import javafx.scene.control.Label;
+import javafx.scene.text.Text;
+import javafx.scene.text.TextFlow;
+
+import org.controlsfx.control.HyperlinkLabel;
+
+import com.sun.javafx.scene.control.behavior.BehaviorBase;
+import com.sun.javafx.scene.control.behavior.KeyBinding;
+import com.sun.javafx.scene.control.skin.BehaviorSkinBase;
+
+public class HyperlinkLabelSkin extends BehaviorSkinBase<HyperlinkLabel, BehaviorBase<HyperlinkLabel>> {
+    
+    /***************************************************************************
+     * 
+     * Static fields
+     * 
+     **************************************************************************/
+    
+    // The strings used to delimit the hyperlinks
+    private static final String HYPERLINK_START = "["; //$NON-NLS-1$
+    private static final String HYPERLINK_END = "]"; //$NON-NLS-1$
+    
+    
+    
+    /***************************************************************************
+     * 
+     * Private fields
+     * 
+     **************************************************************************/
+    
+    private final TextFlow textFlow;
+    private final EventHandler<ActionEvent> eventHandler = new EventHandler<ActionEvent>() {
+        @Override public void handle(final ActionEvent event) {
+            EventHandler<ActionEvent> onActionHandler = getSkinnable().getOnAction();
+            if (onActionHandler != null) {
+                onActionHandler.handle(event);
+            }
+        }
+    };
+    
+    
+
+    /***************************************************************************
+     * 
+     * Constructors
+     * 
+     **************************************************************************/
+    
+    public HyperlinkLabelSkin(HyperlinkLabel control) {
+        super(control, new BehaviorBase<>(control, Collections.<KeyBinding> emptyList()));
+        
+        this.textFlow = new TextFlow();
+        getChildren().add(textFlow);
+        updateText();
+        
+        registerChangeListener(control.textProperty(), "TEXT"); //$NON-NLS-1$
+    }
+
+    
+    
+    /***************************************************************************
+     * 
+     * Implementation
+     * 
+     **************************************************************************/
+    
+    @Override protected void handleControlPropertyChanged(String p) {
+        super.handleControlPropertyChanged(p);
+        
+        if (p == "TEXT") { //$NON-NLS-1$
+            updateText();
+        }
+    }
+    
+    // splits up the string into Text and Hyperlink nodes, and places them
+    // into a TextFlow instance
+    private void updateText() {
+        final String text = getSkinnable().getText();
+        
+        if (text == null || text.isEmpty()) {
+            textFlow.getChildren().clear();
+            return;
+        }
+        
+        // parse the text and put it into an array list
+        final List<Node> nodes = new ArrayList<>();
+        
+        int start = 0;
+        final int textLength = text.length();
+        while (start != -1 && start < textLength) {
+            int startPos = text.indexOf(HYPERLINK_START, start);
+            int endPos = text.indexOf(HYPERLINK_END, startPos);
+            
+            // if the startPos is -1, there are no more hyperlinks...
+            if (startPos == -1 || endPos == -1) {
+                if (textLength > start) {
+                    // ...but there is still text to turn into one last label
+                    Label label = new Label(text.substring(start));
+                    nodes.add(label);
+                    break;
+                }
+            }
+            
+            // firstly, create a label from start to startPos
+            Text label = new Text(text.substring(start, startPos));
+            nodes.add(label);
+            
+            // if endPos is greater than startPos, create a hyperlink
+            Hyperlink hyperlink = new Hyperlink(text.substring(startPos + 1, endPos));
+            hyperlink.setPadding(new Insets(0, 0, 0, 0));
+            hyperlink.setOnAction(eventHandler);
+            nodes.add(hyperlink);
+            
+            start = endPos + 1;
+        }
+        
+        textFlow.getChildren().setAll(nodes);
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/skin/InfoOverlaySkin.java b/controlsfx/src/main/java/impl/org/controlsfx/skin/InfoOverlaySkin.java
new file mode 100644
index 0000000..71bc6ad
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/skin/InfoOverlaySkin.java
@@ -0,0 +1,248 @@
+/**
+ * Copyright (c) 2014, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package impl.org.controlsfx.skin;
+
+import java.util.Collections;
+
+import javafx.animation.Animation.Status;
+import javafx.animation.Interpolator;
+import javafx.animation.KeyFrame;
+import javafx.animation.KeyValue;
+import javafx.animation.Timeline;
+import javafx.beans.binding.Bindings;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.event.EventHandler;
+import javafx.geometry.HPos;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.geometry.VPos;
+import javafx.scene.Cursor;
+import javafx.scene.Node;
+import javafx.scene.control.Label;
+import javafx.scene.control.ToggleButton;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.HBox;
+import javafx.util.Duration;
+
+import org.controlsfx.control.InfoOverlay;
+
+import com.sun.javafx.scene.control.behavior.BehaviorBase;
+import com.sun.javafx.scene.control.behavior.KeyBinding;
+import com.sun.javafx.scene.control.skin.BehaviorSkinBase;
+
+public class InfoOverlaySkin extends BehaviorSkinBase<InfoOverlay, BehaviorBase<InfoOverlay>> {
+    
+    private final ImageView EXPAND_IMAGE = new ImageView(new Image(InfoOverlay.class.getResource("expand.png").toExternalForm())); //$NON-NLS-1$
+    private final ImageView COLLAPSE_IMAGE = new ImageView(new Image(InfoOverlay.class.getResource("collapse.png").toExternalForm())); //$NON-NLS-1$
+    
+    private static final Duration TRANSITION_DURATION = new Duration(350.0);
+
+    private Node content;
+    private Label infoLabel;
+    private HBox infoPanel;
+    private ToggleButton expandCollapseButton;
+
+    // animation support
+    private Timeline timeline;
+    private DoubleProperty transition = new SimpleDoubleProperty(this, "transition", 0.0) { //$NON-NLS-1$
+        @Override protected void invalidated() {
+            getSkinnable().requestLayout();
+        }
+    };
+
+    public InfoOverlaySkin(final InfoOverlay control) {
+        super(control, new BehaviorBase<>(control, Collections.<KeyBinding> emptyList()));
+
+        // content
+        content = control.getContent();
+        control.hoverProperty().addListener(new ChangeListener<Boolean>() {
+            @Override public void changed(ObservableValue<? extends Boolean> o, Boolean wasHover, Boolean isHover) {
+                if (control.isShowOnHover()) {
+                    if ((isHover && ! isExpanded()) || (!isHover && isExpanded())) {
+                        doToggle();
+                    }
+                }
+            }
+        });
+
+        // text
+        infoLabel = new Label();
+        infoLabel.setWrapText(true);
+        infoLabel.setAlignment(Pos.TOP_LEFT);
+        infoLabel.getStyleClass().add("info"); //$NON-NLS-1$
+        infoLabel.textProperty().bind(control.textProperty());
+
+        // button to expand / collapse the info overlay
+        expandCollapseButton = new ToggleButton();
+        expandCollapseButton.setMouseTransparent(true);
+        expandCollapseButton.visibleProperty().bind(Bindings.not(control.showOnHoverProperty()));
+        expandCollapseButton.managedProperty().bind(Bindings.not(control.showOnHoverProperty()));
+        updateToggleButton();
+
+        // container for the info overlay and the button
+        infoPanel = new HBox(infoLabel, expandCollapseButton);
+        infoPanel.setAlignment(Pos.TOP_LEFT);
+        infoPanel.setFillHeight(true);
+        infoPanel.getStyleClass().add("info-panel"); //$NON-NLS-1$
+        infoPanel.setCursor(Cursor.HAND);
+        infoPanel.setOnMouseClicked(new EventHandler<MouseEvent>() {
+            @Override public void handle(MouseEvent e) {
+                if (! control.isShowOnHover()) {
+                    doToggle();
+                }
+            }
+        });
+
+        // adding everything to the scenegraph
+        getChildren().addAll(content, infoPanel);
+        
+        registerChangeListener(control.contentProperty(), "CONTENT"); //$NON-NLS-1$
+    }
+    
+    @Override protected void handleControlPropertyChanged(String p) {
+        super.handleControlPropertyChanged(p);
+        
+        if ("CONTENT".equals(p)) { //$NON-NLS-1$
+            getChildren().remove(0);
+            getChildren().add(0, getSkinnable().getContent());
+            getSkinnable().requestLayout();
+        }
+    }
+    
+    private void doToggle() {
+        // do animation to show / hide the info panel
+        expandCollapseButton.setSelected(!expandCollapseButton.isSelected());
+        toggleInfoPanel();
+        updateToggleButton();
+    }
+    
+    private boolean isExpanded() {
+        return expandCollapseButton.isSelected();
+    }
+
+    @Override
+    protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) {
+        final double contentPrefHeight = content.prefHeight(contentWidth);
+        
+        // we calculate the pref width of the expand/collapse button. We will
+        // ensure that the button does not get smaller than this.
+        final double toggleButtonPrefWidth = expandCollapseButton.prefWidth(-1);
+        expandCollapseButton.setMinWidth(toggleButtonPrefWidth);
+
+        // All remaining width goes to the info label
+        final Insets infoPanelPadding = infoPanel.getPadding();
+        final double infoLabelWidth = snapSize(contentWidth - toggleButtonPrefWidth - 
+                infoPanelPadding.getLeft() - infoPanelPadding.getRight());
+
+        // we then can work out the necessary height for the info panel, based on
+        // whether it is expanded or not, and given the current state of the animation.
+        final double prefInfoPanelHeight = (snapSize(infoLabel.prefHeight(infoLabelWidth)) +
+                snapSpace(infoPanel.getPadding().getTop()) +
+                snapSpace(infoPanel.getPadding().getBottom())) *
+                transition.get();
+
+        infoLabel.setMaxWidth(infoLabelWidth);
+        infoLabel.setMaxHeight(prefInfoPanelHeight);
+
+        // position the imageView
+        layoutInArea(content, contentX, contentY,
+                                contentWidth, contentHeight, -1, HPos.CENTER, VPos.TOP);
+        
+        // position the infoPanel (the HBox consisting of the Label and ToggleButton)
+        layoutInArea(infoPanel, contentX, snapPosition(contentPrefHeight - prefInfoPanelHeight),
+                                contentWidth, prefInfoPanelHeight, 0, HPos.CENTER, VPos.BOTTOM);
+    }
+
+    private void updateToggleButton() {
+        if (expandCollapseButton.isSelected()) {
+            expandCollapseButton.getStyleClass().setAll("collapse-button"); //$NON-NLS-1$
+            expandCollapseButton.setGraphic(COLLAPSE_IMAGE);
+        } else {
+            expandCollapseButton.getStyleClass().setAll("expand-button"); //$NON-NLS-1$
+            expandCollapseButton.setGraphic(EXPAND_IMAGE);
+        }
+    }
+    
+    @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+        double insets = topInset + bottomInset;
+        return insets + (content == null ? 0 : content.prefHeight(width));
+    }
+    
+    @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+        double insets = leftInset + rightInset;
+        return insets + (content == null ? 0 : content.prefWidth(height));
+    }
+    
+    @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
+    }
+    
+    @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return computePrefWidth(height, topInset, rightInset, bottomInset, leftInset);
+    }
+    
+    private void toggleInfoPanel() {
+        // animate!
+        // The best way I know how is to transition a number between 0.0 and 1.0
+        // over a set duration, and have this request layout as it goes. Then,
+        // use this value and multiply it against the actualInfoPanelHeight
+        // variable in layoutChildren - this will give a nice smooth animation.
+        if (content == null) {
+            return;
+        }
+
+        Duration duration;
+        if (timeline != null && (timeline.getStatus() != Status.STOPPED)) {
+            duration = timeline.getCurrentTime();
+            timeline.stop();
+        } else {
+            duration = TRANSITION_DURATION;
+        }
+
+        timeline = new Timeline();
+        timeline.setCycleCount(1);
+
+        KeyFrame k1, k2;
+
+        if (isExpanded()) {
+            k1 = new KeyFrame(Duration.ZERO, new KeyValue(transition, 0));
+            k2 = new KeyFrame(duration,new KeyValue(transition, 1, Interpolator.LINEAR));
+        } else {
+            k1 = new KeyFrame(Duration.ZERO, new KeyValue(transition, 1));
+            k2 = new KeyFrame(duration, new KeyValue(transition, 0, Interpolator.LINEAR));
+        }
+
+        timeline.getKeyFrames().setAll(k1, k2);
+        timeline.play();
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/skin/ListSelectionViewSkin.java b/controlsfx/src/main/java/impl/org/controlsfx/skin/ListSelectionViewSkin.java
new file mode 100644
index 0000000..376fac0
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/skin/ListSelectionViewSkin.java
@@ -0,0 +1,474 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.skin;
+
+import static java.util.Objects.requireNonNull;
+import static javafx.scene.control.SelectionMode.MULTIPLE;
+import static javafx.scene.input.MouseEvent.MOUSE_CLICKED;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javafx.beans.InvalidationListener;
+import javafx.beans.binding.Bindings;
+import javafx.geometry.Orientation;
+import javafx.geometry.Pos;
+import javafx.scene.Node;
+import javafx.scene.control.Button;
+import javafx.scene.control.ListView;
+import javafx.scene.control.SkinBase;
+import javafx.scene.input.MouseButton;
+import javafx.scene.layout.ColumnConstraints;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.RowConstraints;
+import javafx.scene.layout.StackPane;
+import javafx.scene.layout.VBox;
+
+import org.controlsfx.control.ListSelectionView;
+import org.controlsfx.glyphfont.FontAwesome;
+
+public class ListSelectionViewSkin<T> extends SkinBase<ListSelectionView<T>> {
+
+    private GridPane gridPane;
+    private final HBox horizontalButtonBox;
+    private final VBox verticalButtonBox;
+    private Button moveToTarget;
+    private Button moveToTargetAll;
+    private Button moveToSourceAll;
+    private Button moveToSource;
+    private ListView<T> sourceListView;
+    private ListView<T> targetListView;
+
+    public ListSelectionViewSkin(ListSelectionView<T> view) {
+        super(view);
+
+        sourceListView = requireNonNull(createSourceListView(),
+                "source list view can not be null");
+        sourceListView.setId("source-list-view");
+        sourceListView.setItems(view.getSourceItems());
+
+        targetListView = requireNonNull(createTargetListView(),
+                "target list view can not be null");
+        targetListView.setId("target-list-view");
+        targetListView.setItems(view.getTargetItems());
+
+        sourceListView.cellFactoryProperty().bind(view.cellFactoryProperty());
+        targetListView.cellFactoryProperty().bind(view.cellFactoryProperty());
+
+        gridPane = createGridPane();
+        horizontalButtonBox = createHorizontalButtonBox();
+        verticalButtonBox = createVerticalButtonBox();
+
+        getChildren().add(gridPane);
+
+        InvalidationListener updateListener = o -> updateView();
+
+        view.sourceHeaderProperty().addListener(updateListener);
+        view.sourceFooterProperty().addListener(updateListener);
+        view.targetHeaderProperty().addListener(updateListener);
+        view.targetFooterProperty().addListener(updateListener);
+
+        updateView();
+
+        getSourceListView().addEventHandler(
+                MOUSE_CLICKED,
+                event -> {
+                    if (event.getButton() == MouseButton.PRIMARY
+                            && event.getClickCount() == 2) {
+                        moveToTarget();
+                    }
+                });
+
+        getTargetListView().addEventHandler(
+                MOUSE_CLICKED,
+                event -> {
+                    if (event.getButton() == MouseButton.PRIMARY
+                            && event.getClickCount() == 2) {
+                        moveToSource();
+                    }
+                });
+        
+        view.orientationProperty().addListener(observable -> updateView());
+    }
+
+    private GridPane createGridPane() {
+        GridPane gridPane = new GridPane();
+        gridPane.getStyleClass().add("grid-pane");
+
+        return gridPane;
+    }
+
+    // Constraints used when view's orientation is HORIZONTAL
+    private void setHorizontalViewContraints() {
+        gridPane.getColumnConstraints().clear();
+        gridPane.getRowConstraints().clear();
+    
+        ColumnConstraints col1 = new ColumnConstraints();
+
+        col1.setFillWidth(true);
+        col1.setHgrow(Priority.ALWAYS);
+        col1.setMaxWidth(Double.MAX_VALUE);
+        col1.setPrefWidth(200);
+
+        ColumnConstraints col2 = new ColumnConstraints();
+        col2.setFillWidth(true);
+        col2.setHgrow(Priority.NEVER);
+
+        ColumnConstraints col3 = new ColumnConstraints();
+        col3.setFillWidth(true);
+        col3.setHgrow(Priority.ALWAYS);
+        col3.setMaxWidth(Double.MAX_VALUE);
+        col3.setPrefWidth(200);
+
+        gridPane.getColumnConstraints().addAll(col1, col2, col3);
+
+        RowConstraints row1 = new RowConstraints();
+        row1.setFillHeight(true);
+        row1.setVgrow(Priority.NEVER);
+
+        RowConstraints row2 = new RowConstraints();
+        row2.setMaxHeight(Double.MAX_VALUE);
+        row2.setPrefHeight(200);
+        row2.setVgrow(Priority.ALWAYS);
+
+        RowConstraints row3 = new RowConstraints();
+        row3.setFillHeight(true);
+        row3.setVgrow(Priority.NEVER);
+
+        gridPane.getRowConstraints().addAll(row1, row2, row3);
+    }
+
+    // Constraints used when view's orientation is VERTICAL
+    private void setVerticalViewContraints() {
+        gridPane.getColumnConstraints().clear();
+        gridPane.getRowConstraints().clear();
+    
+        ColumnConstraints col1 = new ColumnConstraints();
+
+        col1.setFillWidth(true);
+        col1.setHgrow(Priority.ALWAYS);
+        col1.setMaxWidth(Double.MAX_VALUE);
+        col1.setPrefWidth(200);
+
+        gridPane.getColumnConstraints().addAll(col1);
+
+        RowConstraints row1 = new RowConstraints();
+        row1.setFillHeight(true);
+        row1.setVgrow(Priority.NEVER);
+
+        RowConstraints row2 = new RowConstraints();
+        row2.setMaxHeight(Double.MAX_VALUE);
+        row2.setPrefHeight(200);
+        row2.setVgrow(Priority.ALWAYS);
+
+        RowConstraints row3 = new RowConstraints();
+        row3.setFillHeight(true);
+        row3.setVgrow(Priority.NEVER);
+        
+        RowConstraints row4 = new RowConstraints();
+        row4.setFillHeight(true);
+        row4.setVgrow(Priority.NEVER);
+        
+        RowConstraints row5 = new RowConstraints();
+        row5.setFillHeight(true);
+        row5.setVgrow(Priority.NEVER);
+
+        RowConstraints row6 = new RowConstraints();
+        row6.setMaxHeight(Double.MAX_VALUE);
+        row6.setPrefHeight(200);
+        row6.setVgrow(Priority.ALWAYS);
+
+        RowConstraints row7 = new RowConstraints();
+        row7.setFillHeight(true);
+        row7.setVgrow(Priority.NEVER);
+        
+
+        gridPane.getRowConstraints().addAll(row1, row2, row3, row4, row5, row6, row7);
+    }
+
+    // Used when view's orientation is HORIZONTAL
+    private VBox createVerticalButtonBox() {
+        VBox box = new VBox(5);
+        box.setFillWidth(true);
+
+        FontAwesome fontAwesome = new FontAwesome();
+        moveToTarget = new Button("",
+                fontAwesome.create(FontAwesome.Glyph.ANGLE_RIGHT));
+        moveToTargetAll = new Button("",
+                fontAwesome.create(FontAwesome.Glyph.ANGLE_DOUBLE_RIGHT));
+
+        moveToSource = new Button("",
+                fontAwesome.create(FontAwesome.Glyph.ANGLE_LEFT));
+        moveToSourceAll = new Button("",
+                fontAwesome.create(FontAwesome.Glyph.ANGLE_DOUBLE_LEFT));
+
+        updateButtons();
+        
+        box.getChildren().addAll(moveToTarget, moveToTargetAll, moveToSource,
+                moveToSourceAll);
+
+        return box;
+    }
+
+    // Used when view's orientation is VERTICAL
+    private HBox createHorizontalButtonBox() {
+        HBox box = new HBox(5);
+        box.setFillHeight(true);
+
+        FontAwesome fontAwesome = new FontAwesome();
+        moveToTarget = new Button("",
+                fontAwesome.create(FontAwesome.Glyph.ANGLE_DOWN));
+        moveToTargetAll = new Button("",
+                fontAwesome.create(FontAwesome.Glyph.ANGLE_DOUBLE_DOWN));
+
+        moveToSource = new Button("",
+                fontAwesome.create(FontAwesome.Glyph.ANGLE_UP));
+        moveToSourceAll = new Button("",
+                fontAwesome.create(FontAwesome.Glyph.ANGLE_DOUBLE_UP));
+
+        updateButtons();
+        
+        box.getChildren().addAll(moveToTarget, moveToTargetAll, moveToSource,
+                moveToSourceAll);
+
+        return box;
+    }
+
+    private void updateButtons() {
+        
+        moveToTarget.getStyleClass().add("move-to-target-button");
+        moveToTargetAll.getStyleClass().add("move-to-target-all-button");
+        moveToSource.getStyleClass().add("move-to-source-button");
+        moveToSourceAll.getStyleClass().add("move-to-source-all-button");
+
+        moveToTarget.setMaxWidth(Double.MAX_VALUE);
+        moveToTargetAll.setMaxWidth(Double.MAX_VALUE);
+        moveToSource.setMaxWidth(Double.MAX_VALUE);
+        moveToSourceAll.setMaxWidth(Double.MAX_VALUE);
+
+        getSourceListView().itemsProperty().addListener(
+                it -> bindMoveAllButtonsToDataModel());
+
+        getTargetListView().itemsProperty().addListener(
+                it -> bindMoveAllButtonsToDataModel());
+
+        getSourceListView().selectionModelProperty().addListener(
+                it -> bindMoveButtonsToSelectionModel());
+
+        getTargetListView().selectionModelProperty().addListener(
+                it -> bindMoveButtonsToSelectionModel());
+
+        bindMoveButtonsToSelectionModel();
+        bindMoveAllButtonsToDataModel();
+
+        moveToTarget.setOnAction(evt -> moveToTarget());
+
+        moveToTargetAll.setOnAction(evt -> moveToTargetAll());
+
+        moveToSource.setOnAction(evt -> moveToSource());
+
+        moveToSourceAll.setOnAction(evt -> moveToSourceAll());
+    }
+    
+    private void bindMoveAllButtonsToDataModel() {
+        moveToTargetAll.disableProperty().bind(
+                Bindings.isEmpty(getSourceListView().getItems()));
+
+        moveToSourceAll.disableProperty().bind(
+                Bindings.isEmpty(getTargetListView().getItems()));
+    }
+
+    private void bindMoveButtonsToSelectionModel() {
+        moveToTarget.disableProperty().bind(
+                Bindings.isEmpty(getSourceListView().getSelectionModel()
+                        .getSelectedItems()));
+
+        moveToSource.disableProperty().bind(
+                Bindings.isEmpty(getTargetListView().getSelectionModel()
+                        .getSelectedItems()));
+    }
+
+    private void updateView() {
+        gridPane.getChildren().clear();
+
+        Node sourceHeader = getSkinnable().getSourceHeader();
+        Node targetHeader = getSkinnable().getTargetHeader();
+        Node sourceFooter = getSkinnable().getSourceFooter();
+        Node targetFooter = getSkinnable().getTargetFooter();
+
+        ListView<T> sourceList = getSourceListView();
+        ListView<T> targetList = getTargetListView();
+
+        StackPane stackPane = new StackPane();
+        stackPane.setAlignment(Pos.CENTER);
+
+        Orientation orientation = getSkinnable().getOrientation();
+
+        if (orientation == Orientation.HORIZONTAL) {
+            setHorizontalViewContraints();
+            
+            if (sourceHeader != null) {
+                gridPane.add(sourceHeader, 0, 0);
+            }
+
+            if (targetHeader != null) {
+                gridPane.add(targetHeader, 2, 0);
+            }
+
+            if (sourceList != null) {
+                gridPane.add(sourceList, 0, 1);
+            }
+
+            if (targetList != null) {
+                gridPane.add(targetList, 2, 1);
+            }
+
+            if (sourceFooter != null) {
+                gridPane.add(sourceFooter, 0, 2);
+            }
+
+            if (targetFooter != null) {
+                gridPane.add(targetFooter, 2, 2);
+            }
+
+            stackPane.getChildren().add(verticalButtonBox);
+            gridPane.add(stackPane, 1, 1);
+        } else {
+            setVerticalViewContraints();
+            
+            if (sourceHeader != null) {
+                gridPane.add(sourceHeader, 0, 0);
+            }
+
+            if (targetHeader != null) {
+                gridPane.add(targetHeader, 0, 4);
+            }
+
+            if (sourceList != null) {
+                gridPane.add(sourceList, 0, 1);
+            }
+
+            if (targetList != null) {
+                gridPane.add(targetList, 0, 5);
+            }
+
+            if (sourceFooter != null) {
+                gridPane.add(sourceFooter, 0, 2);
+            }
+
+            if (targetFooter != null) {
+                gridPane.add(targetFooter, 0, 6);
+            }
+
+            stackPane.getChildren().add(horizontalButtonBox);
+            gridPane.add(stackPane, 0, 3);
+        }
+    }
+
+    private void moveToTarget() {
+        move(getSourceListView(), getTargetListView());
+        getSourceListView().getSelectionModel().clearSelection();
+    }
+
+    private void moveToTargetAll() {
+        move(getSourceListView(), getTargetListView(), new ArrayList<>(
+                getSourceListView().getItems()));
+        getSourceListView().getSelectionModel().clearSelection();
+    }
+
+    private void moveToSource() {
+        move(getTargetListView(), getSourceListView());
+        getTargetListView().getSelectionModel().clearSelection();
+    }
+
+    private void moveToSourceAll() {
+        move(getTargetListView(), getSourceListView(), new ArrayList<>(
+                getTargetListView().getItems()));
+        getTargetListView().getSelectionModel().clearSelection();
+    }
+
+    private void move(ListView<T> viewA, ListView<T> viewB) {
+        List<T> selectedItems = new ArrayList<>(viewA.getSelectionModel()
+                .getSelectedItems());
+        move(viewA, viewB, selectedItems);
+    }
+
+    private void move(ListView<T> viewA, ListView<T> viewB, List<T> items) {
+        for (T item : items) {
+            viewA.getItems().remove(item);
+            viewB.getItems().add(item);
+        }
+    }
+
+    /**
+     * Returns the source list view (shown on the left-hand side).
+     *
+     * @return the source list view
+     */
+    public final ListView<T> getSourceListView() {
+        return sourceListView;
+    }
+
+    /**
+     * Returns the target list view (shown on the right-hand side).
+     *
+     * @return the target list view
+     */
+    public final ListView<T> getTargetListView() {
+        return targetListView;
+    }
+
+    /**
+     * Creates the {@link ListView} instance used on the left-hand side as the
+     * source list. This method can be overridden to provide a customized list
+     * view control.
+     *
+     * @return the source list view
+     */
+    protected ListView<T> createSourceListView() {
+        return createListView();
+    }
+
+    /**
+     * Creates the {@link ListView} instance used on the right-hand side as the
+     * target list. This method can be overridden to provide a customized list
+     * view control.
+     *
+     * @return the target list view
+     */
+    protected ListView<T> createTargetListView() {
+        return createListView();
+    }
+
+    private ListView<T> createListView() {
+        ListView<T> view = new ListView<>();
+        view.getSelectionModel().setSelectionMode(MULTIPLE);
+        return view;
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/skin/MaskerPaneSkin.java b/controlsfx/src/main/java/impl/org/controlsfx/skin/MaskerPaneSkin.java
new file mode 100644
index 0000000..0e7cf61
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/skin/MaskerPaneSkin.java
@@ -0,0 +1,79 @@
+/**
+ * Copyright (c) 2014, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.skin;
+
+import javafx.geometry.Pos;
+import javafx.scene.control.Label;
+import javafx.scene.control.SkinBase;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.StackPane;
+import javafx.scene.layout.VBox;
+import org.controlsfx.control.MaskerPane;
+
+public class MaskerPaneSkin extends SkinBase<MaskerPane> {
+
+    public MaskerPaneSkin(MaskerPane maskerPane) {
+        super(maskerPane);
+        getChildren().add(createMasker(maskerPane));
+    }
+
+    private StackPane createMasker(MaskerPane maskerPane) {
+        VBox vBox = new VBox();
+        vBox.setAlignment(Pos.CENTER);
+        vBox.setSpacing(10.0);
+        vBox.getStyleClass().add("masker-center"); //$NON-NLS-1$
+
+        vBox.getChildren().add(createLabel());
+        vBox.getChildren().add(createProgressIndicator());
+
+        HBox hBox = new HBox();
+        hBox.setAlignment(Pos.CENTER);
+        hBox.getChildren().addAll(vBox);
+
+        StackPane glass = new StackPane();
+        glass.setAlignment(Pos.CENTER);
+        glass.getStyleClass().add("masker-glass"); //$NON-NLS-1$
+        glass.getChildren().add(hBox);
+
+        return glass;
+    }
+
+    private Label createLabel() {
+        Label text = new Label();
+        text.textProperty().bind(getSkinnable().textProperty());
+        text.getStyleClass().add("masker-text"); //$NON-NLS-1$
+        return text;
+    }
+
+    private Label createProgressIndicator() {
+        Label graphic = new Label();
+        graphic.setGraphic(getSkinnable().getProgressNode());
+        graphic.visibleProperty().bind(getSkinnable().progressVisibleProperty());
+        graphic.getStyleClass().add("masker-graphic"); //$NON-NLS-1$
+        return graphic;
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/skin/MasterDetailPaneSkin.java b/controlsfx/src/main/java/impl/org/controlsfx/skin/MasterDetailPaneSkin.java
new file mode 100644
index 0000000..8a954b8
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/skin/MasterDetailPaneSkin.java
@@ -0,0 +1,473 @@
+/**
+ * Copyright (c) 2014, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.skin;
+
+import static java.lang.Double.MAX_VALUE;
+import static javafx.geometry.Orientation.HORIZONTAL;
+import static javafx.geometry.Orientation.VERTICAL;
+
+import java.util.List;
+
+import javafx.animation.Animation;
+import javafx.animation.KeyFrame;
+import javafx.animation.KeyValue;
+import javafx.animation.Timeline;
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.ListChangeListener;
+import javafx.geometry.Side;
+import javafx.scene.Node;
+import javafx.scene.control.SkinBase;
+import javafx.scene.control.SplitPane;
+import javafx.scene.control.SplitPane.Divider;
+import javafx.scene.layout.Region;
+import javafx.util.Duration;
+
+import org.controlsfx.control.MasterDetailPane;
+
+public class MasterDetailPaneSkin extends SkinBase<MasterDetailPane> {
+
+    private boolean changing = false;
+    private SplitPane splitPane;
+    private final Timeline timeline = new Timeline();
+    private BooleanProperty showDetailForTimeline = new SimpleBooleanProperty();
+
+    public MasterDetailPaneSkin(MasterDetailPane pane) {
+        super(pane);
+
+        this.splitPane = new SplitPane();
+        this.splitPane.setDividerPosition(0, pane.getDividerPosition());
+
+        /**
+         * We listen to the change of dividers (when adding or removing node), and then
+         * we listen to their position to update correctly the dividerPosition of 
+         * the MasterDetailPane.
+         */
+        this.splitPane.getDividers().addListener(new ListChangeListener<Divider>() {
+
+            @Override
+            public void onChanged(ListChangeListener.Change<? extends Divider> change) {
+                while (change.next()) {
+                    if (change.wasAdded()) {
+                        change.getAddedSubList().get(0).positionProperty().addListener(updateDividerPositionListener);
+                    } else if (change.wasRemoved()) {
+                        change.getRemoved().get(0).positionProperty().removeListener(updateDividerPositionListener);
+                    }
+                }
+            }
+        });
+        
+        SplitPane.setResizableWithParent(getSkinnable().getDetailNode(), false);
+
+        switch (getSkinnable().getDetailSide()) {
+            case BOTTOM:
+            case TOP:
+                splitPane.setOrientation(VERTICAL);
+                break;
+            case LEFT:
+            case RIGHT:
+                splitPane.setOrientation(HORIZONTAL);
+                break;
+        }
+
+        getSkinnable().masterNodeProperty().addListener(
+                new ChangeListener<Node>() {
+                    @Override
+                    public void changed(ObservableValue<? extends Node> value,
+                            Node oldNode, Node newNode) {
+
+                        if (oldNode != null) {
+                            splitPane.getItems().remove(oldNode);
+                        }
+
+                        if (newNode != null) {
+
+                            updateMinAndMaxSizes();
+
+                            int masterIndex = 0;
+                            switch (splitPane.getOrientation()) {
+                                case HORIZONTAL:
+                                    switch (getSkinnable().getDetailSide()) {
+                                        case LEFT:
+                                            masterIndex = 1;
+                                            break;
+                                        case RIGHT:
+                                            masterIndex = 0;
+                                            break;
+                                        default:
+                                            throw new IllegalArgumentException(
+                                                    "illegal details position " //$NON-NLS-1$
+                                                            + getSkinnable()
+                                                            .getDetailSide()
+                                                            + " for orientation " //$NON-NLS-1$
+                                                            + splitPane
+                                                            .getOrientation());
+                                    }
+                                    break;
+                                case VERTICAL:
+                                    switch (getSkinnable().getDetailSide()) {
+                                        case TOP:
+                                            masterIndex = 1;
+                                            break;
+                                        case BOTTOM:
+                                            masterIndex = 0;
+                                            break;
+                                        default:
+                                            throw new IllegalArgumentException(
+                                                    "illegal details position " //$NON-NLS-1$
+                                                            + getSkinnable()
+                                                            .getDetailSide()
+                                                            + " for orientation " //$NON-NLS-1$
+                                                            + splitPane
+                                                            .getOrientation());
+                                    }
+                                    break;
+                            }
+                            List<Node> items = splitPane.getItems();
+                            if (items.isEmpty()) {
+                                items.add(newNode);
+                            } else {
+                                items.add(masterIndex, newNode);
+                            }
+                        }
+                    }
+                });
+
+        getSkinnable().detailNodeProperty().addListener(
+                new ChangeListener<Node>() {
+                    @Override
+                    public void changed(ObservableValue<? extends Node> value,
+                            Node oldNode, Node newNode) {
+
+                        if (oldNode != null) {
+                            splitPane.getItems().remove(oldNode);
+                        }
+
+                        /**
+                        * If the detailNode is not showing, we do not force
+                        * it to show.
+                        */
+                        if (newNode != null && getSkinnable().isShowDetailNode()) {
+
+                            /**
+                             * Force the divider to take the value of the Pane,
+                             * and not compute his.
+                             */
+                            splitPane.setDividerPositions(getSkinnable().getDividerPosition());
+                            updateMinAndMaxSizes();
+
+                            SplitPane.setResizableWithParent(newNode, false);
+
+                            int detailsIndex = 0;
+                            switch (splitPane.getOrientation()) {
+                                case HORIZONTAL:
+                                    switch (getSkinnable().getDetailSide()) {
+                                        case LEFT:
+                                            detailsIndex = 0;
+                                            break;
+                                        case RIGHT:
+                                            detailsIndex = 1;
+                                            break;
+                                        default:
+                                            throw new IllegalArgumentException(
+                                                    "illegal details position " //$NON-NLS-1$
+                                                    + getSkinnable()
+                                                    .getDetailSide()
+                                                    + " for orientation " //$NON-NLS-1$
+                                                    + splitPane
+                                                    .getOrientation());
+                                    }
+                                    break;
+                                case VERTICAL:
+                                    switch (getSkinnable().getDetailSide()) {
+                                        case TOP:
+                                            detailsIndex = 0;
+                                            break;
+                                        case BOTTOM:
+                                            detailsIndex = 1;
+                                            break;
+                                        default:
+                                            throw new IllegalArgumentException(
+                                                    "illegal details position " //$NON-NLS-1$
+                                                    + getSkinnable()
+                                                    .getDetailSide()
+                                                    + " for orientation " //$NON-NLS-1$
+                                                    + splitPane
+                                                    .getOrientation());
+                                    }
+                                    break;
+                            }
+                            List<Node> items = splitPane.getItems();
+                            if (items.isEmpty()) {
+                                items.add(newNode);
+                            } else {
+                                items.add(detailsIndex, newNode);
+                            }
+                        }
+                    }
+                });
+
+        getSkinnable().showDetailNodeProperty().addListener(
+                new ChangeListener<Boolean>() {
+                    @Override
+                    public void changed(
+                            ObservableValue<? extends Boolean> value,
+                            Boolean oldShow, Boolean newShow) {
+                                /**
+                                 * https://bitbucket.org/controlsfx/controlsfx/issue/456/masterdetailpane-bug-of-adding-infinite
+                                 *
+                                 * Fixed bug - when close or show is still animated jump to last frame of animation
+                                 ** and fire finished event to complete the previous demand
+                                 *
+                                 */
+                                if (getSkinnable().isAnimated() && timeline.getStatus() == Animation.Status.RUNNING) {
+                                    timeline.jumpTo("endAnimation");
+                                    timeline.getOnFinished().handle(null);
+                                }
+
+                                if (newShow) {
+                                    open();
+                                } else {
+                                    close();
+                                }
+                            }
+                });
+
+        getSkinnable().detailSideProperty().addListener(
+                new ChangeListener<Side>() {
+                    @Override
+                    public void changed(ObservableValue<? extends Side> value,
+                            Side oldPos, Side newPos) {
+                        if (getSkinnable().isShowDetailNode()) {
+                            splitPane.getItems().clear();
+                        }
+                        switch (newPos) {
+                            case TOP:
+                            case BOTTOM:
+                                splitPane.setOrientation(VERTICAL);
+                                break;
+                            case LEFT:
+                            case RIGHT:
+                                splitPane.setOrientation(HORIZONTAL);
+                        }
+                        switch (newPos) {
+                            case TOP:
+                            case LEFT:
+                                if (getSkinnable().isShowDetailNode()) {
+                                    splitPane.getItems().add(
+                                            getSkinnable().getDetailNode());
+                                    splitPane.getItems().add(
+                                            getSkinnable().getMasterNode());
+                                }
+                                switch (oldPos) {
+                                    case BOTTOM:
+                                    case RIGHT:
+                                        getSkinnable().setDividerPosition(1 - getSkinnable().getDividerPosition());
+                                        break;
+                                    default:
+                                        break;
+                                }
+                                break;
+                            case BOTTOM:
+                            case RIGHT:
+                                if (getSkinnable().isShowDetailNode()) {
+                                    splitPane.getItems().add(
+                                            getSkinnable().getMasterNode());
+                                    splitPane.getItems().add(
+                                            getSkinnable().getDetailNode());
+                                }
+                                switch (oldPos) {
+                                    case TOP:
+                                    case LEFT:
+                                        getSkinnable().setDividerPosition(1 - getSkinnable().getDividerPosition());
+                                        break;
+                                    default:
+                                        break;
+                                }
+                                break;
+                        }
+                        if (getSkinnable().isShowDetailNode()) {
+                            splitPane.setDividerPositions(getSkinnable().getDividerPosition());
+                        }
+                    }
+                });
+
+        updateMinAndMaxSizes();
+
+        getChildren().add(splitPane);
+
+        splitPane.getItems().add(getSkinnable().getMasterNode());
+
+        if (getSkinnable().isShowDetailNode()) {
+            switch (getSkinnable().getDetailSide()) {
+                case TOP:
+                case LEFT:
+                    splitPane.getItems().add(0, getSkinnable().getDetailNode());
+                    break;
+                case BOTTOM:
+                case RIGHT:
+                    splitPane.getItems().add(getSkinnable().getDetailNode());
+                    break;
+            }
+
+            bindDividerPosition();
+        }
+
+        timeline.setOnFinished(evt -> {
+            if (!showDetailForTimeline.get()) {
+                unbindDividerPosition();
+                splitPane.getItems().remove(
+                        getSkinnable().getDetailNode());
+                getSkinnable().getDetailNode().setOpacity(1);
+            }
+            changing = false;
+        });
+    }
+
+    private InvalidationListener listenersDivider = new InvalidationListener() {
+        @Override
+        public void invalidated(Observable arg0) {
+            changing = true;
+            splitPane.setDividerPosition(0, getSkinnable().getDividerPosition());
+            changing = false;
+        }
+    };
+
+    private void bindDividerPosition() {
+        getSkinnable().dividerPositionProperty().addListener(listenersDivider);
+    }
+
+    private void unbindDividerPosition() {
+        getSkinnable().dividerPositionProperty().removeListener(listenersDivider);
+    }
+
+    private void updateMinAndMaxSizes() {
+        if (getSkinnable().getMasterNode() instanceof Region) {
+            ((Region) getSkinnable().getMasterNode()).setMinSize(0, 0);
+            ((Region) getSkinnable().getMasterNode()).setMaxSize(MAX_VALUE,
+                    MAX_VALUE);
+        }
+
+        if (getSkinnable().getDetailNode() instanceof Region) {
+            ((Region) getSkinnable().getDetailNode()).setMinSize(0, 0);
+            ((Region) getSkinnable().getDetailNode()).setMaxSize(MAX_VALUE,
+                    MAX_VALUE);
+        }
+    }
+
+    private void open() {
+        changing = true;
+        Node node = getSkinnable().getDetailNode();
+
+        switch (getSkinnable().getDetailSide()) {
+            case TOP:
+            case LEFT:
+                splitPane.getItems().add(0, node);
+                splitPane.setDividerPositions(0);
+                break;
+            case BOTTOM:
+            case RIGHT:
+                splitPane.getItems().add(node);
+                splitPane.setDividerPositions(1);
+                break;
+        }
+
+        updateMinAndMaxSizes();
+        maybeAnimatePositionChange(getSkinnable().getDividerPosition(), true);
+    }
+
+    private void close() {
+        changing = true;
+        if (!splitPane.getDividers().isEmpty()) {
+
+            /*
+             * Do we collapse by moving the divider to the left/right or
+             * top/bottom?
+             */
+            double targetLocation = 0;
+            switch (getSkinnable().getDetailSide()) {
+                case BOTTOM:
+                case RIGHT:
+                    targetLocation = 1;
+                    break;
+                default:
+                    break;
+            }
+
+            maybeAnimatePositionChange(targetLocation, false);
+        }
+    }
+
+    private void maybeAnimatePositionChange(final double position,
+            final boolean showDetail) {
+        showDetailForTimeline.set(showDetail);
+
+        Divider divider = splitPane.getDividers().get(0);
+
+        if (showDetailForTimeline.get()) {
+            unbindDividerPosition();
+            bindDividerPosition();
+        }
+
+        if (getSkinnable().isAnimated()) {
+            KeyValue positionKeyValue = new KeyValue(
+                    divider.positionProperty(), position);
+            KeyValue opacityKeyValue = new KeyValue(getSkinnable()
+                    .getDetailNode().opacityProperty(), showDetailForTimeline.get() ? 1 : 0);
+
+            KeyFrame keyFrame = new KeyFrame(Duration.seconds(.1), "endAnimation", positionKeyValue, opacityKeyValue);
+
+            timeline.getKeyFrames().clear();
+            timeline.getKeyFrames().add(keyFrame);
+
+            timeline.playFromStart();
+        } else {
+            getSkinnable().getDetailNode().setOpacity(1);
+            divider.setPosition(position);
+
+            if (!showDetailForTimeline.get()) {
+                unbindDividerPosition();
+                splitPane.getItems().remove(getSkinnable().getDetailNode());
+            }
+            changing = false;
+        }
+    }
+
+    private ChangeListener<Number> updateDividerPositionListener = new ChangeListener<Number>() {
+
+        @Override
+        public void changed(ObservableValue<? extends Number> ov, Number t, Number t1) {
+            if (!changing) {
+                getSkinnable().setDividerPosition(t1.doubleValue());
+            }
+        }
+    };
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/skin/NotificationBar.java b/controlsfx/src/main/java/impl/org/controlsfx/skin/NotificationBar.java
new file mode 100644
index 0000000..2702eba
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/skin/NotificationBar.java
@@ -0,0 +1,309 @@
+/**
+ * Copyright (c) 2014, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.skin;
+
+import javafx.animation.Animation.Status;
+import javafx.animation.Interpolator;
+import javafx.animation.KeyFrame;
+import javafx.animation.KeyValue;
+import javafx.animation.Timeline;
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.collections.ObservableList;
+import javafx.event.ActionEvent;
+import javafx.event.Event;
+import javafx.event.EventHandler;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.geometry.VPos;
+import javafx.scene.Node;
+import javafx.scene.control.Button;
+import javafx.scene.control.ButtonBar;
+import javafx.scene.control.Label;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.StackPane;
+import javafx.util.Duration;
+
+import org.controlsfx.control.NotificationPane;
+import org.controlsfx.control.action.Action;
+import org.controlsfx.control.action.ActionUtils;
+
+ at SuppressWarnings("deprecation")
+public abstract class NotificationBar extends Region {
+
+    private static final double MIN_HEIGHT = 40;
+
+    final Label label;
+    Label title;
+    ButtonBar actionsBar;
+    Button closeBtn;
+
+    private final GridPane pane;
+    
+    public DoubleProperty transition = new SimpleDoubleProperty() {
+        @Override protected void invalidated() {
+            requestContainerLayout();
+        }
+    };
+    
+    
+    public void requestContainerLayout() {
+        layoutChildren();
+    }
+    
+    public String getTitle() {
+        return ""; //$NON-NLS-1$
+    }
+    
+    public boolean isCloseButtonVisible() {
+        return true;
+    }
+    
+    public abstract String getText();
+    public abstract Node getGraphic();
+    public abstract ObservableList<Action> getActions();
+    public abstract void hide();
+    public abstract boolean isShowing();
+    public abstract boolean isShowFromTop();
+    
+    public abstract double getContainerHeight();
+    public abstract void relocateInParent(double x, double y);
+    
+    public NotificationBar() {
+        getStyleClass().add("notification-bar"); //$NON-NLS-1$
+        
+        setVisible(isShowing());
+        
+        pane = new GridPane();
+        pane.getStyleClass().add("pane"); //$NON-NLS-1$
+        pane.setAlignment(Pos.BASELINE_LEFT);
+        getChildren().setAll(pane);
+        
+        // initialise title area, if one is set
+        String titleStr = getTitle();
+        if (titleStr != null && ! titleStr.isEmpty()) {
+            title = new Label();
+            title.getStyleClass().add("title"); //$NON-NLS-1$
+            title.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
+            GridPane.setHgrow(title, Priority.ALWAYS);
+
+            title.setText(titleStr);
+            title.opacityProperty().bind(transition);
+        }
+        
+        // initialise label area
+        label = new Label();
+        label.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
+        GridPane.setVgrow(label, Priority.ALWAYS);
+        GridPane.setHgrow(label, Priority.ALWAYS);
+
+        label.setText(getText());
+        label.setGraphic(getGraphic());
+        label.opacityProperty().bind(transition);
+
+        // initialise actions area
+        getActions().addListener(new InvalidationListener() {
+            @Override public void invalidated(Observable arg0) {
+                updatePane();
+            }
+        });
+
+        // initialise close button area
+        closeBtn = new Button();
+        closeBtn.setOnAction(new EventHandler<ActionEvent>() {
+            @Override public void handle(ActionEvent arg0) {
+                hide();
+            }
+        });
+        closeBtn.getStyleClass().setAll("close-button"); //$NON-NLS-1$
+        StackPane graphic = new StackPane();
+        graphic.getStyleClass().setAll("graphic"); //$NON-NLS-1$
+        closeBtn.setGraphic(graphic);
+        closeBtn.setMinSize(17, 17);
+        closeBtn.setPrefSize(17, 17);
+        closeBtn.setFocusTraversable(false);
+        closeBtn.opacityProperty().bind(transition);
+        GridPane.setMargin(closeBtn, new Insets(0, 0, 0, 8));
+        
+        // position the close button in the best place, depending on the height
+        double minHeight = minHeight(-1);
+        GridPane.setValignment(closeBtn, minHeight == MIN_HEIGHT ? VPos.CENTER : VPos.TOP);
+        
+        // put it all together
+        updatePane();
+    }
+
+    void updatePane() {
+        actionsBar = ActionUtils.createButtonBar(getActions());
+        actionsBar.opacityProperty().bind(transition);
+        GridPane.setHgrow(actionsBar, Priority.SOMETIMES);
+        pane.getChildren().clear();
+        
+        int row = 0;
+        
+        if (title != null) {
+            pane.add(title, 0, row++);
+        }
+        
+        pane.add(label, 0, row);
+        pane.add(actionsBar, 1, row);
+        
+        if (isCloseButtonVisible()) {
+            pane.add(closeBtn, 2, 0, 1, row+1);
+        }
+    }
+
+    @Override protected void layoutChildren() {
+        final double w = getWidth();
+        final double h = computePrefHeight(-1);
+        
+        final double notificationBarHeight = prefHeight(w);
+        final double notificationMinHeight = minHeight(w);
+        
+        if (isShowFromTop()) {
+            // place at top of area
+            pane.resize(w, h);
+            relocateInParent(0, (transition.get() - 1) * notificationMinHeight);
+        } else {
+            // place at bottom of area
+            pane.resize(w, notificationBarHeight);
+            relocateInParent(0, getContainerHeight() - notificationBarHeight);
+        }
+    }
+
+    @Override protected double computeMinHeight(double width) {
+        return Math.max(super.computePrefHeight(width), MIN_HEIGHT);
+    }
+
+    @Override protected double computePrefHeight(double width) {
+        return Math.max(pane.prefHeight(width), minHeight(width)) * transition.get();
+    }
+
+    public void doShow() {
+        transitionStartValue = 0;
+        doAnimationTransition();
+    }
+
+    public void doHide() {
+        transitionStartValue = 1;
+        doAnimationTransition();
+    }
+
+
+
+    // --- animation timeline code
+    private final Duration TRANSITION_DURATION = new Duration(350.0);
+    private Timeline timeline;
+    private double transitionStartValue;
+    private void doAnimationTransition() {
+        Duration duration;
+
+        if (timeline != null && (timeline.getStatus() != Status.STOPPED)) {
+            duration = timeline.getCurrentTime();
+
+            // fix for #70 - the notification pane freezes up as it has zero
+            // duration to expand / contract
+            duration = duration == Duration.ZERO ? TRANSITION_DURATION : duration;
+            transitionStartValue = transition.get();
+            // --- end of fix
+
+            timeline.stop();
+        } else {
+            duration = TRANSITION_DURATION;
+        }
+
+        timeline = new Timeline();
+        timeline.setCycleCount(1);
+
+        KeyFrame k1, k2;
+
+        if (isShowing()) {
+            k1 = new KeyFrame(
+                    Duration.ZERO,
+                    new EventHandler<ActionEvent>() {
+                        @Override public void handle(ActionEvent event) {
+                            // start expand
+                            setCache(true);
+                            setVisible(true);
+
+                            pane.fireEvent(new Event(NotificationPane.ON_SHOWING));
+                        }
+                    },
+                    new KeyValue(transition, transitionStartValue)
+                    );
+
+            k2 = new KeyFrame(
+                    duration,
+                    new EventHandler<ActionEvent>() {
+                        @Override public void handle(ActionEvent event) {
+                            // end expand
+                            pane.setCache(false);
+
+                            pane.fireEvent(new Event(NotificationPane.ON_SHOWN));
+                        }
+                    },
+                    new KeyValue(transition, 1, Interpolator.EASE_OUT)
+
+                    );
+        } else {
+            k1 = new KeyFrame(
+                    Duration.ZERO,
+                    new EventHandler<ActionEvent>() {
+                        @Override public void handle(ActionEvent event) {
+                            // Start collapse
+                            pane.setCache(true);
+
+                            pane.fireEvent(new Event(NotificationPane.ON_HIDING));
+                        }
+                    },
+                    new KeyValue(transition, transitionStartValue)
+                    );
+
+            k2 = new KeyFrame(
+                    duration,
+                    new EventHandler<ActionEvent>() {
+                        @Override public void handle(ActionEvent event) {
+                            // end collapse
+                            setCache(false);
+                            setVisible(false);
+
+                            pane.fireEvent(new Event(NotificationPane.ON_HIDDEN));
+                        }
+                    },
+                    new KeyValue(transition, 0, Interpolator.EASE_IN)
+                    );
+        }
+
+        timeline.getKeyFrames().setAll(k1, k2);
+        timeline.play();
+    }
+}
+
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/skin/NotificationPaneSkin.java b/controlsfx/src/main/java/impl/org/controlsfx/skin/NotificationPaneSkin.java
new file mode 100644
index 0000000..58d4046
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/skin/NotificationPaneSkin.java
@@ -0,0 +1,201 @@
+/**
+ * Copyright (c) 2013, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.skin;
+
+import java.util.Collections;
+
+import com.sun.javafx.scene.traversal.ParentTraversalEngine;
+import javafx.collections.ObservableList;
+import javafx.scene.Node;
+import javafx.scene.shape.Rectangle;
+
+import org.controlsfx.control.NotificationPane;
+import org.controlsfx.control.action.Action;
+
+import com.sun.javafx.scene.control.behavior.BehaviorBase;
+import com.sun.javafx.scene.control.behavior.KeyBinding;
+import com.sun.javafx.scene.control.skin.BehaviorSkinBase;
+
+public class NotificationPaneSkin extends BehaviorSkinBase<NotificationPane, BehaviorBase<NotificationPane>> {
+
+    private NotificationBar notificationBar;
+    private Node content;
+    private Rectangle clip = new Rectangle();
+
+    public NotificationPaneSkin(final NotificationPane control) {
+        super(control, new BehaviorBase<>(control, Collections.<KeyBinding> emptyList()));
+
+        notificationBar = new NotificationBar() {
+            @Override public void requestContainerLayout() {
+                control.requestLayout();
+            }
+
+            @Override public String getText() {
+                return control.getText();
+            }
+
+            @Override public Node getGraphic() {
+                return control.getGraphic();
+            }
+
+            @Override public ObservableList<Action> getActions() {
+                return control.getActions();
+            }
+
+            @Override public boolean isShowing() {
+                return control.isShowing();
+            }
+
+            @Override public boolean isShowFromTop() {
+                return control.isShowFromTop();
+            }
+
+            @Override public void hide() {
+                control.hide();
+            }
+
+            @Override public boolean isCloseButtonVisible() {
+                    return control.isCloseButtonVisible();
+            }
+
+            @Override public double getContainerHeight() {
+                return control.getHeight();
+            }
+
+            @Override public void relocateInParent(double x, double y) {
+                notificationBar.relocate(x, y);
+            }
+        };
+
+        control.setClip(clip);
+        updateContent();
+
+        registerChangeListener(control.heightProperty(), "HEIGHT"); //$NON-NLS-1$
+        registerChangeListener(control.contentProperty(), "CONTENT"); //$NON-NLS-1$
+        registerChangeListener(control.textProperty(), "TEXT"); //$NON-NLS-1$
+        registerChangeListener(control.graphicProperty(), "GRAPHIC"); //$NON-NLS-1$
+        registerChangeListener(control.showingProperty(), "SHOWING"); //$NON-NLS-1$
+        registerChangeListener(control.showFromTopProperty(), "SHOW_FROM_TOP"); //$NON-NLS-1$
+        registerChangeListener(control.closeButtonVisibleProperty(), "CLOSE_BUTTON_VISIBLE"); //$NON-NLS-1$
+
+        // Fix for Issue #522: Prevent NotificationPane from receiving focus
+        ParentTraversalEngine engine = new ParentTraversalEngine(getSkinnable());
+        getSkinnable().setImpl_traversalEngine(engine);
+        engine.setOverriddenFocusTraversability(false);
+    }
+    
+    @Override protected void handleControlPropertyChanged(String p) {
+        super.handleControlPropertyChanged(p);
+        
+        if ("CONTENT".equals(p)) { //$NON-NLS-1$
+            updateContent();
+        } else if ("TEXT".equals(p)) { //$NON-NLS-1$
+            notificationBar.label.setText(getSkinnable().getText());
+        } else if ("GRAPHIC".equals(p)) { //$NON-NLS-1$
+            notificationBar.label.setGraphic(getSkinnable().getGraphic());
+        } else if ("SHOWING".equals(p)) { //$NON-NLS-1$
+            if (getSkinnable().isShowing()) {
+                notificationBar.doShow();
+            } else {
+                notificationBar.doHide();
+            }
+        } else if ("SHOW_FROM_TOP".equals(p)) { //$NON-NLS-1$
+            if (getSkinnable().isShowing()) {
+                getSkinnable().requestLayout();
+            }
+        } else if ("CLOSE_BUTTON_VISIBLE".equals(p)) { //$NON-NLS-1$
+            notificationBar.updatePane();
+        }else if ( "HEIGHT".equals(p)){
+            // For resolving https://bitbucket.org/controlsfx/controlsfx/issue/409
+            if (getSkinnable().isShowing() && !getSkinnable().isShowFromTop()) {
+                notificationBar.requestLayout();
+            }
+        }
+    }
+    
+    private void updateContent() {
+        if (content != null) {
+            getChildren().remove(content);
+        }
+        
+        content = getSkinnable().getContent();
+        
+        if (content == null) {
+            getChildren().setAll(notificationBar);
+        } else {
+            getChildren().setAll(content, notificationBar);
+        }
+    }
+    
+    @Override protected void layoutChildren(double x, double y, double w, double h) {
+        final double notificationBarHeight = notificationBar.prefHeight(w);
+        
+        notificationBar.resize(w, notificationBarHeight);
+        
+        // layout the content
+        if (content != null) {
+            content.resizeRelocate(x, y, w, h);
+        }
+        
+        // and update the clip so that the notification bar does not draw outside
+        // the bounds of the notification pane
+        clip.setX(x);
+        clip.setY(y);
+        clip.setWidth(w);
+        clip.setHeight(h);
+    }
+    
+    @Override
+    protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return content == null ? 0 : content.minWidth(height);
+    };
+    
+    @Override
+    protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return content == null ? 0 : content.minHeight(width);
+    };
+    
+    @Override
+    protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return content == null ? 0 : content.prefWidth(height);
+    };
+    
+    @Override
+    protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return content == null ? 0 : content.prefHeight(width);
+    };
+    
+    @Override
+    protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return content == null ? 0 : content.maxWidth(height);
+    };
+    
+    @Override
+    protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return content == null ? 0 : content.maxHeight(width);
+    };
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/skin/PlusMinusSliderSkin.java b/controlsfx/src/main/java/impl/org/controlsfx/skin/PlusMinusSliderSkin.java
new file mode 100644
index 0000000..f5bf821
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/skin/PlusMinusSliderSkin.java
@@ -0,0 +1,160 @@
+/**
+ * Copyright (c) 2014, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.skin;
+
+import static javafx.scene.input.MouseEvent.MOUSE_PRESSED;
+import static javafx.scene.input.MouseEvent.MOUSE_RELEASED;
+import javafx.animation.AnimationTimer;
+import javafx.animation.KeyFrame;
+import javafx.animation.KeyValue;
+import javafx.animation.Timeline;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.event.EventHandler;
+import javafx.geometry.Orientation;
+import javafx.scene.control.SkinBase;
+import javafx.scene.control.Slider;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.Region;
+import javafx.util.Duration;
+
+import org.controlsfx.control.PlusMinusSlider;
+import org.controlsfx.control.PlusMinusSlider.PlusMinusEvent;
+
+public class PlusMinusSliderSkin extends SkinBase<PlusMinusSlider> {
+    
+	private SliderReader reader;
+
+	private Slider slider;
+
+	private Region plusRegion;
+
+	private Region minusRegion;
+
+	private BorderPane borderPane;
+
+	public PlusMinusSliderSkin(PlusMinusSlider adjuster) {
+		super(adjuster);
+
+		/*
+		 * We are not supporting any key events, yet. Adding this filter makes
+		 * sure the user doesn't use the standard key bindings of the slider. In
+		 * that case the thumb would not move itself back automatically (e.g.
+		 * after pressing "arrow right").
+		 */
+		adjuster.addEventFilter(KeyEvent.ANY, new EventHandler<KeyEvent>() {
+			@Override
+			public void handle(KeyEvent event) {
+				event.consume();
+			}
+		});
+
+		slider = new Slider(-1, 1, 0);
+
+		slider.valueProperty().addListener(new ChangeListener<Number>() {
+			@Override
+			public void changed(ObservableValue<? extends Number> observable,
+					Number oldValue, Number newValue) {
+				getSkinnable().getProperties().put("plusminusslidervalue", //$NON-NLS-1$
+						newValue.doubleValue());
+			}
+		});
+
+		slider.orientationProperty().bind(adjuster.orientationProperty());
+
+		slider.addEventHandler(MOUSE_PRESSED, new EventHandler<MouseEvent>() {
+
+			@Override
+			public void handle(MouseEvent evt) {
+				reader = new SliderReader();
+				reader.start();
+			}
+		});
+
+		slider.addEventHandler(MOUSE_RELEASED, new EventHandler<MouseEvent>() {
+
+			@Override
+			public void handle(MouseEvent evt) {
+				if (reader != null) {
+					reader.stop();
+				}
+
+				KeyValue keyValue = new KeyValue(slider.valueProperty(), 0);
+				KeyFrame keyFrame = new KeyFrame(Duration.millis(100), keyValue);
+				Timeline timeline = new Timeline(keyFrame);
+				timeline.play();
+			}
+		});
+
+		plusRegion = new Region();
+		plusRegion.getStyleClass().add("adjust-plus"); //$NON-NLS-1$
+
+		minusRegion = new Region();
+		minusRegion.getStyleClass().add("adjust-minus"); //$NON-NLS-1$
+
+		borderPane = new BorderPane();
+
+		updateLayout(adjuster.getOrientation());
+
+		getChildren().add(borderPane);
+
+		adjuster.orientationProperty().addListener((observable, oldValue, newValue) -> updateLayout(newValue));
+	}
+
+	private void updateLayout(Orientation orientation) {
+		borderPane.getChildren().clear();
+
+		switch (orientation) {
+		case HORIZONTAL:
+			borderPane.setLeft(minusRegion);
+			borderPane.setCenter(slider);
+			borderPane.setRight(plusRegion);
+			break;
+		case VERTICAL:
+			borderPane.setTop(plusRegion);
+			borderPane.setCenter(slider);
+			borderPane.setBottom(minusRegion);
+			break;
+		}
+	}
+
+	class SliderReader extends AnimationTimer {
+		private long lastTime = System.currentTimeMillis();
+
+		@Override
+		public void handle(long now) {
+			// max speed: 100 hundred times per second
+			if (now - lastTime > 10000000) {
+				lastTime = now;
+				slider.fireEvent(new PlusMinusEvent(slider, slider,
+						PlusMinusEvent.VALUE_CHANGED, slider.getValue()));
+			}
+		}
+	}
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/skin/PopOverSkin.java b/controlsfx/src/main/java/impl/org/controlsfx/skin/PopOverSkin.java
new file mode 100644
index 0000000..0d18a86
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/skin/PopOverSkin.java
@@ -0,0 +1,717 @@
+/**
+ * Copyright (c) 2013 - 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.skin;
+
+import static java.lang.Double.MAX_VALUE;
+import static javafx.geometry.Pos.CENTER_LEFT;
+import static javafx.scene.control.ContentDisplay.GRAPHIC_ONLY;
+import static javafx.scene.paint.Color.YELLOW;
+import static org.controlsfx.control.PopOver.ArrowLocation.BOTTOM_CENTER;
+import static org.controlsfx.control.PopOver.ArrowLocation.BOTTOM_LEFT;
+import static org.controlsfx.control.PopOver.ArrowLocation.BOTTOM_RIGHT;
+import static org.controlsfx.control.PopOver.ArrowLocation.LEFT_BOTTOM;
+import static org.controlsfx.control.PopOver.ArrowLocation.LEFT_CENTER;
+import static org.controlsfx.control.PopOver.ArrowLocation.LEFT_TOP;
+import static org.controlsfx.control.PopOver.ArrowLocation.RIGHT_BOTTOM;
+import static org.controlsfx.control.PopOver.ArrowLocation.RIGHT_CENTER;
+import static org.controlsfx.control.PopOver.ArrowLocation.RIGHT_TOP;
+import static org.controlsfx.control.PopOver.ArrowLocation.TOP_CENTER;
+import static org.controlsfx.control.PopOver.ArrowLocation.TOP_LEFT;
+import static org.controlsfx.control.PopOver.ArrowLocation.TOP_RIGHT;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.controlsfx.control.PopOver;
+import org.controlsfx.control.PopOver.ArrowLocation;
+
+import javafx.beans.InvalidationListener;
+import javafx.beans.binding.Bindings;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.event.EventHandler;
+import javafx.geometry.Point2D;
+import javafx.geometry.Pos;
+import javafx.scene.Group;
+import javafx.scene.Node;
+import javafx.scene.control.Label;
+import javafx.scene.control.Skin;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.StackPane;
+import javafx.scene.shape.Circle;
+import javafx.scene.shape.HLineTo;
+import javafx.scene.shape.Line;
+import javafx.scene.shape.LineTo;
+import javafx.scene.shape.MoveTo;
+import javafx.scene.shape.Path;
+import javafx.scene.shape.PathElement;
+import javafx.scene.shape.QuadCurveTo;
+import javafx.scene.shape.VLineTo;
+import javafx.stage.Window;
+
+public class PopOverSkin implements Skin<PopOver> {
+
+    private static final String DETACHED_STYLE_CLASS = "detached"; //$NON-NLS-1$
+
+    private double xOffset;
+    private double yOffset;
+
+    private boolean tornOff;
+
+    private Label title;
+    private Label closeIcon;
+
+    private Path path;
+    private Path clip;
+
+    private BorderPane content;
+    private StackPane titlePane;
+    private StackPane stackPane;
+
+    private Point2D dragStartLocation;
+
+    private PopOver popOver;
+
+    public PopOverSkin(final PopOver popOver) {
+
+        this.popOver = popOver;
+
+        stackPane = popOver.getRoot();
+        stackPane.setPickOnBounds(false);
+
+        Bindings.bindContent(stackPane.getStyleClass(), popOver.getStyleClass());
+
+        /*
+         * The min width and height equal 2 * corner radius + 2 * arrow indent +
+         * 2 * arrow size.
+         */
+        stackPane.minWidthProperty().bind(
+                Bindings.add(Bindings.multiply(2, popOver.arrowSizeProperty()),
+                        Bindings.add(
+                                Bindings.multiply(2,
+                                        popOver.cornerRadiusProperty()),
+                                Bindings.multiply(2,
+                                        popOver.arrowIndentProperty()))));
+
+        stackPane.minHeightProperty().bind(stackPane.minWidthProperty());
+
+        title = new Label();
+        title.textProperty().bind(popOver.titleProperty());
+        title.setMaxSize(MAX_VALUE, MAX_VALUE);
+        title.setAlignment(Pos.CENTER);
+        title.getStyleClass().add("text"); //$NON-NLS-1$
+
+        closeIcon = new Label();
+        closeIcon.setGraphic(createCloseIcon());
+        closeIcon.setMaxSize(MAX_VALUE, MAX_VALUE);
+        closeIcon.setContentDisplay(GRAPHIC_ONLY);
+        closeIcon.visibleProperty().bind(popOver.detachedProperty().or(popOver.headerAlwaysVisibleProperty()));
+        closeIcon.getStyleClass().add("icon"); //$NON-NLS-1$
+        closeIcon.setAlignment(CENTER_LEFT);
+        closeIcon.getGraphic().setOnMouseClicked(evt -> popOver.hide());
+
+        titlePane = new StackPane();
+        titlePane.getChildren().add(title);
+        titlePane.getChildren().add(closeIcon);
+        titlePane.getStyleClass().add("title"); //$NON-NLS-1$
+
+        content = new BorderPane();
+        content.setCenter(popOver.getContentNode());
+        content.getStyleClass().add("content"); //$NON-NLS-1$
+
+        if (popOver.isDetached() || popOver.isHeaderAlwaysVisible()) {
+            content.setTop(titlePane);
+        }
+
+        if (popOver.isDetached()) {
+            popOver.getStyleClass().add(DETACHED_STYLE_CLASS);
+            content.getStyleClass().add(DETACHED_STYLE_CLASS);
+        }
+
+        popOver.headerAlwaysVisibleProperty().addListener((o, oV, isVisible) -> {
+            if (isVisible) {
+                content.setTop(titlePane);
+            } else if (!popOver.isDetached()) {
+                content.setTop(null);
+            }
+        });
+
+        InvalidationListener updatePathListener = observable -> updatePath();
+        getPopupWindow().xProperty().addListener(updatePathListener);
+        getPopupWindow().yProperty().addListener(updatePathListener);
+        popOver.arrowLocationProperty().addListener(updatePathListener);
+        popOver.contentNodeProperty().addListener(
+                (value, oldContent, newContent) -> content
+                        .setCenter(newContent));
+        popOver.detachedProperty()
+                .addListener((value, oldDetached, newDetached) -> {
+
+                    if (newDetached) {
+                        popOver.getStyleClass().add(DETACHED_STYLE_CLASS);
+                        content.getStyleClass().add(DETACHED_STYLE_CLASS);
+                        content.setTop(titlePane);
+
+                        switch (getSkinnable().getArrowLocation()) {
+                        case LEFT_TOP:
+                        case LEFT_CENTER:
+                        case LEFT_BOTTOM:
+                            popOver.setAnchorX(
+                                    popOver.getAnchorX() + popOver.getArrowSize());
+                            break;
+                        case TOP_LEFT:
+                        case TOP_CENTER:
+                        case TOP_RIGHT:
+                            popOver.setAnchorY(
+                                    popOver.getAnchorY() + popOver.getArrowSize());
+                            break;
+                        default:
+                            break;
+                        }
+                    } else {
+                        popOver.getStyleClass().remove(DETACHED_STYLE_CLASS);
+                        content.getStyleClass().remove(DETACHED_STYLE_CLASS);
+
+                        if (!popOver.isHeaderAlwaysVisible()) {
+                            content.setTop(null);
+                        }
+                    }
+
+                    popOver.sizeToScene();
+
+                    updatePath();
+                });
+
+        path = new Path();
+        path.getStyleClass().add("border"); //$NON-NLS-1$
+        path.setManaged(false);
+
+        clip = new Path();
+
+        /*
+         * The clip is a path and the path has to be filled with a color.
+         * Otherwise clipping will not work.
+         */
+        clip.setFill(YELLOW);
+
+        createPathElements();
+        updatePath();
+
+        final EventHandler<MouseEvent> mousePressedHandler = evt -> {
+            if (popOver.isDetachable() || popOver.isDetached()) {
+                tornOff = false;
+
+                xOffset = evt.getScreenX();
+                yOffset = evt.getScreenY();
+
+                dragStartLocation = new Point2D(xOffset, yOffset);
+            }
+        };
+
+        final EventHandler<MouseEvent> mouseReleasedHandler = evt -> {
+            if (tornOff && !getSkinnable().isDetached()) {
+                tornOff = false;
+                getSkinnable().detach();
+            }
+        };
+
+        final EventHandler<MouseEvent> mouseDragHandler = evt -> {
+            if (popOver.isDetachable() || popOver.isDetached()) {
+                double deltaX = evt.getScreenX() - xOffset;
+                double deltaY = evt.getScreenY() - yOffset;
+
+                Window window = getSkinnable().getScene().getWindow();
+
+                window.setX(window.getX() + deltaX);
+                window.setY(window.getY() + deltaY);
+
+                xOffset = evt.getScreenX();
+                yOffset = evt.getScreenY();
+
+                if (dragStartLocation.distance(xOffset, yOffset) > 20) {
+                    tornOff = true;
+                    updatePath();
+                } else if (tornOff) {
+                    tornOff = false;
+                    updatePath();
+                }
+            }
+        };
+
+        stackPane.setOnMousePressed(mousePressedHandler);
+        stackPane.setOnMouseDragged(mouseDragHandler);
+        stackPane.setOnMouseReleased(mouseReleasedHandler);
+
+        stackPane.getChildren().add(path);
+        stackPane.getChildren().add(content);
+
+        content.setClip(clip);
+    }
+
+    @Override
+    public Node getNode() {
+        return stackPane;
+    }
+
+    @Override
+    public PopOver getSkinnable() {
+        return popOver;
+    }
+
+    @Override
+    public void dispose() {
+    }
+
+    private Node createCloseIcon() {
+        Group group = new Group();
+        group.getStyleClass().add("graphics"); //$NON-NLS-1$
+
+        Circle circle = new Circle();
+        circle.getStyleClass().add("circle"); //$NON-NLS-1$
+        circle.setRadius(6);
+        circle.setCenterX(6);
+        circle.setCenterY(6);
+        group.getChildren().add(circle);
+
+        Line line1 = new Line();
+        line1.getStyleClass().add("line"); //$NON-NLS-1$
+        line1.setStartX(4);
+        line1.setStartY(4);
+        line1.setEndX(8);
+        line1.setEndY(8);
+        group.getChildren().add(line1);
+
+        Line line2 = new Line();
+        line2.getStyleClass().add("line"); //$NON-NLS-1$
+        line2.setStartX(8);
+        line2.setStartY(4);
+        line2.setEndX(4);
+        line2.setEndY(8);
+        group.getChildren().add(line2);
+
+        return group;
+    }
+
+    private MoveTo moveTo;
+
+    private QuadCurveTo topCurveTo, rightCurveTo, bottomCurveTo, leftCurveTo;
+
+    private HLineTo lineBTop, lineETop, lineHTop, lineKTop;
+    private LineTo lineCTop, lineDTop, lineFTop, lineGTop, lineITop, lineJTop;
+
+    private VLineTo lineBRight, lineERight, lineHRight, lineKRight;
+    private LineTo lineCRight, lineDRight, lineFRight, lineGRight, lineIRight,
+            lineJRight;
+
+    private HLineTo lineBBottom, lineEBottom, lineHBottom, lineKBottom;
+    private LineTo lineCBottom, lineDBottom, lineFBottom, lineGBottom,
+            lineIBottom, lineJBottom;
+
+    private VLineTo lineBLeft, lineELeft, lineHLeft, lineKLeft;
+    private LineTo lineCLeft, lineDLeft, lineFLeft, lineGLeft, lineILeft,
+            lineJLeft;
+
+    private void createPathElements() {
+        DoubleProperty centerYProperty = new SimpleDoubleProperty();
+        DoubleProperty centerXProperty = new SimpleDoubleProperty();
+
+        DoubleProperty leftEdgeProperty = new SimpleDoubleProperty();
+        DoubleProperty leftEdgePlusRadiusProperty = new SimpleDoubleProperty();
+
+        DoubleProperty topEdgeProperty = new SimpleDoubleProperty();
+        DoubleProperty topEdgePlusRadiusProperty = new SimpleDoubleProperty();
+
+        DoubleProperty rightEdgeProperty = new SimpleDoubleProperty();
+        DoubleProperty rightEdgeMinusRadiusProperty = new SimpleDoubleProperty();
+
+        DoubleProperty bottomEdgeProperty = new SimpleDoubleProperty();
+        DoubleProperty bottomEdgeMinusRadiusProperty = new SimpleDoubleProperty();
+
+        DoubleProperty cornerProperty = getSkinnable().cornerRadiusProperty();
+
+        DoubleProperty arrowSizeProperty = getSkinnable().arrowSizeProperty();
+        DoubleProperty arrowIndentProperty = getSkinnable()
+                .arrowIndentProperty();
+
+        centerYProperty.bind(Bindings.divide(stackPane.heightProperty(), 2));
+        centerXProperty.bind(Bindings.divide(stackPane.widthProperty(), 2));
+
+        leftEdgePlusRadiusProperty.bind(Bindings.add(leftEdgeProperty,
+                getSkinnable().cornerRadiusProperty()));
+
+        topEdgePlusRadiusProperty.bind(Bindings.add(topEdgeProperty,
+                getSkinnable().cornerRadiusProperty()));
+
+        rightEdgeProperty.bind(stackPane.widthProperty());
+        rightEdgeMinusRadiusProperty.bind(Bindings.subtract(rightEdgeProperty,
+                getSkinnable().cornerRadiusProperty()));
+
+        bottomEdgeProperty.bind(stackPane.heightProperty());
+        bottomEdgeMinusRadiusProperty.bind(Bindings.subtract(
+                bottomEdgeProperty, getSkinnable().cornerRadiusProperty()));
+
+        // INIT
+        moveTo = new MoveTo();
+        moveTo.xProperty().bind(leftEdgePlusRadiusProperty);
+        moveTo.yProperty().bind(topEdgeProperty);
+
+        //
+        // TOP EDGE
+        //
+        lineBTop = new HLineTo();
+        lineBTop.xProperty().bind(
+                Bindings.add(leftEdgePlusRadiusProperty, arrowIndentProperty));
+
+        lineCTop = new LineTo();
+        lineCTop.xProperty().bind(
+                Bindings.add(lineBTop.xProperty(), arrowSizeProperty));
+        lineCTop.yProperty().bind(
+                Bindings.subtract(topEdgeProperty, arrowSizeProperty));
+
+        lineDTop = new LineTo();
+        lineDTop.xProperty().bind(
+                Bindings.add(lineCTop.xProperty(), arrowSizeProperty));
+        lineDTop.yProperty().bind(topEdgeProperty);
+
+        lineETop = new HLineTo();
+        lineETop.xProperty().bind(
+                Bindings.subtract(centerXProperty, arrowSizeProperty));
+
+        lineFTop = new LineTo();
+        lineFTop.xProperty().bind(centerXProperty);
+        lineFTop.yProperty().bind(
+                Bindings.subtract(topEdgeProperty, arrowSizeProperty));
+
+        lineGTop = new LineTo();
+        lineGTop.xProperty().bind(
+                Bindings.add(centerXProperty, arrowSizeProperty));
+        lineGTop.yProperty().bind(topEdgeProperty);
+
+        lineHTop = new HLineTo();
+        lineHTop.xProperty().bind(
+                Bindings.subtract(Bindings.subtract(
+                        rightEdgeMinusRadiusProperty, arrowIndentProperty),
+                        Bindings.multiply(arrowSizeProperty, 2)));
+
+        lineITop = new LineTo();
+        lineITop.xProperty().bind(
+                Bindings.subtract(Bindings.subtract(
+                        rightEdgeMinusRadiusProperty, arrowIndentProperty),
+                        arrowSizeProperty));
+        lineITop.yProperty().bind(
+                Bindings.subtract(topEdgeProperty, arrowSizeProperty));
+
+        lineJTop = new LineTo();
+        lineJTop.xProperty().bind(
+                Bindings.subtract(rightEdgeMinusRadiusProperty,
+                        arrowIndentProperty));
+        lineJTop.yProperty().bind(topEdgeProperty);
+
+        lineKTop = new HLineTo();
+        lineKTop.xProperty().bind(rightEdgeMinusRadiusProperty);
+
+        //
+        // RIGHT EDGE
+        //
+        rightCurveTo = new QuadCurveTo();
+        rightCurveTo.xProperty().bind(rightEdgeProperty);
+        rightCurveTo.yProperty().bind(
+                Bindings.add(topEdgeProperty, cornerProperty));
+        rightCurveTo.controlXProperty().bind(rightEdgeProperty);
+        rightCurveTo.controlYProperty().bind(topEdgeProperty);
+
+        lineBRight = new VLineTo();
+        lineBRight.yProperty().bind(
+                Bindings.add(topEdgePlusRadiusProperty, arrowIndentProperty));
+
+        lineCRight = new LineTo();
+        lineCRight.xProperty().bind(
+                Bindings.add(rightEdgeProperty, arrowSizeProperty));
+        lineCRight.yProperty().bind(
+                Bindings.add(lineBRight.yProperty(), arrowSizeProperty));
+
+        lineDRight = new LineTo();
+        lineDRight.xProperty().bind(rightEdgeProperty);
+        lineDRight.yProperty().bind(
+                Bindings.add(lineCRight.yProperty(), arrowSizeProperty));
+
+        lineERight = new VLineTo();
+        lineERight.yProperty().bind(
+                Bindings.subtract(centerYProperty, arrowSizeProperty));
+
+        lineFRight = new LineTo();
+        lineFRight.xProperty().bind(
+                Bindings.add(rightEdgeProperty, arrowSizeProperty));
+        lineFRight.yProperty().bind(centerYProperty);
+
+        lineGRight = new LineTo();
+        lineGRight.xProperty().bind(rightEdgeProperty);
+        lineGRight.yProperty().bind(
+                Bindings.add(centerYProperty, arrowSizeProperty));
+
+        lineHRight = new VLineTo();
+        lineHRight.yProperty().bind(
+                Bindings.subtract(Bindings.subtract(
+                        bottomEdgeMinusRadiusProperty, arrowIndentProperty),
+                        Bindings.multiply(arrowSizeProperty, 2)));
+
+        lineIRight = new LineTo();
+        lineIRight.xProperty().bind(
+                Bindings.add(rightEdgeProperty, arrowSizeProperty));
+        lineIRight.yProperty().bind(
+                Bindings.subtract(Bindings.subtract(
+                        bottomEdgeMinusRadiusProperty, arrowIndentProperty),
+                        arrowSizeProperty));
+
+        lineJRight = new LineTo();
+        lineJRight.xProperty().bind(rightEdgeProperty);
+        lineJRight.yProperty().bind(
+                Bindings.subtract(bottomEdgeMinusRadiusProperty,
+                        arrowIndentProperty));
+
+        lineKRight = new VLineTo();
+        lineKRight.yProperty().bind(bottomEdgeMinusRadiusProperty);
+
+        //
+        // BOTTOM EDGE
+        //
+
+        bottomCurveTo = new QuadCurveTo();
+        bottomCurveTo.xProperty().bind(rightEdgeMinusRadiusProperty);
+        bottomCurveTo.yProperty().bind(bottomEdgeProperty);
+        bottomCurveTo.controlXProperty().bind(rightEdgeProperty);
+        bottomCurveTo.controlYProperty().bind(bottomEdgeProperty);
+
+        lineBBottom = new HLineTo();
+        lineBBottom.xProperty().bind(
+                Bindings.subtract(rightEdgeMinusRadiusProperty,
+                        arrowIndentProperty));
+
+        lineCBottom = new LineTo();
+        lineCBottom.xProperty().bind(
+                Bindings.subtract(lineBBottom.xProperty(), arrowSizeProperty));
+        lineCBottom.yProperty().bind(
+                Bindings.add(bottomEdgeProperty, arrowSizeProperty));
+
+        lineDBottom = new LineTo();
+        lineDBottom.xProperty().bind(
+                Bindings.subtract(lineCBottom.xProperty(), arrowSizeProperty));
+        lineDBottom.yProperty().bind(bottomEdgeProperty);
+
+        lineEBottom = new HLineTo();
+        lineEBottom.xProperty().bind(
+                Bindings.add(centerXProperty, arrowSizeProperty));
+
+        lineFBottom = new LineTo();
+        lineFBottom.xProperty().bind(centerXProperty);
+        lineFBottom.yProperty().bind(
+                Bindings.add(bottomEdgeProperty, arrowSizeProperty));
+
+        lineGBottom = new LineTo();
+        lineGBottom.xProperty().bind(
+                Bindings.subtract(centerXProperty, arrowSizeProperty));
+        lineGBottom.yProperty().bind(bottomEdgeProperty);
+
+        lineHBottom = new HLineTo();
+        lineHBottom.xProperty().bind(
+                Bindings.add(Bindings.add(leftEdgePlusRadiusProperty,
+                        arrowIndentProperty), Bindings.multiply(
+                        arrowSizeProperty, 2)));
+
+        lineIBottom = new LineTo();
+        lineIBottom.xProperty().bind(
+                Bindings.add(Bindings.add(leftEdgePlusRadiusProperty,
+                        arrowIndentProperty), arrowSizeProperty));
+        lineIBottom.yProperty().bind(
+                Bindings.add(bottomEdgeProperty, arrowSizeProperty));
+
+        lineJBottom = new LineTo();
+        lineJBottom.xProperty().bind(
+                Bindings.add(leftEdgePlusRadiusProperty, arrowIndentProperty));
+        lineJBottom.yProperty().bind(bottomEdgeProperty);
+
+        lineKBottom = new HLineTo();
+        lineKBottom.xProperty().bind(leftEdgePlusRadiusProperty);
+
+        //
+        // LEFT EDGE
+        //
+        leftCurveTo = new QuadCurveTo();
+        leftCurveTo.xProperty().bind(leftEdgeProperty);
+        leftCurveTo.yProperty().bind(
+                Bindings.subtract(bottomEdgeProperty, cornerProperty));
+        leftCurveTo.controlXProperty().bind(leftEdgeProperty);
+        leftCurveTo.controlYProperty().bind(bottomEdgeProperty);
+
+        lineBLeft = new VLineTo();
+        lineBLeft.yProperty().bind(
+                Bindings.subtract(bottomEdgeMinusRadiusProperty,
+                        arrowIndentProperty));
+
+        lineCLeft = new LineTo();
+        lineCLeft.xProperty().bind(
+                Bindings.subtract(leftEdgeProperty, arrowSizeProperty));
+        lineCLeft.yProperty().bind(
+                Bindings.subtract(lineBLeft.yProperty(), arrowSizeProperty));
+
+        lineDLeft = new LineTo();
+        lineDLeft.xProperty().bind(leftEdgeProperty);
+        lineDLeft.yProperty().bind(
+                Bindings.subtract(lineCLeft.yProperty(), arrowSizeProperty));
+
+        lineELeft = new VLineTo();
+        lineELeft.yProperty().bind(
+                Bindings.add(centerYProperty, arrowSizeProperty));
+
+        lineFLeft = new LineTo();
+        lineFLeft.xProperty().bind(
+                Bindings.subtract(leftEdgeProperty, arrowSizeProperty));
+        lineFLeft.yProperty().bind(centerYProperty);
+
+        lineGLeft = new LineTo();
+        lineGLeft.xProperty().bind(leftEdgeProperty);
+        lineGLeft.yProperty().bind(
+                Bindings.subtract(centerYProperty, arrowSizeProperty));
+
+        lineHLeft = new VLineTo();
+        lineHLeft.yProperty().bind(
+                Bindings.add(Bindings.add(topEdgePlusRadiusProperty,
+                        arrowIndentProperty), Bindings.multiply(
+                        arrowSizeProperty, 2)));
+
+        lineILeft = new LineTo();
+        lineILeft.xProperty().bind(
+                Bindings.subtract(leftEdgeProperty, arrowSizeProperty));
+        lineILeft.yProperty().bind(
+                Bindings.add(Bindings.add(topEdgePlusRadiusProperty,
+                        arrowIndentProperty), arrowSizeProperty));
+
+        lineJLeft = new LineTo();
+        lineJLeft.xProperty().bind(leftEdgeProperty);
+        lineJLeft.yProperty().bind(
+                Bindings.add(topEdgePlusRadiusProperty, arrowIndentProperty));
+
+        lineKLeft = new VLineTo();
+        lineKLeft.yProperty().bind(topEdgePlusRadiusProperty);
+
+        topCurveTo = new QuadCurveTo();
+        topCurveTo.xProperty().bind(leftEdgePlusRadiusProperty);
+        topCurveTo.yProperty().bind(topEdgeProperty);
+        topCurveTo.controlXProperty().bind(leftEdgeProperty);
+        topCurveTo.controlYProperty().bind(topEdgeProperty);
+    }
+
+    private Window getPopupWindow() {
+        return getSkinnable().getScene().getWindow();
+    }
+
+    private boolean showArrow(ArrowLocation location) {
+        ArrowLocation arrowLocation = getSkinnable().getArrowLocation();
+        return location.equals(arrowLocation) && !getSkinnable().isDetached()
+                && !tornOff;
+    }
+
+    private void updatePath() {
+        List<PathElement> elements = new ArrayList<>();
+        elements.add(moveTo);
+
+        if (showArrow(TOP_LEFT)) {
+            elements.add(lineBTop);
+            elements.add(lineCTop);
+            elements.add(lineDTop);
+        }
+        if (showArrow(TOP_CENTER)) {
+            elements.add(lineETop);
+            elements.add(lineFTop);
+            elements.add(lineGTop);
+        }
+        if (showArrow(TOP_RIGHT)) {
+            elements.add(lineHTop);
+            elements.add(lineITop);
+            elements.add(lineJTop);
+        }
+        elements.add(lineKTop);
+        elements.add(rightCurveTo);
+
+        if (showArrow(RIGHT_TOP)) {
+            elements.add(lineBRight);
+            elements.add(lineCRight);
+            elements.add(lineDRight);
+        }
+        if (showArrow(RIGHT_CENTER)) {
+            elements.add(lineERight);
+            elements.add(lineFRight);
+            elements.add(lineGRight);
+        }
+        if (showArrow(RIGHT_BOTTOM)) {
+            elements.add(lineHRight);
+            elements.add(lineIRight);
+            elements.add(lineJRight);
+        }
+        elements.add(lineKRight);
+        elements.add(bottomCurveTo);
+
+        if (showArrow(BOTTOM_RIGHT)) {
+            elements.add(lineBBottom);
+            elements.add(lineCBottom);
+            elements.add(lineDBottom);
+        }
+        if (showArrow(BOTTOM_CENTER)) {
+            elements.add(lineEBottom);
+            elements.add(lineFBottom);
+            elements.add(lineGBottom);
+        }
+        if (showArrow(BOTTOM_LEFT)) {
+            elements.add(lineHBottom);
+            elements.add(lineIBottom);
+            elements.add(lineJBottom);
+        }
+        elements.add(lineKBottom);
+        elements.add(leftCurveTo);
+
+        if (showArrow(LEFT_BOTTOM)) {
+            elements.add(lineBLeft);
+            elements.add(lineCLeft);
+            elements.add(lineDLeft);
+        }
+        if (showArrow(LEFT_CENTER)) {
+            elements.add(lineELeft);
+            elements.add(lineFLeft);
+            elements.add(lineGLeft);
+        }
+        if (showArrow(LEFT_TOP)) {
+            elements.add(lineHLeft);
+            elements.add(lineILeft);
+            elements.add(lineJLeft);
+        }
+        elements.add(lineKLeft);
+        elements.add(topCurveTo);
+
+        path.getElements().setAll(elements);
+        clip.getElements().setAll(elements);
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/skin/PropertySheetSkin.java b/controlsfx/src/main/java/impl/org/controlsfx/skin/PropertySheetSkin.java
new file mode 100644
index 0000000..f050288
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/skin/PropertySheetSkin.java
@@ -0,0 +1,350 @@
+/**
+ * Copyright (c) 2013, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.skin;
+
+import static impl.org.controlsfx.i18n.Localization.asKey;
+import static impl.org.controlsfx.i18n.Localization.localize;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+import javafx.beans.value.ObservableValue;
+import javafx.collections.ListChangeListener;
+import javafx.geometry.Insets;
+import javafx.scene.Node;
+import javafx.scene.control.Accordion;
+import javafx.scene.control.Label;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.control.TextField;
+import javafx.scene.control.TitledPane;
+import javafx.scene.control.ToolBar;
+import javafx.scene.control.Tooltip;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.Region;
+
+import org.controlsfx.control.PropertySheet;
+import org.controlsfx.control.PropertySheet.Item;
+import org.controlsfx.control.PropertySheet.Mode;
+import org.controlsfx.control.SegmentedButton;
+import org.controlsfx.control.action.Action;
+import org.controlsfx.control.action.ActionUtils;
+import org.controlsfx.control.textfield.TextFields;
+import org.controlsfx.property.editor.AbstractPropertyEditor;
+import org.controlsfx.property.editor.PropertyEditor;
+
+import com.sun.javafx.scene.control.behavior.BehaviorBase;
+import com.sun.javafx.scene.control.behavior.KeyBinding;
+import com.sun.javafx.scene.control.skin.BehaviorSkinBase;
+
+public class PropertySheetSkin extends BehaviorSkinBase<PropertySheet, BehaviorBase<PropertySheet>> {
+    
+    /**************************************************************************
+     * 
+     * Static fields
+     * 
+     **************************************************************************/
+
+    private static final int MIN_COLUMN_WIDTH = 100;
+    
+    /**************************************************************************
+     * 
+     * fields
+     * 
+     **************************************************************************/
+    
+    private final BorderPane content;
+    private final ScrollPane scroller;
+    private final ToolBar toolbar;
+    private final SegmentedButton modeButton = ActionUtils.createSegmentedButton(
+        new ActionChangeMode(Mode.NAME),
+        new ActionChangeMode(Mode.CATEGORY)
+    );
+    private final TextField searchField = TextFields.createClearableTextField();
+    
+    
+    /**************************************************************************
+     * 
+     * Constructors
+     * 
+     **************************************************************************/
+
+    public PropertySheetSkin(final PropertySheet control) {
+        super(control, new BehaviorBase<>(control, Collections.<KeyBinding> emptyList()));
+        
+        scroller = new ScrollPane();
+        scroller.setFitToWidth(true);
+        
+        toolbar = new ToolBar();
+        toolbar.managedProperty().bind(toolbar.visibleProperty());
+        toolbar.setFocusTraversable(true);
+        
+        // property sheet mode
+        modeButton.managedProperty().bind(modeButton.visibleProperty());
+        modeButton.getButtons().get(getSkinnable().modeProperty().get().ordinal()).setSelected(true);
+        toolbar.getItems().add(modeButton);
+        
+        // property sheet search
+        searchField.setPromptText( localize(asKey("property.sheet.search.field.prompt"))); //$NON-NLS-1$
+        searchField.setMinWidth(0);
+        HBox.setHgrow(searchField, Priority.SOMETIMES);
+        searchField.managedProperty().bind(searchField.visibleProperty());
+        toolbar.getItems().add(searchField);
+        
+        // layout controls
+        content = new BorderPane();
+        content.setTop(toolbar);
+        content.setCenter(scroller);
+        getChildren().add(content);
+              
+        
+        // setup listeners
+        registerChangeListener(control.modeProperty(), "MODE"); //$NON-NLS-1$
+        registerChangeListener(control.propertyEditorFactory(), "EDITOR-FACTORY"); //$NON-NLS-1$
+        registerChangeListener(control.titleFilter(), "FILTER"); //$NON-NLS-1$
+        registerChangeListener(searchField.textProperty(), "FILTER-UI"); //$NON-NLS-1$
+        registerChangeListener(control.modeSwitcherVisibleProperty(), "TOOLBAR-MODE"); //$NON-NLS-1$
+        registerChangeListener(control.searchBoxVisibleProperty(), "TOOLBAR-SEARCH"); //$NON-NLS-1$
+        
+        control.getItems().addListener((ListChangeListener<Item>) change -> refreshProperties());
+        
+        // initialize properly 
+        refreshProperties(); 
+        updateToolbar();
+    }
+
+
+    /**************************************************************************
+     * 
+     * Overriding public API
+     * 
+     **************************************************************************/
+
+    @Override protected void handleControlPropertyChanged(String p) {
+        super.handleControlPropertyChanged(p);
+        
+        if (p == "MODE" || p == "EDITOR-FACTORY" || p == "FILTER") { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+            refreshProperties();
+        } else if (p == "FILTER-UI") { //$NON-NLS-1$
+            getSkinnable().setTitleFilter(searchField.getText());
+        } else if (p == "TOOLBAR-MODE") { //$NON-NLS-1$
+            updateToolbar();
+        } else if (p == "TOOLBAR-SEARCH") { //$NON-NLS-1$
+            updateToolbar();
+        }
+    }
+    
+    @Override protected void layoutChildren(double x, double y, double w, double h) {
+        content.resizeRelocate(x, y, w, h);
+    }
+
+
+
+    /**************************************************************************
+     * 
+     * Implementation
+     * 
+     **************************************************************************/
+    
+    private void updateToolbar() {
+        modeButton.setVisible(getSkinnable().isModeSwitcherVisible());
+        searchField.setVisible(getSkinnable().isSearchBoxVisible());
+        
+        toolbar.setVisible(modeButton.isVisible() || searchField.isVisible());
+    }
+
+    private void refreshProperties() {
+        scroller.setContent(buildPropertySheetContainer());
+    }
+    
+    private Node buildPropertySheetContainer() {
+        switch( getSkinnable().modeProperty().get() ) {
+            case CATEGORY: {
+                // group by category
+                Map<String, List<Item>> categoryMap = new TreeMap<>();
+                for( Item p: getSkinnable().getItems()) {
+                    String category = p.getCategory();
+                    List<Item> list = categoryMap.get(category);
+                    if ( list == null ) {
+                        list = new ArrayList<>();
+                        categoryMap.put( category, list);
+                    }
+                    list.add(p);
+                }
+                
+                // create category-based accordion
+                Accordion accordion = new Accordion();
+                for( String category: categoryMap.keySet() ) {
+                	PropertyPane props = new PropertyPane( categoryMap.get(category));
+                	// Only show non-empty categories 
+                	if ( props.getChildrenUnmodifiable().size() > 0 ) {
+                       TitledPane pane = new TitledPane( category, props );
+                       pane.setExpanded(true);
+                       accordion.getPanes().add(pane);
+                    }
+                }
+                if ( accordion.getPanes().size() > 0 ) {
+                    accordion.setExpandedPane(accordion.getPanes().get(0));
+                }
+                return accordion;
+            }
+            
+            default: return new PropertyPane(getSkinnable().getItems());
+        }
+        
+    }
+
+    
+    /**************************************************************************
+     * 
+     * Support classes / enums
+     * 
+     **************************************************************************/
+    
+    private class ActionChangeMode extends Action {
+        
+    	private final Image CATEGORY_IMAGE = new Image(PropertySheetSkin.class.getResource("/org/controlsfx/control/format-indent-more.png").toExternalForm()); //$NON-NLS-1$
+    	private final Image NAME_IMAGE = new Image(PropertySheetSkin.class.getResource("/org/controlsfx/control/format-line-spacing-triple.png").toExternalForm()); //$NON-NLS-1$
+    	
+        public ActionChangeMode(PropertySheet.Mode mode) {
+            super(""); //$NON-NLS-1$
+            setEventHandler(ae -> getSkinnable().modeProperty().set(mode));
+            
+            if (mode == Mode.CATEGORY) {
+                setGraphic( new ImageView(CATEGORY_IMAGE));
+                setLongText(localize(asKey("property.sheet.group.mode.bycategory"))); //$NON-NLS-1$
+            } else if (mode == Mode.NAME) {
+                setGraphic(new ImageView(NAME_IMAGE));
+                setLongText(localize(asKey("property.sheet.group.mode.byname"))); //$NON-NLS-1$
+            } else {
+                setText("???"); //$NON-NLS-1$
+            }
+        }
+
+    }
+    
+    
+    private class PropertyPane extends GridPane {
+        
+        public PropertyPane( List<Item> properties ) {
+            this( properties, 0 );
+        }
+        
+        public PropertyPane( List<Item> properties, int nestingLevel ) {
+            setVgap(5);
+            setHgap(5);
+            setPadding(new Insets(5, 15, 5, 15 + nestingLevel*10 ));
+            getStyleClass().add("property-pane"); //$NON-NLS-1$
+            setItems(properties);
+//            setGridLinesVisible(true);
+        }
+        
+        public void setItems( List<Item> properties ) {
+            getChildren().clear();
+            
+            String filter = getSkinnable().titleFilter().get();
+            filter = filter == null? "": filter.trim().toLowerCase(); //$NON-NLS-1$
+
+            int row = 0;
+            
+            for (Item item : properties) {
+
+                // filter properties
+                String title = item.getName();
+               
+                if ( !filter.isEmpty() && title.toLowerCase().indexOf( filter ) < 0) continue;
+                
+                // setup property label
+                Label label = new Label(title);
+                label.setMinWidth(MIN_COLUMN_WIDTH);
+                
+                // show description as a tooltip
+                String description = item.getDescription();
+                if ( description != null && !description.trim().isEmpty()) {
+                    label.setTooltip(new Tooltip(description));
+                }
+                
+                add(label, 0, row);
+
+                // setup property editor
+                Node editor = getEditor(item);
+                
+                if (editor instanceof Region) {
+                    ((Region)editor).setMinWidth(MIN_COLUMN_WIDTH);
+                    ((Region)editor).setMaxWidth(Double.MAX_VALUE);
+                }
+                label.setLabelFor(editor);
+                add(editor, 1, row);
+                GridPane.setHgrow(editor, Priority.ALWAYS);
+                
+                //TODO add support for recursive properties
+                
+                row++;
+            }
+            
+        }
+        
+        @SuppressWarnings("unchecked")
+        private Node getEditor(Item item) {
+            @SuppressWarnings("rawtypes")
+            PropertyEditor editor = getSkinnable().getPropertyEditorFactory().call(item);
+            if (editor == null) {
+                editor = new AbstractPropertyEditor<Object, TextField>(item, new TextField(), true) {
+                    {
+                        getEditor().setEditable(false);
+                        getEditor().setDisable(true);
+                    }
+                    
+                    /**
+                     * {@inheritDoc}
+                     */
+                    @Override protected ObservableValue<Object> getObservableValue() {
+                        return (ObservableValue<Object>)(Object)getEditor().textProperty();
+                    }
+                    
+                    /**
+                     * {@inheritDoc}
+                     */
+                    @Override public void setValue(Object value) {
+                        getEditor().setText(value == null? "": value.toString()); //$NON-NLS-1$
+                    }
+                };
+            } else if (! item.isEditable()) {
+                editor.getEditor().setDisable(true);
+            }
+            editor.setValue(item.getValue());
+            return editor.getEditor();
+        }
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/skin/RangeSliderSkin.java b/controlsfx/src/main/java/impl/org/controlsfx/skin/RangeSliderSkin.java
new file mode 100644
index 0000000..2b32492
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/skin/RangeSliderSkin.java
@@ -0,0 +1,580 @@
+/**
+ * Copyright (c) 2013, 2016 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.skin;
+
+import static impl.org.controlsfx.behavior.RangeSliderBehavior.FocusedChild.HIGH_THUMB;
+import static impl.org.controlsfx.behavior.RangeSliderBehavior.FocusedChild.LOW_THUMB;
+import static impl.org.controlsfx.behavior.RangeSliderBehavior.FocusedChild.NONE;
+import static impl.org.controlsfx.behavior.RangeSliderBehavior.FocusedChild.RANGE_BAR;
+import impl.org.controlsfx.behavior.RangeSliderBehavior;
+import impl.org.controlsfx.behavior.RangeSliderBehavior.FocusedChild;
+import javafx.beans.binding.ObjectBinding;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.event.EventHandler;
+import javafx.geometry.Orientation;
+import javafx.geometry.Point2D;
+import javafx.geometry.Side;
+import javafx.scene.Cursor;
+import javafx.scene.chart.NumberAxis;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.StackPane;
+import javafx.util.Callback;
+
+import org.controlsfx.control.RangeSlider;
+
+import com.sun.javafx.scene.control.skin.BehaviorSkinBase;
+import com.sun.javafx.scene.traversal.Direction;
+import com.sun.javafx.scene.traversal.ParentTraversalEngine;
+
+public class RangeSliderSkin extends BehaviorSkinBase<RangeSlider, RangeSliderBehavior> {
+        
+    /** Track if slider is vertical/horizontal and cause re layout */
+    private NumberAxis tickLine = null;
+    private double trackToTickGap = 2;
+
+    private boolean showTickMarks;
+    private double thumbWidth;
+    private double thumbHeight;
+    
+    private Orientation orientation;
+
+    private StackPane track;
+    private double trackStart;
+    private double trackLength;
+    private double lowThumbPos;
+    private double rangeEnd;
+    private double rangeStart;
+    private ThumbPane lowThumb;
+    private ThumbPane highThumb;
+    private StackPane rangeBar; // the bar between the two thumbs, can be dragged
+    
+    // temp fields for mouse drag handling
+    private double preDragPos;          // used as a temp value for low and high thumbs
+    private Point2D preDragThumbPoint;  // in skin coordinates
+    
+    private FocusedChild currentFocus = LOW_THUMB;
+    
+    public RangeSliderSkin(final RangeSlider rangeSlider) {
+        super(rangeSlider, new RangeSliderBehavior(rangeSlider));
+        orientation = getSkinnable().getOrientation();
+        initFirstThumb();
+        initSecondThumb();
+        initRangeBar();
+        registerChangeListener(rangeSlider.lowValueProperty(), "LOW_VALUE"); //$NON-NLS-1$
+        registerChangeListener(rangeSlider.highValueProperty(), "HIGH_VALUE"); //$NON-NLS-1$
+        registerChangeListener(rangeSlider.minProperty(), "MIN"); //$NON-NLS-1$
+        registerChangeListener(rangeSlider.maxProperty(), "MAX"); //$NON-NLS-1$
+        registerChangeListener(rangeSlider.orientationProperty(), "ORIENTATION"); //$NON-NLS-1$
+        registerChangeListener(rangeSlider.showTickMarksProperty(), "SHOW_TICK_MARKS"); //$NON-NLS-1$
+        registerChangeListener(rangeSlider.showTickLabelsProperty(), "SHOW_TICK_LABELS"); //$NON-NLS-1$
+        registerChangeListener(rangeSlider.majorTickUnitProperty(), "MAJOR_TICK_UNIT"); //$NON-NLS-1$
+        registerChangeListener(rangeSlider.minorTickCountProperty(), "MINOR_TICK_COUNT"); //$NON-NLS-1$
+        lowThumb.focusedProperty().addListener(new ChangeListener<Boolean>() {
+            @Override public void changed(ObservableValue<? extends Boolean> ov, Boolean t, Boolean hasFocus) {
+                if (hasFocus) {
+                    currentFocus = LOW_THUMB;
+                }
+            }
+        });
+        highThumb.focusedProperty().addListener(new ChangeListener<Boolean>() {
+            @Override public void changed(ObservableValue<? extends Boolean> ov, Boolean t, Boolean hasFocus) {
+                if (hasFocus) {
+                    currentFocus = HIGH_THUMB;
+                }
+            }
+        });
+        rangeBar.focusedProperty().addListener(new ChangeListener<Boolean>() {
+            @Override public void changed(ObservableValue<? extends Boolean> ov, Boolean t, Boolean hasFocus) {
+                if (hasFocus) {
+                    currentFocus = RANGE_BAR;
+                }
+            }
+        });
+        rangeSlider.focusedProperty().addListener(new ChangeListener<Boolean>() {
+            @Override public void changed(ObservableValue<? extends Boolean> ov, Boolean t, Boolean hasFocus) {
+                if (hasFocus) {
+                    lowThumb.setFocus(true);
+                } else {
+                    lowThumb.setFocus(false);
+                    highThumb.setFocus(false);
+                    currentFocus = NONE;
+                }
+            }
+        });
+
+        EventHandler<KeyEvent> keyEventHandler = new EventHandler<KeyEvent>() {
+            @Override public void handle(KeyEvent event) {
+                if (KeyCode.TAB.equals(event.getCode())) {
+                    if (lowThumb.isFocused()) {
+                        if (event.isShiftDown()) {
+                            lowThumb.setFocus(false);
+                            new ParentTraversalEngine(rangeSlider).select(rangeSlider, Direction.PREVIOUS);
+                        } else {
+                            lowThumb.setFocus(false);
+                            highThumb.setFocus(true);
+                        }
+                        event.consume();
+                    } else if (highThumb.isFocused()) {
+                        if(event.isShiftDown()) {
+                            highThumb.setFocus(false);
+                            lowThumb.setFocus(true);
+                        } else {
+                            highThumb.setFocus(false);
+                            new ParentTraversalEngine(rangeSlider).select(rangeSlider, Direction.NEXT);
+                        }
+                        event.consume();
+                    }
+                }
+            }
+        };
+        getSkinnable().addEventHandler(KeyEvent.KEY_PRESSED, keyEventHandler);  
+        // set up a callback on the behavior to indicate which thumb is currently 
+        // selected (via enum).
+        getBehavior().setSelectedValue(new Callback<Void, FocusedChild>() {
+            @Override public FocusedChild call(Void v) {
+                return currentFocus;
+            }
+        });
+    }
+    
+    private void initFirstThumb() {
+        lowThumb = new ThumbPane();
+        lowThumb.getStyleClass().setAll("low-thumb"); //$NON-NLS-1$
+        lowThumb.setFocusTraversable(true);
+        track = new StackPane();
+        track.getStyleClass().setAll("track"); //$NON-NLS-1$
+
+        getChildren().clear();
+        getChildren().addAll(track, lowThumb);
+        setShowTickMarks(getSkinnable().isShowTickMarks(), getSkinnable().isShowTickLabels());
+        track.setOnMousePressed( new EventHandler<javafx.scene.input.MouseEvent>() {
+            @Override public void handle(javafx.scene.input.MouseEvent me) {
+                if (!lowThumb.isPressed() && !highThumb.isPressed()) {
+                    if (isHorizontal()) {
+                        getBehavior().trackPress(me, (me.getX() / trackLength));
+                    } else {
+                        getBehavior().trackPress(me, (me.getY() / trackLength));
+                    }
+                }
+            }
+        });
+
+        track.setOnMouseReleased( new EventHandler<javafx.scene.input.MouseEvent>() {
+            @Override public void handle(javafx.scene.input.MouseEvent me) {
+                 //Nothing being done with the second param in sliderBehavior
+                //So, passing a dummy value
+                getBehavior().trackRelease(me, 0.0f);
+            }
+        });
+
+        lowThumb.setOnMousePressed(new EventHandler<javafx.scene.input.MouseEvent>() {
+            @Override public void handle(javafx.scene.input.MouseEvent me) {
+                highThumb.setFocus(false);
+                lowThumb.setFocus(true);
+                getBehavior().lowThumbPressed(me, 0.0f);
+                preDragThumbPoint = lowThumb.localToParent(me.getX(), me.getY());
+                preDragPos = (getSkinnable().getLowValue() - getSkinnable().getMin()) /
+                        (getMaxMinusMinNoZero());
+            }
+        });
+
+        lowThumb.setOnMouseReleased(new EventHandler<javafx.scene.input.MouseEvent>() {
+            @Override public void handle(javafx.scene.input.MouseEvent me) {
+                getBehavior().lowThumbReleased(me);
+            }
+        });
+
+        lowThumb.setOnMouseDragged(new EventHandler<javafx.scene.input.MouseEvent>() {
+            @Override public void handle(javafx.scene.input.MouseEvent me) {
+                Point2D cur = lowThumb.localToParent(me.getX(), me.getY());
+                double dragPos = (isHorizontal())?
+                    cur.getX() - preDragThumbPoint.getX() : -(cur.getY() - preDragThumbPoint.getY());
+                getBehavior().lowThumbDragged(me, preDragPos + dragPos / trackLength);
+            }
+        });
+    }
+
+    private void initSecondThumb() {
+        highThumb = new ThumbPane();
+        highThumb.getStyleClass().setAll("high-thumb"); //$NON-NLS-1$
+        highThumb.setFocusTraversable(true);
+        if (!getChildren().contains(highThumb)) {
+            getChildren().add(highThumb);
+        }
+
+        highThumb.setOnMousePressed(new EventHandler<MouseEvent>() {
+            @Override public void handle(MouseEvent e) {
+                lowThumb.setFocus(false);
+                highThumb.setFocus(true);
+                ((RangeSliderBehavior) getBehavior()).highThumbPressed(e, 0.0D);
+                preDragThumbPoint = highThumb.localToParent(e.getX(), e.getY());
+                preDragPos = (((RangeSlider) getSkinnable()).getHighValue() - ((RangeSlider) getSkinnable()).getMin()) / 
+                            (getMaxMinusMinNoZero());
+            }
+        }
+        );
+        highThumb.setOnMouseReleased(new EventHandler<MouseEvent>() {
+            @Override public void handle(MouseEvent e) {
+                ((RangeSliderBehavior) getBehavior()).highThumbReleased(e);
+            }
+        }
+        );
+        highThumb.setOnMouseDragged(new EventHandler<MouseEvent>() {
+            @Override public void handle(MouseEvent e) {
+                boolean orientation = ((RangeSlider) getSkinnable()).getOrientation() == Orientation.HORIZONTAL;
+                double trackLength = orientation ? track.getWidth() : track.getHeight();
+
+                Point2D point2d = highThumb.localToParent(e.getX(), e.getY());
+                double d = ((RangeSlider) getSkinnable()).getOrientation() != Orientation.HORIZONTAL ? -(point2d.getY() - preDragThumbPoint.getY()) : point2d.getX() - preDragThumbPoint.getX();
+                ((RangeSliderBehavior) getBehavior()).highThumbDragged(e, preDragPos + d / trackLength);
+            }
+        });
+    }
+    
+    private void initRangeBar() {
+        rangeBar = new StackPane();
+        rangeBar.cursorProperty().bind(new ObjectBinding<Cursor>() {
+            { bind(rangeBar.hoverProperty()); }
+
+            @Override protected Cursor computeValue() {
+                return rangeBar.isHover() ? Cursor.HAND : Cursor.DEFAULT;
+            }
+        });
+        rangeBar.getStyleClass().setAll("range-bar"); //$NON-NLS-1$
+        
+        rangeBar.setOnMousePressed(new EventHandler<MouseEvent>() {
+            @Override public void handle(MouseEvent e) {
+                rangeBar.requestFocus();
+                preDragPos = isHorizontal() ? e.getX() : -e.getY();
+            }
+        });
+        
+        rangeBar.setOnMouseDragged(new EventHandler<MouseEvent>() {
+            @Override public void handle(MouseEvent e) {
+                double delta = (isHorizontal() ? e.getX() : -e.getY()) - preDragPos;
+                ((RangeSliderBehavior) getBehavior()).moveRange(delta);
+            }
+        });
+        
+         rangeBar.setOnMouseReleased(new EventHandler<MouseEvent>() {
+            @Override public void handle(MouseEvent e) {
+                ((RangeSliderBehavior) getBehavior()).confirmRange();
+            }
+        });
+        
+        getChildren().add(rangeBar);
+    }
+
+    /**
+     * When ticks or labels are changing of visibility, we compute the new
+     * visibility and add the necessary objets. After this method, we must be
+     * sure to add the high Thumb and the rangeBar.
+     *
+     * @param ticksVisible
+     * @param labelsVisible
+     */
+    private void setShowTickMarks(boolean ticksVisible, boolean labelsVisible) {
+        showTickMarks = (ticksVisible || labelsVisible);
+        RangeSlider rangeSlider = getSkinnable();
+        if (showTickMarks) {
+            if (tickLine == null) {
+                tickLine = new NumberAxis();
+                tickLine.tickLabelFormatterProperty().bind(getSkinnable().labelFormatterProperty());
+                tickLine.setAnimated(false);
+                tickLine.setAutoRanging(false);
+                tickLine.setSide(isHorizontal() ? Side.BOTTOM : Side.RIGHT);
+                tickLine.setUpperBound(rangeSlider.getMax());
+                tickLine.setLowerBound(rangeSlider.getMin());
+                tickLine.setTickUnit(rangeSlider.getMajorTickUnit());
+                tickLine.setTickMarkVisible(ticksVisible);
+                tickLine.setTickLabelsVisible(labelsVisible);
+                tickLine.setMinorTickVisible(ticksVisible);
+                // add 1 to the slider minor tick count since the axis draws one
+                // less minor ticks than the number given.
+                tickLine.setMinorTickCount(Math.max(rangeSlider.getMinorTickCount(),0) + 1);
+                getChildren().clear();
+                getChildren().addAll(tickLine, track, lowThumb);
+            } else {
+                tickLine.setTickLabelsVisible(labelsVisible);
+                tickLine.setTickMarkVisible(ticksVisible);
+                tickLine.setMinorTickVisible(ticksVisible);
+            }
+        } 
+        else  {
+            getChildren().clear();
+            getChildren().addAll(track, lowThumb);
+//            tickLine = null;
+        }
+
+        getSkinnable().requestLayout();
+    }
+
+    @Override protected void handleControlPropertyChanged(String p) {
+        super.handleControlPropertyChanged(p);
+        if ("ORIENTATION".equals(p)) { //$NON-NLS-1$
+            orientation = getSkinnable().getOrientation();
+            if (showTickMarks && tickLine != null) {
+                tickLine.setSide(isHorizontal() ? Side.BOTTOM : Side.RIGHT);
+            }
+            getSkinnable().requestLayout();
+        } else if ("MIN".equals(p) ) { //$NON-NLS-1$
+            if (showTickMarks && tickLine != null) {
+                tickLine.setLowerBound(getSkinnable().getMin());
+            }
+            getSkinnable().requestLayout();
+        } else if ("MAX".equals(p)) { //$NON-NLS-1$
+            if (showTickMarks && tickLine != null) {
+                tickLine.setUpperBound(getSkinnable().getMax());
+            }
+            getSkinnable().requestLayout();
+        } else if ("SHOW_TICK_MARKS".equals(p) || "SHOW_TICK_LABELS".equals(p)) { //$NON-NLS-1$ //$NON-NLS-2$
+            setShowTickMarks(getSkinnable().isShowTickMarks(), getSkinnable().isShowTickLabels());
+            if (!getChildren().contains(highThumb))
+                getChildren().add(highThumb);
+            if (!getChildren().contains(rangeBar))
+                getChildren().add(rangeBar);
+        }  else if ("MAJOR_TICK_UNIT".equals(p)) { //$NON-NLS-1$
+            if (tickLine != null) {
+                tickLine.setTickUnit(getSkinnable().getMajorTickUnit());
+                getSkinnable().requestLayout();
+            }
+        } else if ("MINOR_TICK_COUNT".equals(p)) { //$NON-NLS-1$
+            if (tickLine != null) {
+                tickLine.setMinorTickCount(Math.max(getSkinnable().getMinorTickCount(),0) + 1);
+                getSkinnable().requestLayout();
+            }
+        } else if ("LOW_VALUE".equals(p)) { //$NON-NLS-1$
+            positionLowThumb();
+            rangeBar.resizeRelocate(rangeStart, rangeBar.getLayoutY(), 
+                    rangeEnd - rangeStart, rangeBar.getHeight());
+        } else if ("HIGH_VALUE".equals(p)) { //$NON-NLS-1$
+            positionHighThumb();
+            rangeBar.resize(rangeEnd-rangeStart, rangeBar.getHeight());
+        }
+        super.handleControlPropertyChanged(p);
+    }
+    
+    /**
+     *
+     * @return the difference between max and min, but if they have the same
+     * value, 1 is returned instead of 0 because otherwise the division where it
+     * can be used will return Nan.
+     */
+    private double getMaxMinusMinNoZero() {
+        RangeSlider s = getSkinnable();
+        return s.getMax() - s.getMin() == 0 ? 1 : s.getMax() - s.getMin();
+    }
+    
+    /**
+     * Called when ever either min, max or lowValue changes, so lowthumb's layoutX, Y is recomputed.
+     */
+    private void positionLowThumb() {
+        RangeSlider s = getSkinnable();
+        boolean horizontal = isHorizontal();
+        double lx = (horizontal) ? trackStart + (((trackLength * ((s.getLowValue() - s.getMin()) /
+                (getMaxMinusMinNoZero()))) - thumbWidth/2)) : lowThumbPos;
+        double ly = (horizontal) ? lowThumbPos :
+            getSkinnable().getInsets().getTop() + trackLength - (trackLength * ((s.getLowValue() - s.getMin()) /
+                (getMaxMinusMinNoZero()))); //  - thumbHeight/2
+        lowThumb.setLayoutX(lx);
+        lowThumb.setLayoutY(ly);
+        if (horizontal) rangeStart = lx + thumbWidth; else rangeEnd = ly;
+    }
+
+    /**
+     * Called when ever either min, max or highValue changes, so highthumb's layoutX, Y is recomputed.
+     */
+    private void positionHighThumb() {
+        RangeSlider slider = (RangeSlider) getSkinnable();
+        boolean orientation = ((RangeSlider) getSkinnable()).getOrientation() == Orientation.HORIZONTAL;
+
+        double thumbWidth = lowThumb.getWidth();
+        double thumbHeight = lowThumb.getHeight();
+        highThumb.resize(thumbWidth, thumbHeight);
+
+        double pad = 0;//track.impl_getBackgroundFills() == null || track.impl_getBackgroundFills().length <= 0 ? 0.0D : track.impl_getBackgroundFills()[0].getTopLeftCornerRadius();
+        double trackStart = orientation ? track.getLayoutX() : track.getLayoutY();
+        trackStart += pad;
+        double trackLength = orientation ? track.getWidth() : track.getHeight();
+        trackLength -= 2 * pad;
+
+        double x = orientation ? trackStart + (trackLength * ((slider.getHighValue() - slider.getMin()) / (getMaxMinusMinNoZero())) - thumbWidth / 2D) : lowThumb.getLayoutX();
+        double y = orientation ? lowThumb.getLayoutY() : (getSkinnable().getInsets().getTop() + trackLength) - trackLength * ((slider.getHighValue() - slider.getMin()) / (getMaxMinusMinNoZero()));
+        highThumb.setLayoutX(x);
+        highThumb.setLayoutY(y);
+        if (orientation) rangeEnd = x; else rangeStart = y + thumbWidth;
+    }
+    
+    @Override protected void layoutChildren(final double x, final double y,
+            final double w, final double h) {
+        // resize thumb to preferred size
+        thumbWidth = lowThumb.prefWidth(-1);
+        thumbHeight = lowThumb.prefHeight(-1);
+        lowThumb.resize(thumbWidth, thumbHeight);
+        // we are assuming the is common radius's for all corners on the track
+        double trackRadius = track.getBackground() == null ? 0 : track.getBackground().getFills().size() > 0 ?
+                track.getBackground().getFills().get(0).getRadii().getTopLeftHorizontalRadius() : 0;
+      
+        if (isHorizontal()) {
+            double tickLineHeight =  (showTickMarks) ? tickLine.prefHeight(-1) : 0;
+            double trackHeight = track.prefHeight(-1);
+            double trackAreaHeight = Math.max(trackHeight,thumbHeight);
+            double totalHeightNeeded = trackAreaHeight  + ((showTickMarks) ? trackToTickGap+tickLineHeight : 0);
+            double startY = y + ((h - totalHeightNeeded)/2); // center slider in available height vertically
+            trackLength = w - thumbWidth;
+            trackStart = x + (thumbWidth/2);
+            double trackTop = (int)(startY + ((trackAreaHeight-trackHeight)/2));
+            lowThumbPos = (int)(startY + ((trackAreaHeight-thumbHeight)/2));
+            
+            positionLowThumb();
+            // layout track
+            track.resizeRelocate(trackStart - trackRadius, trackTop , trackLength + trackRadius + trackRadius, trackHeight);
+            positionHighThumb();
+            // layout range bar
+            rangeBar.resizeRelocate(rangeStart, trackTop, rangeEnd - rangeStart, trackHeight);
+            // layout tick line
+            if (showTickMarks) {
+                tickLine.setLayoutX(trackStart);
+                tickLine.setLayoutY(trackTop+trackHeight+trackToTickGap);
+                tickLine.resize(trackLength, tickLineHeight);
+                tickLine.requestAxisLayout();
+            } else {
+                if (tickLine != null) {
+                    tickLine.resize(0,0);
+                    tickLine.requestAxisLayout();
+                }
+                tickLine = null;
+            }
+        } else {
+            double tickLineWidth = (showTickMarks) ? tickLine.prefWidth(-1) : 0;
+            double trackWidth = track.prefWidth(-1);
+            double trackAreaWidth = Math.max(trackWidth,thumbWidth);
+            double totalWidthNeeded = trackAreaWidth  + ((showTickMarks) ? trackToTickGap+tickLineWidth : 0) ;
+            double startX = x + ((w - totalWidthNeeded)/2); // center slider in available width horizontally
+            trackLength = h - thumbHeight;
+            trackStart = y + (thumbHeight/2);
+            double trackLeft = (int)(startX + ((trackAreaWidth-trackWidth)/2));
+            lowThumbPos = (int)(startX + ((trackAreaWidth-thumbWidth)/2));
+
+            positionLowThumb();
+            // layout track
+            track.resizeRelocate(trackLeft, trackStart - trackRadius, trackWidth, trackLength + trackRadius + trackRadius);
+            positionHighThumb();
+            // layout range bar
+            rangeBar.resizeRelocate(trackLeft, rangeStart, trackWidth, rangeEnd - rangeStart);
+            // layout tick line
+            if (showTickMarks) {
+                tickLine.setLayoutX(trackLeft+trackWidth+trackToTickGap);
+                tickLine.setLayoutY(trackStart);
+                tickLine.resize(tickLineWidth, trackLength);
+                tickLine.requestAxisLayout();
+            } else {
+                if (tickLine != null) {
+                    tickLine.resize(0,0);
+                    tickLine.requestAxisLayout();
+                }
+                tickLine = null;
+            }
+        }
+    }
+    
+    private double minTrackLength() {
+        return 2*lowThumb.prefWidth(-1);
+    }
+    
+    @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+        if (isHorizontal()) {
+            return (leftInset + minTrackLength() + lowThumb.minWidth(-1) + rightInset);
+        } else {
+            return (leftInset + lowThumb.prefWidth(-1) + rightInset);
+        }
+    }
+
+    @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+         if (isHorizontal()) {
+            return (topInset + lowThumb.prefHeight(-1) + bottomInset);
+        } else {
+            return (topInset + minTrackLength() + lowThumb.prefHeight(-1) + bottomInset);
+        }
+    }
+
+    @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+        if (isHorizontal()) {
+            if(showTickMarks) {
+                return Math.max(140, tickLine.prefWidth(-1));
+            } else {
+                return 140;
+            }
+        } else {
+            //return (padding.getLeft()) + Math.max(thumb.prefWidth(-1), track.prefWidth(-1)) + padding.getRight();
+            return leftInset + Math.max(lowThumb.prefWidth(-1), track.prefWidth(-1)) +
+            ((showTickMarks) ? (trackToTickGap+tickLine.prefWidth(-1)) : 0) + rightInset;
+        }
+    }
+
+    @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+        if (isHorizontal()) {
+            return getSkinnable().getInsets().getTop() + Math.max(lowThumb.prefHeight(-1), track.prefHeight(-1)) +
+             ((showTickMarks) ? (trackToTickGap+tickLine.prefHeight(-1)) : 0)  + bottomInset;
+        } else {
+            if(showTickMarks) {
+                return Math.max(140, tickLine.prefHeight(-1));
+            } else {
+                return 140;
+            }
+        }
+    }
+
+    @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+        if (isHorizontal()) {
+            return Double.MAX_VALUE;
+        } else {
+            return getSkinnable().prefWidth(-1);
+        }
+    }
+
+    @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+        if (isHorizontal()) {
+            return getSkinnable().prefHeight(width);
+        } else {
+            return Double.MAX_VALUE;
+        }
+    }
+    
+    private boolean isHorizontal() {
+        return orientation == null || orientation == Orientation.HORIZONTAL;
+    }
+
+    private static class ThumbPane extends StackPane {
+        public void setFocus(boolean value) {
+            setFocused(value);
+        }
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/skin/RatingSkin.java b/controlsfx/src/main/java/impl/org/controlsfx/skin/RatingSkin.java
new file mode 100644
index 0000000..8c13fc3
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/skin/RatingSkin.java
@@ -0,0 +1,329 @@
+/**
+ * Copyright (c) 2013, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.skin;
+
+import impl.org.controlsfx.behavior.RatingBehavior;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javafx.event.EventHandler;
+import javafx.geometry.Orientation;
+import javafx.geometry.Point2D;
+import javafx.scene.Node;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.VBox;
+import javafx.scene.shape.Rectangle;
+
+import org.controlsfx.control.Rating;
+import org.controlsfx.tools.Utils;
+
+import com.sun.javafx.scene.control.skin.BehaviorSkinBase;
+
+/**
+ *
+ */
+public class RatingSkin extends BehaviorSkinBase<Rating, RatingBehavior> {
+        
+    /***************************************************************************
+     * 
+     * Private fields
+     * 
+     **************************************************************************/
+    
+    private static final String STRONG = "strong"; //$NON-NLS-1$
+    
+    private boolean updateOnHover;
+    private boolean partialRating;
+    
+    // the container for the traditional rating control. If updateOnHover and
+    // partialClipping are disabled, this will show a combination of strong
+    // and non-strong graphics, depending on the current rating value
+    private Pane backgroundContainer;
+    
+    // the container for the strong graphics which may be partially clipped.
+    // Note that this only exists if updateOnHover or partialClipping is enabled.
+    private Pane foregroundContainer;
+    
+    private double rating = -1;
+
+    private Rectangle forgroundClipRect;
+        
+    private final EventHandler<MouseEvent> mouseMoveHandler = new EventHandler<MouseEvent>() {
+        @Override public void handle(MouseEvent event) {
+
+        	// if we support updateOnHover, calculate the intended rating based on the mouse 
+        	// location and update the control property with it.
+        	
+            if (updateOnHover) {
+            	updateRatingFromMouseEvent(event);
+            }
+        }
+    };
+    
+    private final EventHandler<MouseEvent> mouseClickHandler = new EventHandler<MouseEvent>() {
+        @Override public void handle(MouseEvent event) {
+
+        	// if we are not updating on hover, calculate the intended rating based on the mouse 
+        	// location and update the control property with it.
+        	
+            if (! updateOnHover) {
+            	updateRatingFromMouseEvent(event);
+            }
+        }
+    };
+    
+    private void updateRatingFromMouseEvent(MouseEvent event) {
+    	Rating control = getSkinnable();
+    	if (! control.ratingProperty().isBound()) {
+        	Point2D mouseLocation = new Point2D(event.getSceneX(), event.getSceneY());
+    		control.setRating(calculateRating(mouseLocation));
+    	}
+    }
+
+    /***************************************************************************
+     * 
+     * Constructors
+     * 
+     **************************************************************************/
+    
+    public RatingSkin(Rating control) {
+        super(control, new RatingBehavior(control));
+        
+        this.updateOnHover = control.isUpdateOnHover();
+        this.partialRating = control.isPartialRating();
+        
+        // init
+        recreateButtons();
+        updateRating();
+        // -- end init
+        
+        registerChangeListener(control.ratingProperty(), "RATING"); //$NON-NLS-1$
+        registerChangeListener(control.maxProperty(), "MAX"); //$NON-NLS-1$
+        registerChangeListener(control.orientationProperty(), "ORIENTATION"); //$NON-NLS-1$
+        registerChangeListener(control.updateOnHoverProperty(), "UPDATE_ON_HOVER"); //$NON-NLS-1$
+        registerChangeListener(control.partialRatingProperty(), "PARTIAL_RATING"); //$NON-NLS-1$
+        // added to ensure clip is correctly calculated when control is first shown:
+        registerChangeListener(control.boundsInLocalProperty(), "BOUNDS"); //$NON-NLS-1$
+    }
+
+    
+    
+    /***************************************************************************
+     * 
+     * Implementation
+     * 
+     **************************************************************************/
+    
+    @Override protected void handleControlPropertyChanged(String p) {
+        super.handleControlPropertyChanged(p);
+        
+        if (p == "RATING") { //$NON-NLS-1$
+            updateRating();
+        } else if (p == "MAX") { //$NON-NLS-1$
+            recreateButtons();
+        } else if (p == "ORIENTATION") { //$NON-NLS-1$
+            recreateButtons();
+        } else if (p == "PARTIAL_RATING") { //$NON-NLS-1$
+            this.partialRating = getSkinnable().isPartialRating();
+            recreateButtons();
+        } else if (p == "UPDATE_ON_HOVER") { //$NON-NLS-1$
+            this.updateOnHover = getSkinnable().isUpdateOnHover();
+            recreateButtons();
+        } else if (p == "BOUNDS") { //$NON-NLS-1$
+        	if (this.partialRating) {
+        		updateClip();
+        	}
+        }
+    }
+    
+    private void recreateButtons() {
+        backgroundContainer = null;
+        foregroundContainer = null;
+        
+        backgroundContainer = isVertical() ? new VBox() : new HBox();
+        backgroundContainer.getStyleClass().add("container"); //$NON-NLS-1$
+        getChildren().setAll(backgroundContainer);
+        
+        if (updateOnHover || partialRating) {
+            foregroundContainer = isVertical() ? new VBox() : new HBox();
+            foregroundContainer.getStyleClass().add("container"); //$NON-NLS-1$
+            foregroundContainer.setMouseTransparent(true);
+            getChildren().add(foregroundContainer);
+            
+            forgroundClipRect = new Rectangle();
+            foregroundContainer.setClip(forgroundClipRect);
+            
+        }
+        
+        for (int index = 0; index <= getSkinnable().getMax(); index++) {
+            Node backgroundNode = createButton();
+            
+            if (index > 0) {
+                if (isVertical()) {
+                    backgroundContainer.getChildren().add(0,backgroundNode);
+                } else {
+                    backgroundContainer.getChildren().add(backgroundNode);
+                }
+                
+                if (partialRating) {
+                    Node foregroundNode = createButton();
+                    foregroundNode.getStyleClass().add(STRONG);
+                    foregroundNode.setMouseTransparent(true);
+                    
+                    if (isVertical()) {
+                        foregroundContainer.getChildren().add(0,foregroundNode);
+                    } else {
+                        foregroundContainer.getChildren().add(foregroundNode);
+                    }
+                }
+            }
+        }
+        
+        updateRating();
+    }
+    
+    // Calculate the rating based on a mouse position (in Scene coordinates).
+    // If we support partial ratings, the value is calculated directly.
+    // Otherwise the ceil of the value is computed.
+    private double calculateRating(Point2D sceneLocation) {
+        final Point2D b = backgroundContainer.sceneToLocal(sceneLocation);
+        
+        final double x = b.getX();
+        final double y = b.getY();
+        
+        final Rating control = getSkinnable();
+        
+        final int max = control.getMax();
+        final double w = control.getWidth() - (snappedLeftInset() + snappedRightInset());
+        final double h = control.getHeight() - (snappedTopInset() + snappedBottomInset());
+        
+        double newRating = -1;
+        
+        if (isVertical()) {
+            newRating = ((h - y) / h) * max;
+        } else {
+            newRating = (x / w) * max;
+        }
+        
+        if (! partialRating) {
+            newRating = Utils.clamp(1, Math.ceil(newRating), control.getMax());
+        }
+        
+        return newRating;
+    }
+    
+    private void updateClip() {
+        final Rating control = getSkinnable();
+        final double h = control.getHeight() - (snappedTopInset() + snappedBottomInset());
+        final double w = control.getWidth() - (snappedLeftInset() + snappedRightInset());
+        
+        if (isVertical()) {
+        	final double y = h * rating / control.getMax() ;
+            forgroundClipRect.relocate(0, h - y);
+            forgroundClipRect.setWidth(control.getWidth());
+            forgroundClipRect.setHeight(y);
+        } else {
+        	final double x = w * rating / control.getMax();        	
+            forgroundClipRect.setWidth(x);
+            forgroundClipRect.setHeight(control.getHeight());
+        }
+    	
+    }
+    
+//    private double getSpacing() {
+//        return (backgroundContainer instanceof HBox) ?
+//                ((HBox)backgroundContainer).getSpacing() :
+//                ((VBox)backgroundContainer).getSpacing();
+//    }
+    
+    private Node createButton() {
+        Region btn = new Region();
+        btn.getStyleClass().add("button"); //$NON-NLS-1$
+        
+        btn.setOnMouseMoved(mouseMoveHandler);
+        btn.setOnMouseClicked(mouseClickHandler);
+        return btn;
+    }
+    
+    // Update the skin based on a new value for the rating.
+    // If we support partial ratings, updates the clip.
+    // Otherwise, updates the style classes for the buttons.
+    
+    private void updateRating() {
+    	
+    	double newRating = getSkinnable().getRating();
+    	    	
+        if (newRating == rating) return;
+        
+        rating = Utils.clamp(0, newRating, getSkinnable().getMax());
+
+        if (partialRating) {
+        	updateClip();
+        } else {
+            updateButtonStyles();
+        }
+    }
+
+	private void updateButtonStyles() {
+		final int max = getSkinnable().getMax();
+
+		// make a copy of the buttons list so that we can reverse the order if
+		// the list is vertical (as the buttons are ordered bottom to top).
+		List<Node> buttons = new ArrayList<>(backgroundContainer.getChildren());
+		if (isVertical()) {
+		    Collections.reverse(buttons);
+		}
+		
+		for (int i = 0; i < max; i++) {
+		    Node button = buttons.get(i);
+   
+		    final List<String> styleClass = button.getStyleClass();
+		    final boolean containsStrong = styleClass.contains(STRONG);
+		    
+		    if (i < rating) {
+		        if (! containsStrong) {
+		            styleClass.add(STRONG);
+		        }
+		    } else if (containsStrong) {
+		        styleClass.remove(STRONG);
+		    }
+		}
+	}
+    
+    private boolean isVertical() {
+        return getSkinnable().getOrientation() == Orientation.VERTICAL;
+    }
+    
+    @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return super.computePrefWidth(height, topInset, rightInset, bottomInset, leftInset);
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/skin/SegmentedButtonSkin.java b/controlsfx/src/main/java/impl/org/controlsfx/skin/SegmentedButtonSkin.java
new file mode 100644
index 0000000..ebb921e
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/skin/SegmentedButtonSkin.java
@@ -0,0 +1,117 @@
+/**
+ * Copyright (c) 2013, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.skin;
+
+import java.util.Collections;
+
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.collections.ObservableList;
+import javafx.scene.control.ToggleButton;
+import javafx.scene.control.ToggleGroup;
+import javafx.scene.layout.HBox;
+
+import org.controlsfx.control.SegmentedButton;
+
+import com.sun.javafx.scene.control.behavior.BehaviorBase;
+import com.sun.javafx.scene.control.behavior.KeyBinding;
+import com.sun.javafx.scene.control.skin.BehaviorSkinBase;
+
+public class SegmentedButtonSkin extends BehaviorSkinBase<SegmentedButton, BehaviorBase<SegmentedButton>> {
+        
+    private static final String ONLY_BUTTON = "only-button"; //$NON-NLS-1$
+    private static final String LEFT_PILL   = "left-pill"; //$NON-NLS-1$
+    private static final String CENTER_PILL = "center-pill"; //$NON-NLS-1$
+    private static final String RIGHT_PILL  = "right-pill"; //$NON-NLS-1$
+
+    private final HBox container;
+
+    public SegmentedButtonSkin(SegmentedButton control) {
+        super(control, new BehaviorBase<>(control, Collections.<KeyBinding> emptyList()));
+        
+        container = new HBox();
+        
+        getChildren().add(container);
+        
+        updateButtons();
+        getButtons().addListener(new InvalidationListener() {
+            @Override public void invalidated(Observable observable) {
+                updateButtons();
+            }
+        });
+        
+        control.toggleGroupProperty().addListener((observable, oldValue, newValue) -> {
+            getButtons().forEach((button) -> {
+                button.setToggleGroup(newValue);
+            });
+        });
+    }
+    
+    private ObservableList<ToggleButton> getButtons() {
+        return getSkinnable().getButtons();
+    }
+    
+    private void updateButtons() {
+        ObservableList<ToggleButton> buttons = getButtons();
+        ToggleGroup group = getSkinnable().getToggleGroup();
+        
+        container.getChildren().clear();
+        
+        for (int i = 0; i < getButtons().size(); i++) {
+            ToggleButton t = buttons.get(i);
+            
+            if (group != null) {
+                t.setToggleGroup(group);
+            }
+            
+            t.getStyleClass().removeAll(ONLY_BUTTON, LEFT_PILL, CENTER_PILL, RIGHT_PILL);
+            container.getChildren().add(t);
+
+            if (i == buttons.size() - 1) {
+                if (i == 0) {
+                    t.getStyleClass().add(ONLY_BUTTON);
+                } else {
+                    t.getStyleClass().add(RIGHT_PILL);
+                }
+            } else if (i == 0) {
+                t.getStyleClass().add(LEFT_PILL);
+            } else {
+                t.getStyleClass().add(CENTER_PILL);
+            }
+        }
+    }
+
+    @Override
+    protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return getSkinnable().prefWidth(height);
+    }
+
+    @Override
+    protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return getSkinnable().prefHeight(width);
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/skin/SnapshotViewSkin.java b/controlsfx/src/main/java/impl/org/controlsfx/skin/SnapshotViewSkin.java
new file mode 100644
index 0000000..5bdf23e
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/skin/SnapshotViewSkin.java
@@ -0,0 +1,566 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.skin;
+
+import impl.org.controlsfx.behavior.SnapshotViewBehavior;
+import javafx.beans.binding.Bindings;
+import javafx.beans.binding.BooleanBinding;
+import javafx.beans.property.ReadOnlyBooleanProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.geometry.Bounds;
+import javafx.geometry.Pos;
+import javafx.geometry.Rectangle2D;
+import javafx.scene.Cursor;
+import javafx.scene.Node;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.GridPane;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Rectangle;
+import javafx.scene.shape.StrokeType;
+
+import org.controlsfx.control.SnapshotView;
+import org.controlsfx.control.SnapshotView.Boundary;
+
+import com.sun.javafx.scene.control.skin.BehaviorSkinBase;
+
+/**
+ * View for the {@link SnapshotView}. It displays the node and the selection and manages their positioning. Mouse events
+ * are handed over to the {@link SnapshotViewBehavior} which uses them to change the selection.
+ */
+public class SnapshotViewSkin extends BehaviorSkinBase<SnapshotView, SnapshotViewBehavior> {
+
+    /* ************************************************************************
+     *                                                                         *
+     * Attributes & Properties                                                 *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * The currently displayed node; when the {@link SnapshotView#nodeProperty() node} property changes
+     * {@link #updateNode() updateNode} will set the new one.
+     */
+    private Node node;
+
+    /**
+     * The pane displaying the {@link #node}.
+     */
+    private final GridPane gridPane;
+
+    /**
+     * The (mutable) rectangle which represents the selected area.
+     */
+    private final Rectangle selectedArea;
+
+    /**
+     * The rectangle whose stroke represents the unselected area. Binding is used to ensure that the rectangle itself
+     * always has the same size and position as the {@link #selectedArea}.
+     */
+    private final Rectangle unselectedArea;
+
+    /**
+     * The node capturing mouse events.
+     */
+    private final Node mouseNode;
+
+    /* ************************************************************************
+     *                                                                         *
+     * Constructor & Initialization                                            *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * Creates a new skin for the specified {@link SnapshotView}.
+     * 
+     * @param snapshotView
+     *            the {@link SnapshotView} this skin will display
+     */
+    public SnapshotViewSkin(SnapshotView snapshotView) {
+
+        super(snapshotView, new SnapshotViewBehavior(snapshotView));
+
+        this.gridPane = createGridPane();
+        this.selectedArea = new Rectangle();
+        this.unselectedArea = new Rectangle();
+        this.mouseNode = createMouseNode();
+
+        buildSceneGraph();
+        initializeAreas();
+
+        registerChangeListener(snapshotView.nodeProperty(), "NODE"); //$NON-NLS-1$
+        registerChangeListener(snapshotView.selectionProperty(), "SELECTION"); //$NON-NLS-1$
+    }
+
+    @Override
+    protected void handleControlPropertyChanged(String p) {
+        super.handleControlPropertyChanged(p);
+
+        if ("NODE".equals(p)) { //$NON-NLS-1$
+            updateNode();
+        } else if ("SELECTION".equals(p)) { //$NON-NLS-1$
+            updateSelection();
+        }
+    }
+
+    /**
+     * Creates the grid pane which will contain the node.
+     * 
+     * @return a {@link GridPane}
+     */
+    private static GridPane createGridPane() {
+        GridPane pane = new GridPane();
+        pane.setAlignment(Pos.CENTER);
+        return pane;
+    }
+
+    /**
+     * Creates the node which will be used to capture mouse events. Events are handed over to
+     * {@link #handleMouseEvent(MouseEvent) handleMouseEvent}.
+     * 
+     * @return a {@link Node}
+     */
+    private Node createMouseNode() {
+        Rectangle mouseNode = new Rectangle();
+
+        // make the node transparent and make sure its size does not affect the control's size
+        mouseNode.setFill(Color.TRANSPARENT);
+        mouseNode.setManaged(false);
+
+        // bind width and height to the control
+        mouseNode.widthProperty().bind(getSkinnable().widthProperty());
+        mouseNode.heightProperty().bind(getSkinnable().heightProperty());
+
+        // let it handle the mouse events if allowed by the user
+        mouseNode.addEventHandler(MouseEvent.ANY, this::handleMouseEvent);
+        mouseNode.mouseTransparentProperty().bind(getSkinnable().selectionMouseTransparentProperty());
+
+        return mouseNode;
+    }
+
+    /**
+     * Builds this skin's scene graph.
+     */
+    private void buildSceneGraph() {
+        getChildren().addAll(gridPane, unselectedArea, selectedArea, mouseNode);
+        updateNode();
+    }
+
+    /**
+     * Initializes the {@link #selectedArea} and the {@link #unselectedArea}. This includes their style and their
+     * bindings to the {@link SnapshotView#selectionProperty() selection} property.
+     */
+    private void initializeAreas() {
+        styleAreas();
+        bindAreaCoordinatesTogether();
+        bindAreaVisibilityToSelection();
+    }
+
+    /**
+     * Styles the selected and unselected area.
+     */
+    private void styleAreas() {
+        selectedArea.fillProperty().bind(getSkinnable().selectionAreaFillProperty());
+        selectedArea.strokeProperty().bind(getSkinnable().selectionBorderPaintProperty());
+        selectedArea.strokeWidthProperty().bind(getSkinnable().selectionBorderWidthProperty());
+        selectedArea.setStrokeType(StrokeType.OUTSIDE);
+        // if the control's layout depends on this rectangle,
+        // the stroke's width messes up the layout if the selection is on the pane's edge
+        selectedArea.setManaged(false);
+        selectedArea.setMouseTransparent(true);
+
+        unselectedArea.setFill(Color.TRANSPARENT);
+        unselectedArea.strokeProperty().bind(getSkinnable().unselectedAreaFillProperty());
+        unselectedArea.strokeWidthProperty().bind(
+                Bindings.max(getSkinnable().widthProperty(), getSkinnable().heightProperty()));
+        unselectedArea.setStrokeType(StrokeType.OUTSIDE);
+        // this call is crucial! it prevents the enormous unselected area from messing up the layout
+        unselectedArea.setManaged(false);
+        unselectedArea.setMouseTransparent(true);
+    }
+
+    /**
+     * Binds the position and size of {@link #unselectedArea} to {@link #selectedArea}.
+     */
+    private void bindAreaCoordinatesTogether() {
+        unselectedArea.xProperty().bind(selectedArea.xProperty());
+        unselectedArea.yProperty().bind(selectedArea.yProperty());
+        unselectedArea.widthProperty().bind(selectedArea.widthProperty());
+        unselectedArea.heightProperty().bind(selectedArea.heightProperty());
+    }
+
+    /**
+     * Binds the visibility of {@link #selectedArea} and {@link #unselectedArea} to the {@code SnapshotView} 's
+     * {@link SnapshotView#selectionActiveProperty() selectionActive} and {@link SnapshotView#hasSelectionProperty()
+     * selectionValid} properties.
+     */
+    @SuppressWarnings("unused")
+    private void bindAreaVisibilityToSelection() {
+        ReadOnlyBooleanProperty selectionExists = getSkinnable().hasSelectionProperty();
+        ReadOnlyBooleanProperty selectionActive = getSkinnable().selectionActiveProperty();
+        BooleanBinding existsAndActive = Bindings.and(selectionExists, selectionActive);
+
+        selectedArea.visibleProperty().bind(existsAndActive);
+        unselectedArea.visibleProperty().bind(existsAndActive);
+
+        // UGLY WORKAROUND AHEAD!
+        // The clipper should be created in 'styleAreas' but due to the problem explained in 'Clipper.setClip(Node)'
+        // it has to be created here where the visibility is determined.
+
+        // clip the unselected area according to the view's property - this is done by a designated inner class
+        new Clipper(getSkinnable(), unselectedArea, () -> unselectedArea.visibleProperty().bind(existsAndActive));
+    }
+
+    /* ************************************************************************
+     *                                                                         *
+     * Node                                                                    *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * Displays the current {@link SnapshotView#nodeProperty() node}.
+     */
+    private void updateNode() {
+        if (node != null) {
+            gridPane.getChildren().remove(node);
+        }
+
+        node = getSkinnable().getNode();
+        if (node != null) {
+            gridPane.getChildren().add(0, node);
+        }
+    }
+
+    /* ************************************************************************
+     *                                                                         *
+     * Selection                                                               *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * Updates the position and size of {@link #selectedArea} (and by binding that of {@link #unselectedArea}) to a
+     * changed selection.
+     */
+    private void updateSelection() {
+        boolean showSelection = getSkinnable().hasSelection() && getSkinnable().isSelectionActive();
+
+        if (showSelection) {
+            // the selection can be properly displayed
+            Rectangle2D selection = getSkinnable().getSelection();
+            setSelection(selection.getMinX(), selection.getMinY(), selection.getWidth(), selection.getHeight());
+        } else {
+            // in this case the selection areas are invisible,
+            // so the only thing left to do is to make sure their coordinates are not all over the place
+            // (this is not strictly necessary but makes the skin's state cleaner)
+            setSelection(0, 0, 0, 0);
+        }
+    }
+
+    /**
+     * Updates the position and size of {@link #selectedArea} (and by binding that of {@link #unselectedArea}) to the
+     * specified arguments.
+     * 
+     * @param x
+     *            the new x coordinate of the upper left corner
+     * @param y
+     *            the new y coordinate of the upper left corner
+     * @param width
+     *            the new width
+     * @param height
+     *            the new height
+     */
+    private void setSelection(double x, double y, double width, double height) {
+        selectedArea.setX(x);
+        selectedArea.setY(y);
+        selectedArea.setWidth(width);
+        selectedArea.setHeight(height);
+    }
+
+    /* ************************************************************************
+     *                                                                         *
+     * Mouse Events                                                            *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * Handles mouse events.
+     * 
+     * @param event
+     *            the {@link MouseEvent} to handle
+     */
+    private void handleMouseEvent(MouseEvent event) {
+        Cursor newCursor = getBehavior().handleMouseEvent(event);
+        mouseNode.setCursor(newCursor);
+    }
+
+    /* ************************************************************************
+     *                                                                         *
+     * Inner Classes                                                           *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * Clips the unselected area to the {@link SnapshotView#unselectedAreaBoundaryProperty() unselectedAreaBoundary}.
+     *
+     */
+    private static class Clipper {
+
+        /**
+         * The snapshot view to whose {@link Node#boundsInLocalProperty() boundsInLocal} the {@link #clippedNode} will
+         * be clipped.
+         */
+        private final SnapshotView snapshotView;
+
+        /**
+         * The node to which the clips will be added.
+         */
+        private final Node clippedNode;
+
+        /**
+         * A function which rebinds the clip's visibility after it was unbound. Only necessary because of the workaround
+         * explained in {@link #setClip(Node) setClip}.
+         */
+        private final Runnable rebindClippedNodeVisibility;
+
+        /**
+         * The {@link Rectangle} used to clip the {@link #clippedNode} to {@link Boundary#CONTROL}.
+         */
+        private final Rectangle controlClip;
+
+        /**
+         * The {@link Rectangle} used to clip the {@link #clippedNode} to {@link Boundary#NODE}.
+         */
+        private final Rectangle nodeClip;
+
+        /**
+         * A listener which updates the {@link #controlClip} when the {@link #snapshotView}'s
+         * {@link Node#boundsInLocalProperty() boundsInLocal} change.
+         */
+        private final ChangeListener<Bounds> updateControlClipToNewBoundsListener;
+
+        /**
+         * A listener which updates the {@link #nodeClip} when the {@link SnapshotView#nodeProperty() node}'s
+         * {@link Node#boundsInParentProperty() boundsInParent} change.
+         */
+        private final ChangeListener<Bounds> updateNodeClipToNewBoundsListener;
+
+        /**
+         * Creates a new clipper with the specified arguments.
+         * 
+         * @param snapshotView
+         *            the {@link SnapshotView} to whose bounds the {@code clippedNode} will be clipped
+         * @param clippedNode
+         *            the {@link Node} whose bounds will be clipped
+         * @param rebindClippedNodeVisibility
+         *            a function which rebinds the {@code clippedNode}'s visibility
+         */
+        public Clipper(SnapshotView snapshotView, Node clippedNode, Runnable rebindClippedNodeVisibility) {
+            this.snapshotView = snapshotView;
+            this.clippedNode = clippedNode;
+            this.rebindClippedNodeVisibility = rebindClippedNodeVisibility;
+
+            // for 'CONTROL', clip to the control's bounds
+            controlClip = new Rectangle();
+            updateControlClipToNewBoundsListener =
+                    (o, oldBounds, newBounds) -> resizeRectangleToBounds(controlClip, newBounds);
+
+            // for 'NODE', clip to the node's bounds
+            nodeClip = new Rectangle();
+            // create the listener which will resize the rectangle
+            updateNodeClipToNewBoundsListener =
+                    (o, oldBounds, newBounds) -> resizeRectangleToBounds(nodeClip, newBounds);
+
+            // set the clipping and keep updating it
+            setClipping();
+            snapshotView.unselectedAreaBoundaryProperty().addListener((o, oldBoundary, newBoundary) -> setClipping());
+        }
+
+        /**
+         * Sets clipping to the current {@link SnapshotView#unselectedAreaBoundaryProperty() unselectedAreaBoundary}.
+         */
+        private void setClipping() {
+            Boundary boundary = snapshotView.getUnselectedAreaBoundary();
+            switch (boundary) {
+            case CONTROL:
+                clipToControl();
+                break;
+            case NODE:
+                clipToNode();
+                break;
+            default:
+                throw new IllegalArgumentException("The boundary " + boundary + " is not fully implemented."); //$NON-NLS-1$ //$NON-NLS-2$
+            }
+        }
+
+        /**
+         * Clips the {@link #clippedNode} to {@link #controlClip} and keeps updating the latter when the control changes
+         * its bounds.
+         */
+        private void clipToControl() {
+            // stop resizing the node clip
+            updateNodeClipToChangingNode(snapshotView.nodeProperty(), snapshotView.getNode(), null);
+
+            // resize the control clip and keep doing so
+            resizeRectangleToBounds(controlClip, snapshotView.getBoundsInLocal());
+            snapshotView.boundsInLocalProperty().addListener(updateControlClipToNewBoundsListener);
+
+            // set the clip
+            setClip(controlClip);
+        }
+
+        /**
+         * Clips the {@link #clippedNode} to {@link #nodeClip} and keeps updating the latter when the control changes
+         * its bounds.
+         */
+        private void clipToNode() {
+            // update the node clip to the new bounds and whenever the node changes its bounds
+            updateNodeClipToChangingNode(snapshotView.nodeProperty(), null, snapshotView.getNode());
+            // move that listener from old to new nodes
+            snapshotView.nodeProperty().addListener(this::updateNodeClipToChangingNode);
+
+            // set the clip
+            setClip(nodeClip);
+        }
+
+        /**
+         * Resizes the {@link #nodeClip} to the specified new node's {@link Node#boundsInParentProperty()
+         * boundsInParent} (or to an empty rectangle if it is {@code null}) and moves the
+         * {@link #updateNodeClipToNewBoundsListener} from the old to the new node's {@code boundInParents} property.
+         * <p>
+         * Designed to be used as a lambda method reference.
+         * 
+         * @param o
+         *            the {@link ObservableValue} which changed its value
+         * @param oldNode
+         *            the old node
+         * @param newNode
+         *            the new node
+         */
+        private void updateNodeClipToChangingNode(
+                @SuppressWarnings("unused") ObservableValue<? extends Node> o, Node oldNode, Node newNode) {
+
+            // resize the rectangle to match the new node
+            resizeRectangleToNodeBounds(nodeClip, newNode);
+
+            // move the listener from one node to the next
+            if (oldNode != null) {
+                oldNode.boundsInParentProperty().removeListener(updateNodeClipToNewBoundsListener);
+            }
+            if (newNode != null) {
+                newNode.boundsInParentProperty().addListener(updateNodeClipToNewBoundsListener);
+            }
+        }
+
+        /**
+         * Resizes the specified rectangle to the specified node's {@link Node#boundsInParentProperty() boundsInParent}.
+         * 
+         * @param rectangle
+         *            the {@link Rectangle} which will be resized
+         * @param node
+         *            the {@link Node} to whose bounds the {@code rectangle} will be resized
+         */
+        private static void resizeRectangleToNodeBounds(Rectangle rectangle, Node node) {
+            if (node == null) {
+                resizeRectangleToZero(rectangle);
+            } else {
+                resizeRectangleToBounds(rectangle, node.getBoundsInParent());
+            }
+        }
+
+        /**
+         * Resized the specified rectangle so that its upper left point is {@code (0, 0)} and its width and height are
+         * both 0.
+         * 
+         * @param rectangle
+         *            the {@link Rectangle} which will be resized
+         */
+        private static void resizeRectangleToZero(Rectangle rectangle) {
+            rectangle.setX(0);
+            rectangle.setY(0);
+            rectangle.setWidth(0);
+            rectangle.setHeight(0);
+        }
+
+        /**
+         * Resized the specified rectangle so that it matches the specified bounds, i.e. it will have the same upper
+         * left point and width and height.
+         * 
+         * @param rectangle
+         *            the {@link Rectangle} which will be resized
+         * @param bounds
+         *            the {@link Bounds} to which the rectangle will be resized
+         */
+        private static void resizeRectangleToBounds(Rectangle rectangle, Bounds bounds) {
+            rectangle.setX(bounds.getMinX());
+            rectangle.setY(bounds.getMinY());
+            rectangle.setWidth(bounds.getWidth());
+            rectangle.setHeight(bounds.getHeight());
+        }
+
+        /**
+         * Sets the specified clip on the {@link #clippedNode}.
+         * 
+         * @param clip
+         *            the {@link Node} which is used as a clip
+         */
+        private void setClip(Node clip) {
+
+            /*
+             * UGLY WORKAROUND
+             * 
+             * Setting the clip on the unselected area while it is invisible leads to either the clip having no effect
+             * or no area being displayed at all. Obviously I'm doing something wrong but I couldn't determine the root
+             * cause so I fixed the symptom. Now the area is turned visible, the clip is set and then it is made
+             * invisible again.
+             * 
+             * Everything below but 'clippedNode.setClip(clip);' is part of that workaround. To reproduce the bug
+             * comment all those lines out. Then, after 'HelloSnapshotView' started, select 'NODE' for the unselected
+             * area boundary and draw a selection on the node. The area above the node which is not selected should be
+             * painted in a semi-opaque black but due to the bug it is not. Instead the area outside of the selection
+             * has no paint at all and is simply transparent.
+             * Note that if the boundary is turned back to CONTROL, a selection is made and then NODE is set again, the
+             * clips works properly and the preexisting selection's outer area is clipped to the node.  
+             * 
+             * If someone finds out what the *$#&? I've been doing wrong, please fix and be so kind to mail to
+             * nipa at codefx.org! :)
+             */
+
+            boolean workAroundVisibilityProblem = !clippedNode.isVisible();
+            if (workAroundVisibilityProblem) {
+                clippedNode.visibleProperty().unbind();
+                clippedNode.setVisible(true);
+            }
+
+            clippedNode.setClip(clip);
+
+            if (workAroundVisibilityProblem) {
+                rebindClippedNodeVisibility.run();
+            }
+        }
+
+    }
+
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/skin/StatusBarSkin.java b/controlsfx/src/main/java/impl/org/controlsfx/skin/StatusBarSkin.java
new file mode 100644
index 0000000..39a1b79
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/skin/StatusBarSkin.java
@@ -0,0 +1,100 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.skin;
+
+import javafx.beans.Observable;
+import javafx.beans.binding.Bindings;
+import javafx.scene.control.Label;
+import javafx.scene.control.ProgressBar;
+import javafx.scene.control.SkinBase;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+
+import org.controlsfx.control.StatusBar;
+
+public class StatusBarSkin extends SkinBase<StatusBar> {
+    
+    private HBox leftBox;
+    private HBox rightBox;
+    private Label label;
+    private ProgressBar progressBar;
+
+    public StatusBarSkin(StatusBar statusBar) {
+        super(statusBar);
+
+        leftBox = new HBox();
+        leftBox.getStyleClass().add("left-items"); //$NON-NLS-1$
+
+        rightBox = new HBox();
+        rightBox.getStyleClass().add("right-items"); //$NON-NLS-1$
+
+        progressBar = new ProgressBar();
+        progressBar.progressProperty().bind(statusBar.progressProperty());
+        progressBar.visibleProperty().bind(
+                Bindings.notEqual(0, statusBar.progressProperty()));
+
+        label = new Label();
+        label.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
+        label.textProperty().bind(statusBar.textProperty());
+        label.graphicProperty().bind(statusBar.graphicProperty());
+        label.getStyleClass().add("status-label"); //$NON-NLS-1$
+
+        leftBox.getChildren().setAll(getSkinnable().getLeftItems());
+
+        rightBox.getChildren().setAll(getSkinnable().getRightItems());
+
+        statusBar.getLeftItems().addListener(
+                (Observable evt) -> leftBox.getChildren().setAll(
+                        getSkinnable().getLeftItems()));
+
+        statusBar.getRightItems().addListener(
+                (Observable evt) -> rightBox.getChildren().setAll(
+                        getSkinnable().getRightItems()));
+
+        GridPane gridPane = new GridPane();
+
+        GridPane.setFillHeight(leftBox, true);
+        GridPane.setFillHeight(rightBox, true);
+        GridPane.setFillHeight(label, true);
+        GridPane.setFillHeight(progressBar, true);
+
+        GridPane.setVgrow(leftBox, Priority.ALWAYS);
+        GridPane.setVgrow(rightBox, Priority.ALWAYS);
+        GridPane.setVgrow(label, Priority.ALWAYS);
+        GridPane.setVgrow(progressBar, Priority.ALWAYS);
+
+        GridPane.setHgrow(label, Priority.ALWAYS);
+
+        gridPane.add(leftBox, 0, 0);
+        gridPane.add(label, 1, 0);
+        gridPane.add(progressBar, 2, 0);
+        gridPane.add(rightBox, 4, 0);
+
+        getChildren().add(gridPane);
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/skin/TaskProgressViewSkin.java b/controlsfx/src/main/java/impl/org/controlsfx/skin/TaskProgressViewSkin.java
new file mode 100644
index 0000000..6e6fba6
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/skin/TaskProgressViewSkin.java
@@ -0,0 +1,167 @@
+/**
+ * Copyright (c) 2014, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.skin;
+
+import javafx.beans.binding.Bindings;
+import javafx.concurrent.Task;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.Node;
+import javafx.scene.control.Button;
+import javafx.scene.control.ContentDisplay;
+import javafx.scene.control.Label;
+import javafx.scene.control.ListCell;
+import javafx.scene.control.ListView;
+import javafx.scene.control.ProgressBar;
+import javafx.scene.control.SkinBase;
+import javafx.scene.control.Tooltip;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.VBox;
+import javafx.util.Callback;
+
+import org.controlsfx.control.TaskProgressView;
+
+public class TaskProgressViewSkin<T extends Task<?>> extends
+        SkinBase<TaskProgressView<T>> {
+
+    public TaskProgressViewSkin(TaskProgressView<T> monitor) {
+        super(monitor);
+
+        BorderPane borderPane = new BorderPane();
+        borderPane.getStyleClass().add("box");
+
+        // list view
+        ListView<T> listView = new ListView<>();
+        listView.setPrefSize(500, 400);
+        listView.setPlaceholder(new Label("No tasks running"));
+        listView.setCellFactory(param -> new TaskCell());
+        listView.setFocusTraversable(false);
+
+        Bindings.bindContent(listView.getItems(), monitor.getTasks());
+        borderPane.setCenter(listView);
+
+        getChildren().add(listView);
+    }
+
+    class TaskCell extends ListCell<T> {
+        private ProgressBar progressBar;
+        private Label titleText;
+        private Label messageText;
+        private Button cancelButton;
+
+        private T task;
+        private BorderPane borderPane;
+
+        public TaskCell() {
+            titleText = new Label();
+            titleText.getStyleClass().add("task-title");
+
+            messageText = new Label();
+            messageText.getStyleClass().add("task-message");
+
+            progressBar = new ProgressBar();
+            progressBar.setMaxWidth(Double.MAX_VALUE);
+            progressBar.setMaxHeight(8);
+            progressBar.getStyleClass().add("task-progress-bar");
+
+            cancelButton = new Button("Cancel");
+            cancelButton.getStyleClass().add("task-cancel-button");
+            cancelButton.setTooltip(new Tooltip("Cancel Task"));
+            cancelButton.setOnAction(evt -> {
+                if (task != null) {
+                    task.cancel();
+                }
+            });
+
+            VBox vbox = new VBox();
+            vbox.setSpacing(4);
+            vbox.getChildren().add(titleText);
+            vbox.getChildren().add(progressBar);
+            vbox.getChildren().add(messageText);
+
+            BorderPane.setAlignment(cancelButton, Pos.CENTER);
+            BorderPane.setMargin(cancelButton, new Insets(0, 0, 0, 4));
+
+            borderPane = new BorderPane();
+            borderPane.setCenter(vbox);
+            borderPane.setRight(cancelButton);
+            setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
+        }
+
+        @Override
+        public void updateIndex(int index) {
+            super.updateIndex(index);
+
+            /*
+             * I have no idea why this is necessary but it won't work without
+             * it. Shouldn't the updateItem method be enough?
+             */
+            if (index == -1) {
+                setGraphic(null);
+                getStyleClass().setAll("task-list-cell-empty");
+            }
+        }
+
+        @Override
+        protected void updateItem(T task, boolean empty) {
+            super.updateItem(task, empty);
+
+            this.task = task;
+
+            if (empty || task == null) {
+                getStyleClass().setAll("task-list-cell-empty");
+                setGraphic(null);
+            } else if (task != null) {
+                getStyleClass().setAll("task-list-cell");
+                progressBar.progressProperty().bind(task.progressProperty());
+                titleText.textProperty().bind(task.titleProperty());
+                messageText.textProperty().bind(task.messageProperty());
+                cancelButton.disableProperty().bind(
+                        Bindings.not(task.runningProperty()));
+
+                Callback<T, Node> factory = getSkinnable().getGraphicFactory();
+                if (factory != null) {
+                    Node graphic = factory.call(task);
+                    if (graphic != null) {
+                        BorderPane.setAlignment(graphic, Pos.CENTER);
+                        BorderPane.setMargin(graphic, new Insets(0, 4, 0, 0));
+                        borderPane.setLeft(graphic);
+                    }
+                } else {
+                	/*
+                	 * Really needed. The application might have used a graphic
+                	 * factory before and then disabled it. In this case the border
+                	 * pane might still have an old graphic in the left position.
+                	 */
+                	borderPane.setLeft(null);
+                }
+
+                setGraphic(borderPane);
+            }
+        }
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/skin/ToggleSwitchSkin.java b/controlsfx/src/main/java/impl/org/controlsfx/skin/ToggleSwitchSkin.java
new file mode 100644
index 0000000..e752032
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/skin/ToggleSwitchSkin.java
@@ -0,0 +1,242 @@
+/**
+ * Copyright (c) 2015, 2016 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package impl.org.controlsfx.skin;
+
+import com.sun.javafx.css.converters.SizeConverter;
+import javafx.animation.TranslateTransition;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.value.WritableValue;
+import javafx.css.CssMetaData;
+import javafx.css.Styleable;
+import javafx.css.StyleableDoubleProperty;
+import javafx.css.StyleableProperty;
+import javafx.geometry.Pos;
+import javafx.scene.control.Label;
+import javafx.scene.control.SkinBase;
+import javafx.scene.layout.StackPane;
+import javafx.util.Duration;
+import org.controlsfx.control.ToggleSwitch;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Basic Skin implementation for the {@link ToggleSwitch}
+ */
+public class ToggleSwitchSkin extends SkinBase<ToggleSwitch>
+{
+    private final StackPane thumb;
+    private final StackPane thumbArea;
+    private final Label label;
+    private final StackPane labelContainer;
+    private final TranslateTransition transition;
+
+    /**
+     * Constructor for all ToggleSwitchSkin instances.
+     *
+     * @param control The ToggleSwitch for which this Skin should attach to.
+     */
+    public ToggleSwitchSkin(ToggleSwitch control) {
+        super(control);
+
+        thumb = new StackPane();
+        thumbArea = new StackPane();
+        label = new Label();
+        labelContainer = new StackPane();
+        transition = new TranslateTransition(Duration.millis(getThumbMoveAnimationTime()), thumb);
+
+        label.textProperty().bind(control.textProperty());
+        getChildren().addAll(labelContainer, thumbArea, thumb);
+        labelContainer.getChildren().addAll(label);
+        StackPane.setAlignment(label, Pos.CENTER_LEFT);
+
+        thumb.getStyleClass().setAll("thumb");
+        thumbArea.getStyleClass().setAll("thumb-area");
+
+        thumbArea.setOnMouseReleased(event -> mousePressedOnToggleSwitch(control));
+        thumb.setOnMouseReleased(event -> mousePressedOnToggleSwitch(control));
+        control.selectedProperty().addListener((observable, oldValue, newValue) -> {
+            if (newValue.booleanValue() != oldValue.booleanValue())
+                selectedStateChanged();
+        });
+    }
+
+    private void selectedStateChanged() {
+        if(transition != null){
+            transition.stop();
+        }
+        
+        double thumbAreaWidth = snapSize(thumbArea.prefWidth(-1));
+        double thumbWidth = snapSize(thumb.prefWidth(-1));
+        double distance = thumbAreaWidth - thumbWidth;
+        /**
+         * If we are not selected, we need to go from right to left.
+         */
+        if (!getSkinnable().isSelected()) {
+            thumb.setLayoutX(thumbArea.getLayoutX());
+            transition.setFromX(distance);
+            transition.setToX(0);
+        } else {
+            thumb.setTranslateX(thumbArea.getLayoutX());
+            transition.setFromX(0);
+            transition.setToX(distance);
+        }
+        transition.setCycleCount(1);
+        transition.play();
+    }
+
+    private void mousePressedOnToggleSwitch(ToggleSwitch toggleSwitch) {
+        toggleSwitch.setSelected(!toggleSwitch.isSelected());
+    }
+
+
+    /**
+     * How many milliseconds it should take for the thumb to go from
+     * one edge to the other
+     */
+    private DoubleProperty thumbMoveAnimationTime = null;
+
+    private DoubleProperty thumbMoveAnimationTimeProperty() {
+        if (thumbMoveAnimationTime == null) {
+            thumbMoveAnimationTime = new StyleableDoubleProperty(200) {
+
+                @Override
+                public Object getBean() {
+                    return ToggleSwitchSkin.this;
+                }
+
+                @Override
+                public String getName() {
+                    return "thumbMoveAnimationTime";
+                }
+
+                @Override
+                public CssMetaData<ToggleSwitch,Number> getCssMetaData() {
+                    return THUMB_MOVE_ANIMATION_TIME;
+                }
+           };
+        }
+        return thumbMoveAnimationTime;
+    }
+
+    private double getThumbMoveAnimationTime() {
+        return thumbMoveAnimationTime == null ? 200 : thumbMoveAnimationTime.get();
+    }
+
+    @Override
+    protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) {
+        ToggleSwitch toggleSwitch = getSkinnable();
+        double thumbWidth = snapSize(thumb.prefWidth(-1));
+        double thumbHeight = snapSize(thumb.prefHeight(-1));
+        thumb.resize(thumbWidth, thumbHeight);
+        //We must reset the TranslateX otherwise the thumb is mis-aligned when window is resized.
+         if (transition != null) {
+            transition.stop();
+        }
+        thumb.setTranslateX(0);
+
+        double thumbAreaY = snapPosition(contentY);
+        double thumbAreaWidth = snapSize(thumbArea.prefWidth(-1));
+        double thumbAreaHeight = snapSize(thumbArea.prefHeight(-1));
+
+        thumbArea.resize(thumbAreaWidth, thumbAreaHeight);
+        thumbArea.setLayoutX(contentWidth - thumbAreaWidth);
+        thumbArea.setLayoutY(thumbAreaY);
+
+        labelContainer.resize(contentWidth - thumbAreaWidth, thumbAreaHeight);
+        labelContainer.setLayoutY(thumbAreaY);
+
+        if (!toggleSwitch.isSelected())
+            thumb.setLayoutX(thumbArea.getLayoutX());
+        else
+            thumb.setLayoutX(thumbArea.getLayoutX() + thumbAreaWidth - thumbWidth);
+        thumb.setLayoutY(thumbAreaY + (thumbAreaHeight - thumbHeight) / 2);
+    }
+
+
+    @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return leftInset + label.prefWidth(-1) + thumbArea.prefWidth(-1) + rightInset;
+    }
+
+    @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return topInset + Math.max(thumb.prefHeight(-1), label.prefHeight(-1)) + bottomInset;
+    }
+
+    @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return leftInset + label.prefWidth(-1) + 20 + thumbArea.prefWidth(-1) + rightInset;
+    }
+
+    @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return topInset + Math.max(thumb.prefHeight(-1), label.prefHeight(-1)) + bottomInset;
+    }
+
+    private static final CssMetaData<ToggleSwitch, Number> THUMB_MOVE_ANIMATION_TIME =
+            new CssMetaData<ToggleSwitch, Number>("-thumb-move-animation-time",
+                    SizeConverter.getInstance(), 200) {
+
+                @Override
+                public boolean isSettable(ToggleSwitch toggleSwitch) {
+                    final ToggleSwitchSkin skin = (ToggleSwitchSkin) toggleSwitch.getSkin();
+                    return skin.thumbMoveAnimationTime == null ||
+                            !skin.thumbMoveAnimationTime.isBound();
+                }
+
+                @Override
+                public StyleableProperty<Number> getStyleableProperty(ToggleSwitch toggleSwitch) {
+                    final ToggleSwitchSkin skin = (ToggleSwitchSkin) toggleSwitch.getSkin();
+                    return (StyleableProperty<Number>) (WritableValue<Number>) skin.thumbMoveAnimationTimeProperty();
+                }
+            };
+
+    private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
+
+    static {
+        final List<CssMetaData<? extends Styleable, ?>> styleables =
+                new ArrayList<CssMetaData<? extends Styleable, ?>>(SkinBase.getClassCssMetaData());
+        styleables.add(THUMB_MOVE_ANIMATION_TIME);
+        STYLEABLES = Collections.unmodifiableList(styleables);
+    }
+
+    /**
+     * @return The CssMetaData associated with this class, which may include the
+     * CssMetaData of its super classes.
+     */
+    public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
+        return STYLEABLES;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
+        return getClassCssMetaData();
+    }
+}
+
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/CellView.java b/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/CellView.java
new file mode 100644
index 0000000..0b34af7
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/CellView.java
@@ -0,0 +1,651 @@
+/**
+ * Copyright (c) 2013, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.spreadsheet;
+
+import java.util.Objects;
+import java.util.Optional;
+import java.util.logging.Logger;
+import javafx.animation.FadeTransition;
+import javafx.application.Platform;
+import javafx.beans.binding.When;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.beans.value.WeakChangeListener;
+import javafx.collections.ObservableList;
+import javafx.collections.SetChangeListener;
+import javafx.collections.WeakSetChangeListener;
+import javafx.event.EventHandler;
+import javafx.event.WeakEventHandler;
+import javafx.scene.Node;
+import javafx.scene.control.ContentDisplay;
+import javafx.scene.control.Control;
+import javafx.scene.control.SelectionMode;
+import javafx.scene.control.TableCell;
+import javafx.scene.control.TablePositionBase;
+import javafx.scene.control.TableView;
+import javafx.scene.control.TableView.TableViewFocusModel;
+import javafx.scene.control.TableView.TableViewSelectionModel;
+import javafx.scene.control.Tooltip;
+import javafx.scene.image.ImageView;
+import javafx.scene.input.DragEvent;
+import javafx.scene.input.Dragboard;
+import javafx.scene.input.MouseButton;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.input.TransferMode;
+import javafx.scene.layout.Region;
+import javafx.util.Duration;
+import org.controlsfx.control.spreadsheet.Grid;
+import org.controlsfx.control.spreadsheet.SpreadsheetCell;
+import org.controlsfx.control.spreadsheet.SpreadsheetCellEditor;
+import org.controlsfx.control.spreadsheet.SpreadsheetCellType;
+import org.controlsfx.control.spreadsheet.SpreadsheetView;
+
+/**
+ * 
+ * The View cell that will be visible on screen. It holds the
+ * {@link SpreadsheetCell}.
+ */
+public class CellView extends TableCell<ObservableList<SpreadsheetCell>, SpreadsheetCell> {
+    private final SpreadsheetHandle handle;
+    /**
+     * Because we don't want to recreate Tooltip each time the TableCell is
+     * re-used. We save it properly here so we avoid recreating it each time
+     * since it's really time-consuming.
+     */
+    private Tooltip tooltip;
+
+    /***************************************************************************
+     * * Static Fields * *
+     **************************************************************************/
+    private static final String ANCHOR_PROPERTY_KEY = "table.anchor"; //$NON-NLS-1$
+    private static final int TOOLTIP_MAX_WIDTH = 400;
+    private static final Duration FADE_DURATION = Duration.millis(200);
+
+    static TablePositionBase<?> getAnchor(Control table, TablePositionBase<?> focusedCell) {
+        return hasAnchor(table) ? (TablePositionBase<?>) table.getProperties().get(ANCHOR_PROPERTY_KEY) : focusedCell;
+    }
+
+    static boolean hasAnchor(Control table) {
+        return table.getProperties().get(ANCHOR_PROPERTY_KEY) != null;
+    }
+
+    static void setAnchor(Control table, TablePositionBase anchor) {
+        if (table != null && anchor == null) {
+            removeAnchor(table);
+        } else {
+            table.getProperties().put(ANCHOR_PROPERTY_KEY, anchor);
+        }
+    }
+    
+    static void removeAnchor(Control table) {
+        table.getProperties().remove(ANCHOR_PROPERTY_KEY);
+    }
+    /***************************************************************************
+     * * Constructor * *
+     **************************************************************************/
+    public CellView(SpreadsheetHandle handle) {
+        this.handle = handle;
+        // When we detect a drag, we start the Full Drag so that other event
+        // will be fired
+        this.addEventHandler(MouseEvent.DRAG_DETECTED, new WeakEventHandler<>(startFullDragEventHandler));
+        setOnMouseDragEntered(new WeakEventHandler<>(dragMouseEventHandler));
+        
+        itemProperty().addListener(itemChangeListener);
+            }
+
+    /***************************************************************************
+     * * Public Methods * *
+     **************************************************************************/
+    @Override
+    public void startEdit() {
+        if (!isEditable()) {
+            getTableView().edit(-1, null);
+            return;
+        } 
+        /**
+         * If this CellView has no parent, this means that it was stacked into
+         * the cellsMap of the GridRowSkin, but the weakRef was dropped. So this
+         * CellView is still reacting to events, but it's not part of the
+         * sceneGraph! So we must deactivate this cell and let the real Cell in
+         * the sceneGraph take the edition.
+         */
+        if(getParent() == null){
+            updateTableView(null);
+            updateTableRow(null);
+            updateTableColumn(null);
+            return;
+        }
+        final int column = this.getTableView().getColumns().indexOf(this.getTableColumn());
+        final int row = getIndex();
+        // We start to edit only if the Cell is a normal Cell (aka visible).
+        final SpreadsheetView spv = handle.getView();
+        final Grid grid = spv.getGrid();
+        final SpreadsheetView.SpanType type = grid.getSpanType(spv, row, column);
+        //FIXME with the reverse algorithm in virtualFlow, is this still necessary?
+        if (type == SpreadsheetView.SpanType.NORMAL_CELL || type == SpreadsheetView.SpanType.ROW_VISIBLE) {
+
+            /**
+             * We may come to the situation where this method is called two
+             * times. One time by the row inside the VirtualFlow. And another by
+             * the row inside myFixedCells used by our GridVirtualFlow.
+             * 
+             * In that case, we have to give priority to the one used by the
+             * VirtualFlow. So we just check if the row is managed. If not, we
+             * know for sure that the our GridVirtualFlow has stepped out.
+             */
+            if (!getTableRow().isManaged()) {
+                return;
+            }
+
+            GridCellEditor editor = getEditor(getItem(), spv);
+            if (editor != null) {
+                super.startEdit();
+                setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
+                editor.startEdit();
+            }else{
+                getTableView().edit(-1, null);
+            }
+        }
+    }
+
+    @Override
+    public void commitEdit(SpreadsheetCell newValue) {
+        //When commiting, we bring the value smoothly.
+        FadeTransition fadeTransition = new FadeTransition(FADE_DURATION, this);
+        fadeTransition.setFromValue(0);
+        fadeTransition.setToValue(1);
+        fadeTransition.play();
+        
+        if (!isEditing()) {
+            return;
+        }
+        super.commitEdit(newValue);
+
+        setContentDisplay(ContentDisplay.LEFT);
+        updateItem(newValue, false);
+
+        if (getTableView() != null) {
+            getTableView().requestFocus();
+        }
+    }
+
+    @Override
+    public void cancelEdit() {
+        if (!isEditing()) {
+            return;
+        }
+
+        super.cancelEdit();
+
+        setContentDisplay(ContentDisplay.LEFT);
+        updateItem(getItem(), false);
+
+        if (getTableView() != null) {
+            getTableView().requestFocus();
+        }
+    }
+
+    @Override
+    public void updateItem(final SpreadsheetCell item, boolean empty) {
+        final boolean emptyRow = getTableView().getItems().size() < getIndex() + 1;
+        /**
+         * don't call super.updateItem() because it will trigger cancelEdit() if
+         * the cell is being edited. It causes calling commitEdit() ALWAYS call
+         * cancelEdit as well which is undesired.
+         * 
+         */
+        if (!isEditing()) {
+            super.updateItem(item, empty && emptyRow);
+        }
+        if (empty && isSelected()) {
+            updateSelected(false);
+        }
+        if (empty && emptyRow) {
+            textProperty().unbind();
+            setText(null);
+            // do not nullify graphic here. Let the TableRow to control cell
+            // dislay
+            // setGraphic(null);
+            setContentDisplay(null);
+        } else if (!isEditing() && item != null) {
+            show(item);
+            if (item.getGraphic() == null) {
+                setGraphic(null);
+            }
+        }
+    }
+
+    /**
+     * Called in the gridRowSkinBase when doing layout This allow not to
+     * override opacity in the row and let the cell handle itself
+     * @param cell
+     */
+    public void show(final SpreadsheetCell cell) {
+        // We reset the settings
+        textProperty().bind(cell.textProperty());
+        setCellGraphic(cell);
+
+        Optional<String> tooltipText = cell.getTooltip();
+        String trimTooltip = tooltipText.isPresent() ? tooltipText.get().trim() : null;
+        
+        if (trimTooltip != null && !trimTooltip.isEmpty()) {
+            /**
+             * Here we check if the Tooltip has not been created in order NOT TO
+             * re-create it for nothing as it is a really time-consuming
+             * operation.
+             */
+            Tooltip localTooltip = getAvailableTooltip();
+            if (localTooltip != null) {
+                if (!Objects.equals(localTooltip.getText(), trimTooltip)) {
+                    getTooltip().setText(trimTooltip);
+                }
+            } else {
+                /**
+                 * Ensure that modification of ToolTip are set on the JFX thread
+                 * because an exception can be thrown otherwise.
+                 */
+                getValue(() -> {
+                    Tooltip newTooltip = new Tooltip(tooltipText.get());
+                    newTooltip.setWrapText(true);
+                    newTooltip.setMaxWidth(TOOLTIP_MAX_WIDTH);
+                    setTooltip(newTooltip);
+                }
+                );
+            }
+        } else {
+            //We save that tooltip
+            if(getTooltip() != null){
+                tooltip = getTooltip();
+            }
+            setTooltip(null);
+        }
+        
+        setWrapText(cell.isWrapText());
+
+        setEditable(cell.isEditable());
+        
+        if (cell.getCellType().acceptDrop()) {
+            setOnDragOver(new EventHandler<DragEvent>() {
+
+                @Override
+                public void handle(DragEvent event) {
+                    Dragboard db = event.getDragboard();
+                    if (db.hasFiles()) {
+                        event.acceptTransferModes(TransferMode.ANY);
+                    } else {
+                        event.consume();
+                    }
+                }
+            });
+            // Dropping over surface
+            setOnDragDropped(new EventHandler<DragEvent>() {
+                @Override
+                public void handle(DragEvent event) {
+                    Dragboard db = event.getDragboard();
+                    boolean success = false;
+                    if (db.hasFiles() && db.getFiles().size() == 1) {
+                        if (getItem().getCellType().match(db.getFiles().get(0))) {
+                            handle.getView().getGrid().setCellValue(getItem().getRow(), getItem().getColumn(),
+                                    getItem().getCellType().convertValue(db.getFiles().get(0)));
+                            success = true;
+                        }
+                    }
+                    event.setDropCompleted(success);
+                    event.consume();
+                }
+            });
+        } else {
+            setOnDragOver(null);
+            setOnDragDropped(null);
+        }
+    }
+
+    /***************************************************************************
+     * * Private Methods * *
+     **************************************************************************/
+
+    /**
+     * See if a tootlip is available (either on the TableCell already, or in the
+     * Stack). And then set it to the TableCell.
+     *
+     * @return
+     */
+    private Tooltip getAvailableTooltip(){
+        if(getTooltip() != null){
+            return getTooltip();
+        }
+        if(tooltip != null){
+            setTooltip(tooltip);
+            return tooltip;
+        }
+        return null;
+    }
+    
+    private void setCellGraphic(SpreadsheetCell item) {
+
+        if (isEditing()) {
+            return;
+        }
+        Node graphic = item.getGraphic();
+        if (graphic != null) {
+            /**
+             * This workaround is added for the first row containing a graphic
+             * because for an unknown reason, the graphic is translated to a
+             * negative value so it's not fully visible. So we add those
+             * listener that watch those changes, and try to get the previous
+             * value (the right one) if the new value goes out of bounds.
+             */
+//            if (item.getRow() == 0) {
+//                graphic.layoutXProperty().removeListener(firstRowLayoutXListener);
+//                graphic.layoutXProperty().addListener(firstRowLayoutXListener);
+//
+//                graphic.layoutYProperty().removeListener(firstRowLayoutYListener);
+//                graphic.layoutYProperty().addListener(firstRowLayoutYListener);
+//            }
+            
+            if (graphic instanceof ImageView) {
+                ImageView image = (ImageView) graphic;
+                image.setCache(true);
+                image.setPreserveRatio(true);
+                image.setSmooth(true);
+                if(image.getImage() != null){
+                image.fitHeightProperty().bind(
+                        new When(heightProperty().greaterThan(image.getImage().getHeight())).then(
+                                image.getImage().getHeight()).otherwise(heightProperty()));
+                image.fitWidthProperty().bind(
+                        new When(widthProperty().greaterThan(image.getImage().getWidth())).then(
+                                image.getImage().getWidth()).otherwise(widthProperty()));
+                }
+                /**
+                 * If we have a Region and no text, we force it to take full
+                 * space. But we want to impact the minSize in order to let the
+                 * prefSize to be computed if necessary.
+                 */
+            } else if (graphic instanceof Region && item.getItem() == null) {
+                Region region = (Region) graphic;
+                region.minHeightProperty().bind(heightProperty());
+                region.minWidthProperty().bind(widthProperty());
+            }
+            setGraphic(graphic);
+            /**
+             * In case of a resize of the column, we have new cells that steal
+             * the image from the original TableCell. So we check here if we are
+             * not in that case so that the Graphic of the SpreadsheetCell will
+             * always be on the latest tableView and therefore fully visible.
+             */
+            if (!getChildren().contains(graphic)) {
+                getChildren().add(graphic);
+            }
+        } else {
+            setGraphic(null);
+        }
+    }
+
+//    private final ChangeListener<Number> firstRowLayoutXListener = new ChangeListener<Number>() {
+//        @Override
+//        public void changed(ObservableValue<? extends Number> ov, Number oldLayoutX, Number newLayoutX) {
+//            if (getItem() != null && getItem().getGraphic() != null && newLayoutX.doubleValue() < 0 && oldLayoutX != null) {
+//                getItem().getGraphic().setLayoutX(oldLayoutX.doubleValue());
+//            }
+//        }
+//    };
+//    
+//    private final ChangeListener<Number> firstRowLayoutYListener = new ChangeListener<Number>() {
+//        @Override
+//        public void changed(ObservableValue<? extends Number> ov, Number oldLayoutY, Number newLayoutY) {
+//            if (getItem() != null && getItem().getGraphic() != null && newLayoutY.doubleValue() < 0 && oldLayoutY != null) {
+//                getItem().getGraphic().setLayoutY(oldLayoutY.doubleValue());
+//            }
+//        }
+//    };
+    
+    /**
+     * Return an instance of Editor specific to the Cell type We are not using
+     * the build-in editor-Cell because we cannot know in advance which editor
+     * we will need. Furthermore, we want to control the behavior very closely
+     * in regards of the spanned cell (invisible etc).
+     * 
+     * @param cell
+     *            The SpreadsheetCell
+     * @param bc
+     *            The SpreadsheetCell
+     * @return
+     */
+    private GridCellEditor getEditor(final SpreadsheetCell cell, final SpreadsheetView spv) {
+        SpreadsheetCellType<?> cellType = cell.getCellType();
+        Optional<SpreadsheetCellEditor> cellEditor = spv.getEditor(cellType);
+
+        if (cellEditor.isPresent()) {
+            GridCellEditor editor = handle.getCellsViewSkin().getSpreadsheetCellEditorImpl();
+            /**
+             * Sometimes, we end up here with the editor already editing. But
+             * this case should not happen. If a cell is calling startEdit,
+             * this means we want to edit the cell and the editor should not be
+             * editing another cell. So we just cancel the edition and give the
+             * editor to the cell because we may not be able to edit anything.
+             */
+            if (editor.isEditing()) {
+                if (editor.getModelCell() != null) {
+                    StringBuilder builder = new StringBuilder();
+                    builder.append("The cell at row ").append(editor.getModelCell().getRow())
+                            .append(" and column ").append(editor.getModelCell().getColumn())
+                            .append(" was in edition and cell at row ").append(cell.getRow())
+                            .append(" and column ").append(cell.getColumn())
+                            .append(" requested edition. This situation should not happen as the previous cell should not be in edition.");
+                    Logger.getLogger("root").warning(builder.toString());
+                }
+
+                editor.endEdit(false);
+            }
+            
+            editor.updateSpreadsheetCell(this);
+            editor.updateDataCell(cell);
+            editor.updateSpreadsheetCellEditor(cellEditor.get());
+            return editor;
+        } else {
+            return null;
+        }
+    }
+
+    private final ChangeListener<Node> graphicListener = new ChangeListener<Node>() {
+        @Override
+        public void changed(ObservableValue<? extends Node> arg0, Node arg1, Node newGraphic) {
+            setCellGraphic(getItem());
+        }
+    };
+
+    private final WeakChangeListener<Node> weakGraphicListener = new WeakChangeListener<>(graphicListener);
+    
+    private final SetChangeListener<String> styleClassListener = new SetChangeListener<String>() {
+        @Override
+        public void onChanged(javafx.collections.SetChangeListener.Change<? extends String> arg0) {
+            if (arg0.wasAdded()) {
+                getStyleClass().add(arg0.getElementAdded());
+            } else if (arg0.wasRemoved()) {
+                getStyleClass().remove(arg0.getElementRemoved());
+            }
+        }
+    };
+    
+    private final WeakSetChangeListener<String> weakStyleClassListener = new WeakSetChangeListener<>(styleClassListener);
+    
+    //Listeners for the styles, not initialized by default in order not to impact performance
+    private ChangeListener<String> styleListener;
+    private WeakChangeListener<String> weakStyleListener;
+    
+    /**
+     * Method that will select all the cells between the drag place and that
+     * cell.
+     * 
+     * @param e
+     */
+    private void dragSelect(MouseEvent e) {
+        // If the mouse event is not contained within this tableCell, then
+        // we don't want to react to it.
+        if (!this.contains(e.getX(), e.getY())) {
+            return;
+        }
+        final TableView<ObservableList<SpreadsheetCell>> tableView = getTableView();
+        if (tableView == null) {
+            return;
+        }
+
+        final int count = tableView.getItems().size();
+        if (getIndex() >= count) {
+            return;
+        }
+
+        final TableViewSelectionModel<ObservableList<SpreadsheetCell>> sm = tableView.getSelectionModel();
+        if (sm == null) {
+            return;
+        }
+
+        final int row = getIndex();
+        final int column = tableView.getVisibleLeafIndex(getTableColumn());
+
+        // For spanned Cells
+        final SpreadsheetCell cell = (SpreadsheetCell) getItem();
+        final int rowCell = cell.getRow() + cell.getRowSpan() - 1;
+        final int columnCell = cell.getColumn() + cell.getColumnSpan() - 1;
+
+        final TableViewFocusModel<?> fm = tableView.getFocusModel();
+        if (fm == null) {
+            return;
+        }
+
+        final TablePositionBase<?> focusedCell = fm.getFocusedCell();
+        final MouseButton button = e.getButton();
+        if (button == MouseButton.PRIMARY) {
+            // we add all cells/rows between the current selection focus and
+            // this cell/row (inclusive) to the current selection.
+            final TablePositionBase<?> anchor = getAnchor(tableView, focusedCell);
+
+            /**
+             * FIXME We need to clarify how we want to select the cells. If a
+             * spanned cell is in the way, which minRow/maxRow will be taken?
+             * Where the mouse is exactly? Or where the "motherCell" is? This
+             * needs some thinking.
+             */
+            // and then determine all row and columns which must be selected
+            int minRow = Math.min(anchor.getRow(), row);
+            minRow = Math.min(minRow, rowCell);
+            int maxRow = Math.max(anchor.getRow(), row);
+            maxRow = Math.max(maxRow, rowCell);
+            int minColumn = Math.min(anchor.getColumn(), column);
+            minColumn = Math.min(minColumn, columnCell);
+            int maxColumn = Math.max(anchor.getColumn(), column);
+            maxColumn = Math.max(maxColumn, columnCell);
+
+            // clear selection, but maintain the anchor
+            if (!e.isShortcutDown())
+                sm.clearSelection();
+            if (minColumn != -1 && maxColumn != -1)
+                sm.selectRange(minRow, tableView.getColumns().get(minColumn), maxRow,
+                        tableView.getColumns().get(maxColumn));
+            setAnchor(tableView, anchor);
+        }
+
+    }
+
+    /**
+     * Will safely execute the request on the JFX thread by checking whether we
+     * are on the JFX thread or not.
+     * 
+     * @param runnable
+     */
+    public static void getValue(final Runnable runnable) {
+        if (Platform.isFxApplicationThread()) {
+            runnable.run();
+        } else {
+            Platform.runLater(runnable);
+        }
+    }
+
+    @Override
+    protected javafx.scene.control.Skin<?> createDefaultSkin() {
+        return new CellViewSkin(this);
+    };
+    
+    private final EventHandler<MouseEvent> startFullDragEventHandler = new EventHandler<MouseEvent>() {
+        @Override
+        public void handle(MouseEvent arg0) {
+            if (handle.getGridView().getSelectionModel().getSelectionMode().equals(SelectionMode.MULTIPLE)) {
+                setAnchor(getTableView(), getTableView().getFocusModel().getFocusedCell());
+                startFullDrag();
+            }
+        }
+    };
+    
+    private final EventHandler<MouseEvent> dragMouseEventHandler = new EventHandler<MouseEvent>() {
+        @Override
+        public void handle(MouseEvent arg0) {
+            dragSelect(arg0);
+        }
+    };
+    
+    private final ChangeListener<SpreadsheetCell> itemChangeListener = new ChangeListener<SpreadsheetCell>() {
+
+        @Override
+        public void changed(ObservableValue<? extends SpreadsheetCell> arg0, SpreadsheetCell oldItem,
+                SpreadsheetCell newItem) {
+            if (oldItem != null) {
+                oldItem.getStyleClass().removeListener(weakStyleClassListener);
+                oldItem.graphicProperty().removeListener(weakGraphicListener);
+                
+                if(oldItem.styleProperty() != null){
+                    oldItem.styleProperty().removeListener(weakStyleListener);
+                }
+            }
+            if (newItem != null) {
+         getStyleClass().clear();
+                getStyleClass().setAll(newItem.getStyleClass());
+
+                newItem.getStyleClass().addListener(weakStyleClassListener);
+                setCellGraphic(newItem);
+                newItem.graphicProperty().addListener(weakGraphicListener);
+                
+                if(newItem.styleProperty() != null){
+                    initStyleListener();
+                    newItem.styleProperty().addListener(weakStyleListener);
+                    setStyle(newItem.getStyle());
+                }else{
+                    //We clear the previous style.
+                    setStyle(null);
+                }
+    }
+        }
+    };
+    
+    private void initStyleListener(){
+        if(styleListener == null){
+            styleListener = (ObservableValue<? extends String> observable, String oldValue, String newValue) -> {
+                styleProperty().set(newValue);
+            };
+        }
+        weakStyleListener = new WeakChangeListener<>(styleListener);
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/CellViewSkin.java b/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/CellViewSkin.java
new file mode 100644
index 0000000..a41ff3d
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/CellViewSkin.java
@@ -0,0 +1,224 @@
+/**
+ * Copyright (c) 2013, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.spreadsheet;
+
+import com.sun.javafx.scene.control.skin.TableCellSkin;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.beans.value.WeakChangeListener;
+import javafx.collections.ObservableList;
+import javafx.event.Event;
+import javafx.event.EventHandler;
+import javafx.event.WeakEventHandler;
+import javafx.scene.Node;
+import javafx.scene.control.TableCell;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.Region;
+import org.controlsfx.control.spreadsheet.SpreadsheetCell;
+import org.controlsfx.control.spreadsheet.SpreadsheetCell.CornerPosition;
+
+/**
+ *
+ * This is the skin for the {@link CellView}.
+ *
+ * Its main goal is to draw an object (a triangle) on cells which have their
+ * {@link SpreadsheetCell#commentedProperty()} set to true.
+ *
+ */
+public class CellViewSkin extends TableCellSkin<ObservableList<SpreadsheetCell>, SpreadsheetCell> {
+
+    private final static String TOP_LEFT_CLASS = "top-left"; //$NON-NLS-1$
+    private final static String TOP_RIGHT_CLASS = "top-right"; //$NON-NLS-1$
+    private final static String BOTTOM_RIGHT_CLASS = "bottom-right"; //$NON-NLS-1$
+    private final static String BOTTOM_LEFT_CLASS = "bottom-left"; //$NON-NLS-1$
+    /**
+     * The size of the edge of the triangle FIXME Handling of static variable
+     * will be changed.
+     */
+    private static final int TRIANGLE_SIZE = 8;
+    /**
+     * The region we will add on the cell when necessary.
+     */
+    private Region topLeftRegion = null;
+    private Region topRightRegion = null;
+    private Region bottomRightRegion = null;
+    private Region bottomLeftRegion = null;
+
+    public CellViewSkin(TableCell<ObservableList<SpreadsheetCell>, SpreadsheetCell> tableCell) {
+        super(tableCell);
+        tableCell.itemProperty().addListener(weakItemChangeListener);
+        if (tableCell.getItem() != null) {
+            tableCell.getItem().addEventHandler(SpreadsheetCell.CORNER_EVENT_TYPE, weakTriangleEventHandler);
+        }
+    }
+
+    @Override
+    protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+        /**
+         * If we have an Image in the Cell, its fitHeight will be affected by
+         * the cell height (see CellView). But during calculation for autofit
+         * option, we want to know the real prefHeight of this cell. Apparently,
+         * the fitHeight option is returned by default so we must override and
+         * return the Height of the image inside.
+         */
+        Node graphic = getSkinnable().getGraphic();
+        if (graphic != null && graphic instanceof ImageView) {
+            ImageView view = (ImageView) graphic;
+            if (view.getImage() != null) {
+                return view.getImage().getHeight();
+            }
+        }
+        return super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
+    }
+    
+    @Override
+    protected void layoutChildren(double x, final double y, final double w, final double h) {
+        super.layoutChildren(x, y, w, h);
+        if (getSkinnable().getItem() != null) {
+            layoutTriangle();
+        }
+    }
+
+    private void layoutTriangle() {
+        SpreadsheetCell cell = getSkinnable().getItem();
+        
+        handleTopLeft(cell);
+        handleTopRight(cell);
+        handleBottomLeft(cell);
+        handleBottomRight(cell);
+
+        getSkinnable().requestLayout();
+    }
+
+    private void handleTopLeft(SpreadsheetCell cell) {
+        if (cell.isCornerActivated(CornerPosition.TOP_LEFT)) {
+            if (topLeftRegion == null) {
+                topLeftRegion = getRegion(CornerPosition.TOP_LEFT);
+            }
+            if (!getChildren().contains(topLeftRegion)) {
+                getChildren().add(topLeftRegion);
+            }
+            topLeftRegion.relocate(0, snappedTopInset() - 1);
+        } else if (topLeftRegion != null) {
+            getChildren().remove(topLeftRegion);
+            topLeftRegion = null;
+        }
+    }
+
+    private void handleTopRight(SpreadsheetCell cell) {
+        if (cell.isCornerActivated(CornerPosition.TOP_RIGHT)) {
+            if (topRightRegion == null) {
+                topRightRegion = getRegion(CornerPosition.TOP_RIGHT);
+            }
+            if (!getChildren().contains(topRightRegion)) {
+                getChildren().add(topRightRegion);
+            }
+            topRightRegion.relocate(getSkinnable().getWidth() - TRIANGLE_SIZE, snappedTopInset() - 1);
+        } else if (topRightRegion != null) {
+            getChildren().remove(topRightRegion);
+            topRightRegion = null;
+        }
+    }
+
+    private void handleBottomRight(SpreadsheetCell cell) {
+        if (cell.isCornerActivated(CornerPosition.BOTTOM_RIGHT)) {
+            if (bottomRightRegion == null) {
+                bottomRightRegion = getRegion(CornerPosition.BOTTOM_RIGHT);
+            }
+            if (!getChildren().contains(bottomRightRegion)) {
+                getChildren().add(bottomRightRegion);
+            }
+            bottomRightRegion.relocate(getSkinnable().getWidth() - TRIANGLE_SIZE, getSkinnable().getHeight() - TRIANGLE_SIZE);
+        } else if (bottomRightRegion != null) {
+            getChildren().remove(bottomRightRegion);
+            bottomRightRegion = null;
+        }
+    }
+    private void handleBottomLeft(SpreadsheetCell cell) {
+        if (cell.isCornerActivated(CornerPosition.BOTTOM_LEFT)) {
+            if (bottomLeftRegion == null) {
+                bottomLeftRegion = getRegion(CornerPosition.BOTTOM_LEFT);
+            }
+            if (!getChildren().contains(bottomLeftRegion)) {
+                getChildren().add(bottomLeftRegion);
+            }
+            bottomLeftRegion.relocate(0, getSkinnable().getHeight() - TRIANGLE_SIZE);
+        } else if (bottomLeftRegion != null) {
+            getChildren().remove(bottomLeftRegion);
+            bottomLeftRegion = null;
+        }
+    }
+
+    private static Region getRegion(CornerPosition position) {
+        Region region = new Region();
+        region.resize(TRIANGLE_SIZE, TRIANGLE_SIZE);
+        region.getStyleClass().add("cell-corner"); //$NON-NLS-1$
+        switch (position) {
+            case TOP_LEFT:
+                region.getStyleClass().add(TOP_LEFT_CLASS);
+                break;
+            case TOP_RIGHT:
+                region.getStyleClass().add(TOP_RIGHT_CLASS);
+                break;
+            case BOTTOM_RIGHT:
+                region.getStyleClass().add(BOTTOM_RIGHT_CLASS);
+                break;
+            case BOTTOM_LEFT:
+                region.getStyleClass().add(BOTTOM_LEFT_CLASS);
+                break;
+
+        }
+
+        return region;
+    }
+    
+    private final EventHandler<Event> triangleEventHandler = new EventHandler<Event>() {
+
+        @Override
+        public void handle(Event event) {
+            getSkinnable().requestLayout();
+        }
+    };
+    private final WeakEventHandler weakTriangleEventHandler = new WeakEventHandler(triangleEventHandler);
+
+    private final ChangeListener<SpreadsheetCell> itemChangeListener = new ChangeListener<SpreadsheetCell>() {
+        @Override
+        public void changed(ObservableValue<? extends SpreadsheetCell> arg0, SpreadsheetCell oldCell,
+                SpreadsheetCell newCell) {
+            if (oldCell != null) {
+                oldCell.removeEventHandler(SpreadsheetCell.CORNER_EVENT_TYPE, weakTriangleEventHandler);
+            }
+            if (newCell != null) {
+                newCell.addEventHandler(SpreadsheetCell.CORNER_EVENT_TYPE, weakTriangleEventHandler);
+            }
+            if (getSkinnable().getItem() != null) {
+                layoutTriangle();
+            }
+        }
+    };
+    private final WeakChangeListener<SpreadsheetCell> weakItemChangeListener = new WeakChangeListener<>(itemChangeListener);
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/FocusModelListener.java b/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/FocusModelListener.java
new file mode 100644
index 0000000..a359fb2
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/FocusModelListener.java
@@ -0,0 +1,142 @@
+/**
+ * Copyright (c) 2013, 2014 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.spreadsheet;
+
+import javafx.application.Platform;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.ObservableList;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TablePosition;
+import javafx.scene.control.TableView;
+import org.controlsfx.control.spreadsheet.SpreadsheetCell;
+import org.controlsfx.control.spreadsheet.SpreadsheetView;
+
+/**
+ *
+ * The FocusModel Listener adapted to the SpreadsheetView regarding Span.
+ */
+public class FocusModelListener implements ChangeListener<TablePosition<ObservableList<SpreadsheetCell>, ?>> {
+
+    private final TableView.TableViewFocusModel<ObservableList<SpreadsheetCell>> tfm;
+    private final SpreadsheetGridView cellsView;
+    private final SpreadsheetView spreadsheetView;
+
+    /**
+     * Constructor.
+     *
+     * @param spreadsheetView
+     * @param cellsView
+     */
+    public FocusModelListener(SpreadsheetView spreadsheetView, SpreadsheetGridView cellsView) {
+        tfm = cellsView.getFocusModel();
+        this.spreadsheetView = spreadsheetView;
+        this.cellsView = cellsView;
+    }
+
+    @Override
+    public void changed(ObservableValue<? extends TablePosition<ObservableList<SpreadsheetCell>, ?>> ov,
+            final TablePosition<ObservableList<SpreadsheetCell>, ?> oldPosition,
+            final TablePosition<ObservableList<SpreadsheetCell>, ?> newPosition) {
+        final SpreadsheetView.SpanType spanType = spreadsheetView.getSpanType(newPosition.getRow(), newPosition.getColumn());
+        switch (spanType) {
+            case ROW_SPAN_INVISIBLE:
+                // If we notice that the new focused cell is the previous one,
+                // then it means that we were
+                // already on the cell and we wanted to go below.
+                if (!spreadsheetView.isPressed() && oldPosition.getColumn() == newPosition.getColumn() && oldPosition.getRow() == newPosition.getRow() - 1) {
+                    Platform.runLater(() -> {
+                        tfm.focus(getNextRowNumber(oldPosition, cellsView), oldPosition.getTableColumn());
+                    });
+
+                } else {
+                    // If the current focused cell if hidden by row span, we go
+                    // above
+                    Platform.runLater(() -> {
+                        tfm.focus(newPosition.getRow() - 1, newPosition.getTableColumn());
+                    });
+                }
+
+                break;
+            case BOTH_INVISIBLE:
+                // If the current focused cell if hidden by a both (row and
+                // column) span, we go left-above
+                Platform.runLater(() -> {
+                    tfm.focus(newPosition.getRow() - 1, cellsView.getColumns().get(newPosition.getColumn() - 1));
+                });
+                break;
+            case COLUMN_SPAN_INVISIBLE:
+                // If we notice that the new focused cell is the previous one,
+                // then it means that we were
+                // already on the cell and we wanted to go right.
+                if (!spreadsheetView.isPressed() && oldPosition.getColumn() == newPosition.getColumn() - 1 && oldPosition.getRow() == newPosition.getRow()) {
+
+                    Platform.runLater(() -> {
+                        tfm.focus(oldPosition.getRow(), getTableColumnSpan(oldPosition, cellsView));
+                    });
+                } else {
+                    // If the current focused cell if hidden by column span, we
+                    // go left
+
+                    Platform.runLater(() -> {
+                        tfm.focus(newPosition.getRow(), cellsView.getColumns().get(newPosition.getColumn() - 1));
+                    });
+                }
+            default:
+                break;
+        }
+    }
+
+    /**
+     * Return the TableColumn right after the current TablePosition (including
+     * the ColumSpan to be on a visible Cell)
+     *
+     * @param t the current TablePosition
+     * @return
+     */
+    static TableColumn<ObservableList<SpreadsheetCell>, ?> getTableColumnSpan(final TablePosition<?, ?> t, SpreadsheetGridView cellsView) {
+        return cellsView.getVisibleLeafColumn(t.getColumn()
+                + cellsView.getItems().get(t.getRow()).get(t.getColumn()).getColumnSpan());
+    }
+
+    /**
+     * Return the Row number right after the current TablePosition (including
+     * the RowSpan to be on a visible Cell)
+     *
+     * @param pos
+     * @param cellsView
+     * @return
+     */
+    public static int getNextRowNumber(final TablePosition<?, ?> pos, TableView<ObservableList<SpreadsheetCell>> cellsView) {
+        return cellsView.getItems().get(pos.getRow()).get(pos.getColumn()).getRowSpan()
+                + cellsView.getItems().get(pos.getRow()).get(pos.getColumn()).getRow();
+    }
+    
+    public static int getPreviousRowNumber(final TablePosition<?, ?> pos, TableView<ObservableList<SpreadsheetCell>> cellsView) {
+        return cellsView.getItems().get(pos.getRow()).get(pos.getColumn()).getRow() -1;
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/GridCellEditor.java b/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/GridCellEditor.java
new file mode 100644
index 0000000..5efe627
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/GridCellEditor.java
@@ -0,0 +1,270 @@
+/**
+ * Copyright (c) 2013, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.spreadsheet;
+
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.beans.binding.Bindings;
+import javafx.beans.binding.BooleanExpression;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.event.EventHandler;
+import javafx.scene.Node;
+import javafx.scene.control.Control;
+import javafx.scene.control.TextArea;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyEvent;
+
+import org.controlsfx.control.spreadsheet.SpreadsheetCell;
+import org.controlsfx.control.spreadsheet.SpreadsheetCellEditor;
+import org.controlsfx.control.spreadsheet.SpreadsheetCellType;
+import org.controlsfx.control.spreadsheet.SpreadsheetView;
+
+public class GridCellEditor {
+
+    /***************************************************************************
+     * * Protected/Private Fields * *
+     **************************************************************************/
+
+    private final SpreadsheetHandle handle;
+    // transient properties - these fields will change based on the current
+    // cell being edited.
+    private SpreadsheetCell modelCell;
+    private CellView viewCell;
+    private BooleanExpression focusProperty;
+
+    private boolean editing = false;
+    
+    //The cell's editor 
+    private SpreadsheetCellEditor spreadsheetCellEditor;
+    
+    //The last key pressed in order to select cell below if it was "enter"
+    private KeyCode lastKeyPressed;
+
+    /***************************************************************************
+     * * Constructor * *
+     **************************************************************************/
+
+    /**
+     * Construct the GridCellEditor.
+     */
+    public GridCellEditor(SpreadsheetHandle handle) {
+        this.handle = handle;
+    }
+
+    /***************************************************************************
+     * * Public Methods * *
+     **************************************************************************/
+    /**
+     * Update the internal {@link SpreadsheetCell}.
+     * 
+     * @param cell
+     */
+    public void updateDataCell(SpreadsheetCell cell) {
+        this.modelCell = cell;
+    }
+
+    /**
+     * Update the internal {@link CellView}
+     * 
+     * @param cell
+     */
+    public void updateSpreadsheetCell(CellView cell) {
+        this.viewCell = cell;
+    }
+
+    /**
+     * Update the SpreadsheetCellEditor
+     * 
+     * @param spreadsheetCellEditor
+     */
+    public void updateSpreadsheetCellEditor(final SpreadsheetCellEditor spreadsheetCellEditor) {
+        this.spreadsheetCellEditor = spreadsheetCellEditor;
+    }
+
+    /**
+     * Whenever you want to stop the edition, you call that method.<br/>
+     * True means you're trying to commit the value, then 
+     * {@link SpreadsheetCellType#match(java.lang.Object) } will be called 
+     * in order to verify that the value is correct.<br/>
+     * 
+     * False means you're trying to cancel the value and it will be follow by
+     * {@link #end()}.<br/>
+     * See SpreadsheetCellEditor description
+     * 
+     * @param commitValue true means commit, false means cancel
+     */
+    public void endEdit(boolean commitValue) {
+        if (commitValue && editing) {
+            final SpreadsheetView view = handle.getView();
+            boolean match = modelCell.getCellType().match(spreadsheetCellEditor.getControlValue());
+
+            if (match && viewCell != null) {
+                Object value = modelCell.getCellType().convertValue(spreadsheetCellEditor.getControlValue());
+
+                // We update the value
+                view.getGrid().setCellValue(modelCell.getRow(), modelCell.getColumn(), value);
+                editing = false;
+                viewCell.commitEdit(modelCell);
+                end();
+                spreadsheetCellEditor.end();
+
+                //We select the cell below if "enter" was typed.
+                if (KeyCode.ENTER.equals(lastKeyPressed)) {
+                    handle.getView().getSelectionModel().clearAndSelectNextCell();
+                } else if (KeyCode.TAB.equals(lastKeyPressed)) {
+                    handle.getView().getSelectionModel().clearAndSelectRightCell();
+                    handle.getCellsViewSkin().scrollHorizontally();
+                }
+            }
+        }
+        
+        if (editing) {
+            editing = false;
+            if(viewCell != null){
+                viewCell.cancelEdit();
+            }
+            end();
+            if(spreadsheetCellEditor != null){
+                spreadsheetCellEditor.end();
+            }
+        }
+    }
+
+    /**
+     * Return if this editor is currently being used.
+     * 
+     * @return if this editor is being used.
+     */
+    public boolean isEditing() {
+        return editing;
+    }
+
+    public SpreadsheetCell getModelCell() {
+        return modelCell;
+    }
+
+    /***************************************************************************
+     * * Protected/Private Methods * *
+     **************************************************************************/
+    void startEdit() {
+        //If we do not reset this, it could false the endEdit behavior in case no key was pressed.
+        lastKeyPressed = null;
+        editing = true;
+        
+        handle.getGridView().addEventFilter(KeyEvent.KEY_PRESSED, enterKeyPressed);
+
+        handle.getCellsViewSkin().getVBar().valueProperty().addListener(endEditionListener);
+        handle.getCellsViewSkin().getHBar().valueProperty().addListener(endEditionListener);
+        
+        Control editor = spreadsheetCellEditor.getEditor();
+
+        // Then we call the user editor in order for it to be ready
+        Object value = modelCell.getItem();
+        //We don't want the editor to go beyond the cell boundaries
+        Double maxHeight = Math.min(viewCell.getHeight(), spreadsheetCellEditor.getMaxHeight());
+        
+        if (editor != null) {
+            viewCell.setGraphic(editor);
+            editor.setMaxHeight(maxHeight);
+            editor.setPrefWidth(viewCell.getWidth());
+        }
+
+        spreadsheetCellEditor.startEdit(value);
+        
+        if (editor != null) {
+            focusProperty = getFocusProperty(editor);
+            focusProperty.addListener(focusListener);
+        }
+    }
+
+    private void end() {
+        if(focusProperty != null){
+            focusProperty.removeListener(focusListener);
+            focusProperty = null;
+        }
+        handle.getCellsViewSkin().getVBar().valueProperty().removeListener(endEditionListener);
+        handle.getCellsViewSkin().getHBar().valueProperty().removeListener(endEditionListener);
+        
+        handle.getGridView().removeEventFilter(KeyEvent.KEY_PRESSED, enterKeyPressed);
+
+        this.modelCell = null;
+        this.viewCell = null;
+    }
+
+    /**
+     * If we have a TextArea, we need to return a custom BooleanExpression
+     * because we want to let the editor in place even if the user is touching
+     * the scrollBars inside the textArea.
+     *
+     * @param control
+     * @return
+     */
+    private BooleanExpression getFocusProperty(Control control) {
+        if (control instanceof TextArea) {
+            return Bindings.createBooleanBinding(() -> {
+                if(handle.getView().getScene() == null){
+                    return false;
+                }
+                for (Node n = handle.getView().getScene().getFocusOwner(); n != null; n = n.getParent()) {
+                    if (n == control) {
+                        return true;
+                    }
+                }
+                return false;
+            }, handle.getView().getScene().focusOwnerProperty());
+        } else {
+            return control.focusedProperty();
+        }
+    }
+     
+    /**
+     * When we stop editing a cell, if enter was pressed, we want to go to the next line.
+     */
+    private final EventHandler<KeyEvent> enterKeyPressed = new EventHandler<KeyEvent>() {
+        @Override
+        public void handle(KeyEvent t) {
+            lastKeyPressed = t.getCode();
+        }
+    };
+    
+    private final ChangeListener<Boolean> focusListener = new ChangeListener<Boolean>() {
+        @Override
+        public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean isFocus) {
+            if (!isFocus) {
+                endEdit(true);
+            }
+        }
+    };
+    
+    private final InvalidationListener endEditionListener = new InvalidationListener() {
+        @Override
+        public void invalidated(Observable observable) {
+            endEdit(true);
+        }
+    };
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/GridRow.java b/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/GridRow.java
new file mode 100644
index 0000000..bbffc04
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/GridRow.java
@@ -0,0 +1,161 @@
+/**
+ * Copyright (c) 2013, 2014 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.spreadsheet;
+
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.beans.WeakInvalidationListener;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.collections.MapChangeListener;
+import javafx.collections.ObservableList;
+import javafx.event.Event;
+import javafx.event.EventHandler;
+import javafx.event.WeakEventHandler;
+import javafx.scene.control.Skin;
+import javafx.scene.control.TableRow;
+import javafx.scene.input.MouseEvent;
+import org.controlsfx.control.spreadsheet.SpreadsheetCell;
+import org.controlsfx.control.spreadsheet.SpreadsheetView;
+
+
+/**
+ * 
+ * The tableRow which will holds the SpreadsheetCell.
+ */
+public class GridRow extends TableRow<ObservableList<SpreadsheetCell>> {
+
+    /***************************************************************************
+     * * Private Fields * *
+     **************************************************************************/
+    private final SpreadsheetHandle handle;
+    /**
+     * When the row is fixed, it may have a shift from its original position
+     * which we need in order to layout the cells properly and also for the
+     * rectangle selection.
+     */
+    DoubleProperty verticalShift = new SimpleDoubleProperty();
+
+    /***************************************************************************
+     * * Constructor * *
+     **************************************************************************/
+    public GridRow(SpreadsheetHandle handle) {
+        super();
+        this.handle = handle;
+      
+        /**
+         *  FIXME Bug? When re-using the row, it should re-compute the prefHeight and not
+         *  keep the old value.
+         */
+        this.indexProperty().addListener(weakPrefHeightListener);
+        this.visibleProperty().addListener(weakPrefHeightListener);
+        
+        handle.getView().gridProperty().addListener(weakPrefHeightListener);
+        
+        /**
+         * When the height is changing elsewhere, we need to update ourself if necessary.
+         */
+        handle.getCellsViewSkin().rowHeightMap.addListener(new MapChangeListener<Integer, Double>() {
+
+            @Override
+            public void onChanged(MapChangeListener.Change<? extends Integer, ? extends Double> change) {
+                if (change.wasAdded() && change.getKey() == getIndex()) {
+                    setRowHeight(change.getValueAdded());
+                } else if (change.wasRemoved() && change.getKey() == getIndex()) {
+                    setRowHeight(computePrefHeight(-1));
+                }
+            }
+        });
+        /**
+         * When we are adding deported cells (fixed in columns) into a row via
+         * addCell. The cell is not receiving the DRAG_DETECTED eventHandler
+         * because it's the row that receives it first. If it's the case, we
+         * must give the event to the cell underneath.
+         */
+        this.addEventHandler(MouseEvent.DRAG_DETECTED, weakDragHandler);
+    }
+    /***************************************************************************
+     * * Protected Methods * *
+     **************************************************************************/
+
+    void addCell(CellView cell) {
+        getChildren().add(cell);
+    }
+
+    void removeCell(CellView gc) {
+        getChildren().remove(gc);
+    }
+    
+    SpreadsheetView getSpreadsheetView() {
+        return handle.getView();
+    }
+
+    @Override
+    protected double computePrefHeight(double width) {
+        return handle.getCellsViewSkin().getRowHeight(getIndex());
+    }
+    
+    @Override
+    protected double computeMinHeight(double width) {
+        return handle.getCellsViewSkin().getRowHeight(getIndex());
+    }
+    
+    @Override
+    protected Skin<?> createDefaultSkin() {
+        return new GridRowSkin(handle, this);
+    }
+    
+    private final InvalidationListener setPrefHeightListener = new InvalidationListener() {
+
+        @Override
+        public void invalidated(Observable o) {
+            setRowHeight(computePrefHeight(-1));
+        }
+    };
+    
+    private final WeakInvalidationListener weakPrefHeightListener = new WeakInvalidationListener(setPrefHeightListener);
+    
+    public void setRowHeight(double height) {
+        CellView.getValue(() -> {
+            setHeight(height);
+        });
+        
+        setPrefHeight(height);
+        handle.getCellsViewSkin().rectangleSelection.updateRectangle();
+    }
+    
+    private final EventHandler<MouseEvent> dragDetectedEventHandler = new EventHandler<MouseEvent>() {
+        @Override
+        public void handle(MouseEvent event) {
+            if (event.getTarget().getClass().equals(GridRow.class) && event.getPickResult().getIntersectedNode() != null) {
+                Event.fireEvent(event.getPickResult().getIntersectedNode(), event);
+            }
+        }
+    };
+
+    private final WeakEventHandler<MouseEvent> weakDragHandler = new WeakEventHandler(dragDetectedEventHandler);
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/GridRowSkin.java b/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/GridRowSkin.java
new file mode 100644
index 0000000..a0bd3d1
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/GridRowSkin.java
@@ -0,0 +1,615 @@
+/**
+ * Copyright (c) 2013, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.spreadsheet;
+
+import com.sun.javafx.scene.control.behavior.CellBehaviorBase;
+import com.sun.javafx.scene.control.behavior.TableRowBehavior;
+import com.sun.javafx.scene.control.skin.CellSkinBase;
+import java.lang.ref.Reference;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import javafx.collections.ObservableList;
+import javafx.scene.Node;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableColumnBase;
+import javafx.scene.control.TableRow;
+import org.controlsfx.control.spreadsheet.Grid;
+import org.controlsfx.control.spreadsheet.SpreadsheetCell;
+import org.controlsfx.control.spreadsheet.SpreadsheetColumn;
+import org.controlsfx.control.spreadsheet.SpreadsheetView;
+
+public class GridRowSkin extends CellSkinBase<TableRow<ObservableList<SpreadsheetCell>>, CellBehaviorBase<TableRow<ObservableList<SpreadsheetCell>>>> {
+
+    private final SpreadsheetHandle handle;
+    private final SpreadsheetView spreadsheetView;
+
+    private Reference<HashMap<TableColumnBase, CellView>> cellsMap;
+
+    private final List<CellView> cells = new ArrayList<>();
+
+    public GridRowSkin(SpreadsheetHandle handle, TableRow<ObservableList<SpreadsheetCell>> gridRow) {
+        super(gridRow, new TableRowBehavior<>(gridRow));
+        this.handle = handle;
+        spreadsheetView = handle.getView();
+
+        getSkinnable().setPickOnBounds(false);
+
+        registerChangeListener(gridRow.itemProperty(), "ITEM");
+        registerChangeListener(gridRow.indexProperty(), "INDEX");
+    }
+
+    @Override
+    protected void handleControlPropertyChanged(String p) {
+        super.handleControlPropertyChanged(p);
+
+        if ("INDEX".equals(p)) {
+            // Fix for RT-36661, where empty table cells were showing content, as they
+            // had incorrect table cell indices (but the table row index was correct).
+            // Note that we only do the update on empty cells to avoid the issue
+            // noted below in requestCellUpdate().
+            if (getSkinnable().isEmpty()) {
+                requestCellUpdate();
+            }
+        } else if ("ITEM".equals(p)) {
+            requestCellUpdate();
+        } else if ("FIXED_CELL_SIZE".equals(p)) {
+//            fixedCellSize = fixedCellSizeProperty().get();
+//            fixedCellSizeEnabled = fixedCellSize > 0;
+        }
+    }
+
+    private void requestCellUpdate() {
+        getSkinnable().requestLayout();
+
+        // update the index of all children cells (RT-29849).
+        // Note that we do this after the TableRow item has been updated,
+        // rather than when the TableRow index has changed (as this will be
+        // before the row has updated its item). This will result in the
+        // issue highlighted in RT-33602, where the table cell had the correct
+        // item whilst the row had the old item.
+        final int newIndex = getSkinnable().getIndex();
+        /**
+         * When the index is changing, we need to clear out all the children
+         * because we may end up with useless cell in the row.
+         */
+        getChildren().clear();
+        for (int i = 0, max = cells.size(); i < max; i++) {
+            cells.get(i).updateIndex(newIndex);
+        }
+    }
+
+    @Override
+    protected void layoutChildren(double x, final double y, final double w, final double h) {
+
+        final ObservableList<? extends TableColumnBase<?, ?>> visibleLeafColumns = handle.getGridView().getVisibleLeafColumns();
+        if (visibleLeafColumns.isEmpty()) {
+            super.layoutChildren(x, y, w, h);
+            return;
+        }
+
+        final GridRow control = (GridRow) getSkinnable();
+        final SpreadsheetGridView gridView = (SpreadsheetGridView) handle.getGridView();
+        final Grid grid = spreadsheetView.getGrid();
+        final int index = control.getIndex();
+
+        /**
+         * If this row is out of bounds, this means that the row is displayed
+         * either at the top or at the bottom. In any case, this row is not
+         * meant to be seen so we clear its children list in order not to show
+         * previous TableCell that could be there.
+         */
+        if (index < 0 || index >= gridView.getItems().size()) {
+            getChildren().clear();
+            putCellsInCache();
+            return;
+        }
+
+        final List<SpreadsheetCell> row = grid.getRows().get(index);
+        final List<SpreadsheetColumn> columns = spreadsheetView.getColumns();
+        final ObservableList<TableColumn<ObservableList<SpreadsheetCell>, ?>> tableViewColumns = gridView.getColumns();
+        /**
+         * If we use "setGrid" on SpreadsheetView, we must be careful because we
+         * set our columns after (due to threading safety). So if, by mistake,
+         * we are in layout and the columns are set in SpreadsheetView, but not
+         * in TableView (yet). Then just return and wait for next calling.
+         */
+        if (columns.size() != tableViewColumns.size()) {
+            return;
+        }
+
+        getSkinnable().setVisible(true);
+        // layout the individual column cells
+        double width;
+        double height;
+
+        final double verticalPadding = snappedTopInset() + snappedBottomInset();
+        final double horizontalPadding = snappedLeftInset()
+                + snappedRightInset();
+        /**
+         * Here we make the distinction between the official controlHeight and
+         * the customHeight that we may apply.
+         */
+        double controlHeight = getTableRowHeight(index);
+        double customHeight = controlHeight == Grid.AUTOFIT ? GridViewSkin.DEFAULT_CELL_HEIGHT : controlHeight;
+
+        final GridViewSkin skin = handle.getCellsViewSkin();
+        skin.hBarValue.set(index, true);
+
+        // determine the width of the visible portion of the table
+        double headerWidth = gridView.getWidth();
+        final double hbarValue = skin.getHBar().getValue();
+
+        /**
+         * FOR FIXED ROWS
+         */
+        ((GridRow) getSkinnable()).verticalShift.setValue(getFixedRowShift(index));
+
+        double fixedColumnWidth = 0;
+        List<CellView> fixedCells = new ArrayList();
+
+        //We compute the cells here
+        putCellsInCache();
+
+        boolean firstVisibleCell = false;
+        CellView lastCell = null;
+        boolean needToBeShifted;
+        boolean rowHeightChange = false;
+        for (int indexColumn = 0; indexColumn < columns.size(); indexColumn++) {
+
+            width = snapSize(columns.get(indexColumn).getWidth()) - snapSize(horizontalPadding);
+
+            final SpreadsheetCell spreadsheetCell = row.get(indexColumn);
+            boolean isVisible = !isInvisible(x, width, hbarValue, headerWidth, spreadsheetCell.getColumnSpan());
+
+            if (columns.get(indexColumn).isFixed()) {
+                isVisible = true;
+            }
+
+            if (!isVisible) {
+                if (firstVisibleCell) {
+                    break;
+                }
+                x += width;
+                continue;
+            }
+            final CellView tableCell = getCell(gridView.getColumns().get(indexColumn));
+
+            cells.add(0, tableCell);
+
+            // In case the node was treated previously
+            tableCell.setManaged(true);
+
+            /**
+             * FOR FIXED COLUMNS
+             */
+            double tableCellX = 0;
+
+            /**
+             * We need to update the fixedColumnWidth only on visible cell and
+             * we need to add the full width including the span.
+             *
+             * If we fail to do so, we may be in the situation where x will grow
+             * with the correct width and not fixedColumnWidth. Thus some cell
+             * that should be shifted will not because the computation based on
+             * fixedColumnWidth will be wrong.
+             */
+            boolean increaseFixedWidth = false;
+            //Virtualization of column
+            // We translate that column by the Hbar Value if it's fixed
+            if (columns.get(indexColumn).isFixed()) {
+                if (hbarValue + fixedColumnWidth > x && spreadsheetCell.getColumn() == indexColumn) {
+                    increaseFixedWidth = true;
+                    tableCellX = Math.abs(hbarValue - x + fixedColumnWidth);
+//                	 tableCell.toFront();
+                    fixedColumnWidth += width;
+//                    isVisible = true; // If in fixedColumn, it's obviously visible
+                    fixedCells.add(tableCell);
+                }
+            }
+
+            if (isVisible) {
+                final SpreadsheetView.SpanType spanType = grid.getSpanType(spreadsheetView, index, indexColumn);
+
+                switch (spanType) {
+                    case ROW_SPAN_INVISIBLE:
+                    case BOTH_INVISIBLE:
+                        fixedCells.remove(tableCell);
+                        getChildren().remove(tableCell);
+//                        cells.remove(tableCell);
+                        x += width;
+                        continue; // we don't want to fall through
+                    case COLUMN_SPAN_INVISIBLE:
+                        fixedCells.remove(tableCell);
+                        getChildren().remove(tableCell);
+//                        cells.remove(tableCell);
+                        continue; // we don't want to fall through
+                    case ROW_VISIBLE:
+//                        final TableViewSpanSelectionModel sm = (TableViewSpanSelectionModel) handle.getGridView().getSelectionModel();
+//                        final TableColumn<ObservableList<SpreadsheetCell>, ?> col = tableViewColumns.get(indexColumn);
+
+                    /**
+                     * In case this cell was selected before but we scroll
+                     * up/down and it's invisible now. It has to pass his
+                         * "selected property" to the new Cell in charge of
+                         * spanning
+                     */
+//                        final TablePosition<ObservableList<SpreadsheetCell>, ?> selectedPosition = sm.isSelectedRange(index, col, indexColumn);
+                    // If the selected cell is in the same row, no need to re-select it
+//                        if (selectedPosition != null
+//                                //When shift selecting, all cells become ROW_VISIBLE so
+//                                //We avoid loop selecting here
+//                                && skin.containsRow(index)
+//                                && selectedPosition.getRow() != index) {
+//                            sm.clearSelection(selectedPosition.getRow(),
+//                                    selectedPosition.getTableColumn());
+//                            sm.select(index, col);
+//                        }
+                    case NORMAL_CELL: // fall through and carry on
+                        if (tableCell.getIndex() != index) {
+                            tableCell.updateIndex(index);
+                        } else {
+                            tableCell.updateItem(spreadsheetCell, false);
+                        }
+                        /**
+                         * Here we need to add the cells on the first position
+                         * because this row may contain some deported cells from
+                         * other rows in order to be on top in term of z-order.
+                         * So the cell we're currently adding must not recover
+                         * them.
+                         */
+                        if (tableCell.getParent() == null) {
+                            getChildren().add(0, tableCell);
+                        }
+                }
+
+                if (spreadsheetCell.getColumnSpan() > 1) {
+                    /**
+                     * we need to span multiple columns, so we sum up the width
+                     * of the additional columns, adding it to the width
+                     * variable
+                     */
+                    for (int i = 1, colSpan = spreadsheetCell.getColumnSpan(), max1 = columns
+                            .size() - indexColumn; i < colSpan && i < max1; i++) {
+                        double tempWidth = snapSize(columns.get(indexColumn + i).getWidth());
+                        width += tempWidth;
+                        if (increaseFixedWidth) {
+                            fixedColumnWidth += tempWidth;
+                        }
+                    }
+                }
+
+                /**
+                 * If we are in autofit and the prefHeight of this cell is
+                 * superior to the default cell height. Then we will use this
+                 * new height for row's height.
+                 *
+                 * We then need to apply the value to previous cell, and also
+                 * layout the children because since we are layouting upward,
+                 * next rows needs to know that this row is bigger than usual.
+                 */
+                if (controlHeight == Grid.AUTOFIT && !tableCell.isEditing()) {
+                    double tempHeight = tableCell.prefHeight(width);
+                    if (tempHeight > customHeight) {
+                        rowHeightChange = true;
+                        skin.rowHeightMap.put(index, tempHeight);
+                        for (CellView cell : cells) {
+                            /**
+                             * We need to add the difference between the
+                             * previous height and the new height. If we were
+                             * just setting the new height, the row spanning
+                             * cell would be shorter. That's why we need to use
+                             * the cell height.
+                             */
+                            cell.resize(cell.getWidth(), cell.getHeight() + (tempHeight - customHeight));
+                        }
+                        customHeight = tempHeight;
+                        skin.getFlow().layoutChildren();
+                    }
+                }
+
+                height = customHeight;
+                height = snapSize(height) - snapSize(verticalPadding);
+                /**
+                 * We need to span multiple rows, so we sum up the height of all
+                 * the rows. The height of the current row is ignored and the
+                 * whole value is computed.
+                 */
+                if (spreadsheetCell.getRowSpan() > 1) {
+                    height = 0;
+                    final int maxRow = spreadsheetCell.getRow() + spreadsheetCell.getRowSpan();
+                    for (int i = spreadsheetCell.getRow(); i < maxRow; ++i) {
+                        height += snapSize(skin.getRowHeight(i));
+                    }
+                }
+
+                //Fix for JDK-8146406
+                needToBeShifted = false;
+                /**
+                 * If the current cell has no left border, and the previous cell
+                 * had no right border, and we're fixed. We may have the problem
+                 * where there is a tiny gap between the cells when scrolling
+                 * horizontally. Thus we must enlarge this cell a bit, and shift
+                 * it a bit in order to mask that gap. If the cell has a border
+                 * defined, the problem seems not to happen.
+                 */
+                if (spreadsheetView.getFixedRows().contains(index) 
+                        && lastCell != null 
+                        && !hasRightBorder(lastCell)
+                        && !hasLeftBorder(tableCell)) {
+                    tableCell.resize(width +1, height);
+                    needToBeShifted = true;
+                } else {
+                    tableCell.resize(width, height);
+                }
+                lastCell = tableCell;
+                // We want to place the layout always at the starting cell.
+                double spaceBetweenTopAndMe = 0;
+                for (int p = spreadsheetCell.getRow(); p < index; ++p) {
+                    spaceBetweenTopAndMe += skin.getRowHeight(p);
+                }
+
+                tableCell.relocate(x + tableCellX + (needToBeShifted? -1 : 0), snappedTopInset()
+                        - spaceBetweenTopAndMe + ((GridRow) getSkinnable()).verticalShift.get());
+
+                // Request layout is here as (partial) fix for RT-28684
+//                 tableCell.requestLayout();
+            } else {
+                getChildren().remove(tableCell);
+            }
+            x += width;
+        }
+        skin.fixedColumnWidth = fixedColumnWidth;
+        handleFixedCell(fixedCells, index);
+        removeUselessCell(index);
+        if (handle.getCellsViewSkin().lastRowLayout.get() == true) {
+            handle.getCellsViewSkin().lastRowLayout.setValue(false);
+        }
+        /**
+         * If we modified an height here, ROW_HEIGHT_CHANGE will not be
+         * triggered, because it's not the user who has modified that. So the
+         * rectangle will not update, we need to force it here.
+         */
+        if (rowHeightChange && spreadsheetView.getFixedRows().contains(index)) {
+            skin.computeFixedRowHeight();
+        }
+    }
+
+    private boolean hasRightBorder(CellView tableCell) {
+        return tableCell.getBorder() != null 
+                && !tableCell.getBorder().isEmpty() 
+                && tableCell.getBorder().getStrokes().get(0).getWidths().getRight() > 0;
+    }
+    
+    private boolean hasLeftBorder(CellView tableCell) {
+        return tableCell.getBorder() != null 
+                && !tableCell.getBorder().isEmpty() 
+                && tableCell.getBorder().getStrokes().get(0).getWidths().getLeft()> 0;
+    }
+
+    /**
+     * Here we want to remove of the sceneGraph cells that are not used.
+     *
+     * Before we were removing the cells that we were getting from the cache.
+     * But that is not enough because some cells can be added somehow, and stay
+     * within the row. Since we do not often clear the children because of some
+     * deportedCell present inside, we must use that Predicate to clear all
+     * CellView not contained in cells and with the same index. Thus we preserve
+     * the deported cell.
+     */
+    private void removeUselessCell(int index) {
+        getChildren().removeIf((Node t) -> {
+            if (t instanceof CellView) {
+                return !cells.contains(t) && ((CellView) t).getIndex() == index;
+            }
+            return false;
+        });
+    }
+
+    /**
+     * This handles the fixed cells in column.
+     *
+     * @param fixedCells
+     * @param index
+     */
+    private void handleFixedCell(List<CellView> fixedCells, int index) {
+        if (fixedCells.isEmpty()) {
+            return;
+        }
+
+        /**
+         * If we have a fixedCell (in column) and that cell may be recovered by
+         * a rowSpan, we want to put that tableCell ahead in term of z-order. So
+         * we need to put it in another row.
+         */
+        if (handle.getCellsViewSkin().rowToLayout.get(index)) {
+            GridRow gridRow = handle.getCellsViewSkin().getFlow().getTopRow();
+            if (gridRow != null) {
+                for (CellView cell : fixedCells) {
+                    final double originalLayoutY = getSkinnable().getLayoutY() + cell.getLayoutY();
+                    gridRow.removeCell(cell);
+                    gridRow.addCell(cell);
+                    if (handle.getCellsViewSkin().deportedCells.containsKey(gridRow)) {
+                        handle.getCellsViewSkin().deportedCells.get(gridRow).add(cell);
+                    } else {
+                        Set<CellView> temp = new HashSet<>();
+                        temp.add(cell);
+                        handle.getCellsViewSkin().deportedCells.put(gridRow, temp);
+                    }
+                    /**
+                     * I need to have the layoutY of the original row, but also
+                     * to remove the layoutY of the row I'm adding in. Because
+                     * if the first row is fixed and is undergoing a bit of
+                     * translate in order to be visible, we need to remove that
+                     * "bit of translate".
+                     */
+                    cell.relocate(cell.getLayoutX(), originalLayoutY - gridRow.getLayoutY());
+                }
+            }
+        } else {
+            for (CellView cell : fixedCells) {
+                cell.toFront();
+            }
+        }
+    }
+
+    /**
+     * Return the Cache. Here we use a WeakReference because the WeakHashMap is
+     * not working. TableCell added to it are not removed if the GC wants them.
+     * So we put the whole cache in WeakReference. In normal condition, the
+     * cache is not trashed that much and is efficient. In the case where the
+     * user scroll horizontally a lot, that cache can then be trashed in order
+     * to avoid OutOfMemoryError.
+     *
+     * @return
+     */
+    private HashMap<TableColumnBase, CellView> getCellsMap() {
+        if (cellsMap == null || cellsMap.get() == null) {
+            HashMap<TableColumnBase, CellView> map = new HashMap<>();
+            cellsMap = new WeakReference<>(map);
+            return map;
+        }
+        return cellsMap.get();
+    }
+
+    /**
+     * This will put all current displayed cell into the cache.
+     */
+    private void putCellsInCache() {
+        for (CellView cell : cells) {
+            getCellsMap().put(cell.getTableColumn(), cell);
+        }
+        cells.clear();
+    }
+
+    /**
+     * This will retrieve a cell for the specified column. If the cell exists in
+     * the cache, it's extracted from it. Otherwise, a cell is created.
+     *
+     * @param tcb
+     * @return
+     */
+    private CellView getCell(TableColumnBase tcb) {
+        TableColumn tableColumn = (TableColumn<CellView, ?>) tcb;
+        CellView cell;
+        if (getCellsMap().containsKey(tableColumn)) {
+            return getCellsMap().remove(tableColumn);
+        } else {
+            cell = (CellView) tableColumn.getCellFactory().call(tableColumn);
+            cell.updateTableColumn(tableColumn);
+            cell.updateTableView(tableColumn.getTableView());
+            cell.updateTableRow(getSkinnable());
+        }
+        return cell;
+    }
+
+    /**
+     * Return the space we need to shift that row if it's fixed. Also update the {@link GridViewSkin#getCurrentlyFixedRow()
+     * } .
+     *
+     * @param index
+     * @return
+     */
+    private double getFixedRowShift(int index) {
+        double tableCellY = 0;
+        int positionY = spreadsheetView.getFixedRows().indexOf(index);
+
+        //FIXME Integrate if fixedCellSize is enabled
+        //Computing how much space we need to translate
+        //because each row has different space.
+        double space = 0;
+        for (int o = 0; o < positionY; ++o) {
+            space += handle.getCellsViewSkin().getRowHeight(spreadsheetView.getFixedRows().get(o));
+        }
+
+        //If true, this row is fixed
+        if (positionY != -1 && getSkinnable().getLocalToParentTransform().getTy() <= space) {
+            //This row is a bit hidden on top so we translate then for it to be fully visible
+            tableCellY = space - getSkinnable().getLocalToParentTransform().getTy();
+            handle.getCellsViewSkin().getCurrentlyFixedRow().add(index);
+        } else {
+            handle.getCellsViewSkin().getCurrentlyFixedRow().remove(index);
+        }
+        return tableCellY;
+    }
+
+    /**
+     * Return the height of a row.
+     *
+     * @param row
+     * @return
+     */
+    private double getTableRowHeight(int row) {
+        Double rowHeightCache = handle.getCellsViewSkin().rowHeightMap.get(row);
+        return rowHeightCache == null ? handle.getView().getGrid().getRowHeight(row) : rowHeightCache;
+    }
+
+    /**
+     * Return true if the current cell is part of the sceneGraph.
+     *
+     * @param x beginning of the cell
+     * @param width total width of the cell
+     * @param hbarValue
+     * @param headerWidth width of the visible portion of the tableView
+     * @param columnSpan
+     * @return
+     */
+    private boolean isInvisible(double x, double width, double hbarValue,
+            double headerWidth, int columnSpan) {
+        return (x + width < hbarValue && columnSpan == 1) || (x > hbarValue + headerWidth);
+    }
+
+    @Override
+    protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+        double prefWidth = 0.0;
+
+        final List<? extends TableColumnBase/*<T,?>*/> visibleLeafColumns = handle.getGridView().getVisibleLeafColumns();
+        for (int i = 0, max = visibleLeafColumns.size(); i < max; i++) {
+            prefWidth += visibleLeafColumns.get(i).getWidth();
+        }
+
+        return prefWidth;
+    }
+
+    @Override
+    protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return getSkinnable().getPrefHeight();
+    }
+
+    @Override
+    protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return getSkinnable().getPrefHeight();
+    }
+
+    @Override
+    protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return super.computeMaxHeight(width, topInset, rightInset, bottomInset, leftInset);
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/GridViewBehavior.java b/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/GridViewBehavior.java
new file mode 100644
index 0000000..9ef5a83
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/GridViewBehavior.java
@@ -0,0 +1,509 @@
+/**
+ * Copyright (c) 2015 ControlsFX All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met: *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer. * Redistributions in binary
+ * form must reproduce the above copyright notice, this list of conditions and
+ * the following disclaimer in the documentation and/or other materials provided
+ * with the distribution. * Neither the name of ControlsFX, any associated
+ * website, nor the names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior written
+ * permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.spreadsheet;
+
+import com.sun.javafx.scene.control.behavior.TableViewBehavior;
+import javafx.collections.ObservableList;
+import javafx.scene.control.SelectionMode;
+import javafx.scene.control.TableColumnBase;
+import javafx.scene.control.TableFocusModel;
+import javafx.scene.control.TablePositionBase;
+import javafx.scene.control.TableSelectionModel;
+import javafx.scene.control.TableView;
+import javafx.util.Pair;
+import org.controlsfx.control.spreadsheet.SpreadsheetCell;
+
+/**
+ *
+ * This overrides {@link TableViewBehavior} in order to modify the selection
+ * behavior. The selection will basically work like Excel:
+ *
+ * Selection will always be rectangles. So selection by SHIFT will produce a
+ * rectangle extending your selection.
+ *
+ * Pressing SHORTCUT with an arrow will on a cell will do:
+ *
+ * - If the cell is empty, we go to the next (in the direction) non empty cell.
+ *
+ * - If the cell is not empty, then we either go to the last non empty cell if
+ * the next is not empty, or the first non empty cell if the next is empty.
+ *
+ * This is meant to increase navigation on non empty cell in a Spreadsheet more
+ * easily.
+ *
+ * Pressing SHORTCUT and SHIFT together will behave as the ShortCut previously
+ * explained but the selection will be extended instead of just selecting the
+ * new cell.
+ *
+ */
+public class GridViewBehavior extends TableViewBehavior<ObservableList<SpreadsheetCell>> {
+
+    private GridViewSkin skin;
+
+    public GridViewBehavior(TableView<ObservableList<SpreadsheetCell>> control) {
+        super(control);
+    }
+
+    void setGridViewSkin(GridViewSkin skin) {
+        this.skin = skin;
+    }
+
+    @Override
+    protected void updateCellVerticalSelection(int delta, Runnable defaultAction) {
+        TableViewSpanSelectionModel sm = (TableViewSpanSelectionModel) getSelectionModel();
+        if (sm == null || sm.getSelectionMode() == SelectionMode.SINGLE) {
+            return;
+        }
+
+        TableFocusModel fm = getFocusModel();
+        if (fm == null) {
+            return;
+        }
+
+        final TablePositionBase focusedCell = getFocusedCell();
+
+        if (isShiftDown && getAnchor() != null) {
+
+            final SpreadsheetCell cell = getControl().getItems().get(fm.getFocusedIndex()).get(focusedCell.getColumn());
+            sm.direction = new Pair<>(delta, 0);
+            /**
+             * If the delta is >0, it means we want to go down, so we need to
+             * target the cell that is after our cell. So we need to take the
+             * last row of our cell if spanning. If the delta is < 0, it means
+             * we want to go up, so we just take the row.
+             */
+            int newRow;
+            if (delta < 0) {
+                newRow = cell.getRow() + delta;
+            } else {
+                newRow = cell.getRow() + cell.getRowSpan() - 1 + delta;
+            }
+
+            // we don't let the newRow go outside the bounds of the data
+            newRow = Math.max(Math.min(getItemCount() - 1, newRow), 0);
+
+            final TablePositionBase<?> anchor = getAnchor();
+            int minRow = Math.min(anchor.getRow(), newRow);
+            int maxRow = Math.max(anchor.getRow(), newRow);
+            int minColumn = Math.min(anchor.getColumn(), focusedCell.getColumn());
+            int maxColumn = Math.max(anchor.getColumn(), focusedCell.getColumn());
+
+            sm.clearSelection();
+            if (minColumn != -1 && maxColumn != -1) {
+                sm.selectRange(minRow, getControl().getColumns().get(minColumn), maxRow,
+                        getControl().getColumns().get(maxColumn));
+            }
+            fm.focus(newRow, focusedCell.getTableColumn());
+        } else {
+            final int focusIndex = fm.getFocusedIndex();
+            if (!sm.isSelected(focusIndex, focusedCell.getTableColumn())) {
+                sm.select(focusIndex, focusedCell.getTableColumn());
+            }
+            defaultAction.run();
+        }
+    }
+
+    @Override
+    protected void updateCellHorizontalSelection(int delta, Runnable defaultAction) {
+        TableViewSpanSelectionModel sm = (TableViewSpanSelectionModel) getSelectionModel();
+        if (sm == null || sm.getSelectionMode() == SelectionMode.SINGLE) {
+            return;
+        }
+
+        TableFocusModel fm = getFocusModel();
+        if (fm == null) {
+            return;
+        }
+
+        final TablePositionBase focusedCell = getFocusedCell();
+        if (focusedCell == null || focusedCell.getTableColumn() == null) {
+            return;
+        }
+
+        TableColumnBase adjacentColumn = getColumn(focusedCell.getTableColumn(), delta);
+        if (adjacentColumn == null) {
+            return;
+        }
+
+        final int focusedCellRow = focusedCell.getRow();
+
+        if (isShiftDown && getAnchor() != null) {
+            final int columnPos = getVisibleLeafIndex(focusedCell.getTableColumn());
+
+            final SpreadsheetCell cell = getControl().getItems().get(focusedCellRow).get(columnPos);
+            
+            sm.direction = new Pair<>(0, delta);
+            final int newColumn;// = columnCell + delta;
+            if (delta < 0) {
+                newColumn = cell.getColumn() + delta;
+            } else {
+                newColumn = cell.getColumn() + cell.getColumnSpan() - 1 + delta;
+            }
+            final TablePositionBase<?> anchor = getAnchor();
+            int minRow = Math.min(anchor.getRow(), focusedCellRow);
+            int maxRow = Math.max(anchor.getRow(), focusedCellRow);
+            int minColumn = Math.min(anchor.getColumn(), newColumn);
+            int maxColumn = Math.max(anchor.getColumn(), newColumn);
+
+            sm.clearSelection();
+            if (minColumn != -1 && maxColumn != -1) {
+                sm.selectRange(minRow, getControl().getColumns().get(minColumn), maxRow,
+                        getControl().getColumns().get(maxColumn));
+            }
+            fm.focus(focusedCell.getRow(), getColumn(newColumn));
+        } else {
+            defaultAction.run();
+        }
+
+    }
+
+    @Override
+    protected void focusPreviousRow() {
+        focusVertical(true);
+    }
+
+    @Override
+    protected void focusNextRow() {
+        focusVertical(false);
+    }
+
+    @Override
+    protected void focusLeftCell() {
+        focusHorizontal(true);
+    }
+
+    @Override
+    protected void focusRightCell() {
+        focusHorizontal(false);
+    }
+
+    @Override
+    protected void discontinuousSelectPreviousRow() {
+        discontinuousSelectVertical(true);
+    }
+
+    @Override
+    protected void discontinuousSelectNextRow() {
+        discontinuousSelectVertical(false);
+    }
+
+    @Override
+    protected void discontinuousSelectPreviousColumn() {
+        discontinuousSelectHorizontal(true);
+    }
+
+    @Override
+    protected void discontinuousSelectNextColumn() {
+        discontinuousSelectHorizontal(false);
+    }
+
+    private void focusVertical(boolean previous) {
+        TableSelectionModel sm = getSelectionModel();
+        if (sm == null || sm.getSelectionMode() == SelectionMode.SINGLE) {
+            return;
+        }
+
+        TableFocusModel fm = getFocusModel();
+        if (fm == null) {
+            return;
+        }
+
+        final TablePositionBase focusedCell = getFocusedCell();
+        if (focusedCell == null || focusedCell.getTableColumn() == null) {
+            return;
+        }
+
+        final SpreadsheetCell cell = getControl().getItems().get(focusedCell.getRow()).get(focusedCell.getColumn());
+        sm.clearAndSelect(previous ? findPreviousRow(focusedCell, cell) : findNextRow(focusedCell, cell), focusedCell.getTableColumn());
+        skin.focusScroll();
+    }
+
+    private void focusHorizontal(boolean previous) {
+        TableSelectionModel sm = getSelectionModel();
+        if (sm == null) {
+            return;
+        }
+
+        TableFocusModel fm = getFocusModel();
+        if (fm == null) {
+            return;
+        }
+        final TablePositionBase focusedCell = getFocusedCell();
+        if (focusedCell == null || focusedCell.getTableColumn() == null) {
+            return;
+        }
+
+        final SpreadsheetCell cell = getControl().getItems().get(focusedCell.getRow()).get(focusedCell.getColumn());
+
+        sm.clearAndSelect(focusedCell.getRow(), getControl().getColumns().get(previous ? findPreviousColumn(focusedCell, cell) : findNextColumn(focusedCell, cell)));
+        skin.focusScroll();
+    }
+
+    private int findPreviousRow(TablePositionBase focusedCell, SpreadsheetCell cell) {
+        final ObservableList<ObservableList<SpreadsheetCell>> items = getControl().getItems();
+        SpreadsheetCell temp;
+        //If my cell is empty, I seek the next non-empty
+        if (isEmpty(cell)) {
+            for (int row = focusedCell.getRow() - 1; row >= 0; --row) {
+                temp = items.get(row).get(focusedCell.getColumn());
+                if (!isEmpty(temp)) {
+                    return row;
+                }
+            }
+        } else if (focusedCell.getRow() - 1 >= 0 && !isEmpty(items.get(focusedCell.getRow() - 1).get(focusedCell.getColumn()))) {
+            for (int row = focusedCell.getRow() - 2; row >= 0; --row) {
+                temp = items.get(row).get(focusedCell.getColumn());
+                if (isEmpty(temp)) {
+                    return row + 1;
+                }
+            }
+        } else {
+            //If I'm not empty and the next is empty, I seek the first non empty
+            for (int row = focusedCell.getRow() - 2; row >= 0; --row) {
+                temp = items.get(row).get(focusedCell.getColumn());
+                if (!isEmpty(temp)) {
+                    return row;
+                }
+            }
+        }
+
+        //If we're here, we then select the last on
+        return 0;
+    }
+
+    
+    @Override
+    protected void selectCell(int rowDiff, int columnDiff) {
+        TableViewSpanSelectionModel sm = (TableViewSpanSelectionModel) getSelectionModel();
+        if (sm == null) {
+            return;
+        }
+        sm.direction = new Pair<>(rowDiff, columnDiff);
+
+        TableFocusModel fm = getFocusModel();
+        if (fm == null) {
+            return;
+        }
+
+        TablePositionBase focusedCell = getFocusedCell();
+        int currentRow = focusedCell.getRow();
+        int currentColumn = getVisibleLeafIndex(focusedCell.getTableColumn());
+
+        if (rowDiff < 0 && currentRow <= 0) return;
+        else if (rowDiff > 0 && currentRow >= getItemCount() - 1) return;
+        else if (columnDiff < 0 && currentColumn <= 0) return;
+        else if (columnDiff > 0 && currentColumn >= getVisibleLeafColumns().size() - 1) return;
+        else if (columnDiff > 0 && currentColumn == -1) return;
+
+        TableColumnBase tc = focusedCell.getTableColumn();
+        tc = getColumn(tc, columnDiff);
+
+        int row = focusedCell.getRow() + rowDiff;
+        
+        sm.clearAndSelect(row, tc);
+        setAnchor(row, tc);
+    }
+
+    private int findNextRow(TablePositionBase focusedCell, SpreadsheetCell cell) {
+        final ObservableList<ObservableList<SpreadsheetCell>> items = getControl().getItems();
+        final int itemCount = getItemCount();
+        SpreadsheetCell temp;
+        //If my cell is empty, I seek the next non-empty
+        if (isEmpty(cell)) {
+            for (int row = focusedCell.getRow() + 1; row < itemCount; ++row) {
+                temp = items.get(row).get(focusedCell.getColumn());
+                if (!isEmpty(temp)) {
+                    return row;
+                }
+            }
+        } else if (focusedCell.getRow() + 1 < itemCount && !isEmpty(items.get(focusedCell.getRow() + 1).get(focusedCell.getColumn()))) {
+            for (int row = focusedCell.getRow() + 2; row < getItemCount(); ++row) {
+                temp = items.get(row).get(focusedCell.getColumn());
+                if (isEmpty(temp)) {
+                    return row - 1;
+                }
+            }
+        } else {
+            for (int row = focusedCell.getRow() + 2; row < itemCount; ++row) {
+                temp = items.get(row).get(focusedCell.getColumn());
+                if (!isEmpty(temp)) {
+                    return row;
+                }
+            }
+        }
+        return itemCount - 1;
+    }
+
+    private void discontinuousSelectVertical(boolean previous) {
+        TableSelectionModel sm = getSelectionModel();
+        if (sm == null) {
+            return;
+        }
+
+        TableFocusModel fm = getFocusModel();
+        if (fm == null) {
+            return;
+        }
+        final TablePositionBase focusedCell = getFocusedCell();
+        if (focusedCell == null || focusedCell.getTableColumn() == null) {
+            return;
+        }
+
+        final SpreadsheetCell cell = getControl().getItems().get(fm.getFocusedIndex()).get(focusedCell.getColumn());
+
+        /**
+         * If the delta is >0, it means we want to go down, so we need to target
+         * the cell that is after our cell. So we need to take the last row of
+         * our cell if spanning. If the delta is < 0, it means we want to go up,
+         * so we just take the row.
+         */
+        int newRow = previous ? findPreviousRow(focusedCell, cell) : findNextRow(focusedCell, cell);;
+
+        // we don't let the newRow go outside the bounds of the data
+        newRow = Math.max(Math.min(getItemCount() - 1, newRow), 0);
+
+        final TablePositionBase<?> anchor = getAnchor();
+        int minRow = Math.min(anchor.getRow(), newRow);
+        int maxRow = Math.max(anchor.getRow(), newRow);
+        int minColumn = Math.min(anchor.getColumn(), focusedCell.getColumn());
+        int maxColumn = Math.max(anchor.getColumn(), focusedCell.getColumn());
+
+        sm.clearSelection();
+        if (minColumn != -1 && maxColumn != -1) {
+            sm.selectRange(minRow, getControl().getColumns().get(minColumn), maxRow,
+                    getControl().getColumns().get(maxColumn));
+        }
+        fm.focus(newRow, focusedCell.getTableColumn());
+        skin.focusScroll();
+    }
+
+    private void discontinuousSelectHorizontal(boolean previous) {
+        TableSelectionModel sm = getSelectionModel();
+        if (sm == null) {
+            return;
+        }
+
+        TableFocusModel fm = getFocusModel();
+        if (fm == null) {
+            return;
+        }
+        final TablePositionBase focusedCell = getFocusedCell();
+        if (focusedCell == null || focusedCell.getTableColumn() == null) {
+            return;
+        }
+
+        final int columnPos = getVisibleLeafIndex(focusedCell.getTableColumn());
+        int focusedCellRow = focusedCell.getRow();
+        final SpreadsheetCell cell = getControl().getItems().get(focusedCellRow).get(columnPos);
+
+        final int newColumn = previous ? findPreviousColumn(focusedCell, cell) : findNextColumn(focusedCell, cell);
+
+        final TablePositionBase<?> anchor = getAnchor();
+        int minRow = Math.min(anchor.getRow(), focusedCellRow);
+        int maxRow = Math.max(anchor.getRow(), focusedCellRow);
+        int minColumn = Math.min(anchor.getColumn(), newColumn);
+        int maxColumn = Math.max(anchor.getColumn(), newColumn);
+
+        sm.clearSelection();
+        if (minColumn != -1 && maxColumn != -1) {
+            sm.selectRange(minRow, getControl().getColumns().get(minColumn), maxRow,
+                    getControl().getColumns().get(maxColumn));
+        }
+        fm.focus(focusedCell.getRow(), getColumn(newColumn));
+        skin.focusScroll();
+    }
+
+    private int findNextColumn(TablePositionBase focusedCell, SpreadsheetCell cell) {
+        final ObservableList<ObservableList<SpreadsheetCell>> items = getControl().getItems();
+        final int itemCount = getControl().getColumns().size();
+        SpreadsheetCell temp;
+        //If my cell is empty, I seek the next non-empty
+        if (isEmpty(cell)) {
+            for (int column = focusedCell.getColumn() + 1; column < itemCount; ++column) {
+                temp = items.get(focusedCell.getRow()).get(column);
+                if (!isEmpty(temp)) {
+                    return column;
+                }
+            }
+        } else if (focusedCell.getColumn() + 1 < itemCount && !isEmpty(items.get(focusedCell.getRow()).get(focusedCell.getColumn() + 1))) {
+            for (int column = focusedCell.getColumn() + 2; column < itemCount; ++column) {
+                temp = items.get(focusedCell.getRow()).get(column);
+                if (isEmpty(temp)) {
+                    return column - 1;
+                }
+            }
+        } else {
+            for (int column = focusedCell.getColumn() + 2; column < itemCount; ++column) {
+                temp = items.get(focusedCell.getRow()).get(column);
+                if (!isEmpty(temp)) {
+                    return column;
+                }
+            }
+        }
+        return itemCount - 1;
+    }
+
+    private int findPreviousColumn(TablePositionBase focusedCell, SpreadsheetCell cell) {
+        final ObservableList<ObservableList<SpreadsheetCell>> items = getControl().getItems();
+        SpreadsheetCell temp;
+        //If my cell is empty, I seek the next non-empty
+        if (isEmpty(cell)) {
+            for (int column = focusedCell.getColumn() - 1; column >= 0; --column) {
+                temp = items.get(focusedCell.getRow()).get(column);
+                if (!isEmpty(temp)) {
+                    return column;
+                }
+            }
+        } else if (focusedCell.getColumn() - 1 >= 0 && !isEmpty(items.get(focusedCell.getRow()).get(focusedCell.getColumn() - 1))) {
+            for (int column = focusedCell.getColumn() - 2; column >= 0; --column) {
+                temp = items.get(focusedCell.getRow()).get(column);
+                if (isEmpty(temp)) {
+                    return column + 1;
+                }
+            }
+        } else {
+            for (int column = focusedCell.getColumn() - 2; column >= 0; --column) {
+                temp = items.get(focusedCell.getRow()).get(column);
+                if (!isEmpty(temp)) {
+                    return column;
+                }
+            }
+        }
+        return 0;
+    }
+
+    /**
+     * Cell is empty if there's nothing in it or if we have a NaN instead of a
+     * proper Double.
+     *
+     * @param cell
+     * @return
+     */
+    private boolean isEmpty(SpreadsheetCell cell) {
+        return cell.getGraphic() == null && (cell.getItem() == null
+                || (cell.getItem() instanceof Double && ((Double) cell.getItem()).isNaN()));
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/GridViewSkin.java b/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/GridViewSkin.java
new file mode 100644
index 0000000..7cfcaa3
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/GridViewSkin.java
@@ -0,0 +1,1137 @@
+/**
+ * Copyright (c) 2013, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.spreadsheet;
+
+import java.lang.reflect.Field;
+import java.time.LocalDate;
+import java.util.BitSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.FXCollections;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import javafx.collections.ObservableMap;
+import javafx.collections.ObservableSet;
+import javafx.collections.SetChangeListener;
+import javafx.event.EventHandler;
+import javafx.geometry.HPos;
+import javafx.geometry.VPos;
+import javafx.scene.Node;
+import javafx.scene.control.IndexedCell;
+import javafx.scene.control.ResizeFeaturesBase;
+import javafx.scene.control.TableCell;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableColumnBase;
+import javafx.scene.control.TableFocusModel;
+import javafx.scene.control.TablePositionBase;
+import javafx.scene.control.TableRow;
+import javafx.scene.control.TableSelectionModel;
+import javafx.scene.control.TableView;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.Region;
+import javafx.stage.Screen;
+import javafx.util.Callback;
+
+import org.controlsfx.control.spreadsheet.Grid;
+import org.controlsfx.control.spreadsheet.SpreadsheetCell;
+import org.controlsfx.control.spreadsheet.SpreadsheetColumn;
+import org.controlsfx.control.spreadsheet.SpreadsheetView;
+
+import com.sun.javafx.scene.control.behavior.TableViewBehavior;
+import com.sun.javafx.scene.control.skin.TableHeaderRow;
+import com.sun.javafx.scene.control.skin.TableViewSkinBase;
+import com.sun.javafx.scene.control.skin.VirtualFlow;
+import javafx.application.Platform;
+import javafx.event.Event;
+import javafx.scene.control.ScrollBar;
+
+/**
+ * This skin is actually the skin of the SpreadsheetGridView (tableView)
+ * contained within the SpreadsheetView. The skin for the SpreadsheetView itself
+ * currently resides inside the SpreadsheetView constructor!
+ *
+ * We need to extends directly from TableViewSkinBase in order to work-around
+ * https://javafx-jira.kenai.com/browse/RT-34753 if we want to set a custom
+ * TableViewBehavior.
+ *
+ */
+public class GridViewSkin extends TableViewSkinBase<ObservableList<SpreadsheetCell>,ObservableList<SpreadsheetCell>,TableView<ObservableList<SpreadsheetCell>>,TableViewBehavior<ObservableList<SpreadsheetCell>>,TableRow<ObservableList<SpreadsheetCell>>,TableColumn<ObservableList<SpreadsheetCell>,?>> {
+        
+    /***************************************************************************
+     * * STATIC FIELDS * *
+     **************************************************************************/
+
+    /** Default height of a row. */
+    public static final double DEFAULT_CELL_HEIGHT;
+
+    // FIXME This should seriously be investigated ..
+    private static final double DATE_CELL_MIN_WIDTH = 200 - Screen.getPrimary().getDpi();
+
+    static {
+        double cell_size = 24.0;
+        try {
+            Class<?> clazz = com.sun.javafx.scene.control.skin.CellSkinBase.class;
+            Field f = clazz.getDeclaredField("DEFAULT_CELL_SIZE"); //$NON-NLS-1$
+            f.setAccessible(true);
+            cell_size = f.getDouble(null);
+        } catch (NoSuchFieldException e) {
+            // TODO Auto-generated catch block
+            e.printStackTrace();
+        } catch (SecurityException e) {
+            // TODO Auto-generated catch block
+            e.printStackTrace();
+        } catch (IllegalArgumentException e) {
+            // TODO Auto-generated catch block
+            e.printStackTrace();
+        } catch (IllegalAccessException e) {
+            // TODO Auto-generated catch block
+            e.printStackTrace();
+        }
+        DEFAULT_CELL_HEIGHT = cell_size;
+    }
+
+    /**
+     * When we add some tableCell to some topRow in order for them to be on top
+     * in term of z-order. We may end up with the situation where the row that
+     * put the cell is not in the ViewPort anymore. For example when a fixedRow
+     * has taken over the real row when scrolling down. Then, the tableCell
+     * added is still hanging out in the topRow. That tableCell has no clue that
+     * its "creator" has been destroyed or re-used since that tableCell was not
+     * technically belonging to its "creator". Therefore, we need to track those
+     * cells in order to remove them each time.
+     */
+    final Map<GridRow,Set<CellView>> deportedCells = new HashMap<>();
+    /***************************************************************************
+     * * PRIVATE FIELDS * *
+     **************************************************************************/
+    /**
+     * When resizing, we save the height here in order to override default row
+     * height. package protected.
+     */
+    ObservableMap<Integer, Double> rowHeightMap = FXCollections.observableHashMap();
+
+    /** The editor. */
+    private GridCellEditor gridCellEditor;
+
+    protected final SpreadsheetHandle handle;
+    protected SpreadsheetView spreadsheetView;
+    protected VerticalHeader verticalHeader;
+    protected HorizontalPicker horizontalPickers;
+    
+    /**
+     * The currently fixedRow. This handles an Integer's set of rows being
+     * fixed. NOT Fixable but truly fixed.
+     */
+    private ObservableSet<Integer> currentlyFixedRow = FXCollections.observableSet(new HashSet<Integer>());
+
+    /**
+     * A list of Integer with the current selected Rows. This is useful for
+     * HorizontalHeader and VerticalHeader because they need to highlight when a
+     * selection is made.
+     */
+    private final ObservableList<Integer> selectedRows = FXCollections.observableArrayList();
+
+    /**
+     * A list of Integer with the current selected Columns. This is useful for
+     * HorizontalHeader and VerticalHeader because they need to highlight when a
+     * selection is made.
+     */
+    private final ObservableList<Integer> selectedColumns = FXCollections.observableArrayList();
+
+    /**
+     * The total height of the currently fixedRows.
+     */
+    private double fixedRowHeight = 0;
+
+    /**
+     * These variable try to optimize the layout of the rows in order not to layout
+     * every time every row.
+     * 
+     * So rowToLayout contains the rows that really needs layout(contain span or fixed).
+     * 
+     * And hBarValue is an indicator for the VirtualFlow. When the Hbar is touched, this BitSet
+     * is set to false. And when a row is drawing, it flips its value in this BitSet. 
+     * So that we know when scrolling up or down whether a row has taken into account
+     * that the HBar was moved (otherwise, blank area may appear).
+     */
+    BitSet hBarValue;
+    BitSet rowToLayout;
+    
+    /**
+     * This rectangle will be used for drawing a border around the selection.
+     */
+    RectangleSelection rectangleSelection;
+    
+    /**
+     * This is the current width used by the currently fixed column on the left. 
+     */
+    double fixedColumnWidth;
+    
+    /**
+     * When we try to select cells after a setGrid, we end up with the cell
+     * selected but no visual confirmation. In order to prevent that, we need to
+     * warn the selectionModel when the layout is starting and then the
+     * selectionModel will do the appropriate actions in order to force the
+     * visual to come.
+     */
+    BooleanProperty lastRowLayout = new SimpleBooleanProperty(true);
+    
+    /***************************************************************************
+     * * CONSTRUCTOR * *
+     **************************************************************************/
+    public GridViewSkin(final SpreadsheetHandle handle) {
+        super(handle.getGridView(), new GridViewBehavior(handle.getGridView()));
+        super.init(handle.getGridView());
+        
+        this.handle = handle;
+        this.spreadsheetView = handle.getView();
+        gridCellEditor = new GridCellEditor(handle);
+        TableView<ObservableList<SpreadsheetCell>> tableView = handle.getGridView();
+
+        //Set a new row factory, useful when handling row height.
+        tableView.setRowFactory(new Callback<TableView<ObservableList<SpreadsheetCell>>, TableRow<ObservableList<SpreadsheetCell>>>() {
+            @Override
+            public TableRow<ObservableList<SpreadsheetCell>> call(TableView<ObservableList<SpreadsheetCell>> p) {
+                return new GridRow(handle);
+            }
+        });
+
+        tableView.getStyleClass().add("cell-spreadsheet"); //$NON-NLS-1$
+
+        getCurrentlyFixedRow().addListener(currentlyFixedRowListener);
+        spreadsheetView.getFixedRows().addListener(fixedRowsListener);
+        spreadsheetView.getFixedColumns().addListener(fixedColumnsListener);
+
+        init();
+        /**
+         * When we are changing the grid we re-instantiate the rowToLayout because
+         * spans and fixedRow may have changed.
+         */
+        handle.getView().gridProperty().addListener(new ChangeListener<Grid>() {
+
+            @Override
+            public void changed(ObservableValue<? extends Grid> ov, Grid t, Grid t1) {
+                 rowToLayout = initRowToLayoutBitSet();
+            }
+        });
+        hBarValue = new BitSet(handle.getView().getGrid().getRowCount());
+        rowToLayout = initRowToLayoutBitSet();
+        // Because fixedRow Listener is not reacting first time.
+        computeFixedRowHeight();
+        
+        
+        EventHandler<MouseEvent> ml = (MouseEvent event) -> {
+            // RT-15127: cancel editing on scroll. This is a bit extreme
+            // (we are cancelling editing on touching the scrollbars).
+            // This can be improved at a later date.
+            if (tableView.getEditingCell() != null) {
+                tableView.edit(-1, null); 
+            }
+            
+            // This ensures that the table maintains the focus, even when the vbar
+            // and hbar controls inside the flow are clicked. Without this, the
+            // focus border will not be shown when the user interacts with the
+            // scrollbars, and more importantly, keyboard navigation won't be
+            // available to the user.
+            tableView.requestFocus();
+        };
+        
+        getFlow().getVerticalBar().addEventFilter(MouseEvent.MOUSE_PRESSED, ml);
+        getFlow().getHorizontalBar().addEventFilter(MouseEvent.MOUSE_PRESSED, ml);
+
+        // init the behavior 'closures'
+        TableViewBehavior<ObservableList<SpreadsheetCell>> behavior = getBehavior();
+        behavior.setOnFocusPreviousRow(new Runnable() {
+            @Override public void run() { onFocusPreviousCell(); }
+        });
+        behavior.setOnFocusNextRow(new Runnable() {
+            @Override public void run() { onFocusNextCell(); }
+        });
+        behavior.setOnMoveToFirstCell(new Runnable() {
+            @Override public void run() { onMoveToFirstCell(); }
+        });
+        behavior.setOnMoveToLastCell(new Runnable() {
+            @Override public void run() { onMoveToLastCell(); }
+        });
+        behavior.setOnScrollPageDown(new Callback<Boolean, Integer>() {
+            @Override public Integer call(Boolean isFocusDriven) { return onScrollPageDown(isFocusDriven); }
+        });
+        behavior.setOnScrollPageUp(new Callback<Boolean, Integer>() {
+            @Override public Integer call(Boolean isFocusDriven) { return onScrollPageUp(isFocusDriven); }
+        });
+        behavior.setOnSelectPreviousRow(new Runnable() {
+            @Override public void run() { onSelectPreviousCell(); }
+        });
+        behavior.setOnSelectNextRow(new Runnable() {
+            @Override public void run() { onSelectNextCell(); }
+        });
+        behavior.setOnSelectLeftCell(new Runnable() {
+            @Override public void run() { onSelectLeftCell(); }
+        });
+        behavior.setOnSelectRightCell(new Runnable() {
+            @Override public void run() { onSelectRightCell(); }
+        });
+
+        registerChangeListener(tableView.fixedCellSizeProperty(), "FIXED_CELL_SIZE");
+    }
+
+    /**
+     * Compute the height of a particular row. If the row is in
+     * {@link Grid#AUTOFIT}, {@link #DEFAULT_CELL_HEIGHT} is returned.
+     *
+     * @param row
+     * @return
+     */
+    public double getRowHeight(int row) {
+        Double rowHeightCache = rowHeightMap.get(row);
+        if (rowHeightCache == null) {
+            double rowHeight = handle.getView().getGrid().getRowHeight(row);
+            return rowHeight == Grid.AUTOFIT ? DEFAULT_CELL_HEIGHT : rowHeight;
+        } else {
+            return rowHeightCache;
+        }
+    }
+
+    public double getFixedRowHeight() {
+        return fixedRowHeight;
+    }
+
+    public ObservableList<Integer> getSelectedRows() {
+        return selectedRows;
+    }
+
+    public ObservableList<Integer> getSelectedColumns() {
+        return selectedColumns;
+    }
+
+    public GridCellEditor getSpreadsheetCellEditorImpl() {
+        return gridCellEditor;
+    }
+
+    /**
+     * This return the GridRow which has the specified index if found. Otherwise
+     * null is returned.
+     *
+     * @param index
+     * @return
+     */
+    public GridRow getRowIndexed(int index) {
+        List<? extends IndexedCell> cells = getFlow().getCells();
+        if (!cells.isEmpty()) {
+            IndexedCell cell = cells.get(0);
+            if (index >= cell.getIndex() && index - cell.getIndex() < cells.size()) {
+                return (GridRow) cells.get(index - cell.getIndex());
+            }
+        }
+        for (IndexedCell cell : getFlow().getFixedCells()) {
+            if (cell.getIndex() == index) {
+                return (GridRow) cell;
+            }
+        }
+        return null;
+    }
+    
+    /**
+     * This return the row at the specified index in the list. The index
+     * specified HAS NOTHING to do with the index of the row.
+     * @see #getRowIndexed(int) for a getting a row with its real index.
+     * @param index
+     * @return
+     */
+    public GridRow getRow(int index) {
+        return (GridRow) getFlow().getCells().get(index);
+    }
+
+    /**
+     * Indicate whether or not the row at the specified index is currently being
+     * displayed.
+     * 
+     * @param index
+     * @return
+     */
+    public final boolean containsRow(int index) {
+        for (Object obj : getFlow().getCells()) {
+            if (((GridRow) obj).getIndex() == index)
+                return true;
+        }
+        return false;
+    }
+
+    public int getCellsSize() {
+        return getFlow().getCells().size();
+    }
+
+    public ScrollBar getHBar() {
+        if (getFlow() != null) {
+            return getFlow().getHorizontalBar();
+        }
+        return null;
+    }
+
+    public ScrollBar getVBar() {
+        return getFlow().getVerticalBar();
+    }
+
+    /**
+     * Will compute for every row the necessary height and fit the line.
+     * This can degrade performance a lot so need to use it wisely. 
+     * But I don't see other solutions right now.
+     */
+    public void resizeRowsToFitContent() {
+        Grid grid = spreadsheetView.getGrid();
+        int maxRows = handle.getView().getGrid().getRowCount();
+        for (int row = 0; row < maxRows; row++) {
+            if (grid.isRowResizable(row)) {
+                resizeRowToFitContent(row);
+            }
+        }
+    }
+    
+    /**
+     * Will compute for the row the necessary height and fit the line.
+     * This can degrade performance a lot so need to use it wisely. 
+     * But I don't see other solutions right now.
+     * @param row
+     */
+    public void resizeRowToFitContent(int row) {
+        if(getSkinnable().getColumns().isEmpty()){
+            return;
+        }
+        final TableColumn<ObservableList<SpreadsheetCell>, ?> col = getSkinnable().getColumns().get(0);
+        List<?> items = itemsProperty().get();
+        if (items == null || items.isEmpty()) {
+            return;
+        }
+        
+        if (!spreadsheetView.getGrid().isRowResizable(row)) {
+            return;
+        }
+        Callback/* <TableColumn<T, ?>, TableCell<T,?>> */ cellFactory = col.getCellFactory();
+        if (cellFactory == null) {
+            return;
+        }
+
+        CellView cell = (CellView) cellFactory.call(col);
+        if (cell == null) {
+            return;
+        }
+
+        // set this property to tell the TableCell we want to know its actual
+        // preferred width, not the width of the associated TableColumnBase
+        cell.getProperties().put("deferToParentPrefWidth", Boolean.TRUE); //$NON-NLS-1$
+        
+        // determine cell padding
+        double padding = 5;
+
+        Node n = cell.getSkin() == null ? null : cell.getSkin().getNode();
+        if (n instanceof Region) {
+            Region r = (Region) n;
+            padding = r.snappedTopInset() + r.snappedBottomInset();
+        }
+
+        double maxHeight;
+        maxHeight = 0;
+        getChildren().add(cell);
+
+        for (TableColumn column : getSkinnable().getColumns()) {
+            cell.updateTableColumn(column);
+            cell.updateTableView(handle.getGridView());
+            cell.updateIndex(row);
+
+            if ((cell.getText() != null && !cell.getText().isEmpty()) || cell.getGraphic() != null) {
+                cell.setWrapText(true);
+
+                cell.impl_processCSS(false);
+                maxHeight = Math.max(maxHeight, cell.prefHeight(column.getWidth()));
+            }
+        }
+        getChildren().remove(cell);
+        rowHeightMap.put(row, maxHeight + padding);
+        Event.fireEvent(spreadsheetView, new SpreadsheetView.RowHeightEvent(row, maxHeight + padding));
+
+        rectangleSelection.updateRectangle();
+    }
+    
+    public void resizeRowsToMaximum() {
+        //First we resize to fit.
+        resizeRowsToFitContent();
+        
+        Grid grid = spreadsheetView.getGrid();
+        
+        //Then we take the maximum and apply it everywhere.
+        double maxHeight = 0;
+        for(int key:rowHeightMap.keySet()){
+            maxHeight = Math.max(maxHeight, rowHeightMap.get(key));
+        }
+        
+        rowHeightMap.clear();
+        int maxRows = handle.getView().getGrid().getRowCount();
+        for (int row = 0; row < maxRows; row++) {
+            if (grid.isRowResizable(row)) {
+                Event.fireEvent(spreadsheetView, new SpreadsheetView.RowHeightEvent(row, maxHeight));
+                rowHeightMap.put(row, maxHeight);
+            }
+        }
+        rectangleSelection.updateRectangle();
+    }
+    
+    public void resizeRowsToDefault() {
+        rowHeightMap.clear();
+        Grid grid = spreadsheetView.getGrid();
+        /**
+         * When resizing to default, we need to go through the visible rows in
+         * order to update them directly. Because if the rowHeightMap is empty,
+         * the rows will not detect that maybe the height has changed.
+         */
+        for (GridRow row : (List<GridRow>) getFlow().getCells()) {
+            if (grid.isRowResizable(row.getIndex())) {
+                double newHeight = row.computePrefHeight(-1);
+                if (row.getPrefHeight() != newHeight) {
+                    row.setRowHeight(newHeight);
+                    row.requestLayout();
+                }
+            }
+        }
+
+        //Fixing https://bitbucket.org/controlsfx/controlsfx/issue/358/
+        getFlow().layoutChildren();
+
+        for (GridRow row : (List<GridRow>) getFlow().getCells()) {
+            double height = getRowHeight(row.getIndex());
+            if (row.getHeight() != height) {
+                if (grid.isRowResizable(row.getIndex())) {
+                    row.setRowHeight(height);
+                }
+            }
+        }
+        rectangleSelection.updateRectangle();
+    }
+    /**
+     * We want to have extra space when displaying LocalDate because they will
+     * use an editor that display a little icon on the right. Thus, that icon is
+     * reducing the visibility of the date string.
+     */
+    @Override
+    public void resizeColumnToFitContent(TableColumn<ObservableList<SpreadsheetCell>, ?> tc, int maxRows) {
+        
+        final TableColumn<ObservableList<SpreadsheetCell>, ?> col = tc;
+        List<?> items = itemsProperty().get();
+        if (items == null || items.isEmpty()) {
+            return;
+        }
+
+        Callback/* <TableColumn<T, ?>, TableCell<T,?>> */ cellFactory = col.getCellFactory();
+        if (cellFactory == null) {
+            return;
+        }
+
+        TableCell<ObservableList<SpreadsheetCell>, ?> cell = (TableCell<ObservableList<SpreadsheetCell>, ?>) cellFactory
+                .call(col);
+        if (cell == null) {
+            return;
+        }
+
+        //The current index of that column
+        int indexColumn = handle.getGridView().getColumns().indexOf(tc);
+        
+        /**
+         * This is to prevent resize of columns that have the same default width
+         * at initialisation. If the "system" is calling this method, the
+         * maxRows will be set at 30. When we set a prefWidth and it's equal to
+         * the "default width", the system wants to resize the column. We must
+         * prevent that, thus we check if the two conditions are met.
+         */
+        if(maxRows == 30 && handle.isColumnWidthSet(indexColumn)){
+            return;
+        }
+        
+        // set this property to tell the TableCell we want to know its actual
+        // preferred width, not the width of the associated TableColumnBase
+        cell.getProperties().put("deferToParentPrefWidth", Boolean.TRUE); //$NON-NLS-1$
+        
+        // determine cell padding
+        double padding = 10;
+        Node n = cell.getSkin() == null ? null : cell.getSkin().getNode();
+        if (n instanceof Region) {
+            Region r = (Region) n;
+            padding = r.snappedLeftInset() + r.snappedRightInset();
+        }
+
+        ObservableList<ObservableList<SpreadsheetCell>> gridRows = spreadsheetView.getGrid().getRows();//.get(row)
+        
+        /**
+         * If maxRows is -1, we take all rows. If it's 30, it means it's coming
+         * from TableColumnHeader during initialization, so we push it to 100.
+         */
+        int rows = maxRows == -1 ? items.size() : Math.min(items.size(), maxRows == 30 ? 100 : maxRows);
+        double maxWidth = 0;
+        boolean datePresent = false;
+        cell.updateTableColumn(col);
+        cell.updateTableView(handle.getGridView());
+        /**
+         * Sometime the skin is not set, and the width computed is zero which
+         * destroy the grid... So in that case, we manually set the skin...
+         */
+        if (cell.getSkin() == null) {
+            cell.setSkin(new CellViewSkin((TableCell<ObservableList<SpreadsheetCell>, SpreadsheetCell>) cell));
+        }
+        for (int row = 0; row < rows; row++) {
+            cell.updateIndex(row);
+            
+            if ((cell.getText() != null && !cell.getText().isEmpty()) || cell.getGraphic() != null) {
+                getChildren().add(cell);
+
+                if (((SpreadsheetCell) cell.getItem()).getItem() instanceof LocalDate) {
+                    datePresent = true;
+                }
+                cell.impl_processCSS(false);
+                double width = cell.prefWidth(-1);
+                /**
+                 * If the cell is spanning in column, we need to take the other
+                 * columns into account in the calculation of the width. So we
+                 * compute the width needed by the cell and we substract the
+                 * other columns width.
+                 *
+                 * Also if the cell considered is not in the column, we still
+                 * have to compute because a previous column may have based its
+                 * calculation on the current width which will be modified.
+                 */
+                SpreadsheetCell spc = gridRows.get(row).get(indexColumn);
+                if (spc.getColumnSpan() > 1) {
+                    for (int i = spc.getColumn(); i < spc.getColumn() + spc.getColumnSpan(); ++i) {
+                        if(i != indexColumn){
+                            width -= spreadsheetView.getColumns().get(i).getWidth();
+                        }
+                    }
+                }
+                maxWidth = Math.max(maxWidth, width);
+                getChildren().remove(cell);
+            }
+        }
+
+        // dispose of the cell to prevent it retaining listeners (see RT-31015)
+        cell.updateIndex(-1);
+
+        // RT-23486
+        double widthMax = maxWidth + padding;
+        if (handle.getGridView().getColumnResizePolicy() == TableView.CONSTRAINED_RESIZE_POLICY) {
+            widthMax = Math.max(widthMax, col.getWidth());
+        }
+
+        if (datePresent && widthMax < DATE_CELL_MIN_WIDTH) {
+            widthMax = DATE_CELL_MIN_WIDTH;
+        }
+        /**
+         * This method is called by the system at initialisation and later by
+         * some methods that check wether the specified column is resizable. So
+         * we do not check if the column is resizable because it will be checked
+         * before. If we end up here, it either means the column is resizable,
+         * OR this is the initialisation and we haven't set a specific width so
+         * we just compute one time the correct width for that column, and once
+         * set, it will not be called again.
+         *
+         * Also, if the prefWidth has already been set but the user resized the
+         * column with his mouse, we must force the column to resize because
+         * setting the prefWidth again will not trigger the listeners.
+         */
+        widthMax = snapSize(widthMax);
+        if (col.getPrefWidth() == widthMax && col.getWidth() != widthMax) {
+            col.impl_setWidth(widthMax);
+        } else {
+            col.setPrefWidth(widthMax);
+        }
+        
+        rectangleSelection.updateRectangle();
+    }
+
+    /***************************************************************************
+     * * PRIVATE/PROTECTED METHOD * *
+     **************************************************************************/
+    protected final void init() {
+        rectangleSelection = new RectangleSelection(this, (TableViewSpanSelectionModel) handle.getGridView().getSelectionModel());
+        getFlow().getVerticalBar().valueProperty().addListener(vbarValueListener);
+        verticalHeader = new VerticalHeader(handle);
+        getChildren().add(verticalHeader);
+
+        ((HorizontalHeader) getTableHeaderRow()).init();
+        verticalHeader.init(this, (HorizontalHeader) getTableHeaderRow());
+        
+        horizontalPickers = new HorizontalPicker((HorizontalHeader) getTableHeaderRow(), spreadsheetView);
+        getChildren().add(horizontalPickers);
+        getFlow().init(spreadsheetView);
+        ((GridViewBehavior)getBehavior()).setGridViewSkin(this);
+    }
+
+    protected final ObservableSet<Integer> getCurrentlyFixedRow() {
+        return currentlyFixedRow;
+    }
+
+    /**
+     * Used in the HorizontalColumnHeader when we need to resize in double
+     * click.
+     * 
+     * @param tc
+     * @param maxRows
+     */
+    public void resize(TableColumnBase<?, ?> tc, int maxRows) {
+        if(tc.isResizable()){
+            int columnIndex = getColumns().indexOf(tc);
+            TableColumn tableColumn = getColumns().get(columnIndex);
+            resizeColumnToFitContent(tableColumn, maxRows);
+            Event.fireEvent(spreadsheetView, new SpreadsheetView.ColumnWidthEvent(columnIndex, tableColumn.getWidth()));
+        }
+    }
+
+    @Override
+    protected void layoutChildren(double x, double y, double w, final double h) {
+        if (spreadsheetView == null) {
+            return;
+        }
+        double verticalHeaderWidth = verticalHeader.computeHeaderWidth();
+        double horizontalPickerHeight = spreadsheetView.getColumnPickers().isEmpty() ? 0: VerticalHeader.PICKER_SIZE;
+        
+        if (spreadsheetView.isShowRowHeader() || !spreadsheetView.getRowPickers().isEmpty()) {
+            x += verticalHeaderWidth;
+            w -= verticalHeaderWidth;
+        } else {
+            x = 0.0;
+        }
+
+        
+        y += horizontalPickerHeight;
+        super.layoutChildren(x, y, w, h-horizontalPickerHeight);
+
+        final double baselineOffset = getSkinnable().getLayoutBounds().getHeight() / 2;
+        double tableHeaderRowHeight = 0;
+
+        if(!spreadsheetView.getColumnPickers().isEmpty()){
+            layoutInArea(horizontalPickers, x, y - VerticalHeader.PICKER_SIZE, w, tableHeaderRowHeight, baselineOffset, HPos.CENTER, VPos.CENTER);
+        }
+        
+        if (spreadsheetView.showColumnHeaderProperty().get()) {
+            // position the table header
+            tableHeaderRowHeight = getTableHeaderRow().prefHeight(-1);
+            layoutInArea(getTableHeaderRow(), x, y, w, tableHeaderRowHeight, baselineOffset, HPos.CENTER, VPos.CENTER);
+
+            y += tableHeaderRowHeight;
+        } else {
+            // This is temporary handled in the HorizontalHeader with Css
+            // FIXME tweak open in https://javafx-jira.kenai.com/browse/RT-32673
+        }
+
+        if (spreadsheetView.isShowRowHeader() || !spreadsheetView.getRowPickers().isEmpty()) {
+            layoutInArea(verticalHeader, x - verticalHeaderWidth, y - tableHeaderRowHeight, w, h, baselineOffset,
+                    HPos.CENTER, VPos.CENTER);
+        }
+    }
+
+    @Override
+    protected void onFocusPreviousCell() {
+        focusScroll();
+    }
+
+    @Override
+    protected void onFocusNextCell() {
+        focusScroll();
+    }
+
+    void focusScroll() {
+        final TableFocusModel<?, ?> fm = getFocusModel();
+        if (fm == null) {
+            return;
+        }
+        /**
+         * ***************************************************************
+         * MODIFIED
+         ****************************************************************
+         */
+        final int row = fm.getFocusedIndex();
+        // We try to make visible the rows that may be hidden by Fixed rows
+        if (!getFlow().getCells().isEmpty()
+                && getFlow().getCells().get(spreadsheetView.getFixedRows().size()).getIndex() > row
+                && !spreadsheetView.getFixedRows().contains(row)) {
+            flow.scrollTo(row);
+        } else {
+            flow.show(row);
+        }
+        scrollHorizontally();
+        /**
+         * ***************************************************************
+         * END OF MODIFIED
+         ****************************************************************
+         */
+    }
+    
+    @Override
+    protected void onSelectPreviousCell() {
+        super.onSelectPreviousCell();
+        scrollHorizontally();
+    }
+
+    @Override
+    protected void onSelectNextCell() {
+        super.onSelectNextCell();
+        scrollHorizontally();
+    }
+
+    @Override
+    protected VirtualFlow<TableRow<ObservableList<SpreadsheetCell>>> createVirtualFlow() {
+        return new GridVirtualFlow<>(this);
+    }
+
+    @Override
+    protected TableHeaderRow createTableHeaderRow() {
+        return new HorizontalHeader(this);
+    }
+    
+    protected HorizontalHeader getHorizontalHeader(){
+        return (HorizontalHeader) getTableHeaderRow();
+    }
+
+    BooleanProperty getTableMenuButtonVisibleProperty() {
+        return tableMenuButtonVisibleProperty();
+    }
+
+    @Override
+    public void scrollHorizontally(){
+        super.scrollHorizontally();
+    }
+    
+    @Override
+    protected void scrollHorizontally(TableColumn<ObservableList<SpreadsheetCell>, ?> col) {
+        if (col == null || !col.isVisible()) {
+            return;
+        }
+        /**
+         * We modified this function so that we ensure that any selected cells
+         * will not be below a fixed column. Because when there's some fixed
+         * columns, the "left border" is not the table anymore, but the right
+         * side of the last fixed columns.
+         *
+         * Moreover, we need to re-compute the fixedColumnWidth because the
+         * layout of the rows hasn't been done yet and the value is not right.
+         * So we might end up below a fixedColumns.
+         */
+        
+        fixedColumnWidth = 0;
+        final double pos = getFlow().getHorizontalBar().getValue();
+        int index = getColumns().indexOf(col);
+        double start = 0;// scrollX;
+
+        for (int i = 0; i < index; ++i) {
+            SpreadsheetColumn column = spreadsheetView.getColumns().get(i);
+            if (column.isFixed()) {
+                fixedColumnWidth += column.getWidth();
+            }
+            start += column.getWidth();
+        }
+
+        final double end = start + col.getWidth();
+
+        // determine the visible width of the table
+        final double headerWidth = handle.getView().getWidth() - snappedLeftInset() - snappedRightInset() - verticalHeader.getVerticalHeaderWidth();
+
+        // determine by how much we need to translate the table to ensure that
+        // the start position of this column lines up with the left edge of the
+        // tableview, and also that the columns don't become detached from the
+        // right edge of the table
+        final double max = getFlow().getHorizontalBar().getMax();
+        double newPos;
+
+        /**
+         * If the starting position of our column if inferior to the left egde
+         * (of tableView or fixed columns), then we need to scroll.
+         */
+        if (start < pos + fixedColumnWidth && start >= 0 && start >= fixedColumnWidth) {
+            newPos = start - fixedColumnWidth < 0 ? start : start - fixedColumnWidth;
+            getFlow().getHorizontalBar().setValue(newPos);
+        //If the starting point is not visible on the right.    
+        } else if(start > pos + headerWidth){
+            final double delta = start < 0 || end > headerWidth ? start - pos - fixedColumnWidth : 0;
+            newPos = pos + delta > max ? max : pos + delta;
+            getFlow().getHorizontalBar().setValue(newPos);
+        }
+        /**
+         * In all other cases, it means the cell is visible so no scroll needed,
+         * because otherwise we may end up with a continous scroll that always
+         * place the selected cell in the center of the screen.
+         */
+    }
+
+    private void verticalScroll() {
+        verticalHeader.requestLayout();
+    }
+
+    GridVirtualFlow<?> getFlow() {
+        return (GridVirtualFlow<?>) flow;
+    }
+
+    /**
+     * Return a BitSet of the rows that needs layout all the time. This
+     * includes any row containing a span, or a fixed row.
+     * @return 
+     */
+    private BitSet initRowToLayoutBitSet(){
+        Grid grid =  handle.getView().getGrid();
+        BitSet bitSet = new BitSet(grid.getRowCount());
+        for(int row = 0;row<grid.getRowCount();++row){
+            if(spreadsheetView.getFixedRows().contains(row)){
+                bitSet.set(row);
+                continue;
+            }
+            List<SpreadsheetCell> myRow = grid.getRows().get(row);
+            for(SpreadsheetCell cell:myRow){
+                
+                if(cell.getRowSpan()>1 /*|| cell.getColumnSpan() >1*/){
+                    bitSet.set(row);
+                    break;
+                }
+            }
+        }
+        return bitSet;
+    }
+    
+    /**
+     * When the vertical moves, we update the verticalHeader
+     */
+    private final InvalidationListener vbarValueListener = new InvalidationListener() {
+        @Override
+        public void invalidated(Observable valueModel) {
+            verticalScroll();
+        }
+    };
+
+    /**
+     * We listen on the FixedRows in order to do the modification in the
+     * VirtualFlow
+     */
+    private final ListChangeListener<Integer> fixedRowsListener = new ListChangeListener<Integer>() {
+        @Override
+        public void onChanged(Change<? extends Integer> c) {
+            hBarValue.clear();
+            while (c.next()) {
+                if (c.wasPermutated()) {
+                    for (Integer fixedRow : c.getList()) {
+                        rowToLayout.set(fixedRow, true);
+                    }
+                } else {
+                    for (Integer unfixedRow : c.getRemoved()) {
+                        rowToLayout.set(unfixedRow, false);
+                    //If the grid permits it, we check the spanning in order not
+                        //to remove a row that might need layout.
+                        if (spreadsheetView.getGrid().getRows().size() > unfixedRow) {
+                            List<SpreadsheetCell> myRow = spreadsheetView.getGrid().getRows().get(unfixedRow);
+                            for (SpreadsheetCell cell : myRow) {
+                                if (cell.getRowSpan() > 1 || cell.getColumnSpan() > 1) {
+                                    rowToLayout.set(unfixedRow, true);
+                                    break;
+                                }
+                            }
+                        }
+                    }
+
+                    //We check for the newly fixedRow
+                    for (Integer fixedRow : c.getAddedSubList()) {
+                        rowToLayout.set(fixedRow, true);
+                    }
+                }
+            }
+            // requestLayout() not responding immediately..
+            getFlow().requestLayout();
+        }
+    };
+
+    /**
+     * We listen on the currentlyFixedRow in order to do the modification in the
+     * FixedRowHeight.
+     */
+    private final SetChangeListener<? super Integer> currentlyFixedRowListener = new SetChangeListener<Integer>() {
+        @Override
+        public void onChanged(javafx.collections.SetChangeListener.Change<? extends Integer> arg0) {
+            computeFixedRowHeight();
+        }
+    };
+
+    /**
+     * We compute the total height of the fixedRows so that the selection can
+     * use it without performance regression.
+     */
+    public void computeFixedRowHeight() {
+        fixedRowHeight = 0;
+        for (int i : getCurrentlyFixedRow()) {
+            fixedRowHeight += getRowHeight(i);
+        }
+    }
+
+    /**
+     * We listen on the FixedColumns in order to do the modification in the
+     * VirtualFlow.
+     */
+    private final ListChangeListener<SpreadsheetColumn> fixedColumnsListener = new ListChangeListener<SpreadsheetColumn>() {
+        @Override
+        public void onChanged(Change<? extends SpreadsheetColumn> c) {
+            hBarValue.clear();
+            getFlow().requestLayout();
+            // requestLayout() not responding immediately..
+//            getFlow().layoutTotal();
+        }
+    };
+
+    @Override
+    protected TableSelectionModel<ObservableList<SpreadsheetCell>> getSelectionModel() {
+        return getSkinnable().getSelectionModel();
+    }
+
+    @Override
+    protected TableFocusModel<ObservableList<SpreadsheetCell>, TableColumn<ObservableList<SpreadsheetCell>, ?>> getFocusModel() {
+        return getSkinnable().getFocusModel();
+    }
+
+    @Override
+    protected TablePositionBase<? extends TableColumn<ObservableList<SpreadsheetCell>, ?>> getFocusedCell() {
+        return getSkinnable().getFocusModel().getFocusedCell();
+    }
+
+    @Override
+    protected ObservableList<? extends TableColumn<ObservableList<SpreadsheetCell>, ?>> getVisibleLeafColumns() {
+        return getSkinnable().getVisibleLeafColumns();
+    }
+
+    @Override
+    protected int getVisibleLeafIndex(TableColumn<ObservableList<SpreadsheetCell>, ?> tc) {
+        return getSkinnable().getVisibleLeafIndex(tc);
+    }
+
+    @Override
+    protected TableColumn<ObservableList<SpreadsheetCell>, ?> getVisibleLeafColumn(int col) {
+        return getSkinnable().getVisibleLeafColumn(col);
+    }
+
+    @Override
+    protected ObservableList<TableColumn<ObservableList<SpreadsheetCell>, ?>> getColumns() {
+        return getSkinnable().getColumns();
+    }
+
+    @Override
+    protected ObservableList<TableColumn<ObservableList<SpreadsheetCell>, ?>> getSortOrder() {
+        return getSkinnable().getSortOrder();
+    }
+
+    @Override
+    protected ObjectProperty<ObservableList<ObservableList<SpreadsheetCell>>> itemsProperty() {
+        return getSkinnable().itemsProperty();
+    }
+
+    @Override
+    protected ObjectProperty<Callback<TableView<ObservableList<SpreadsheetCell>>, TableRow<ObservableList<SpreadsheetCell>>>> rowFactoryProperty() {
+        return getSkinnable().rowFactoryProperty();
+    }
+
+    @Override
+    protected ObjectProperty<Node> placeholderProperty() {
+        return getSkinnable().placeholderProperty();
+    }
+
+    @Override
+    protected BooleanProperty tableMenuButtonVisibleProperty() {
+        return getSkinnable().tableMenuButtonVisibleProperty();
+    }
+
+    @Override
+    protected ObjectProperty<Callback<ResizeFeaturesBase, Boolean>> columnResizePolicyProperty() {
+        return (ObjectProperty<Callback<ResizeFeaturesBase, Boolean>>) (Object)getSkinnable().columnResizePolicyProperty();
+    }
+
+    @Override
+    protected boolean resizeColumn(TableColumn<ObservableList<SpreadsheetCell>, ?> tc, double delta) {
+        getHorizontalHeader().getRootHeader().lastColumnResized = getColumns().indexOf(tc);
+        boolean returnedValue = getSkinnable().resizeColumn(tc, delta);
+        if(returnedValue){
+            Event.fireEvent(spreadsheetView, new SpreadsheetView.ColumnWidthEvent(getColumns().indexOf(tc), tc.getWidth()));
+        }
+        return returnedValue;
+    }
+
+    @Override
+    protected void edit(int index, TableColumn<ObservableList<SpreadsheetCell>, ?> column) {
+        getSkinnable().edit(index, column);
+    }
+
+    @Override
+    public TableRow<ObservableList<SpreadsheetCell>> createCell() {
+        TableRow<ObservableList<SpreadsheetCell>> cell;
+
+        if (getSkinnable().getRowFactory() != null) {
+            cell = getSkinnable().getRowFactory().call(getSkinnable());
+        } else {
+            cell = new TableRow<>();
+        }
+
+        cell.updateTableView(getSkinnable());
+        return cell;
+    }
+
+    @Override
+    public int getItemCount() {
+        return getSkinnable().getItems() == null ? 0 : getSkinnable().getItems().size();
+    }
+
+    /**
+     * If the scene is not yet instantiated, we need to wait otherwise the
+     * VirtualFlow will not shift the cells properly.
+     *
+     * @param value
+     */
+    public void setHbarValue(double value) {
+        setHbarValue(value, 0);
+    }
+
+    public void setHbarValue(double value, int count) {
+        if (count > 5) {
+            return;
+        }
+        final int newCount = count + 1;
+        if (flow.getScene() == null) {
+            Platform.runLater(() -> {
+                setHbarValue(value, newCount);
+            });
+            return;
+        }
+        getHBar().setValue(value);
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/GridVirtualFlow.java b/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/GridVirtualFlow.java
new file mode 100644
index 0000000..c62611f
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/GridVirtualFlow.java
@@ -0,0 +1,426 @@
+/**
+ * Copyright (c) 2013, 2016 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.spreadsheet;
+
+import com.sun.javafx.scene.control.skin.VirtualFlow;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import javafx.beans.Observable;
+import javafx.beans.binding.When;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.ObservableList;
+import javafx.scene.Group;
+import javafx.scene.Node;
+import javafx.scene.control.IndexedCell;
+import javafx.scene.control.ScrollBar;
+import javafx.scene.control.TableRow;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.Region;
+import javafx.scene.shape.Rectangle;
+import org.controlsfx.control.spreadsheet.SpreadsheetCell;
+import org.controlsfx.control.spreadsheet.SpreadsheetView;
+
+final class GridVirtualFlow<T extends IndexedCell<?>> extends VirtualFlow<T> {
+    
+    /**
+     * With that comparator we can lay out our rows in the reverse order. That
+     * is to say from the bottom to the very top. In that manner we are sure
+     * that our spanning cells will COVER the cell below so we don't have any
+     * problems with missing hovering, the editor jammed etc.
+     * <br/>
+     *
+     * The only problem is for the fixed column but the {@link #getTopRow(int) }
+     * now returns the very first row and allow us to put some privileged
+     * TableCell in it if they feel the need to be on top in term of z-order.
+     *
+     * FIXME The best would be to put a TreeList of something like that in order
+     * not to sort the rows everytime, need investigation..
+     */
+    private static final Comparator<GridRow> ROWCMP = new Comparator<GridRow>() {
+        @Override
+        public int compare(GridRow firstRow, GridRow secondRow) {
+            //o1.getIndex() < o2.getIndex() ? -1 : +1;
+            return secondRow.getIndex() - firstRow.getIndex();
+        }
+    };
+
+    /***************************************************************************
+     * * Private Fields * *
+     **************************************************************************/
+    private SpreadsheetView spreadSheetView;
+    private final GridViewSkin gridViewSkin;
+    /**
+     * Store the fixedRow in order to place them at the top when necessary.
+     * That is to say, when the VirtualFlow has not already placed one.
+     */
+    private final ArrayList<T> myFixedCells = new ArrayList<>();
+    public final List<Node> sheetChildren;
+
+    /***************************************************************************
+     * * Constructor * *
+     **************************************************************************/
+    public GridVirtualFlow(GridViewSkin gridViewSkin) {
+        super();
+        this.gridViewSkin = gridViewSkin;
+        final ChangeListener<Number> listenerY = new ChangeListener<Number>() {
+            @Override
+            public void changed(ObservableValue<? extends Number> ov, Number t, Number t1) {
+                layoutTotal();
+            }
+        };
+        getVbar().valueProperty().addListener(listenerY);
+        getHbar().valueProperty().addListener(hBarValueChangeListener);
+        widthProperty().addListener(hBarValueChangeListener);
+        
+        sheetChildren = findSheetChildren();
+        
+        //When we click outside of the grid, we want to deselect all cells.
+        addEventHandler(MouseEvent.MOUSE_PRESSED, (MouseEvent event) -> {
+            if (event.getTarget().getClass() == GridRow.class) {
+                spreadSheetView.getSelectionModel().clearSelection();
+            }
+        });
+    }
+
+    /***************************************************************************
+     * * Public Methods * *
+     **************************************************************************/
+    public void init(SpreadsheetView spv) {
+        /**
+         * The idea is to work-around
+         * https://javafx-jira.kenai.com/browse/RT-36396 in order to have the
+         * same behavior between the vertical scrollBar and the horizontal
+         * scrollBar.
+         */
+        getHbar().maxProperty().addListener(new ChangeListener<Number>() {
+
+            @Override
+            public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
+                //We want to go page by page.
+                getHbar().setBlockIncrement(getWidth());
+                getHbar().setUnitIncrement(newValue.doubleValue()/20);
+            }
+        });
+
+        this.spreadSheetView = spv;
+       
+        //We clip the rectangle selection with a rectangle, inception style.
+        Rectangle rec = new Rectangle();
+        rec.widthProperty().bind(widthProperty().subtract(new When(getVbar().visibleProperty()).then(getVbar().widthProperty()).otherwise(0)));
+        rec.heightProperty().bind(heightProperty().subtract(new When(getHbar().visibleProperty()).then(getHbar().heightProperty()).otherwise(0)));
+        gridViewSkin.rectangleSelection.setClip(rec);
+        
+        getChildren().add(gridViewSkin.rectangleSelection);
+        
+        spv.getFixedRows().addListener((Observable observable) -> {
+            List<T> toRemove = new ArrayList<>();
+            for (T cell : myFixedCells) {
+                if (!spv.getFixedRows().contains(cell.getIndex())) {
+                    cell.setManaged(false);
+                    cell.setVisible(false);
+                    toRemove.add(cell);
+                }
+            }
+            myFixedCells.removeAll(toRemove);
+        });
+    }
+
+    @Override
+    public void show(int index) {
+        super.show(index);
+        layoutTotal();
+        layoutFixedRows();
+    }
+
+    @Override
+    public void scrollTo(int index) {
+        //If we have some fixedRows, we check if the selected row is not below them
+        if (!getCells().isEmpty() && spreadSheetView.getFixedRows().size() > 0) {
+            double offset = gridViewSkin.getFixedRowHeight();
+
+            while (offset >= 0 && index > 0) {
+                index--;
+                offset -= gridViewSkin.getRowHeight(index);
+            }
+        }
+        super.scrollTo(index);
+
+        layoutTotal();
+        layoutFixedRows();
+                }
+
+    @Override
+    public double adjustPixels(final double delta) {
+        final double returnValue = super.adjustPixels(delta);
+
+        layoutTotal();
+        layoutFixedRows();
+
+        return returnValue;
+    }
+    
+    List<T> getFixedCells(){
+        return myFixedCells;
+    }
+    /***************************************************************************
+     * * Protected Methods * *
+     **************************************************************************/
+
+    /**
+     * We need to return here the very top row in term of "z-order". Because we
+     * will add in this row the TableCell that are in fixedColumn and which
+     * needs to be drawn on top of all others.
+     *
+     * @return
+     */
+    GridRow getTopRow() {
+        if (!sheetChildren.isEmpty()) {
+            return (GridRow) sheetChildren.get(sheetChildren.size() - 1);
+        }
+        return null;
+    }
+    
+    @Override
+    protected void layoutChildren() {
+        /**
+         * In fact, we must do a layout even when editing, because if the user
+         * resize the window during edition, if we block layout, the view will
+         * be in a wrong state.
+         */
+        if (spreadSheetView != null
+                /*&& (spreadSheetView.getEditingCell() == null || spreadSheetView
+                        .getEditingCell().getRow() == -1)*/) {
+            sortRows();
+            super.layoutChildren();
+            layoutTotal();
+            layoutFixedRows();
+            
+            /**
+             * Sometimes, the visible amount is not computed when we have few
+             * big rows. If we detect that case, we must compute it manually
+             * otherwise the Vbar is wrongly set.
+             */
+            if (getVbar().getVisibleAmount() == 0.0
+                    && getVbar().isVisible()
+                    && getCells().size() != getCellCount()) {
+                getVbar().setMax(1);
+                getVbar().setVisibleAmount(getCells().size() / (float) getCellCount());
+            }
+        }
+    }
+
+    /**
+     * Layout all the visible rows
+     */
+    protected void layoutTotal() {
+        sortRows();
+        
+        /**
+         * When we layout, we also remove the cell that have been deported into
+         * other rows in order not to have some TableCell hanging out.
+         */
+        for(GridRow row : gridViewSkin.deportedCells.keySet()){
+            for(CellView cell: gridViewSkin.deportedCells.get(row)){
+                row.removeCell(cell);
+            }
+        }
+        gridViewSkin.deportedCells.clear();
+        // When scrolling fast with fixed Rows, cells is empty and not recreated..
+        if (getCells().isEmpty()) {
+            reconfigureCells();
+        }
+        
+        for (GridRow cell : (List<GridRow>)getCells()) {
+            if (cell != null && cell.getIndex() >= 0 && (!gridViewSkin.hBarValue.get(cell.getIndex()) || gridViewSkin.rowToLayout.get(cell.getIndex()))) {
+                cell.requestLayout();
+            }
+        }
+    }
+
+    protected ScrollBar getVerticalBar() {
+        return getVbar();
+    }
+    protected ScrollBar getHorizontalBar() {
+        return getHbar();
+    }
+
+    @Override
+    protected List<T> getCells() {
+        return super.getCells();
+    }
+
+    /***************************************************************************
+     * * Private Methods * *
+     **************************************************************************/
+
+    /**
+     * WARNING : This is bad but no other options right now. This will find the
+     * sheetChildren of the VirtualFlow, aka where the cells are kept and
+     * clipped. See layoutFixedRows() or getTopRow() for use.
+     *
+     * @return
+     */
+    private List<Node> findSheetChildren(){
+        if(!getChildren().isEmpty()){
+            if(getChildren().get(0) instanceof Region){
+                Region region = (Region) getChildren().get(0);
+                if(!region.getChildrenUnmodifiable().isEmpty()){
+                    if(region.getChildrenUnmodifiable().get(0) instanceof Group){
+                        return ((Group)region.getChildrenUnmodifiable().get(0)).getChildren();
+                    }
+                }
+            }
+        }
+        return new ArrayList<>();
+    }
+    
+    /**
+     * Layout the fixed rows to position them correctly
+     */
+    private void layoutFixedRows() {
+
+		//We must have a cell in ViewPort because otherwise
+        //we short-circuit the VirtualFlow.
+        if (spreadSheetView.getFixedRows().size() > 0 && getFirstVisibleCellWithinViewPort() != null) {
+            sortRows();
+            /**
+             * What I do is just going after the VirtualFlow in order to ADD
+             * (not replace like before) new rows at the top.
+             *
+             * If the VirtualFlow has the row, then I will hide mine and let him
+             * handle. But if the row is missing, then I must show mine in order
+             * to have the fixed row.
+             */
+            T row = null;
+            Integer fixedRowIndex;
+            
+            rows:
+            for (int i = spreadSheetView.getFixedRows().size() - 1; i >= 0; i--) {
+                fixedRowIndex = spreadSheetView.getFixedRows().get(i);
+                T lastCell = getLastVisibleCellWithinViewPort();
+                //If the fixed row is out of bounds
+                if (lastCell != null && fixedRowIndex > lastCell.getIndex()) {
+                    if (row != null) {
+                        row.setVisible(false);
+                        row.setManaged(false);
+                        sheetChildren.remove(row);
+                    }
+                    continue;
+                }
+
+                //We see if the row is laid out by the VirtualFlow
+                for (T virtualFlowCells : getCells()) {
+                    if (virtualFlowCells.getIndex() > fixedRowIndex) {
+                        break;
+                    } else if (virtualFlowCells.getIndex() == fixedRowIndex) {
+                        row = containsRows(fixedRowIndex);
+                        if (row != null) {
+                            row.setVisible(false);
+                            row.setManaged(false);
+                            sheetChildren.remove(row);
+                        }
+                        /**
+                         * OLD COMMENT : We must push to Front only if the row
+                         * is at the very top and has a risk to be recovered.
+                         * This is happening only if this row is translated.
+                         *
+                         * NEW COMMENT: I'm not sure about this.. Since the
+                         * fixedColumn are not in the special top row, we don't
+                         * care if the row is pushed to front.. need
+                         * investigation
+                         */
+                        virtualFlowCells.toFront();
+                        continue rows;
+                    }
+                }
+                
+                row = containsRows(fixedRowIndex);
+                if (row == null) {
+                    /**
+                     * getAvailableCell is not added our cell to the ViewPort in some cases.
+                     * So we need to instantiate it ourselves.
+                     */
+                    row = getCreateCell().call(this);
+                    row.getProperties().put("newcell", null); //$NON-NLS-1$
+                	 
+                    setCellIndex(row, fixedRowIndex);
+                    resizeCellSize(row);
+                    myFixedCells.add(row);
+                }
+                
+                /**
+                 * Sometime, when we set a new Grid on a SpreadsheetView without recreating it,
+                 * we can end up with some rows not being added to the ViewPort.
+                 * So we must be sure it's in and add it ourself otherwise.
+                 */
+                if(!sheetChildren.contains(row)){
+                    sheetChildren.add(row);
+                }
+               
+                row.setManaged(true);
+                row.setVisible(true);
+                row.toFront();
+                row.requestLayout();
+            }
+        }
+    }
+
+    /**
+     * Verify if the row has been added to myFixedCell
+     * @param i
+     * @return
+     */
+    private T containsRows(int i){
+    	for(T cell:myFixedCells){
+    		if(cell.getIndex() == i)
+    			return cell;
+    	}
+    	return null;
+    }
+    /**
+     * Sort the rows so that they stay in order for layout
+     */
+    private void sortRows() {
+        final List<GridRow> temp = (List<GridRow>) getCells();
+        final List<GridRow> tset = new ArrayList<>(temp);
+        Collections.sort(tset, ROWCMP);
+        for (final TableRow<ObservableList<SpreadsheetCell>> r : tset) {
+            r.toFront();
+        }
+    }
+    
+    private final ChangeListener<Number> hBarValueChangeListener = new ChangeListener<Number>() {
+        @Override
+        public void changed(ObservableValue<? extends Number> ov, Number t, Number t1) {
+            gridViewSkin.hBarValue.clear();
+        }
+    };
+}
+
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/HorizontalHeader.java b/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/HorizontalHeader.java
new file mode 100644
index 0000000..6540da8
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/HorizontalHeader.java
@@ -0,0 +1,349 @@
+/**
+ * Copyright (c) 2013, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.spreadsheet;
+
+import com.sun.javafx.scene.control.skin.NestedTableColumnHeader;
+import com.sun.javafx.scene.control.skin.TableColumnHeader;
+import com.sun.javafx.scene.control.skin.TableHeaderRow;
+import java.util.BitSet;
+import java.util.List;
+import javafx.application.Platform;
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import javafx.event.EventHandler;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TablePosition;
+import javafx.scene.control.TableView.TableViewSelectionModel;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.shape.Rectangle;
+import org.controlsfx.control.spreadsheet.SpreadsheetCell;
+import org.controlsfx.control.spreadsheet.SpreadsheetColumn;
+import org.controlsfx.control.spreadsheet.SpreadsheetView;
+
+/**
+ * The set of horizontal (column) headers.
+ */
+public class HorizontalHeader extends TableHeaderRow {
+
+    final GridViewSkin gridViewSkin;
+
+    // Indicate whether the this HorizontalHeader is activated or not
+    private boolean working = true;
+    /**
+     * When the columns header are clicked, we consider the column as selected.
+     * This BitSet is reset when a modification on cells is done.
+     */
+    protected BitSet selectedColumns = new BitSet();
+
+    /***************************************************************************
+     * 
+     * Constructor
+     * 
+     **************************************************************************/
+    public HorizontalHeader(final GridViewSkin skin) {
+        super(skin);
+        gridViewSkin = skin;
+    }
+
+    /**************************************************************************
+     * 
+     * Public API
+     * 
+     **************************************************************************/
+    public void init() {
+        SpreadsheetView spv = gridViewSkin.handle.getView();
+        updateHorizontalHeaderVisibility(spv.isShowColumnHeader());
+
+        //Visibility of vertical Header listener
+        spv.showRowHeaderProperty().addListener(verticalHeaderListener);
+        gridViewSkin.verticalHeader.verticalHeaderWidthProperty().addListener(verticalHeaderListener);
+
+        //Visibility of horizontal Header listener
+        spv.showColumnHeaderProperty().addListener(horizontalHeaderVisibilityListener);
+
+        //Selection listener to highlight header
+        gridViewSkin.getSelectedColumns().addListener(selectionListener);
+
+        //Fixed Column listener to change style of header
+        spv.getFixedColumns().addListener(fixedColumnsListener);
+
+        Platform.runLater(() -> {
+            //We are doing that because some columns may be already fixed.
+            for (SpreadsheetColumn column : spv.getFixedColumns()) {
+                fixColumn(column);
+            }
+            requestLayout();
+            /**
+             * Clicking on header select the whole column.
+             */
+            installHeaderMouseEvent();
+        });
+
+        /**
+         * When we are setting a new Grid (model) on the SpreadsheetView, it
+         * appears that the headers are re-created. So we need to listen to
+         * those changes in order to re-apply our css style class. Otherwise
+         * we'll end up with fixedColumns but no graphic confirmation.
+         */
+        getRootHeader().getColumnHeaders().addListener((Observable o) -> {
+            for (SpreadsheetColumn fixItem : spv.getFixedColumns()) {
+                fixColumn(fixItem);
+            }
+            updateHighlightSelection();
+            installHeaderMouseEvent();
+        });
+    }
+
+    @Override
+    public HorizontalHeaderColumn getRootHeader() {
+        return (HorizontalHeaderColumn) super.getRootHeader();
+    }
+
+    void clearSelectedColumns(){
+        selectedColumns.clear();
+    }
+    /**************************************************************************
+     * 
+     * Protected methods
+     * 
+     **************************************************************************/
+    @Override
+    protected void updateTableWidth() {
+        super.updateTableWidth();
+        // snapping added for RT-19428
+        double padding = 0;
+
+        if (working && gridViewSkin != null
+                && gridViewSkin.spreadsheetView != null
+                && gridViewSkin.spreadsheetView.showRowHeaderProperty().get()
+                && gridViewSkin.verticalHeader != null) {
+            padding += gridViewSkin.verticalHeader.getVerticalHeaderWidth();
+        }
+
+        Rectangle clip = ((Rectangle) getClip());
+        
+        clip.setWidth(clip.getWidth() == 0 ? 0 : clip.getWidth() - padding);
+    }
+
+    @Override
+    protected void updateScrollX() {
+        super.updateScrollX();
+        gridViewSkin.horizontalPickers.updateScrollX();
+        
+        if (working) {
+            requestLayout();
+            getRootHeader().layoutFixedColumns();
+        }
+    }
+
+    @Override
+    protected NestedTableColumnHeader createRootHeader() {
+        return new HorizontalHeaderColumn(getTableSkin(), null);
+    }
+
+    /**************************************************************************
+     * 
+     * Private methods.
+     * 
+     **************************************************************************/
+    
+    /**
+     * When we click on header, we want to select the whole column.
+     */
+    private void installHeaderMouseEvent() {
+        for (final TableColumnHeader columnHeader : getRootHeader().getColumnHeaders()) {
+            EventHandler<MouseEvent> mouseEventHandler = (MouseEvent mouseEvent) -> {
+                if (mouseEvent.isPrimaryButtonDown()) {
+                    headerClicked((TableColumn) columnHeader.getTableColumn(), mouseEvent);
+                }
+            };
+            columnHeader.getChildrenUnmodifiable().get(0).setOnMousePressed(mouseEventHandler);
+        }
+    }
+    /**
+     * If a header is clicked, we must select the whole column. If Control key of
+     * Shift key is pressed, we must not deselect the previous selection but
+     * just act like the {@link GridViewBehavior} would.
+     *
+     * @param column
+     * @param event
+     */
+    private void headerClicked(TableColumn column, MouseEvent event) {
+        TableViewSelectionModel<ObservableList<SpreadsheetCell>> sm = gridViewSkin.handle.getGridView().getSelectionModel();
+        int lastRow = gridViewSkin.spreadsheetView.getGrid().getRowCount() - 1;
+        int indexColumn = column.getTableView().getColumns().indexOf(column);
+        TablePosition focusedPosition = sm.getTableView().getFocusModel().getFocusedCell();
+        if (event.isShortcutDown()) {
+            BitSet tempSet = (BitSet) selectedColumns.clone();
+            sm.selectRange(0, column, lastRow, column);
+            selectedColumns.or(tempSet);
+            selectedColumns.set(indexColumn);
+        } else if (event.isShiftDown() && focusedPosition != null && focusedPosition.getTableColumn() != null) {
+            sm.clearSelection();
+            sm.selectRange(0, column, lastRow, focusedPosition.getTableColumn());
+            sm.getTableView().getFocusModel().focus(0, focusedPosition.getTableColumn());
+            int min = Math.min(indexColumn, focusedPosition.getColumn());
+            int max = Math.max(indexColumn, focusedPosition.getColumn());
+            selectedColumns.set(min, max + 1);
+        } else {
+            sm.clearSelection();
+            sm.selectRange(0, column, lastRow, column);
+            //And we want to have the focus on the first cell in order to be able to copy/paste between columns.
+            sm.getTableView().getFocusModel().focus(0, column);
+            selectedColumns.set(indexColumn);
+        }
+    }
+    /**
+     * Whether the Vertical Header is showing, we need to update the width
+     * because some space on the left will be available/used.
+     */
+    private final InvalidationListener verticalHeaderListener = new InvalidationListener() {
+
+        @Override
+        public void invalidated(Observable observable) {
+            updateTableWidth();
+        }
+    };
+
+    /**
+     * Whether the Horizontal Header is showing, we need to toggle its
+     * visibility.
+     */
+    private final ChangeListener<Boolean> horizontalHeaderVisibilityListener = new ChangeListener<Boolean>() {
+        @Override
+        public void changed(ObservableValue<? extends Boolean> arg0, Boolean arg1, Boolean arg2) {
+            updateHorizontalHeaderVisibility(arg2);
+        }
+    };
+
+    /**
+     * When we fix/unfix some columns, we change the style of the Label header
+     * text
+     */
+    private final ListChangeListener<SpreadsheetColumn> fixedColumnsListener = new ListChangeListener<SpreadsheetColumn>() {
+
+        @Override
+        public void onChanged(javafx.collections.ListChangeListener.Change<? extends SpreadsheetColumn> change) {
+            while (change.next()) {
+               //If we unfix a column
+               for (SpreadsheetColumn remitem : change.getRemoved()) {
+                   unfixColumn(remitem);
+               }
+               //If we fix one
+               for (SpreadsheetColumn additem : change.getAddedSubList()) {
+                   fixColumn(additem);
+               }
+            }
+            updateHighlightSelection();
+        }
+    };
+
+    /**
+     * Fix this column regarding the style
+     *
+     * @param column
+     */
+    private void fixColumn(SpreadsheetColumn column) {
+        addStyleHeader(gridViewSkin.spreadsheetView.getColumns().indexOf(column));
+    }
+
+    /**
+     * Unfix this column regarding the style
+     *
+     * @param column
+     */
+    private void unfixColumn(SpreadsheetColumn column) {
+        removeStyleHeader(gridViewSkin.spreadsheetView.getColumns().indexOf(column));
+    }
+
+    /**
+     * Add the fix style of the header Label of the specified column
+     *
+     * @param i
+     */
+    private void removeStyleHeader(Integer i) {
+        if (getRootHeader().getColumnHeaders().size() > i) {
+            getRootHeader().getColumnHeaders().get(i).getStyleClass().removeAll("fixed"); //$NON-NLS-1$
+        }
+    }
+
+    /**
+     * Remove the fix style of the header Label of the specified column
+     *
+     * @param i
+     */
+    private void addStyleHeader(Integer i) {
+        if (getRootHeader().getColumnHeaders().size() > i) {
+            getRootHeader().getColumnHeaders().get(i).getStyleClass().addAll("fixed"); //$NON-NLS-1$
+        }
+    }
+
+    /**
+     * When we select some cells, we want the header to be highlighted
+     */
+    private final InvalidationListener selectionListener = new InvalidationListener() {
+        @Override
+        public void invalidated(Observable valueModel) {
+            updateHighlightSelection();
+        }
+    };
+
+    /**
+     * Highlight the header Label when selection change.
+     */
+    private void updateHighlightSelection() {
+        for (final TableColumnHeader i : getRootHeader().getColumnHeaders()) {
+            i.getStyleClass().removeAll("selected"); //$NON-NLS-1$
+
+        }
+        final List<Integer> selectedColumns = gridViewSkin.getSelectedColumns();
+        for (final Integer i : selectedColumns) {
+            if (getRootHeader().getColumnHeaders().size() > i) {
+                getRootHeader().getColumnHeaders().get(i).getStyleClass()
+                        .addAll("selected"); //$NON-NLS-1$
+            }
+        }
+
+    }
+
+    private void updateHorizontalHeaderVisibility(boolean visible) {
+        working = visible;
+        setManaged(working);
+        if (!visible) {
+            getStyleClass().add("invisible"); //$NON-NLS-1$
+        } else {
+            getStyleClass().remove("invisible"); //$NON-NLS-1$
+            requestLayout();
+            getRootHeader().layoutFixedColumns();
+            updateHighlightSelection();
+        }
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/HorizontalHeaderColumn.java b/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/HorizontalHeaderColumn.java
new file mode 100644
index 0000000..a8d7726
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/HorizontalHeaderColumn.java
@@ -0,0 +1,149 @@
+/**
+ * Copyright (c) 2013, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.spreadsheet;
+
+import org.controlsfx.control.spreadsheet.SpreadsheetView;
+
+import javafx.event.EventHandler;
+import javafx.scene.control.TableColumnBase;
+import javafx.scene.input.MouseEvent;
+
+import com.sun.javafx.scene.control.skin.NestedTableColumnHeader;
+import com.sun.javafx.scene.control.skin.TableColumnHeader;
+import com.sun.javafx.scene.control.skin.TableViewSkinBase;
+import javafx.beans.Observable;
+import javafx.beans.value.ObservableValue;
+
+/**
+ * A cell column header.
+ */
+public class HorizontalHeaderColumn extends NestedTableColumnHeader {
+
+    int lastColumnResized = -1;
+    
+    public HorizontalHeaderColumn(
+            TableViewSkinBase<?, ?, ?, ?, ?, ?> skin, TableColumnBase<?, ?> tc) {
+        super(skin, tc);
+        /**
+         * Resolve https://bitbucket.org/controlsfx/controlsfx/issue/395
+         * and https://bitbucket.org/controlsfx/controlsfx/issue/434
+         */
+        widthProperty().addListener((Observable observable) -> {
+            ((GridViewSkin)skin).hBarValue.clear();
+            ((GridViewSkin)skin).rectangleSelection.updateRectangle();
+        });
+        
+        /**
+         * We want to resize all other selected columns when we resize one.
+         *
+         * I cannot really determine when a resize is finished. Apparently, when
+         * this variable Layout is set to 0, it means the drag is done, so until
+         * a beter solution is shown, it will do the trick.
+         */
+        columnReorderLine.layoutXProperty().addListener((ObservableValue<? extends Number> observable, Number oldValue, Number newValue) -> {
+            HorizontalHeader headerRow = (HorizontalHeader) ((GridViewSkin) skin).getTableHeaderRow();
+            GridViewSkin mySkin = ((GridViewSkin) skin);
+            if (newValue.intValue() == 0 && lastColumnResized >= 0) {
+                if (headerRow.selectedColumns.get(lastColumnResized)) {
+                    double width1 = mySkin.getColumns().get(lastColumnResized).getWidth();
+                    for (int i = headerRow.selectedColumns.nextSetBit(0); i >= 0; i = headerRow.selectedColumns.nextSetBit(i + 1)) {
+                        mySkin.getColumns().get(i).setPrefWidth(width1);
+                    }
+                }
+            }
+        });
+    }
+
+    @Override
+    protected TableColumnHeader createTableColumnHeader(final TableColumnBase col) {
+        TableViewSkinBase<?,?,?,?,?,TableColumnBase<?,?>> tableViewSkin = getTableViewSkin();
+        if (col.getColumns().isEmpty()) {
+            final TableColumnHeader columnHeader = new TableColumnHeader(tableViewSkin, col);
+            /**
+             * When the user double click on a header, we want to resize the
+             * column to fit the content.
+             */
+            columnHeader.setOnMousePressed(new EventHandler<MouseEvent>() {
+                @Override
+                public void handle(MouseEvent mouseEvent) {
+                    if (mouseEvent.getClickCount() == 2 && mouseEvent.isPrimaryButtonDown()) {
+                        ((GridViewSkin) (Object) tableViewSkin).resize(col, -1);
+                    }
+                }
+            });
+            return columnHeader;
+        } else {
+            return new HorizontalHeaderColumn(getTableViewSkin(), col);
+        }
+    }
+
+    @Override
+    protected void layoutChildren() {
+        super.layoutChildren();
+        layoutFixedColumns();
+    }
+
+    /**
+     * We want ColumnHeader to be fixed when we freeze some columns
+     *
+     */
+    public void layoutFixedColumns() {
+        SpreadsheetHandle handle = ((GridViewSkin) (Object) getTableViewSkin()).handle;
+        final SpreadsheetView spreadsheetView = handle.getView();
+        if (handle.getCellsViewSkin() == null || getChildren().isEmpty()) {
+            return;
+        }
+        double hbarValue = handle.getCellsViewSkin().getHBar().getValue();
+
+        final int labelHeight = (int) getChildren().get(0).prefHeight(-1);
+        double fixedColumnWidth = 0;
+        double x = snappedLeftInset();
+        int max = getColumnHeaders().size();
+        max = max > spreadsheetView.getColumns().size() ? spreadsheetView.getColumns().size() : max;
+        for (int j = 0 ; j < max; j++) {
+            final TableColumnHeader n = getColumnHeaders().get(j);
+            final double prefWidth = snapSize(n.prefWidth(-1));
+            n.setPrefHeight(24.0);
+            //If the column is fixed
+            if (spreadsheetView.getFixedColumns().indexOf(spreadsheetView.getColumns().get(j)) != -1) {
+                double tableCellX = 0;
+                //If the column is hidden we have to translate it
+                if (hbarValue + fixedColumnWidth > x) {
+
+                    tableCellX = Math.abs(hbarValue - x + fixedColumnWidth);
+
+                    n.toFront();
+                    fixedColumnWidth += prefWidth;
+                }
+                n.relocate(x + tableCellX, labelHeight + snappedTopInset());
+            }
+
+            x += prefWidth;
+        }
+
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/HorizontalPicker.java b/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/HorizontalPicker.java
new file mode 100644
index 0000000..9ef50ab
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/HorizontalPicker.java
@@ -0,0 +1,152 @@
+/**
+ * Copyright (c) 2014 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.spreadsheet;
+
+import com.sun.javafx.scene.control.skin.TableColumnHeader;
+import java.util.Stack;
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.event.EventHandler;
+import javafx.scene.control.Label;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.StackPane;
+import javafx.scene.shape.Rectangle;
+import org.controlsfx.control.spreadsheet.Picker;
+import org.controlsfx.control.spreadsheet.SpreadsheetView;
+
+/**
+ *
+ * This class will display all the available pickers. It is a StackPane clipped
+ * which contain a inner Region that display the picker. In that way, we don't
+ * need to re-layout every time but just "slide" the inner Region inside this
+ * class so that the pickers are sliding along with the TableColumnHeaders.
+ */
+public class HorizontalPicker extends StackPane {
+
+    private static final String PICKER_INDEX = "PickerIndex"; //$NON-NLS-1$
+
+    private final HorizontalHeader horizontalHeader;
+
+    private final SpreadsheetView spv;
+    private final Stack<Label> pickerPile;
+    private final Stack<Label> pickerUsed;
+
+    private final InnerHorizontalPicker innerPicker = new InnerHorizontalPicker();
+
+    public HorizontalPicker(HorizontalHeader horizontalHeader, SpreadsheetView spv) {
+        this.horizontalHeader = horizontalHeader;
+        this.spv = spv;
+
+        pickerPile = new Stack<>();
+        pickerUsed = new Stack<>();
+
+        //Clip this StackPane just like the TableHeaderRow.
+        Rectangle clip = new Rectangle();
+        clip.setSmooth(true);
+        clip.setHeight(VerticalHeader.PICKER_SIZE);
+        clip.widthProperty().bind(horizontalHeader.widthProperty());
+        setClip(clip);
+
+        getChildren().add(innerPicker);
+
+        horizontalHeader.getRootHeader().getColumnHeaders().addListener(layoutListener);
+        spv.getColumnPickers().addListener(layoutListener);
+    }
+
+    @Override
+    protected void layoutChildren() {
+        //Just relocate the inner for sliding.
+        innerPicker.relocate(horizontalHeader.getRootHeader().getLayoutX(), snappedTopInset());
+        //We must turn off pickers that are behind fixed columns
+        for (Label label : pickerUsed) {
+            label.setVisible(label.getLayoutX() + innerPicker.getLayoutX() + label.getWidth() > horizontalHeader.gridViewSkin.fixedColumnWidth);
+        }
+    }
+
+    /**
+     * Method called by the HorizontalHeader in order to slide the pickers.
+     */
+    public void updateScrollX() {
+        requestLayout();
+    }
+
+    private Label getPicker(Picker picker) {
+        Label pickerLabel;
+        if (pickerPile.isEmpty()) {
+            pickerLabel = new Label();
+            pickerLabel.getStyleClass().addListener(layoutListener);
+            pickerLabel.setOnMouseClicked(pickerMouseEvent);
+        } else {
+            pickerLabel = pickerPile.pop();
+        }
+        pickerUsed.push(pickerLabel);
+        pickerLabel.getStyleClass().setAll(picker.getStyleClass());
+        pickerLabel.getProperties().put(PICKER_INDEX, picker);
+        return pickerLabel;
+    }
+
+    private final EventHandler<MouseEvent> pickerMouseEvent = (MouseEvent mouseEvent) -> {
+        Label picker = (Label) mouseEvent.getSource();
+
+        ((Picker) picker.getProperties().get(PICKER_INDEX)).onClick();
+    };
+
+    /**
+     * Inner class that will lay out all the pickers.
+     */
+    private class InnerHorizontalPicker extends Region {
+
+        @Override
+        protected void layoutChildren() {
+            pickerPile.addAll(pickerUsed.subList(0, pickerUsed.size()));
+            //Unbind every picker used before setting new ones.
+            for (Label label : pickerUsed) {
+                label.layoutXProperty().unbind();
+                label.setVisible(true);
+            }
+            pickerUsed.clear();
+
+            getChildren().clear();
+            int index = 0;
+            for (TableColumnHeader column : horizontalHeader.getRootHeader().getColumnHeaders()) {
+                if (spv.getColumnPickers().containsKey(index)) {
+                    Label label = getPicker(spv.getColumnPickers().get(index));
+                    label.resize(column.getWidth(), VerticalHeader.PICKER_SIZE);
+                    label.layoutXProperty().bind(column.layoutXProperty());
+
+                    getChildren().add(0, label);
+                }
+                index++;
+            }
+        }
+    }
+
+    private final InvalidationListener layoutListener = (Observable arg0) -> {
+        innerPicker.requestLayout();
+    };
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/RectangleSelection.java b/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/RectangleSelection.java
new file mode 100644
index 0000000..7496914
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/RectangleSelection.java
@@ -0,0 +1,436 @@
+/**
+ * Copyright (c) 2014, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.spreadsheet;
+
+import java.util.List;
+import java.util.TreeSet;
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.event.EventHandler;
+import javafx.scene.control.IndexedCell;
+import javafx.scene.control.TablePosition;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.shape.Rectangle;
+import org.controlsfx.control.spreadsheet.Grid;
+import org.controlsfx.control.spreadsheet.GridChange;
+import org.controlsfx.control.spreadsheet.SpreadsheetCell;
+import org.controlsfx.control.spreadsheet.SpreadsheetColumn;
+
+/**
+ *
+ * This class extends Rectangle and will draw a rectangle with a border to the
+ * selection.
+ */
+public class RectangleSelection extends Rectangle {
+
+    private final GridViewSkin skin;
+    private final TableViewSpanSelectionModel sm;
+    private final SelectionRange selectionRange;
+
+    public RectangleSelection(GridViewSkin skin, TableViewSpanSelectionModel sm) {
+        this.skin = skin;
+        this.sm = sm;
+        getStyleClass().add("selection-rectangle"); //$NON-NLS-1$
+        setMouseTransparent(true);
+
+        selectionRange = new SelectionRange();
+        skin.getVBar().valueProperty().addListener(layoutListener);
+
+        //When draging, it's not working properly so we remove the rectangle.
+        skin.getVBar().addEventFilter(MouseEvent.MOUSE_DRAGGED, new EventHandler<MouseEvent>() {
+
+            @Override
+            public void handle(MouseEvent event) {
+                skin.getVBar().valueProperty().removeListener(layoutListener);
+                setVisible(false);
+                skin.getVBar().addEventFilter(MouseEvent.MOUSE_RELEASED, new EventHandler<MouseEvent>() {
+
+                    @Override
+                    public void handle(MouseEvent event) {
+                        skin.getVBar().removeEventFilter(MouseEvent.MOUSE_RELEASED, this);
+                        skin.getVBar().valueProperty().addListener(layoutListener);
+                        updateRectangle();
+                    }
+                });
+            }
+        });
+
+        skin.getHBar().valueProperty().addListener(layoutListener);
+        sm.getSelectedCells().addListener((Observable observable) -> {
+            skin.getHorizontalHeader().clearSelectedColumns();
+            skin.verticalHeader.clearSelectedRows();
+            selectionRange.fill(sm.getSelectedCells());
+            updateRectangle();
+        });
+    }
+
+    private final InvalidationListener layoutListener = (Observable observable) -> {
+        updateRectangle();
+    };
+
+    public final void updateRectangle() {
+        if (sm.getSelectedCells().isEmpty()
+                || skin.getSelectedRows().isEmpty()
+                || skin.getSelectedColumns().isEmpty()
+                || selectionRange.range == null) {
+            setVisible(false);
+            return;
+        }
+
+        IndexedCell topRowCell = skin.getFlow().getTopRow();
+        if(topRowCell == null){
+            return;
+        }
+        //We fetch the first and last row currently displayed
+        int topRow = topRowCell.getIndex();
+        IndexedCell bottomRowCell = skin.getFlow().getCells().get(skin.getFlow().getCells().size() - 1);
+        if(bottomRowCell == null){
+            return;
+        }
+        int bottomRow = bottomRowCell.getIndex();
+
+        int minRow = selectionRange.range.getTop();
+        if (minRow > bottomRow) {
+            setVisible(false);
+            return;
+        }
+        minRow = Math.max(minRow, topRow);
+
+        int maxRow = selectionRange.range.getBottom();
+        if (maxRow < topRow) {
+            setVisible(false);
+            return;
+        }
+
+        maxRow = Math.min(maxRow, bottomRow);
+        int minColumn = selectionRange.range.getLeft();
+        int maxColumn = selectionRange.range.getRight();
+
+        GridRow gridMinRow = skin.getRowIndexed(minRow);
+        if (gridMinRow == null) {
+            setVisible(false);
+            return;
+        }
+
+        Grid grid = skin.spreadsheetView.getGrid();
+        if (maxRow >= grid.getRowCount() || maxColumn >= grid.getColumnCount()) {
+            setVisible(false);
+            return;
+        }
+        SpreadsheetCell cell = grid.getRows().get(maxRow).get(maxColumn);
+        handleHorizontalPositioning(minColumn, maxColumn, cell.getColumnSpan());
+
+        //If we are out of sight
+        if (getX() + getWidth() < 0) {
+            setVisible(false);
+            return;
+        }
+
+        GridRow gridMaxRow = skin.getRowIndexed(maxRow);
+        if (gridMaxRow == null) {
+            setVisible(false);
+            return;
+        }
+        setVisible(true);
+
+        handleVerticalPositioning(minRow, maxRow, gridMinRow, gridMaxRow, cell.getRowSpan());
+    }
+
+    /**
+     * This will compute and assign the y and height properties of the
+     * rectangle.
+     *
+     * @param minRow
+     * @param maxRow
+     * @param gridMinRow
+     */
+    private void handleVerticalPositioning(int minRow, int maxRow, GridRow gridMinRow, GridRow gridMaxRow, int rowSpan) {
+        double height = 0;
+        for (int i = maxRow; i <= maxRow /*+ (rowSpan - 1)*/; ++i) {
+            height += skin.getRowHeight(i);
+        }
+
+        /**
+         * If we are not in fixed row, we will just take the layout Y, and if
+         * it's below some of our fixed rows, we will take the fixedRowheight as
+         * value.
+         */
+        if (!skin.getCurrentlyFixedRow().contains(minRow)) {
+            yProperty().unbind();
+            //If we have fixedRows, we do not want to overlap them with the rectangle.
+            if (gridMinRow.getLayoutY() < skin.getFixedRowHeight()) {
+                setY(skin.getFixedRowHeight());
+            } else {
+                yProperty().bind(gridMinRow.layoutYProperty());
+            }
+            /**
+             * If we are in fixedRow, we cannot trust the layoutY alone. We also
+             * need to rely on the verticalShift for shifting the rectangle to
+             * the right starting position.\n
+             *
+             */
+        } else {
+            yProperty().bind(gridMinRow.layoutYProperty().add(gridMinRow.verticalShift));
+        }
+
+        /**
+         * Finally we compute the height by subtracting our starting point to
+         * the ending point.
+         */
+        heightProperty().bind(gridMaxRow.layoutYProperty().add(gridMaxRow.verticalShift).subtract(yProperty()).add(height));
+    }
+
+    /**
+     * This will compute and assign the x and width propertis of the Rectangle.
+     *
+     * @param minColumn
+     * @param maxColumn
+     */
+    private void handleHorizontalPositioning(int minColumn, int maxColumn, int columnSpan) {
+        double x = 0;
+
+        final List<SpreadsheetColumn> columns = skin.spreadsheetView.getColumns();
+        if(columns.size() <= minColumn || columns.size() <= maxColumn){
+            return;
+        }
+        //We first compute the total space between the left edge and our first column
+        for (int i = 0; i < minColumn; ++i) {
+            //Here we use Ceil because we want to "snapSize" otherwise we may end up with a weird shift.
+            x += snapSize(columns.get(i).getWidth());
+        }
+        
+
+        /**
+         * We then substract the value of the Hbar in order to place it properly
+         * because 0 means the left edge or the SpreadsheetView and we want to
+         * consider the left edge of the viewport of the virtualFlow.
+         */
+        x -= skin.getHBar().getValue();
+
+        //Then we compute the width by adding the space between the min and max column
+        double width = 0;
+        for (int i = minColumn; i <= maxColumn /*+ (columnSpan - 1)*/; ++i) {
+            width += snapSize(columns.get(i).getWidth());
+        }
+
+        //FIXED COLUMNS
+        /**
+         * If the selection is not on a fixed column, we may have the case where
+         * the first selected cell will be hid by a fixed column. If so, we must
+         * translate the starting point in because the rectangle must also be
+         * hidden by the fixed column.
+         */
+        if (!skin.spreadsheetView.getFixedColumns().contains(columns.get(minColumn))) {
+            if (x < skin.fixedColumnWidth) {
+                //Since I translate the starting point, I must reduce the width by the value I'm translating.
+                width -= skin.fixedColumnWidth - x;
+                x = skin.fixedColumnWidth;
+            }
+            /**
+             * If the maxColumn is contained within the fixed column, we may
+             * look at the starting point and the ending point. Because prior
+             * computation are wrong since our columns are fixed on the left. So
+             * there initial position are worthless and we must consider their
+             * current position compared to each other.
+             *
+             */
+        } else {
+            /**
+             * If x + width is inferior, we can re-compute the width by checking
+             * our fixed columns interval
+             */
+            if (x + width < skin.fixedColumnWidth) {
+                x = 0;
+                width = 0;
+                for (SpreadsheetColumn column : skin.spreadsheetView.getFixedColumns()) {
+                    int indexColumn = columns.indexOf(column);
+                    if (indexColumn < minColumn && indexColumn != minColumn) {
+                        x += snapSize(column.getWidth());
+                    }
+                    if (indexColumn >= minColumn && indexColumn <= maxColumn) {
+                        width += snapSize(column.getWidth());
+                    }
+                }
+                /**
+                 * If just x is inferior to fixedColumnWidth, we just adjust the
+                 * width by substracting the gap between the original x and the
+                 * new x.
+                 */
+            } else if (x < skin.fixedColumnWidth) {
+                double tempX = 0;
+                for (SpreadsheetColumn column : skin.spreadsheetView.getFixedColumns()) {
+                    int indexColumn = columns.indexOf(column);
+                    if (indexColumn < minColumn && indexColumn != minColumn) {
+                        tempX += snapSize(column.getWidth());
+                    }
+                }
+                width -= tempX - x;
+                x = tempX;
+            }
+        }
+        setX(x);
+        setWidth(width);
+    }
+
+    /**
+     * Returns a value ceiled to the nearest pixel.
+     *
+     * @param value the size value to be snapped
+     * @return value ceiled to nearest pixel
+     */
+    private double snapSize(double value) {
+        return Math.ceil(value);
+    }
+    
+    /**
+     * Utility class to transform a list of selected cells into a union of
+     * ranges.
+     */
+    public static class SelectionRange {
+
+        private final TreeSet<Long> set = new TreeSet<>();
+        private GridRange range;
+
+        public SelectionRange() {
+        }
+
+        /**
+         * Construct a SelectionRange with a List of Pair where the value is the
+         * row (of the WsGrid) and the value is column(of the WsGrid).
+         *
+         * @param list
+         */
+        public void fill(List<TablePosition> list) {
+            set.clear();
+            for (TablePosition pos : list) {
+                set.add(key(pos.getRow(), pos.getColumn()));
+            }
+            computeRange();
+        }
+        
+        public void fillGridRange(List<GridChange> list) {
+            set.clear();
+            for (GridChange pos : list) {
+                set.add(key(pos.getRow(), pos.getColumn()));
+            }
+            computeRange();
+        }
+
+        public GridRange getRange(){
+            return range;
+        }
+        private Long key(int row, int column) {
+            return (((long) row) << 32) | column;
+        }
+
+        private int getRow(Long l) {
+            return (int) (l >> 32);
+        }
+
+        private int getColumn(Long l) {
+            return (int) (l & 0xffFFffFF);
+        }
+
+        /**
+         * return a list of WsGridRange
+         *
+         * @return
+         */
+        private void computeRange() {
+            range = null;
+            while (!set.isEmpty()) {
+                if (range != null) {
+                    range = null;
+                    return;
+                }
+
+                long first = set.first();
+                set.remove(first);
+
+                int row = getRow(first);
+                int column = getColumn(first);
+
+                //Go in row
+                while (set.contains(key(row, column + 1))) {
+                    ++column;
+                    set.remove(key(row, column));
+                }
+
+                //Go in column
+                boolean flag = true;
+                while (flag) {
+                    ++row;
+                    for (int col = getColumn(first); col <= column; ++col) {
+                        if (!set.contains(key(row, col))) {
+                            flag = false;
+                            break;
+                        }
+                    }
+                    if (flag) {
+                        for (int col = getColumn(first); col <= column; ++col) {
+                            set.remove(key(row, col));
+                        }
+                    } else {
+                        --row;
+                    }
+                }
+                range = new GridRange(getRow(first), row, getColumn(first), column);
+            }
+        }
+    }
+
+    public static class GridRange {
+
+        private final int top;
+        private final int bottom;
+        private final int left;
+        private final int right;
+
+        public GridRange(int top, int bottom, int left, int right) {
+            this.top = top;
+            this.bottom = bottom;
+            this.left = left;
+            this.right = right;
+        }
+
+        public int getTop() {
+            return top;
+        }
+
+        public int getBottom() {
+            return bottom;
+        }
+
+        public int getLeft() {
+            return left;
+        }
+
+        public int getRight() {
+            return right;
+        }
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/SelectedCellsMapTemp.java b/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/SelectedCellsMapTemp.java
new file mode 100644
index 0000000..9d37392
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/SelectedCellsMapTemp.java
@@ -0,0 +1,205 @@
+/**
+ * Copyright (c) 2014 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.spreadsheet;
+
+import java.util.BitSet;
+import java.util.Collection;
+import java.util.Map;
+import java.util.TreeMap;
+import javafx.collections.FXCollections;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import javafx.collections.transformation.SortedList;
+import javafx.scene.control.TablePositionBase;
+
+/**
+ * This class is copied from com.sun.javafx.scene.control.SelectedCellsMap
+ * temporary in 8u20 to resolve https://javafx-jira.kenai.com/browse/RT-38306
+ * 
+ * Will be removed in 8u40
+ *
+ * @param <T>
+ */
+public class SelectedCellsMapTemp<T extends TablePositionBase> {
+    private final ObservableList<T> selectedCells;
+    private final ObservableList<T> sortedSelectedCells;
+
+    private final Map<Integer, BitSet> selectedCellBitSetMap;
+
+    public SelectedCellsMapTemp(final ListChangeListener<T> listener) {
+        selectedCells = FXCollections.<T>observableArrayList();
+        sortedSelectedCells = new SortedList<>(selectedCells, (T o1, T o2) -> {
+            int result =  o1.getRow() - o2.getRow();
+           return result == 0 ? (o1.getColumn() - o2.getColumn())  : result;
+        });
+        sortedSelectedCells.addListener(listener);
+
+        selectedCellBitSetMap = new TreeMap<>((o1, o2) -> o1.compareTo(o2));
+    }
+
+    public int size() {
+        return selectedCells.size();
+    }
+
+    public T get(int i) {
+        if (i < 0) {
+            return null;
+        }
+        return sortedSelectedCells.get(i);
+    }
+
+    public void add(T tp) {
+        final int row = tp.getRow();
+        final int columnIndex = tp.getColumn();
+
+        // update the bitset map
+        BitSet bitset;
+        if (! selectedCellBitSetMap.containsKey(row)) {
+            bitset = new BitSet();
+            selectedCellBitSetMap.put(row, bitset);
+        } else {
+            bitset = selectedCellBitSetMap.get(row);
+        }
+
+        if (columnIndex >= 0) {
+            boolean isAlreadySet = bitset.get(columnIndex);
+            bitset.set(columnIndex);
+
+            if (! isAlreadySet) {
+                // add into the list
+                selectedCells.add(tp);
+            }
+        } else {
+            // FIXME slow path (for now)
+            if (! selectedCells.contains(tp)) {
+                selectedCells.add(tp);
+            }
+        }
+    }
+
+    public void addAll(Collection<T> cells) {
+        // update bitset
+        for (T tp : cells) {
+            final int row = tp.getRow();
+            final int columnIndex = tp.getColumn();
+
+            // update the bitset map
+            BitSet bitset;
+            if (! selectedCellBitSetMap.containsKey(row)) {
+                bitset = new BitSet();
+                selectedCellBitSetMap.put(row, bitset);
+            } else {
+                bitset = selectedCellBitSetMap.get(row);
+            }
+
+            if (columnIndex < 0) {
+                continue;
+            }
+
+            bitset.set(columnIndex);
+        }
+
+        // add into the list
+        selectedCells.addAll(cells);
+    }
+
+    public void setAll(Collection<T> cells) {
+        // update bitset
+        selectedCellBitSetMap.clear();
+        for (T tp : cells) {
+            final int row = tp.getRow();
+            final int columnIndex = tp.getColumn();
+
+            // update the bitset map
+            BitSet bitset;
+            if (! selectedCellBitSetMap.containsKey(row)) {
+                bitset = new BitSet();
+                selectedCellBitSetMap.put(row, bitset);
+            } else {
+                bitset = selectedCellBitSetMap.get(row);
+            }
+
+            if (columnIndex < 0) {
+                continue;
+            }
+
+            bitset.set(columnIndex);
+        }
+
+        // add into the list
+        selectedCells.setAll(cells);
+    }
+
+    public void remove(T tp) {
+        final int row = tp.getRow();
+        final int columnIndex = tp.getColumn();
+
+        // update the bitset map
+        if (selectedCellBitSetMap.containsKey(row)) {
+            BitSet bitset = selectedCellBitSetMap.get(row);
+
+            if (columnIndex >= 0) {
+                bitset.clear(columnIndex);
+            }
+
+            if (bitset.isEmpty()) {
+                selectedCellBitSetMap.remove(row);
+            }
+        }
+
+        // update list
+        selectedCells.remove(tp);
+    }
+
+    public void clear() {
+        // update bitset
+        selectedCellBitSetMap.clear();
+
+        // update list
+        selectedCells.clear();
+    }
+
+    public boolean isSelected(int row, int columnIndex) {
+        if (columnIndex < 0) {
+            return selectedCellBitSetMap.containsKey(row);
+        } else {
+            return selectedCellBitSetMap.containsKey(row) ? selectedCellBitSetMap.get(row).get(columnIndex) : false;
+        }
+    }
+
+    public int indexOf(T tp) {
+        return sortedSelectedCells.indexOf(tp);
+    }
+
+    public boolean isEmpty() {
+        return selectedCells.isEmpty();
+    }
+
+    public ObservableList<T> getSelectedCells() {
+        return selectedCells;
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/SpreadsheetGridView.java b/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/SpreadsheetGridView.java
new file mode 100644
index 0000000..c796043
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/SpreadsheetGridView.java
@@ -0,0 +1,72 @@
+/**
+ * Copyright (c) 2013, 2014, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.spreadsheet;
+
+import javafx.collections.ObservableList;
+import javafx.scene.control.TableView;
+import org.controlsfx.control.spreadsheet.SpreadsheetCell;
+import org.controlsfx.control.spreadsheet.SpreadsheetView;
+
+public class SpreadsheetGridView extends TableView<ObservableList<SpreadsheetCell>> {
+    private final SpreadsheetHandle handle;
+
+    /*
+     * cache the stylesheet as lookup takes time and the getUserAgentStylesheet is called repeatedly
+     */
+    private String stylesheet;
+
+    /**
+     * We don't want to show the current value in the TextField when we are
+     * editing by typing a key. We want directly to take those typed letters
+     * and put them into the textfield.
+     */
+    public SpreadsheetGridView(SpreadsheetHandle handle) {
+        this.handle = handle;
+    }
+
+    @Override
+    public String getUserAgentStylesheet() {
+        /*
+         * For more information please see RT-40658
+         */
+        if (stylesheet == null) {
+            stylesheet = SpreadsheetView.class.getResource("spreadsheet.css") //$NON-NLS-1$
+                    .toExternalForm();
+        }
+
+        return stylesheet;
+    }
+
+    @Override
+    protected javafx.scene.control.Skin<?> createDefaultSkin() {
+        return new GridViewSkin(handle);
+    }
+
+    public GridViewSkin getGridViewSkin() {
+        return handle.getCellsViewSkin();
+    }
+};
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/SpreadsheetHandle.java b/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/SpreadsheetHandle.java
new file mode 100644
index 0000000..474f009
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/SpreadsheetHandle.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2013, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package impl.org.controlsfx.spreadsheet;
+
+import org.controlsfx.control.spreadsheet.SpreadsheetView;
+
+/**
+ * Implementation vs public bridge.
+ */
+public abstract class SpreadsheetHandle {
+	/** Access the main control. */
+	protected abstract SpreadsheetView getView();
+	/** Accesses the grid (ie cell table) in the spreadsheet. */
+	protected abstract SpreadsheetGridView getGridView();
+	/** Accesses the grid view (ie cell table view). */
+	protected abstract GridViewSkin getCellsViewSkin();
+        /** Whether that column width has been set by the user. */
+        protected abstract boolean isColumnWidthSet(int indexColumn);
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/TableViewSpanSelectionModel.java b/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/TableViewSpanSelectionModel.java
new file mode 100644
index 0000000..c2b585b
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/TableViewSpanSelectionModel.java
@@ -0,0 +1,912 @@
+/**
+ * Copyright (c) 2013, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.spreadsheet;
+
+import com.sun.javafx.collections.MappingChange;
+import com.sun.javafx.collections.NonIterableChange;
+import com.sun.javafx.scene.control.ReadOnlyUnbackedObservableList;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import javafx.animation.KeyFrame;
+import javafx.animation.Timeline;
+import javafx.beans.InvalidationListener;
+import javafx.beans.NamedArg;
+import javafx.beans.Observable;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import javafx.collections.WeakListChangeListener;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.event.WeakEventHandler;
+import javafx.scene.control.SelectionMode;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableColumnBase;
+import javafx.scene.control.TablePosition;
+import javafx.scene.control.TableView;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.input.MouseEvent;
+import javafx.util.Duration;
+import javafx.util.Pair;
+import org.controlsfx.control.spreadsheet.SpreadsheetCell;
+import org.controlsfx.control.spreadsheet.SpreadsheetView;
+
+/**
+ *
+ * The Selection Model adapted for the SpreadsheetView regarding span.
+ */
+public class TableViewSpanSelectionModel extends
+        TableView.TableViewSelectionModel<ObservableList<SpreadsheetCell>>{
+
+    private boolean shift = false; // Register state of 'shift' key
+    private boolean key = false; // Register if we last touch the keyboard
+    // or the mouse
+    private boolean drag = false; // register if we are dragging (no
+    // edition)
+    private MouseEvent mouseEvent;
+    private boolean makeAtomic;
+    private SpreadsheetGridView cellsView;
+
+    private SpreadsheetView spreadsheetView;
+    // the only 'proper' internal data structure, selectedItems and
+    // selectedIndices
+    // are both 'read-only and unbacked'.
+    private final SelectedCellsMapTemp<TablePosition<ObservableList<SpreadsheetCell>, ?>> selectedCellsMap;
+
+    // we create a ReadOnlyUnbackedObservableList of selectedCells here so
+    // that we can fire custom list change events.
+    private final ReadOnlyUnbackedObservableList<TablePosition<ObservableList<SpreadsheetCell>, ?>> selectedCellsSeq;
+
+    /**
+     * We use these variable in order to stay on the same row/column when
+     * navigating with arrows. If we are going down, and we are arriving on a
+     * column-spanning cell, when going down again, we don't want to go on the
+     * starting column of the spanning cell but on the same column we arrived
+     * previously.
+     */
+    private int oldCol = -1;
+    private TableColumn oldTableColumn = null;
+    private int oldRow = -1;
+    Pair<Integer, Integer> direction;
+    private int oldColSpan = -1;
+    private int oldRowSpan = -1;
+    /**
+     * Make the tableView move when selection operating outside bounds
+     */
+    private final Timeline timer;
+
+    private final EventHandler<ActionEvent> timerEventHandler = (ActionEvent event) -> {
+        GridViewSkin skin = (GridViewSkin) getCellsViewSkin();
+        if (mouseEvent != null && !cellsView.contains(mouseEvent.getX(), mouseEvent.getY())) {
+            double sceneX = mouseEvent.getSceneX();
+            double sceneY = mouseEvent.getSceneY();
+            double layoutX = cellsView.getLayoutX();
+            double layoutY = cellsView.getLayoutY();
+            double layoutXMax = layoutX + cellsView.getWidth();
+            double layoutYMax = layoutY + cellsView.getHeight();
+
+            if (sceneX > layoutXMax) {
+                skin.getHBar().increment();
+            } else if (sceneX < layoutX) {
+                skin.getHBar().decrement();
+            }
+            if (sceneY > layoutYMax) {
+                skin.getVBar().increment();
+            } else if (sceneY < layoutY) {
+                skin.getVBar().decrement();
+            }
+        }
+    };
+    /**
+     * When the drag is over, we remove the listener and stop the timer
+     */
+    private final EventHandler<MouseEvent> dragDoneHandler = new EventHandler<MouseEvent>() {
+        @Override
+        public void handle(MouseEvent mouseEvent) {
+            drag = false;
+            timer.stop();
+            spreadsheetView.removeEventHandler(MouseEvent.MOUSE_RELEASED, this);
+        }
+    };
+
+    private final EventHandler<KeyEvent> keyPressedEventHandler = (KeyEvent keyEvent) -> {
+        key = true;
+        shift = keyEvent.isShiftDown();
+    };
+
+    private final EventHandler<MouseEvent> mousePressedEventHandler = (MouseEvent mouseEvent1) -> {
+        key = false;
+        shift = mouseEvent1.isShiftDown();
+    };
+
+    private final EventHandler<MouseEvent> onDragDetectedEventHandler = new EventHandler<MouseEvent>() {
+        @Override
+        public void handle(MouseEvent mouseEvent) {
+            cellsView.addEventHandler(MouseEvent.MOUSE_RELEASED, dragDoneHandler);
+            drag = true;
+            timer.setCycleCount(Timeline.INDEFINITE);
+            timer.play();
+        }
+    };
+
+    private final EventHandler<MouseEvent> onMouseDragEventHandler = (MouseEvent e) -> {
+        mouseEvent = e;
+    };
+
+    private final ListChangeListener<TablePosition<ObservableList<SpreadsheetCell>, ?>> listChangeListener = this::handleSelectedCellsListChangeEvent;
+
+    /**
+     * *********************************************************************
+     *
+     * Constructors
+     *
+     *********************************************************************
+     */
+    /**
+     * Constructor
+     * @param spreadsheetView
+     * @param cellsView
+     */
+    public TableViewSpanSelectionModel(@NamedArg("spreadsheetView") SpreadsheetView spreadsheetView, @NamedArg("cellsView") SpreadsheetGridView cellsView) {
+        super(cellsView);
+        this.cellsView = cellsView;
+        this.spreadsheetView = spreadsheetView;
+
+        timer = new Timeline(new KeyFrame(Duration.millis(100), new WeakEventHandler<>((timerEventHandler))));
+        cellsView.addEventHandler(KeyEvent.KEY_PRESSED, new WeakEventHandler<>(keyPressedEventHandler));
+
+        cellsView.addEventFilter(MouseEvent.MOUSE_PRESSED, new WeakEventHandler<>(mousePressedEventHandler));
+        cellsView.setOnDragDetected(new WeakEventHandler<>(onDragDetectedEventHandler));
+
+        cellsView.setOnMouseDragged(new WeakEventHandler<>(onMouseDragEventHandler));
+
+        selectedCellsMap = new SelectedCellsMapTemp<>(new WeakListChangeListener<>(listChangeListener));
+
+        selectedCellsSeq = new ReadOnlyUnbackedObservableList<TablePosition<ObservableList<SpreadsheetCell>, ?>>() {
+            @Override
+            public TablePosition<ObservableList<SpreadsheetCell>, ?> get(int i) {
+                return selectedCellsMap.get(i);
+            }
+
+            @Override
+            public int size() {
+                return selectedCellsMap.size();
+            }
+        };
+    }
+
+    private void handleSelectedCellsListChangeEvent(
+            ListChangeListener.Change<? extends TablePosition<ObservableList<SpreadsheetCell>, ?>> c) {
+        if (makeAtomic) {
+            return;
+        }
+
+        selectedCellsSeq.callObservers(new MappingChange<>(c, MappingChange.NOOP_MAP, selectedCellsSeq));
+        c.reset();
+    }
+
+    /**
+     * *********************************************************************
+     * * Public selection API * *
+     * ********************************************************************
+     */
+    private TablePosition<ObservableList<SpreadsheetCell>, ?> old = null;
+
+    @Override
+    public void select(int row, TableColumn<ObservableList<SpreadsheetCell>, ?> column) {
+        if (row < 0 || row >= getItemCount()) {
+            return;
+        }
+
+        // if I'm in cell selection mode but the column is null, I don't
+        // want
+        // to select the whole row instead...
+        if (isCellSelectionEnabled() && column == null) {
+            return;
+        }
+        // Variable we need for algorithm
+        TablePosition<ObservableList<SpreadsheetCell>, ?> posFinal = new TablePosition<>(getTableView(), row,
+                column);
+
+        final SpreadsheetView.SpanType spanType = spreadsheetView.getSpanType(row, posFinal.getColumn());
+
+        /**
+         * We check if we are on covered cell. If so we have the algorithm of
+         * the focus model to give the selection to the right cell.
+         *
+         */
+        switch (spanType) {
+            case ROW_SPAN_INVISIBLE:
+                /**
+                 * If we notice that the new selected cell is the previous one,
+                 * then it means that we were already on the cell and we wanted
+                 * to go below. We make sure that old is not null, and that the
+                 * move is initiated by keyboard. Because if it's a click, then
+                 * we just want to go on the clicked cell (not below)
+                 */
+                if (old != null && !shift && old.getColumn() == posFinal.getColumn()
+                        && old.getRow() == posFinal.getRow() - 1) {
+                    int visibleRow = FocusModelListener.getNextRowNumber(old, cellsView);
+                    /**
+                     * If the visibleRow we're targeting is out of bounds, we do
+                     * not want to get a visibleCell, so we step out. But we
+                     * need to set edition to false because we will be going
+                     * back to the old cell and we could go to edition.
+                     */
+                    if (visibleRow < getItemCount()) {
+                        posFinal = getVisibleCell(visibleRow, old.getTableColumn(), old.getColumn());
+                        break;
+                    }
+                }
+                // If the current selected cell if hidden by row span, we go
+                // above
+                posFinal = getVisibleCell(row, column, posFinal.getColumn());
+                break;
+            case BOTH_INVISIBLE:
+                // If the current selected cell if hidden by a both (row and
+                // column) span, we go left-above
+                posFinal = getVisibleCell(row, column, posFinal.getColumn());
+                break;
+            case COLUMN_SPAN_INVISIBLE:
+                // If we notice that the new selected cell is the previous one,
+                // then it means that we were
+                // already on the cell and we wanted to go right.
+                if (old != null && !shift && old.getColumn() == posFinal.getColumn() - 1
+                        && old.getRow() == posFinal.getRow()) {
+                    posFinal = getVisibleCell(old.getRow(), FocusModelListener.getTableColumnSpan(old, cellsView), getTableColumnSpanInt(old));
+                } else {
+                    // If the current selected cell if hidden by column span, we
+                    // go left
+                        posFinal = getVisibleCell(row, column, posFinal.getColumn());
+                }
+            default:
+                break;
+        }
+
+        if (direction != null && key) {
+            /**
+             * If I'm going up or down, and the previous cell had a column span,
+             * then we take the column used before instead of the current
+             * column.
+             */
+            if (direction.getKey() != 0 && oldColSpan > 1) {
+                posFinal = getVisibleCell(posFinal.getRow(), oldTableColumn, oldCol);
+            } else if (direction.getValue() != 0 && oldRowSpan > 1) {
+                posFinal = getVisibleCell(oldRow, posFinal.getTableColumn(), posFinal.getColumn());
+            }
+        }
+        old = posFinal;
+
+        //If it's a click, we register everything.
+        if (!key) {
+            oldRow = old.getRow();
+            oldCol = old.getColumn();
+            oldTableColumn = old.getTableColumn();
+        } else {
+            //If we're going up or down, we register the row changing, not the column.
+            if (direction != null && direction.getKey() != 0) {
+                oldRow = old.getRow();
+            } else if (direction != null && direction.getValue() != 0) {
+                oldCol = old.getColumn();
+                oldTableColumn = old.getTableColumn();
+            }
+        }
+        if (getSelectionMode() == SelectionMode.SINGLE) {
+            quietClearSelection();
+        }
+        SpreadsheetCell cell = cellsView.getItems().get(old.getRow()).get(old.getColumn());
+        oldRowSpan = cell.getRowSpan();
+        oldColSpan = cell.getColumnSpan();
+        for (int i = cell.getRow(); i < cell.getRowSpan() + cell.getRow(); ++i) {
+            for (int j = cell.getColumn(); j < cell.getColumnSpan() + cell.getColumn(); ++j) {
+                posFinal = new TablePosition<>(getTableView(), i, getTableView().getVisibleLeafColumn(j));
+                selectedCellsMap.add(posFinal);
+            }
+        }
+
+        updateScroll(old);
+        addSelectedRowsAndColumns(old);
+
+        setSelectedIndex(old.getRow());
+        setSelectedItem(getModelItem(old.getRow()));
+        if (getTableView().getFocusModel() == null) {
+            return;
+        }
+
+        getTableView().getFocusModel().focus(old.getRow(), old.getTableColumn());
+    }
+
+    /**
+     * We try to make visible the rows that may be hidden by Fixed rows.
+     *
+     * @param posFinal
+     */
+    private void updateScroll(TablePosition<ObservableList<SpreadsheetCell>, ?> posFinal) {
+
+        /**
+         * We don't want to do any scroll when dragging or selecting with click.
+         * Only keyboard action arrow action.
+         */
+        if (!drag && key && getCellsViewSkin().getCellsSize() != 0 && spreadsheetView.getFixedRows().size() != 0) {
+
+            int start = getCellsViewSkin().getRow(0).getIndex();
+            double posFinalOffset = 0;
+            for (int j = start; j < posFinal.getRow(); ++j) {
+                posFinalOffset += getSpreadsheetViewSkin().getRowHeight(j);
+            }
+
+            if (getCellsViewSkin().getFixedRowHeight() > posFinalOffset) {
+                cellsView.scrollTo(posFinal.getRow());
+            }
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public void clearSelection(int row, TableColumn<ObservableList<SpreadsheetCell>, ?> column) {
+
+        final TablePosition<ObservableList<SpreadsheetCell>, ?> tp = new TablePosition<>(getTableView(), row,
+                column);
+        if (tp.getRow() < 0 || tp.getColumn() < 0) {
+            return;
+        }
+        List<TablePosition<ObservableList<SpreadsheetCell>, ?>> selectedCells;
+        if ((selectedCells = isSelectedRange(row, column, tp.getColumn())) != null) {
+            for (TablePosition<ObservableList<SpreadsheetCell>, ?> cell : selectedCells) {
+                selectedCellsMap.remove(cell);
+                removeSelectedRowsAndColumns(cell);
+                focus(cell.getRow());
+            }
+        } else {
+            for (TablePosition<ObservableList<SpreadsheetCell>, ?> pos : getSelectedCells()) {
+                if (pos.equals(tp)) {
+                    selectedCellsMap.remove(pos);
+                    removeSelectedRowsAndColumns(pos);
+                    // give focus to this cell index
+                    focus(row);
+                    return;
+                }
+            }
+        }
+    }
+
+    /**
+     * When we set a new grid, we need to update the selected Cells because
+     * otherwise we will end up with TablePosition which have "-1" as their
+     * column number. So we need to verify that the old selected cells are still
+     * selectable and select them.
+     *
+     * @param selectedCells
+     */
+    public void verifySelectedCells(List<Pair<Integer, Integer>> selectedCells) {
+        List<TablePosition<ObservableList<SpreadsheetCell>, ?>> newList = new ArrayList<>();
+        clearSelection();
+
+        final int itemCount = getItemCount();
+        final int columnSize = getTableView().getColumns().size();
+        final HashSet<Integer> selectedRows = new HashSet<>();
+        final HashSet<Integer> selectedColumns = new HashSet<>();
+        TablePosition<ObservableList<SpreadsheetCell>, ?> pos = null;
+        for (Pair<Integer, Integer> position : selectedCells) {
+            if (position.getKey() < 0
+                    || position.getKey() >= itemCount
+                    || position.getValue() < 0
+                    || position.getValue() >= columnSize) {
+                continue;
+            }
+
+            final TableColumn<ObservableList<SpreadsheetCell>, ?> column = getTableView().getVisibleLeafColumn(position.getValue());
+
+            pos = getVisibleCell(position.getKey(), column, position.getValue());
+            // We store all the selectedColumn and Rows, we will update
+            // just once at the end
+            final SpreadsheetCell cell = cellsView.getItems().get(pos.getRow()).get(pos.getColumn());
+            for (int i = cell.getRow(); i < cell.getRowSpan() + cell.getRow(); ++i) {
+                selectedColumns.add(i);
+                for (int j = cell.getColumn(); j < cell.getColumnSpan() + cell.getColumn(); ++j) {
+                    selectedRows.add(j);
+                    pos = new TablePosition<>(getTableView(), i, getTableView().getVisibleLeafColumn(j));
+                    newList.add(pos);
+                }
+            }
+        }
+        selectedCellsMap.setAll(newList);
+
+        final TablePosition finalPos = pos;
+        // Then we update visuals just once
+        GridViewSkin skin = getSpreadsheetViewSkin();
+        //If the skin is null, we just wait till everything is ready..
+        if (skin == null) {
+            cellsView.skinProperty().addListener(new InvalidationListener() {
+
+                @Override
+                public void invalidated(Observable observable) {
+                    cellsView.skinProperty().removeListener(this);
+                    GridViewSkin skin = getSpreadsheetViewSkin();
+                    if (skin != null) {
+                        updateSelectedVisuals(skin, finalPos, selectedRows, selectedColumns);
+                    }
+                }
+            });
+        } else {
+            updateSelectedVisuals(skin, pos, selectedRows, selectedColumns);
+        }
+    }
+
+    /**
+     * When all the selection has been made, we just need to light up the
+     * indicators that are showing which indexes are selected.
+     *
+     * @param skin
+     * @param pos
+     * @param selectedRows
+     * @param selectedColumns
+     */
+    private void updateSelectedVisuals(GridViewSkin skin, TablePosition pos, HashSet<Integer> selectedRows, HashSet<Integer> selectedColumns) {
+        if (skin != null) {
+            skin.getSelectedRows().addAll(selectedColumns);
+            skin.getSelectedColumns().addAll(selectedRows);
+        }
+
+        /**
+         * If we made some selection, we need to force the visual selected
+         * confirmation to come when the layout is starting. Doing it before
+         * will result in a selected cell with no css applied to it.
+         */
+        if (pos != null) {
+            getCellsViewSkin().lastRowLayout.set(true);
+            getCellsViewSkin().lastRowLayout.addListener(new InvalidationListener() {
+
+                @Override
+                public void invalidated(Observable observable) {
+                    handleSelectedCellsListChangeEvent(new NonIterableChange.SimpleAddChange<>(0,
+                            selectedCellsMap.size(), selectedCellsSeq));
+                    getCellsViewSkin().lastRowLayout.removeListener(this);
+                }
+            });
+        }
+    }
+
+    @Override
+    public void selectRange(int minRow, TableColumnBase<ObservableList<SpreadsheetCell>, ?> minColumn, int maxRow,
+            TableColumnBase<ObservableList<SpreadsheetCell>, ?> maxColumn) {
+
+        if (getSelectionMode() == SelectionMode.SINGLE) {
+            quietClearSelection();
+            select(maxRow, maxColumn);
+            return;
+        }
+        SpreadsheetCell cell;
+
+        makeAtomic = true;
+
+        final int itemCount = getItemCount();
+
+        final int minColumnIndex = getTableView().getVisibleLeafIndex(
+                (TableColumn<ObservableList<SpreadsheetCell>, ?>) minColumn);
+        final int maxColumnIndex = getTableView().getVisibleLeafIndex(
+                (TableColumn<ObservableList<SpreadsheetCell>, ?>) maxColumn);
+        final int _minColumnIndex = Math.min(minColumnIndex, maxColumnIndex);
+        final int _maxColumnIndex = Math.max(minColumnIndex, maxColumnIndex);
+
+        final int _minRow = Math.min(minRow, maxRow);
+        final int _maxRow = Math.max(minRow, maxRow);
+
+        HashSet<Integer> selectedRows = new HashSet<>();
+        HashSet<Integer> selectedColumns = new HashSet<>();
+
+        for (int _row = _minRow; _row <= _maxRow; _row++) {
+            for (int _col = _minColumnIndex; _col <= _maxColumnIndex; _col++) {
+                // begin copy/paste of select(int, column) method (with some
+                // slight modifications)
+                if (_row < 0 || _row >= itemCount) {
+                    continue;
+                }
+
+                final TableColumn<ObservableList<SpreadsheetCell>, ?> column = getTableView().getVisibleLeafColumn(
+                        _col);
+
+                // if I'm in cell selection mode but the column is null, I
+                // don't want
+                // to select the whole row instead...
+                if (column == null) {
+                    continue;
+                }
+
+                TablePosition<ObservableList<SpreadsheetCell>, ?> pos = getVisibleCell(_row, column, _col);
+
+                // We store all the selectedColumn and Rows, we will update
+                // just once at the end
+                cell = cellsView.getItems().get(pos.getRow()).get(pos.getColumn());
+                for (int i = cell.getRow(); i < cell.getRowSpan() + cell.getRow(); ++i) {
+                    selectedColumns.add(i);
+                    for (int j = cell.getColumn(); j < cell.getColumnSpan() + cell.getColumn(); ++j) {
+                        selectedRows.add(j);
+                        pos = new TablePosition<>(getTableView(), i, getTableView().getVisibleLeafColumn(j));
+                        selectedCellsMap.add(pos);
+                    }
+                }
+
+//                makeAtomic = true;
+                // end copy/paste
+            }
+        }
+        makeAtomic = false;
+
+        // Then we update visuals just once
+        getSpreadsheetViewSkin().getSelectedRows().addAll(selectedColumns);
+        getSpreadsheetViewSkin().getSelectedColumns().addAll(selectedRows);
+
+        // fire off events
+        setSelectedIndex(maxRow);
+        setSelectedItem(getModelItem(maxRow));
+        if (getTableView().getFocusModel() == null) {
+            return;
+        }
+
+        //FIXME Focus is wrong, and endIndex also..
+        getTableView().getFocusModel().focus(maxRow, (TableColumn<ObservableList<SpreadsheetCell>, ?>) maxColumn);
+
+        /**
+         * If we end up on a spanned cell, there is not reliable way to
+         * determine which is the last index, certainly not the maxRow and
+         * maxColumn. So right now we need to take this extreme measure in order
+         * to be sure that the cells will be highlighted correctly.
+         */
+        final int startChangeIndex = selectedCellsMap.indexOf(new TablePosition<>(getTableView(), minRow,
+                (TableColumn<ObservableList<SpreadsheetCell>, ?>) minColumn));
+        final int endChangeIndex = selectedCellsMap.getSelectedCells().size() - 1;//indexOf(new TablePosition<>(getTableView(), maxRow,
+//                (TableColumn<ObservableList<SpreadsheetCell>, ?>) maxColumn));
+
+        if (startChangeIndex > -1 && endChangeIndex > -1) {
+            final int startIndex = Math.min(startChangeIndex, endChangeIndex);
+            final int endIndex = Math.max(startChangeIndex, endChangeIndex);
+            handleSelectedCellsListChangeEvent(new NonIterableChange.SimpleAddChange<>(startIndex,
+                    endIndex + 1, selectedCellsSeq));
+        }
+    }
+
+    @Override
+    public void selectAll() {
+        if (getSelectionMode() == SelectionMode.SINGLE) {
+            return;
+        }
+
+        quietClearSelection();
+
+        List<TablePosition<ObservableList<SpreadsheetCell>, ?>> indices = new ArrayList<>();
+        TableColumn<ObservableList<SpreadsheetCell>, ?> column;
+        TablePosition<ObservableList<SpreadsheetCell>, ?> tp = null;
+
+        for (int col = 0; col < getTableView().getVisibleLeafColumns().size(); col++) {
+            column = getTableView().getVisibleLeafColumns().get(col);
+            for (int row = 0; row < getItemCount(); row++) {
+                tp = new TablePosition<>(getTableView(), row, column);
+                indices.add(tp);
+            }
+        }
+        selectedCellsMap.setAll(indices);
+
+        // Then we update visuals just once
+        ArrayList<Integer> selectedColumns = new ArrayList<>();
+        for (int col = 0; col < spreadsheetView.getGrid().getColumnCount(); col++) {
+            selectedColumns.add(col);
+        }
+
+        ArrayList<Integer> selectedRows = new ArrayList<>();
+        for (int row = 0; row < spreadsheetView.getGrid().getRowCount(); row++) {
+            selectedRows.add(row);
+        }
+        getSpreadsheetViewSkin().getSelectedRows().addAll(selectedRows);
+        getSpreadsheetViewSkin().getSelectedColumns().addAll(selectedColumns);
+
+        if (tp != null) {
+            select(tp.getRow(), tp.getTableColumn());
+            //Just like verticalHeader, the focus should be put on the 
+            //first cell to ease copy/paste operation.
+            getTableView().getFocusModel().focus(0, getTableView().getColumns().get(0));
+        }
+    }
+
+    @Override
+    public boolean isSelected(int row, TableColumn<ObservableList<SpreadsheetCell>, ?> column) {
+        // When in cell selection mode, we currently do NOT support
+        // selecting
+        // entire rows, so a isSelected(row, null)
+        // should always return false.
+        if (column == null || row < 0) {
+            return false;
+        }
+
+        int columnIndex = getTableView().getVisibleLeafIndex(column);
+
+        if (getCellsViewSkin().getCellsSize() != 0) {
+            TablePosition<ObservableList<SpreadsheetCell>, ?> posFinal = getVisibleCell(row, column, columnIndex);
+            return selectedCellsMap.isSelected(posFinal.getRow(), posFinal.getColumn());
+        } else {
+            return selectedCellsMap.isSelected(row, columnIndex);
+        }
+    }
+
+    /**
+     * Return the tablePosition of a selected cell inside a spanned cell if any.
+     *
+     * @param row
+     * @param column
+     * @param col
+     * @return
+     */
+    public List<TablePosition<ObservableList<SpreadsheetCell>, ?>> isSelectedRange(int row,
+            TableColumn<ObservableList<SpreadsheetCell>, ?> column, int col) {
+
+        if (col < 0 || row < 0) {
+            return null;
+        }
+
+        final SpreadsheetCell cellSpan = cellsView.getItems().get(row).get(col);
+        final int infRow = cellSpan.getRow();
+        final int supRow = infRow + cellSpan.getRowSpan();
+
+        final int infCol = cellSpan.getColumn();
+        final int supCol = infCol + cellSpan.getColumnSpan();
+        List<TablePosition<ObservableList<SpreadsheetCell>, ?>> selectedCells = new ArrayList<>();
+        for (final TablePosition<ObservableList<SpreadsheetCell>, ?> tp : getSelectedCells()) {
+            if (tp.getRow() >= infRow && tp.getRow() < supRow && tp.getColumn() >= infCol
+                    && tp.getColumn() < supCol) {
+                selectedCells.add(tp);
+            }
+        }
+        return selectedCells.isEmpty()? null : selectedCells;
+    }
+
+    /**
+     * *********************************************************************
+     * * Support code * *
+     * ********************************************************************
+     */
+    private void addSelectedRowsAndColumns(TablePosition<?, ?> position) {
+        GridViewSkin skin = getSpreadsheetViewSkin();
+        if (skin == null) {
+            return;
+        }
+        final SpreadsheetCell cell = cellsView.getItems().get(position.getRow()).get(position.getColumn());
+        for (int i = cell.getRow(); i < cell.getRowSpan() + cell.getRow(); ++i) {
+            skin.getSelectedRows().add(i);
+            for (int j = cell.getColumn(); j < cell.getColumnSpan() + cell.getColumn(); ++j) {
+                skin.getSelectedColumns().add(j);
+            }
+        }
+    }
+
+    private void removeSelectedRowsAndColumns(TablePosition<?, ?> position) {
+        final SpreadsheetCell cell = cellsView.getItems().get(position.getRow()).get(position.getColumn());
+        for (int i = cell.getRow(); i < cell.getRowSpan() + cell.getRow(); ++i) {
+            getSpreadsheetViewSkin().getSelectedRows().remove(Integer.valueOf(i));
+            for (int j = cell.getColumn(); j < cell.getColumnSpan() + cell.getColumn(); ++j) {
+                getSpreadsheetViewSkin().getSelectedColumns().remove(Integer.valueOf(j));
+            }
+        }
+    }
+
+    @Override
+    public void clearAndSelect(int row, TableColumn<ObservableList<SpreadsheetCell>, ?> column) {
+            // RT-33558 if this method has been called with a given row/column
+        // intersection, and that row/column intersection is the only
+        // selection currently, then this method becomes a no-op.
+
+        // This is understandable but not compatible with spanning
+        // selection.
+            /*
+         * if (getSelectedCells().size() == 1 && isSelected(row, column)) {
+         * return; }
+         */
+        makeAtomic = true;
+        // firstly we make a copy of the selection, so that we can send out
+        // the correct details in the selection change event
+        List<TablePosition<ObservableList<SpreadsheetCell>, ?>> previousSelection = new ArrayList<>(
+                selectedCellsMap.getSelectedCells());
+
+        // then clear the current selection
+        clearSelection();
+
+        // and select the new row
+        select(row, column);
+
+        makeAtomic = false;
+
+        // fire off a single add/remove/replace notification (rather than
+        // individual remove and add notifications) - see RT-33324
+        if (old != null && old.getColumn() >= 0) {
+            TableColumn<ObservableList<SpreadsheetCell>, ?> columnFinal = getTableView().getColumns().get(
+                    old.getColumn());
+            int changeIndex = selectedCellsSeq.indexOf(new TablePosition<>(getTableView(), old.getRow(),
+                    columnFinal));
+            NonIterableChange.GenericAddRemoveChange<TablePosition<ObservableList<SpreadsheetCell>, ?>> change = new NonIterableChange.GenericAddRemoveChange<>(
+                    changeIndex, changeIndex + 1, previousSelection, selectedCellsSeq);
+            handleSelectedCellsListChangeEvent(change);
+        }
+    }
+
+    /**
+     * FIXME I don't understand why TablePosition is not parameterized in the
+     * API..
+     *
+     * @return
+     */
+    @Override
+    public ObservableList<TablePosition> getSelectedCells() {
+        return (ObservableList<TablePosition>) (Object) selectedCellsSeq;
+    }
+
+    @Override
+    public void selectAboveCell() {
+        final TablePosition<ObservableList<SpreadsheetCell>, ?> pos = getFocusedCell();
+        if (pos.getRow() == -1) {
+            select(getItemCount() - 1);
+        } else if (pos.getRow() > 0) {
+            select(pos.getRow() - 1, pos.getTableColumn());
+        }
+
+    }
+
+    @Override
+    public void selectBelowCell() {
+        final TablePosition<ObservableList<SpreadsheetCell>, ?> pos = getFocusedCell();
+
+        if (pos.getRow() == -1) {
+            select(0);
+        } else if (pos.getRow() < getItemCount() - 1) {
+            select(pos.getRow() + 1, pos.getTableColumn());
+        }
+
+    }
+
+    @Override
+    public void selectLeftCell() {
+        if (!isCellSelectionEnabled()) {
+            return;
+        }
+
+        final TablePosition<ObservableList<SpreadsheetCell>, ?> pos = getFocusedCell();
+        if (pos.getColumn() - 1 >= 0) {
+            select(pos.getRow(), getTableColumn(pos.getTableColumn(), -1));
+        }
+
+    }
+
+    @Override
+    public void selectRightCell() {
+        if (!isCellSelectionEnabled()) {
+            return;
+        }
+
+        final TablePosition<ObservableList<SpreadsheetCell>, ?> pos = getFocusedCell();
+        if (pos.getColumn() + 1 < getTableView().getVisibleLeafColumns().size()) {
+            select(pos.getRow(), getTableColumn(pos.getTableColumn(), 1));
+        }
+
+    }
+
+    @Override
+    public void clearSelection() {
+        if (!makeAtomic) {
+            setSelectedIndex(-1);
+            setSelectedItem(getModelItem(-1));
+            focus(-1);
+        }
+        quietClearSelection();
+    }
+
+    private void quietClearSelection() {
+        selectedCellsMap.clear();
+        GridViewSkin skin = getSpreadsheetViewSkin();
+        if (skin != null) {
+            skin.getSelectedRows().clear();
+            skin.getSelectedColumns().clear();
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private TablePosition<ObservableList<SpreadsheetCell>, ?> getFocusedCell() {
+        if (getTableView().getFocusModel() == null) {
+            return new TablePosition<>(getTableView(), -1, null);
+        }
+        return (TablePosition<ObservableList<SpreadsheetCell>, ?>) cellsView.getFocusModel().getFocusedCell();
+    }
+
+    private TableColumn<ObservableList<SpreadsheetCell>, ?> getTableColumn(
+            TableColumn<ObservableList<SpreadsheetCell>, ?> column, int offset) {
+        final int columnIndex = getTableView().getVisibleLeafIndex(column);
+        final int newColumnIndex = columnIndex + offset;
+        return getTableView().getVisibleLeafColumn(newColumnIndex);
+    }
+
+    private GridViewSkin getSpreadsheetViewSkin() {
+        return (GridViewSkin) getCellsViewSkin();
+    }
+
+    /**
+     * For a position, return the Visible Cell associated with It can be the top
+     * of the span cell if it's visible, or it can be the first row visible if
+     * we have scrolled
+     *
+     * @param row
+     * @param column
+     * @param col
+     * @return
+     */
+    private TablePosition<ObservableList<SpreadsheetCell>, ?> getVisibleCell(int row,
+            TableColumn<ObservableList<SpreadsheetCell>, ?> column, int col) {
+        final SpreadsheetView.SpanType spanType = spreadsheetView.getSpanType(row, col);
+        switch (spanType) {
+            case NORMAL_CELL:
+            case ROW_VISIBLE:
+                return new TablePosition<>(cellsView, row, column);
+            case BOTH_INVISIBLE:
+            case COLUMN_SPAN_INVISIBLE:
+            case ROW_SPAN_INVISIBLE:
+            default:
+                final SpreadsheetCell cellSpan = cellsView.getItems().get(row).get(col);
+                if (getCellsViewSkin() == null || (getCellsViewSkin().getCellsSize() != 0 && getNonFixedRow(0).getIndex() <= cellSpan.getRow())) {
+                    return new TablePosition<>(cellsView, cellSpan.getRow(), cellsView.getColumns().get(
+                            cellSpan.getColumn()));
+                } else { // If it's not, then it's the firstkey
+                    return new TablePosition<>(cellsView, getNonFixedRow(0).getIndex(), cellsView.getColumns().get(
+                            cellSpan.getColumn()));
+                }
+        }
+    }
+
+    /**
+     * @return the inner table view skin
+     */
+    final GridViewSkin getCellsViewSkin() {
+        return (GridViewSkin) (cellsView.getSkin());
+    }
+
+    /**
+     * Return the {@link GridRow} at the specified index
+     *
+     * @param index
+     * @return
+     */
+    private GridRow getNonFixedRow(int index) {
+        return getCellsViewSkin().getRow(index);
+    }
+
+    /**
+     * Return the TableColumn right after the current TablePosition (including
+     * the ColumSpan to be on a visible Cell)
+     *
+     * @param t the current TablePosition
+     * @return
+     */
+    private int getTableColumnSpanInt(final TablePosition<?, ?> t) {
+        return t.getColumn() + cellsView.getItems().get(t.getRow()).get(t.getColumn()).getColumnSpan();
+    }
+
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/VerticalHeader.java b/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/VerticalHeader.java
new file mode 100644
index 0000000..0cefc9e
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/spreadsheet/VerticalHeader.java
@@ -0,0 +1,675 @@
+/**
+ * Copyright (c) 2013, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.spreadsheet;
+
+import static impl.org.controlsfx.i18n.Localization.asKey;
+import static impl.org.controlsfx.i18n.Localization.localize;
+
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.List;
+import java.util.Set;
+import java.util.Stack;
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.ReadOnlyDoubleProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.ObservableList;
+import javafx.event.ActionEvent;
+import javafx.event.Event;
+import javafx.event.EventHandler;
+import javafx.geometry.NodeOrientation;
+import javafx.scene.Cursor;
+import javafx.scene.control.ContextMenu;
+import javafx.scene.control.Label;
+import javafx.scene.control.MenuItem;
+import javafx.scene.control.ScrollBar;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableView.TableViewSelectionModel;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.StackPane;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Rectangle;
+import javafx.stage.WindowEvent;
+import org.controlsfx.control.spreadsheet.Picker;
+import org.controlsfx.control.spreadsheet.SpreadsheetCell;
+import org.controlsfx.control.spreadsheet.SpreadsheetView;
+
+/**
+ * Display the vertical header on the left of the cells (view), the index of the
+ * lines displayed on screen.
+ */
+public class VerticalHeader extends StackPane {
+
+    public static final int PICKER_SIZE = 16;
+    private static final int DRAG_RECT_HEIGHT = 5;
+    private static final String TABLE_ROW_KEY = "TableRow"; //$NON-NLS-1$
+    private static final String PICKER_INDEX = "PickerIndex"; //$NON-NLS-1$
+    private static final String TABLE_LABEL_KEY = "Label"; //$NON-NLS-1$
+    private static final Image pinImage = new Image(SpreadsheetView.class.getResource("pinSpreadsheetView.png").toExternalForm()); //$NON-NLS-1$
+
+    /**
+     * *************************************************************************
+     * * Private Fields * *
+     * ************************************************************************
+     */
+    private final SpreadsheetHandle handle;
+    private final SpreadsheetView spreadsheetView;
+    private double horizontalHeaderHeight;
+    /**
+     * This represents the VerticalHeader width. It's the total amount of space
+     * used by the VerticalHeader. It's composed of the sum of the
+     * SpreadsheetView {@link SpreadsheetView#getRowHeaderWidth() } and the size
+     * of the pickers (which is fixed right now).
+     *
+     */
+    private final DoubleProperty innerVerticalHeaderWidth = new SimpleDoubleProperty();
+    private Rectangle clip; // Ensure that children do not go out of bounds
+    private ContextMenu blankContextMenu;
+
+    // used for column resizing
+    private double lastY = 0.0F;
+    private static double dragAnchorY = 0.0;
+
+    // drag rectangle overlays
+    private final List<Rectangle> dragRects = new ArrayList<>();
+
+    private final List<Label> labelList = new ArrayList<>();
+    private GridViewSkin skin;
+    private boolean resizing = false;
+
+    private final Stack<Label> pickerPile;
+    private final Stack<Label> pickerUsed;
+    
+    /**
+     * This BitSet keeps track of the selected rows (when clicked on their
+     * header) in order to allow multi-resize.
+     */
+    private final BitSet selectedRows = new BitSet();
+
+    /**
+     * ****************************************************************
+     * CONSTRUCTOR
+     *
+     * @param handle
+     * ***************************************************************
+     */
+    public VerticalHeader(final SpreadsheetHandle handle) {
+        this.handle = handle;
+        this.spreadsheetView = handle.getView();
+        pickerPile = new Stack<>();
+        pickerUsed = new Stack<>();
+    }
+
+    /**
+     * *************************************************************************
+     * * Private/Protected Methods *
+     * ***********************************************************************
+     */
+    /**
+     * Init
+     *
+     * @param skin
+     * @param horizontalHeader
+     */
+    void init(final GridViewSkin skin, HorizontalHeader horizontalHeader) {
+        this.skin = skin;
+        // Adjust position upon HorizontalHeader height
+        horizontalHeader.heightProperty().addListener(new ChangeListener<Number>() {
+            @Override
+            public void changed(ObservableValue<? extends Number> arg0, Number oldHeight, Number newHeight) {
+                horizontalHeaderHeight = newHeight.doubleValue();
+                requestLayout();
+            }
+        });
+
+        // When the Grid is changing, we need to update our information.
+        handle.getView().gridProperty().addListener(layout);
+
+        // Clip property to stay within bounds
+        clip = new Rectangle(getVerticalHeaderWidth(), snapSize(skin.getSkinnable().getHeight()));
+        clip.relocate(snappedTopInset(), snappedLeftInset());
+        clip.setSmooth(false);
+        clip.heightProperty().bind(skin.getSkinnable().heightProperty());
+        clip.widthProperty().bind(innerVerticalHeaderWidth);
+        VerticalHeader.this.setClip(clip);
+
+        // We desactivate and activate the verticalHeader upon request
+        spreadsheetView.showRowHeaderProperty().addListener(layout);
+
+        // When the Column header is showing or not, we need to update the
+        // position of the verticalHeader
+        spreadsheetView.showColumnHeaderProperty().addListener(layout);
+        spreadsheetView.getFixedRows().addListener(layout);
+        spreadsheetView.fixingRowsAllowedProperty().addListener(layout);
+        spreadsheetView.rowHeaderWidthProperty().addListener(layout);
+
+        // In case we resize the view in any manners
+        spreadsheetView.heightProperty().addListener(layout);
+
+        //When rowPickers is changing
+        spreadsheetView.getRowPickers().addListener(layout);
+
+        // For layout properly the verticalHeader when there are some selected
+        // items
+        skin.getSelectedRows().addListener(layout);
+
+        blankContextMenu = new ContextMenu();
+    }
+
+    public double getVerticalHeaderWidth() {
+        return innerVerticalHeaderWidth.get();
+    }
+    
+    public ReadOnlyDoubleProperty verticalHeaderWidthProperty(){
+        return innerVerticalHeaderWidth;
+    }
+
+    public double computeHeaderWidth() {
+        double width = 0;
+        if (!spreadsheetView.getRowPickers().isEmpty()) {
+            width += PICKER_SIZE;
+        }
+        if (spreadsheetView.isShowRowHeader()) {
+            width += spreadsheetView.getRowHeaderWidth();
+        }
+        return width;
+    }
+
+    void clearSelectedRows(){
+        selectedRows.clear();
+    }
+    
+    @Override
+    protected void layoutChildren() {
+        if (resizing) {
+            return;
+        }
+        if ((spreadsheetView.isShowRowHeader() || !spreadsheetView.getRowPickers().isEmpty()) && skin.getCellsSize() > 0) {
+
+            double x = snappedLeftInset();
+            /**
+             * Pickers
+             */
+            pickerPile.addAll(pickerUsed.subList(0, pickerUsed.size()));
+            pickerUsed.clear();
+            if (!spreadsheetView.getRowPickers().isEmpty()) {
+                innerVerticalHeaderWidth.setValue(PICKER_SIZE);
+                x += PICKER_SIZE;
+            } else {
+                innerVerticalHeaderWidth.setValue(0);
+            }
+            if (spreadsheetView.isShowRowHeader()) {
+                innerVerticalHeaderWidth.setValue(getVerticalHeaderWidth() + spreadsheetView.getRowHeaderWidth());
+            }
+
+            getChildren().clear();
+
+            final int cellSize = skin.getCellsSize();
+
+            int rowCount = 0;
+            Label label;
+
+            rowCount = addVisibleRows(rowCount, x, cellSize);
+
+//            if (spreadsheetView.isShowRowHeader()) {
+                rowCount = addFixedRows(rowCount, x, cellSize);
+//            }
+            // First one blank and on top (z-order) of the others
+            if (spreadsheetView.showColumnHeaderProperty().get()) {
+                label = getLabel(rowCount++, null);
+                label.setOnMousePressed((MouseEvent event) -> {
+                    spreadsheetView.getSelectionModel().selectAll();
+                });
+                label.setText(""); //$NON-NLS-1$
+                label.resize(spreadsheetView.getRowHeaderWidth(), horizontalHeaderHeight);
+                label.layoutYProperty().unbind();
+                label.setLayoutY(0);
+                label.setLayoutX(x);
+                label.getStyleClass().clear();
+                label.setContextMenu(blankContextMenu);
+                getChildren().add(label);
+            }
+
+            ScrollBar hbar = handle.getCellsViewSkin().getHBar();
+            //FIXME handle height.
+            if (hbar.isVisible()) {
+                // Last one blank and on top (z-order) of the others
+                label = getLabel(rowCount++, null);
+                label.getProperties().put(TABLE_ROW_KEY, null);
+                label.setText(""); //$NON-NLS-1$
+                label.resize(getVerticalHeaderWidth(), hbar.getHeight());
+                label.layoutYProperty().unbind();
+                label.relocate(snappedLeftInset(), getHeight() - hbar.getHeight());
+                label.getStyleClass().clear();
+                label.setContextMenu(blankContextMenu);
+                getChildren().add(label);
+            }
+        } else {
+            getChildren().clear();
+        }
+    }
+
+    private int addFixedRows(int rowCount, double x, int cellSize) {
+        double spaceUsedByFixedRows = 0;
+        int rowIndex;
+        Label label;
+        final Set<Integer> currentlyFixedRow = handle.getCellsViewSkin().getCurrentlyFixedRow();
+        // Then we iterate over the FixedRows if any
+        if (!spreadsheetView.getFixedRows().isEmpty() && cellSize != 0) {
+            for (int j = 0; j < spreadsheetView.getFixedRows().size(); ++j) {
+
+                rowIndex = spreadsheetView.getFixedRows().get(j);
+                if (!currentlyFixedRow.contains(rowIndex)) {
+                    break;
+                }
+                double rowHeight = skin.getRowHeight(rowIndex);
+                double y = spreadsheetView.showColumnHeaderProperty().get() ? snappedTopInset() + horizontalHeaderHeight + spaceUsedByFixedRows
+                        : snappedTopInset() + spaceUsedByFixedRows;
+                
+                if (spreadsheetView.getRowPickers().containsKey(rowIndex)) {
+                    Label picker = getPicker(spreadsheetView.getRowPickers().get(rowIndex));
+                    picker.resize(PICKER_SIZE, rowHeight);
+                    picker.layoutYProperty().unbind();
+                    picker.setLayoutY(y);
+                    getChildren().add(picker);
+                }
+                if (spreadsheetView.isShowRowHeader()) {
+                    label = getLabel(rowCount++, rowIndex);
+                    GridRow row = skin.getRowIndexed(rowIndex);
+                    label.getProperties().put(TABLE_ROW_KEY, row);
+                    label.setText(getRowHeader(rowIndex));
+                    label.resize(spreadsheetView.getRowHeaderWidth(), rowHeight);
+                    label.setContextMenu(getRowContextMenu(rowIndex));
+                    if(row != null){
+                        label.layoutYProperty().bind(row.layoutYProperty().add(horizontalHeaderHeight).add(row.verticalShift));
+                    }
+                    label.setLayoutX(x);
+                    final ObservableList<String> css = label.getStyleClass();
+                    if (skin.getSelectedRows().contains(rowIndex)) {
+                        css.addAll("selected"); //$NON-NLS-1$
+                    } else {
+                        css.removeAll("selected"); //$NON-NLS-1$
+                    }
+                    css.addAll("fixed"); //$NON-NLS-1$
+                    getChildren().add(label);
+                    // position drag overlay to intercept row resize requests if authorized by the grid.
+                    if (spreadsheetView.getGrid().isRowResizable(rowIndex)) {
+                        Rectangle dragRect = getDragRect(rowCount++);
+                        dragRect.getProperties().put(TABLE_ROW_KEY, row);
+                        dragRect.getProperties().put(TABLE_LABEL_KEY, label);
+                        dragRect.setWidth(label.getWidth());
+                        dragRect.relocate(snappedLeftInset() + x, y + rowHeight - DRAG_RECT_HEIGHT);
+                        getChildren().add(dragRect);
+                    }
+                }
+                spaceUsedByFixedRows += skin.getRowHeight(rowIndex);
+
+                
+            }
+        }
+        return rowCount;
+    }
+
+    private int addVisibleRows(int rowCount, double x, int cellSize) {
+        int rowIndex;
+        // We add horizontalHeaderHeight because we need to
+        // take the other header into account.
+        double y = snappedTopInset();
+
+        if (spreadsheetView.showColumnHeaderProperty().get()) {
+            y += horizontalHeaderHeight;
+        }
+
+        // The Labels must be aligned with the rows
+        if (cellSize != 0) {
+            y += skin.getRow(0).getLocalToParentTransform().getTy();
+        }
+
+        Label label;
+        // We don't want to add Label if there are no rows associated with.
+        final int modelRowCount = spreadsheetView.getGrid().getRowCount();
+
+        int i = 0;
+
+        GridRow row = skin.getRow(i);
+
+        double fixedRowHeight = skin.getFixedRowHeight();
+        double rowHeaderWidth = spreadsheetView.getRowHeaderWidth();
+        double height;
+
+        // We iterate over the visibleRows
+        while (cellSize != 0 && row != null && row.getIndex() < modelRowCount) {
+            rowIndex = row.getIndex();
+            height = row.getHeight();
+            /**
+             * Picker
+             */
+            if (row.getLayoutY() >= fixedRowHeight && spreadsheetView.getRowPickers().containsKey(rowIndex)) {
+                Label picker = getPicker(spreadsheetView.getRowPickers().get(rowIndex));
+                picker.resize(PICKER_SIZE, height);
+                picker.layoutYProperty().bind(row.layoutYProperty().add(horizontalHeaderHeight));
+                getChildren().add(picker);
+            }
+
+            if (spreadsheetView.isShowRowHeader()) {
+                label = getLabel(rowCount++, rowIndex);
+                label.getProperties().put(TABLE_ROW_KEY, row);
+                label.setText(getRowHeader(rowIndex));
+                label.resize(rowHeaderWidth, height);
+                label.setLayoutX(x);
+                label.layoutYProperty().bind(row.layoutYProperty().add(horizontalHeaderHeight));
+                label.setContextMenu(getRowContextMenu(rowIndex));
+
+                getChildren().add(label);
+                // We want to highlight selected rows
+                final ObservableList<String> css = label.getStyleClass();
+                if (skin.getSelectedRows().contains(rowIndex)) {
+                    css.addAll("selected"); //$NON-NLS-1$
+                } else {
+                    css.removeAll("selected"); //$NON-NLS-1$
+                }
+                if (spreadsheetView.getFixedRows().contains(rowIndex)) {
+                    css.addAll("fixed"); //$NON-NLS-1$
+                } else {
+                    css.removeAll("fixed"); //$NON-NLS-1$
+                }
+
+                y += height;
+
+                // position drag overlay to intercept row resize requests if authorized by the grid.
+                if (spreadsheetView.getGrid().isRowResizable(rowIndex)) {
+                    Rectangle dragRect = getDragRect(rowCount++);
+                    dragRect.getProperties().put(TABLE_ROW_KEY, row);
+                    dragRect.getProperties().put(TABLE_LABEL_KEY, label);
+                    dragRect.setWidth(label.getWidth());
+                    dragRect.relocate(snappedLeftInset() + x, y - DRAG_RECT_HEIGHT);
+                    getChildren().add(dragRect);
+                }
+            }
+            row = skin.getRow(++i);
+        }
+        return rowCount;
+    }
+
+    private final EventHandler<MouseEvent> rectMousePressed = new EventHandler<MouseEvent>() {
+        @Override
+        public void handle(MouseEvent me) {
+
+            if (me.getClickCount() == 2 && me.isPrimaryButtonDown()) {
+                Rectangle rect = (Rectangle) me.getSource();
+                GridRow row = (GridRow) rect.getProperties().get(TABLE_ROW_KEY);
+                skin.resizeRowToFitContent(row.getIndex());
+                requestLayout();
+            } else {
+                // rather than refer to the rect variable, we just grab
+                // it from the source to prevent a small memory leak.
+                dragAnchorY = me.getSceneY();
+                resizing = true;
+            }
+            me.consume();
+        }
+    };
+
+    private final EventHandler<MouseEvent> rectMouseDragged = new EventHandler<MouseEvent>() {
+        @Override
+        public void handle(MouseEvent me) {
+            Rectangle rect = (Rectangle) me.getSource();
+            GridRow row = (GridRow) rect.getProperties().get(TABLE_ROW_KEY);
+            Label label = (Label) rect.getProperties().get(TABLE_LABEL_KEY);
+            if (row != null) {
+                rowResizing(row, label, me);
+            }
+            me.consume();
+        }
+    };
+
+    private void rowResizing(GridRow gridRow, Label label, MouseEvent me) {
+        double draggedY = me.getSceneY() - dragAnchorY;
+        if (gridRow.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT) {
+            draggedY = -draggedY;
+        }
+
+        double delta = draggedY - lastY;
+
+        Double newHeight = gridRow.getHeight() + delta;
+        if (newHeight < 0) {
+            return;
+        }
+        handle.getCellsViewSkin().rowHeightMap.put(gridRow.getIndex(), newHeight);
+        Event.fireEvent(spreadsheetView, new SpreadsheetView.RowHeightEvent(gridRow.getIndex(), newHeight));
+        label.resize(spreadsheetView.getRowHeaderWidth(), newHeight);
+        gridRow.setPrefHeight(newHeight);
+        gridRow.requestLayout();
+        
+        lastY = draggedY;
+    }
+
+    private final EventHandler<MouseEvent> rectMouseReleased = new EventHandler<MouseEvent>() {
+        @Override
+        public void handle(MouseEvent me) {
+            lastY = 0.0F;
+            resizing = false;
+            requestLayout();
+            me.consume();
+            //We resize the other selected rows if the resized one is selected.
+            Rectangle rect = (Rectangle) me.getSource();
+            GridRow row = (GridRow) rect.getProperties().get(TABLE_ROW_KEY);
+            if (selectedRows.get(row.getIndex())) {
+                double height = row.getHeight();
+                for (int i = selectedRows.nextSetBit(0); i >= 0; i = selectedRows.nextSetBit(i + 1)) {
+                    skin.rowHeightMap.put(i, height);
+                    Event.fireEvent(spreadsheetView, new SpreadsheetView.RowHeightEvent(i, height));
+                }
+            }
+        }
+    };
+
+    /**
+     * Create a new label and put it in the pile or just grab one from the pile.
+     *
+     * @param rowNumber
+     * @return
+     */
+    private Label getLabel(int rowNumber, Integer row) {
+        Label label;
+        if (labelList.isEmpty() || labelList.size() <= rowNumber) {
+            label = new Label();
+            labelList.add(label);
+        } else {
+            label = labelList.get(rowNumber);
+        }
+        // We want to select the whole row when clicking on a header.
+        label.setOnMousePressed(row == null ? null : (MouseEvent event) -> {
+            if (event.isPrimaryButtonDown()) {
+                if (event.getClickCount() == 2) {
+                    skin.resizeRowToFitContent(row);
+                    requestLayout();
+                } else {
+                    headerClicked(row, event);
+                }
+            }
+        });
+        return label;
+    }
+
+    /**
+     * If a header is clicked, we must select the whole row. If Control key of
+     * Shift key is pressed, we must not deselect the previous selection but
+     * just act like the {@link GridViewBehavior} would.
+     *
+     * @param row
+     * @param event
+     */
+    private void headerClicked(int row, MouseEvent event) {
+        TableViewSelectionModel<ObservableList<SpreadsheetCell>> sm = handle.getGridView().getSelectionModel();
+        int focusedRow = sm.getFocusedIndex();
+        int rowCount = handle.getView().getGrid().getRowCount();
+        ObservableList<TableColumn<ObservableList<SpreadsheetCell>, ?>> columns = sm.getTableView().getColumns();
+        TableColumn<ObservableList<SpreadsheetCell>, ?> firstColumn = columns.get(0);
+        TableColumn<ObservableList<SpreadsheetCell>, ?> lastColumn = columns.get(columns.size() - 1);
+
+        if (event.isShortcutDown()) {
+            BitSet tempSet = (BitSet) selectedRows.clone();
+            sm.selectRange(row, firstColumn, row, lastColumn);
+            selectedRows.or(tempSet);
+            selectedRows.set(row);
+        } else if (event.isShiftDown() && focusedRow >= 0 && focusedRow < rowCount) {
+            sm.clearSelection();
+            sm.selectRange(focusedRow, firstColumn, row, lastColumn);
+            //We want to let the focus on the focused row.
+            sm.getTableView().getFocusModel().focus(focusedRow, firstColumn);
+            int min = Math.min(row, focusedRow);
+            int max = Math.max(row, focusedRow);
+            selectedRows.set(min, max + 1);
+        } else {
+            sm.clearSelection();
+            sm.selectRange(row, firstColumn, row, lastColumn);
+            //And we want to have the focus on the first cell in order to be able to copy/paste between rows.
+            sm.getTableView().getFocusModel().focus(row, firstColumn);
+            selectedRows.set(row);
+        }
+    }
+    
+    private Label getPicker(Picker picker) {
+        Label pickerLabel;
+        if (pickerPile.isEmpty()) {
+            pickerLabel = new Label();
+            picker.getStyleClass().addListener(layout);
+            pickerLabel.setOnMouseClicked(pickerMouseEvent);
+        } else {
+            pickerLabel = pickerPile.pop();
+        }
+        pickerUsed.push(pickerLabel);
+
+        pickerLabel.getStyleClass().setAll(picker.getStyleClass());
+        pickerLabel.getProperties().put(PICKER_INDEX, picker);
+        return pickerLabel;
+    }
+
+    private final EventHandler<MouseEvent> pickerMouseEvent = new EventHandler<MouseEvent>() {
+
+        @Override
+        public void handle(MouseEvent mouseEvent) {
+            Label picker = (Label) mouseEvent.getSource();
+
+            ((Picker) picker.getProperties().get(PICKER_INDEX)).onClick();
+        }
+    };
+
+    /**
+     * Create a new Rectangle and put it in the pile or just grab one from the
+     * pile.
+     *
+     * @param rowNumber
+     * @return
+     */
+    private Rectangle getDragRect(int rowNumber) {
+        if (dragRects.isEmpty() || dragRects.size() <= rowNumber) {
+            final Rectangle rect = new Rectangle();
+            rect.setWidth(getVerticalHeaderWidth());
+            rect.setHeight(DRAG_RECT_HEIGHT);
+            rect.setFill(Color.TRANSPARENT);
+            rect.setSmooth(false);
+            rect.setOnMousePressed(rectMousePressed);
+            rect.setOnMouseDragged(rectMouseDragged);
+            rect.setOnMouseReleased(rectMouseReleased);
+            rect.setCursor(Cursor.V_RESIZE);
+            dragRects.add(rect);
+            return rect;
+        } else {
+            return dragRects.get(rowNumber);
+        }
+    }
+
+    /**
+     * Return a contextMenu for fixing a row if possible.
+     *
+     * @param row
+     * @return
+     */
+    private ContextMenu getRowContextMenu(final Integer row) {
+        if (spreadsheetView.isRowFixable(row)) {
+            final ContextMenu contextMenu = new ContextMenu();
+
+            MenuItem fixItem = new MenuItem(localize(asKey("spreadsheet.verticalheader.menu.fix"))); //$NON-NLS-1$
+            contextMenu.setOnShowing(new EventHandler<WindowEvent>() {
+
+                @Override
+                public void handle(WindowEvent event) {
+                    if (spreadsheetView.getFixedRows().contains(row)) {
+                        fixItem.setText(localize(asKey("spreadsheet.verticalheader.menu.unfix"))); //$NON-NLS-1$
+                    } else {
+                        fixItem.setText(localize(asKey("spreadsheet.verticalheader.menu.fix"))); //$NON-NLS-1$
+                    }
+                }
+            });
+            fixItem.setGraphic(new ImageView(pinImage));
+
+            fixItem.setOnAction(new EventHandler<ActionEvent>() {
+                @Override
+                public void handle(ActionEvent arg0) {
+                    if (spreadsheetView.getFixedRows().contains(row)) {
+                        spreadsheetView.getFixedRows().remove(row);
+                    } else {
+                        spreadsheetView.getFixedRows().add(row);
+                    }
+                }
+            });
+            contextMenu.getItems().add(fixItem);
+
+            return contextMenu;
+        } else {
+            return blankContextMenu;
+        }
+    }
+
+    /**
+     * Return the String header associated with this row index.
+     *
+     * @param index
+     * @return
+     */
+    private String getRowHeader(int index) {
+        return spreadsheetView.getGrid().getRowHeaders().size() > index ? spreadsheetView
+                .getGrid().getRowHeaders().get(index) : String.valueOf(index + 1);
+    }
+
+    /**
+     * *************************************************************************
+     * * Listeners * *
+     * ************************************************************************
+     */
+    private final InvalidationListener layout = (Observable arg0) -> {
+        requestLayout();
+    };
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/table/ColumnFilter.java b/controlsfx/src/main/java/impl/org/controlsfx/table/ColumnFilter.java
new file mode 100644
index 0000000..50d79c6
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/table/ColumnFilter.java
@@ -0,0 +1,308 @@
+/**
+ * Copyright (c) 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.table;
+
+import javafx.beans.Observable;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.beans.value.WeakChangeListener;
+import javafx.collections.FXCollections;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import javafx.collections.WeakListChangeListener;
+import javafx.scene.control.ContextMenu;
+import javafx.scene.control.CustomMenuItem;
+import javafx.scene.control.TableColumn;
+import org.controlsfx.control.table.TableFilter;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.BiPredicate;
+
+public final class ColumnFilter<T,R> {
+    private final TableFilter<T> tableFilter;
+    private final TableColumn<T,R> tableColumn;
+
+    private final ObservableList<FilterValue<T,R>> filterValues;
+
+    private final DupeCounter<R> filterValuesDupeCounter = new DupeCounter<>(false);
+    private final DupeCounter<R> visibleValuesDupeCounter = new DupeCounter<>(false);
+    private final HashSet<R> unselectedValues = new HashSet<>();
+    private final HashMap<CellIdentity<T>,ChangeListener<R>> trackedCells = new HashMap<>();
+    
+    private boolean lastFilter = false;
+    private boolean isDirty = false;
+    private BiPredicate<String,String> searchStrategy = (inputString, subjectString) -> subjectString.contains(inputString);
+    private volatile FilterPanel filterPanel;
+
+    private boolean initialized = false;
+
+    private final ListChangeListener<T> backingListListener = lc -> {
+        while (lc.next()) {
+            if (lc.wasAdded()) {
+                lc.getAddedSubList().stream()
+                        .forEach(t -> addBackingItem(t, getTableColumn().getCellObservableValue(t)));
+            }
+            if (lc.wasRemoved()) {
+                lc.getRemoved().stream()
+                        .forEach(t -> removeBackingItem(t, getTableColumn().getCellObservableValue(t)));
+            }
+        }
+    };
+
+    private final ListChangeListener<T> itemsListener = lc -> {
+        while (lc.next()) {
+            if (lc.wasAdded()) {
+                lc.getAddedSubList().stream()
+                        .map(getTableColumn()::getCellObservableValue)
+                        .forEach(this::addVisibleItem);
+            }
+            if (lc.wasRemoved()) {
+                lc.getRemoved().stream()
+                        .map(getTableColumn()::getCellObservableValue)
+                        .forEach(this::removeVisibleItem);
+            }
+        }
+    };
+
+    private final ChangeListener<R> changeListener = (observable, oldValue, newValue) -> {
+        if (filterValuesDupeCounter.add(newValue) == 1) {
+            getFilterValues().add(new FilterValue<>(newValue,this));
+        }
+        removeValue(oldValue);
+    };
+
+    private final ListChangeListener<FilterValue<T, R>> filterValueListChangeListener = lc -> {
+        while (lc.next()) {
+            if (lc.wasRemoved()) {
+                lc.getRemoved().stream()
+                        .filter(v -> !v.selectedProperty().get())
+                        .forEach(unselectedValues::remove);
+            }
+            if (lc.wasUpdated()) {
+                int from = lc.getFrom();
+                int to = lc.getTo();
+                lc.getList().subList(from, to).forEach(v -> {
+                    isDirty = true;
+
+                    boolean value = v.selectedProperty().getValue();
+                    if (!value) {
+                        unselectedValues.add(v.getValue());
+                    } else {
+                        unselectedValues.remove(v.getValue());
+                    }
+                });
+            }
+        }
+    };
+
+    public ColumnFilter(TableFilter<T> tableFilter, TableColumn<T,R> tableColumn) {
+        this.tableFilter = tableFilter;
+        this.tableColumn = tableColumn;
+
+        this.filterValues = FXCollections.observableArrayList(cb -> new Observable[] { cb.selectedProperty()});
+        this.attachContextMenu();
+    }
+    void setFilterPanel(FilterPanel filterPanel) {
+        this.filterPanel = filterPanel;
+    }
+    FilterPanel getFilterPanel() {
+        return filterPanel;
+    }
+    public void initialize() {
+        if (!initialized) {
+            initializeListeners();
+            initializeValues();
+            initialized = true;
+        }
+    }
+    public boolean isInitialized() {
+        return initialized;
+    }
+
+    public void selectValue(Object value) {
+        filterPanel.selectValue(value);
+    }
+    public void unselectValue(Object value) {
+        filterPanel.unSelectValue(value);
+    }
+    public void selectAllValues() {
+        filterPanel.selectAllValues();
+    }
+    public void unSelectAllValues() {
+        filterPanel.unSelectAllValues();
+    }
+    public boolean wasLastFiltered() {
+        return lastFilter;
+    }
+    public boolean hasUnselections() {
+        return unselectedValues.size() != 0;
+    }
+    public void setSearchStrategy(BiPredicate<String,String> searchStrategy) {
+        this.searchStrategy = searchStrategy;
+    }
+    public BiPredicate<String,String> getSearchStrategy() {
+        return searchStrategy;
+    }
+    public boolean isFiltered() {
+        return isDirty || unselectedValues.size() > 0;
+    }
+    public boolean valueIsVisible(R value) {
+        return visibleValuesDupeCounter.get(value) > 0;
+    }
+    public void applyFilter() {
+    	tableFilter.executeFilter();
+    	lastFilter = true;
+    	tableFilter.getColumnFilters().stream().filter(c -> !c.equals(this)).forEach(c -> c.lastFilter = false);
+    	tableFilter.getColumnFilters().stream().flatMap(c -> c.filterValues.stream()).forEach(FilterValue::refreshScope);
+        isDirty = false;
+    }
+
+    public void resetAllFilters() { 
+    	tableFilter.getColumnFilters().stream().flatMap(c -> c.filterValues.stream()).forEach(fv -> fv.selectedProperty().set(true));
+    	tableFilter.resetFilter();
+    	tableFilter.getColumnFilters().stream().forEach(c -> c.lastFilter = false);
+    	tableFilter.getColumnFilters().stream().flatMap(c -> c.filterValues.stream()).forEach(FilterValue::refreshScope);
+        isDirty = false;
+    }
+
+    public ObservableList<FilterValue<T,R>> getFilterValues() {
+        return filterValues;
+    }
+
+    public TableColumn<T,R> getTableColumn() {
+        return tableColumn;
+    }
+    public TableFilter<T> getTableFilter() { 
+        return tableFilter;
+    }
+    public boolean evaluate(T item) {
+        ObservableValue<R> value = tableColumn.getCellObservableValue(item);
+
+        return unselectedValues.size() == 0
+                || !unselectedValues.contains(value.getValue());
+    }
+
+    private void initializeValues() {
+        tableFilter.getBackingList().stream()
+                .forEach(t -> addBackingItem(t, tableColumn.getCellObservableValue(t)));
+        tableFilter.getTableView().getItems().stream()
+                .map(tableColumn::getCellObservableValue).forEach(this::addVisibleItem);
+
+    }
+
+    private void addBackingItem(T item, ObservableValue<R> cellValue) {
+        if (cellValue == null) {
+            return;
+        }
+        if (filterValuesDupeCounter.add(cellValue.getValue()) == 1) {
+            filterValues.add(new FilterValue<>(cellValue.getValue(),this));
+        }
+
+        //listen to cell value and track it
+        CellIdentity<T> trackedCellValue = new CellIdentity<>(item);
+
+		ChangeListener<R> cellListener = new WeakChangeListener(changeListener);
+        cellValue.addListener(cellListener);
+        trackedCells.put(trackedCellValue,cellListener);
+    }
+    private void removeBackingItem(T item, ObservableValue<R> cellValue) {
+        if (cellValue == null) {
+            return;
+        }
+        removeValue(cellValue.getValue());
+
+        //remove listener from cell
+        ChangeListener<R> listener = trackedCells.get(new CellIdentity<>(item));
+        cellValue.removeListener(listener);
+        trackedCells.remove(new CellIdentity<>(item));
+    }
+    private void removeValue(R value) {
+        boolean removedLastDuplicate = filterValuesDupeCounter.remove(value) == 0;
+        if (removedLastDuplicate) {
+            // Remove the FilterValue associated with the value
+            FilterValue<T,R> existingFilterValue = getFilterValues().stream()
+                    .filter(fv -> Objects.equals(fv.getValue(), value)).findAny().get();
+            getFilterValues().remove(existingFilterValue);
+        }
+    }
+    private void addVisibleItem(ObservableValue<R>  cellValue) {
+        if (cellValue != null) {
+            visibleValuesDupeCounter.add(cellValue.getValue());
+        }
+    }
+    private void removeVisibleItem(ObservableValue<R>  cellValue) {
+        if (cellValue != null) {
+            visibleValuesDupeCounter.remove(cellValue.getValue());
+        }
+    }
+    private void initializeListeners() {
+        //listen to backing list and update distinct values accordingly
+        tableFilter.getBackingList().addListener(new WeakListChangeListener<T>(backingListListener));
+
+        //listen to visible items and update visible values accordingly
+        tableFilter.getTableView().getItems().addListener(new WeakListChangeListener<T>(itemsListener));
+
+        //listen to selections on filterValues
+        filterValues.addListener(new WeakListChangeListener<>(filterValueListChangeListener));
+    }
+
+    /**Leverages tableColumn's context menu to attach filter panel */
+    private void attachContextMenu() {
+
+        ContextMenu contextMenu = new ContextMenu();
+
+        CustomMenuItem item = FilterPanel.getInMenuItem(this, contextMenu);
+
+        contextMenu.getStyleClass().add("column-filter");
+        contextMenu.getItems().add(item);
+
+        tableColumn.setContextMenu(contextMenu);
+
+        contextMenu.setOnShowing(ae -> initialize());
+    }
+
+    private static final class CellIdentity<T> {
+        private final T item;
+
+        CellIdentity(T item) {
+            this.item = item;
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            return this.item == ((CellIdentity<?>)other).item;
+        }
+
+        @Override
+        public int hashCode() {
+            return System.identityHashCode(item);
+        }
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/table/DupeCounter.java b/controlsfx/src/main/java/impl/org/controlsfx/table/DupeCounter.java
new file mode 100644
index 0000000..851a6ce
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/table/DupeCounter.java
@@ -0,0 +1,52 @@
+package impl.org.controlsfx.table;
+
+import java.util.HashMap;
+import java.util.Optional;
+
+final class DupeCounter<T> {
+
+    private final HashMap<T,Integer> counts = new HashMap<>();
+    private final boolean enforceFloor;
+
+    public DupeCounter(boolean enforceFloor) {
+        this.enforceFloor = enforceFloor;
+    }
+    public int add(T value) {
+        Integer prev = counts.get(value);
+        int newVal;
+        if (prev == null) {
+            newVal = 1;
+            counts.put(value, newVal);
+        }  else {
+            newVal = prev + 1;
+            counts.put(value, newVal);
+        }
+        return newVal;
+    }
+    public int get(T value) {
+        return Optional.ofNullable(counts.get(value)).orElse(0);
+    }
+    public int remove(T value) {
+        Integer prev = counts.get(value);
+        if (prev != null && prev > 0) {
+            int newVal = prev - 1;
+            if (newVal == 0) {
+                counts.remove(value);
+            } else {
+                counts.put(value, newVal);
+            }
+            return newVal;
+        }
+        else if (enforceFloor) {
+            throw new IllegalStateException();
+        }
+        else {
+            return 0;
+        }
+    }
+
+    @Override
+    public String toString() {
+        return counts.toString();
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/table/FilterPanel.java b/controlsfx/src/main/java/impl/org/controlsfx/table/FilterPanel.java
new file mode 100644
index 0000000..3f417ba
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/table/FilterPanel.java
@@ -0,0 +1,275 @@
+/**
+ * Copyright (c) 2015, 2016, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.table;
+
+import com.sun.javafx.scene.control.skin.NestedTableColumnHeader;
+import com.sun.javafx.scene.control.skin.TableColumnHeader;
+import com.sun.javafx.scene.control.skin.TableViewSkin;
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.beans.WeakInvalidationListener;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.WeakChangeListener;
+import javafx.collections.transformation.FilteredList;
+import javafx.collections.transformation.SortedList;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.geometry.Side;
+import javafx.scene.control.*;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.VBox;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Optional;
+import java.util.function.Supplier;
+
+
+public final class FilterPanel<T,R> extends VBox {
+
+    private final ColumnFilter<T,R> columnFilter;
+
+    private final FilteredList<FilterValue> filterList;
+    private static final String promptText = "Search...";
+    private final TextField searchBox = new TextField();
+    private boolean searchMode = false;
+    private boolean bumpedWidth = false;
+
+    private final ListView<FilterValue> checkListView;
+	
+    // This collection will reference column header listeners. References must be kept locally because weak listeners are registered
+    private final Collection<InvalidationListener> columnHeadersChangeListeners = new ArrayList();
+
+    private static final Image filterIcon = new Image("/impl/org/controlsfx/table/filter.png");
+
+    private static final Supplier<ImageView> filterImageView = () -> {
+        ImageView imageView = new ImageView(filterIcon);
+        imageView.setFitHeight(15);
+        imageView.setPreserveRatio(true);
+        return imageView;
+    };
+
+    private final ChangeListener<Skin<?>> skinListener = (w, o, n) -> {
+        // Clear references to listeners, this will (eventually) cause the WeakListeners to expire
+        columnHeadersChangeListeners.clear();
+
+        if (n instanceof TableViewSkin) {
+            TableViewSkin<?> skin = (TableViewSkin<?>) n;
+            checkChangeContextMenu(skin, getColumnFilter().getTableColumn(), this);
+        }
+    };
+
+    void selectAllValues() {
+        checkListView.getItems().stream()
+                .forEach(item -> item.selectedProperty().set(true));
+    }
+    void unSelectAllValues() {
+        checkListView.getItems().stream()
+                .forEach(item -> item.selectedProperty().set(false));
+    }
+    void selectValue(Object value) {
+        checkListView.getItems().stream().filter(item -> item.getValue().equals(value))
+                .forEach(item -> item.selectedProperty().set(true));
+    }
+    void unSelectValue(Object value) {
+        checkListView.getItems().stream().filter(item -> item.getValue() == value)
+                .forEach(item -> item.selectedProperty().set(false));
+    }
+
+    FilterPanel(ColumnFilter<T,R> columnFilter, ContextMenu contextMenu) {
+        columnFilter.setFilterPanel(this);
+        this.columnFilter = columnFilter;
+        getStyleClass().add("filter-panel");
+
+        //initialize search box
+        setPadding(new Insets(3));
+
+        searchBox.setPromptText(promptText);
+        getChildren().add(searchBox);
+
+        //initialize checklist view
+
+        filterList = new FilteredList<>(new SortedList<>(columnFilter.getFilterValues()), t -> true);
+        checkListView = new ListView<>();
+        checkListView.setItems(new SortedList<>(filterList, FilterValue::compareTo));
+
+        getChildren().add(checkListView);
+
+        //initialize apply button
+        HBox buttonBox = new HBox();
+
+        Button applyBttn = new Button("APPLY");
+        HBox.setHgrow(applyBttn, Priority.ALWAYS);
+
+        applyBttn.setOnAction(e -> {
+                    if (searchMode) {
+                        filterList.forEach(v -> v.selectedProperty().setValue(true));
+
+                        columnFilter.getFilterValues().stream()
+                                .filter(v -> !filterList.stream().filter(fl -> fl.equals(v)).findAny().isPresent())
+                                .forEach(v -> v.selectedProperty().setValue(false));
+
+                        resetSearchFilter();
+                    }
+                    if (columnFilter.getTableFilter().isDirty()) {
+                        columnFilter.applyFilter();
+                        columnFilter.getTableFilter().getColumnFilters().stream().map(ColumnFilter::getFilterPanel)
+                                .forEach(fp -> {
+                                    if (!fp.columnFilter.hasUnselections()) {
+                                        fp.columnFilter.getTableColumn().setGraphic(null);
+                                    } else {
+                                        fp.columnFilter.getTableColumn().setGraphic(filterImageView.get());
+                                        if (!bumpedWidth) {
+                                            fp.columnFilter.getTableColumn().setPrefWidth(columnFilter.getTableColumn().getWidth() + 20);
+                                            bumpedWidth = true;
+                                        }
+                                    }
+                                });
+                    }
+                contextMenu.hide();
+                });
+
+        buttonBox.getChildren().add(applyBttn);
+
+        //initialize unselect all button
+        Button unselectAllButton = new Button("NONE");
+        HBox.setHgrow(unselectAllButton, Priority.ALWAYS);
+
+        unselectAllButton.setOnAction(e -> columnFilter.getFilterValues().forEach(v -> v.selectedProperty().set(false)));
+        buttonBox.getChildren().add(unselectAllButton);
+
+        //initialize reset buttons
+        Button selectAllButton = new Button("ALL");
+        HBox.setHgrow(selectAllButton, Priority.ALWAYS);
+
+        selectAllButton.setOnAction(e -> {
+            columnFilter.getFilterValues().forEach(v -> v.selectedProperty().set(true));
+        });
+
+        buttonBox.getChildren().add(selectAllButton);
+
+        Button clearAllButton = new Button("RESET ALL");
+        HBox.setHgrow(clearAllButton, Priority.ALWAYS);
+
+        clearAllButton.setOnAction(e -> {
+            columnFilter.resetAllFilters();
+            columnFilter.getTableFilter().getColumnFilters().stream().forEach(cf -> cf.getTableColumn().setGraphic(null));
+            contextMenu.hide();
+        });
+        buttonBox.getChildren().add(clearAllButton);
+
+        buttonBox.setAlignment(Pos.BASELINE_CENTER);
+
+
+        getChildren().add(buttonBox);
+    }
+
+    public void resetSearchFilter() {
+        this.filterList.setPredicate(t -> true);
+        searchBox.clear();
+    }
+    public static <T,R> CustomMenuItem getInMenuItem(ColumnFilter<T,R> columnFilter, ContextMenu contextMenu) {
+
+        FilterPanel<T,R> filterPanel = new FilterPanel<>(columnFilter, contextMenu);
+
+        CustomMenuItem menuItem = new CustomMenuItem();
+
+        filterPanel.initializeListeners();
+
+        menuItem.contentProperty().set(filterPanel);
+
+        columnFilter.getTableFilter().getTableView().skinProperty().addListener(new WeakChangeListener<>(filterPanel.skinListener));
+
+        menuItem.setHideOnClick(false);
+        return menuItem;
+    }
+    private void initializeListeners() {
+        searchBox.textProperty().addListener(l -> {
+            searchMode = !searchBox.getText().isEmpty();
+            filterList.setPredicate(val -> searchBox.getText().isEmpty() ||
+                    columnFilter.getSearchStrategy().test(searchBox.getText(), Optional.ofNullable(val.getValue()).map(Object::toString).orElse("")));
+        });
+    }
+
+    /* Methods below helps will anchor the context menu under the column */
+    private static void checkChangeContextMenu(TableViewSkin<?> skin, TableColumn<?, ?> column, FilterPanel filterPanel) {
+        NestedTableColumnHeader header = skin.getTableHeaderRow().getRootHeader();
+        InvalidationListener listener = filterPanel.getOrCreateChangeListener(header, column);
+        header.getColumnHeaders().addListener(new WeakInvalidationListener(listener));
+        changeContextMenu(header, column);
+    }
+
+    private InvalidationListener getOrCreateChangeListener(NestedTableColumnHeader header, TableColumn<?, ?> column) {
+        InvalidationListener listener = (Observable obs) -> changeContextMenu(header, column);
+
+        // Keep a reference locally because this listener will be used with a WeakInvalidationListener
+        columnHeadersChangeListeners.add(listener);
+
+        return listener;
+    }
+
+    private static void changeContextMenu(NestedTableColumnHeader header, TableColumn<?, ?> column) {
+        TableColumnHeader headerSkin = scan(column, header);
+        if (headerSkin != null) {
+            headerSkin.setOnContextMenuRequested(ev -> {
+                ContextMenu cMenu = column.getContextMenu();
+                if (cMenu != null) {
+                    cMenu.show(headerSkin, Side.BOTTOM, 5, 5);
+                }
+                ev.consume();
+            });
+        }
+    }
+
+    private static TableColumnHeader scan(TableColumn<?, ?> search,
+                                          TableColumnHeader header) {
+        // firstly test that the parent isn't what we are looking for
+        if (search.equals(header.getTableColumn())) {
+            return header;
+        }
+
+        if (header instanceof NestedTableColumnHeader) {
+            NestedTableColumnHeader parent = (NestedTableColumnHeader) header;
+            for (int i = 0; i < parent.getColumnHeaders().size(); i++) {
+                TableColumnHeader result = scan(search, parent
+                        .getColumnHeaders().get(i));
+                if (result != null) {
+                    return result;
+                }
+            }
+        }
+
+        return null;
+    }
+
+    public ColumnFilter<T,R> getColumnFilter() {
+        return columnFilter;
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/table/FilterValue.java b/controlsfx/src/main/java/impl/org/controlsfx/table/FilterValue.java
new file mode 100644
index 0000000..fc00c05
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/table/FilterValue.java
@@ -0,0 +1,68 @@
+package impl.org.controlsfx.table;
+
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.beans.WeakInvalidationListener;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.Label;
+import javafx.scene.layout.HBox;
+import javafx.scene.paint.Color;
+
+import java.util.Optional;
+
+final class FilterValue<T,R> extends HBox implements Comparable<FilterValue<T,R>> {
+
+    private final R value;
+    private final BooleanProperty isSelected = new SimpleBooleanProperty(true);
+    private final BooleanProperty inScope = new SimpleBooleanProperty(true);
+    private final ColumnFilter<T,R> columnFilter;
+    private final InvalidationListener scopeListener;
+
+
+    FilterValue(R value, ColumnFilter<T,R> columnFilter) {
+        this.value = value;
+        this.columnFilter = columnFilter;
+
+        final CheckBox checkBox = new CheckBox();
+        final Label label = new Label();
+        label.setText(Optional.ofNullable(value).map(Object::toString).orElse(null));
+        scopeListener = (Observable v) -> label.textFillProperty().set(getInScopeProperty().get() ? Color.BLACK : Color.LIGHTGRAY);
+        inScope.addListener(new WeakInvalidationListener(scopeListener));
+        checkBox.selectedProperty().bindBidirectional(selectedProperty());
+        getChildren().addAll(checkBox,label);
+    }
+
+    public R getValue() {
+        return value;
+    }
+
+    public BooleanProperty selectedProperty() {
+        return isSelected;
+    }
+    public BooleanProperty getInScopeProperty() {
+        return inScope;
+    }
+
+    public void refreshScope() {
+        inScope.setValue(columnFilter.wasLastFiltered() || columnFilter.valueIsVisible(value));
+    }
+
+    @Override
+    public String toString() {
+        return Optional.ofNullable(value).map(Object::toString).orElse("");
+    }
+
+
+    @Override
+    public int compareTo(FilterValue<T,R> other) {
+        if (value != null && other.value != null) {
+            if (value instanceof Comparable<?> && other.value instanceof Comparable<?>) {
+                return ((Comparable<Object>) value).compareTo(((Comparable<Object>) other.value));
+            }
+        }
+        return Optional.ofNullable(value).map(Object::toString).orElse("")
+                .compareTo(Optional.ofNullable(other).map(Object::toString).orElse(""));
+    }
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/tools/MathTools.java b/controlsfx/src/main/java/impl/org/controlsfx/tools/MathTools.java
new file mode 100644
index 0000000..6284bf6
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/tools/MathTools.java
@@ -0,0 +1,96 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.tools;
+
+import java.util.Objects;
+
+/**
+ * Contains methods {@link Math} might also contain but doesn't.
+ */
+public class MathTools {
+
+    /**
+     * Checks whether the specified value lies in the closed interval defined by the specified bounds.
+     * 
+     * @param lowerBound
+     *            the interval's lower bound; included in the interval
+     * @param value
+     *            the value which will be checked
+     * @param upperBound
+     *            the interval's upper bound; included in the interval
+     * @return {@code true} if {@code lowerBound} <= {@code value} <= {@code upperBound} <br>
+     *         {@code false} otherwise
+     */
+    public static boolean isInInterval(double lowerBound, double value, double upperBound) {
+        return lowerBound <= value && value <= upperBound;
+    }
+
+    /**
+     * Checks whether the specified value lies in the closed interval defined by the specified bounds. If it does, it is
+     * returned; otherwise the bound closer to the value will be returned.
+     * 
+     * @param lowerBound
+     *            the interval's lower bound; included in the interval
+     * @param value
+     *            the value which will be checked
+     * @param upperBound
+     *            the interval's upper bound; included in the interval
+     * @return {@code value} if {@code lowerBound} <= {@code value} <= {@code upperBound} <br>
+     *         {@code lowerBound} if {@code value} < {@code lowerBound} <br>
+     *         {@code upperBound} if {@code upperBound} < {@code value}
+     */
+    public static double inInterval(double lowerBound, double value, double upperBound) {
+        if (value < lowerBound)
+            return lowerBound;
+        if (upperBound < value)
+            return upperBound;
+        return value;
+    }
+
+    /**
+     * Returns the smallest value in the specified array according to {@link Math#min(double, double)}.
+     * 
+     * @param values
+     *            a non-null, non-empty array of double values
+     * @return a value from the array which is smaller then or equal to any other value from the array
+     * @throws NullPointerException
+     *             if the values array is {@code null}
+     * @throws IllegalArgumentException
+     *             if the values array is empty (i.e. has {@code length} 0)
+     */
+    public static double min(double... values) {
+        Objects.requireNonNull(values, "The specified value array must not be null."); //$NON-NLS-1$
+        if (values.length == 0)
+            throw new IllegalArgumentException("The specified value array must contain at least one element."); //$NON-NLS-1$
+
+        double min = Double.MAX_VALUE;
+        for (double value : values)
+            min = Math.min(value, min);
+        return min;
+    }
+
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/tools/PrefixSelectionCustomizer.java b/controlsfx/src/main/java/impl/org/controlsfx/tools/PrefixSelectionCustomizer.java
new file mode 100644
index 0000000..c03367c
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/tools/PrefixSelectionCustomizer.java
@@ -0,0 +1,221 @@
+/**
+ * Copyright (c) 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.tools;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.controlsfx.control.PrefixSelectionChoiceBox;
+import org.controlsfx.control.PrefixSelectionComboBox;
+
+import javafx.collections.ObservableList;
+import javafx.event.EventHandler;
+import javafx.scene.control.ChoiceBox;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.Control;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyEvent;
+import javafx.util.StringConverter;
+
+/**
+ * <p>This utility class can be used to customize a {@link ChoiceBox} or
+ * {@link ComboBox} and enable the "prefix selection" feature. This will enable
+ * the user to type letters or digits on the keyboard while the {@link ChoiceBox}
+ * or {@link ComboBox} has the focus. The {@link ChoiceBox} or {@link ComboBox} 
+ * will attempt to select the first item it can find with a matching prefix ignoring 
+ * case.
+ * 
+ * <p>This feature is available natively on the Windows combo box control, so many
+ * users have asked for it. There is a feature request to include this feature
+ * into JavaFX (<a href="https://javafx-jira.kenai.com/browse/RT-18064">Issue RT-18064</a>). 
+ * The class is published as part of ContorlsFX to allow testing and feedback.
+ * 
+ * <h3>Example</h3>
+ *  
+ * <p>Let's look at an example to clarify this. The combo box offers the items 
+ * ["Aaaaa", "Abbbb", "Abccc", "Abcdd", "Abcde"]. The user now types "abc" in 
+ * quick succession (and then stops typing). The combo box will select a new entry 
+ * on every key pressed. The first entry it will select is "Aaaaa" since it is the 
+ * first entry that starts with an "a" (case ignored). It will then select "Abbbb", 
+ * since this is the first entry that started with "ab" and will finally settle for 
+ * "Abccc".
+ * 
+ * <ul><table>
+ *   <tr><th>Keys typed</th><th>Element selected</th></tr>
+ *   <tr><td>a</td><td>Aaaaa<td></tr>
+ *   <tr><td>aaa</td><td>Aaaaa<td></tr>
+ *   <tr><td>ab</td><td>Abbbb<td></tr>
+ *   <tr><td>abc</td><td>Abccc<td></tr>
+ *   <tr><td>xyz</td><td>-<td></tr>
+ * </table></ul>
+ * 
+ * <h3>Usage</h3>
+ * 
+ * <p>A common use case is to customize a {@link ChoiceBox} or {@link ComboBox}
+ * that has been loaded as part of an FXML. In this case you can use the utility
+ * methods {@link #customize(ChoiceBox)} or {@link #customize(ComboBox)}. This
+ * will install a {@link EventHandler} that monitors the {@link KeyEvent}
+ * events.
+ * 
+ * <p>If you are coding, you can also use the preconfigured classes
+ * {@link PrefixSelectionChoiceBox} and {@link PrefixSelectionComboBox} as a
+ * substitute for {@link ChoiceBox} and {@link ComboBox}.
+ * 
+ * @see PrefixSelectionChoiceBox
+ * @see PrefixSelectionComboBox
+ */
+public class PrefixSelectionCustomizer {
+    private static final String SELECTION_PREFIX_STRING = "selectionPrefixString";
+    private static final Object SELECTION_PREFIX_TASK = "selectionPrefixTask";
+
+    private static EventHandler<KeyEvent> handler = new EventHandler<KeyEvent>() {
+        private ScheduledExecutorService executorService = null;
+
+        @Override
+        public void handle(KeyEvent event) {
+            keyPressed(event);
+        }
+
+        private <T> void keyPressed(KeyEvent event) {
+            KeyCode code = event.getCode();
+            if (code.isLetterKey() || code.isDigitKey() || code == KeyCode.SPACE) {
+                String letter = code.impl_getChar();
+                if (event.getSource() instanceof ComboBox) {
+                    ComboBox<T> comboBox = (ComboBox<T>) event.getSource();
+                    T item = getEntryWithKey(letter, comboBox.getConverter(), comboBox.getItems(), comboBox);
+                    if (item != null) {
+                        comboBox.setValue(item);
+                    }
+                } else if (event.getSource() instanceof ChoiceBox) {
+                    ChoiceBox<T> choiceBox = (ChoiceBox<T>) event.getSource();
+                    T item = getEntryWithKey(letter, choiceBox.getConverter(), choiceBox.getItems(), choiceBox);
+                    if (item != null) {
+                        choiceBox.setValue(item);
+                    }
+                }
+            }
+        }
+
+        private <T> T getEntryWithKey(String letter, StringConverter<T> converter, ObservableList<T> items, Control control) {
+            T result = null;
+
+            // The converter is null by default for the ChoiceBox. The ComboBox has a default converter
+            if (converter == null) {
+                converter = new StringConverter<T>() {
+                    @Override
+                    public String toString(T t) {
+                        return t == null ? null : t.toString();
+                    }
+
+                    @Override
+                    public T fromString(String string) {
+                        return null;
+                    }
+                };
+            }
+
+            String selectionPrefixString = (String) control.getProperties().get(SELECTION_PREFIX_STRING);
+            if (selectionPrefixString == null) {
+                selectionPrefixString = letter.toUpperCase();
+            } else {
+                selectionPrefixString += letter.toUpperCase();
+            }
+            control.getProperties().put(SELECTION_PREFIX_STRING, selectionPrefixString);
+
+            for (T item : items) {
+                String string = converter.toString(item);
+                if (string != null && string.toUpperCase().startsWith(selectionPrefixString)) {
+                    result = item;
+                    break;
+                }
+            }
+
+            ScheduledFuture<?> task = (ScheduledFuture<?>) control.getProperties().get(SELECTION_PREFIX_TASK);
+            if (task != null) {
+                task.cancel(false);
+            }
+            task = getExecutorService().schedule(
+                    () -> control.getProperties().put(SELECTION_PREFIX_STRING, ""), 500, TimeUnit.MILLISECONDS); 
+            control.getProperties().put(SELECTION_PREFIX_TASK, task);
+
+            return result;
+        }
+
+        private ScheduledExecutorService getExecutorService() {
+            if (executorService == null) {
+                executorService = Executors.newScheduledThreadPool(1,
+                        runnabble -> {
+                            Thread result = new Thread(runnabble);
+                            result.setDaemon(true);
+                            return result;
+                        });
+            }
+            return executorService;
+        }
+
+    };
+
+    /**
+     * This will install an {@link EventHandler} that monitors the
+     * {@link KeyEvent} events to enable the "prefix selection" feature.
+     * The {@link EventHandler} will only be installed if the {@link ComboBox}
+     * is <b>not</b> editable.
+     * 
+     * @param comboBox
+     *            The {@link ComboBox} that should be customized
+     * 
+     * @see PrefixSelectionCustomizer
+     */
+    public static void customize(ComboBox<?> comboBox) {
+        if (!comboBox.isEditable()) {
+            comboBox.addEventHandler(KeyEvent.KEY_PRESSED, handler);
+        }
+        comboBox.editableProperty().addListener((o, oV, nV) -> {
+            if (!nV) {
+                comboBox.addEventHandler(KeyEvent.KEY_PRESSED, handler);
+            } else {
+                comboBox.removeEventHandler(KeyEvent.KEY_PRESSED, handler);
+            }
+        });
+    }
+
+    /**
+     * This will install an {@link EventHandler} that monitors the
+     * {@link KeyEvent} events to enable the "prefix selection" feature.
+     * 
+     * @param choiceBox
+     *            The {@link ChoiceBox} that should be customized
+     * 
+     * @see PrefixSelectionCustomizer
+     */
+    public static void customize(ChoiceBox<?> choiceBox) {
+        choiceBox.addEventHandler(KeyEvent.KEY_PRESSED, handler);
+    }
+
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/CoordinatePosition.java b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/CoordinatePosition.java
new file mode 100644
index 0000000..e015701
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/CoordinatePosition.java
@@ -0,0 +1,84 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.tools.rectangle;
+
+/**
+ * Enumerates all possible positions coordinates can have relative to a rectangle.
+ */
+public enum CoordinatePosition {
+
+    /**
+     * The coordinates are inside the rectangle.
+     */
+    IN_RECTANGLE,
+
+    /**
+     * The coordinates are outside of the rectangle.
+     */
+    OUT_OF_RECTANGLE,
+
+    /**
+     * The coordinates are close to the northern edge of the rectangle.
+     */
+    NORTH_EDGE,
+
+    /**
+     * The coordinates are close to the northern and the eastern edge of the rectangle.
+     */
+    NORTHEAST_EDGE,
+
+    /**
+     * The coordinates are close to the eastern edge of the rectangle.
+     */
+    EAST_EDGE,
+
+    /**
+     * The coordinates are close to the southern and eastern edge of the rectangle.
+     */
+    SOUTHEAST_EDGE,
+
+    /**
+     * The coordinates are close to the southern edge of the rectangle.
+     */
+    SOUTH_EDGE,
+
+    /**
+     * The coordinates are close to the southern and western edge of the rectangle.
+     */
+    SOUTHWEST_EDGE,
+
+    /**
+     * The coordinates are close to the western edge of the rectangle.
+     */
+    WEST_EDGE,
+
+    /**
+     * The coordinates are close to the northern and the western edge of the rectangle.
+     */
+    NORTHWEST_EDGE,
+
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/CoordinatePositions.java b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/CoordinatePositions.java
new file mode 100644
index 0000000..13ec009
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/CoordinatePositions.java
@@ -0,0 +1,222 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.tools.rectangle;
+
+import java.util.EnumSet;
+
+import javafx.geometry.Point2D;
+import javafx.geometry.Rectangle2D;
+
+/**
+ * Computes coordinate positions relative to a rectangle.
+ */
+public class CoordinatePositions {
+
+    /**
+     * Returns all positions the specified point has regarding the specified rectangle and its edges (using the
+     * specified tolerance).
+     * 
+     * @param rectangle
+     *            the rectangle relative to which the point will be checked
+     * @param point
+     *            the checked point
+     * @param edgeTolerance
+     *            the tolerance in pixels used to determine whether the coordinates are on some edge
+     * @return a set of those positions the coordinates have regarding the specified rectangle
+     */
+    public static EnumSet<CoordinatePosition> onRectangleAndEdges(
+            Rectangle2D rectangle, Point2D point, double edgeTolerance) {
+
+        EnumSet<CoordinatePosition> positions = EnumSet.noneOf(CoordinatePosition.class);
+        positions.add(inRectangle(rectangle, point));
+        positions.add(onEdges(rectangle, point, edgeTolerance));
+        return positions;
+    }
+
+    /*
+     * RECTANGLE
+     */
+
+    /**
+     * Returns the position the specified coordinates have regarding the specified rectangle. Edges are not checked.
+     * 
+     * @param rectangle
+     *            the rectangle relative to which the point will be checked
+     * @param point
+     *            the checked point
+     * @return depending on the point either {@link CoordinatePosition#IN_RECTANGLE IN_RECTANGLE} or
+     *         {@link CoordinatePosition#OUT_OF_RECTANGLE OUT_OF_RECTANGLE}
+     */
+    public static CoordinatePosition inRectangle(Rectangle2D rectangle, Point2D point) {
+        if (rectangle.contains(point)) {
+            return CoordinatePosition.IN_RECTANGLE;
+        } else {
+            return CoordinatePosition.OUT_OF_RECTANGLE;
+        }
+    }
+
+    /*
+     * EDGES
+     */
+
+    /**
+     * Returns the position the specified coordinates have regarding the specified rectangle's edges using the specified
+     * tolerance.
+     * 
+     * @param rectangle
+     *            the rectangle relative to which the point will be checked
+     * @param point
+     *            the checked point
+     * @param edgeTolerance
+     *            the tolerance in pixels used to determine whether the coordinates are on some edge
+     * @return the edge position the coordinates have regarding the specified rectangle; the value will be null if the
+     *         point is not near any edge
+     */
+    public static CoordinatePosition onEdges(Rectangle2D rectangle, Point2D point,
+            double edgeTolerance) {
+
+        CoordinatePosition vertical = closeToVertical(rectangle, point, edgeTolerance);
+        CoordinatePosition horizontal = closeToHorizontal(rectangle, point, edgeTolerance);
+
+        return extractSingleCardinalDirection(vertical, horizontal);
+    }
+
+    /**
+     * Returns the vertical bound the specified coordinates are closest to, if the distance is smaller than the edge
+     * tolerance. Otherwise, null is returned.
+     * 
+     * @param rectangle
+     *            the rectangle relative to which the point will be checked
+     * @param point
+     *            the checked point
+     * @param edgeTolerance
+     *            the tolerance in pixels used to determine whether the coordinates are on some edge
+     * @return EAST_EDGE, WEST_EDGE or null
+     */
+    private static CoordinatePosition closeToVertical(Rectangle2D rectangle, Point2D point, double edgeTolerance) {
+
+        double xDistanceToLeft = Math.abs(point.getX() - rectangle.getMinX());
+        double xDistanceToRight = Math.abs(point.getX() - rectangle.getMaxX());
+        boolean xCloseToLeft = xDistanceToLeft < edgeTolerance && xDistanceToLeft < xDistanceToRight;
+        boolean xCloseToRight = xDistanceToRight < edgeTolerance && xDistanceToRight < xDistanceToLeft;
+
+        if (!xCloseToLeft && !xCloseToRight) {
+            return null;
+        }
+
+        boolean yCloseToVertical = rectangle.getMinY() - edgeTolerance < point.getY()
+                && point.getY() < rectangle.getMaxY() + edgeTolerance;
+        if (yCloseToVertical) {
+            if (xCloseToLeft) {
+                return CoordinatePosition.WEST_EDGE;
+            }
+            if (xCloseToRight) {
+                return CoordinatePosition.EAST_EDGE;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Returns the horizontal bound the specified coordinates are closest to, if the distance is smaller than the edge
+     * tolerance. Otherwise, null is returned.
+     * 
+     * @param rectangle
+     *            the rectangle relative to which the point will be checked
+     * @param point
+     *            the checked point
+     * @param edgeTolerance
+     *            the tolerance in pixels used to determine whether the coordinates are on some edge
+     * @return NORTH_EDGE, SOUTH_EDGE or null
+     */
+    private static CoordinatePosition closeToHorizontal(Rectangle2D rectangle, Point2D point, double edgeTolerance) {
+
+        double yDistanceToUpper = Math.abs(point.getY() - rectangle.getMinY());
+        double yDistanceToLower = Math.abs(point.getY() - rectangle.getMaxY());
+        boolean yCloseToUpper = yDistanceToUpper < edgeTolerance && yDistanceToUpper < yDistanceToLower;
+        boolean yCloseToLower = yDistanceToLower < edgeTolerance && yDistanceToLower < yDistanceToUpper;
+
+        if (!yCloseToUpper && !yCloseToLower) {
+            return null;
+        }
+
+        boolean xCloseToHorizontal = rectangle.getMinX() - edgeTolerance < point.getX()
+                && point.getX() < rectangle.getMaxX() + edgeTolerance;
+        if (xCloseToHorizontal) {
+            if (yCloseToUpper) {
+                return CoordinatePosition.NORTH_EDGE;
+            }
+            if (yCloseToLower) {
+                return CoordinatePosition.SOUTH_EDGE;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Extracts a single cardinal direction from the two specified positions. The conditions stated below are not
+     * checked!
+     * 
+     * @param vertical
+     *            a vertical edge (EAST or WEST) or null
+     * @param horizontal
+     *            a horizontal edge (NORTH OR SOUTH) or null
+     * @return the single coordinate position which matches the specified positions <br>
+     *         (e.g. NORTH for (null, NORTH) and SOUTHWEST for (WEST, SOUTH))
+     */
+    private static CoordinatePosition extractSingleCardinalDirection(CoordinatePosition vertical,
+            CoordinatePosition horizontal) {
+        if (vertical == null) {
+            return horizontal;
+        }
+
+        if (horizontal == null) {
+            return vertical;
+        }
+
+        // north
+        if (horizontal == CoordinatePosition.NORTH_EDGE && vertical == CoordinatePosition.EAST_EDGE) {
+            return CoordinatePosition.NORTHEAST_EDGE;
+        }
+        if (horizontal == CoordinatePosition.NORTH_EDGE && vertical == CoordinatePosition.WEST_EDGE) {
+            return CoordinatePosition.NORTHWEST_EDGE;
+        }
+
+        // south
+        if (horizontal == CoordinatePosition.SOUTH_EDGE && vertical == CoordinatePosition.EAST_EDGE) {
+            return CoordinatePosition.SOUTHEAST_EDGE;
+        }
+        if (horizontal == CoordinatePosition.SOUTH_EDGE && vertical == CoordinatePosition.WEST_EDGE) {
+            return CoordinatePosition.SOUTHWEST_EDGE;
+        }
+
+        throw new IllegalArgumentException();
+    }
+
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/Edge2D.java b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/Edge2D.java
new file mode 100644
index 0000000..e9f17e6
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/Edge2D.java
@@ -0,0 +1,249 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.tools.rectangle;
+
+import java.util.Objects;
+
+import javafx.geometry.Orientation;
+import javafx.geometry.Point2D;
+
+/**
+ * The edge of a rectangle, i.e. a vertical or horizontal line segment.
+ */
+public class Edge2D {
+
+    /*
+     * ATTRIBUTES
+     */
+
+    /**
+     * The edge's center point.
+     */
+    private final Point2D centerPoint;
+
+    /**
+     * The edge's orientation.
+     */
+    private final Orientation orientation;
+
+    /**
+     * The edge's length.
+     */
+    private final double length;
+
+    /*
+     * ATTRIBUTES
+     */
+
+    /**
+     * Creates a new edge which is specified by its center point, orientation and length.
+     * 
+     * @param centerPoint
+     *            the edge's center point
+     * @param orientation
+     *            the edge's orientation
+     * @param length
+     *            the edge's length; must be non-negative.
+     */
+    public Edge2D(Point2D centerPoint, Orientation orientation, double length) {
+        Objects.requireNonNull(centerPoint, "The specified center point must not be null."); //$NON-NLS-1$
+        Objects.requireNonNull(orientation, "The specified orientation must not be null."); //$NON-NLS-1$
+        if (length < 0)
+            throw new IllegalArgumentException(
+                    "The length must not be negative, i.e. zero or a positive value is alowed."); //$NON-NLS-1$
+
+        this.centerPoint = centerPoint;
+        this.orientation = orientation;
+        this.length = length;
+    }
+
+    /*
+     * CORNERS AND DISTANCES
+     */
+
+    /**
+     * Returns the edge's upper left end point. It has ({@link #getLength() length} / 2) distance from the center point
+     * and depending on the edge's orientation either the same X (for {@link Orientation#HORIZONTAL}) or Y (for
+     * {@link Orientation#VERTICAL}) coordinate.
+     * 
+     * @return the edge's upper left point
+     */
+    public Point2D getUpperLeft() {
+        if (isHorizontal()) {
+            // horizontal
+            double cornersX = centerPoint.getX() - (length / 2);
+            double edgesY = centerPoint.getY();
+            return new Point2D(cornersX, edgesY);
+        } else {
+            // vertical
+            double edgesX = centerPoint.getX();
+            double cornersY = centerPoint.getY() - (length / 2);
+            return new Point2D(edgesX, cornersY);
+        }
+    }
+
+    /**
+     * Returns the edge's lower right end point. It has ({@link #getLength() length} / 2) distance from the center point
+     * and depending on the edge's orientation either the same X (for {@link Orientation#HORIZONTAL}) or Y (for
+     * {@link Orientation#VERTICAL}) coordinate.
+     * 
+     * @return the edge's lower right point
+     */
+    public Point2D getLowerRight() {
+        if (isHorizontal()) {
+            // horizontal
+            double cornersX = centerPoint.getX() + (length / 2);
+            double edgesY = centerPoint.getY();
+            return new Point2D(cornersX, edgesY);
+        } else {
+            // vertical
+            double edgesX = centerPoint.getX();
+            double cornersY = centerPoint.getY() + (length / 2);
+            return new Point2D(edgesX, cornersY);
+        }
+    }
+
+    /**
+     * Returns the distance of the specified point to the edge in terms of the dimension orthogonal to the edge's
+     * orientation. The sign denotes whether on which side of the edge, the point lies.<br>
+     * So e.g. if the edge is horizontal, only the Y coordinate's difference between the specified point and the edge is
+     * considered. If the point lies to the right of the edge, the returned value is positive.
+     * 
+     * @param otherPoint
+     *            the point to where the distance is computed
+     * @return the distance
+     */
+    public double getOrthogonalDifference(Point2D otherPoint) {
+        Objects.requireNonNull(otherPoint, "The other point must nt be null."); //$NON-NLS-1$
+
+        if (isHorizontal())
+            // horizontal -> subtract y coordinates
+            return otherPoint.getY() - centerPoint.getY();
+        else
+            // vertical-> subtract x coordinates
+            return otherPoint.getX() - centerPoint.getX();
+    }
+
+    /*
+     * ATTRIBUTE ACCESS
+     */
+
+    /**
+     * @return the edge's center point
+     */
+    public Point2D getCenterPoint() {
+        return centerPoint;
+    }
+
+    /**
+     * Returns this edge's orientation. Note that the orientation can also be checked with {@link #isHorizontal()} and
+     * {@link #isVertical()}.
+     * 
+     * @return the edge's orientation
+     */
+    public Orientation getOrientation() {
+        return orientation;
+    }
+
+    /**
+     * Indicates whether this is a {@link Orientation#HORIZONTAL horizontal} edge.
+     * 
+     * @return true if {@link #getOrientation()} returns {@link Orientation#HORIZONTAL}
+     */
+    public boolean isHorizontal() {
+        return orientation == Orientation.HORIZONTAL;
+    }
+
+    /**
+     * Indicates whether this is a {@link Orientation#VERTICAL horizontal} edge.
+     * 
+     * @return true if {@link #getOrientation()} returns {@link Orientation#VERTICAL}
+     */
+    public boolean isVertical() {
+        return orientation == Orientation.VERTICAL;
+    }
+
+    /**
+     * @return the edge's length
+     */
+    public double getLength() {
+        return length;
+    }
+
+    /*
+     * EQUALS, HASHCODE & TOSTRING
+     */
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((centerPoint == null) ? 0 : centerPoint.hashCode());
+        long temp;
+        temp = Double.doubleToLongBits(length);
+        result = prime * result + (int) (temp ^ (temp >>> 32));
+        result = prime * result + ((orientation == null) ? 0 : orientation.hashCode());
+        return result;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        Edge2D other = (Edge2D) obj;
+        if (centerPoint == null) {
+            if (other.centerPoint != null)
+                return false;
+        } else if (!centerPoint.equals(other.centerPoint))
+            return false;
+        if (Double.doubleToLongBits(length) != Double.doubleToLongBits(other.length))
+            return false;
+        if (orientation != other.orientation)
+            return false;
+        return true;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String toString() {
+        return "Edge2D [centerX = " + centerPoint.getX() + ", centerY = " + centerPoint.getY() //$NON-NLS-1$ //$NON-NLS-2$
+                + ", orientation = " + orientation + ", length = " + length + "]"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+    }
+
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/Rectangles2D.java b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/Rectangles2D.java
new file mode 100644
index 0000000..80f5d90
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/Rectangles2D.java
@@ -0,0 +1,796 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.tools.rectangle;
+
+import impl.org.controlsfx.tools.MathTools;
+
+import java.util.Objects;
+
+import javafx.geometry.Bounds;
+import javafx.geometry.Orientation;
+import javafx.geometry.Point2D;
+import javafx.geometry.Rectangle2D;
+
+/**
+ * Usability methods for rectangles.
+ */
+public class Rectangles2D {
+
+    /*
+     * CHECKS
+     */
+
+    /**
+     * Indicates whether the specified rectangle contains the specified edge.
+     * 
+     * @param rectangle
+     *            the rectangle to check
+     * @param edge
+     *            the edge to check
+     * @return {@code true} if both end points of the edge are {@link Rectangle2D#contains(Point2D) contained} in the
+     *         rectangle
+     */
+    public static boolean contains(Rectangle2D rectangle, Edge2D edge) {
+        Objects.requireNonNull(rectangle, "The argument 'rectangle' must not be null."); //$NON-NLS-1$
+        Objects.requireNonNull(edge, "The argument 'edge' must not be null."); //$NON-NLS-1$
+
+        boolean edgeInBounds = rectangle.contains(edge.getUpperLeft()) && rectangle.contains(edge.getLowerRight());
+        return edgeInBounds;
+    }
+
+    /*
+     * POINT
+     */
+
+    /**
+     * Moves the specified point into the specified rectangle. If the point is already with the rectangle, it is
+     * returned. Otherwise the point in the rectangle which is closest to the specified one is returned.
+     * 
+     * @param rectangle
+     *            the {@link Rectangle2D} into which the point should be moved
+     * @param point
+     *            the {@link Point2D} which is checked
+     * @return either the specified {@code point} or the {@link Point2D} which is closest to it while still being
+     *         contained on the {@code rectangle}
+     */
+    public static Point2D inRectangle(Rectangle2D rectangle, Point2D point) {
+        Objects.requireNonNull(rectangle, "The argument 'rectangle' must not be null."); //$NON-NLS-1$
+        Objects.requireNonNull(point, "The argument 'point' must not be null."); //$NON-NLS-1$
+
+        if (rectangle.contains(point)) {
+            return point;
+        }
+
+        // force the x and y coordinate into the rectangle
+        double newX = MathTools.inInterval(rectangle.getMinX(), point.getX(), rectangle.getMaxX());
+        double newY = MathTools.inInterval(rectangle.getMinY(), point.getY(), rectangle.getMaxY());
+        return new Point2D(newX, newY);
+    }
+
+    /**
+     * Returns the center of the specified rectangle as a point.
+     * 
+     * @param rectangle
+     *            the {@link Rectangle2D} whose center point will be returned
+     * @return the {@link Point2D} whose x/y coordinates lie at {@code (min + max) / 2}.
+     */
+    public static Point2D getCenterPoint(Rectangle2D rectangle) {
+        Objects.requireNonNull(rectangle, "The argument 'rectangle' must not be null."); //$NON-NLS-1$
+
+        double centerX = (rectangle.getMinX() + rectangle.getMaxX()) / 2;
+        double centerY = (rectangle.getMinY() + rectangle.getMaxY()) / 2;
+        return new Point2D(centerX, centerY);
+    }
+
+    /*
+     * OTHER RECTANGLE
+     */
+
+    /**
+     * Returns the rectangle which represents the intersection of the two specified rectangles.
+     * 
+     * @param a
+     *            a {@link Rectangle2D}
+     * @param b
+     *            another {@link Rectangle2D}
+     * @return a {@link Rectangle2D} which is the intersection of {@code a} and {@code b}; possible
+     *         {@link Rectangle2D#EMPTY}.
+     */
+    public static Rectangle2D intersection(Rectangle2D a, Rectangle2D b) {
+        Objects.requireNonNull(a, "The argument 'a' must not be null."); //$NON-NLS-1$
+        Objects.requireNonNull(b, "The argument 'b' must not be null."); //$NON-NLS-1$
+
+        if (a.intersects(b)) {
+            double intersectionMinX = Math.max(a.getMinX(), b.getMinX());
+            double intersectionMaxX = Math.min(a.getMaxX(), b.getMaxX());
+            double intersectionWidth = intersectionMaxX - intersectionMinX;
+            double intersectionMinY = Math.max(a.getMinY(), b.getMinY());
+            double intersectionMaxY = Math.min(a.getMaxY(), b.getMaxY());
+            double intersectionHeight = intersectionMaxY - intersectionMinY;
+            return new Rectangle2D(intersectionMinX, intersectionMinY, intersectionWidth, intersectionHeight);
+        } else {
+            return Rectangle2D.EMPTY;
+        }
+    }
+
+    /*
+     * TWO CORNERS
+     */
+
+    /**
+     * Creates a new rectangle with the two specified corners. The two corners will be interpreted as being diagonal of
+     * each other.
+     * 
+     * @param oneCorner
+     *            one corner
+     * @param diagonalCorner
+     *            another corner, diagonal from the first
+     * @return the {@link Rectangle2D} which is defined by the two corners
+     */
+    public static Rectangle2D forDiagonalCorners(Point2D oneCorner, Point2D diagonalCorner) {
+        Objects.requireNonNull(oneCorner, "The specified corner must not be null."); //$NON-NLS-1$
+        Objects.requireNonNull(diagonalCorner, "The specified diagonal corner must not be null."); //$NON-NLS-1$
+
+        double minX = Math.min(oneCorner.getX(), diagonalCorner.getX());
+        double minY = Math.min(oneCorner.getY(), diagonalCorner.getY());
+        double width = Math.abs(oneCorner.getX() - diagonalCorner.getX());
+        double height = Math.abs(oneCorner.getY() - diagonalCorner.getY());
+
+        return new Rectangle2D(minX, minY, width, height);
+    }
+
+    /*
+     * CORNER AND SIZE
+     */
+
+    /**
+     * Creates a new rectangle with the specified {@code upperLeft} corner and the specified {@code width} and
+     * {@code height}.
+     * 
+     * @param upperLeft
+     *            one corner
+     * @param width
+     *            the new rectangle's width
+     * @param height
+     *            the new rectangle's height
+     * @return the {@link Rectangle2D} which is defined by the specified upper left corner and width and height
+     */
+    public static Rectangle2D forUpperLeftCornerAndSize(Point2D upperLeft, double width, double height) {
+        return new Rectangle2D(upperLeft.getX(), upperLeft.getY(), width, height);
+    }
+
+    /*
+     * CORNER AND RATIO
+     */
+
+    /**
+     * Creates a new rectangle with the two specified corners. The two corners will be interpreted as being diagonal of
+     * each other. The returned rectangle will have the specified {@code fixedCorner} as its corner. The other one will
+     * either be on the same x- or y-parallel as the {@code diagonalCorner} but will be such that the rectangle has the
+     * specified {@code ratio}.
+     * 
+     * @param fixedCorner
+     *            one corner
+     * @param diagonalCorner
+     *            another corner, diagonal from the first
+     * @param ratio
+     *            the ratio the returned rectangle must have; must be non-negative
+     * @return the {@link Rectangle2D} which is defined by the {@code fixedCorner}, the x- or y-parallel of the
+     *         {@code diagonalCorner} and the {@code ratio}
+     */
+    public static Rectangle2D forDiagonalCornersAndRatio(Point2D fixedCorner, Point2D diagonalCorner, double ratio) {
+        Objects.requireNonNull(fixedCorner, "The specified fixed corner must not be null."); //$NON-NLS-1$
+        Objects.requireNonNull(diagonalCorner, "The specified diagonal corner must not be null."); //$NON-NLS-1$
+        if (ratio < 0) {
+            throw new IllegalArgumentException("The specified ratio " + ratio + " must be larger than zero."); //$NON-NLS-1$ //$NON-NLS-2$
+        }
+
+        // the coordinate differences - note that they can be negative
+        double xDifference = diagonalCorner.getX() - fixedCorner.getX();
+        double yDifference = diagonalCorner.getY() - fixedCorner.getY();
+
+        // the following calls will only change one of the two differences:
+        // the one whose value is too large compared to what it should be based on the other difference and the ratio;
+        // its value will instead be the other difference time or divided by the ratio
+        double xDifferenceByRatio = correctCoordinateDifferenceByRatio(xDifference, yDifference, ratio);
+        double yDifferenceByRatio = correctCoordinateDifferenceByRatio(yDifference, xDifference, 1 / ratio);
+
+        // these are the coordinates of the upper left corner of the future rectangle
+        double minX = getMinCoordinate(fixedCorner.getX(), xDifferenceByRatio);
+        double minY = getMinCoordinate(fixedCorner.getY(), yDifferenceByRatio);
+
+        double width = Math.abs(xDifferenceByRatio);
+        double height = Math.abs(yDifferenceByRatio);
+
+        return new Rectangle2D(minX, minY, width, height);
+    }
+
+    /**
+     * Returns the difference with the following properties:<br>
+     * - it has the same sign as the specified difference <br>
+     * - its absolute value is the minimum of the absolute values of ...<br>
+     * ... the specified difference and <br>
+     * ... the product of the specified ratio and the other specified difference <br>
+     * 
+     * @param difference
+     *            the difference to check
+     * @param otherDifference
+     *            the other difference
+     * @param ratioAsMultiplier
+     *            the ratio as a multiplier for the other difference
+     * @return the corrected difference
+     */
+    private static double correctCoordinateDifferenceByRatio(double difference, double otherDifference,
+            double ratioAsMultiplier) {
+        double differenceByRatio = otherDifference * ratioAsMultiplier;
+        double correctedDistance = Math.min(Math.abs(difference), Math.abs(differenceByRatio));
+
+        return correctedDistance * Math.signum(difference);
+    }
+
+    /**
+     * Returns the minimum coordinate such that a rectangle starting from that coordinate will contain the fixed
+     * coordinate as a corner.
+     * 
+     * @param fixedCoordinate
+     *            the coordinate which must be a corner
+     * @param difference
+     *            the difference in the computed coordinate
+     * @return fixedCoordinate + difference; if difference < 0 <br>
+     *         fixedCoordinate; else
+     */
+    private static double getMinCoordinate(double fixedCoordinate, double difference) {
+        if (difference < 0) {
+            return fixedCoordinate + difference;
+        }
+
+        return fixedCoordinate;
+    }
+
+    /*
+     * CENTER AND SIZE
+     */
+
+    /**
+     * Creates a new rectangle with the specified center and the specified width and height.
+     * 
+     * @param centerPoint
+     *            the center point o the new rectangle
+     * @param width
+     *            the width of the new rectangle
+     * @param height
+     *            the height of the new rectangle
+     * @return a rectangle with the specified center and size
+     */
+    public static Rectangle2D forCenterAndSize(Point2D centerPoint, double width, double height) {
+        Objects.requireNonNull(centerPoint, "The specified center point must not be null."); //$NON-NLS-1$
+
+        double absoluteWidth = Math.abs(width);
+        double absoluteHeight = Math.abs(height);
+        double minX = centerPoint.getX() - absoluteWidth / 2;
+        double minY = centerPoint.getY() - absoluteHeight / 2;
+
+        return new Rectangle2D(minX, minY, width, height);
+    }
+
+    /*
+     * ORIGINAL, AREA AND RATIO
+     */
+
+    /**
+     * Creates a new rectangle with the same center point and area as the specified {@code original} rectangle with the
+     * specified {@code ratio}.
+     * 
+     * @param original
+     *            the original rectangle
+     * @param ratio
+     *            the new ratio
+     * @return a new {@link Rectangle2D} with the same center point as the {@code original} and the specified
+     *         {@code ratio}; it has the same area as the {@code original}
+     * @throws NullPointerException
+     *             if the {@code original} rectangle is null
+     */
+    public static Rectangle2D fixRatio(Rectangle2D original, double ratio) {
+        Objects.requireNonNull(original, "The specified original rectangle must not be null."); //$NON-NLS-1$
+        if (ratio < 0) {
+            throw new IllegalArgumentException("The specified ratio " + ratio + " must be larger than zero."); //$NON-NLS-1$ //$NON-NLS-2$
+        }
+
+        return createWithFixedRatioWithinBounds(original, ratio, null);
+    }
+
+    /**
+     * Creates a new rectangle with the same center point and area (if possible) as the specified {@code original}
+     * rectangle with the specified {@code ratio} and respecting the specified {@code bounds}.
+     * 
+     * @param original
+     *            the original rectangle
+     * @param ratio
+     *            the new ratio
+     * @param bounds
+     *            the bounds within which the new rectangle will be located
+     * @return a new {@link Rectangle2D} with the same center point as the {@code original} and the specified
+     *         {@code ratio}; it has the same area as the {@code original} unless this would violate the bounds; in this
+     *         case it is as large as possible while still staying within the bounds
+     * @throws NullPointerException
+     *             if the {@code original} or {@code bounds} rectangle is null
+     * @throws IllegalArgumentException
+     *             if the {@code original} rectangle's center point is out of the bounds
+     */
+    public static Rectangle2D fixRatioWithinBounds(Rectangle2D original, double ratio, Rectangle2D bounds) {
+        Objects.requireNonNull(original, "The specified original rectangle must not be null."); //$NON-NLS-1$
+        Objects.requireNonNull(bounds, "The specified bounds for the new rectangle must not be null."); //$NON-NLS-1$
+        if (ratio < 0) {
+            throw new IllegalArgumentException("The specified ratio " + ratio + " must be larger than zero."); //$NON-NLS-1$ //$NON-NLS-2$
+        }
+
+        return createWithFixedRatioWithinBounds(original, ratio, bounds);
+    }
+
+    /**
+     * Creates a new rectangle with the same center point and area as the specified {@code original} rectangle with the
+     * specified {@code ratio} and respecting the specified {@code bounds} (if not-{@code null}).
+     * 
+     * @param original
+     *            the original rectangle
+     * @param ratio
+     *            the new ratio
+     * @param bounds
+     *            the bounds within which the new rectangle will be located; might be {@code null}
+     * @return a new {@link Rectangle2D} with the same center point as the {@code original} and the specified
+     *         {@code ratio}; it has the same area as the {@code original} unless this would violate the bounds; in this
+     *         case it is as large as possible while still staying within the bounds
+     * @throws IllegalArgumentException
+     *             if the {@code original} rectangle's center point is out of the {@code bounds}
+     */
+    private static Rectangle2D createWithFixedRatioWithinBounds(Rectangle2D original, double ratio, Rectangle2D bounds) {
+        Point2D centerPoint = getCenterPoint(original);
+
+        boolean centerPointInBounds = bounds == null || bounds.contains(centerPoint);
+        if (!centerPointInBounds) {
+            throw new IllegalArgumentException("The center point " + centerPoint //$NON-NLS-1$
+                    + " of the original rectangle is out of the specified bounds."); //$NON-NLS-1$
+        }
+
+        double area = original.getWidth() * original.getHeight();
+
+        return createForCenterAreaAndRatioWithinBounds(centerPoint, area, ratio, bounds);
+    }
+
+    /*
+     * CENTER, AREA AND RATIO
+     */
+
+    /**
+     * Creates a new rectangle with the specified {@code centerPoint}, {@code area} and {@code ratio}.
+     * 
+     * @param centerPoint
+     *            the new rectangle's center point
+     * @param area
+     *            the new rectangle's area
+     * @param ratio
+     *            the new ratio
+     * @return a new {@link Rectangle2D} with the specified {@code centerPoint}, {@code area} and {@code ratio}
+     * @throws IllegalArgumentException
+     *             if the {@code centerPoint} is out of the {@code bounds}
+     */
+    public static Rectangle2D forCenterAndAreaAndRatio(Point2D centerPoint, double area, double ratio) {
+        Objects.requireNonNull(centerPoint, "The specified center point of the new rectangle must not be null."); //$NON-NLS-1$
+        if (area < 0) {
+            throw new IllegalArgumentException("The specified area " + area + " must be larger than zero."); //$NON-NLS-1$ //$NON-NLS-2$
+        }
+        if (ratio < 0) {
+            throw new IllegalArgumentException("The specified ratio " + ratio + " must be larger than zero."); //$NON-NLS-1$ //$NON-NLS-2$
+        }
+
+        return createForCenterAreaAndRatioWithinBounds(centerPoint, area, ratio, null);
+    }
+
+    /**
+     * Creates a new rectangle with the specified {@code centerPoint}, {@code area} (if possible) and {@code ratio},
+     * respecting the specified {@code bounds}.
+     * 
+     * @param centerPoint
+     *            the new rectangle's center point
+     * @param area
+     *            the new rectangle's area (if possible without violating the bounds)
+     * @param ratio
+     *            the new ratio
+     * @param bounds
+     *            the bounds within which the new rectangle will be located
+     * @return a new {@link Rectangle2D} with the specified {@code centerPoint} and {@code ratio}; it has the specified
+     *         {@code area} unless this would violate the {@code bounds}; in this case it is as large as possible while
+     *         still staying within the bounds
+     * @throws IllegalArgumentException
+     *             if the {@code centerPoint} is out of the {@code bounds}
+     */
+    public static Rectangle2D forCenterAndAreaAndRatioWithinBounds(
+            Point2D centerPoint, double area, double ratio, Rectangle2D bounds) {
+
+        Objects.requireNonNull(centerPoint, "The specified center point of the new rectangle must not be null."); //$NON-NLS-1$
+        Objects.requireNonNull(bounds, "The specified bounds for the new rectangle must not be null."); //$NON-NLS-1$
+        boolean centerPointInBounds = bounds.contains(centerPoint);
+        if (!centerPointInBounds) {
+            throw new IllegalArgumentException("The center point " + centerPoint //$NON-NLS-1$
+                    + " of the original rectangle is out of the specified bounds."); //$NON-NLS-1$
+        }
+        if (area < 0) {
+            throw new IllegalArgumentException("The specified area " + area + " must be larger than zero."); //$NON-NLS-1$ //$NON-NLS-2$
+        }
+        if (ratio < 0) {
+            throw new IllegalArgumentException("The specified ratio " + ratio + " must be larger than zero."); //$NON-NLS-1$ //$NON-NLS-2$
+        }
+
+        return createForCenterAreaAndRatioWithinBounds(centerPoint, area, ratio, bounds);
+    }
+
+    /**
+     * Creates a new rectangle with the specified {@code centerPoint}, {@code area} (if possible) and {@code ratio},
+     * respecting the specified {@code bounds} (if not-null).
+     * 
+     * @param centerPoint
+     *            the new rectangle's center point
+     * @param area
+     *            the new rectangle's area (if possible without violating the bounds)
+     * @param ratio
+     *            the new ratio
+     * @param bounds
+     *            the bounds within which the new rectangle will be located
+     * @return a new {@link Rectangle2D} with the specified {@code centerPoint} and {@code ratio}; it has the specified
+     *         {@code area} unless this would violate the {@code bounds}; in this case it is as large as possible while
+     *         still staying within the bounds
+     * @throws IllegalArgumentException
+     *             if the {@code centerPoint} is out of the {@code bounds}
+     */
+    private static Rectangle2D createForCenterAreaAndRatioWithinBounds(
+            Point2D centerPoint, double area, double ratio, Rectangle2D bounds) {
+
+        double newWidth = Math.sqrt(area * ratio);
+        double newHeight = area / newWidth;
+
+        boolean boundsSpecified = bounds != null;
+        if (boundsSpecified) {
+            double reductionFactor = lengthReductionToStayWithinBounds(centerPoint, newWidth, newHeight, bounds);
+            newWidth *= reductionFactor;
+            newHeight *= reductionFactor;
+        }
+
+        return Rectangles2D.forCenterAndSize(centerPoint, newWidth, newHeight);
+    }
+
+    /**
+     * Computes the factor by which the specified width and height must be multiplied to keep a rectangle with their
+     * ratio and the specified center point within the specified bounds.
+     * 
+     * @param centerPoint
+     *            the center point of the new rectangle
+     * @param width
+     *            the original width which might be too large
+     * @param height
+     *            the original height which might be too large
+     * @param bounds
+     *            the bounds within which the new rectangle will be located
+     * @return the factor with which the width and height must be multiplied to stay within the bounds; always in the
+     *         closed interval [0; 1]
+     * @throws IllegalArgumentException
+     *             if the {@code centerPoint} is out of the {@code bounds}; if {@code width} or {@code height} are not
+     *             larger than zero
+     */
+    private static double lengthReductionToStayWithinBounds(
+            Point2D centerPoint, double width, double height, Rectangle2D bounds) {
+
+        Objects.requireNonNull(centerPoint, "The specified center point of the new rectangle must not be null."); //$NON-NLS-1$
+        Objects.requireNonNull(bounds, "The specified bounds for the new rectangle must not be null."); //$NON-NLS-1$
+        boolean centerPointInBounds = bounds.contains(centerPoint);
+        if (!centerPointInBounds) {
+            throw new IllegalArgumentException("The center point " + centerPoint //$NON-NLS-1$
+                    + " of the original rectangle is out of the specified bounds."); //$NON-NLS-1$
+        }
+        if (width < 0) {
+            throw new IllegalArgumentException("The specified width " + width + " must be larger than zero."); //$NON-NLS-1$ //$NON-NLS-2$
+        }
+        if (height < 0) {
+            throw new IllegalArgumentException("The specified height " + height + " must be larger than zero."); //$NON-NLS-1$ //$NON-NLS-2$
+        }
+
+        /*
+         * Compute the center point's distance to all edges. The width and height must be reduced (by the returned
+         * factor) such that their halves (!) are not greater than those distances. This can be done by finding the
+         * minimum ratio between the distance and the halved width/height.
+         */
+
+        double distanceToEast = Math.abs(centerPoint.getX() - bounds.getMinX());
+        double distanceToWest = Math.abs(centerPoint.getX() - bounds.getMaxX());
+        double distanceToNorth = Math.abs(centerPoint.getY() - bounds.getMinY());
+        double distanceToSouth = Math.abs(centerPoint.getY() - bounds.getMaxY());
+
+        // the returned factor must not be greater than one; otherwise the size would increase
+        return MathTools.min(1,
+                distanceToEast / width * 2, distanceToWest / width * 2,
+                distanceToNorth / height * 2, distanceToSouth / height * 2);
+    }
+
+    /*
+     * EDGES
+     */
+
+    /**
+     * Returns a rectangle that has the specified edge and has its opposing edge on the parallel axis defined by the
+     * specified point's X or Y coordinate (depending on the edge's orientation).
+     * 
+     * @param edge
+     *            the edge which will be contained in the returned rectangle
+     * @param point
+     *            the point whose X or Y coordinate defines the other edge
+     * @return a rectangle
+     */
+    public static Rectangle2D forEdgeAndOpposingPoint(Edge2D edge, Point2D point) {
+        double otherDimension = edge.getOrthogonalDifference(point);
+        return createForEdgeAndOtherDimension(edge, otherDimension);
+    }
+
+    /**
+     * Returns a rectangle that is principally defined by the specified edge and point. It should have the specified
+     * edge as one of its own and its parallel edge should contain the point. While this would already well-define the
+     * rectangle (compare {@link #forEdgeAndOpposingPoint(Edge2D, Point2D) forEdgeAndOpposingPoint}) the additionally
+     * specified ratio and bounds have precedence over these arguments:<br>
+     * The returned rectangle will have the ratio and will be within the bounds. If the bounds make it possible, the
+     * specified point will lie on the edge parallel to the specified one. In order to maintain the ratio, this will
+     * make it necessary to not use the specified edge but instead one with a different length. The new edge will have
+     * the same center point as the specified one.<br>
+     * This results on the following behavior: As the point is moved closer to or further away from the edge, the
+     * resulting rectangle shrinks and grows while being anchored to the specified edge's center point and keeping the
+     * ratio. This is limited by the bounds.
+     * 
+     * @param edge
+     *            the edge which defines the center point and orientation of one of the rectangle's edges; must be
+     *            within the specified {@code bounds}
+     * @param point
+     *            the point to which the rectangle spans if ratio and bounds allow it
+     * @param ratio
+     *            the ratio the new rectangle must have
+     * @param bounds
+     *            the bounds within which the new rectangle must lie
+     * @return a rectangle
+     */
+    public static Rectangle2D forEdgeAndOpposingPointAndRatioWithinBounds(
+            Edge2D edge, Point2D point, double ratio, Rectangle2D bounds) {
+
+        Objects.requireNonNull(edge, "The specified edge must not be null."); //$NON-NLS-1$
+        Objects.requireNonNull(point, "The specified point must not be null."); //$NON-NLS-1$
+        Objects.requireNonNull(bounds, "The specified bounds must not be null."); //$NON-NLS-1$
+
+        boolean edgeInBounds = contains(bounds, edge);
+        if (!edgeInBounds) {
+            throw new IllegalArgumentException(
+                    "The specified edge " + edge + " is not entirely contained on the specified bounds."); //$NON-NLS-1$ //$NON-NLS-2$
+        }
+        if (ratio < 0) {
+            throw new IllegalArgumentException("The specified ratio " + ratio + " must be larger than zero."); //$NON-NLS-1$ //$NON-NLS-2$
+        }
+
+        /*
+         * 1. move the point into the bounds
+         * 2. create an edge whose length matches the distance to the point and the ratio
+         * 3. correct that edge so that it lies within the bounds
+         * 4. create a new rectangle from that edge and the ratio
+         */
+
+        Point2D boundedPoint = movePointIntoBounds(point, bounds);
+        Edge2D unboundedEdge = resizeEdgeForDistanceAndRatio(edge, boundedPoint, ratio);
+        Edge2D boundedEdge = resizeEdgeForBounds(unboundedEdge, bounds);
+
+        // when computing the other dimension, note that the sign of the original difference between edge and point is
+        // important; otherwise the "direction" of the resize is wrong
+        double otherDimension = Math.signum(boundedEdge.getOrthogonalDifference(boundedPoint));
+        if (boundedEdge.isHorizontal()) {
+            // edge horizontal -> width fixed -> use length to compute height
+            otherDimension *= boundedEdge.getLength() / ratio;
+        } else {
+            // edge vertical -> height fixed -> use length to compute width
+            otherDimension *= boundedEdge.getLength() * ratio;
+        }
+
+        return createForEdgeAndOtherDimension(boundedEdge, otherDimension);
+    }
+
+    /**
+     * Returns either the specified point if if the specified bounds {@link Rectangle2D#contains(Point2D) contain} it or
+     * a point whose X and/or Y coordinates are moved into the bounds.
+     * 
+     * @param point
+     *            the point to move into the {@code bounds}
+     * @param bounds
+     *            the bounds into which the {@code point} will be moved
+     * @return either {@code point} or a new {@link Point2D} whose coordinates were changed so that it lies within the
+     *         {@code bounds}
+     */
+    private static Point2D movePointIntoBounds(Point2D point, Rectangle2D bounds) {
+        if (bounds.contains(point)) {
+            return point;
+        } else {
+            double boundedPointX = MathTools.inInterval(bounds.getMinX(), point.getX(), bounds.getMaxX());
+            double boundedPointY = MathTools.inInterval(bounds.getMinY(), point.getY(), bounds.getMaxY());
+            return new Point2D(boundedPointX, boundedPointY);
+        }
+    }
+
+    /**
+     * Returns an edge with the same center point and orientation as the specified edge. Its length has the specified
+     * ratio to the distance of the edge and the specified point.
+     * 
+     * @param edge
+     *            the edge whose center point and orientation defines the returned edge's center point and orientation
+     * @param point
+     *            the point to which the distance is measured
+     * @param ratio
+     *            the ratio between the distance to the {@code point} and the returned edge's length
+     * @return an {@link Edge2D}
+     */
+    private static Edge2D resizeEdgeForDistanceAndRatio(Edge2D edge, Point2D point, double ratio) {
+        double distance = Math.abs(edge.getOrthogonalDifference(point));
+        if (edge.isHorizontal()) {
+            // a horizontal edge's length lies in the X axis; the distance lies in the Y axis: x = y * ratio
+            double xLength = distance * ratio;
+            return new Edge2D(edge.getCenterPoint(), edge.getOrientation(), xLength);
+        } else {
+            // a vertical edge's length lies in the Y axis; the distance lies in the X axis: y = x / ratio
+            double yLength = distance / ratio;
+            return new Edge2D(edge.getCenterPoint(), edge.getOrientation(), yLength);
+        }
+    }
+
+    /**
+     * Returns an edge with the same center point and orientation as the specified edge. If necessary, its length is
+     * reduced to fit within the bounds.
+     * 
+     * @param edge
+     *            the edge whose center point and orientation defines the returned edge's center point and orientation;
+     *            the center point must be within the bounds or an {@link IllegalArgumentException} will be thrown
+     * @param bounds
+     *            the bounds within which the returned edge must be contained
+     * @return either the specified {@code edge} if it is with in the {@code bounds} or one with a corrected length
+     * @throws IllegalArgumentException
+     *             if the {@code edge}'s center point is out of {@code bounds}
+     */
+    private static Edge2D resizeEdgeForBounds(Edge2D edge, Rectangle2D bounds) {
+        // return the same edge if it is in the bounds
+        boolean edgeInBounds = contains(bounds, edge);
+        if (edgeInBounds) {
+            return edge;
+        }
+
+        // make sure the bounds contain the edge's center point
+        boolean centerPointInBounds = bounds.contains(edge.getCenterPoint());
+        if (!centerPointInBounds) {
+            throw new IllegalArgumentException(
+                    "The specified edge's center point (" + edge + ") is out of the specified bounds (" + bounds + ")."); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+        }
+
+        if (edge.isHorizontal()) {
+            // compute the length bounds for the left and right part of the edge
+            double leftPartLengthBound = Math.abs(bounds.getMinX() - edge.getCenterPoint().getX());
+            double rightPartLengthBound = Math.abs(bounds.getMaxX() - edge.getCenterPoint().getX());
+            // compute the length of the left and right parts of the edge
+            double leftPartLength = MathTools.inInterval(0, edge.getLength() / 2, leftPartLengthBound);
+            double rightPartLength = MathTools.inInterval(0, edge.getLength() / 2, rightPartLengthBound);
+            // compute the total length as double of the smaller length
+            double horizontalLength = Math.min(leftPartLength, rightPartLength) * 2;
+            return new Edge2D(edge.getCenterPoint(), edge.getOrientation(), horizontalLength);
+        } else {
+            // compute the length bounds for the lower and upper part of the edge
+            double lowerPartLengthBound = Math.abs(bounds.getMinY() - edge.getCenterPoint().getY());
+            double upperPartLengthBound = Math.abs(bounds.getMaxY() - edge.getCenterPoint().getY());
+            // compute the length of the lower and upper part of the edge
+            double lowerPartLength = MathTools.inInterval(0, edge.getLength() / 2, lowerPartLengthBound);
+            double upperPartLength = MathTools.inInterval(0, edge.getLength() / 2, upperPartLengthBound);
+            // compute the total length as double of the smaller length
+            double verticalLength = Math.min(lowerPartLength, upperPartLength) * 2;
+            return new Edge2D(edge.getCenterPoint(), edge.getOrientation(), verticalLength);
+        }
+    }
+
+    /**
+     * Returns a rectangle that has the specified edge and a height or width (depending on the edge's orientation) as
+     * specified by {@code otherDimension}.
+     * 
+     * @param edge
+     *            the edge which will be contained in the returned rectangle
+     * @param otherDimension
+     *            if the edge's orientation is {@link Orientation#HORIZONTAL horizontal}, this is interpreted as the
+     *            height; if the edge's orientation is {@link Orientation#VERTICAL vertical}, this is interpreted as the
+     *            width
+     * @return a rectangle
+     */
+    private static Rectangle2D createForEdgeAndOtherDimension(Edge2D edge, double otherDimension) {
+        if (edge.isHorizontal()) {
+            return createForHorizontalEdgeAndHeight(edge, otherDimension);
+        } else {
+            return createForVerticalEdgeAndWidth(edge, otherDimension);
+        }
+    }
+
+    /**
+     * Returns a rectangle that has the specified horizontal edge and height. Depending on whether the width is positive
+     * or negative, the specified edge will be the upper or lower edge of the returned rectangle.
+     * 
+     * @param horizontalEdge
+     *            the horizontal edge which will be contained in the returned rectangle
+     * @param height
+     *            the returned rectangle's height
+     * @return a rectangle
+     */
+    private static Rectangle2D createForHorizontalEdgeAndHeight(Edge2D horizontalEdge, double height) {
+        Point2D leftEdgeEndPoint = horizontalEdge.getUpperLeft();
+        double upperLeftX = leftEdgeEndPoint.getX();
+        // if the height is negative, reduce the Y coordinate by that amount
+        double upperLeftY = leftEdgeEndPoint.getY() + Math.min(0, height);
+
+        double absoluteWidth = Math.abs(horizontalEdge.getLength());
+        double absoluteHeight = Math.abs(height);
+
+        return new Rectangle2D(upperLeftX, upperLeftY, absoluteWidth, absoluteHeight);
+    }
+
+    /**
+     * Returns a rectangle that has the specified horizontal edge and width. Depending on whether the width is positive
+     * or negative, the specified edge will be the left or right edge of the returned rectangle.
+     * 
+     * @param verticalEdge
+     *            the vertical edge which will be contained in the returned rectangle
+     * @param width
+     *            the returned rectangle's height
+     * @return a rectangle
+     */
+    private static Rectangle2D createForVerticalEdgeAndWidth(Edge2D verticalEdge, double width) {
+        Point2D upperEdgeEndPoint = verticalEdge.getUpperLeft();
+        // if the width is negative, reduce the X coordinate by that amount
+        double upperLeftX = upperEdgeEndPoint.getX() + Math.min(0, width);
+        double upperLeftY = upperEdgeEndPoint.getY();
+
+        double absoluteWidth = Math.abs(width);
+        double absoluteHeight = Math.abs(verticalEdge.getLength());
+
+        return new Rectangle2D(upperLeftX, upperLeftY, absoluteWidth, absoluteHeight);
+    }
+
+    /*
+     * MISC
+     */
+
+    /**
+     * Returns a rectangle with the same coordinates as the specified bounds.
+     * 
+     * @param bounds
+     *            the {@link Bounds} for which the rectangle will be created
+     * @return a {@link Rectangle2D} with the same minX-, minY-, maxX- and maxY-coordiantes as the specified bounds
+     */
+    public static Rectangle2D fromBounds(Bounds bounds) {
+        return new Rectangle2D(bounds.getMinX(), bounds.getMinY(), bounds.getWidth(), bounds.getHeight());
+    }
+
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/AbstractBeginEndCheckingChangeStrategy.java b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/AbstractBeginEndCheckingChangeStrategy.java
new file mode 100644
index 0000000..61e0e82
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/AbstractBeginEndCheckingChangeStrategy.java
@@ -0,0 +1,149 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.tools.rectangle.change;
+
+import java.util.Objects;
+
+import javafx.geometry.Point2D;
+import javafx.geometry.Rectangle2D;
+
+/**
+ * Abstract superclass to implementations of {@link Rectangle2DChangeStrategy}. Checks whether the specified points are not-null
+ * and the "begin-continue-end"-contract.
+ */
+abstract class AbstractBeginEndCheckingChangeStrategy implements Rectangle2DChangeStrategy {
+
+    // ATTRIBUTES
+
+    /**
+     * Indicates whether {@link #beginChange(Point2D) beginChange} was called.
+     */
+    private boolean beforeBegin;
+
+    // CONSTRUCTOR
+
+    /**
+     * Creates a change strategy which checks whether begin and end are correctly called.
+     */
+    protected AbstractBeginEndCheckingChangeStrategy() {
+        beforeBegin = true;
+    }
+
+    // IMPLEMENTATION OF 'ChangeStrategy'
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public final Rectangle2D beginChange(Point2D point) {
+        Objects.requireNonNull(point, "The specified point must not be null."); //$NON-NLS-1$
+        if (!beforeBegin)
+            throw new IllegalStateException(
+                    "The change already began, so 'beginChange' must not be called again before 'endChange' was called."); //$NON-NLS-1$
+        beforeBegin = false;
+
+        beforeBeginHook(point);
+        return doBegin(point);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public final Rectangle2D continueChange(Point2D point) {
+        Objects.requireNonNull(point, "The specified point must not be null."); //$NON-NLS-1$
+        if (beforeBegin)
+            throw new IllegalStateException("The change did not begin. Call 'beginChange' before 'continueChange'."); //$NON-NLS-1$
+
+        return doContinue(point);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public final Rectangle2D endChange(Point2D point) {
+        Objects.requireNonNull(point, "The specified point must not be null."); //$NON-NLS-1$
+        if (beforeBegin)
+            throw new IllegalStateException("The change did not begin. Call 'beginChange' before 'endChange'."); //$NON-NLS-1$
+
+        Rectangle2D finalRectangle = doEnd(point);
+        afterEndHook(point);
+        beforeBegin = true;
+        return finalRectangle;
+    }
+
+    //ABSTRACT METHODS
+
+    /**
+     * Called before the change begins at the specified point.
+     * 
+     * @param point
+     *            a point
+     */
+    protected void beforeBeginHook(Point2D point) {
+        // can be overridden by subclasses
+    }
+
+    /**
+     * Begins the change at the specified point.
+     * 
+     * @param point
+     *            a point
+     * @return the new rectangle
+     */
+    protected abstract Rectangle2D doBegin(Point2D point);
+
+    /**
+     * Continues the change to the specified point. Must not be called before a call to {@link #beginChange}.
+     * 
+     * @param point
+     *            a point
+     * @return the new rectangle
+     */
+    protected abstract Rectangle2D doContinue(Point2D point);
+
+    /**
+     * Ends the change at the specified point. Must not be called before a call to {@link #beginChange}.
+     * 
+     * @param point
+     *            a point
+     * @return the new rectangle
+     */
+    protected abstract Rectangle2D doEnd(Point2D point);
+
+    /**
+     * Called after the change ends at the specified point.
+     * 
+     * @param point
+     *            a point
+     */
+    protected void afterEndHook(Point2D point) {
+        // can be overridden by subclasses
+    }
+
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/AbstractFixedEdgeChangeStrategy.java b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/AbstractFixedEdgeChangeStrategy.java
new file mode 100644
index 0000000..ccc5019
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/AbstractFixedEdgeChangeStrategy.java
@@ -0,0 +1,137 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.tools.rectangle.change;
+
+import impl.org.controlsfx.tools.rectangle.Edge2D;
+import impl.org.controlsfx.tools.rectangle.Rectangles2D;
+import javafx.geometry.Point2D;
+import javafx.geometry.Rectangle2D;
+
+/**
+ * Abstract superclass to those implementations of {@link Rectangle2DChangeStrategy} which compute their rectangle by
+ * spanning it from a fixed edge to the parallel edge defined by the point given to
+ * {@link Rectangle2DChangeStrategy#continueChange(Point2D) continueChange}. <br>
+ * The edge is fixed during the change but can be changed in between changes. Implemented such that a ratio is respected
+ * if specified.
+ */
+abstract class AbstractFixedEdgeChangeStrategy extends AbstractRatioRespectingChangeStrategy {
+
+    // ATTRIBUTES
+
+    /**
+     * A rectangle which defines the bounds within which the new rectangle must be contained.
+     */
+    private final Rectangle2D bounds;
+
+    /**
+     * The edge which is fixed during the change. In {@link #doBegin(Point2D)} it is set to {@link #getFixedEdge()}; in
+     * {@link #doEnd(Point2D)} it is set to {@code null}.
+     */
+    private Edge2D fixedEdge;
+
+    // CONSTRUCTOR
+
+    /**
+     * Creates a fixed edge change strategy. It respects the specified {@code ratio} if {@code ratioFixed} is
+     * {@code true}.
+     * 
+     * @param ratioFixed
+     *            indicates whether the ratio will be fixed
+     * @param ratio
+     *            defines the fixed ratio
+     * @param bounds
+     *            the bounds within which the new rectangle must be contained
+     */
+    protected AbstractFixedEdgeChangeStrategy(boolean ratioFixed, double ratio, Rectangle2D bounds) {
+        super(ratioFixed, ratio);
+        this.bounds = bounds;
+    }
+
+    // ABSTRACT METHODS
+
+    /**
+     * Returns the edge which is fixed during the change. Called once when the change begins.
+     * 
+     * @return the edge which is fixed during the change
+     */
+    protected abstract Edge2D getFixedEdge();
+
+    // IMPLEMENTATION OF 'do...'
+
+    /**
+     * Creates a new rectangle from the two edges defined by {@link #fixedEdge} and its parallel through the specified
+     * point.
+     * 
+     * @param point
+     *            the point defining the parallel edge
+     * @return the rectangle defined the two edges
+     */
+    private final Rectangle2D createFromEdges(Point2D point) {
+        Point2D pointInBounds = Rectangles2D.inRectangle(bounds, point);
+
+        if (isRatioFixed()) {
+            return Rectangles2D.forEdgeAndOpposingPointAndRatioWithinBounds(
+                    fixedEdge, pointInBounds, getRatio(), bounds);
+        } else {
+            return Rectangles2D.forEdgeAndOpposingPoint(fixedEdge, pointInBounds);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected final Rectangle2D doBegin(Point2D point) {
+        boolean startPointNotInBounds = !bounds.contains(point);
+        if (startPointNotInBounds) {
+            throw new IllegalArgumentException(
+                    "The change's start point (" + point + ") must lie within the bounds (" + bounds + ")."); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+        }
+
+        fixedEdge = getFixedEdge();
+        return createFromEdges(point);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected Rectangle2D doContinue(Point2D point) {
+        return createFromEdges(point);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected final Rectangle2D doEnd(Point2D point) {
+        Rectangle2D newRectangle = createFromEdges(point);
+        fixedEdge = null;
+        return newRectangle;
+    }
+
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/AbstractFixedPointChangeStrategy.java b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/AbstractFixedPointChangeStrategy.java
new file mode 100644
index 0000000..27353e8
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/AbstractFixedPointChangeStrategy.java
@@ -0,0 +1,139 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.tools.rectangle.change;
+
+import impl.org.controlsfx.tools.rectangle.Rectangles2D;
+
+import java.util.Objects;
+
+import javafx.geometry.Point2D;
+import javafx.geometry.Rectangle2D;
+
+/**
+ * Abstract superclass to those implementations of {@link Rectangle2DChangeStrategy} which compute their rectangle by
+ * spanning it from a fixed point to the point given to {@link Rectangle2DChangeStrategy#continueChange(Point2D)
+ * continueChange}. <br>
+ * The point is fixed during the change but can be changed in between changes. Implemented such that a ratio is
+ * respected if specified.
+ */
+abstract class AbstractFixedPointChangeStrategy extends AbstractRatioRespectingChangeStrategy {
+
+    // ATTRIBUTES
+
+    /**
+     * A rectangle which defines the bounds within which the new rectangle must be contained.
+     */
+    private final Rectangle2D bounds;
+
+    /**
+     * The point which is fixed during the change. In {@link #doBegin(Point2D)} it is set to {@link #getFixedCorner()};
+     * in {@link #doEnd(Point2D)} it is set to {@code null}.
+     */
+    private Point2D fixedCorner;
+
+    // CONSTRUCTOR
+
+    /**
+     * Creates a fixed corner change strategy. It respects the specified {@code ratio} if {@code ratioFixed} is
+     * {@code true}.
+     * 
+     * @param ratioFixed
+     *            indicates whether the ratio will be fixed
+     * @param ratio
+     *            defines the fixed ratio
+     * @param bounds
+     *            the bounds within which the new rectangle must be contained
+     */
+    protected AbstractFixedPointChangeStrategy(boolean ratioFixed, double ratio, Rectangle2D bounds) {
+        super(ratioFixed, ratio);
+        Objects.requireNonNull(bounds, "The argument 'bounds' must not be null."); //$NON-NLS-1$
+
+        this.bounds = bounds;
+    }
+
+    // ABSTRACT METHODS
+
+    /**
+     * Returns the corner which is fixed during the change. Called once when the change begins.
+     * 
+     * @return the corner which is fixed during the change
+     */
+    protected abstract Point2D getFixedCorner();
+
+    // IMPLEMENTATION OF 'do...'
+
+    /**
+     * Creates a new rectangle from the two corners defined by {@link #getFixedCorner()} and the specified point.
+     * 
+     * @param point
+     *            the second corner
+     * @return the rectangle defined the two corners
+     */
+    private final Rectangle2D createFromCorners(Point2D point) {
+        Point2D pointInBounds = Rectangles2D.inRectangle(bounds, point);
+
+        if (isRatioFixed()) {
+            return Rectangles2D.forDiagonalCornersAndRatio(fixedCorner, pointInBounds, getRatio());
+        } else {
+            return Rectangles2D.forDiagonalCorners(fixedCorner, pointInBounds);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected final Rectangle2D doBegin(Point2D point) {
+        boolean startPointNotInBounds = !bounds.contains(point);
+        if (startPointNotInBounds) {
+            throw new IllegalArgumentException(
+                    "The change's start point (" + point + ") must lie within the bounds (" + bounds + ")."); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+        }
+
+        fixedCorner = getFixedCorner();
+        return createFromCorners(point);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected Rectangle2D doContinue(Point2D point) {
+        return createFromCorners(point);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected final Rectangle2D doEnd(Point2D point) {
+        Rectangle2D newRectangle = createFromCorners(point);
+        fixedCorner = null;
+        return newRectangle;
+    }
+
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/AbstractPreviousRectangleChangeStrategy.java b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/AbstractPreviousRectangleChangeStrategy.java
new file mode 100644
index 0000000..bf169d3
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/AbstractPreviousRectangleChangeStrategy.java
@@ -0,0 +1,77 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.tools.rectangle.change;
+
+import java.util.Objects;
+
+import javafx.geometry.Rectangle2D;
+
+/**
+ * Abstract superclass to those implementations of {@link Rectangle2DChangeStrategy}, which are based on a previous
+ * rectangle. Respects a ratio if one is set.
+ */
+abstract class AbstractPreviousRectangleChangeStrategy extends AbstractRatioRespectingChangeStrategy {
+
+    // ATTRIBUTES
+
+    /**
+     * The rectangle these changes are based on.
+     */
+    private final Rectangle2D previous;
+
+    // CONSTRUCTOR
+
+    /**
+     * Creates a change strategy which is based on the specified {@code previous} rectangle and respects the specified
+     * {@code ratio} if {@code ratioFixed} is {@code true}.
+     * 
+     * @param previous
+     *            the previous rectangle this change is based on
+     * @param ratioFixed
+     *            indicates whether the ratio will be fixed
+     * @param ratio
+     *            defines the fixed ratio
+     */
+    protected AbstractPreviousRectangleChangeStrategy(Rectangle2D previous, boolean ratioFixed, double ratio) {
+        super(ratioFixed, ratio);
+
+        Objects.requireNonNull(previous, "The previous rectangle must not be null."); //$NON-NLS-1$
+        this.previous = previous;
+    }
+
+    // ATTRIBUTE ACCESS
+
+    /**
+     * The previous rectangle this change is based on.
+     * 
+     * @return the previous rectangle
+     */
+    protected final Rectangle2D getPrevious() {
+        return previous;
+    }
+
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/AbstractRatioRespectingChangeStrategy.java b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/AbstractRatioRespectingChangeStrategy.java
new file mode 100644
index 0000000..1d89c73
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/AbstractRatioRespectingChangeStrategy.java
@@ -0,0 +1,86 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.tools.rectangle.change;
+
+/**
+ * Abstract superclass to implementations of {@link Rectangle2DChangeStrategy}, which might be parameterized such that only
+ * rectangles of a defined ratio are created. This parameterization happens during construction. Subclasses must
+ * implement the ratio handling themselves! This class only holds the parameters.
+ */
+abstract class AbstractRatioRespectingChangeStrategy extends AbstractBeginEndCheckingChangeStrategy {
+
+    // ATTRIBUTES
+
+    /**
+     * Indicates whether the current selection must have a fixed ratio. If so, 'ratio' can be used.
+     */
+    private final boolean ratioFixed;
+
+    /**
+     * The currently used ratio. Should only be used if 'ratioFixed' is true.
+     */
+    private final double ratio;
+
+    // CONSTRUCTOR
+
+    /**
+     * Creates a change strategy which respects the specified {@code ratio} if {@code ratioFixed} is {@code true}.
+     * 
+     * @param ratioFixed
+     *            indicates whether the ratio will be fixed
+     * @param ratio
+     *            defines the fixed ratio
+     */
+    protected AbstractRatioRespectingChangeStrategy(boolean ratioFixed, double ratio) {
+        super();
+        this.ratioFixed = ratioFixed;
+        this.ratio = ratio;
+    }
+
+    // Attribute Access
+
+    /**
+     * Indicates whether the ratio is fixed. If so, the ratio can be accessed with {@link #getRatio()}.
+     * 
+     * @return true if the ratio is fixed; false otherwise
+     */
+    protected final boolean isRatioFixed() {
+        return ratioFixed;
+    }
+
+    /**
+     * The current ratio. Can only be called without exception when {@link #isRatioFixed()} returns true.
+     * 
+     * @return the current ratio
+     */
+    protected final double getRatio() {
+        if (!ratioFixed)
+            throw new IllegalStateException("The ratio is not fixed."); //$NON-NLS-1$
+        return ratio;
+    }
+
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/MoveChangeStrategy.java b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/MoveChangeStrategy.java
new file mode 100644
index 0000000..26f8c9d
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/MoveChangeStrategy.java
@@ -0,0 +1,166 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.tools.rectangle.change;
+
+import impl.org.controlsfx.tools.MathTools;
+
+import java.util.Objects;
+
+import javafx.geometry.Point2D;
+import javafx.geometry.Rectangle2D;
+
+/**
+ * Moves the rectangle around.
+ */
+public class MoveChangeStrategy extends AbstractPreviousRectangleChangeStrategy {
+
+    /*
+     * The previous rectangle will be moved around using a vector computed from the start to the current point.
+     * The moved rectangle will be forced within defined bounds.
+     */
+
+    // ATTRIBUTES
+
+    /**
+     * A rectangle which defines the bounds within which the previous rectangle can be moved.
+     */
+    private final Rectangle2D bounds;
+
+    /**
+     * The starting point of the selection change. The move will be computed relative to this point.
+     */
+    private Point2D startingPoint;
+
+    // CONSTRUCTORS
+
+    /**
+     * Creates a new change strategy which moves the specified rectangle within the specified bounds.
+     * 
+     * @param previous
+     *            the previous rectangle this move is based on
+     * @param bounds
+     *            the bounds within which the rectangle can be moved
+     */
+    public MoveChangeStrategy(Rectangle2D previous, Rectangle2D bounds) {
+        super(previous, false, 0);
+        Objects.requireNonNull(bounds, "The specified bounds must not be null."); //$NON-NLS-1$
+        this.bounds = bounds;
+    }
+
+    /**
+     * Creates a new change strategy which moves the specified rectangle within the specified bounds defined by the
+     * rectangle from {@code (0, 0)} to {@code (maxX, maxY)}.
+     * 
+     * @param previous
+     *            the previous rectangle this move is based on
+     * @param maxX
+     *            the maximal x-coordinate of the right edge of the created rectangles; must be greater than or equal to
+     *            the previous rectangle's width
+     * @param maxY
+     *            the maximal y-coordinate of the lower edge of the created rectangles; must be greater than or equal to
+     *            the previous rectangle's height
+     */
+    public MoveChangeStrategy(Rectangle2D previous, double maxX, double maxY) {
+        super(previous, false, 0);
+        if (maxX < previous.getWidth()) {
+            throw new IllegalArgumentException(
+                    "The specified maximal x-coordinate must be greater than or equal to the previous rectangle's width."); //$NON-NLS-1$
+        }
+        if (maxY < previous.getHeight()) {
+            throw new IllegalArgumentException(
+                    "The specified maximal y-coordinate must be greater than or equal to the previous rectangle's height."); //$NON-NLS-1$
+        }
+
+        bounds = new Rectangle2D(0, 0, maxX, maxY);
+    }
+
+    // IMPLEMENTATION OF 'do...'
+
+    /**
+     * Moves the previous rectangle to the specified point relative to the {@link #startingPoint}.
+     * 
+     * @param point
+     *            the vector from the {@link #startingPoint} to this point defines the movement
+     * @return the moved rectangle
+     */
+    private final Rectangle2D moveRectangleToPoint(Point2D point) {
+
+        /*
+         * The computation makes sure that no part of the rectangle can be moved out the bounds.
+         * To achieve this, the coordinates of the future rectangle's upper left corner are forced into the intervals
+         *  - [boundsMinX, boundsMaxX - previousRectangleWidth],
+         *  - [boundsMinY, boundsMaxY - previousRectangleHeight] respectively.
+         */
+
+        // vector from starting to specified point
+        double xMove = point.getX() - startingPoint.getX();
+        double yMove = point.getY() - startingPoint.getY();
+
+        // upper left corner
+        double upperLeftX = getPrevious().getMinX() + xMove;
+        double upperLeftY = getPrevious().getMinY() + yMove;
+
+        // upper bounds for upper left corner
+        double maxX = bounds.getMaxX() - getPrevious().getWidth();
+        double maxY = bounds.getMaxY() - getPrevious().getHeight();
+
+        // corrected upper left corner
+        double correctedUpperLeftX = MathTools.inInterval(bounds.getMinX(), upperLeftX, maxX);
+        double correctedUpperLeftY = MathTools.inInterval(bounds.getMinY(), upperLeftY, maxY);
+
+        // rectangle from corrected upper left corner with the previous rectangle's width and height
+        return new Rectangle2D(
+                correctedUpperLeftX, correctedUpperLeftY,
+                getPrevious().getWidth(), getPrevious().getHeight());
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected Rectangle2D doBegin(Point2D point) {
+        this.startingPoint = point;
+        return getPrevious();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected Rectangle2D doContinue(Point2D point) {
+        return moveRectangleToPoint(point);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected Rectangle2D doEnd(Point2D point) {
+        return moveRectangleToPoint(point);
+    }
+
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/NewChangeStrategy.java b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/NewChangeStrategy.java
new file mode 100644
index 0000000..e62f4b4
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/NewChangeStrategy.java
@@ -0,0 +1,82 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.tools.rectangle.change;
+
+import javafx.geometry.Point2D;
+import javafx.geometry.Rectangle2D;
+
+/**
+ * A strategy which creates a new rectangle.
+ */
+public class NewChangeStrategy extends AbstractFixedPointChangeStrategy {
+
+    /*
+     * The new selection will have the starting point as a fixed corner. The other corner will always be the current
+     * point modulo the ratio which will be respected if enforced. Both is handled by the superclass.
+     */
+
+    // ATTRIBUTES
+
+    /**
+     * The starting point of this change.
+     */
+    private Point2D startingPoint;
+
+    // CONSTRUCTOR
+
+    /**
+     * Creates a change strategy which creates a new rectangle. It respects the specified {@code ratio} if
+     * {@code ratioFixed} is {@code true}.
+     * 
+     * @param ratioFixed
+     *            indicates whether the ratio will be fixed
+     * @param ratio
+     *            defines the fixed ratio
+     * @param bounds
+     *            the bounds within which the new rectangle must be contained
+     */
+    public NewChangeStrategy(boolean ratioFixed, double ratio, Rectangle2D bounds) {
+        super(ratioFixed, ratio, bounds);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected void beforeBeginHook(Point2D point) {
+        startingPoint = point;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected Point2D getFixedCorner() {
+        return startingPoint;
+    }
+
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/Rectangle2DChangeStrategy.java b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/Rectangle2DChangeStrategy.java
new file mode 100644
index 0000000..3784106
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/Rectangle2DChangeStrategy.java
@@ -0,0 +1,73 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.tools.rectangle.change;
+
+import javafx.geometry.Point2D;
+import javafx.geometry.Rectangle2D;
+
+/**
+ * A {@code Rectangle2DChangeStrategy} creates instances of {@link Rectangle2D} based on the coordinates of the begin,
+ * continuation and end of an action. The behavior is undefined if these three methods are not called on the following
+ * order:<br>
+ * ({@link #beginChange(Point2D) begin} -> {@link #continueChange(Point2D) continue}* -> {@link #endChange(Point2D) end}
+ * )* <br>
+ * <br>
+ * Most implementations will be creating new rectangles based on an existing one. If the created ones constantly replace
+ * the original, this effectively "changes" the rectangle's appearance (note that {@link Rectangle2D} instances
+ * themselves are immutable !). This interface and its implementations were created to easily allow a GUI user to change
+ * an existing rectangle by typical resize and move operations.
+ */
+public interface Rectangle2DChangeStrategy {
+
+    /**
+     * Begins the change at the specified point.
+     * 
+     * @param point
+     *            a point
+     * @return the new rectangle
+     */
+    Rectangle2D beginChange(Point2D point);
+
+    /**
+     * Continues the change to the specified point. Must not be called before a call to {@link #beginChange}.
+     * 
+     * @param point
+     *            a point
+     * @return the new rectangle
+     */
+    Rectangle2D continueChange(Point2D point);
+
+    /**
+     * Ends the change at the specified point. Must not be called before a call to {@link #beginChange}.
+     * 
+     * @param point
+     *            a point
+     * @return the new rectangle
+     */
+    Rectangle2D endChange(Point2D point);
+
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/ToEastChangeStrategy.java b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/ToEastChangeStrategy.java
new file mode 100644
index 0000000..ccbe662
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/ToEastChangeStrategy.java
@@ -0,0 +1,104 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.tools.rectangle.change;
+
+import impl.org.controlsfx.tools.rectangle.Edge2D;
+import javafx.geometry.Orientation;
+import javafx.geometry.Point2D;
+import javafx.geometry.Rectangle2D;
+
+/**
+ * A strategy which enlarges an existing rectangle to the east.
+ */
+public class ToEastChangeStrategy extends AbstractFixedEdgeChangeStrategy {
+
+    /*
+     * The new rectangle will have the existing rectangle's western edge as a fixed edge. The parallel edge will
+     * be defined by the current point (modulo the ratio which will be respected if enforced), which is handled by the
+     * superclass.
+     */
+
+    // ATTRIBUTES
+
+    /**
+     * The new rectangle's western edge.
+     */
+    private final Edge2D westernEdge;
+
+    // CONSTRUCTOR
+
+    /**
+     * Creates a new change strategy which enlarges the specified {@code original} rectangle to the east. The given
+     * {@code ratio} is enforced when indicated by {@code ratioFixed}.
+     * 
+     * @param original
+     *            the original rectangle
+     * @param ratioFixed
+     *            indicates whether the rectangle's ratio will be fixed to the {@code ratio}
+     * @param ratio
+     *            the possibly fixed ratio of the rectangle created by this strategy
+     * @param bounds
+     *            the bounds within which the rectangle can be resized
+     */
+    public ToEastChangeStrategy(Rectangle2D original, boolean ratioFixed, double ratio, Rectangle2D bounds) {
+        super(ratioFixed, ratio, bounds);
+        Point2D edgeCenterPoint = new Point2D(original.getMinX(), (original.getMinY() + original.getMaxY()) / 2);
+        westernEdge = new Edge2D(edgeCenterPoint, Orientation.VERTICAL, original.getMaxY() - original.getMinY());
+    }
+
+    /**
+     * Creates a new change strategy which enlarges the specified {@code original} rectangle to the northeast. The given
+     * {@code ratio} is enforced when indicated by {@code ratioFixed}.
+     * 
+     * @param original
+     *            the original rectangle
+     * @param ratioFixed
+     *            indicates whether the rectangle's ratio will be fixed to the {@code ratio}
+     * @param ratio
+     *            the possibly fixed ratio of the rectangle created by this strategy
+     * @param maxX
+     *            the maximal x-coordinate of the right edge of the created rectangles; must be greater than or equal to
+     *            the previous rectangle's width
+     * @param maxY
+     *            the maximal y-coordinate of the lower edge of the created rectangles; must be greater than or equal to
+     *            the previous rectangle's height
+     */
+    public ToEastChangeStrategy(Rectangle2D original, boolean ratioFixed, double ratio, double maxX, double maxY) {
+        this(original, ratioFixed, ratio, new Rectangle2D(0, 0, maxX, maxY));
+    }
+
+    // IMPLEMENTATION OF 'AbstractFixedEdgeChangeStrategy'
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected Edge2D getFixedEdge() {
+        return westernEdge;
+    }
+
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/ToNorthChangeStrategy.java b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/ToNorthChangeStrategy.java
new file mode 100644
index 0000000..29674d0
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/ToNorthChangeStrategy.java
@@ -0,0 +1,104 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.tools.rectangle.change;
+
+import impl.org.controlsfx.tools.rectangle.Edge2D;
+import javafx.geometry.Orientation;
+import javafx.geometry.Point2D;
+import javafx.geometry.Rectangle2D;
+
+/**
+ * A strategy which enlarges an existing rectangle to the north.
+ */
+public class ToNorthChangeStrategy extends AbstractFixedEdgeChangeStrategy {
+
+    /*
+     * The new rectangle will have the existing rectangle's southern edge as a fixed edge. The parallel edge will
+     * be defined by the current point (modulo the ratio which will be respected if enforced), which is handled by the
+     * superclass.
+     */
+
+    // ATTRIBUTES
+
+    /**
+     * The new rectangle's southern edge.
+     */
+    private final Edge2D southernEdge;
+
+    // CONSTRUCTOR
+
+    /**
+     * Creates a new change strategy which enlarges the specified {@code original} rectangle to the north. The given
+     * {@code ratio} is enforced when indicated by {@code ratioFixed}.
+     * 
+     * @param original
+     *            the original rectangle
+     * @param ratioFixed
+     *            indicates whether the rectangle's ratio will be fixed to the {@code ratio}
+     * @param ratio
+     *            the possibly fixed ratio of the rectangle created by this strategy
+     * @param bounds
+     *            the bounds within which the rectangle can be resized
+     */
+    public ToNorthChangeStrategy(Rectangle2D original, boolean ratioFixed, double ratio, Rectangle2D bounds) {
+        super(ratioFixed, ratio, bounds);
+        Point2D edgeCenterPoint = new Point2D((original.getMinX() + original.getMaxX()) / 2, original.getMaxY());
+        southernEdge = new Edge2D(edgeCenterPoint, Orientation.HORIZONTAL, original.getMaxX() - original.getMinX());
+    }
+
+    /**
+     * Creates a new change strategy which enlarges the specified {@code original} rectangle to the northeast. The given
+     * {@code ratio} is enforced when indicated by {@code ratioFixed}.
+     * 
+     * @param original
+     *            the original rectangle
+     * @param ratioFixed
+     *            indicates whether the rectangle's ratio will be fixed to the {@code ratio}
+     * @param ratio
+     *            the possibly fixed ratio of the rectangle created by this strategy
+     * @param maxX
+     *            the maximal x-coordinate of the right edge of the created rectangles; must be greater than or equal to
+     *            the previous rectangle's width
+     * @param maxY
+     *            the maximal y-coordinate of the lower edge of the created rectangles; must be greater than or equal to
+     *            the previous rectangle's height
+     */
+    public ToNorthChangeStrategy(Rectangle2D original, boolean ratioFixed, double ratio, double maxX, double maxY) {
+        this(original, ratioFixed, ratio, new Rectangle2D(0, 0, maxX, maxY));
+    }
+
+    // IMPLEMENTATION OF 'AbstractFixedEdgeChangeStrategy'
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected Edge2D getFixedEdge() {
+        return southernEdge;
+    }
+
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/ToNortheastChangeStrategy.java b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/ToNortheastChangeStrategy.java
new file mode 100644
index 0000000..5edb288
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/ToNortheastChangeStrategy.java
@@ -0,0 +1,80 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.tools.rectangle.change;
+
+import javafx.geometry.Point2D;
+import javafx.geometry.Rectangle2D;
+
+/**
+ * A strategy which enlarges an existing rectangle to the northeast.
+ */
+public class ToNortheastChangeStrategy extends AbstractFixedPointChangeStrategy {
+
+    /*
+     * The new rectangle will have the existing rectangle's southwestern corner as a fixed corner. The other corner will
+     * always be the current point (modulo the ratio which will be respected if enforced), which is handled by the
+     * superclass.
+     */
+
+    // ATTRIBUTES
+
+    /**
+     * The new rectangle's southwestern corner.
+     */
+    private final Point2D southwesternCorner;
+
+    // CONSTRUCTOR
+
+    /**
+     * Creates a new change strategy which enlarges the specified {@code original} rectangle to the northeast. The given
+     * {@code ratio} is enforced when indicated by {@code ratioFixed}.
+     * 
+     * @param original
+     *            the original rectangle
+     * @param ratioFixed
+     *            indicates whether the rectangle's ratio will be fixed to the {@code ratio}
+     * @param ratio
+     *            the possibly fixed ratio of the rectangle created by this strategy
+     * @param bounds
+     *            the bounds within which the new rectangle must be contained
+     */
+    public ToNortheastChangeStrategy(Rectangle2D original, boolean ratioFixed, double ratio, Rectangle2D bounds) {
+        super(ratioFixed, ratio, bounds);
+        southwesternCorner = new Point2D(original.getMinX(), original.getMaxY());
+    }
+
+    // IMPLEMENTATION OF 'AbstractFixedPointChangeStrategy'
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected Point2D getFixedCorner() {
+        return southwesternCorner;
+    }
+
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/ToNorthwestChangeStrategy.java b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/ToNorthwestChangeStrategy.java
new file mode 100644
index 0000000..1831333
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/ToNorthwestChangeStrategy.java
@@ -0,0 +1,80 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.tools.rectangle.change;
+
+import javafx.geometry.Point2D;
+import javafx.geometry.Rectangle2D;
+
+/**
+ * A strategy which enlarges an existing rectangle to the northwest.
+ */
+public class ToNorthwestChangeStrategy extends AbstractFixedPointChangeStrategy {
+
+    /*
+     * The new rectangle will have the existing rectangle's southeastern corner as a fixed corner. The other corner will
+     * always be the current point (modulo the ratio which will be respected if enforced), which is handled by the
+     * superclass.
+     */
+
+    // ATTRIBUTES
+
+    /**
+     * The new rectangle's southeastern corner.
+     */
+    private final Point2D southeasternCorner;
+
+    // CONSTRUCTOR
+
+    /**
+     * Creates a new change strategy which enlarges the specified {@code original} rectangle to the northwest. The given
+     * {@code ratio} is enforced when indicated by {@code ratioFixed}.
+     * 
+     * @param original
+     *            the original rectangle
+     * @param ratioFixed
+     *            indicates whether the rectangle's ratio will be fixed to the {@code ratio}
+     * @param ratio
+     *            the possibly fixed ratio of the rectangle created by this strategy
+     * @param bounds
+     *            the bounds within which the new rectangle must be contained
+     */
+    public ToNorthwestChangeStrategy(Rectangle2D original, boolean ratioFixed, double ratio, Rectangle2D bounds) {
+        super(ratioFixed, ratio, bounds);
+        southeasternCorner = new Point2D(original.getMaxX(), original.getMaxY());
+    }
+
+    // IMPLEMENTATION OF 'AbstractFixedPointChangeStrategy'
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected Point2D getFixedCorner() {
+        return southeasternCorner;
+    }
+
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/ToSouthChangeStrategy.java b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/ToSouthChangeStrategy.java
new file mode 100644
index 0000000..3ea9f78
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/ToSouthChangeStrategy.java
@@ -0,0 +1,104 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.tools.rectangle.change;
+
+import impl.org.controlsfx.tools.rectangle.Edge2D;
+import javafx.geometry.Orientation;
+import javafx.geometry.Point2D;
+import javafx.geometry.Rectangle2D;
+
+/**
+ * A strategy which enlarges an existing rectangle to the south.
+ */
+public class ToSouthChangeStrategy extends AbstractFixedEdgeChangeStrategy {
+
+    /*
+     * The new rectangle will have the existing rectangle's northern edge as a fixed edge. The parallel edge will
+     * be defined by the current point (modulo the ratio which will be respected if enforced), which is handled by the
+     * superclass.
+     */
+
+    // ATTRIBUTES
+
+    /**
+     * The new rectangle's northern edge.
+     */
+    private final Edge2D northernEdge;
+
+    // CONSTRUCTOR
+
+    /**
+     * Creates a new change strategy which enlarges the specified {@code original} rectangle to the south. The given
+     * {@code ratio} is enforced when indicated by {@code ratioFixed}.
+     * 
+     * @param original
+     *            the original rectangle
+     * @param ratioFixed
+     *            indicates whether the rectangle's ratio will be fixed to the {@code ratio}
+     * @param ratio
+     *            the possibly fixed ratio of the rectangle created by this strategy
+     * @param bounds
+     *            the bounds within which the rectangle can be resized
+     */
+    public ToSouthChangeStrategy(Rectangle2D original, boolean ratioFixed, double ratio, Rectangle2D bounds) {
+        super(ratioFixed, ratio, bounds);
+        Point2D edgeCenterPoint = new Point2D((original.getMinX() + original.getMaxX()) / 2, original.getMinY());
+        northernEdge = new Edge2D(edgeCenterPoint, Orientation.HORIZONTAL, original.getMaxX() - original.getMinX());
+    }
+
+    /**
+     * Creates a new change strategy which enlarges the specified {@code original} rectangle to the northeast. The given
+     * {@code ratio} is enforced when indicated by {@code ratioFixed}.
+     * 
+     * @param original
+     *            the original rectangle
+     * @param ratioFixed
+     *            indicates whether the rectangle's ratio will be fixed to the {@code ratio}
+     * @param ratio
+     *            the possibly fixed ratio of the rectangle created by this strategy
+     * @param maxX
+     *            the maximal x-coordinate of the right edge of the created rectangles; must be greater than or equal to
+     *            the previous rectangle's width
+     * @param maxY
+     *            the maximal y-coordinate of the lower edge of the created rectangles; must be greater than or equal to
+     *            the previous rectangle's height
+     */
+    public ToSouthChangeStrategy(Rectangle2D original, boolean ratioFixed, double ratio, double maxX, double maxY) {
+        this(original, ratioFixed, ratio, new Rectangle2D(0, 0, maxX, maxY));
+    }
+
+    // IMPLEMENTATION OF 'AbstractFixedEdgeChangeStrategy'
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected Edge2D getFixedEdge() {
+        return northernEdge;
+    }
+
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/ToSoutheastChangeStrategy.java b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/ToSoutheastChangeStrategy.java
new file mode 100644
index 0000000..430c3f5
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/ToSoutheastChangeStrategy.java
@@ -0,0 +1,80 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.tools.rectangle.change;
+
+import javafx.geometry.Point2D;
+import javafx.geometry.Rectangle2D;
+
+/**
+ * A strategy which enlarges an existing rectangle to the southeast.
+ */
+public class ToSoutheastChangeStrategy extends AbstractFixedPointChangeStrategy {
+
+    /*
+     * The new rectangle will have the existing rectangle's northwestern corner as a fixed corner. The other corner will
+     * always be the current point (modulo the ratio which will be respected if enforced), which is handled by the
+     * superclass.
+     */
+
+    // ATTRIBUTES
+
+    /**
+     * The new rectangle's northwestern corner.
+     */
+    private final Point2D northwesternCorner;
+
+    // CONSTRUCTOR
+
+    /**
+     * Creates a new change strategy which enlarges the specified {@code original} rectangle to the southeast. The given
+     * {@code ratio} is enforced when indicated by {@code ratioFixed}.
+     * 
+     * @param original
+     *            the original rectangle
+     * @param ratioFixed
+     *            indicates whether the rectangle's ratio will be fixed to the {@code ratio}
+     * @param ratio
+     *            the possibly fixed ratio of the rectangle created by this strategy
+     * @param bounds
+     *            the bounds within which the new rectangle must be contained
+     */
+    public ToSoutheastChangeStrategy(Rectangle2D original, boolean ratioFixed, double ratio, Rectangle2D bounds) {
+        super(ratioFixed, ratio, bounds);
+        northwesternCorner = new Point2D(original.getMinX(), original.getMinY());
+    }
+
+    // IMPLEMENTATION OF 'AbstractFixedPointChangeStrategy'
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected Point2D getFixedCorner() {
+        return northwesternCorner;
+    }
+
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/ToSouthwestChangeStrategy.java b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/ToSouthwestChangeStrategy.java
new file mode 100644
index 0000000..cad2aee
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/ToSouthwestChangeStrategy.java
@@ -0,0 +1,82 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.tools.rectangle.change;
+
+import javafx.geometry.Point2D;
+import javafx.geometry.Rectangle2D;
+
+/**
+ * A strategy which enlarges an existing rectangle to the southwest.
+ * 
+ * @author pan
+ */
+public class ToSouthwestChangeStrategy extends AbstractFixedPointChangeStrategy {
+
+    /*
+     * The new rectangle will have the existing rectangle's northeastern corner as a fixed corner. The other corner will
+     * always be the current point (modulo the ratio which will be respected if enforced), which is handled by the
+     * superclass.
+     */
+
+    // ATTRIBUTES
+
+    /**
+     * The new rectangle's northeastern corner.
+     */
+    private final Point2D northeasternCorner;
+
+    // CONSTRUCTOR
+
+    /**
+     * Creates a new change strategy which enlarges the specified {@code original} rectangle to the southwest. The given
+     * {@code ratio} is enforced when indicated by {@code ratioFixed}.
+     * 
+     * @param original
+     *            the original rectangle
+     * @param ratioFixed
+     *            indicates whether the rectangle's ratio will be fixed to the {@code ratio}
+     * @param ratio
+     *            the possibly fixed ratio of the rectangle created by this strategy
+     * @param bounds
+     *            the bounds within which the new rectangle must be contained
+     */
+    public ToSouthwestChangeStrategy(Rectangle2D original, boolean ratioFixed, double ratio, Rectangle2D bounds) {
+        super(ratioFixed, ratio, bounds);
+        northeasternCorner = new Point2D(original.getMaxX(), original.getMinY());
+    }
+
+    // IMPLEMENTATION OF 'AbstractFixedPointChangeStrategy'
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected Point2D getFixedCorner() {
+        return northeasternCorner;
+    }
+
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/ToWestChangeStrategy.java b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/ToWestChangeStrategy.java
new file mode 100644
index 0000000..4500992
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/tools/rectangle/change/ToWestChangeStrategy.java
@@ -0,0 +1,104 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.tools.rectangle.change;
+
+import impl.org.controlsfx.tools.rectangle.Edge2D;
+import javafx.geometry.Orientation;
+import javafx.geometry.Point2D;
+import javafx.geometry.Rectangle2D;
+
+/**
+ * A strategy which enlarges an existing rectangle to the west.
+ */
+public class ToWestChangeStrategy extends AbstractFixedEdgeChangeStrategy {
+
+    /*
+     * The new rectangle will have the existing rectangle's eastern edge as a fixed edge. The parallel edge will
+     * be defined by the current point (modulo the ratio which will be respected if enforced), which is handled by the
+     * superclass.
+     */
+
+    // ATTRIBUTES
+
+    /**
+     * The new rectangle's eastern edge.
+     */
+    private final Edge2D easternEdge;
+
+    // CONSTRUCTOR
+
+    /**
+     * Creates a new change strategy which enlarges the specified {@code original} rectangle to the west. The given
+     * {@code ratio} is enforced when indicated by {@code ratioFixed}.
+     * 
+     * @param original
+     *            the original rectangle
+     * @param ratioFixed
+     *            indicates whether the rectangle's ratio will be fixed to the {@code ratio}
+     * @param ratio
+     *            the possibly fixed ratio of the rectangle created by this strategy
+     * @param bounds
+     *            the bounds within which the rectangle can be resized
+     */
+    public ToWestChangeStrategy(Rectangle2D original, boolean ratioFixed, double ratio, Rectangle2D bounds) {
+        super(ratioFixed, ratio, bounds);
+        Point2D edgeCenterPoint = new Point2D(original.getMaxX(), (original.getMinY() + original.getMaxY()) / 2);
+        easternEdge = new Edge2D(edgeCenterPoint, Orientation.VERTICAL, original.getMaxY() - original.getMinY());
+    }
+
+    /**
+     * Creates a new change strategy which enlarges the specified {@code original} rectangle to the northeast. The given
+     * {@code ratio} is enforced when indicated by {@code ratioFixed}.
+     * 
+     * @param original
+     *            the original rectangle
+     * @param ratioFixed
+     *            indicates whether the rectangle's ratio will be fixed to the {@code ratio}
+     * @param ratio
+     *            the possibly fixed ratio of the rectangle created by this strategy
+     * @param maxX
+     *            the maximal x-coordinate of the right edge of the created rectangles; must be greater than or equal to
+     *            the previous rectangle's width
+     * @param maxY
+     *            the maximal y-coordinate of the lower edge of the created rectangles; must be greater than or equal to
+     *            the previous rectangle's height
+     */
+    public ToWestChangeStrategy(Rectangle2D original, boolean ratioFixed, double ratio, double maxX, double maxY) {
+        this(original, ratioFixed, ratio, new Rectangle2D(0, 0, maxX, maxY));
+    }
+
+    // IMPLEMENTATION OF 'AbstractFixedEdgeChangeStrategy'
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected Edge2D getFixedEdge() {
+        return easternEdge;
+    }
+
+}
diff --git a/controlsfx/src/main/java/impl/org/controlsfx/version/VersionChecker.java b/controlsfx/src/main/java/impl/org/controlsfx/version/VersionChecker.java
new file mode 100644
index 0000000..4e91d0f
--- /dev/null
+++ b/controlsfx/src/main/java/impl/org/controlsfx/version/VersionChecker.java
@@ -0,0 +1,222 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package impl.org.controlsfx.version;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.Properties;
+
+import com.sun.javafx.runtime.VersionInfo;
+
+public class VersionChecker {
+    
+    private static final String javaFXVersion;
+    private static final String controlsFXSpecTitle;
+    private static final String controlsFXSpecVersion;
+    private static final String controlsFXImpVersion;
+    
+    private static final Package controlsFX;
+    
+    private static Properties props;
+    
+    static {
+        controlsFX = VersionChecker.class.getPackage();
+        
+        javaFXVersion         = VersionInfo.getVersion();
+        controlsFXSpecTitle   = getControlsFXSpecificationTitle();
+        controlsFXSpecVersion = getControlsFXSpecificationVersion();
+        controlsFXImpVersion  = getControlsFXImplementationVersion();
+    }
+
+    private VersionChecker() {
+        // no-op
+    }
+    
+    public static void doVersionCheck() {
+        // We keep ControlsFX bleeding edge, and we try to let our version numbers
+        // do the talking. However, we can't always ensure people do the right 
+        // thing, so here we will check the ControlsFX and JavaFX version numbers,
+        // to ensure they match.
+        // Fortunately, our system is simple at present: we use the
+        // 'controlsFXSpec' value to represent what we require. In other
+        // words, ControlsFX 8.0.0 has controlsFXSpecVersion of 8.0.0, so it will work on 
+        // JavaFX 8.0.0 and later versions. Conversely, ControlsFX 8.0.6_20 has a controlsFXSpecVersion of
+        // 8.0.20 (controlsFXSpecTitle of Java 8u20), which means that ControlsFX will only work on JavaFX 8u20
+        // and later versions.
+        
+        if (controlsFXSpecVersion == null) {
+            // FIXME temporary fix to allow ControlsFX to work when run inside
+            // an IDE (i.e. for developers of ControlsFX).
+            return;
+        }
+        
+        Comparable[] splitSpecVersion = toComparable(controlsFXSpecVersion.split("\\.")); //$NON-NLS-1$
+        
+        // javaFXVersion may contain '-' like 8.0.20-ea so replace them with '.' before splitting.
+        Comparable[] splitJavaVersion = toComparable(javaFXVersion.replace('-', '.').split("\\.")); //$NON-NLS-1$
+
+        boolean notSupportedVersion = false;
+
+        // Check Major Version
+        if (splitSpecVersion[0].compareTo(splitJavaVersion[0]) > 0) {
+            notSupportedVersion = true;
+        } else if (splitSpecVersion[0].compareTo(splitJavaVersion[0]) == 0) {
+            // Check Minor Version
+            if (splitSpecVersion[1].compareTo(splitJavaVersion[2])>0) {
+                notSupportedVersion = true;
+            }
+        }
+
+        if (notSupportedVersion) {
+            throw new RuntimeException("ControlsFX Error: ControlsFX " + //$NON-NLS-1$
+                controlsFXImpVersion + " requires at least " + controlsFXSpecTitle); //$NON-NLS-1$
+        }
+    }
+
+    private static Comparable<Comparable>[] toComparable(String[] tokens) {
+        Comparable[] ret= new Comparable[tokens.length];
+        for (int i = 0; i<tokens.length; i++) {
+            String token = tokens[i];
+            try {
+                ret[i] = new Integer(token);
+            }
+            catch (NumberFormatException e) {
+                ret[i] = token;
+            }
+        }
+        return ret;
+    }
+
+    private static String getControlsFXSpecificationTitle() {
+        // firstly try to read from manifest
+        try {
+            return controlsFX.getSpecificationTitle();
+        } catch (NullPointerException e) {
+            // no-op
+        }
+        
+        // try to read it from the controlsfx-build.properties if running
+        // from within an IDE
+        return getPropertyValue("controlsfx_specification_title"); //$NON-NLS-1$
+        
+        
+//        try {
+//            Properties prop = new Properties();
+//            File file = new File("../controlsfx-build.properties");
+//            if (file.exists()) {
+//                prop.load(new FileReader(file));
+//                String version = prop.getProperty("controlsfx_specification_title");
+//                if (version != null && !version.isEmpty()) {
+//                    return version;
+//                }
+//            }
+//        } catch (IOException e) {
+//            // no-op
+//        }
+//        
+//        return null;
+    }
+    
+    private static String getControlsFXSpecificationVersion() {
+        
+        // firstly try to read from manifest
+        try {
+            return controlsFX.getSpecificationVersion();
+        } catch (NullPointerException e) {
+            // no-op
+        }
+        
+        // try to read it from the controlsfx-build.properties if running
+        // from within an IDE
+        return getPropertyValue("controlsfx_specification_title"); //$NON-NLS-1$
+        
+//        try {
+//            Properties prop = new Properties();
+//            File file = new File("../controlsfx-build.properties");
+//            if (file.exists()) {
+//                prop.load(new FileReader(file));
+//                String version = prop.getProperty("controlsfx_specification_version");
+//                if (version != null && !version.isEmpty()) {
+//                    return version;
+//                }
+//            }
+//        } catch (IOException e) {
+//            // no-op
+//        }
+//        
+//        return null;
+    }
+    
+    private static String getControlsFXImplementationVersion() {
+        
+        // firstly try to read from manifest
+        try {
+            return controlsFX.getImplementationVersion();
+        } catch (NullPointerException e) {
+            // no-op
+        }
+        
+        // try to read it from the controlsfx-build.properties if running
+        // from within an IDE
+        
+        return getPropertyValue("controlsfx_specification_title") + //$NON-NLS-1$
+        	   getPropertyValue("artifact_suffix"); //$NON-NLS-1$
+        
+        
+//        try {
+//            Properties prop = new Properties();
+//            File file = new File("../controlsfx-build.properties");
+//            if (file.exists()) {
+//                prop.load(new FileReader(file));
+//                String version = prop.getProperty("controlsfx_version");
+//                if (version != null && !version.isEmpty()) {
+//                    return version;
+//                }
+//            }
+//        } catch (IOException e) {
+//            // no-op
+//        }
+//        
+//        return null;
+    }
+    
+    private static synchronized String getPropertyValue(String key) {
+    	
+    	if ( props == null ) {
+        	try {
+                File file = new File("../controlsfx-build.properties"); //$NON-NLS-1$
+                if (file.exists()) {
+                    props.load(new FileReader(file));
+                }
+            } catch (IOException e) {
+                // no-op
+            }
+    	}
+    	return props.getProperty(key);
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/BreadCrumbBar.java b/controlsfx/src/main/java/org/controlsfx/control/BreadCrumbBar.java
new file mode 100644
index 0000000..38bfe10
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/BreadCrumbBar.java
@@ -0,0 +1,343 @@
+/**
+ * Copyright (c) 2014, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control;
+
+import impl.org.controlsfx.skin.BreadCrumbBarSkin;
+import impl.org.controlsfx.skin.BreadCrumbBarSkin.BreadCrumbButton;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.ObjectPropertyBase;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.event.Event;
+import javafx.event.EventDispatchChain;
+import javafx.event.EventHandler;
+import javafx.event.EventType;
+import javafx.scene.control.Button;
+import javafx.scene.control.Skin;
+import javafx.scene.control.TreeItem;
+import javafx.util.Callback;
+
+import com.sun.javafx.event.EventHandlerManager;
+
+/**
+ * Represents a bread crumb bar. This control is useful to visualize and navigate 
+ * a hierarchical path structure, such as file systems.
+ * 
+ * <p>Shown below is a screenshot of the BreadCrumbBar control:
+ * 
+ * <br>
+ * <center>
+ * <img src="breadCrumbBar.png" alt="Screenshot of BreadCrumbBar">
+ * </center>
+ */
+public class BreadCrumbBar<T> extends ControlsFXControl {
+
+    private final EventHandlerManager eventHandlerManager = new EventHandlerManager(this);
+
+
+    /**
+     * Represents an Event which is fired when a bread crumb was activated.
+     */
+    @SuppressWarnings("serial")
+    public static class BreadCrumbActionEvent<TE> extends Event {
+        
+        /**
+         * The event type that should be listened to by people interested in 
+         * knowing when the {@link BreadCrumbBar#selectedCrumbProperty() selected crumb}
+         * has changed.
+         */
+        @SuppressWarnings("rawtypes")
+        public static final EventType<BreadCrumbActionEvent> CRUMB_ACTION = new EventType<>("CRUMB_ACTION"); //$NON-NLS-1$
+
+        private final TreeItem<TE> selectedCrumb;
+
+        /**
+         * Creates a new event that can subsequently be fired.
+         */
+        public BreadCrumbActionEvent(TreeItem<TE> selectedCrumb) {
+            super(CRUMB_ACTION);
+            this.selectedCrumb = selectedCrumb;
+        }
+
+        /**
+         * Returns the crumb which was the action target.
+         */
+        public TreeItem<TE> getSelectedCrumb() {
+            return selectedCrumb;
+        }
+    }
+    
+    
+    
+    /**
+     * Construct a tree model from the flat list which then can be set 
+     * as selectedCrumb node to be shown 
+     * @param crumbs
+     */
+    public static <T> TreeItem<T> buildTreeModel(@SuppressWarnings("unchecked") T... crumbs){
+        TreeItem<T> subRoot = null;
+        for (T crumb : crumbs) {
+            TreeItem<T> currentNode = new TreeItem<>(crumb);
+            if(subRoot == null){
+                subRoot = currentNode; 
+            }else{
+                subRoot.getChildren().add(currentNode);
+                subRoot = currentNode;
+            }
+        }
+        return subRoot;
+    }
+
+
+    
+    
+    
+    
+    /***************************************************************************
+     * 
+     * Private fields
+     * 
+     **************************************************************************/
+
+
+    /**
+     * Default crumb node factory. This factory is used when no custom factory is specified by the user.
+     */
+    private final Callback<TreeItem<T>, Button> defaultCrumbNodeFactory = new Callback<TreeItem<T>, Button>(){
+        @Override
+        public Button call(TreeItem<T> crumb) {
+            return new BreadCrumbBarSkin.BreadCrumbButton(crumb.getValue() != null ? crumb.getValue().toString() : ""); //$NON-NLS-1$
+        }
+    };
+    
+    
+    
+    
+    /***************************************************************************
+     *                                                                         *
+     * Constructors                                                            *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * Creates an empty bread crumb bar
+     */
+    public BreadCrumbBar(){
+        this(null);
+    }
+
+    /**
+     * Creates a bread crumb bar with the given TreeItem as the currently 
+     * selected crumb.
+     */
+    public BreadCrumbBar(TreeItem<T> selectedCrumb) {
+        getStyleClass().add(DEFAULT_STYLE_CLASS);
+        setSelectedCrumb(selectedCrumb);
+        setCrumbFactory(defaultCrumbNodeFactory);
+    }
+    
+    
+    
+    /***************************************************************************
+     *                                                                         *
+     * Public API                                                              *
+     *                                                                         *
+     **************************************************************************/
+
+    /** {@inheritDoc} */
+    @Override public EventDispatchChain buildEventDispatchChain(EventDispatchChain tail) {
+        return tail.prepend(eventHandlerManager);
+    }
+    
+    
+    
+    /***************************************************************************
+     *                                                                         *
+     * Properties                                                              *
+     *                                                                         *
+     **************************************************************************/
+
+    // --- selectedCrumb
+    /**
+     * Represents the bottom-most path node (the node on the most-right side in 
+     * terms of the bread crumb bar). The full path is then being constructed 
+     * using getParent() of the tree-items.
+     * 
+     * <p>
+     * Consider the following hierarchy:
+     * [Root] > [Folder] > [SubFolder] > [myfile.txt]
+     * 
+     * To show the above bread crumb bar, you have to set the [myfile.txt] tree-node as selected crumb.
+     */
+    public final ObjectProperty<TreeItem<T>> selectedCrumbProperty() {
+        return selectedCrumb;
+    }
+    private final ObjectProperty<TreeItem<T>> selectedCrumb = 
+            new SimpleObjectProperty<>(this, "selectedCrumb"); //$NON-NLS-1$
+    
+    /**
+     * Get the current target path
+     */
+    public final TreeItem<T> getSelectedCrumb() {
+        return selectedCrumb.get();
+    }
+
+    /**
+     * Select one node in the BreadCrumbBar for being the bottom-most path node.
+     * @param selectedCrumb 
+     */
+    public final void setSelectedCrumb(TreeItem<T> selectedCrumb){
+        this.selectedCrumb.set(selectedCrumb);
+    }
+
+    
+    // --- autoNavigation
+    /**
+     * Enable or disable auto navigation (default is enabled).
+     * If auto navigation is enabled, it will automatically navigate to the crumb which was clicked by the user.
+     * @return a {@link BooleanProperty}
+     */
+    public final BooleanProperty autoNavigationEnabledProperty() {
+        return autoNavigation;
+    }
+    
+    private final BooleanProperty autoNavigation = 
+            new SimpleBooleanProperty(this, "autoNavigationEnabled", true); //$NON-NLS-1$
+    
+    /**
+     * Return whether auto-navigation is enabled.
+     * @return whether auto-navigation is enabled.
+     */
+    public final boolean isAutoNavigationEnabled() {
+        return autoNavigation.get();
+    }
+    
+    /**
+     * Enable or disable auto navigation (default is enabled).
+     * If auto navigation is enabled, it will automatically navigate to the crumb which was clicked by the user.
+     * @param enabled 
+     */
+    public final void setAutoNavigationEnabled(boolean enabled) {
+        autoNavigation.set(enabled);
+    }
+
+
+    
+    // --- crumbFactory
+    /**
+     * Return an ObjectProperty of the CrumbFactory.
+     * @return an ObjectProperty of the CrumbFactory.
+     */
+    public final ObjectProperty<Callback<TreeItem<T>, Button>> crumbFactoryProperty() {
+        return crumbFactory;
+    }
+    
+    private final ObjectProperty<Callback<TreeItem<T>, Button>> crumbFactory = 
+            new SimpleObjectProperty<>(this, "crumbFactory"); //$NON-NLS-1$
+
+    /**
+     * Sets the crumb factory to create (custom) {@link BreadCrumbButton} instances.
+     * <code>null</code> is not allowed and will result in a fall back to the default factory.
+     * @param value 
+     */
+    public final void setCrumbFactory(Callback<TreeItem<T>, Button> value) {
+        if(value == null){
+            value = defaultCrumbNodeFactory;
+        }
+        crumbFactoryProperty().set(value);
+    }
+
+    /**
+     * Returns the cell factory that will be used to create {@link BreadCrumbButton} 
+     * instances
+     */
+    public final Callback<TreeItem<T>, Button> getCrumbFactory() {
+        return crumbFactory.get();
+    }
+
+    
+    // --- onCrumbAction
+    /**
+     * @return an ObjectProperty representing the crumbAction EventHandler being used.
+     */
+    public final ObjectProperty<EventHandler<BreadCrumbActionEvent<T>>> onCrumbActionProperty() { 
+        return onCrumbAction; 
+    }
+    
+    /**
+     * Set a new EventHandler for when a user selects a crumb.
+     * @param value 
+     */
+    public final void setOnCrumbAction(EventHandler<BreadCrumbActionEvent<T>> value) {
+        onCrumbActionProperty().set(value); 
+    }
+    
+    /**
+     * Return the EventHandler currently used when a user selects a crumb.
+     * @return the EventHandler currently used when a user selects a crumb.
+     */
+    public final EventHandler<BreadCrumbActionEvent<T>> getOnCrumbAction() { 
+        return onCrumbActionProperty().get(); 
+    }
+    
+    private ObjectProperty<EventHandler<BreadCrumbActionEvent<T>>> onCrumbAction = new ObjectPropertyBase<EventHandler<BreadCrumbBar.BreadCrumbActionEvent<T>>>() {
+        @SuppressWarnings({ "rawtypes", "unchecked" })
+        @Override protected void invalidated() {
+            eventHandlerManager.setEventHandler(BreadCrumbActionEvent.CRUMB_ACTION, (EventHandler<BreadCrumbActionEvent>)(Object)get());
+        }
+
+        @Override
+        public Object getBean() {
+            return BreadCrumbBar.this;
+        }
+
+        @Override
+        public String getName() {
+            return "onCrumbAction"; //$NON-NLS-1$
+        }
+    };
+    
+
+    /***************************************************************************
+     *                                                                         *
+     * Stylesheet Handling                                                     *
+     *                                                                         *
+     **************************************************************************/
+
+    private static final String DEFAULT_STYLE_CLASS = "bread-crumb-bar"; //$NON-NLS-1$
+
+    /** {@inheritDoc} */
+    @Override protected Skin<?> createDefaultSkin() {
+        return new BreadCrumbBarSkin<>(this);
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getUserAgentStylesheet() {
+        return getUserAgentStylesheet(BreadCrumbBar.class, "breadcrumbbar.css");
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/CheckBitSetModelBase.java b/controlsfx/src/main/java/org/controlsfx/control/CheckBitSetModelBase.java
new file mode 100644
index 0000000..0ba976f
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/CheckBitSetModelBase.java
@@ -0,0 +1,315 @@
+/**
+ * Copyright (c) 2013, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control;
+
+import java.util.BitSet;
+import java.util.Map;
+
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+
+import com.sun.javafx.collections.MappingChange;
+import com.sun.javafx.collections.NonIterableChange;
+import com.sun.javafx.scene.control.ReadOnlyUnbackedObservableList;
+
+// not public API
+abstract class CheckBitSetModelBase<T> implements IndexedCheckModel<T> { 
+    
+    /***********************************************************************
+     *                                                                     *
+     * Internal properties                                                 *
+     *                                                                     *
+     **********************************************************************/
+
+    private final Map<T, BooleanProperty> itemBooleanMap;
+    
+    private final BitSet checkedIndices;
+    private final ReadOnlyUnbackedObservableList<Integer> checkedIndicesList;
+    private final ReadOnlyUnbackedObservableList<T> checkedItemsList;
+    
+    
+
+    /***********************************************************************
+     *                                                                     *
+     * Constructors                                                        *
+     *                                                                     *
+     **********************************************************************/
+
+    CheckBitSetModelBase(final Map<T, BooleanProperty> itemBooleanMap) {
+        this.itemBooleanMap = itemBooleanMap;
+        
+        this.checkedIndices = new BitSet();
+        
+        this.checkedIndicesList = new ReadOnlyUnbackedObservableList<Integer>() {
+            @Override public Integer get(int index) {
+                if (index < 0 || index >= getItemCount()) return -1;
+
+                for (int pos = 0, val = checkedIndices.nextSetBit(0);
+                    val >= 0 || pos == index;
+                    pos++, val = checkedIndices.nextSetBit(val+1)) {
+                        if (pos == index) return val;
+                }
+
+                return -1;
+            }
+
+            @Override public int size() {
+                return checkedIndices.cardinality();
+            }
+
+            @Override public boolean contains(Object o) {
+                if (o instanceof Number) {
+                    Number n = (Number) o;
+                    int index = n.intValue();
+
+                    return index >= 0 && index < checkedIndices.length() &&
+                            checkedIndices.get(index);
+                }
+
+                return false;
+            }
+        };
+        
+        this.checkedItemsList = new ReadOnlyUnbackedObservableList<T>() {
+            @Override public T get(int i) {
+                int pos = checkedIndicesList.get(i);
+                if (pos < 0 || pos >= getItemCount()) return null;
+                return getItem(pos);
+            }
+
+            @Override public int size() {
+                return checkedIndices.cardinality();
+            }
+        };
+        
+        final MappingChange.Map<Integer,T> map = f -> getItem(f);
+        
+        checkedIndicesList.addListener(new ListChangeListener<Integer>() {
+            @Override public void onChanged(final Change<? extends Integer> c) {
+                // when the selectedIndices ObservableList changes, we manually call
+                // the observers of the selectedItems ObservableList.
+                boolean hasRealChangeOccurred = false;
+                while (c.next() && ! hasRealChangeOccurred) {
+                    hasRealChangeOccurred = c.wasAdded() || c.wasRemoved();
+                }
+
+                if (hasRealChangeOccurred) {
+                    c.reset();
+                    checkedItemsList.callObservers(new MappingChange<>(c, map, checkedItemsList));
+                }
+                c.reset();
+            }
+        });
+        
+        // this code is to handle the situation where a developer is manually
+        // toggling the check model, and expecting the UI to update (without
+        // this it won't happen!).
+        getCheckedItems().addListener(new ListChangeListener<T>() {
+            @Override public void onChanged(ListChangeListener.Change<? extends T> c) {
+                while (c.next()) {
+                    if (c.wasAdded()) {
+                        for (T item : c.getAddedSubList()) {
+                            BooleanProperty p = getItemBooleanProperty(item);
+                            if (p != null) {
+                                p.set(true);
+                            }
+                        }
+                    } 
+                    
+                    if (c.wasRemoved()) {
+                        for (T item : c.getRemoved()) {
+                            BooleanProperty p = getItemBooleanProperty(item);
+                            if (p != null) {
+                                p.set(false);
+                            }
+                        }
+                    }
+                }
+            }
+        });
+    }
+    
+    
+    
+    /***********************************************************************
+     *                                                                     *
+     * Abstract API                                                        *
+     *                                                                     *
+     **********************************************************************/
+
+    @Override
+    public abstract T getItem(int index);
+    
+    @Override
+    public abstract int getItemCount();
+    
+    @Override
+    public abstract int getItemIndex(T item);
+    
+    BooleanProperty getItemBooleanProperty(T item) {
+        return itemBooleanMap.get(item);
+    }
+    
+    
+    /***********************************************************************
+     *                                                                     *
+     * Public selection API                                                *
+     *                                                                     *
+     **********************************************************************/
+    
+    /**
+     * Returns a read-only list of the currently checked indices in the CheckBox.
+     */
+    @Override
+    public ObservableList<Integer> getCheckedIndices() {
+        return checkedIndicesList;
+    }
+    
+    /**
+     * Returns a read-only list of the currently checked items in the CheckBox.
+     */
+    @Override
+    public ObservableList<T> getCheckedItems() {
+        return checkedItemsList;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void checkAll() {
+        for (int i = 0; i < getItemCount(); i++) {
+            check(i);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void checkIndices(int... indices) {
+        for (int i = 0; i < indices.length; i++) {
+            check(indices[i]);
+        }
+    }
+    
+    /** {@inheritDoc} */
+    @Override public void clearCheck(T item) {
+        int index = getItemIndex(item);
+        clearCheck(index);        
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void clearChecks() {
+        for( int index = 0; index < checkedIndices.length(); index++) {
+            clearCheck(index);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void clearCheck(int index) {
+        if (index < 0 || index >= getItemCount()) return;
+        checkedIndices.clear(index);
+        
+        final int changeIndex = checkedIndicesList.indexOf(index);
+        checkedIndicesList.callObservers(new NonIterableChange.SimpleRemovedChange<>(changeIndex, changeIndex, index, checkedIndicesList));
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    public boolean isEmpty() {
+        return checkedIndices.isEmpty();
+    }
+    
+    /** {@inheritDoc} */
+    @Override public boolean isChecked(T item) {
+        int index = getItemIndex(item);
+        return isChecked(index);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isChecked(int index) {
+        return checkedIndices.get(index);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void check(int index) {
+        if (index < 0 || index >= getItemCount()) return;
+        checkedIndices.set(index);
+        final int changeIndex = checkedIndicesList.indexOf(index);
+        checkedIndicesList.callObservers(new NonIterableChange.SimpleAddChange<>(changeIndex, changeIndex+1, checkedIndicesList));
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void check(T item) {
+        int index = getItemIndex(item);
+        check(index);
+    }
+
+    
+    
+    
+    /***********************************************************************
+     *                                                                     *
+     * Private implementation                                              *
+     *                                                                     *
+     **********************************************************************/
+    
+    protected void updateMap() {
+        // reset the map
+        itemBooleanMap.clear();
+        for (int i = 0; i < getItemCount(); i++) {
+            final int index = i;
+            final T item = getItem(index);
+            
+            final BooleanProperty booleanProperty = new SimpleBooleanProperty(item, "selected", false); //$NON-NLS-1$
+            itemBooleanMap.put(item, booleanProperty);
+            
+            // this is where we listen to changes to the boolean properties,
+            // updating the selected indices list (and therefore indirectly
+            // the selected items list) when the checkbox is toggled
+            booleanProperty.addListener(new InvalidationListener() {
+                @Override public void invalidated(Observable o) {
+                    if (booleanProperty.get()) {
+                        checkedIndices.set(index);
+                        final int changeIndex = checkedIndicesList.indexOf(index);
+                        checkedIndicesList.callObservers(new NonIterableChange.SimpleAddChange<>(changeIndex, changeIndex+1, checkedIndicesList));                            
+                    } else {
+                        final int changeIndex = checkedIndicesList.indexOf(index);
+                        checkedIndices.clear(index);
+                        checkedIndicesList.callObservers(new NonIterableChange.SimpleRemovedChange<>(changeIndex, changeIndex, index, checkedIndicesList));
+                    }
+                }
+            });
+        }
+    }
+}
\ No newline at end of file
diff --git a/controlsfx/src/main/java/org/controlsfx/control/CheckComboBox.java b/controlsfx/src/main/java/org/controlsfx/control/CheckComboBox.java
new file mode 100644
index 0000000..86f175e
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/CheckComboBox.java
@@ -0,0 +1,297 @@
+/**
+ * Copyright (c) 2013, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control;
+
+import impl.org.controlsfx.skin.CheckComboBoxSkin;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.Skin;
+import javafx.util.StringConverter;
+
+/**
+ * A simple UI control that makes it possible to select zero or more items within
+ * a ComboBox-like control. Each row item shows a {@link CheckBox}, and the state
+ * of each row can be queried via the {@link #checkModelProperty() check model}.
+ * 
+ * <h3>Screenshot</h3>
+ * <p>The following screenshot shows the CheckComboBox with some sample data:
+ * 
+ * <br>
+ * <img src="checkComboBox.png" alt="Screenshot of CheckComboBox">
+ * 
+ * <h3>Code Example:</h3>
+ * <p>To create the CheckComboBox shown in the screenshot, simply do the 
+ * following:
+ * 
+ * <pre>
+ * {@code
+ * // create the data to show in the CheckComboBox 
+ * final ObservableList<String> strings = FXCollections.observableArrayList();
+ * for (int i = 0; i <= 100; i++) {
+ *     strings.add("Item " + i);
+ * }
+ * 
+ * // Create the CheckComboBox with the data 
+ * final CheckComboBox<String> checkComboBox = new CheckComboBox<String>(strings);
+ * 
+ * // and listen to the relevant events (e.g. when the selected indices or 
+ * // selected items change).
+ * checkComboBox.getCheckModel().getSelectedItems().addListener(new ListChangeListener<String>() {
+ *     public void onChanged(ListChangeListener.Change<? extends String> c) {
+ *         System.out.println(checkComboBox.getCheckModel().getSelectedItems());
+ *     }
+ * });}
+ * }</pre>
+ *
+ * @param <T> The type of the data in the ComboBox.
+ */
+public class CheckComboBox<T> extends ControlsFXControl {
+    
+    /**************************************************************************
+     * 
+     * Private fields
+     * 
+     **************************************************************************/
+    
+    private final ObservableList<T> items;
+    private final Map<T, BooleanProperty> itemBooleanMap;
+    
+
+    
+    /**************************************************************************
+     * 
+     * Constructors
+     * 
+     **************************************************************************/
+    
+    /**
+     * Creates a new CheckComboBox instance with an empty list of choices.
+     */
+    public CheckComboBox() {
+        this(null);
+    }
+    
+    /**
+     * Creates a new CheckComboBox instance with the given items available as
+     * choices.
+     * 
+     * @param items The items to display within the CheckComboBox.
+     */
+    public CheckComboBox(final ObservableList<T> items) {
+        final int initialSize = items == null ? 32 : items.size();
+        
+        this.itemBooleanMap = new HashMap<>(initialSize);
+        this.items = items == null ? FXCollections.<T>observableArrayList() : items;
+        setCheckModel(new CheckComboBoxBitSetCheckModel<>(this.items, itemBooleanMap));
+    }
+    
+    
+    
+    /**************************************************************************
+     * 
+     * Public API
+     * 
+     **************************************************************************/
+    
+    /** {@inheritDoc} */
+    @Override protected Skin<?> createDefaultSkin() {
+        return new CheckComboBoxSkin<>(this);
+    }
+    
+    /**
+     * Represents the list of choices available to the user, from which they can
+     * select zero or more items.
+     */
+    public ObservableList<T> getItems() {
+        return items;
+    }
+    
+    /**
+     * Returns the {@link BooleanProperty} for a given item index in the 
+     * CheckComboBox. This is useful if you want to bind to the property.
+     */
+    public BooleanProperty getItemBooleanProperty(int index) {
+        if (index < 0 || index >= items.size()) return null;
+        return getItemBooleanProperty(getItems().get(index));
+    }
+    
+    /**
+     * Returns the {@link BooleanProperty} for a given item in the 
+     * CheckComboBox. This is useful if you want to bind to the property.
+     */
+    public BooleanProperty getItemBooleanProperty(T item) {
+        return itemBooleanMap.get(item);
+    }
+    
+    
+    
+    /**************************************************************************
+     * 
+     * Properties
+     * 
+     **************************************************************************/
+
+    // --- Check Model
+    private ObjectProperty<IndexedCheckModel<T>> checkModel = 
+            new SimpleObjectProperty<>(this, "checkModel"); //$NON-NLS-1$
+    
+    /**
+     * Sets the 'check model' to be used in the CheckComboBox - this is the
+     * code that is responsible for representing the selected state of each
+     * {@link CheckBox} - that is, whether each {@link CheckBox} is checked or 
+     * not (and not to be confused with the 
+     * selection model concept, which is used in the ComboBox control to 
+     * represent the selection state of each row).. 
+     */
+    public final void setCheckModel(IndexedCheckModel<T> value) {
+        checkModelProperty().set(value);
+    }
+
+    /**
+     * Returns the currently installed check model.
+     */
+    public final IndexedCheckModel<T> getCheckModel() {
+        return checkModel == null ? null : checkModel.get();
+    }
+
+    /**
+     * The check model provides the API through which it is possible
+     * to check single or multiple items within a CheckComboBox, as  well as inspect
+     * which items have been checked by the user. Note that it has a generic
+     * type that must match the type of the CheckComboBox itself.
+     */
+    public final ObjectProperty<IndexedCheckModel<T>> checkModelProperty() {
+        return checkModel;
+    }
+    
+    // --- converter
+    private ObjectProperty<StringConverter<T>> converter = 
+            new SimpleObjectProperty<StringConverter<T>>(this, "converter");
+    
+    /**
+     * A {@link StringConverter} that, given an object of type T, will 
+     * return a String that can be used to represent the object visually.
+     */
+    public final ObjectProperty<StringConverter<T>> converterProperty() { 
+        return converter; 
+    }
+    
+    /** 
+     * Sets the {@link StringConverter} to be used in the control.
+     * @param value A {@link StringConverter} that, given an object of type T, will 
+     * return a String that can be used to represent the object visually.
+     */
+    public final void setConverter(StringConverter<T> value) { 
+        converterProperty().set(value); 
+    }
+    
+    /**
+     * A {@link StringConverter} that, given an object of type T, will 
+     * return a String that can be used to represent the object visually.
+     */
+    public final StringConverter<T> getConverter() { 
+        return converterProperty().get(); 
+    }
+    
+    
+    
+    /**************************************************************************
+     * 
+     * Implementation
+     * 
+     **************************************************************************/
+    
+    
+    
+    
+    /**************************************************************************
+     * 
+     * Support classes
+     * 
+     **************************************************************************/
+    
+    private static class CheckComboBoxBitSetCheckModel<T> extends CheckBitSetModelBase<T> {
+        
+        /***********************************************************************
+         *                                                                     *
+         * Internal properties                                                 *
+         *                                                                     *
+         **********************************************************************/
+        
+        private final ObservableList<T> items;
+        
+        
+        
+        /***********************************************************************
+         *                                                                     *
+         * Constructors                                                        *
+         *                                                                     *
+         **********************************************************************/
+        
+        CheckComboBoxBitSetCheckModel(final ObservableList<T> items, final Map<T, BooleanProperty> itemBooleanMap) {
+            super(itemBooleanMap);
+            
+            this.items = items;
+            this.items.addListener(new ListChangeListener<T>() {
+                @Override public void onChanged(Change<? extends T> c) {
+                    updateMap();
+                }
+            });
+            
+            updateMap();
+        }
+        
+        
+        
+        /***********************************************************************
+         *                                                                     *
+         * Implementing abstract API                                           *
+         *                                                                     *
+         **********************************************************************/
+
+        @Override public T getItem(int index) {
+            return items.get(index);
+        }
+        
+        @Override public int getItemCount() {
+            return items.size();
+        }
+        
+        @Override public int getItemIndex(T item) {
+            return items.indexOf(item);
+        }
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/CheckListView.java b/controlsfx/src/main/java/org/controlsfx/control/CheckListView.java
new file mode 100644
index 0000000..1ec23b0
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/CheckListView.java
@@ -0,0 +1,264 @@
+/**
+ * Copyright (c) 2013, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.FXCollections;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.ListView;
+import javafx.scene.control.cell.CheckBoxListCell;
+import javafx.util.Callback;
+
+/**
+ * A simple UI control that makes it possible to select zero or more items within
+ * a ListView without the need to set a custom cell factory or manually create
+ * boolean properties for each row - simply use the 
+ * {@link #checkModelProperty() check model} to request the current selection 
+ * state.
+ * 
+ * <h3>Screenshot</h3>
+ * <p>The following screenshot shows the CheckListView with some sample data:
+ * 
+ * <br>
+ * <img src="checkListView.png" alt="Screenshot of CheckListView">
+ * 
+ * <h3>Code Example:</h3>
+ * <p>To create the CheckListView shown in the screenshot, simply do the 
+ * following:
+ * 
+ * <pre>
+ * {@code
+ * // create the data to show in the CheckListView 
+ * final ObservableList<String> strings = FXCollections.observableArrayList();
+ * for (int i = 0; i <= 100; i++) {
+ *     strings.add("Item " + i);
+ * }
+ * 
+ * // Create the CheckListView with the data 
+ * final CheckListView<String> checkListView = new CheckListView<>(strings);
+ *       
+ * // and listen to the relevant events (e.g. when the selected indices or 
+ * // selected items change).
+ * checkListView.getCheckModel().getCheckedItems().addListener(new ListChangeListener<String>() {
+ *     public void onChanged(ListChangeListener.Change<? extends String> c) {
+ *         System.out.println(checkListView.getCheckModel().getCheckedItems());
+ *     }
+ * });
+ * }</pre>
+ *
+ * @param <T> The type of the data in the CheckListView.
+ */
+public class CheckListView<T> extends ListView<T> {
+    
+    /**************************************************************************
+     * 
+     * Private fields
+     * 
+     **************************************************************************/
+    
+    private final Map<T, BooleanProperty> itemBooleanMap;
+    
+
+    
+    /**************************************************************************
+     * 
+     * Constructors
+     * 
+     **************************************************************************/
+    
+    /**
+     * Creates a new CheckListView instance with an empty list of choices.
+     */
+    public CheckListView() {
+        this(FXCollections.<T> observableArrayList());
+    }
+    
+    /**
+     * Creates a new CheckListView instance with the given items available as
+     * choices.
+     * 
+     * @param items The items to display within the CheckListView.
+     */
+    public CheckListView(ObservableList<T> items) {
+        super(items);
+        this.itemBooleanMap = new HashMap<>();
+        
+        setCheckModel(new CheckListViewBitSetCheckModel<>(getItems(), itemBooleanMap));
+        itemsProperty().addListener(ov -> {
+            setCheckModel(new CheckListViewBitSetCheckModel<>(getItems(), itemBooleanMap));
+        });
+        
+        setCellFactory(listView -> new CheckBoxListCell<>(new Callback<T, ObservableValue<Boolean>>() {
+            @Override public ObservableValue<Boolean> call(T item) {
+                return getItemBooleanProperty(item);
+            }
+        }));
+    }
+    
+    
+    
+    /**************************************************************************
+     * 
+     * Public API
+     * 
+     **************************************************************************/
+    
+    /**
+     * Returns the {@link BooleanProperty} for a given item index in the 
+     * CheckListView. This is useful if you want to bind to the property.
+     */
+    public BooleanProperty getItemBooleanProperty(int index) {
+        if (index < 0 || index >= getItems().size()) return null;
+        return getItemBooleanProperty(getItems().get(index));
+    }
+    
+    /**
+     * Returns the {@link BooleanProperty} for a given item in the 
+     * CheckListView. This is useful if you want to bind to the property.
+     */
+    public BooleanProperty getItemBooleanProperty(T item) {
+        return itemBooleanMap.get(item);
+    }
+    
+    
+    
+    /**************************************************************************
+     * 
+     * Properties
+     * 
+     **************************************************************************/
+
+    // --- Check Model
+    private ObjectProperty<IndexedCheckModel<T>> checkModel = 
+            new SimpleObjectProperty<>(this, "checkModel"); //$NON-NLS-1$
+    
+    /**
+     * Sets the 'check model' to be used in the CheckListView - this is the
+     * code that is responsible for representing the selected state of each
+     * {@link CheckBox} - that is, whether each {@link CheckBox} is checked or 
+     * not (and not to be confused with the 
+     * selection model concept, which is used in the ListView control to 
+     * represent the selection state of each row).. 
+     */
+    public final void setCheckModel(IndexedCheckModel<T> value) {
+        checkModelProperty().set(value);
+    }
+
+    /**
+     * Returns the currently installed check model.
+     */
+    public final IndexedCheckModel<T> getCheckModel() {
+        return checkModel == null ? null : checkModel.get();
+    }
+
+    /**
+     * The check model provides the API through which it is possible
+     * to check single or multiple items within a CheckListView, as  well as inspect
+     * which items have been checked by the user. Note that it has a generic
+     * type that must match the type of the CheckListView itself.
+     */
+    public final ObjectProperty<IndexedCheckModel<T>> checkModelProperty() {
+        return checkModel;
+    }
+    
+    
+    
+    /**************************************************************************
+     * 
+     * Implementation
+     * 
+     **************************************************************************/
+    
+    
+    
+    
+    /**************************************************************************
+     * 
+     * Support classes
+     * 
+     **************************************************************************/
+    
+    private static class CheckListViewBitSetCheckModel<T> extends CheckBitSetModelBase<T> {
+        
+        /***********************************************************************
+         *                                                                     *
+         * Internal properties                                                 *
+         *                                                                     *
+         **********************************************************************/
+        
+        private final ObservableList<T> items;
+        
+        
+        
+        /***********************************************************************
+         *                                                                     *
+         * Constructors                                                        *
+         *                                                                     *
+         **********************************************************************/
+        
+        CheckListViewBitSetCheckModel(final ObservableList<T> items, final Map<T, BooleanProperty> itemBooleanMap) {
+            super(itemBooleanMap);
+            
+            this.items = items;
+            this.items.addListener(new ListChangeListener<T>() {
+                @Override public void onChanged(Change<? extends T> c) {
+                    updateMap();
+                }
+            });
+            
+            updateMap();
+        }
+        
+        
+        
+        /***********************************************************************
+         *                                                                     *
+         * Implementing abstract API                                           *
+         *                                                                     *
+         **********************************************************************/
+
+        @Override public T getItem(int index) {
+            return items.get(index);
+        }
+        
+        @Override public int getItemCount() {
+            return items.size();
+        }
+        
+        @Override public int getItemIndex(T item) {
+            return items.indexOf(item);
+        }
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/CheckModel.java b/controlsfx/src/main/java/org/controlsfx/control/CheckModel.java
new file mode 100644
index 0000000..c305ae2
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/CheckModel.java
@@ -0,0 +1,66 @@
+/**
+ * Copyright (c) 2014, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control;
+
+import javafx.collections.ObservableList;
+
+public interface CheckModel<T> {
+    
+    /**
+     * Returns the count of items in the control.
+     */
+    public int getItemCount();
+
+    /**
+     * Returns a read-only list of the currently checked items in the control.
+     */
+    public ObservableList<T> getCheckedItems();
+
+    /**
+     * Checks all items in the control
+     */
+    public void checkAll();
+    
+    public void clearCheck(T item);
+    
+    /**
+     * Unchecks all items in the control
+     */
+    public void clearChecks();
+    
+    /**
+     * Returns true if there are no checked items in the control.
+     */
+    public boolean isEmpty();
+    
+    public boolean isChecked(T item);
+    
+    /**
+     * Checks the given item in the control.
+     */
+    public void check(T item);
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/CheckTreeView.java b/controlsfx/src/main/java/org/controlsfx/control/CheckTreeView.java
new file mode 100644
index 0000000..d8b8689
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/CheckTreeView.java
@@ -0,0 +1,327 @@
+/**
+ * Copyright (c) 2013, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.CheckBoxTreeItem;
+import javafx.scene.control.TreeItem;
+import javafx.scene.control.TreeView;
+import javafx.scene.control.cell.CheckBoxTreeCell;
+
+/**
+ * A simple UI control that makes it possible to select zero or more items within
+ * a TreeView without the need to set a custom cell factory or manually create
+ * boolean properties for each row - simply use the 
+ * {@link #checkModelProperty() check model} to request the current selection 
+ * state.
+ * 
+ * <h3>Screenshot</h3>
+ * <p>The following screenshot shows the CheckTreeView with some sample data:
+ * 
+ * <br>
+ * <img src="checkTreeView.png" alt="Screenshot of CheckTreeView">
+ * 
+ * <h3>Code Example:</h3>
+ * <p>To create the CheckTreeView shown in the screenshot, simply do the 
+ * following:
+ * 
+ * <pre>
+ * {@code
+ * // create the data to show in the CheckTreeView 
+ * CheckBoxTreeItem<String> root = new CheckBoxTreeItem<String>("Root");
+ * root.setExpanded(true);
+ * root.getChildren().addAll(
+ *               new CheckBoxTreeItem<String>("Jonathan"),
+ *               new CheckBoxTreeItem<String>("Eugene"),
+ *               new CheckBoxTreeItem<String>("Henri"),
+ *               new CheckBoxTreeItem<String>("Samir"));
+ * 
+ * // Create the CheckTreeView with the data 
+ * final CheckTreeView<String> checkTreeView = new CheckTreeView<>(root);
+ *       
+ * // and listen to the relevant events (e.g. when the checked items change).
+ * checkTreeView.getCheckModel().getCheckedItems().addListener(new ListChangeListener<TreeItem<String>>() {
+ *      public void onChanged(ListChangeListener.Change<? extends TreeItem<String>> c) {
+ *          System.out.println(checkTreeView.getCheckModel().getCheckedItems());
+ *      }
+ * });
+ * }</pre>
+ *
+ * @param <T> The type of the data in the TreeView.
+ */
+public class CheckTreeView<T> extends TreeView<T> {
+    
+    /**************************************************************************
+     * 
+     * Private fields
+     * 
+     **************************************************************************/
+    
+    
+
+    
+    /**************************************************************************
+     * 
+     * Constructors
+     * 
+     **************************************************************************/
+    
+    /**
+     * Creates a new CheckTreeView instance with an empty tree of choices.
+     */
+    public CheckTreeView() {
+        this(null);
+    }
+    
+    /**
+     * Creates a new CheckTreeView instance with the given CheckBoxTreeItem set
+     * as the tree root.
+     * 
+     * @param root The root tree item to display in the CheckTreeView.
+     */
+    public CheckTreeView(final CheckBoxTreeItem<T> root) {
+        super(root);
+        rootProperty().addListener(o -> updateCheckModel());
+
+        updateCheckModel();
+
+        setCellFactory(CheckBoxTreeCell.<T> forTreeView());
+    }
+    
+    protected void updateCheckModel() {
+        if (getRoot() != null) {        
+            setCheckModel(new CheckTreeViewCheckModel<>(this));
+        }
+    }
+    
+    
+    /**************************************************************************
+     * 
+     * Public API
+     * 
+     **************************************************************************/
+    
+    /**
+     * Returns the {@link BooleanProperty} for a given item index in the 
+     * CheckTreeView. This is useful if you want to bind to the property.
+     */
+    public BooleanProperty getItemBooleanProperty(int index) {
+        CheckBoxTreeItem<T> treeItem = (CheckBoxTreeItem<T>) getTreeItem(index);
+        return treeItem.selectedProperty();
+    }
+    
+    
+    
+    /**************************************************************************
+     * 
+     * Properties
+     * 
+     **************************************************************************/
+
+    // --- Check Model
+    private ObjectProperty<CheckModel<TreeItem<T>>> checkModel = 
+            new SimpleObjectProperty<>(this, "checkModel"); //$NON-NLS-1$
+    
+    /**
+     * Sets the 'check model' to be used in the CheckTreeView - this is the
+     * code that is responsible for representing the selected state of each
+     * {@link CheckBox} - that is, whether each {@link CheckBox} is checked or 
+     * not (and not to be confused with the 
+     * selection model concept, which is used in the TreeView control to 
+     * represent the selection state of each row).. 
+     */
+    public final void setCheckModel(CheckModel<TreeItem<T>> value) {
+        checkModelProperty().set(value);
+    }
+
+    /**
+     * Returns the currently installed check model.
+     */
+    public final CheckModel<TreeItem<T>> getCheckModel() {
+        return checkModel == null ? null : checkModel.get();
+    }
+
+    /**
+     * The check model provides the API through which it is possible
+     * to check single or multiple items within a CheckTreeView, as  well as inspect
+     * which items have been checked by the user. Note that it has a generic
+     * type that must match the type of the CheckTreeView itself.
+     */
+    public final ObjectProperty<CheckModel<TreeItem<T>>> checkModelProperty() {
+        return checkModel;
+    }
+    
+    
+    
+    /**************************************************************************
+     * 
+     * Implementation
+     * 
+     **************************************************************************/
+    
+    
+    
+    
+    /**************************************************************************
+     * 
+     * Support classes
+     * 
+     **************************************************************************/
+    
+    private static class CheckTreeViewCheckModel<T> implements CheckModel<TreeItem<T>> {// extends CheckBitSetModelBase<TreeItem<T>> {
+        
+        /***********************************************************************
+         *                                                                     *
+         * Internal properties                                                 *
+         *                                                                     *
+         **********************************************************************/
+        
+        private final CheckTreeView<T> treeView;
+        private final TreeItem<T> root;
+        
+        private ObservableList<TreeItem<T>> checkedItems = FXCollections.observableArrayList();
+        
+        
+        
+        /***********************************************************************
+         *                                                                     *
+         * Constructors                                                        *
+         *                                                                     *
+         **********************************************************************/
+        
+        CheckTreeViewCheckModel(final CheckTreeView<T> treeView) {
+            this.treeView = treeView;
+            this.root = treeView.getRoot();
+            this.root.addEventHandler(CheckBoxTreeItem.<T>checkBoxSelectionChangedEvent(), e -> {
+                CheckBoxTreeItem<T> treeItem = e.getTreeItem();
+                
+                if (treeItem.isSelected()) { // && ! treeItem.isIndeterminate()) {
+                    check(treeItem);
+                } else { 
+                    clearCheck(treeItem);
+                }
+            });
+            
+            // we should reset the check model and then update the checked items
+            // based on the currently checked items in the tree view
+            clearChecks();
+            for (int i = 0; i < treeView.getExpandedItemCount(); i++) {
+                CheckBoxTreeItem<T> treeItem = (CheckBoxTreeItem<T>) treeView.getTreeItem(i);
+                if (treeItem.isSelected() && ! treeItem.isIndeterminate()) {
+                    check(treeItem);
+                }
+            }
+        }
+        
+        
+        
+        /***********************************************************************
+         *                                                                     *
+         * Implementing abstract API                                           *
+         *                                                                     *
+         **********************************************************************/
+
+        @Override public int getItemCount() {
+            return treeView.getExpandedItemCount();
+        }
+
+
+        // TODO make read-only
+        @Override public ObservableList<TreeItem<T>> getCheckedItems() {
+            return checkedItems;
+        }
+
+        @Override public void checkAll() {
+            iterateOverTree(this::check);
+        }
+
+        @Override public void clearCheck(TreeItem<T> item) {
+            if (item instanceof CheckBoxTreeItem) {
+                ((CheckBoxTreeItem<T>)item).setSelected(false);
+            }
+            checkedItems.remove(item);
+        }
+
+        @Override public void clearChecks() {
+            List<TreeItem<T>> items = new ArrayList<>(checkedItems);
+            for(TreeItem<T> item : items){
+                clearCheck(item);
+            }
+        }
+
+        @Override public boolean isEmpty() {
+            return checkedItems.isEmpty();
+        }
+
+        @Override public boolean isChecked(TreeItem<T> item) {
+            return checkedItems.contains(item);
+        }
+
+        @Override public void check(TreeItem<T> item) {
+            if (item instanceof CheckBoxTreeItem) {
+                ((CheckBoxTreeItem<T>)item).setSelected(true);
+            }
+            if (!checkedItems.contains(item)) {
+                checkedItems.add(item);
+            }
+        }
+        
+        
+        
+        /***********************************************************************
+         *                                                                     *
+         * Private Implementation                                              *
+         *                                                                     *
+         **********************************************************************/
+        
+        private void iterateOverTree(Consumer<TreeItem<T>> consumer) {
+            processNode(consumer, root);
+        }
+        
+        private void processNode(Consumer<TreeItem<T>> consumer, TreeItem<T> node) {
+            if (node == null) return;
+            consumer.accept(node);
+            processChildren(consumer, node.getChildren());
+        }
+        
+        private void processChildren(Consumer<TreeItem<T>> consumer, List<TreeItem<T>> children) {
+            if (children == null) return;
+            for (TreeItem<T> child : children) {
+                processNode(consumer, child);
+            }
+        }
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/ControlsFXControl.java b/controlsfx/src/main/java/org/controlsfx/control/ControlsFXControl.java
new file mode 100644
index 0000000..15b3057
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/ControlsFXControl.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control;
+
+import impl.org.controlsfx.version.VersionChecker;
+import javafx.scene.control.Control;
+
+abstract class ControlsFXControl extends Control {
+
+    public ControlsFXControl() {
+        VersionChecker.doVersionCheck();
+    }
+
+    private String stylesheet;
+
+    /**
+     * A helper method that ensures that the resource based lookup of the user
+     * agent stylesheet only happens once. Caches the external form of the
+     * resource.
+     *
+     * @param clazz
+     *            the class used for the resource lookup
+     * @param fileName
+     *            the name of the user agent stylesheet
+     * @return the external form of the user agent stylesheet (the path)
+     */
+    protected final String getUserAgentStylesheet(Class<?> clazz,
+            String fileName) {
+
+        /*
+         * For more information please see RT-40658
+         */
+        if (stylesheet == null) {
+            stylesheet = clazz.getResource(fileName).toExternalForm();
+        }
+
+        return stylesheet;
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/GridCell.java b/controlsfx/src/main/java/org/controlsfx/control/GridCell.java
new file mode 100644
index 0000000..158ec89
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/GridCell.java
@@ -0,0 +1,125 @@
+/**
+ * Copyright (c) 2013, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control;
+
+import impl.org.controlsfx.skin.GridCellSkin;
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.scene.control.IndexedCell;
+import javafx.scene.control.ListView;
+import javafx.scene.control.Skin;
+import javafx.scene.control.TableView;
+
+/**
+ * A GridCell is created to represent items in the {@link GridView} 
+ * {@link GridView#getItems() items list}. As with other JavaFX UI controls
+ * (like {@link ListView}, {@link TableView}, etc), the {@link GridView} control
+ * is virtualised, meaning it is exceedingly memory and CPU efficient. Refer to
+ * the {@link GridView} class documentation for more details.
+ *  
+ * @see GridView
+ */
+public class GridCell<T> extends IndexedCell<T> {
+    
+    /**************************************************************************
+     * 
+     * Constructors
+     * 
+     **************************************************************************/
+
+    /**
+     * Creates a default GridCell instance.
+     */
+	public GridCell() {
+		getStyleClass().add("grid-cell"); //$NON-NLS-1$
+		
+//		itemProperty().addListener(new ChangeListener<T>() {
+//            @Override public void changed(ObservableValue<? extends T> arg0, T oldItem, T newItem) {
+//                updateItem(newItem, newItem == null);
+//            }
+//        });
+		
+		// TODO listen for index change and update index and item, rather than
+		// listen to just item update as above. This requires the GridCell to 
+		// know about its containing GridRow (and the GridRow to know its 
+		// containing GridView)
+		indexProperty().addListener(new InvalidationListener() {
+            @Override public void invalidated(Observable observable) {
+                final GridView<T> gridView = getGridView();
+                if (gridView == null) return;
+                
+                if(getIndex() < 0) {
+                    updateItem(null, true);
+                    return;
+                }
+                T item = gridView.getItems().get(getIndex());
+                
+//                updateIndex(getIndex());
+                updateItem(item, item == null);
+            }
+        });
+	}
+	
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override protected Skin<?> createDefaultSkin() {
+        return new GridCellSkin<>(this);
+    }
+	
+	
+	
+	/**************************************************************************
+     * 
+     * Properties
+     * 
+     **************************************************************************/
+	
+	/**
+     * The {@link GridView} that this GridCell exists within.
+     */
+    public SimpleObjectProperty<GridView<T>> gridViewProperty() {
+        return gridView;
+    }
+    private final SimpleObjectProperty<GridView<T>> gridView = 
+            new SimpleObjectProperty<>(this, "gridView"); //$NON-NLS-1$
+    
+    /**
+     * Sets the {@link GridView} that this GridCell exists within.
+     */
+    public final void updateGridView(GridView<T> gridView) {
+        this.gridView.set(gridView);
+    }
+    
+    /**
+     * Returns the {@link GridView} that this GridCell exists within.
+     */
+    public GridView<T> getGridView() {
+        return gridView.get();
+    }
+}
\ No newline at end of file
diff --git a/controlsfx/src/main/java/org/controlsfx/control/GridView.java b/controlsfx/src/main/java/org/controlsfx/control/GridView.java
new file mode 100644
index 0000000..c575fe3
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/GridView.java
@@ -0,0 +1,560 @@
+/**
+ * Copyright (c) 2013, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control;
+
+import impl.org.controlsfx.skin.GridViewSkin;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.css.CssMetaData;
+import javafx.css.StyleConverter;
+import javafx.css.Styleable;
+import javafx.css.StyleableDoubleProperty;
+import javafx.css.StyleableProperty;
+import javafx.scene.Node;
+import javafx.scene.control.Cell;
+import javafx.scene.control.Control;
+import javafx.scene.control.ListCell;
+import javafx.scene.control.Skin;
+import javafx.scene.paint.Color;
+import javafx.util.Callback;
+
+import org.controlsfx.control.cell.ColorGridCell;
+
+/**
+ * A GridView is a virtualised control for displaying {@link #getItems()} in a
+ * visual, scrollable, grid-like fashion. In other words, whereas a ListView 
+ * shows one {@link ListCell} per row, in a GridView there will be zero or more
+ * {@link GridCell} instances on a single row.
+ * 
+ * <p> This approach means that the number of GridCell instances
+ * instantiated will be a significantly smaller number than the number of 
+ * items in the GridView items list, as only enough GridCells are created for
+ * the visible area of the GridView. This helps to improve performance and 
+ * reduce memory consumption. 
+ * 
+ * <p>Because each {@link GridCell} extends from {@link Cell}, the same approach
+ * of cell factories that is taken in other UI controls is also taken in GridView.
+ * This has two main benefits: 
+ * 
+ * <ol>
+ *   <li>GridCells are created on demand and without user involvement,
+ *   <li>GridCells can be arbitrarily complex. A simple GridCell may just have 
+ *   its {@link GridCell#textProperty() text property} set, whereas a more complex
+ *   GridCell can have an arbitrarily complex scenegraph set inside its
+ *   {@link GridCell#graphicProperty() graphic property} (as it accepts any Node).
+ * </ol>
+ *
+ * <h3>Examples</h3>
+ * <p>The following screenshot shows the GridView with the {@link ColorGridCell}
+ * being used:
+ * 
+ * <br>
+ * <img src="gridView.png" alt="Screenshot of GridView">
+ * 
+ * <p>To create this GridView was simple. Note that the majority of the code below
+ * is related to randomly creating the colours to be represented:
+ * 
+ * <pre>
+ * {@code
+ * GridView<Color> myGrid = new GridView<>(list);
+ * myGrid.setCellFactory(new Callback<GridView<Color>, GridCell<Color>>() {
+ *     public GridCell<Color> call(GridView<Color> gridView) {
+ *         return new ColorGridCell();
+ *     }
+ * });
+ * Random r = new Random(System.currentTimeMillis());
+ * for(int i = 0; i < 500; i++) {
+ *     list.add(new Color(r.nextDouble(), r.nextDouble(), r.nextDouble(), 1.0));
+ * }
+ * }</pre>
+ * 
+ * @see GridCell
+ */
+public class GridView<T> extends ControlsFXControl {
+
+    /**************************************************************************
+     * 
+     * Constructors
+     * 
+     **************************************************************************/
+    
+    /**
+     * Creates a default, empty GridView control.
+     */
+    public GridView() {
+        this(FXCollections.<T> observableArrayList());
+    }
+    
+    /**
+     * Creates a default GridView control with the provided items prepopulated.
+     * 
+     * @param items The items to display inside the GridView.
+     */
+    public GridView(ObservableList<T> items) {
+        getStyleClass().add(DEFAULT_STYLE_CLASS);
+        setItems(items);
+    }
+    
+    
+    
+    /**************************************************************************
+     * 
+     * Public API
+     * 
+     **************************************************************************/
+    
+    /**
+     * {@inheritDoc}
+     */
+    @Override protected Skin<?> createDefaultSkin() {
+        return new GridViewSkin<>(this);
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getUserAgentStylesheet() {
+        return getUserAgentStylesheet(GridView.class, "gridview.css");
+    }
+    
+    /**************************************************************************
+     * 
+     * Properties
+     * 
+     **************************************************************************/
+    
+    // --- horizontal cell spacing
+    /**
+     * Property for specifying how much spacing there is between each cell
+     * in a row (i.e. how much horizontal spacing there is).
+     */
+    public final DoubleProperty horizontalCellSpacingProperty() {
+        if (horizontalCellSpacing == null) {
+            horizontalCellSpacing = new StyleableDoubleProperty(12) {
+                @Override public CssMetaData<GridView<?>, Number> getCssMetaData() {
+                    return GridView.StyleableProperties.HORIZONTAL_CELL_SPACING;
+                }
+                
+                @Override public Object getBean() {
+                    return GridView.this;
+                }
+
+                @Override public String getName() {
+                    return "horizontalCellSpacing"; //$NON-NLS-1$
+                }
+            };
+        }
+        return horizontalCellSpacing;
+    }
+    private DoubleProperty horizontalCellSpacing;
+    
+    /**
+     * Sets the amount of horizontal spacing there should be between cells in
+     * the same row.
+     * @param value The amount of spacing to use.
+     */
+    public final void setHorizontalCellSpacing(double value) {
+        horizontalCellSpacingProperty().set(value);
+    }
+    
+    /**
+     * Returns the amount of horizontal spacing there is between cells in
+     * the same row.
+     */
+    public final double getHorizontalCellSpacing() {
+        return horizontalCellSpacing == null ? 12.0 : horizontalCellSpacing.get();
+    }
+
+
+    
+    // --- vertical cell spacing
+    /**
+     * Property for specifying how much spacing there is between each cell
+     * in a column (i.e. how much vertical spacing there is).
+     */
+    private DoubleProperty verticalCellSpacing;
+    public final DoubleProperty verticalCellSpacingProperty() {
+        if (verticalCellSpacing == null) {
+            verticalCellSpacing = new StyleableDoubleProperty(12) {
+                @Override public CssMetaData<GridView<?>, Number> getCssMetaData() {
+                    return GridView.StyleableProperties.VERTICAL_CELL_SPACING;
+                }
+                
+                @Override public Object getBean() {
+                    return GridView.this;
+                }
+
+                @Override public String getName() {
+                    return "verticalCellSpacing"; //$NON-NLS-1$
+                }
+            };
+        }
+        return verticalCellSpacing;
+    }
+    
+    /**
+     * Sets the amount of vertical spacing there should be between cells in
+     * the same column.
+     * @param value The amount of spacing to use.
+     */
+    public final void setVerticalCellSpacing(double value) {
+        verticalCellSpacingProperty().set(value);
+    }
+
+    /**
+     * Returns the amount of vertical spacing there is between cells in
+     * the same column.
+     */
+    public final double getVerticalCellSpacing() {
+        return verticalCellSpacing == null ? 12.0 : verticalCellSpacing.get();
+    }
+
+    
+    
+    // --- cell width
+    /**
+     * Property representing the width that all cells should be.
+     */
+    public final DoubleProperty cellWidthProperty() {
+        if (cellWidth == null) {
+            cellWidth = new StyleableDoubleProperty(64) {
+                @Override public CssMetaData<GridView<?>, Number> getCssMetaData() {
+                    return GridView.StyleableProperties.CELL_WIDTH;
+                }
+                
+                @Override public Object getBean() {
+                    return GridView.this;
+                }
+
+                @Override public String getName() {
+                    return "cellWidth"; //$NON-NLS-1$
+                }
+            };
+        }
+        return cellWidth;
+    }
+    private DoubleProperty cellWidth;
+
+    /**
+     * Sets the width that all cells should be.
+     */
+    public final void setCellWidth(double value) {
+        cellWidthProperty().set(value);
+    }
+
+    /**
+     * Returns the width that all cells should be.
+     */
+    public final double getCellWidth() {
+        return cellWidth == null ? 64.0 : cellWidth.get();
+    }
+
+    
+    // --- cell height
+    /**
+     * Property representing the height that all cells should be.
+     */
+    public final DoubleProperty cellHeightProperty() {
+        if (cellHeight == null) {
+            cellHeight = new StyleableDoubleProperty(64) {
+                @Override public CssMetaData<GridView<?>, Number> getCssMetaData() {
+                    return GridView.StyleableProperties.CELL_HEIGHT;
+                }
+                
+                @Override public Object getBean() {
+                    return GridView.this;
+                }
+
+                @Override public String getName() {
+                    return "cellHeight"; //$NON-NLS-1$
+                }
+            };
+        }
+        return cellHeight;
+    }
+    private DoubleProperty cellHeight;
+
+    /**
+     * Sets the height that all cells should be.
+     */
+    public final void setCellHeight(double value) {
+        cellHeightProperty().set(value);
+    }
+
+    /**
+     * Returns the height that all cells should be.
+     */
+    public final double getCellHeight() {
+        return cellHeight == null ? 64.0 : cellHeight.get();
+    }
+
+    
+    // I've removed this functionality until there is a clear need for it.
+    // To re-enable it, there is code in GridRowSkin that has been commented
+    // out that must be re-enabled.
+    // Don't forget also to enable the styleable property further down in this
+    // class.
+//    // --- horizontal alignment
+//    private ObjectProperty<HPos> horizontalAlignment;
+//    public final ObjectProperty<HPos> horizontalAlignmentProperty() {
+//        if (horizontalAlignment == null) {
+//            horizontalAlignment = new StyleableObjectProperty<HPos>(HPos.CENTER) {
+//                @Override public CssMetaData<GridView<?>,HPos> getCssMetaData() {
+//                    return GridView.StyleableProperties.HORIZONTAL_ALIGNMENT;
+//                }
+//                
+//                @Override public Object getBean() {
+//                    return GridView.this;
+//                }
+//
+//                @Override public String getName() {
+//                    return "horizontalAlignment";
+//                }
+//            };
+//        }
+//        return horizontalAlignment;
+//    }
+//
+//    public final void setHorizontalAlignment(HPos value) {
+//        horizontalAlignmentProperty().set(value);
+//    }
+//
+//    public final HPos getHorizontalAlignment() {
+//        return horizontalAlignment == null ? HPos.CENTER : horizontalAlignment.get();
+//    }
+
+    
+    // --- cell factory
+    /**
+     * Property representing the cell factory that is currently set in this
+     * GridView, or null if no cell factory has been set (in which case the 
+     * default cell factory provided by the GridView skin will be used). The cell
+     * factory is used for instantiating enough GridCell instances for the 
+     * visible area of the GridView. Refer to the GridView class documentation
+     * for more information and examples.
+     */
+    public final ObjectProperty<Callback<GridView<T>, GridCell<T>>> cellFactoryProperty() {
+        if (cellFactory == null) {
+            cellFactory = new SimpleObjectProperty<>(this, "cellFactory"); //$NON-NLS-1$
+        }
+        return cellFactory;
+    }
+    private ObjectProperty<Callback<GridView<T>, GridCell<T>>> cellFactory;
+
+    /**
+     * Sets the cell factory to use to create {@link GridCell} instances to 
+     * show in the GridView.
+     */
+    public final void setCellFactory(Callback<GridView<T>, GridCell<T>> value) {
+        cellFactoryProperty().set(value);
+    }
+
+    /**
+     * Returns the cell factory that will be used to create {@link GridCell} 
+     * instances to show in the GridView.
+     */
+    public final Callback<GridView<T>, GridCell<T>> getCellFactory() {
+        return cellFactory == null ? null : cellFactory.get();
+    }
+
+    
+    // --- items
+    /**
+     * The items to be displayed in the GridView (as rendered via {@link GridCell}
+     * instances). For example, if the {@link ColorGridCell} were being used
+     * (as in the case at the top of this class documentation), this items list
+     * would be populated with {@link Color} values. It is important to 
+     * appreciate that the items list is used for the data, not the rendering.
+     * What is meant by this is that the items list should contain Color values,
+     * not the {@link Node nodes} that represent the Color. The actual rendering
+     * should be left up to the {@link #cellFactoryProperty() cell factory},
+     * where it will take the Color value and create / update the display as
+     * necessary. 
+     */
+    public final ObjectProperty<ObservableList<T>> itemsProperty() {
+        if (items == null) {
+            items = new SimpleObjectProperty<>(this, "items"); //$NON-NLS-1$
+        }
+        return items;
+    }
+    private ObjectProperty<ObservableList<T>> items;
+    
+    /**
+     * Sets a new {@link ObservableList} as the items list underlying GridView.
+     * The old items list will be discarded.
+     */
+    public final void setItems(ObservableList<T> value) {
+        itemsProperty().set(value);
+    }
+
+    /**
+     * Returns the currently-in-use items list that is being used by the
+     * GridView.
+     */
+    public final ObservableList<T> getItems() {
+        return items == null ? null : items.get();
+    }
+
+    
+    
+    
+    
+    /***************************************************************************
+     *                                                                         *
+     * Stylesheet Handling                                                     *
+     *                                                                         *
+     **************************************************************************/
+
+    private static final String DEFAULT_STYLE_CLASS = "grid-view"; //$NON-NLS-1$
+
+    /** @treatAsPrivate */
+    private static class StyleableProperties {
+        private static final CssMetaData<GridView<?>,Number> HORIZONTAL_CELL_SPACING = 
+            new CssMetaData<GridView<?>,Number>("-fx-horizontal-cell-spacing", StyleConverter.getSizeConverter(), 12d) { //$NON-NLS-1$
+
+            @Override public Double getInitialValue(GridView<?> node) {
+                return node.getHorizontalCellSpacing();
+            }
+
+            @Override public boolean isSettable(GridView<?> n) {
+                return n.horizontalCellSpacing == null || !n.horizontalCellSpacing.isBound();
+            }
+
+            @Override
+            @SuppressWarnings("unchecked")
+            public StyleableProperty<Number> getStyleableProperty(GridView<?> n) {
+                return (StyleableProperty<Number>)n.horizontalCellSpacingProperty();
+            }
+        };
+        
+        private static final CssMetaData<GridView<?>,Number> VERTICAL_CELL_SPACING = 
+            new CssMetaData<GridView<?>,Number>("-fx-vertical-cell-spacing", StyleConverter.getSizeConverter(), 12d) { //$NON-NLS-1$
+
+            @Override public Double getInitialValue(GridView<?> node) {
+                return node.getVerticalCellSpacing();
+            }
+
+            @Override public boolean isSettable(GridView<?> n) {
+                return n.verticalCellSpacing == null || !n.verticalCellSpacing.isBound();
+            }
+
+            @Override
+            @SuppressWarnings("unchecked")
+            public StyleableProperty<Number> getStyleableProperty(GridView<?> n) {
+                return (StyleableProperty<Number>)n.verticalCellSpacingProperty();
+            }
+        };
+        
+        private static final CssMetaData<GridView<?>,Number> CELL_WIDTH = 
+            new CssMetaData<GridView<?>,Number>("-fx-cell-width", StyleConverter.getSizeConverter(), 64d) { //$NON-NLS-1$
+
+            @Override public Double getInitialValue(GridView<?> node) {
+                return node.getCellWidth();
+            }
+
+            @Override public boolean isSettable(GridView<?> n) {
+                return n.cellWidth == null || !n.cellWidth.isBound();
+            }
+
+            @Override
+            @SuppressWarnings("unchecked")
+            public StyleableProperty<Number> getStyleableProperty(GridView<?> n) {
+                return (StyleableProperty<Number>)n.cellWidthProperty();
+            }
+        };
+        
+        private static final CssMetaData<GridView<?>,Number> CELL_HEIGHT = 
+            new CssMetaData<GridView<?>,Number>("-fx-cell-height", StyleConverter.getSizeConverter(), 64d) { //$NON-NLS-1$
+
+            @Override public Double getInitialValue(GridView<?> node) {
+                return node.getCellHeight();
+            }
+
+            @Override public boolean isSettable(GridView<?> n) {
+                return n.cellHeight == null || !n.cellHeight.isBound();
+            }
+
+            @Override
+            @SuppressWarnings("unchecked")
+            public StyleableProperty<Number> getStyleableProperty(GridView<?> n) {
+                return (StyleableProperty<Number>)n.cellHeightProperty();
+            }
+        };
+        
+//        private static final CssMetaData<GridView<?>,HPos> HORIZONTAL_ALIGNMENT = 
+//            new CssMetaData<GridView<?>,HPos>("-fx-horizontal_alignment",
+//                new EnumConverter<HPos>(HPos.class), 
+//                HPos.CENTER) {
+//
+//            @Override public HPos getInitialValue(GridView node) {
+//                return node.getHorizontalAlignment();
+//            }
+//
+//            @Override public boolean isSettable(GridView n) {
+//                return n.horizontalAlignment == null || !n.horizontalAlignment.isBound();
+//            }
+//
+//            @Override public StyleableProperty<HPos> getStyleableProperty(GridView n) {
+//                return (StyleableProperty<HPos>)n.horizontalAlignmentProperty();
+//            }
+//        };
+            
+        private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
+        static {
+            final List<CssMetaData<? extends Styleable, ?>> styleables =
+                new ArrayList<>(Control.getClassCssMetaData());
+            styleables.add(HORIZONTAL_CELL_SPACING);
+            styleables.add(VERTICAL_CELL_SPACING);
+            styleables.add(CELL_WIDTH);
+            styleables.add(CELL_HEIGHT);
+//            styleables.add(HORIZONTAL_ALIGNMENT);
+            STYLEABLES = Collections.unmodifiableList(styleables);
+        }
+    }
+
+    /**
+     * @return The CssMetaData associated with this class, which may include the
+     * CssMetaData of its super classes.
+     */
+    public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
+        return StyleableProperties.STYLEABLES;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
+        return getClassCssMetaData();
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/HiddenSidesPane.java b/controlsfx/src/main/java/org/controlsfx/control/HiddenSidesPane.java
new file mode 100644
index 0000000..9aa37cb
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/HiddenSidesPane.java
@@ -0,0 +1,426 @@
+/**
+ * Copyright (c) 2014, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control;
+
+import impl.org.controlsfx.skin.HiddenSidesPaneSkin;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.geometry.Side;
+import javafx.scene.Node;
+import javafx.scene.control.Skin;
+import javafx.util.Duration;
+
+/**
+ * A pane used to display a full-size content node and four initially hidden
+ * nodes on the four sides. The hidden nodes can be made visible by moving the
+ * mouse cursor to the edges (see {@link #setTriggerDistance(double)}) of the
+ * pane. The hidden node will appear (at its preferred width or height) with a
+ * short slide-in animation. The node will disappear again as soon as the mouse
+ * cursor exits it. A hidden node / side can also be pinned by calling
+ * {@link #setPinnedSide(Side)}. It will remain visible as long as it stays
+ * pinned.
+ * 
+ * <h3>Screenshot</h3> The following screenshots shows the right side node
+ * hovering over a table after it was made visible:
+ * 
+ * <center><img src="hiddenSidesPane.png" alt="Screenshot of HiddenSidesPane">
+ * 
+ * </center> <h3>Code Sample</h3>
+ * 
+ * <pre>
+ * HiddenSidesPane pane = new HiddenSidesPane();
+ * pane.setContent(new TableView());
+ * pane.setRight(new ListView());
+ * </pre>
+ */
+public class HiddenSidesPane extends ControlsFXControl {
+
+    /**
+     * Constructs a new pane with the given content node and the four side
+     * nodes. Each one of the side nodes may be null.
+     * 
+     * @param content
+     *            the primary node that will fill the entire width and height of
+     *            the pane
+     * @param top
+     *            the hidden node on the top side
+     * @param right
+     *            the hidden node on the right side
+     * @param bottom
+     *            the hidden node on the bottom side
+     * @param left
+     *            the hidden node on the left side
+     */
+    public HiddenSidesPane(Node content, Node top, Node right, Node bottom,
+            Node left) {
+        setContent(content);
+        setTop(top);
+        setRight(right);
+        setBottom(bottom);
+        setLeft(left);
+    }
+
+    /**
+     * Constructs a new pane with no content and no side nodes.
+     */
+    public HiddenSidesPane() {
+        this(null, null, null, null, null);
+    }
+
+    @Override
+    protected Skin<?> createDefaultSkin() {
+        return new HiddenSidesPaneSkin(this);
+    }
+
+    private DoubleProperty triggerDistance = new SimpleDoubleProperty(this,
+            "triggerDistance", 16); //$NON-NLS-1$
+
+    /**
+     * The property that stores the distance to the pane's edges that will
+     * trigger the appearance of the hidden side nodes.<br>
+     * Setting the property to zero or a negative value will disable this
+     * functionality, so a hidden side can only be made visible with
+     * {@link #setPinnedSide(Side)}.
+     * 
+     * @return the trigger distance property
+     */
+    public final DoubleProperty triggerDistanceProperty() {
+        return triggerDistance;
+    }
+
+    /**
+     * Returns the value of the trigger distance property.
+     * 
+     * @return the trigger distance property value
+     */
+    public final double getTriggerDistance() {
+        return triggerDistance.get();
+    }
+
+    /**
+     * Set the value of the trigger distance property. <br>
+     * Setting the property to zero or a negative value will disable this
+     * functionality, so a hidden side can only be made visible with
+     * {@link #setPinnedSide(Side)}.
+     * 
+     * @param distance
+     *            the new value for the trigger distance property
+     */
+    public final void setTriggerDistance(double distance) {
+        triggerDistance.set(distance);
+    }
+
+    // Content node support.
+
+    private ObjectProperty<Node> content = new SimpleObjectProperty<>(this,
+            "content"); //$NON-NLS-1$
+
+    /**
+     * The property that is used to store a reference to the content node. The
+     * content node will fill the entire width and height of the pane.
+     * 
+     * @return the content node property
+     */
+    public final ObjectProperty<Node> contentProperty() {
+        return content;
+    }
+
+    /**
+     * Returns the value of the content node property.
+     * 
+     * @return the content node property value
+     */
+    public final Node getContent() {
+        return contentProperty().get();
+    }
+
+    /**
+     * Sets the value of the content node property.
+     * 
+     * @param content
+     *            the new content node
+     */
+    public final void setContent(Node content) {
+        contentProperty().set(content);
+    }
+
+    // Top node support.
+
+    private ObjectProperty<Node> top = new SimpleObjectProperty<>(this,
+            "top"); //$NON-NLS-1$
+
+    /**
+     * The property used to store a reference to the node shown at the top side
+     * of the pane.
+     * 
+     * @return the hidden node at the top side of the pane
+     */
+    public final ObjectProperty<Node> topProperty() {
+        return top;
+    }
+
+    /**
+     * Returns the value of the top node property.
+     * 
+     * @return the top node property value
+     */
+    public final Node getTop() {
+        return topProperty().get();
+    }
+
+    /**
+     * Sets the value of the top node property.
+     * 
+     * @param top
+     *            the top node value
+     */
+    public final void setTop(Node top) {
+        topProperty().set(top);
+    }
+
+    // Right node support.
+
+    /**
+     * The property used to store a reference to the node shown at the right
+     * side of the pane.
+     * 
+     * @return the hidden node at the right side of the pane
+     */
+    private ObjectProperty<Node> right = new SimpleObjectProperty<>(this,
+            "right"); //$NON-NLS-1$
+
+    /**
+     * Returns the value of the right node property.
+     * 
+     * @return the right node property value
+     */
+    public final ObjectProperty<Node> rightProperty() {
+        return right;
+    }
+
+    /**
+     * Returns the value of the right node property.
+     * 
+     * @return the right node property value
+     */
+    public final Node getRight() {
+        return rightProperty().get();
+    }
+
+    /**
+     * Sets the value of the right node property.
+     * 
+     * @param right
+     *            the right node value
+     */
+    public final void setRight(Node right) {
+        rightProperty().set(right);
+    }
+
+    // Bottom node support.
+
+    /**
+     * The property used to store a reference to the node shown at the bottom
+     * side of the pane.
+     * 
+     * @return the hidden node at the bottom side of the pane
+     */
+    private ObjectProperty<Node> bottom = new SimpleObjectProperty<>(this,
+            "bottom"); //$NON-NLS-1$
+
+    /**
+     * Returns the value of the bottom node property.
+     * 
+     * @return the bottom node property value
+     */
+    public final ObjectProperty<Node> bottomProperty() {
+        return bottom;
+    }
+
+    /**
+     * Returns the value of the bottom node property.
+     * 
+     * @return the bottom node property value
+     */
+    public final Node getBottom() {
+        return bottomProperty().get();
+    }
+
+    /**
+     * Sets the value of the bottom node property.
+     * 
+     * @param bottom
+     *            the bottom node value
+     */
+    public final void setBottom(Node bottom) {
+        bottomProperty().set(bottom);
+    }
+
+    // Left node support.
+
+    /**
+     * The property used to store a reference to the node shown at the left side
+     * of the pane.
+     * 
+     * @return the hidden node at the left side of the pane
+     */
+    private ObjectProperty<Node> left = new SimpleObjectProperty<>(this,
+            "left"); //$NON-NLS-1$
+
+    /**
+     * Returns the value of the left node property.
+     * 
+     * @return the left node property value
+     */
+    public final ObjectProperty<Node> leftProperty() {
+        return left;
+    }
+
+    /**
+     * Returns the value of the left node property.
+     * 
+     * @return the left node property value
+     */
+    public final Node getLeft() {
+        return leftProperty().get();
+    }
+
+    /**
+     * Sets the value of the left node property.
+     * 
+     * @param left
+     *            the left node value
+     */
+    public final void setLeft(Node left) {
+        leftProperty().set(left);
+    }
+
+    // Pinned side support.
+
+    private ObjectProperty<Side> pinnedSide = new SimpleObjectProperty<>(
+            this, "pinnedSide"); //$NON-NLS-1$
+
+    /**
+     * Returns the pinned side property. The value of this property determines
+     * if one of the four hidden sides stays visible all the time.
+     * 
+     * @return the pinned side property
+     */
+    public final ObjectProperty<Side> pinnedSideProperty() {
+        return pinnedSide;
+    }
+
+    /**
+     * Returns the value of the pinned side property.
+     * 
+     * @return the pinned side property value
+     */
+    public final Side getPinnedSide() {
+        return pinnedSideProperty().get();
+    }
+
+    /**
+     * Sets the value of the pinned side property.
+     * 
+     * @param side
+     *            the new pinned side value
+     */
+    public final void setPinnedSide(Side side) {
+        pinnedSideProperty().set(side);
+    }
+
+    // slide in animation delay
+
+    private final ObjectProperty<Duration> animationDelay = new SimpleObjectProperty<>(
+            this, "animationDelay", Duration.millis(300)); //$NON-NLS-1$
+
+    /**
+     * Returns the animation delay property. The value of this property
+     * determines the delay before the hidden side slide in / slide out
+     * animation starts to play.
+     * 
+     * @return animation delay property
+     */
+    public final ObjectProperty<Duration> animationDelayProperty() {
+        return animationDelay;
+    }
+
+    /**
+     * Returns the animation delay
+     * 
+     * @return animation delay
+     */
+    public final Duration getAnimationDelay() {
+        return animationDelay.get();
+    }
+
+    /**
+     * Set the animation delay
+     * 
+     * @param duration
+     *            slide in animation delay
+     */
+    public final void setAnimationDelay(Duration duration) {
+        animationDelay.set(duration);
+    }
+
+    // slide in / slide out duration
+
+    private final ObjectProperty<Duration> animationDuration = new SimpleObjectProperty<>(
+            this, "animationDuration", Duration.millis(200)); //$NON-NLS-1$
+
+    /**
+     * Returns the animation duration property. The value of this property
+     * determines the fade in time for a hidden side to become visible.
+     * 
+     * @return animation delay property
+     */
+    public final ObjectProperty<Duration> animationDurationProperty() {
+        return animationDuration;
+    }
+
+    /**
+     * Returns the animation delay
+     * 
+     * @return animation delay
+     */
+    public final Duration getAnimationDuration() {
+        return animationDuration.get();
+    }
+
+    /**
+     * Set the animation delay
+     * 
+     * @param duration
+     *            animation duration
+     */
+    public final void setAnimationDuration(Duration duration) {
+        animationDuration.set(duration);
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/HyperlinkLabel.java b/controlsfx/src/main/java/org/controlsfx/control/HyperlinkLabel.java
new file mode 100644
index 0000000..abf3b90
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/HyperlinkLabel.java
@@ -0,0 +1,211 @@
+/**
+ * Copyright (c) 2013, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control;
+
+import impl.org.controlsfx.skin.HyperlinkLabelSkin;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.event.EventTarget;
+import javafx.scene.control.Hyperlink;
+import javafx.scene.control.Skin;
+
+import com.sun.javafx.event.EventHandlerManager;
+
+/**
+ * A UI control that will convert the given text into a series of text labels
+ * and {@link Hyperlink hyperlinks}, based on the use of delimiter characters
+ * to specify where hyperlinks should appear. The delimiter characters are 
+ * square braces (that is, [ and ]). To create a hyperlink in a string you would
+ * therefore do something like 
+ * <code>hyperlinkLabel.setText("Click [here] for more information!");</code>,
+ * with the word 'here' appearing as a hyperlink that a use may click. This 
+ * approach therefore allows for hyperlinks to be easily embedded within a 
+ * label.
+ * 
+ * <p>Once hyperlinks have been declared in a text string, it is necessary to
+ * respond to the user interacting with the hyperlink (most commonly via mouse
+ * clicks). To do so, you register a single event handler for action events on
+ * the HyperlinkLabel instance, and then determine what to do within that 
+ * callback. For example:
+ * 
+ * <pre>
+ * {@code
+ * hyperlinkLabel.setOnAction(new EventHandler<ActionEvent>() {
+ *     public void handle(ActionEvent event) {
+ *         Hyperlink link = (Hyperlink)event.getSource();
+ *         final String str = link == null ? "" : link.getText();
+ *         switch(str) {
+ *             case "here": // do 'here' action
+ *                          break;
+ *             case "exit": // do exit action
+ *                          break;
+ *         }
+ *     }
+ * });}</pre>
+ * 
+ * <p>This simple single-handler approach was chosen over any more complex 
+ * per-hyperlink solution because it is anticipated that most use cases will 
+ * normally consist of one, or very few hyperlinks, and it was therefore unlikely
+ * that the increased API complexity would be warranted.
+ * 
+ * <h3>Screenshot</h3>
+ * <p>To demonstrate what a HyperlinkLabel looks like, refer to the screenshot
+ * below, when the text 
+ * <code>"Hello [world]! I [wonder] what hyperlink [you] [will] [click]"</code>
+ * was passed in to the HyperlinkLabel instance:
+ * 
+ * <br><br>
+ * <center><img src="hyperlinkLabel.PNG" alt="Screenshot of HyperlinkLabel"></center>
+ * 
+ * @see Hyperlink
+ * @see ActionEvent
+ */
+public class HyperlinkLabel extends ControlsFXControl implements EventTarget {
+    
+    /***************************************************************************
+     * 
+     * Private fields
+     * 
+     **************************************************************************/
+    
+    private final EventHandlerManager eventHandlerManager =
+            new EventHandlerManager(this);
+    
+    
+    
+    /***************************************************************************
+     * 
+     * Constructors
+     * 
+     **************************************************************************/
+    
+    /**
+     * Creates an empty HyperlinkLabel instance with no {@link #textProperty() text}
+     * specified.
+     */
+    public HyperlinkLabel() {
+        this(null);
+    }
+    
+    /**
+     * Creates a HyperlinkLabel instance with the given text value used as the 
+     * initial text.
+     * 
+     * @param text The text to display to the user.
+     */
+    public HyperlinkLabel(String text) {
+        setText(text);
+    }
+    
+    
+    
+    /**************************************************************************
+     * 
+     * Public API
+     * 
+     **************************************************************************/
+    
+    /**
+     * {@inheritDoc}
+     */
+    @Override protected Skin<?> createDefaultSkin() {
+        return new HyperlinkLabelSkin(this);
+    }
+    
+    
+    // --- text
+    private final StringProperty text = new SimpleStringProperty(this, "text"); //$NON-NLS-1$
+    
+    /**
+     * Return a {@link StringProperty} representing the text being displayed.
+     * @return a {@link StringProperty}.
+     */
+    public final StringProperty textProperty() { 
+        return text; 
+    }
+    
+    /**
+     * Return the text currently displayed.
+     * @return the text currently displayed.
+     */
+    public final String getText() {
+        return text.get();
+    }
+    
+    /**
+     * Set a new text to display to the user, using the delimiter characters [ and ]
+     * to indicate where hyperlinks should be displayed.
+     * @param value 
+     */
+    public final void setText(String value) {
+        text.set(value);
+    }
+    
+    
+    // --- onAction
+    private ObjectProperty<EventHandler<ActionEvent>> onAction;
+    
+    /**
+     * The action, which is invoked whenever a hyperlink is fired. This
+     * may be due to the user clicking on the hyperlink with the mouse, or by
+     * a touch event, or by a key press.
+     * @return an {@link ObjectProperty} representing the action.
+     */
+    public final ObjectProperty<EventHandler<ActionEvent>> onActionProperty() {
+        if (onAction == null) {
+            onAction = new SimpleObjectProperty<EventHandler<ActionEvent>>(this, "onAction") { //$NON-NLS-1$
+                @Override protected void invalidated() {
+                    eventHandlerManager.setEventHandler(ActionEvent.ACTION, get());
+                }
+            };
+        }
+        return onAction;
+    }
+
+    /**
+     * Sets a new EventHandler which will be invoked whenever a hyperlink is 
+     * fired.
+     * @param value 
+     */
+    public final void setOnAction(EventHandler<ActionEvent> value) {
+        onActionProperty().set( value);
+    }
+
+    /**
+     * 
+     * @return the action, which is invoked whenever a hyperlink is fired.
+     */
+    public final EventHandler<ActionEvent> getOnAction() {
+        return onAction == null ? null : onAction.get();
+    }
+
+    
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/IndexedCheckModel.java b/controlsfx/src/main/java/org/controlsfx/control/IndexedCheckModel.java
new file mode 100644
index 0000000..acb77cd
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/IndexedCheckModel.java
@@ -0,0 +1,68 @@
+/**
+ * Copyright (c) 2014, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control;
+
+import javafx.collections.ObservableList;
+
+public interface IndexedCheckModel<T> extends CheckModel<T> {
+
+    /**
+     * Returns the item in the given index in the control.
+     */
+    public T getItem(int index);
+
+    /**
+     * Returns the index of the given item.
+     */
+    public int getItemIndex(T item);
+
+    /**
+     * Returns a read-only list of the currently checked indices in the control.
+     */
+    public ObservableList<Integer> getCheckedIndices();
+
+    /**
+     * Checks the given indices in the control
+     */
+    public void checkIndices(int... indices);
+
+    /**
+     * Unchecks the given index in the control
+     */
+    public void clearCheck(int index);
+
+    /**
+     * Returns true if the given index represents an item that is checked in the control.
+     */
+    public boolean isChecked(int index);
+
+    /**
+     * Checks the item in the given index in the control.
+     */
+    public void check(int index);
+
+}
\ No newline at end of file
diff --git a/controlsfx/src/main/java/org/controlsfx/control/InfoOverlay.java b/controlsfx/src/main/java/org/controlsfx/control/InfoOverlay.java
new file mode 100644
index 0000000..3d9cf5d
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/InfoOverlay.java
@@ -0,0 +1,220 @@
+/**
+ * Copyright (c) 2014, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.controlsfx.control;
+
+import impl.org.controlsfx.skin.InfoOverlaySkin;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+import javafx.scene.Node;
+import javafx.scene.control.Skin;
+import javafx.scene.image.ImageView;
+
+/**
+ * A simple UI control that allows for an information popup to be displayed over 
+ * a node to describe it in further detail. In some ways, it can be thought of 
+ * as a always visible tooltip (although by default it is collapsed so only the
+ * first line is shown - clicking on it will expand it to show all text).
+ * 
+ * <p>Shown below is a screenshot of the InfoOverlay control in both its 
+ * collapsed and expanded states:
+ * 
+ * <br>
+ * <center>
+ * <img src="infoOverlay.png" alt="Screenshot of InfoOverlay">
+ * </center>
+ */
+public class InfoOverlay extends ControlsFXControl {
+    
+    /***************************************************************************
+     *                                                                         *
+     * Constructors                                                            *
+     *                                                                         *
+     **************************************************************************/
+    
+    /**
+     * Constructs a default InfoOverlay control with no node or text. 
+     */
+    public InfoOverlay() {
+        this((Node)null, null);
+    }
+    
+    /**
+     * Attempts to construct an InfoOverlay instance using the given string
+     * to load an image, and to place the given text string over top of it.
+     * 
+     * @param imageUrl The image file to attempt to load.
+     * @param text The text to display over top of the image.
+     */
+    public InfoOverlay(String imageUrl, String text) {
+        this(new ImageView(imageUrl), text);
+    }
+
+    /**
+     * Constructs an InfoOverlay instance using the given Node (which can be
+     * an arbitrarily complex node / scenegraph, or a simple ImageView, for example),
+     * and places the given text string over top of it.
+     * 
+     * @param content The arbitrarily complex scenegraph over which the text will be displayed.
+     * @param text The text to display over top of the node.
+     */
+    public InfoOverlay(Node content, String text) {
+        getStyleClass().setAll(DEFAULT_STYLE_CLASS);
+
+        setContent(content);
+        setText(text);
+    }
+    
+    
+    
+    /***************************************************************************
+     *                                                                         *
+     * Public API                                                              *
+     *                                                                         *
+     **************************************************************************/
+    
+    /** {@inheritDoc} */   
+    @Override protected Skin<?> createDefaultSkin() {
+        return new InfoOverlaySkin(this);
+    }
+    
+    
+    
+    /***************************************************************************
+     *                                                                         *
+     * Properties                                                              *
+     *                                                                         *
+     **************************************************************************/
+
+    // --- content
+     private ObjectProperty<Node> content = new SimpleObjectProperty<>(this, "content"); //$NON-NLS-1$
+     
+    /**
+     * 
+     * @return an {@link ObjectProperty} containing the arbitrarily complex
+     * scenegraph over which the text will be displayed.
+     */
+    public final ObjectProperty<Node> contentProperty() {
+        return content;
+    }
+
+    /**
+     * Sets a new value for the {@link #contentProperty() }.
+     * @param content 
+     */
+    public final void setContent(Node content) {
+        contentProperty().set(content);
+    }
+
+    /**
+     * 
+     * @return the arbitrarily complex scenegraph over which the text will be 
+     * displayed.
+     */
+    public final Node getContent() {
+        return contentProperty().get();
+    }
+
+    
+    // --- text
+    
+    private StringProperty text = new SimpleStringProperty(this, "text"); //$NON-NLS-1$
+    
+    /**
+     * @return A {@link StringProperty} representing the text displayed over top
+     * of the {@link #contentProperty() content}.
+     */
+    public final StringProperty textProperty() {
+        return text;
+    }
+
+    /**
+     * 
+     * @return The text displayed over top of the {@link #contentProperty() content}.
+     */
+    public final String getText() {
+        return textProperty().get();
+    }
+
+    /**
+     * Specifies the text to display over top of the {@link #contentProperty() content}.
+     * @param text 
+     */
+    public final void setText(String text) {
+        textProperty().set(text);
+    }
+    
+    
+    // --- showOnHover
+    private BooleanProperty showOnHover = new SimpleBooleanProperty(this, "showOnHover", true); //$NON-NLS-1$
+    
+    /**
+     * 
+     * @return A {@link BooleanProperty} representing whether the overlay on 
+     * hover of the content node is showing.
+     */
+    public final BooleanProperty showOnHoverProperty() {
+        return showOnHover;
+    }
+
+    /**
+     * 
+     * @return whether the overlay on hover of the content node is showing.
+     */
+    public final boolean isShowOnHover() {
+        return showOnHoverProperty().get();
+    }
+
+    /**
+     * Specifies whether to show the overlay on hover of the content node (and 
+     * to hide it again when the content is no longer being hovered). By default 
+     * this is true. 
+     * @param value 
+     */
+    public final void setShowOnHover(boolean value) {
+        showOnHoverProperty().set(value);
+    }
+
+    
+    
+    /***************************************************************************
+     *                                                                         *
+     * Stylesheet Handling                                                     *
+     *                                                                         *
+     **************************************************************************/
+    
+    private static final String DEFAULT_STYLE_CLASS = "info-overlay"; //$NON-NLS-1$
+
+    /** {@inheritDoc} */
+    @Override public String getUserAgentStylesheet() {
+        return getUserAgentStylesheet(InfoOverlay.class, "info-overlay.css");
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/ListSelectionView.java b/controlsfx/src/main/java/org/controlsfx/control/ListSelectionView.java
new file mode 100644
index 0000000..6c6d669
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/ListSelectionView.java
@@ -0,0 +1,370 @@
+/**
+ * Copyright (c) 2014, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control;
+
+import static impl.org.controlsfx.i18n.Localization.asKey;
+import static impl.org.controlsfx.i18n.Localization.localize;
+
+import impl.org.controlsfx.skin.ListSelectionViewSkin;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.geometry.Orientation;
+import javafx.scene.Node;
+import javafx.scene.control.Cell;
+import javafx.scene.control.Label;
+import javafx.scene.control.ListCell;
+import javafx.scene.control.ListView;
+import javafx.scene.control.Skin;
+import javafx.util.Callback;
+
+/**
+ * A control used to perform a multi-selection via the help of two list views.
+ * Items can be moved from one list (source) to the other (target). This can be
+ * done by either double clicking on the list items or by using one of the
+ * "move" buttons between the two lists. Each list can be decorated with a
+ * header and a footer node. The default header nodes are simply two labels
+ * ("Available", "Selected").
+ *
+ * <h3>Screenshot</h3>
+ *
+ * <center><img src="list-selection-view.png" alt="Screenshot of ListSelectionView"></center>
+ *
+ * <h3>Code Example</h3>
+ *
+ * <pre>
+ * ListSelectionView<String> view = new ListSelectionView<>();
+ * view.getSourceItems().add("One", "Two", "Three");
+ * view.getTargetItems().add("Four", "Five");
+ * </pre>
+ *
+ * @param <T>
+ *            the type of the list items
+ */
+public class ListSelectionView<T> extends ControlsFXControl {
+
+    private static final String DEFAULT_STYLECLASS = "list-selection-view";
+
+    /**
+     * Constructs a new dual list view.
+     */
+    public ListSelectionView() {
+        getStyleClass().add(DEFAULT_STYLECLASS);
+
+        Label sourceHeader = new Label(
+                localize(asKey("listSelectionView.header.source")));
+        sourceHeader.getStyleClass().add("list-header-label");
+        sourceHeader.setId("source-header-label");
+        setSourceHeader(sourceHeader);
+
+        Label targetHeader = new Label(
+                localize(asKey("listSelectionView.header.target")));
+        targetHeader.getStyleClass().add("list-header-label");
+        targetHeader.setId("target-header-label");
+        setTargetHeader(targetHeader);
+    }
+
+    @Override
+    protected Skin<ListSelectionView<T>> createDefaultSkin() {
+        return new ListSelectionViewSkin<>(this);
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getUserAgentStylesheet() {
+        return getUserAgentStylesheet(ListSelectionView.class, "listselectionview.css");
+    }
+
+    private final ObjectProperty<Node> sourceHeader = new SimpleObjectProperty<>(
+            this, "sourceHeader");
+
+    /**
+     * A property used to store a reference to a node that will be displayed
+     * above the source list view. The default node is a {@link Label}
+     * displaying the text "Available".
+     *
+     * @return the property used to store the source header node
+     */
+    public final ObjectProperty<Node> sourceHeaderProperty() {
+        return sourceHeader;
+    }
+
+    /**
+     * Returns the value of {@link #sourceHeaderProperty()}.
+     *
+     * @return the source header node
+     */
+    public final Node getSourceHeader() {
+        return sourceHeader.get();
+    }
+
+    /**
+     * Sets the value of {@link #sourceHeaderProperty()}.
+     *
+     * @param node
+     *            the new header node to use for the source list
+     */
+    public final void setSourceHeader(Node node) {
+        sourceHeader.set(node);
+    }
+
+    private final ObjectProperty<Node> sourceFooter = new SimpleObjectProperty<>(
+            this, "sourceFooter");
+
+    /**
+     * A property used to store a reference to a node that will be displayed
+     * below the source list view. The default node is a node with two buttons
+     * for easily selecting / deselecting all elements in the list view.
+     *
+     * @return the property used to store the source footer node
+     */
+    public final ObjectProperty<Node> sourceFooterProperty() {
+        return sourceFooter;
+    }
+
+    /**
+     * Returns the value of {@link #sourceFooterProperty()}.
+     *
+     * @return the source footer node
+     */
+    public final Node getSourceFooter() {
+        return sourceFooter.get();
+    }
+
+    /**
+     * Sets the value of {@link #sourceFooterProperty()}.
+     *
+     * @param node
+     *            the new node shown below the source list
+     */
+    public final void setSourceFooter(Node node) {
+        sourceFooter.set(node);
+    }
+
+    private final ObjectProperty<Node> targetHeader = new SimpleObjectProperty<>(
+            this, "targetHeader");
+
+    /**
+     * A property used to store a reference to a node that will be displayed
+     * above the target list view. The default node is a {@link Label}
+     * displaying the text "Selected".
+     *
+     * @return the property used to store the target header node
+     */
+    public final ObjectProperty<Node> targetHeaderProperty() {
+        return targetHeader;
+    }
+
+    /**
+     * Returns the value of {@link #targetHeaderProperty()}.
+     *
+     * @return the source header node
+     */
+    public final Node getTargetHeader() {
+        return targetHeader.get();
+    }
+
+    /**
+     * Sets the value of {@link #targetHeaderProperty()}.
+     *
+     * @param node
+     *            the new node shown above the target list
+     */
+    public final void setTargetHeader(Node node) {
+        targetHeader.set(node);
+    }
+
+    private final ObjectProperty<Node> targetFooter = new SimpleObjectProperty<>(
+            this, "targetFooter");
+
+    /**
+     * A property used to store a reference to a node that will be displayed
+     * below the target list view. The default node is a node with two buttons
+     * for easily selecting / deselecting all elements in the list view.
+     *
+     * @return the property used to store the source footer node
+     */
+    public final ObjectProperty<Node> targetFooterProperty() {
+        return targetFooter;
+    }
+
+    /**
+     * Returns the value of {@link #targetFooterProperty()}.
+     *
+     * @return the source header node
+     */
+    public final Node getTargetFooter() {
+        return targetFooter.get();
+    }
+
+    /**
+     * Sets the value of {@link #targetFooterProperty()}.
+     *
+     * @param node
+     *            the new node shown below the target list
+     */
+    public final void setTargetFooter(Node node) {
+        targetFooter.set(node);
+    }
+
+    private ObjectProperty<ObservableList<T>> sourceItems;
+
+    /**
+     * Sets the underlying data model for the ListView. Note that it has a
+     * generic type that must match the type of the ListView itself.
+     */
+    public final void setSourceItems(ObservableList<T> value) {
+        sourceItemsProperty().set(value);
+    }
+
+    /**
+     * Returns an {@link ObservableList} that contains the items currently being
+     * shown to the user in the source list. This may be null if
+     * {@link #setSourceItems(javafx.collections.ObservableList)} has previously
+     * been called, however, by default it is an empty ObservableList.
+     *
+     * @return An ObservableList containing the items to be shown to the user in
+     *         the source list, or null if the items have previously been set to
+     *         null.
+     */
+    public final ObservableList<T> getSourceItems() {
+        return sourceItemsProperty().get();
+    }
+
+    /**
+     * The underlying data model for the source list view. Note that it has a
+     * generic type that must match the type of the source list view itself.
+     */
+    public final ObjectProperty<ObservableList<T>> sourceItemsProperty() {
+        if (sourceItems == null) {
+            sourceItems = new SimpleObjectProperty<>(this, "sourceItems",
+                    FXCollections.observableArrayList());
+        }
+        return sourceItems;
+    }
+
+    private ObjectProperty<ObservableList<T>> targetItems;
+
+    /**
+     * Sets the underlying data model for the ListView. Note that it has a
+     * generic type that must match the type of the ListView itself.
+     */
+    public final void setTargetItems(ObservableList<T> value) {
+        targetItemsProperty().set(value);
+    }
+
+    /**
+     * Returns an {@link ObservableList} that contains the items currently being
+     * shown to the user in the target list. This may be null if
+     * {@link #setTargetItems(javafx.collections.ObservableList)} has previously
+     * been called, however, by default it is an empty ObservableList.
+     *
+     * @return An ObservableList containing the items to be shown to the user in
+     *         the target list, or null if the items have previously been set to
+     *         null.
+     */
+    public final ObservableList<T> getTargetItems() {
+        return targetItemsProperty().get();
+    }
+
+    /**
+     * The underlying data model for the target list view. Note that it has a
+     * generic type that must match the type of the source list view itself.
+     */
+    public final ObjectProperty<ObservableList<T>> targetItemsProperty() {
+        if (targetItems == null) {
+            targetItems = new SimpleObjectProperty<>(this, "targetItems",
+                    FXCollections.observableArrayList());
+        }
+        return targetItems;
+    }
+
+    // --- Orientation
+    private final ObjectProperty<Orientation> orientation = new SimpleObjectProperty<>(
+            this, "orientation", Orientation.HORIZONTAL); //$NON-NLS-1$;
+
+    /**
+     * The {@link Orientation} of the {@code ListSelectionView} - this can
+     * either be horizontal or vertical.
+     */
+    public final ObjectProperty<Orientation> orientationProperty() {
+        return orientation;
+    }
+
+    /**
+     * Sets the {@link Orientation} of the {@code ListSelectionView} - this can
+     * either be horizontal or vertical.
+     */
+    public final void setOrientation(Orientation value) {
+        orientationProperty().set(value);
+    };
+
+    /**
+     * Returns the {@link Orientation} of the {@code ListSelectionView} - this
+     * can either be horizontal or vertical.
+     */
+    public final Orientation getOrientation() {
+        return orientation.get();
+    }
+
+    // --- Cell Factory
+    private ObjectProperty<Callback<ListView<T>, ListCell<T>>> cellFactory;
+
+    /**
+     * Sets a new cell factory to use by both list views. This forces all old
+     * {@link ListCell}'s to be thrown away, and new ListCell's created with the
+     * new cell factory.
+     */
+    public final void setCellFactory(Callback<ListView<T>, ListCell<T>> value) {
+        cellFactoryProperty().set(value);
+    }
+
+    /**
+     * Returns the current cell factory.
+     */
+    public final Callback<ListView<T>, ListCell<T>> getCellFactory() {
+        return cellFactory == null ? null : cellFactory.get();
+    }
+
+    /**
+     * <p>
+     * Setting a custom cell factory has the effect of deferring all cell
+     * creation, allowing for total customization of the cell. Internally, the
+     * ListView is responsible for reusing ListCells - all that is necessary is
+     * for the custom cell factory to return from this function a ListCell which
+     * might be usable for representing any item in the ListView.
+     *
+     * <p>
+     * Refer to the {@link Cell} class documentation for more detail.
+     */
+    public final ObjectProperty<Callback<ListView<T>, ListCell<T>>> cellFactoryProperty() {
+        if (cellFactory == null) {
+            cellFactory = new SimpleObjectProperty<>(this, "cellFactory");
+        }
+        return cellFactory;
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/MaskerPane.java b/controlsfx/src/main/java/org/controlsfx/control/MaskerPane.java
new file mode 100644
index 0000000..0003d46
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/MaskerPane.java
@@ -0,0 +1,117 @@
+/**
+ * Copyright (c) 2014, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control;
+
+import impl.org.controlsfx.skin.MaskerPaneSkin;
+import javafx.beans.property.*;
+import javafx.scene.Node;
+import javafx.scene.control.ProgressIndicator;
+import javafx.scene.control.Skin;
+import javafx.scene.layout.StackPane;
+
+
+/**
+ * <p>MaskerPane is designed to be placed alongside other controls in a {@link StackPane},
+ * in order to visually mask these controls, preventing them from being accessed
+ * for a short period of time. This comes in handy whenever waiting on asynchronous
+ * code to finish, and you do not want the user to be able to modify the state
+ * of the UI while waiting.</p>
+ *
+ * <p>To use this control, it is necessary to place it as the last child in a {@link StackPane},
+ * with the other children being masked by this MaskerPane when visible. Simply use
+ * {@link #setVisible(boolean)} to toggle between visible states.</p>
+ */
+public class MaskerPane extends ControlsFXControl {
+
+    /**************************************************************************
+     *
+     * Constructors
+     *
+     **************************************************************************/
+
+    /**
+     * Construct a new {@link MaskerPane}
+     */
+    public MaskerPane() { getStyleClass().add("masker-pane"); } //$NON-NLS-1$
+
+
+
+    /**************************************************************************
+     *
+     * Properties
+     *
+     **************************************************************************/
+
+    // -- Background Color
+
+    // -- Progress
+    private final DoubleProperty progress = new SimpleDoubleProperty(this, "progress", -1.0); //$NON-NLS-1$
+    public final DoubleProperty progressProperty() { return progress; }
+    public final double getProgress() { return progress.get(); }
+    public final void setProgress(double progress) { this.progress.set(progress); }
+
+    // -- Progress Node
+    private final ObjectProperty<Node> progressNode = new SimpleObjectProperty<Node>() {
+        {
+            ProgressIndicator node = new ProgressIndicator();
+            node.progressProperty().bind(progress);
+            setValue(node);
+        }
+
+        @Override public String getName() { return "progressNode"; } //$NON-NLS-1$
+        @Override public Object getBean() { return MaskerPane.this; }
+    };
+    public final ObjectProperty<Node> progressNodeProperty() { return progressNode; }
+    public final Node getProgressNode() { return progressNode.get();}
+    public final void setProgressNode(Node progressNode) { this.progressNode.set(progressNode); }
+
+    // -- Progress Visibility
+    private final BooleanProperty progressVisible = new SimpleBooleanProperty(this, "progressVisible", true); //$NON-NLS-1$
+    public final BooleanProperty progressVisibleProperty() { return progressVisible; }
+    public final boolean getProgressVisible() { return progressVisible.get(); }
+    public final void setProgressVisible(boolean progressVisible) { this.progressVisible.set(progressVisible); }
+
+    // -- Text
+    private final StringProperty text = new SimpleStringProperty(this, "text", "Please Wait..."); //$NON-NLS-1$
+    public final StringProperty textProperty() { return text; }
+    public final String getText() { return text.get(); }
+    public final void setText(String text) { this.text.set(text); }
+
+
+
+    /**************************************************************************
+     *
+     * Interface implementation
+     *
+     **************************************************************************/
+
+    /** {@inheritDoc} */
+    @Override protected Skin<?> createDefaultSkin() { return new MaskerPaneSkin(this); }
+
+    /** {@inheritDoc} */
+    @Override  public String getUserAgentStylesheet() { return getUserAgentStylesheet(MaskerPane.class, "maskerpane.css"); } //$NON-NLS-1$
+}
\ No newline at end of file
diff --git a/controlsfx/src/main/java/org/controlsfx/control/MasterDetailPane.java b/controlsfx/src/main/java/org/controlsfx/control/MasterDetailPane.java
new file mode 100644
index 0000000..18f1061
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/MasterDetailPane.java
@@ -0,0 +1,386 @@
+/**
+ * Copyright (c) 2014, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control;
+
+import impl.org.controlsfx.skin.MasterDetailPaneSkin;
+
+import java.util.Objects;
+
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.geometry.Pos;
+import javafx.geometry.Side;
+import javafx.scene.Node;
+import javafx.scene.control.Label;
+import javafx.scene.control.Skin;
+
+/**
+ * A master / detail pane is used to display two nodes with a strong
+ * relationship to each other. Most of the time the user works with the
+ * information displayed in the master node but every once in a while additional
+ * information is required and can be made visible via the detail node. By
+ * default the detail appears with a short slide-in animation and disappears
+ * with a slide-out. This control allows the detail node to be positioned in
+ * four different locations (top, bottom, left, or right). 
+ * <h3>Screenshot</h3>
+ * To better describe what a master / detail pane is, please refer to the picture
+ * below:
+ * <center><img src="masterDetailPane.png" alt="Screenshot of MasterDetailPane"></center>
+ * <h3>Code Sample</h3>
+ * <pre>
+ * {@code
+ * MasterDetailPane pane = new MasterDetailPane();
+ * pane.setMasterNode(new TableView());
+ * pane.setDetailNode(new PropertySheet());
+ * pane.setDetailSide(Side.RIGHT);
+ * pane.setShowDetailNode(true);
+ * }</pre>
+ */
+public class MasterDetailPane extends ControlsFXControl {
+
+    /**
+     * Constructs a new pane.
+     * 
+     * @param side
+     *            the position where the detail will be shown (top, bottom,
+     *            left, right)
+     * @param masterNode
+     *            the master node (always visible)
+     * @param detailNode
+     *            the detail node (slides in and out)
+     * @param showDetail
+     *            the initial state of the detail node (shown or hidden)
+     */
+    public MasterDetailPane(Side side, Node masterNode, Node detailNode,
+            boolean showDetail) {
+
+        super();
+
+        Objects.requireNonNull(side);
+        Objects.requireNonNull(masterNode);
+        Objects.requireNonNull(detailNode);
+        
+        getStyleClass().add("master-detail-pane"); //$NON-NLS-1$
+
+        setDetailSide(side);
+        setMasterNode(masterNode);
+        setDetailNode(detailNode);
+        setShowDetailNode(showDetail);
+
+        switch (side) {
+        case BOTTOM:
+        case RIGHT:
+            setDividerPosition(.8);
+            break;
+        case TOP:
+        case LEFT:
+            setDividerPosition(.2);
+            break;
+        default:
+            break;
+
+        }
+    }
+
+    /**
+     * Constructs a new pane with two placeholder nodes.
+     * 
+     * @param pos
+     *            the position where the details will be shown (top, bottom,
+     *            left, right)
+     * @param showDetail
+     *            the initial state of the detail node (shown or hidden)
+     */
+    public MasterDetailPane(Side pos, boolean showDetail) {
+        this(pos, new Placeholder(true), new Placeholder(false), showDetail);
+    }
+
+    /**
+     * Constructs a new pane with two placeholder nodes. The detail node will be
+     * shown.
+     * 
+     * @param pos
+     *            the position where the details will be shown (top, bottom,
+     *            left, right)
+     */
+    public MasterDetailPane(Side pos) {
+        this(pos, new Placeholder(true), new Placeholder(false), true);
+    }
+
+    /**
+     * Constructs a new pane with two placeholder nodes. The detail node will be
+     * shown and to the right of the master node.
+     */
+    public MasterDetailPane() {
+        this(Side.RIGHT, new Placeholder(true), new Placeholder(false), true);
+    }
+
+    /** {@inheritDoc} */
+    @Override protected Skin<?> createDefaultSkin() {
+        return new MasterDetailPaneSkin(this);
+    }
+    
+    /** {@inheritDoc} */
+    @Override public String getUserAgentStylesheet() {
+        return getUserAgentStylesheet(MasterDetailPane.class, "masterdetailpane.css");
+    }
+
+    // Detail postion support
+
+    private final ObjectProperty<Side> detailSide = new SimpleObjectProperty<>(
+            this, "detailSide", Side.RIGHT); //$NON-NLS-1$
+
+    /**
+     * The property used to store the side where the detail node will be shown.
+     * 
+     * @return the details side property
+     */
+    public final ObjectProperty<Side> detailSideProperty() {
+        return detailSide;
+    }
+
+    /**
+     * Returns the value of the detail side property.
+     * 
+     * @return the side where the detail node will be shown (left, right, top,
+     *         bottom)
+     */
+    public final Side getDetailSide() {
+        return detailSideProperty().get();
+    }
+
+    /**
+     * Sets the value of the detail side property.
+     * 
+     * @param side
+     *            the side where the detail node will be shown (left, right,
+     *            top, bottom)
+     */
+    public final void setDetailSide(Side side) {
+        Objects.requireNonNull(side);
+        detailSideProperty().set(side);
+    }
+
+    // Show / hide detail node support.
+
+    private final BooleanProperty showDetailNode = new SimpleBooleanProperty(
+            this, "showDetailNode", true); //$NON-NLS-1$
+
+    /**
+     * The property used to store the visibility of the detail node.
+     * 
+     * @return true if the pane is currently expanded (shows the detail node)
+     */
+    public final BooleanProperty showDetailNodeProperty() {
+        return showDetailNode;
+    }
+
+    /**
+     * Returns the value of the "show detail node" property.
+     * 
+     * @return true if the pane is currently expanded (shows the detail node)
+     */
+    public final boolean isShowDetailNode() {
+        return showDetailNodeProperty().get();
+    }
+
+    /**
+     * Sets the value of the "show detail node" property.
+     * 
+     * @param show
+     *            if true the pane will show the detail node
+     */
+    public final void setShowDetailNode(boolean show) {
+        showDetailNodeProperty().set(show);
+    }
+
+    // Master node support.
+
+    private final ObjectProperty<Node> masterNode = new SimpleObjectProperty<>(
+            this, "masterNode"); //$NON-NLS-1$
+
+    /**
+     * The property used to store the master node.
+     * 
+     * @return the master node property
+     */
+    public final ObjectProperty<Node> masterNodeProperty() {
+        return masterNode;
+    }
+
+    /**
+     * Returns the value of the master node property.
+     * 
+     * @return the master node
+     */
+    public final Node getMasterNode() {
+        return masterNodeProperty().get();
+    }
+
+    /**
+     * Sets the value of the master node property.
+     * 
+     * @param node
+     *            the new master node
+     */
+    public final void setMasterNode(Node node) {
+        Objects.requireNonNull(node);
+        masterNodeProperty().set(node);
+    }
+
+    // Detail node support.
+
+    private final ObjectProperty<Node> detailNode = new SimpleObjectProperty<>(
+            this, "detailNode"); //$NON-NLS-1$
+
+    /**
+     * The property used to store the detail node.
+     * 
+     * @return the detail node property
+     */
+    public final ObjectProperty<Node> detailNodeProperty() {
+        return detailNode;
+    }
+
+    /**
+     * Returns the value of the detail node property.
+     * 
+     * @return the detail node
+     */
+    public final Node getDetailNode() {
+        return detailNodeProperty().get();
+    }
+
+    /**
+     * Sets the value of the detail node property.
+     * 
+     * @param node
+     *            the new master node
+     */
+    public final void setDetailNode(Node node) {
+        Objects.requireNonNull(node);
+        detailNodeProperty().set(node);
+    }
+
+    // Animation support.
+
+    private final BooleanProperty animated = new SimpleBooleanProperty(this,
+            "animated", true); //$NON-NLS-1$
+
+    /**
+     * The property used to store the "animated" flag. If true then the detail
+     * node will be shown / hidden with a short slide in / out animation.
+     * 
+     * @return the "animated" property
+     */
+    public final BooleanProperty animatedProperty() {
+        return animated;
+    }
+
+    /**
+     * Returns the value of the "animated" property.
+     * 
+     * @return true if the detail node will be shown with a short animation
+     *         (slide in)
+     */
+    public final boolean isAnimated() {
+        return animatedProperty().get();
+    }
+
+    /**
+     * Sets the value of the "animated" property.
+     * 
+     * @param animated
+     *            if true the detail node will be shown with a short animation
+     *            (slide in)
+     */
+    public final void setAnimated(boolean animated) {
+        animatedProperty().set(animated);
+    }
+
+    private DoubleProperty dividerPosition = new SimpleDoubleProperty(this,
+            "dividerPosition", .33); //$NON-NLS-1$
+
+    /**
+     * Stores the location of the divider.
+     * 
+     * @return the divider location
+     */
+    public final DoubleProperty dividerPositionProperty() {
+        return dividerPosition;
+    }
+
+    /**
+     * Returns the value of the divider position property.
+     * 
+     * @return the position of the divider
+     */
+    public final double getDividerPosition() {
+        return dividerPosition.get();
+    }
+
+    /**
+     * Sets the value of the divider position property.
+     * 
+     * @param position
+     *            the new divider position.
+     */
+    public final void setDividerPosition(double position) {
+        /**
+         * See https://bitbucket.org/controlsfx/controlsfx/issue/145/divider-position-in-masterdetailpane-is
+         * 
+         * Thie work-around is not the best ever found but at least it works.
+         */
+        if(getDividerPosition() == position){
+            dividerPosition.set(-1);
+        }
+        dividerPosition.set(position);
+    }
+
+    /*
+     * A placeholder for the constructors that do not accept a master or a
+     * detail node.
+     */
+    private static final class Placeholder extends Label {
+
+        public Placeholder(boolean master) {
+            super(master ? "Master" : "Detail"); //$NON-NLS-1$ //$NON-NLS-2$
+
+            setAlignment(Pos.CENTER);
+
+            if (master) {
+                setStyle("-fx-background-color: -fx-background;"); //$NON-NLS-1$
+            } else {
+                setStyle("-fx-background-color: derive(-fx-background, -10%);"); //$NON-NLS-1$
+            }
+        }
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/NotificationPane.java b/controlsfx/src/main/java/org/controlsfx/control/NotificationPane.java
new file mode 100644
index 0000000..4bb82b9
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/NotificationPane.java
@@ -0,0 +1,630 @@
+/**
+ * Copyright (c) 2013, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control;
+
+import impl.org.controlsfx.skin.NotificationPaneSkin;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.ReadOnlyBooleanProperty;
+import javafx.beans.property.ReadOnlyBooleanWrapper;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.event.Event;
+import javafx.event.EventHandler;
+import javafx.event.EventType;
+import javafx.scene.Node;
+import javafx.scene.control.Skin;
+import javafx.scene.web.WebView;
+
+import org.controlsfx.control.action.Action;
+
+/**
+ * The NotificationPane control is a container control that, when prompted by
+ * the {@link #show()} method, will show a non-modal message to the user. The
+ * notification appears as a bar that will slide in to their application window,
+ * either from the top or the bottom of the NotificationPane (based on 
+ * {@link #showFromTopProperty()}) wherever that may be in the scenegraph.
+ * 
+ * <h3>Alternative Styling</h3>
+ * <p>As is visible in the screenshots further down this documentation, 
+ * there are two different styles supported by the NotificationPane control.
+ * Firstly, there is the default style based on the JavaFX Modena look. The 
+ * alternative style is what is currently referred to as the 'dark' look. To 
+ * enable this functionality, simply do the following:
+ * 
+ * <pre>
+ * {@code
+ * NotificationPane notificationPane = new NotificationPane();   
+ * notificationPane.getStyleClass().add(NotificationPane.STYLE_CLASS_DARK);
+ * }</pre>
+ * 
+ * <h3>Screenshots</h3>
+ * <p>To better explain NotificationPane, here is a table showing both the
+ * default and dark look for the NotificationPane on a Windows machine (although
+ * that shouldn't impact the visuals a great deal). Also, to show the difference
+ * between top and bottom placement, these two modes are also captured in the
+ * screenshots below:
+ * 
+ * <br>
+ * <center>
+ * <table style="border: 1px solid gray; max-width:750px" summary="NotificationPane Screenshots">
+ *   <tr>
+ *     <th width="200"><center><h3>Setting</h3></center></th>
+ *     <th width="520"><center><h3>Screenshot</h3></center></th>
+ *   </tr>
+ *   <tr>
+ *     <td valign="top" style="text-align:center;"><strong>Light theme from top:</strong></td>
+ *     <td><center><img src="notication-pane-light-top.png" alt="Screenshot of NotificationPane - Light theme from top"></center></td>
+ *   </tr>
+ *   <tr>
+ *     <td valign="top" style="text-align:center;"><strong>Light theme from bottom:</strong></td>
+ *     <td><center><img src="notication-pane-light-bottom.png" alt="Screenshot of NotificationPane - Light theme from bottom"></center></td>
+ *   </tr>
+ *   <tr>
+ *     <td valign="top" style="text-align:center;"><strong>Dark theme from top:</strong></td>
+ *     <td><center><img src="notication-pane-dark-top.png" alt="Screenshot of NotificationPane - Dark theme from top"></center></td>
+ *   </tr>
+ *   <tr>
+ *     <td valign="top" style="text-align:center;"><strong>Dark theme from bottom:</strong></td>
+ *     <td><center><img src="notication-pane-dark-bottom.png" alt="Screenshot of NotificationPane - Dark theme from bottom"></center></td>
+ *   </tr>
+ * </table>
+ * </center>
+ * 
+ * <h3>Code Examples</h3>
+ * 
+ * <p>NotificationPane is a conceptually very simple control - you simply create
+ * your user interface as you normally would, and then wrap it inside the 
+ * NotificationPane. You can then show a notification bar on top of your content
+ * simply by calling {@link #show()} on the notification bar. Here is an example: 
+ * 
+ * <pre>
+ * {@code
+ * // Create a WebView
+ * WebView webView = new WebView();
+ * 
+ * // Wrap it inside a NotificationPane
+ * NotificationPane notificationPane = new NotificationPane(webView);
+ * 
+ * // and put the NotificationPane inside a Tab
+ * Tab tab1 = new Tab("Tab 1");
+ * tab1.setContent(notificationPane);
+ * 
+ * // and the Tab inside a TabPane. We just have one tab here, but of course 
+ * // you can have more!
+ * TabPane tabPane = new TabPane();
+ * tabPane.getTabs().addAll(tab1);
+ * }</pre>
+ * 
+ * <p>Now that the notification pane is installed inside the tab, at some point
+ * later in you application lifecycle, you can do something like the following
+ * to have the notification bar slide into view:
+ * 
+ * <pre>
+ * {@code 
+ * notificationPane.setText("Do you want to save your password?");
+ * notificationPane.getActions().add(new AbstractAction("Save Password") {
+ *     public void execute(ActionEvent ae) {
+ *         // do save...
+ *           
+ *         // then hide...
+ *         notificationPane.hide();
+ *     }
+ * }}</pre>
+ * 
+ * @see Action
+ */
+public class NotificationPane extends ControlsFXControl {
+    
+    /***************************************************************************
+     * 
+     * Static fields
+     * 
+     **************************************************************************/
+    
+    public static final String STYLE_CLASS_DARK = "dark"; //$NON-NLS-1$
+    
+    /**
+     * Called when the NotificationPane <b>will</b> be shown.
+     */
+    public static final EventType<Event> ON_SHOWING =
+            new EventType<>(Event.ANY, "NOTIFICATION_PANE_ON_SHOWING"); //$NON-NLS-1$
+
+    /**
+     * Called when the NotificationPane shows.
+     */
+    public static final EventType<Event> ON_SHOWN =
+            new EventType<>(Event.ANY, "NOTIFICATION_PANE_ON_SHOWN"); //$NON-NLS-1$
+
+    /**
+     * Called when the NotificationPane <b>will</b> be hidden.
+     */
+    public static final EventType<Event> ON_HIDING =
+            new EventType<>(Event.ANY, "NOTIFICATION_PANE_ON_HIDING"); //$NON-NLS-1$
+
+    /**
+     * Called when the NotificationPane is hidden.
+     */
+    public static final EventType<Event> ON_HIDDEN =
+            new EventType<>(Event.ANY, "NOTIFICATION_PANE_ON_HIDDEN"); //$NON-NLS-1$
+    
+    
+    
+    /***************************************************************************
+     * 
+     * Constructors
+     * 
+     **************************************************************************/
+    
+    /**
+     * Creates an instance of NotificationPane with no 
+     * {@link #contentProperty() content}, {@link #textProperty() text}, 
+     * {@link #graphicProperty() graphic} properties set, and no 
+     * {@link #getActions() actions} specified.
+     */
+    public NotificationPane() {
+        this(null);
+    }
+    
+    /**
+     * Creates an instance of NotificationPane with the 
+     * {@link #contentProperty() content} property set, but no 
+     * {@link #textProperty() text} or
+     * {@link #graphicProperty() graphic} property set, and no
+     * {@link #getActions() actions} specified.
+     * 
+     * @param content The content to show in the NotificationPane behind where
+     *      the notification bar will appear, that is, the content 
+     *      <strong>will not</strong>appear in the notification bar. 
+     */
+    public NotificationPane(Node content) {
+        getStyleClass().add(DEFAULT_STYLE_CLASS);
+        setContent(content);
+        
+        updateStyleClasses();
+    }
+    
+    
+    
+    /***************************************************************************
+     * 
+     * Overriding public API
+     * 
+     **************************************************************************/
+    
+    /** {@inheritDoc} */
+    @Override protected Skin<?> createDefaultSkin() {
+        return new NotificationPaneSkin(this);
+    }
+    
+    /** {@inheritDoc} */
+    @Override public String getUserAgentStylesheet() {
+        return getUserAgentStylesheet(NotificationPane.class, "notificationpane.css");
+    }
+    
+    /***************************************************************************
+     * 
+     * Properties
+     * 
+     **************************************************************************/
+    
+    // --- content
+    private ObjectProperty<Node> content = new SimpleObjectProperty<>(this, "content"); //$NON-NLS-1$
+    
+    /**
+     * The content property represents what is shown in the scene 
+     * <strong>that is not within</strong> the notification bar. In other words,
+     * it is what the notification bar should appear on top of. For example, in
+     * the scenario where you are using a {@link WebView} to show to the user
+     * websites, and you want to popup up a notification bar to save a password,
+     * the content would be the {@link WebView}. Refer to the 
+     * {@link NotificationPane} class documentation for more details.
+     *  
+     * @return A property representing the content of this NotificationPane.
+     */
+    public final ObjectProperty<Node> contentProperty() {
+        return content;
+    }
+    
+    /**
+     * Set the content to be shown in the scene, 
+     * <strong>that is not within</strong> the notification bar.
+     * @param value 
+     */
+    public final void setContent(Node value) {
+        this.content.set(value); 
+    }
+    
+    /**
+     * 
+     * @return The content shown in the scene.
+     */
+    public final Node getContent() {
+        return content.get();
+    }
+    
+    
+    // --- text
+    private StringProperty text = new SimpleStringProperty(this, "text"); //$NON-NLS-1$
+    
+    /**
+     * The text property represents the text to show within the popup 
+     * notification bar that appears on top of the 
+     * {@link #contentProperty() content} that is within the NotificationPane.
+     * 
+     * @return A property representing the text shown in the notification bar.
+     */
+    public final StringProperty textProperty() {
+        return text;
+    }
+    
+    /**
+     * Sets the text to show within the popup 
+     * notification bar that appears on top of the 
+     * {@link #contentProperty() content}.
+     * @param value 
+     */
+    public final void setText(String value) {
+        this.text.set(value); 
+    }
+    
+    /**
+     * 
+     * @return the text showing within the popup 
+     * notification bar that appears on top of the 
+     * {@link #contentProperty() content}.
+     */
+    public final String getText() {
+        return text.get();
+    }
+    
+    
+    // --- graphic
+    private ObjectProperty<Node> graphic = new SimpleObjectProperty<>(this, "graphic"); //$NON-NLS-1$
+    
+    /**
+     * The graphic property represents the {@link Node} to show within the popup 
+     * notification bar that appears on top of the 
+     * {@link #contentProperty() content} that is within the NotificationPane.
+     * Despite the term 'graphic', this can be an arbitrarily complex scenegraph
+     * in its own right.
+     * 
+     * @return A property representing the graphic shown in the notification bar.
+     */
+    public final ObjectProperty<Node> graphicProperty() {
+        return graphic;
+    }
+    
+    /**
+     * Sets the {@link Node} to show within the popup 
+     * notification bar.
+     * @param value 
+     */
+    public final void setGraphic(Node value) {
+        this.graphic.set(value); 
+    }
+    
+    /**
+     * 
+     * @return the {@link Node} to show within the popup 
+     * notification bar.
+     */
+    public final Node getGraphic() {
+        return graphic.get();
+    }
+    
+    
+    // --- showing
+    private ReadOnlyBooleanWrapper showing = new ReadOnlyBooleanWrapper(this, "showing"); //$NON-NLS-1$
+    
+    /**
+     * A read-only property that represents whether the notification bar popup
+     * should be showing to the user or not. To toggle visibility, use the
+     * {@link #show()} and {@link #hide()} methods.
+     * 
+     * @return A property representing whether the notification bar is currently showing.
+     */
+    public final ReadOnlyBooleanProperty showingProperty() {
+        return showing.getReadOnlyProperty();
+    }
+    private final void setShowing(boolean value) {
+        this.showing.set(value); 
+    }
+    /**
+     * 
+     * @return whether the notification bar is currently showing.
+     */
+    public final boolean isShowing() {
+        return showing.get();
+    }
+    
+    
+    // --- show from top
+    private BooleanProperty showFromTop = new SimpleBooleanProperty(this, "showFromTop", true) { //$NON-NLS-1$
+        @Override protected void invalidated() {
+            updateStyleClasses();
+        }
+    };
+    
+    /**
+     * A property representing whether the notification bar should appear from the
+     * top or the bottom of the NotificationPane area. By default it will appear 
+     * from the top, but this can be changed by setting this property to false.
+     * 
+     * @return A property representing where the notification bar should appear from.
+     */
+    public final BooleanProperty showFromTopProperty() {
+        return showFromTop;
+    }
+    
+    /**
+     * Sets whether the notification bar should appear from the
+     * top or the bottom of the NotificationPane area.
+     * @param value 
+     */
+    public final void setShowFromTop(boolean value) {
+        this.showFromTop.set(value); 
+    }
+    
+    /**
+     * @return whether the notification bar is appearing from the
+     * top or the bottom of the NotificationPane area.
+     */
+    public final boolean isShowFromTop() {
+        return showFromTop.get();
+    }
+    
+    
+    // --- On Showing
+    public final ObjectProperty<EventHandler<Event>> onShowingProperty() { return onShowing; }
+    /**
+     * Called just prior to the {@code NotificationPane} being shown.
+     */
+    public final void setOnShowing(EventHandler<Event> value) { onShowingProperty().set(value); }
+    public final EventHandler<Event> getOnShowing() { return onShowingProperty().get(); }
+    private ObjectProperty<EventHandler<Event>> onShowing = new SimpleObjectProperty<EventHandler<Event>>(this, "onShowing") { //$NON-NLS-1$
+        @Override protected void invalidated() {
+            setEventHandler(ON_SHOWING, get());
+         }
+     };
+
+
+    // -- On Shown
+    public final ObjectProperty<EventHandler<Event>> onShownProperty() { return onShown; }
+    /**
+     * Called just after the {@link NotificationPane} is shown.
+     */
+    public final void setOnShown(EventHandler<Event> value) { onShownProperty().set(value); }
+    public final EventHandler<Event> getOnShown() { return onShownProperty().get(); }
+    private ObjectProperty<EventHandler<Event>> onShown = new SimpleObjectProperty<EventHandler<Event>>(this, "onShown") { //$NON-NLS-1$
+        @Override protected void invalidated() {
+            setEventHandler(ON_SHOWN, get());
+        }
+    };
+
+
+    // --- On Hiding
+    public final ObjectProperty<EventHandler<Event>> onHidingProperty() { return onHiding; }
+    /**
+     * Called just prior to the {@link NotificationPane} being hidden.
+     */
+    public final void setOnHiding(EventHandler<Event> value) { onHidingProperty().set(value); }
+    public final EventHandler<Event> getOnHiding() { return onHidingProperty().get(); }
+    private ObjectProperty<EventHandler<Event>> onHiding = new SimpleObjectProperty<EventHandler<Event>>(this, "onHiding") { //$NON-NLS-1$
+        @Override protected void invalidated() {
+            setEventHandler(ON_HIDING, get());
+        }
+    };
+
+
+    // --- On Hidden
+    public final ObjectProperty<EventHandler<Event>> onHiddenProperty() { return onHidden; }
+    /**
+     * Called just after the {@link NotificationPane} has been hidden.
+     */
+    public final void setOnHidden(EventHandler<Event> value) { onHiddenProperty().set(value); }
+    public final EventHandler<Event> getOnHidden() { return onHiddenProperty().get(); }
+    private ObjectProperty<EventHandler<Event>> onHidden = new SimpleObjectProperty<EventHandler<Event>>(this, "onHidden") { //$NON-NLS-1$
+        @Override protected void invalidated() {
+            setEventHandler(ON_HIDDEN, get());
+        }
+    };
+    
+    // --- close button visibility
+    private BooleanProperty closeButtonVisible = new SimpleBooleanProperty(this, "closeButtonVisible", true); //$NON-NLS-1$
+        
+    /**
+     * A property representing whether the close button in the {@code NotificationPane} should be visible or not.
+     * By default it will appear but this can be changed by setting this property to false.
+     * 
+     * @return A property representing whether the close button in the {@code NotificationPane} should be visible.
+     */
+    public final BooleanProperty closeButtonVisibleProperty() {
+        return closeButtonVisible;
+    }
+    
+    /**
+     * Sets whether the close button in {@code NotificationPane} should be visible.
+     * 
+     * @param value 
+     */
+    public final void setCloseButtonVisible(boolean value) {
+        this.closeButtonVisible.set(value);
+    }
+    
+    /**
+     * @return whether the close button in {@code NotificationPane} is visible.
+     */
+    public final boolean isCloseButtonVisible() {
+        return closeButtonVisible.get();
+    }
+    
+    /***************************************************************************
+     * 
+     * Public API
+     * 
+     **************************************************************************/
+    
+    // --- actions
+    private final ObservableList<Action> actions = FXCollections.<Action> observableArrayList();
+
+    /**
+     * Observable list of actions used for the actions area of the notification 
+     * bar. Modifying the contents of this list will change the actions available to
+     * the user.
+     * @return The {@link ObservableList} of actions available to the user.
+     */
+    public final ObservableList<Action> getActions() {
+        return actions;
+    }
+    
+    /**
+     * Call this to make the notification bar appear on top of the 
+     * {@link #contentProperty() content} of this {@link NotificationPane}.
+     * If the notification bar is already showing this will be a no-op.
+     */
+    public void show() {
+        setShowing(true);
+    }
+    
+    /**
+     * Shows the NotificationPane with the 
+     * {@link #contentProperty() content} and {@link #textProperty() text} 
+     * property set, but no {@link #graphicProperty() graphic} property set, and
+     * no {@link #getActions() actions} specified.
+     * 
+     * @param text The text to show in the notification pane.
+     */
+    public void show(final String text) {
+        hideAndThen(new Runnable() {
+            @Override public void run() {
+                setText(text);
+                setShowing(true);
+            }
+        });
+    }
+    
+    /**
+     * Shows the NotificationPane with the 
+     * {@link #contentProperty() content}, {@link #textProperty() text} and 
+     * {@link #graphicProperty() graphic} properties set, but no 
+     * {@link #getActions() actions} specified.
+     * 
+     * @param text The text to show in the notification pane.
+     * @param graphic The node to show in the notification pane.
+     */
+    public void show(final String text, final Node graphic) {
+        hideAndThen(new Runnable() {
+            @Override public void run() {
+                setText(text);
+                setGraphic(graphic);
+                setShowing(true);
+            }
+        });
+    }
+    
+    /**
+     * Shows the NotificationPane with the 
+     * {@link #contentProperty() content}, {@link #textProperty() text} and 
+     * {@link #graphicProperty() graphic} property set, and the provided actions 
+     * copied into the {@link #getActions() actions} list.
+     * 
+     * @param text The text to show in the notification pane.
+     * @param graphic The node to show in the notification pane.
+     * @param actions The actions to show in the notification pane.
+     */
+    public void show(final String text, final Node graphic, final Action... actions) {
+        hideAndThen(new Runnable() {
+            @Override public void run() {
+                setText(text);
+                setGraphic(graphic);
+
+                if (actions == null) {
+                    getActions().clear();
+                } else {
+                    for (Action action : actions) {
+                        if (action == null) continue;
+                        getActions().add(action);
+                    }
+                }
+                
+                setShowing(true);
+            }
+        });
+    }
+    
+    /**
+     * Call this to make the notification bar disappear from the 
+     * {@link #contentProperty() content} of this {@link NotificationPane}.
+     * If the notification bar is already hidden this will be a no-op.
+     */
+    public void hide() {
+        setShowing(false);
+    }
+    
+    
+    
+    /**************************************************************************
+     *                                                                         *
+     * Private Implementation                                                  *
+     *                                                                         *
+     **************************************************************************/
+    
+    private void updateStyleClasses() {
+        getStyleClass().removeAll("top", "bottom"); //$NON-NLS-1$ //$NON-NLS-2$
+        getStyleClass().add(isShowFromTop() ? "top" : "bottom"); //$NON-NLS-1$ //$NON-NLS-2$
+    }
+    
+    private void hideAndThen(final Runnable r) {
+        if (isShowing()) {
+            final EventHandler<Event> eventHandler = new EventHandler<Event>() {
+                @Override public void handle(Event e) {
+                    r.run();
+                    removeEventHandler(NotificationPane.ON_HIDDEN, this);
+                }
+            };
+            addEventHandler(NotificationPane.ON_HIDDEN, eventHandler);
+            hide();
+        } else {
+            r.run();
+        }
+    }
+    
+    
+    
+    /**************************************************************************
+     *                                                                         *
+     * Stylesheet Handling                                                     *
+     *                                                                         *
+     **************************************************************************/
+     
+     private static final String DEFAULT_STYLE_CLASS = "notification-pane"; //$NON-NLS-1$
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/Notifications.java b/controlsfx/src/main/java/org/controlsfx/control/Notifications.java
new file mode 100644
index 0000000..f223db1
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/Notifications.java
@@ -0,0 +1,645 @@
+/**
+ * Copyright (c) 2014, 2016, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control;
+
+import impl.org.controlsfx.skin.NotificationBar;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import javafx.animation.KeyFrame;
+import javafx.animation.KeyValue;
+import javafx.animation.ParallelTransition;
+import javafx.animation.Timeline;
+import javafx.animation.Transition;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.geometry.Pos;
+import javafx.geometry.Rectangle2D;
+import javafx.scene.Node;
+import javafx.scene.Scene;
+import javafx.scene.image.ImageView;
+import javafx.stage.Popup;
+import javafx.stage.PopupWindow;
+import javafx.stage.Screen;
+import javafx.stage.Window;
+import javafx.util.Duration;
+
+import org.controlsfx.control.action.Action;
+import org.controlsfx.tools.Utils;
+
+/**
+ * An API to show popup notification messages to the user in the corner of their
+ * screen, unlike the {@link NotificationPane} which shows notification messages
+ * within your application itself.
+ * 
+ * <h3>Screenshot</h3>
+ * <p>
+ * The following screenshot shows a sample notification rising from the
+ * bottom-right corner of my screen:
+ * 
+ * <br>
+ * <br>
+ * <img src="notifications.png" alt="Screenshot of Notifications">
+ * 
+ * <h3>Code Example:</h3>
+ * <p>
+ * To create the notification shown in the screenshot, simply do the following:
+ * 
+ * <pre>
+ * {@code
+ * Notifications.create()
+ *              .title("Title Text")
+ *              .text("Hello World 0!")
+ *              .showWarning();
+ * }
+ * </pre>
+ */
+public class Notifications {
+
+    /***************************************************************************
+     * * Static fields * *
+     **************************************************************************/
+
+    private static final String STYLE_CLASS_DARK = "dark"; //$NON-NLS-1$
+
+    /***************************************************************************
+     * * Private fields * *
+     **************************************************************************/
+
+    private String title;
+    private String text;
+    private Node graphic;
+    private ObservableList<Action> actions = FXCollections.observableArrayList();
+    private Pos position = Pos.BOTTOM_RIGHT;
+    private Duration hideAfterDuration = Duration.seconds(5);
+    private boolean hideCloseButton;
+    private EventHandler<ActionEvent> onAction;
+    private Window owner;
+    private Screen screen = Screen.getPrimary();
+
+    private List<String> styleClass = new ArrayList<>();
+
+    /***************************************************************************
+     * * Constructors * *
+     **************************************************************************/
+
+    // we do not allow instantiation of the Notifications class directly - users
+    // must go via the builder API (that is, calling create())
+    private Notifications() {
+        // no-op
+    }
+
+    /***************************************************************************
+     * * Public API * *
+     **************************************************************************/
+
+    /**
+     * Call this to begin the process of building a notification to show.
+     */
+    public static Notifications create() {
+        return new Notifications();
+    }
+
+    /**
+     * Specify the text to show in the notification.
+     */
+    public Notifications text(String text) {
+        this.text = text;
+        return this;
+    }
+
+    /**
+     * Specify the title to show in the notification.
+     */
+    public Notifications title(String title) {
+        this.title = title;
+        return this;
+    }
+
+    /**
+     * Specify the graphic to show in the notification.
+     */
+    public Notifications graphic(Node graphic) {
+        this.graphic = graphic;
+        return this;
+    }
+
+    /**
+     * Specify the position of the notification on screen, by default it is
+     * {@link Pos#BOTTOM_RIGHT bottom-right}.
+     */
+    public Notifications position(Pos position) {
+        this.position = position;
+        return this;
+    }
+
+    /**
+     * The dialog window owner - which can be {@link Screen}, {@link Window}
+     * or {@link Node}. If specified, the notifications will be inside
+     * the owner, otherwise the notifications will be shown within the whole
+     * primary (default) screen.
+     */
+    public Notifications owner(Object owner) {
+        if (owner instanceof Screen) {
+            this.screen = (Screen) owner;
+        } else {
+            this.owner = Utils.getWindow(owner);
+        }
+        return this;
+    }
+
+    /**
+     * Specify the duration that the notification should show, after which it
+     * will be hidden.
+     */
+    public Notifications hideAfter(Duration duration) {
+        this.hideAfterDuration = duration;
+        return this;
+    }
+
+    /**
+     * Specify what to do when the user clicks on the notification (in addition
+     * to the notification hiding, which happens whenever the notification is
+     * clicked on).
+     */
+    public Notifications onAction(EventHandler<ActionEvent> onAction) {
+        this.onAction = onAction;
+        return this;
+    }
+
+    /**
+     * Specify that the notification should use the built-in dark styling,
+     * rather than the default 'modena' notification style (which is a
+     * light-gray).
+     */
+    public Notifications darkStyle() {
+        styleClass.add(STYLE_CLASS_DARK);
+        return this;
+    }
+
+    /**
+     * Specify that the close button in the top-right corner of the notification
+     * should not be shown.
+     */
+    public Notifications hideCloseButton() {
+        this.hideCloseButton = true;
+        return this;
+    }
+
+    /**
+     * Specify the actions that should be shown in the notification as buttons.
+     */
+    public Notifications action(Action... actions) {
+        this.actions = actions == null ? FXCollections.<Action> observableArrayList() : FXCollections
+                .observableArrayList(actions);
+        return this;
+    }
+
+    /**
+     * Instructs the notification to be shown, and that it should use the
+     * built-in 'warning' graphic.
+     */
+    public void showWarning() {
+        graphic(new ImageView(Notifications.class.getResource("/org/controlsfx/dialog/dialog-warning.png").toExternalForm())); //$NON-NLS-1$
+        show();
+    }
+
+    /**
+     * Instructs the notification to be shown, and that it should use the
+     * built-in 'information' graphic.
+     */
+    public void showInformation() {
+        graphic(new ImageView(Notifications.class.getResource("/org/controlsfx/dialog/dialog-information.png").toExternalForm())); //$NON-NLS-1$
+        show();
+    }
+
+    /**
+     * Instructs the notification to be shown, and that it should use the
+     * built-in 'error' graphic.
+     */
+    public void showError() {
+        graphic(new ImageView(Notifications.class.getResource("/org/controlsfx/dialog/dialog-error.png").toExternalForm())); //$NON-NLS-1$
+        show();
+    }
+
+    /**
+     * Instructs the notification to be shown, and that it should use the
+     * built-in 'confirm' graphic.
+     */
+    public void showConfirm() {
+        graphic(new ImageView(Notifications.class.getResource("/org/controlsfx/dialog/dialog-confirm.png").toExternalForm())); //$NON-NLS-1$
+        show();
+    }
+
+    /**
+     * Instructs the notification to be shown.
+     */
+    public void show() {
+        NotificationPopupHandler.getInstance().show(this);
+    }
+
+    /***************************************************************************
+     * * Private support classes * *
+     **************************************************************************/
+
+    // not public so no need for JavaDoc
+    private static final class NotificationPopupHandler {
+
+        private static final NotificationPopupHandler INSTANCE = new NotificationPopupHandler();
+
+        private double startX;
+        private double startY;
+        private double screenWidth;
+        private double screenHeight;
+
+        static final NotificationPopupHandler getInstance() {
+            return INSTANCE;
+        }
+
+        private final Map<Pos, List<Popup>> popupsMap = new HashMap<>();
+        private final double padding = 15;
+
+        // for animating in the notifications
+        private ParallelTransition parallelTransition = new ParallelTransition();
+
+        private boolean isShowing = false;
+
+        public void show(Notifications notification) {
+            Window window;
+            if (notification.owner == null) {
+                /*
+                 * If the owner is not set, we work with the whole screen.
+                 */
+                Rectangle2D screenBounds = notification.screen.getVisualBounds();
+                startX = screenBounds.getMinX();
+                startY = screenBounds.getMinY();
+                screenWidth = screenBounds.getWidth();
+                screenHeight = screenBounds.getHeight();
+
+                window = Utils.getWindow(null);
+            } else {
+                /*
+                 * If the owner is set, we will make the notifications popup
+                 * inside its window.
+                 */
+                startX = notification.owner.getX();
+                startY = notification.owner.getY();
+                screenWidth = notification.owner.getWidth();
+                screenHeight = notification.owner.getHeight();
+                window = notification.owner;
+            }
+            show(window, notification);
+        }
+
+        private void show(Window owner, final Notifications notification) {
+            // Stylesheets which are added to the scene of a popup aren't 
+            // considered for styling. For this reason, we need to find the next 
+            // window in the hierarchy which isn't a popup.  
+            Window ownerWindow = owner;
+            while (ownerWindow instanceof PopupWindow) {
+                ownerWindow = ((PopupWindow) ownerWindow).getOwnerWindow();
+            }
+            // need to install our CSS
+            Scene ownerScene = ownerWindow.getScene();
+            if (ownerScene != null) {
+                String stylesheetUrl = Notifications.class.getResource("notificationpopup.css").toExternalForm(); //$NON-NLS-1$
+                if (!ownerScene.getStylesheets().contains(stylesheetUrl)) {
+                    // The stylesheet needs to be added at the beginning so that
+                    // the styling can be adjusted with custom stylesheets.
+                    ownerScene.getStylesheets().add(0, stylesheetUrl);
+                }
+            }
+
+            final Popup popup = new Popup();
+            popup.setAutoFix(false);
+
+            final Pos p = notification.position;
+
+            final NotificationBar notificationBar = new NotificationBar() {
+                @Override public String getTitle() {
+                    return notification.title;
+                }
+
+                @Override public String getText() {
+                    return notification.text;
+                }
+
+                @Override public Node getGraphic() {
+                    return notification.graphic;
+                }
+
+                @Override public ObservableList<Action> getActions() {
+                    return notification.actions;
+                }
+
+                @Override public boolean isShowing() {
+                    return isShowing;
+                }
+
+                @Override protected double computeMinWidth(double height) {
+                    String text = getText();
+                    Node graphic = getGraphic();
+                    if ((text == null || text.isEmpty()) && (graphic != null)) {
+                        return graphic.minWidth(height);
+                    }
+                    return 400;
+                }
+
+                @Override protected double computeMinHeight(double width) {
+                    String text = getText();
+                    Node graphic = getGraphic();
+                    if ((text == null || text.isEmpty()) && (graphic != null)) {
+                        return graphic.minHeight(width);
+                    }
+                    return 100;
+                }
+
+                @Override public boolean isShowFromTop() {
+                    return NotificationPopupHandler.this.isShowFromTop(notification.position);
+                }
+
+                @Override public void hide() {
+                    isShowing = false;
+
+                    // this would slide the notification bar out of view,
+                    // but I prefer the fade out below
+                    // doHide();
+
+                    // animate out the popup by fading it
+                    createHideTimeline(popup, this, p, Duration.ZERO).play();
+                }
+
+                @Override public boolean isCloseButtonVisible() {
+                    return !notification.hideCloseButton;
+                }
+
+                @Override public double getContainerHeight() {
+                    return startY + screenHeight;
+                }
+
+                @Override public void relocateInParent(double x, double y) {
+                    // this allows for us to slide the notification upwards
+                    switch (p) {
+                    case BOTTOM_LEFT:
+                    case BOTTOM_CENTER:
+                    case BOTTOM_RIGHT:
+                        popup.setAnchorY(y - padding);
+                        break;
+                    default:
+                        // no-op
+                        break;
+                    }
+                }
+            };
+
+            notificationBar.getStyleClass().addAll(notification.styleClass);
+
+            notificationBar.setOnMouseClicked(e -> {
+                if (notification.onAction != null) {
+                    ActionEvent actionEvent = new ActionEvent(notificationBar, notificationBar);
+                    notification.onAction.handle(actionEvent);
+
+                    // animate out the popup
+                    createHideTimeline(popup, notificationBar, p, Duration.ZERO).play();
+                }
+            });
+
+            popup.getContent().add(notificationBar);
+            popup.show(owner, 0, 0);
+
+            // determine location for the popup
+            double anchorX = 0, anchorY = 0;
+            final double barWidth = notificationBar.getWidth();
+            final double barHeight = notificationBar.getHeight();
+
+            // get anchorX
+            switch (p) {
+            case TOP_LEFT:
+            case CENTER_LEFT:
+            case BOTTOM_LEFT:
+                anchorX = padding + startX;
+                break;
+
+            case TOP_CENTER:
+            case CENTER:
+            case BOTTOM_CENTER:
+                anchorX = startX + (screenWidth / 2.0) - barWidth / 2.0 - padding / 2.0;
+                break;
+
+            default:
+            case TOP_RIGHT:
+            case CENTER_RIGHT:
+            case BOTTOM_RIGHT:
+                anchorX = startX + screenWidth - barWidth - padding;
+                break;
+            }
+
+            // get anchorY
+            switch (p) {
+            case TOP_LEFT:
+            case TOP_CENTER:
+            case TOP_RIGHT:
+                anchorY = padding + startY;
+                break;
+
+            case CENTER_LEFT:
+            case CENTER:
+            case CENTER_RIGHT:
+                anchorY = startY + (screenHeight / 2.0) - barHeight / 2.0 - padding / 2.0;
+                break;
+
+            default:
+            case BOTTOM_LEFT:
+            case BOTTOM_CENTER:
+            case BOTTOM_RIGHT:
+                anchorY = startY + screenHeight - barHeight - padding;
+                break;
+            }
+
+            popup.setAnchorX(anchorX);
+            popup.setAnchorY(anchorY);
+
+            isShowing = true;
+            notificationBar.doShow();
+
+            addPopupToMap(p, popup);
+
+            // begin a timeline to get rid of the popup
+            Timeline timeline = createHideTimeline(popup, notificationBar, p, notification.hideAfterDuration);
+            timeline.play();
+        }
+
+        private void hide(Popup popup, Pos p) {
+            popup.hide();
+            removePopupFromMap(p, popup);
+        }
+
+        private Timeline createHideTimeline(final Popup popup, NotificationBar bar, final Pos p, Duration startDelay) {
+            KeyValue fadeOutBegin = new KeyValue(bar.opacityProperty(), 1.0);
+            KeyValue fadeOutEnd = new KeyValue(bar.opacityProperty(), 0.0);
+
+            KeyFrame kfBegin = new KeyFrame(Duration.ZERO, fadeOutBegin);
+            KeyFrame kfEnd = new KeyFrame(Duration.millis(500), fadeOutEnd);
+
+            Timeline timeline = new Timeline(kfBegin, kfEnd);
+            timeline.setDelay(startDelay);
+            timeline.setOnFinished(new EventHandler<ActionEvent>() {
+                @Override
+                public void handle(ActionEvent e) {
+                    hide(popup, p);
+                }
+            });
+
+            return timeline;
+        }
+
+        private void addPopupToMap(Pos p, Popup popup) {
+            List<Popup> popups;
+            if (!popupsMap.containsKey(p)) {
+                popups = new LinkedList<>();
+                popupsMap.put(p, popups);
+            } else {
+                popups = popupsMap.get(p);
+            }
+
+            doAnimation(p, popup);
+
+            // add the popup to the list so it is kept in memory and can be
+            // accessed later on
+            popups.add(popup);
+        }
+
+        private void removePopupFromMap(Pos p, Popup popup) {
+            if (popupsMap.containsKey(p)) {
+                List<Popup> popups = popupsMap.get(p);
+                popups.remove(popup);
+            }
+        }
+
+        private void doAnimation(Pos p, Popup changedPopup) {
+            List<Popup> popups = popupsMap.get(p);
+            if (popups == null) {
+                return;
+            }
+
+            final double newPopupHeight = changedPopup.getContent().get(0).getBoundsInParent().getHeight();
+
+            parallelTransition.stop();
+            parallelTransition.getChildren().clear();
+
+            final boolean isShowFromTop = isShowFromTop(p);
+
+            // animate all other popups in the list upwards so that the new one
+            // is in the 'new' area.
+            // firstly, we need to determine the target positions for all popups
+            double sum = 0;
+            double targetAnchors[] = new double[popups.size()];
+            for (int i = popups.size() - 1; i >= 0; i--) {
+                Popup _popup = popups.get(i);
+
+                final double popupHeight = _popup.getContent().get(0).getBoundsInParent().getHeight();
+
+                if (isShowFromTop) {
+                    if (i == popups.size() - 1) {
+                        sum = startY + newPopupHeight + padding;
+                    } else {
+                        sum += popupHeight;
+                    }
+                    targetAnchors[i] = sum;
+                } else {
+                    if (i == popups.size() - 1) {
+                        sum = changedPopup.getAnchorY() - popupHeight;
+                    } else {
+                        sum -= popupHeight;
+                    }
+
+                    targetAnchors[i] = sum;
+                }
+            }
+
+            // then we set up animations for each popup to animate towards the
+            // target
+            for (int i = popups.size() - 1; i >= 0; i--) {
+                final Popup _popup = popups.get(i);
+                final double anchorYTarget = targetAnchors[i];
+                if(anchorYTarget < 0){
+                    _popup.hide();
+                }
+                final double oldAnchorY = _popup.getAnchorY();
+                final double distance = anchorYTarget - oldAnchorY;
+
+                Transition t = new CustomTransition(_popup, oldAnchorY, distance);
+                t.setCycleCount(1);
+                parallelTransition.getChildren().add(t);
+            }
+            parallelTransition.play();
+        }
+
+        private boolean isShowFromTop(Pos p) {
+            switch (p) {
+            case TOP_LEFT:
+            case TOP_CENTER:
+            case TOP_RIGHT:
+                return true;
+            default:
+                return false;
+            }
+        }
+
+        class CustomTransition extends Transition {
+
+            private WeakReference<Popup> popupWeakReference;
+            private double oldAnchorY;
+            private double distance;
+
+            CustomTransition(Popup popup, double oldAnchorY, double distance) {
+                popupWeakReference = new WeakReference<>(popup);
+                this.oldAnchorY = oldAnchorY;
+                this.distance = distance;
+                setCycleDuration(Duration.millis(350.0));
+            }
+
+            @Override
+            protected void interpolate(double frac) {
+                Popup popup = popupWeakReference.get();
+                if (popup != null) {
+                    double newAnchorY = oldAnchorY + distance * frac;
+                    popup.setAnchorY(newAnchorY);
+                }
+            }
+
+        }
+    }
+}
+
diff --git a/controlsfx/src/main/java/org/controlsfx/control/PlusMinusSlider.java b/controlsfx/src/main/java/org/controlsfx/control/PlusMinusSlider.java
new file mode 100644
index 0000000..8749ffd
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/PlusMinusSlider.java
@@ -0,0 +1,345 @@
+/**
+ * Copyright (c) 2014, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control;
+
+import impl.org.controlsfx.skin.PlusMinusSliderSkin;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.ObjectPropertyBase;
+import javafx.beans.property.ReadOnlyDoubleProperty;
+import javafx.beans.property.ReadOnlyDoubleWrapper;
+import javafx.collections.MapChangeListener;
+import javafx.css.CssMetaData;
+import javafx.css.PseudoClass;
+import javafx.css.Styleable;
+import javafx.css.StyleableObjectProperty;
+import javafx.css.StyleableProperty;
+import javafx.event.EventHandler;
+import javafx.event.EventTarget;
+import javafx.event.EventType;
+import javafx.geometry.Orientation;
+import javafx.scene.control.Control;
+import javafx.scene.control.Skin;
+import javafx.scene.input.InputEvent;
+
+import com.sun.javafx.css.converters.EnumConverter;
+
+/**
+ * A plus minus slider allows the user to continously fire an event carrying a
+ * value between -1 and +1 by moving a thumb from its center position to the
+ * left or right (or top and bottom) edge of the control. The thumb will
+ * automatically center itself again on the zero position when the user lets go
+ * of the mouse button. Scrolling through a large list of items at different
+ * speeds is one possible use case for a control like this.
+ * 
+ * <center> <img src="plus-minus-slider.png" alt="Screenshot of PlusMinusSlider"> </center>
+ */
+public class PlusMinusSlider extends ControlsFXControl {
+
+	private static final String DEFAULT_STYLE_CLASS = "plus-minus-slider"; //$NON-NLS-1$
+
+	private static final PseudoClass VERTICAL_PSEUDOCLASS_STATE = PseudoClass
+			.getPseudoClass("vertical"); //$NON-NLS-1$
+
+	private static final PseudoClass HORIZONTAL_PSEUDOCLASS_STATE = PseudoClass
+			.getPseudoClass("horizontal"); //$NON-NLS-1$
+
+	/**
+	 * Constructs a new adjuster control with a default horizontal orientation.
+	 */
+	public PlusMinusSlider() {
+		getStyleClass().add(DEFAULT_STYLE_CLASS);
+
+		setOrientation(Orientation.HORIZONTAL);
+
+		/*
+		 * We are "abusing" the properties map to pass the new value of the
+		 * slider from the skin to the control. It has to be done this way
+		 * because the value property of this control is "read-only".
+		 */
+		getProperties().addListener(new MapChangeListener<Object, Object>() {
+			@Override
+            public void onChanged(MapChangeListener.Change<? extends Object, ? extends Object> change) {
+				if (change.getKey().equals("plusminusslidervalue")) { //$NON-NLS-1$
+					if (change.getValueAdded() != null) {
+						Double valueAdded = (Double) change.getValueAdded();
+						value.set(valueAdded);
+						change.getMap().remove("plusminusslidervalue"); //$NON-NLS-1$
+					}
+				}
+			};
+		});
+	}
+
+    /** {@inheritDoc} */
+    @Override public String getUserAgentStylesheet() {
+        return getUserAgentStylesheet(PlusMinusSlider.class, "plusminusslider.css");
+    }
+
+	@Override
+	protected Skin<?> createDefaultSkin() {
+		return new PlusMinusSliderSkin(this);
+	}
+
+	private ReadOnlyDoubleWrapper value = new ReadOnlyDoubleWrapper(this,
+			"value", 0); //$NON-NLS-1$
+
+	/**
+	 * Returns the value property of the adjuster. The value is always between
+	 * -1 and +1.
+	 * 
+	 * @return the value of the adjuster
+	 */
+	public final ReadOnlyDoubleProperty valueProperty() {
+		return value.getReadOnlyProperty();
+	}
+
+	/**
+	 * Returns the value of the value property.
+	 * 
+	 * @return the value of the adjuster [-1, +1]
+	 * 
+	 * @see #valueProperty()
+	 */
+	public final double getValue() {
+		return valueProperty().get();
+	}
+
+	// orientation
+
+	private ObjectProperty<Orientation> orientation;
+
+	/**
+	 * Sets the value of the orientation property.
+	 * 
+	 * @param value
+	 *            the new orientation (horizontal, vertical).
+	 * @see #orientationProperty()
+	 */
+	public final void setOrientation(Orientation value) {
+		orientationProperty().set(value);
+	}
+
+	/**
+	 * Returns the value of the orientation property.
+	 * 
+	 * @return the current orientation of the control
+	 * @see #orientationProperty()
+	 */
+	public final Orientation getOrientation() {
+		return orientation == null ? Orientation.HORIZONTAL : orientation.get();
+	}
+
+	/**
+	 * Returns the stylable object property used for storing the orientation of
+	 * the adjuster control. The CSS property "-fx-orientation" can be used to
+	 * initialize this value.
+	 * 
+	 * @return the orientation property
+	 */
+	public final ObjectProperty<Orientation> orientationProperty() {
+		if (orientation == null) {
+			orientation = new StyleableObjectProperty<Orientation>(null) {
+				@Override
+				protected void invalidated() {
+					final boolean vertical = (get() == Orientation.VERTICAL);
+					pseudoClassStateChanged(VERTICAL_PSEUDOCLASS_STATE,
+							vertical);
+					pseudoClassStateChanged(HORIZONTAL_PSEUDOCLASS_STATE,
+							!vertical);
+				}
+
+				@Override
+				public CssMetaData<PlusMinusSlider, Orientation> getCssMetaData() {
+					return StyleableProperties.ORIENTATION;
+				}
+
+				@Override
+				public Object getBean() {
+					return PlusMinusSlider.this;
+				}
+
+				@Override
+				public String getName() {
+					return "orientation"; //$NON-NLS-1$
+				}
+			};
+		}
+		return orientation;
+	}
+
+	// event support
+
+	/**
+	 * Stores the event handler that will be informed when the adjuster's value
+	 * changes.
+	 * 
+	 * @return the value change event handler property
+	 */
+	public final ObjectProperty<EventHandler<PlusMinusEvent>> onValueChangedProperty() {
+		return onValueChanged;
+	}
+
+	/**
+	 * Sets an event handler that will receive plus minus events when the user
+	 * moves the adjuster's thumb.
+	 * 
+	 * @param value
+	 *            the event handler
+	 * 
+	 * @see #onValueChangedProperty()
+	 */
+	public final void setOnValueChanged(EventHandler<PlusMinusEvent> value) {
+		onValueChangedProperty().set(value);
+	}
+
+	/**
+	 * Returns the event handler that will be notified when the adjuster's value
+	 * changes.
+	 * 
+	 * @return An EventHandler.
+	 */
+	public final EventHandler<PlusMinusEvent> getOnValueChanged() {
+		return onValueChangedProperty().get();
+	}
+
+	private ObjectProperty<EventHandler<PlusMinusEvent>> onValueChanged = new ObjectPropertyBase<EventHandler<PlusMinusEvent>>() {
+
+		@Override
+		protected void invalidated() {
+			setEventHandler(PlusMinusEvent.VALUE_CHANGED, get());
+		}
+
+		@Override
+		public Object getBean() {
+			return PlusMinusSlider.this;
+		}
+
+		@Override
+		public String getName() {
+			return "onValueChanged"; //$NON-NLS-1$
+		}
+	};
+
+	private static class StyleableProperties {
+
+		private static final CssMetaData<PlusMinusSlider, Orientation> ORIENTATION = new CssMetaData<PlusMinusSlider, Orientation>(
+				"-fx-orientation", new EnumConverter<>( //$NON-NLS-1$
+						Orientation.class), Orientation.VERTICAL) {
+
+			@Override
+			public Orientation getInitialValue(PlusMinusSlider node) {
+				// A vertical Slider should remain vertical
+				return node.getOrientation();
+			}
+
+			@Override
+			public boolean isSettable(PlusMinusSlider n) {
+				return n.orientation == null || !n.orientation.isBound();
+			}
+
+			@SuppressWarnings("unchecked")
+			@Override
+			public StyleableProperty<Orientation> getStyleableProperty(
+					PlusMinusSlider n) {
+				return (StyleableProperty<Orientation>) n.orientationProperty();
+			}
+		};
+
+		private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
+		static {
+			final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
+			styleables.add(ORIENTATION);
+
+			STYLEABLES = Collections.unmodifiableList(styleables);
+		}
+	}
+
+	/**
+	 * @return The CssMetaData associated with this class, which may include the
+	 *         CssMetaData of its super classes.
+	 * @since JavaFX 8.0
+	 */
+	public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
+		return StyleableProperties.STYLEABLES;
+	}
+
+	/**
+	 * An event class used by the {@link PlusMinusSlider} to inform event
+	 * handlers about changes.
+	 */
+	public static class PlusMinusEvent extends InputEvent {
+
+		private static final long serialVersionUID = 2881004583512990781L;
+
+		public static final EventType<PlusMinusEvent> ANY = new EventType<>(
+				InputEvent.ANY, "ANY"); //$NON-NLS-1$
+
+		/**
+		 * An event type used when the value property (
+		 * {@link PlusMinusSlider#valueProperty()}) changes.
+		 */
+		public static final EventType<PlusMinusEvent> VALUE_CHANGED = new EventType<>(
+				PlusMinusEvent.ANY, "VALUE_CHANGED"); //$NON-NLS-1$
+
+		private double value;
+
+		/**
+		 * Constructs a new event object.
+		 * 
+		 * @param source
+		 *            the source of the event (always the
+		 *            {@link PlusMinusSlider})
+		 * @param target
+		 *            the target of the event (always the
+		 *            {@link PlusMinusSlider})
+		 * @param eventType
+		 *            the type of the event (e.g. {@link #VALUE_CHANGED})
+		 * @param value
+		 *            the actual current value of the adjuster
+		 */
+		public PlusMinusEvent(Object source, EventTarget target,
+				EventType<? extends InputEvent> eventType, double value) {
+			super(source, target, eventType);
+
+			this.value = value;
+		}
+
+		/**
+		 * The value of the {@link PlusMinusSlider}.
+		 * 
+		 * @return the value
+		 */
+		public double getValue() {
+			return value;
+		}
+	}
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/PopOver.java b/controlsfx/src/main/java/org/controlsfx/control/PopOver.java
new file mode 100644
index 0000000..b98b4e5
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/PopOver.java
@@ -0,0 +1,1011 @@
+/**
+ * Copyright (c) 2013, 2016 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control;
+
+import impl.org.controlsfx.skin.PopOverSkin;
+import javafx.animation.FadeTransition;
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.beans.WeakInvalidationListener;
+import javafx.beans.property.*;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.beans.value.WeakChangeListener;
+import javafx.event.EventHandler;
+import javafx.event.WeakEventHandler;
+import javafx.geometry.Bounds;
+import javafx.geometry.Insets;
+import javafx.scene.Node;
+import javafx.scene.control.Label;
+import javafx.scene.control.PopupControl;
+import javafx.scene.control.Skin;
+import javafx.scene.layout.StackPane;
+import javafx.stage.Window;
+import javafx.stage.WindowEvent;
+import javafx.util.Duration;
+
+import static impl.org.controlsfx.i18n.Localization.asKey;
+import static impl.org.controlsfx.i18n.Localization.localize;
+import static java.util.Objects.requireNonNull;
+import static javafx.scene.input.MouseEvent.MOUSE_CLICKED;
+
+/**
+ * The PopOver control provides detailed information about an owning node in a
+ * popup window. The popup window has a very lightweight appearance (no default
+ * window decorations) and an arrow pointing at the owner. Due to the nature of
+ * popup windows the PopOver will move around with the parent window when the
+ * user drags it. <br>
+ * <center> <img src="popover.png" alt="Screenshot of PopOver"> </center> <br>
+ * The PopOver can be detached from the owning node by dragging it away from the
+ * owner. It stops displaying an arrow and starts displaying a title and a close
+ * icon. <br>
+ * <br>
+ * <center> <img src="popover-detached.png"
+ * alt="Screenshot of a detached PopOver"> </center> <br>
+ * The following image shows a popover with an accordion content node. PopOver
+ * controls are automatically resizing themselves when the content node changes
+ * its size.<br>
+ * <br>
+ * <center> <img src="popover-accordion.png"
+ * alt="Screenshot of PopOver containing an Accordion"> </center> <br>
+ * For styling apply stylesheets to the root pane of the PopOver.
+ *
+ * <h3>Example:</h3>
+ *
+ * <pre>
+ * PopOver popOver = new PopOver();
+ * popOver.getRoot().getStylesheets().add(...);
+ * </pre>
+ *
+ */
+public class PopOver extends PopupControl {
+
+    private static final String DEFAULT_STYLE_CLASS = "popover"; //$NON-NLS-1$
+
+    private static final Duration DEFAULT_FADE_DURATION = Duration.seconds(.2);
+
+    private double targetX;
+
+    private double targetY;
+
+    private final SimpleBooleanProperty animated = new SimpleBooleanProperty(true);
+    private final ObjectProperty<Duration> fadeInDuration = new SimpleObjectProperty<>(DEFAULT_FADE_DURATION);
+    private final ObjectProperty<Duration> fadeOutDuration = new SimpleObjectProperty<>(DEFAULT_FADE_DURATION);
+
+    /**
+     * Creates a pop over with a label as the content node.
+     */
+    public PopOver() {
+        super();
+
+        getStyleClass().add(DEFAULT_STYLE_CLASS);
+
+        getRoot().getStylesheets().add(
+                PopOver.class.getResource("popover.css").toExternalForm()); //$NON-NLS-1$
+
+        setAnchorLocation(AnchorLocation.WINDOW_TOP_LEFT);
+        setOnHiding(new EventHandler<WindowEvent>() {
+            @Override
+            public void handle(WindowEvent evt) {
+                setDetached(false);
+            }
+        });
+
+        /*
+         * Create some initial content.
+         */
+        Label label = new Label(localize(asKey("popOver.default.content"))); //$NON-NLS-1$
+        label.setPrefSize(200, 200);
+        label.setPadding(new Insets(4));
+        setContentNode(label);
+
+        InvalidationListener repositionListener = observable -> {
+            if (isShowing() && !isDetached()) {
+                show(getOwnerNode(), targetX, targetY);
+                adjustWindowLocation();
+            }
+        };
+
+        arrowSize.addListener(repositionListener);
+        cornerRadius.addListener(repositionListener);
+        arrowLocation.addListener(repositionListener);
+        arrowIndent.addListener(repositionListener);
+        headerAlwaysVisible.addListener(repositionListener);
+
+        /*
+         * A detached popover should of course not automatically hide itself.
+         */
+        detached.addListener(it -> {
+            if (isDetached()) {
+                setAutoHide(false);
+            } else {
+                setAutoHide(true);
+            }
+        });
+
+        setAutoHide(true);
+    }
+
+    /**
+     * Creates a pop over with the given node as the content node.
+     *
+     * @param content
+     *            The content shown by the pop over
+     */
+    public PopOver(Node content) {
+        this();
+
+        setContentNode(content);
+    }
+
+    @Override
+    protected Skin<?> createDefaultSkin() {
+        return new PopOverSkin(this);
+    }
+
+    private final StackPane root = new StackPane();
+
+    /**
+     * The root pane stores the content node of the popover. It is accessible
+     * via this method in order to support proper styling.
+     *
+     * <h3>Example:</h3>
+     *
+     * <pre>
+     * PopOver popOver = new PopOver();
+     * popOver.getRoot().getStylesheets().add(...);
+     * </pre>
+     *
+     * @return the root pane
+     */
+    public final StackPane getRoot() {
+        return root;
+    }
+
+    // Content support.
+
+    private final ObjectProperty<Node> contentNode = new SimpleObjectProperty<Node>(
+            this, "contentNode") { //$NON-NLS-1$
+        @Override
+        public void setValue(Node node) {
+            if (node == null) {
+                throw new IllegalArgumentException(
+                        "content node can not be null"); //$NON-NLS-1$
+            }
+        };
+    };
+
+    /**
+     * Returns the content shown by the pop over.
+     *
+     * @return the content node property
+     */
+    public final ObjectProperty<Node> contentNodeProperty() {
+        return contentNode;
+    }
+
+    /**
+     * Returns the value of the content property
+     *
+     * @return the content node
+     *
+     * @see #contentNodeProperty()
+     */
+    public final Node getContentNode() {
+        return contentNodeProperty().get();
+    }
+
+    /**
+     * Sets the value of the content property.
+     *
+     * @param content
+     *            the new content node value
+     *
+     * @see #contentNodeProperty()
+     */
+    public final void setContentNode(Node content) {
+        contentNodeProperty().set(content);
+    }
+
+    private InvalidationListener hideListener = new InvalidationListener() {
+        @Override
+        public void invalidated(Observable observable) {
+            if (!isDetached()) {
+                hide(Duration.ZERO);
+            }
+        }
+    };
+
+    private WeakInvalidationListener weakHideListener = new WeakInvalidationListener(
+            hideListener);
+
+    private ChangeListener<Number> xListener = new ChangeListener<Number>() {
+        @Override
+        public void changed(ObservableValue<? extends Number> value,
+                Number oldX, Number newX) {
+            if (!isDetached()) {
+                setAnchorX(getAnchorX() + (newX.doubleValue() - oldX.doubleValue()));
+            }
+        }
+    };
+
+    private WeakChangeListener<Number> weakXListener = new WeakChangeListener<>(
+            xListener);
+
+    private ChangeListener<Number> yListener = new ChangeListener<Number>() {
+        @Override
+        public void changed(ObservableValue<? extends Number> value,
+                Number oldY, Number newY) {
+            if (!isDetached()) {
+                setAnchorY(getAnchorY() + (newY.doubleValue() - oldY.doubleValue()));
+            }
+        }
+    };
+
+    private WeakChangeListener<Number> weakYListener = new WeakChangeListener<>(
+            yListener);
+
+    private Window ownerWindow;
+    private final EventHandler<WindowEvent> closePopOverOnOwnerWindowCloseLambda = event -> ownerWindowClosing();
+    private final WeakEventHandler<WindowEvent> closePopOverOnOwnerWindowClose = new WeakEventHandler<>(closePopOverOnOwnerWindowCloseLambda);
+
+    /**
+     * Shows the pop over in a position relative to the edges of the given owner
+     * node. The position is dependent on the arrow location. If the arrow is
+     * pointing to the right then the pop over will be placed to the left of the
+     * given owner. If the arrow points up then the pop over will be placed
+     * below the given owner node. The arrow will slightly overlap with the
+     * owner node.
+     *
+     * @param owner
+     *            the owner of the pop over
+     */
+    public final void show(Node owner) {
+        show(owner, 4);
+    }
+
+    /**
+     * Shows the pop over in a position relative to the edges of the given owner
+     * node. The position is dependent on the arrow location. If the arrow is
+     * pointing to the right then the pop over will be placed to the left of the
+     * given owner. If the arrow points up then the pop over will be placed
+     * below the given owner node.
+     *
+     * @param owner
+     *            the owner of the pop over
+     * @param offset
+     *            if negative specifies the distance to the owner node or when
+     *            positive specifies the number of pixels that the arrow will
+     *            overlap with the owner node (positive values are recommended)
+     */
+    public final void show(Node owner, double offset) {
+        requireNonNull(owner);
+
+        Bounds bounds = owner.localToScreen(owner.getBoundsInLocal());
+
+        switch (getArrowLocation()) {
+        case BOTTOM_CENTER:
+        case BOTTOM_LEFT:
+        case BOTTOM_RIGHT:
+            show(owner, bounds.getMinX() + bounds.getWidth() / 2,
+                    bounds.getMinY() + offset);
+            break;
+        case LEFT_BOTTOM:
+        case LEFT_CENTER:
+        case LEFT_TOP:
+            show(owner, bounds.getMaxX() - offset,
+                    bounds.getMinY() + bounds.getHeight() / 2);
+            break;
+        case RIGHT_BOTTOM:
+        case RIGHT_CENTER:
+        case RIGHT_TOP:
+            show(owner, bounds.getMinX() + offset,
+                    bounds.getMinY() + bounds.getHeight() / 2);
+            break;
+        case TOP_CENTER:
+        case TOP_LEFT:
+        case TOP_RIGHT:
+            show(owner, bounds.getMinX() + bounds.getWidth() / 2,
+                    bounds.getMinY() + bounds.getHeight() - offset);
+            break;
+        default:
+            break;
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public final void show(Window owner) {
+        super.show(owner);
+        ownerWindow = owner;
+
+        if (isAnimated()) {
+            showFadeInAnimation(getFadeInDuration());
+        }
+
+        ownerWindow.addEventFilter(WindowEvent.WINDOW_CLOSE_REQUEST,
+                closePopOverOnOwnerWindowClose);
+        ownerWindow.addEventFilter(WindowEvent.WINDOW_HIDING,
+                closePopOverOnOwnerWindowClose);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public final void show(Window ownerWindow, double anchorX, double anchorY) {
+        super.show(ownerWindow, anchorX, anchorY);
+        this.ownerWindow = ownerWindow;
+
+        if (isAnimated()) {
+            showFadeInAnimation(getFadeInDuration());
+        }
+
+        ownerWindow.addEventFilter(WindowEvent.WINDOW_CLOSE_REQUEST,
+                closePopOverOnOwnerWindowClose);
+        ownerWindow.addEventFilter(WindowEvent.WINDOW_HIDING,
+                closePopOverOnOwnerWindowClose);
+    }
+
+    /**
+     * Makes the pop over visible at the give location and associates it with
+     * the given owner node. The x and y coordinate will be the target location
+     * of the arrow of the pop over and not the location of the window.
+     *
+     * @param owner
+     *            the owning node
+     * @param x
+     *            the x coordinate for the pop over arrow tip
+     * @param y
+     *            the y coordinate for the pop over arrow tip
+     */
+    @Override
+    public final void show(Node owner, double x, double y) {
+        show(owner, x, y, getFadeInDuration());
+    }
+
+    /**
+     * Makes the pop over visible at the give location and associates it with
+     * the given owner node. The x and y coordinate will be the target location
+     * of the arrow of the pop over and not the location of the window.
+     *
+     * @param owner
+     *            the owning node
+     * @param x
+     *            the x coordinate for the pop over arrow tip
+     * @param y
+     *            the y coordinate for the pop over arrow tip
+     * @param fadeInDuration
+     *            the time it takes for the pop over to be fully visible. This duration takes precedence over the fade-in property without setting.
+     */
+    public final void show(Node owner, double x, double y,
+            Duration fadeInDuration) {
+
+        /*
+         * Calling show() a second time without first closing the pop over
+         * causes it to be placed at the wrong location.
+         */
+        if (ownerWindow != null && isShowing()) {
+            super.hide();
+        }
+
+        targetX = x;
+        targetY = y;
+
+        if (owner == null) {
+            throw new IllegalArgumentException("owner can not be null"); //$NON-NLS-1$
+        }
+
+        if (fadeInDuration == null) {
+            fadeInDuration = DEFAULT_FADE_DURATION;
+        }
+
+        /*
+         * This is all needed because children windows do not get their x and y
+         * coordinate updated when the owning window gets moved by the user.
+         */
+        if (ownerWindow != null) {
+            ownerWindow.xProperty().removeListener(weakXListener);
+            ownerWindow.yProperty().removeListener(weakYListener);
+            ownerWindow.widthProperty().removeListener(weakHideListener);
+            ownerWindow.heightProperty().removeListener(weakHideListener);
+        }
+
+        ownerWindow = owner.getScene().getWindow();
+        ownerWindow.xProperty().addListener(weakXListener);
+        ownerWindow.yProperty().addListener(weakYListener);
+        ownerWindow.widthProperty().addListener(weakHideListener);
+        ownerWindow.heightProperty().addListener(weakHideListener);
+
+        setOnShown(evt -> {
+
+            /*
+             * The user clicked somewhere into the transparent background. If
+             * this is the case then hide the window (when attached).
+             */
+            getScene().addEventHandler(MOUSE_CLICKED, mouseEvent -> {
+                if (mouseEvent.getTarget().equals(getScene().getRoot())) {
+                    if (!isDetached()) {
+                        hide();
+                    }
+                }
+            });
+
+            /*
+             * Move the window so that the arrow will end up pointing at the
+             * target coordinates.
+             */
+            adjustWindowLocation();
+        });
+
+        super.show(owner, x, y);
+
+        if (isAnimated()) {
+            showFadeInAnimation(fadeInDuration);
+        }
+
+        // Bug fix - close popup when owner window is closing
+        ownerWindow.addEventFilter(WindowEvent.WINDOW_CLOSE_REQUEST,
+                closePopOverOnOwnerWindowClose);
+        ownerWindow.addEventFilter(WindowEvent.WINDOW_HIDING,
+                closePopOverOnOwnerWindowClose);
+    }
+
+    private void showFadeInAnimation(Duration fadeInDuration) {
+        // Fade In
+        Node skinNode = getSkin().getNode();
+        skinNode.setOpacity(0);
+
+        FadeTransition fadeIn = new FadeTransition(fadeInDuration, skinNode);
+        fadeIn.setFromValue(0);
+        fadeIn.setToValue(1);
+        fadeIn.play();
+    }
+
+    private void ownerWindowClosing() {
+        hide(Duration.ZERO);
+    }
+
+    /**
+     * Hides the pop over by quickly changing its opacity to 0.
+     *
+     * @see #hide(Duration)
+     */
+    @Override
+    public final void hide() {
+        hide(getFadeOutDuration());
+    }
+
+    /**
+     * Hides the pop over by quickly changing its opacity to 0.
+     *
+     * @param fadeOutDuration
+     *            the duration of the fade transition that is being used to
+     *            change the opacity of the pop over
+     * @since 1.0
+     */
+    public final void hide(Duration fadeOutDuration) {
+        //We must remove EventFilter in order to prevent memory leak.
+        if (ownerWindow != null){
+            ownerWindow.removeEventFilter(WindowEvent.WINDOW_CLOSE_REQUEST,
+                    closePopOverOnOwnerWindowClose);
+            ownerWindow.removeEventFilter(WindowEvent.WINDOW_HIDING,
+                closePopOverOnOwnerWindowClose);
+        }
+        if (fadeOutDuration == null) {
+            fadeOutDuration = DEFAULT_FADE_DURATION;
+        }
+
+        if (isShowing()) {
+            if (isAnimated()) {
+                // Fade Out
+                Node skinNode = getSkin().getNode();
+
+                FadeTransition fadeOut = new FadeTransition(fadeOutDuration,
+                        skinNode);
+                fadeOut.setFromValue(skinNode.getOpacity());
+                fadeOut.setToValue(0);
+                fadeOut.setOnFinished(evt -> super.hide());
+                fadeOut.play();
+            } else {
+                super.hide();
+            }
+        }
+    }
+
+    private void adjustWindowLocation() {
+        Bounds bounds = PopOver.this.getSkin().getNode().getBoundsInParent();
+
+        switch (getArrowLocation()) {
+        case TOP_CENTER:
+        case TOP_LEFT:
+        case TOP_RIGHT:
+            setAnchorX(getAnchorX() + bounds.getMinX() - computeXOffset());
+            setAnchorY(getAnchorY() + bounds.getMinY() + getArrowSize());
+            break;
+        case LEFT_TOP:
+        case LEFT_CENTER:
+        case LEFT_BOTTOM:
+            setAnchorX(getAnchorX() + bounds.getMinX() + getArrowSize());
+            setAnchorY(getAnchorY() + bounds.getMinY() - computeYOffset());
+            break;
+        case BOTTOM_CENTER:
+        case BOTTOM_LEFT:
+        case BOTTOM_RIGHT:
+            setAnchorX(getAnchorX() + bounds.getMinX() - computeXOffset());
+            setAnchorY(getAnchorY() - bounds.getMinY() - bounds.getMaxY() - 1);
+            break;
+        case RIGHT_TOP:
+        case RIGHT_BOTTOM:
+        case RIGHT_CENTER:
+            setAnchorX(getAnchorX() - bounds.getMinX() - bounds.getMaxX() - 1);
+            setAnchorY(getAnchorY() + bounds.getMinY() - computeYOffset());
+            break;
+        }
+    }
+
+    private double computeXOffset() {
+        switch (getArrowLocation()) {
+        case TOP_LEFT:
+        case BOTTOM_LEFT:
+            return getCornerRadius() + getArrowIndent() + getArrowSize();
+        case TOP_CENTER:
+        case BOTTOM_CENTER:
+            return getContentNode().prefWidth(-1) / 2;
+        case TOP_RIGHT:
+        case BOTTOM_RIGHT:
+            return getContentNode().prefWidth(-1) - getArrowIndent()
+                    - getCornerRadius() - getArrowSize();
+        default:
+            return 0;
+        }
+    }
+
+    private double computeYOffset() {
+        double prefContentHeight = getContentNode().prefHeight(-1);
+
+        switch (getArrowLocation()) {
+        case LEFT_TOP:
+        case RIGHT_TOP:
+            return getCornerRadius() + getArrowIndent() + getArrowSize();
+        case LEFT_CENTER:
+        case RIGHT_CENTER:
+            return Math.max(prefContentHeight, 2 * (getCornerRadius()
+                    + getArrowIndent() + getArrowSize())) / 2;
+        case LEFT_BOTTOM:
+        case RIGHT_BOTTOM:
+            return Math.max(prefContentHeight - getCornerRadius()
+                    - getArrowIndent() - getArrowSize(), getCornerRadius()
+                    + getArrowIndent() + getArrowSize());
+        default:
+            return 0;
+        }
+    }
+
+    /**
+     * Detaches the pop over from the owning node. The pop over will no longer
+     * display an arrow pointing at the owner node.
+     */
+    public final void detach() {
+        if (isDetachable()) {
+            setDetached(true);
+        }
+    }
+
+    // always show header
+
+    private final BooleanProperty headerAlwaysVisible = new SimpleBooleanProperty(this, "headerAlwaysVisible"); //$NON-NLS-1$
+
+    /**
+     * Determines whether or not the {@link PopOver} header should remain visible, even while attached.
+     */
+    public final BooleanProperty headerAlwaysVisibleProperty() {
+        return headerAlwaysVisible;
+    }
+
+    /**
+     * Sets the value of the headerAlwaysVisible property.
+     *
+     * @param visible
+     *            if true, then the header is visible even while attached
+     *
+     * @see #headerAlwaysVisibleProperty()
+     */
+    public final void setHeaderAlwaysVisible(boolean visible) {
+        headerAlwaysVisible.setValue(visible);
+    }
+
+    /**
+     * Returns the value of the detachable property.
+     *
+     * @return true if the header is visible even while attached
+     *
+     * @see #headerAlwaysVisibleProperty()
+     */
+    public final boolean isHeaderAlwaysVisible() {
+        return headerAlwaysVisible.getValue();
+    }
+
+
+    // detach support
+
+    private final BooleanProperty detachable = new SimpleBooleanProperty(this,
+            "detachable", true); //$NON-NLS-1$
+
+    /**
+     * Determines if the pop over is detachable at all.
+     */
+    public final BooleanProperty detachableProperty() {
+        return detachable;
+    }
+
+    /**
+     * Sets the value of the detachable property.
+     *
+     * @param detachable
+     *            if true then the user can detach / tear off the pop over
+     *
+     * @see #detachableProperty()
+     */
+    public final void setDetachable(boolean detachable) {
+        detachableProperty().set(detachable);
+    }
+
+    /**
+     * Returns the value of the detachable property.
+     *
+     * @return true if the user is allowed to detach / tear off the pop over
+     *
+     * @see #detachableProperty()
+     */
+    public final boolean isDetachable() {
+        return detachableProperty().get();
+    }
+
+    private final BooleanProperty detached = new SimpleBooleanProperty(this,
+            "detached", false); //$NON-NLS-1$
+
+    /**
+     * Determines whether the pop over is detached from the owning node or not.
+     * A detached pop over no longer shows an arrow pointing at the owner and
+     * features its own title bar.
+     *
+     * @return the detached property
+     */
+    public final BooleanProperty detachedProperty() {
+        return detached;
+    }
+
+    /**
+     * Sets the value of the detached property.
+     *
+     * @param detached
+     *            if true the pop over will change its apperance to "detached"
+     *            mode
+     *
+     * @see #detachedProperty()
+     */
+    public final void setDetached(boolean detached) {
+        detachedProperty().set(detached);
+    }
+
+    /**
+     * Returns the value of the detached property.
+     *
+     * @return true if the pop over is currently detached.
+     *
+     * @see #detachedProperty()
+     */
+    public final boolean isDetached() {
+        return detachedProperty().get();
+    }
+
+    // arrow size support
+
+    // TODO: make styleable
+
+    private final DoubleProperty arrowSize = new SimpleDoubleProperty(this,
+            "arrowSize", 12); //$NON-NLS-1$
+
+    /**
+     * Controls the size of the arrow. Default value is 12.
+     *
+     * @return the arrow size property
+     */
+    public final DoubleProperty arrowSizeProperty() {
+        return arrowSize;
+    }
+
+    /**
+     * Returns the value of the arrow size property.
+     *
+     * @return the arrow size property value
+     *
+     * @see #arrowSizeProperty()
+     */
+    public final double getArrowSize() {
+        return arrowSizeProperty().get();
+    }
+
+    /**
+     * Sets the value of the arrow size property.
+     *
+     * @param size
+     *            the new value of the arrow size property
+     *
+     * @see #arrowSizeProperty()
+     */
+    public final void setArrowSize(double size) {
+        arrowSizeProperty().set(size);
+    }
+
+    // arrow indent support
+
+    // TODO: make styleable
+
+    private final DoubleProperty arrowIndent = new SimpleDoubleProperty(this,
+            "arrowIndent", 12); //$NON-NLS-1$
+
+    /**
+     * Controls the distance between the arrow and the corners of the pop over.
+     * The default value is 12.
+     *
+     * @return the arrow indent property
+     */
+    public final DoubleProperty arrowIndentProperty() {
+        return arrowIndent;
+    }
+
+    /**
+     * Returns the value of the arrow indent property.
+     *
+     * @return the arrow indent value
+     *
+     * @see #arrowIndentProperty()
+     */
+    public final double getArrowIndent() {
+        return arrowIndentProperty().get();
+    }
+
+    /**
+     * Sets the value of the arrow indent property.
+     *
+     * @param size
+     *            the arrow indent value
+     *
+     * @see #arrowIndentProperty()
+     */
+    public final void setArrowIndent(double size) {
+        arrowIndentProperty().set(size);
+    }
+
+    // radius support
+
+    // TODO: make styleable
+
+    private final DoubleProperty cornerRadius = new SimpleDoubleProperty(this,
+            "cornerRadius", 6); //$NON-NLS-1$
+
+    /**
+     * Returns the corner radius property for the pop over.
+     *
+     * @return the corner radius property (default is 6)
+     */
+    public final DoubleProperty cornerRadiusProperty() {
+        return cornerRadius;
+    }
+
+    /**
+     * Returns the value of the corner radius property.
+     *
+     * @return the corner radius
+     *
+     * @see #cornerRadiusProperty()
+     */
+    public final double getCornerRadius() {
+        return cornerRadiusProperty().get();
+    }
+
+    /**
+     * Sets the value of the corner radius property.
+     *
+     * @param radius
+     *            the corner radius
+     *
+     * @see #cornerRadiusProperty()
+     */
+    public final void setCornerRadius(double radius) {
+        cornerRadiusProperty().set(radius);
+    }
+
+    // Detached stage title
+
+    private final StringProperty title = new SimpleStringProperty(this, "title", localize(asKey("popOver.default.title"))); //$NON-NLS-1$ //$NON-NLS-2$
+
+    /**
+     * Stores the title to display in the PopOver's header.
+     *
+     * @return the title property
+     */
+    public final StringProperty titleProperty() {
+        return title;
+    }
+
+    /**
+     * Returns the value of the title property.
+     *
+     * @return the detached title
+     * @see #titleProperty()
+     */
+    public final String getTitle() {
+        return titleProperty().get();
+    }
+
+    /**
+     * Sets the value of the title property.
+     *
+     * @param title the title to use when detached
+     * @see #titleProperty()
+     */
+    public final void setTitle(String title) {
+        if (title == null) {
+            throw new IllegalArgumentException("title can not be null"); //$NON-NLS-1$
+        }
+
+        titleProperty().set(title);
+    }
+
+    private final ObjectProperty<ArrowLocation> arrowLocation = new SimpleObjectProperty<>(
+            this, "arrowLocation", ArrowLocation.LEFT_TOP); //$NON-NLS-1$
+
+    /**
+     * Stores the preferred arrow location. This might not be the actual
+     * location of the arrow if auto fix is enabled.
+     *
+     * @see #setAutoFix(boolean)
+     *
+     * @return the arrow location property
+     */
+    public final ObjectProperty<ArrowLocation> arrowLocationProperty() {
+        return arrowLocation;
+    }
+
+    /**
+     * Sets the value of the arrow location property.
+     *
+     * @see #arrowLocationProperty()
+     *
+     * @param location
+     *            the requested location
+     */
+    public final void setArrowLocation(ArrowLocation location) {
+        arrowLocationProperty().set(location);
+    }
+
+    /**
+     * Returns the value of the arrow location property.
+     *
+     * @see #arrowLocationProperty()
+     *
+     * @return the preferred arrow location
+     */
+    public final ArrowLocation getArrowLocation() {
+        return arrowLocationProperty().get();
+    }
+
+    /**
+     * All possible arrow locations.
+     */
+    public enum ArrowLocation {
+        LEFT_TOP, LEFT_CENTER, LEFT_BOTTOM, RIGHT_TOP, RIGHT_CENTER, RIGHT_BOTTOM, TOP_LEFT, TOP_CENTER, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_CENTER, BOTTOM_RIGHT;
+    }
+
+    /**
+     * Stores the fade-in duration. This should be set before calling PopOver.show(..).
+     *
+     * @return the fade-in duration property
+     */
+    public final ObjectProperty<Duration> fadeInDurationProperty() {
+        return fadeInDuration;
+    }
+
+    /**
+     * Stores the fade-out duration.
+     *
+     * @return the fade-out duration property
+     */
+    public final ObjectProperty<Duration> fadeOutDurationProperty() {
+        return fadeOutDuration;
+    }
+
+    /**
+     * Returns the value of the fade-in duration property.
+     *
+     * @return the fade-in duration
+     * @see #fadeInDurationProperty()
+     */
+    public final Duration getFadeInDuration() {
+        return fadeInDurationProperty().get();
+    }
+
+    /**
+     * Sets the value of the fade-in duration property. This should be set before calling PopOver.show(..).
+     *
+     * @param duration the requested fade-in duration
+     * @see #fadeInDurationProperty()
+     */
+    public final void setFadeInDuration(Duration duration) {
+        fadeInDurationProperty().setValue(duration);
+    }
+
+    /**
+     * Returns the value of the fade-out duration property.
+     *
+     * @return the fade-out duration
+     * @see #fadeOutDurationProperty()
+     */
+    public final Duration getFadeOutDuration() {
+        return fadeOutDurationProperty().get();
+    }
+
+    /**
+     * Sets the value of the fade-out duration property.
+     *
+     * @param duration the requested fade-out duration
+     * @see #fadeOutDurationProperty()
+     */
+    public final void setFadeOutDuration(Duration duration) {
+        fadeOutDurationProperty().setValue(duration);
+    }
+
+    /**
+     * Stores the "animated" flag. If true then the PopOver will be shown / hidden with a short fade in / out animation.
+     *
+     * @return the "animated" property
+     */
+    public final BooleanProperty animatedProperty() {
+        return animated;
+    }
+
+    /**
+     * Returns the value of the "animated" property.
+     *
+     * @return true if the PopOver will be shown and hidden with a short fade animation
+     * @see #animatedProperty()
+     */
+    public final boolean isAnimated() {
+        return animatedProperty().get();
+    }
+
+    /**
+     * Sets the value of the "animated" property.
+     *
+     * @param animated if true the PopOver will be shown and hidden with a short fade animation
+     * @see #animatedProperty()
+     */
+    public final void setAnimated(boolean animated) {
+        animatedProperty().set(animated);
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/PrefixSelectionChoiceBox.java b/controlsfx/src/main/java/org/controlsfx/control/PrefixSelectionChoiceBox.java
new file mode 100644
index 0000000..d5cf6ce
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/PrefixSelectionChoiceBox.java
@@ -0,0 +1,77 @@
+/**
+ * Copyright (c) 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control;
+
+import impl.org.controlsfx.tools.PrefixSelectionCustomizer;
+import javafx.scene.control.ChoiceBox;
+
+/**
+ * <p>A simple extension of the {@link ChoiceBox} which selects an entry of
+ * its item list based on keyboard input. The user can type letters or 
+ * digits on the keyboard and die {@link ChoiceBox} will attempt to
+ * select the first item it can find with a matching prefix.
+ *
+ * <p>This feature is available natively on the Windows combo box control, so many
+ * users have asked for it. There is a feature request to include this feature
+ * into JavaFX (<a href="https://javafx-jira.kenai.com/browse/RT-18064">Issue RT-18064</a>). 
+ * The class is published as part of ContorlsFX to allow testing and feedback.
+ * 
+ * <h3>Example</h3>
+ * 
+ * <p>Let's look at an example to clarify this. The choice box offers the items 
+ * ["Aaaaa", "Abbbb", "Abccc", "Abcdd", "Abcde"]. The user now types "abc" in 
+ * quick succession (and then stops typing). The choice box will select a new entry 
+ * on every key pressed. The first entry it will select is "Aaaaa" since it is the 
+ * first entry that starts with an "a" (case ignored). It will then select "Abbbb", 
+ * since this is the first entry that started with "ab" and will finally settle for 
+ * "Abccc".
+ * 
+ * <ul><table>
+ *   <tr><th>Keys typed</th><th>Element selected</th></tr>
+ *   <tr><td>a</td><td>Aaaaa<td></tr>
+ *   <tr><td>aaa</td><td>Aaaaa<td></tr>
+ *   <tr><td>ab</td><td>Abbbb<td></tr>
+ *   <tr><td>abc</td><td>Abccc<td></tr>
+ *   <tr><td>xyz</td><td>-<td></tr>
+ * </table></ul>
+ * 
+ * <p>If you want to modify an existing {@link ChoiceBox} you can use the
+ * {@link PrefixSelectionCustomizer} directly to do this.
+ * 
+ * @see PrefixSelectionCustomizer
+ */
+public class PrefixSelectionChoiceBox<T> extends ChoiceBox<T> {
+
+    /**
+     * Create a non editable {@link ChoiceBox} with the "prefix selection"
+     * feature installed.
+     */
+    public PrefixSelectionChoiceBox() {
+        PrefixSelectionCustomizer.customize(this);
+    }
+    
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/PrefixSelectionComboBox.java b/controlsfx/src/main/java/org/controlsfx/control/PrefixSelectionComboBox.java
new file mode 100644
index 0000000..98092f6
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/PrefixSelectionComboBox.java
@@ -0,0 +1,81 @@
+/**
+ * Copyright (c) 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control;
+
+import impl.org.controlsfx.tools.PrefixSelectionCustomizer;
+import javafx.scene.control.ComboBox;
+
+/**
+ * A simple extension of the {@link ComboBox} which selects an entry of
+ * its item list based on keyboard input. The user can  type letters or 
+ * digits on the keyboard and die ChoiceBox will attempt to
+ * select the first item it can find with a matching prefix.
+ * 
+ * This will only be enabled, when the {@link ComboBox} is not editable, so
+ * this class will be setup as non editable by default.
+ *
+ * <p>This feature is available natively on the Windows combo box control, so many
+ * users have asked for it. There is a feature request to include this feature
+ * into JavaFX (<a href="https://javafx-jira.kenai.com/browse/RT-18064">Issue RT-18064</a>). 
+ * The class is published as part of ContorlsFX to allow testing and feedback.
+ * 
+ * <h3>Example</h3>
+ * 
+ * <p>Let's look at an example to clarify this. The combo box offers the items 
+ * ["Aaaaa", "Abbbb", "Abccc", "Abcdd", "Abcde"]. The user now types "abc" in 
+ * quick succession (and then stops typing). The combo box will select a new entry 
+ * on every key pressed. The first entry it will select is "Aaaaa" since it is the 
+ * first entry that starts with an "a" (case ignored). It will then select "Abbbb", 
+ * since this is the first entry that started with "ab" and will finally settle for 
+ * "Abccc".
+ * 
+ * <ul><table>
+ *   <tr><th>Keys typed</th><th>Element selected</th></tr>
+ *   <tr><td>a</td><td>Aaaaa<td></tr>
+ *   <tr><td>aaa</td><td>Aaaaa<td></tr>
+ *   <tr><td>ab</td><td>Abbbb<td></tr>
+ *   <tr><td>abc</td><td>Abccc<td></tr>
+ *   <tr><td>xyz</td><td>-<td></tr>
+ * </table></ul>
+ * 
+ * <p>If you want to modify an existing {@link ComboBox} you can use the
+ * {@link PrefixSelectionCustomizer} directly to do this.
+ * 
+ * @see PrefixSelectionCustomizer
+ */
+public class PrefixSelectionComboBox<T> extends ComboBox<T> {
+
+    /**
+     * Create a non editable {@link ComboBox} with the "prefix selection"
+     * feature installed.
+     */
+    public PrefixSelectionComboBox() {
+        setEditable(false);
+        PrefixSelectionCustomizer.customize(this);
+    }
+    
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/PropertySheet.java b/controlsfx/src/main/java/org/controlsfx/control/PropertySheet.java
new file mode 100644
index 0000000..2675b7c
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/PropertySheet.java
@@ -0,0 +1,452 @@
+/**
+ * Copyright (c) 2013, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control;
+
+import impl.org.controlsfx.skin.PropertySheetSkin;
+
+import java.util.Optional;
+
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.scene.control.Skin;
+import javafx.util.Callback;
+
+import org.controlsfx.property.BeanPropertyUtils;
+import org.controlsfx.property.editor.DefaultPropertyEditorFactory;
+import org.controlsfx.property.editor.Editors;
+import org.controlsfx.property.editor.PropertyEditor;
+
+/**
+ * The PropertySheet control is a powerful control designed to make it really
+ * easy for developers to present to end users a list of properties that the 
+ * end user is allowed to manipulate. Commonly a property sheet is used in
+ * visual editors and other tools where a lot of properties exist.
+ * 
+ * <p>To better describe what a property sheet is, please refer to the picture
+ * below:
+ * 
+ * <br>
+ * <center><img src="propertySheet.PNG" alt="Screenshot of PropertySheet"></center>
+ * 
+ * <p>In this property sheet there exists two columns: the left column shows a 
+ * label describing the property itself, whereas the right column provides a
+ * {@link PropertyEditor} that allows the end user the means to manipulate the
+ * property. In the screenshot you can see CheckEditor, 
+ * ChoiceEditor, TextEditor and FontEditor, among the
+ * many editors that are available in the {@link Editors}
+ * package.
+ * 
+ * <p>To create a PropertySheet is simple: you firstly instantiate an instance
+ * of PropertySheet, and then you pass in a list of {@link Item} instances,
+ * where each Item represents a single property that is to be editable by the
+ * end user.
+ * 
+ * <h3>Working with JavaBeans</h3>
+ * Because a very common use case for a property sheet is editing properties on
+ * a JavaBean, there is convenience API for making this interaction easier.
+ * Refer to the {@link BeanPropertyUtils class}, in particular the
+ * {@link BeanPropertyUtils#getProperties(Object)} method that will return a 
+ * list of Item instances, one Item instance per property on the given JavaBean.
+ * 
+ * @see Item
+ * @see Mode
+ */
+public class PropertySheet extends ControlsFXControl {
+    
+    
+    /**************************************************************************
+     * 
+     * Static fields
+     * 
+     **************************************************************************/
+    
+    
+    
+    /**************************************************************************
+     * 
+     * Static enumerations / interfaces
+     * 
+     **************************************************************************/
+    
+    /**
+     * Specifies how the {@link PropertySheet} should be laid out. Refer to the 
+     * enumeration values to learn what each one means.
+     */
+    public static enum Mode {
+        /**
+         * Simply displays the properties in the 
+         * {@link PropertySheet#getItems() items list} in the order they are 
+         * in the list. 
+         */
+        NAME,
+        
+        /**
+         * Groups the properties in the 
+         * {@link PropertySheet#getItems() items list} based on their
+         * {@link Item#getCategory() category}.
+         */
+        CATEGORY
+    }
+    
+    
+    
+    /**
+     * A wrapper interface for a single property to be displayed in a
+     * {@link PropertySheet} control.
+     * 
+     * @see PropertySheet
+     */
+    public static interface Item {
+        
+        /**
+         * Returns the class type of the property.
+         */
+        public Class<?> getType();
+
+        /**
+         * Returns a String representation of the category of the property. This
+         * is relevant when the {@link PropertySheet} 
+         * {@link PropertySheet#modeProperty() mode property} is set to
+         * {@link Mode#CATEGORY} - as then all properties with the same category
+         * will be grouped together visually. 
+         */
+        public String getCategory();
+       
+        /**
+         * Returns the display name of the property, which should be short (i.e.
+         * less than two words). This is used to explain to the end user what the
+         * property represents and is displayed beside the {@link PropertyEditor}.
+         * If you need to explain more detail to the user, consider placing it
+         * in the {@link #getDescription()}.
+         */
+        public String getName();
+        
+        /**
+         * A String that will be shown to the user as a tooltip. This allows for
+         * a longer form of detail than what is possible with the {@link #getName()} 
+         * method.
+         */
+        public String getDescription();
+        
+        /**
+         * Returns the current value of the property.
+         */
+        public Object getValue();
+
+        /**
+         * Sets the current value of the property.
+         */
+        public void setValue(Object value);
+        
+        /**
+         * Returns the underlying ObservableValue, where one exists, that the editor 
+         * can monitor for changes.
+         */
+        public Optional<ObservableValue<? extends Object>> getObservableValue();
+        
+        /**
+         * Returns an Optional wrapping the class of the PropertyEditor that 
+         * should be used for editing this item. The default implementation 
+         * returns Optional.empty()
+         * 
+         * The class must have a constructor that can accept a single argument 
+         * of type PropertySheet.Item
+         */
+        default public Optional<Class<? extends PropertyEditor<?>>> getPropertyEditorClass() {
+            return Optional.empty();
+        }
+        
+        /**
+         *  Indicates whether the PropertySheet should allow editing of this 
+         * property, or whether it is read-only. The default implementation 
+         * returns true.
+         */
+        default public boolean isEditable() {
+            return true;
+        }
+   }
+    
+    
+    /**************************************************************************
+     * 
+     * Private fields
+     * 
+     **************************************************************************/
+    
+    private final ObservableList<Item> items;
+    
+    
+    
+    /**************************************************************************
+     * 
+     * Constructors
+     * 
+     **************************************************************************/
+    
+    /**
+     * Creates a default PropertySheet instance with no properties to edit.
+     */
+    public PropertySheet() {
+        this(null);
+    }
+    
+    /**
+     * Creates a PropertySheet instance prepopulated with the items provided
+     * in the items {@link ObservableList}.
+     * 
+     * @param items The items that should appear within the PropertySheet.
+     */
+    public PropertySheet(ObservableList<Item> items) {
+        getStyleClass().add(DEFAULT_STYLE_CLASS);
+        
+        this.items = items == null ? FXCollections.<Item>observableArrayList() : items;
+    }
+    
+    
+    
+    /**************************************************************************
+     * 
+     * Public API
+     * 
+     **************************************************************************/
+    
+    /**
+     * 
+     * @return An ObservableList of properties that will be displayed to the user to allow for them
+     * to be edited. 
+     */
+    public ObservableList<Item> getItems() {
+        return items;
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    @Override protected Skin<?> createDefaultSkin() {
+        return new PropertySheetSkin(this);
+    }
+    
+    /** {@inheritDoc} */
+    @Override public String getUserAgentStylesheet() {
+        return getUserAgentStylesheet(PropertySheet.class, "propertysheet.css");
+    }
+
+    
+    /**************************************************************************
+     * 
+     * Properties
+     * 
+     **************************************************************************/
+    
+    // --- modeProperty 
+    private final SimpleObjectProperty<Mode> modeProperty = 
+            new SimpleObjectProperty<>(this, "mode", Mode.NAME); //$NON-NLS-1$
+    
+    /**
+     * Used to represent how the properties should be laid out in
+     * the PropertySheet. Refer to the {@link Mode} enumeration to better 
+     * understand the available options.
+     * @return A SimpleObjectproperty. 
+     */
+    public final SimpleObjectProperty<Mode> modeProperty() {
+    	return modeProperty;
+    }
+
+    /**
+     * @see Mode
+     * @return how the properties should be laid out in
+     * the PropertySheet.
+     */
+    public final Mode getMode() {
+        return modeProperty.get();
+    }
+
+    /**
+     * Set how the properties should be laid out in
+     * the PropertySheet.
+     * @param mode 
+     */
+    public final void setMode(Mode mode) {
+        modeProperty.set(mode);
+    }
+  
+
+    // --- propertyEditorFactory
+    private final SimpleObjectProperty<Callback<Item, PropertyEditor<?>>> propertyEditorFactory = 
+            new SimpleObjectProperty<>(this, "propertyEditor", new DefaultPropertyEditorFactory()); //$NON-NLS-1$
+    
+    /**
+     * The property editor factory is used by the PropertySheet to determine which
+     * {@link PropertyEditor} to use for a given {@link Item}. By default the
+     * {@link DefaultPropertyEditorFactory} is used, but this may be replaced
+     * or extended by developers wishing to add in (or substitute) their own
+     * property editors.
+     * 
+     * @return A SimpleObjectproperty.
+     */
+    public final SimpleObjectProperty<Callback<Item, PropertyEditor<?>>> propertyEditorFactory() {
+        return propertyEditorFactory;
+    }
+    
+    /**
+     * 
+     * @return The editor factory used by the PropertySheet to determine which
+     * {@link PropertyEditor} to use for a given {@link Item}.
+     */
+    public final Callback<Item, PropertyEditor<?>> getPropertyEditorFactory() {
+        return propertyEditorFactory.get();
+    }
+    
+    /**
+     * Sets a new editor factory used by the PropertySheet to determine which
+     * {@link PropertyEditor} to use for a given {@link Item}.
+     * @param factory 
+     */
+    public final void setPropertyEditorFactory( Callback<Item, PropertyEditor<?>> factory ) {
+        propertyEditorFactory.set( factory == null? new DefaultPropertyEditorFactory(): factory );
+    }
+    
+    
+    // --- modeSwitcherVisible
+    private final SimpleBooleanProperty modeSwitcherVisible = 
+            new SimpleBooleanProperty(this, "modeSwitcherVisible", true); //$NON-NLS-1$
+    
+    /**
+     * This property represents whether a visual option should be presented to
+     * users to switch between the various {@link Mode modes} available. By
+     * default this is true, so setting it to false will hide these buttons.
+     * @return A SimpleBooleanproperty.
+     */
+    public final SimpleBooleanProperty modeSwitcherVisibleProperty() {
+        return modeSwitcherVisible;
+    }
+    
+    /**
+     * 
+     * @return whether a visual option is presented to
+     * users to switch between the various {@link Mode modes} available.
+     */
+    public final boolean isModeSwitcherVisible() {
+        return modeSwitcherVisible.get();
+    }
+    
+    /**
+     * Set whether a visual option should be presented to
+     * users to switch between the various {@link Mode modes} available.
+     * @param visible 
+     */
+    public final void setModeSwitcherVisible( boolean visible ) {
+        modeSwitcherVisible.set(visible);
+    }
+    
+    
+    // --- toolbarSearchVisibleProperty
+    private final SimpleBooleanProperty searchBoxVisible = 
+            new SimpleBooleanProperty(this, "searchBoxVisible", true); //$NON-NLS-1$
+    
+    /**
+     * 
+     */
+    /**
+     * This property represents whether a text field should be presented to
+     * users to allow for them to filter the properties in the property sheet to
+     * only show ones matching the typed input. By default this is true, so 
+     * setting it to false will hide this search field.
+     * @return A SimpleBooleanProperty.
+     */
+    public final SimpleBooleanProperty searchBoxVisibleProperty() {
+        return searchBoxVisible;
+    }
+    
+    /**
+     * 
+     * @return whether a text field should be presented to
+     * users to allow for them to filter the properties in the property sheet to
+     * only show ones matching the typed input.
+     */
+    public final boolean isSearchBoxVisible() {
+        return searchBoxVisible.get();
+    }
+    
+    /**
+     * Sets whether a text field should be presented to
+     * users to allow for them to filter the properties in the property sheet to
+     * only show ones matching the typed input.
+     * @param visible 
+     */
+    public final void setSearchBoxVisible( boolean visible ) {
+        searchBoxVisible.set(visible);
+    }   
+    
+     
+    // --- titleFilterProperty
+    private final SimpleStringProperty titleFilterProperty = 
+            new SimpleStringProperty(this, "titleFilter", ""); //$NON-NLS-1$ //$NON-NLS-2$
+    
+    /**
+     * Regardless of whether the {@link #searchBoxVisibleProperty() search box}
+     * is visible or not, it is possible to filter the options shown on screen
+     * using this title filter property. If the search box is visible, it will
+     * manipulate this property to contain whatever the user types.
+     * @return A SimpleStringProperty.
+     */
+    public final SimpleStringProperty titleFilter() {
+        return titleFilterProperty;
+    }
+    
+    /**
+     * @see #titleFilter() 
+     * @return the filter for filtering the options shown on screen
+     */
+    public final String getTitleFilter() {
+        return titleFilterProperty.get();
+    }
+    
+    /**
+     * Sets the filter for filtering the options shown on screen.
+     * @param filter 
+     * @see #titleFilter() 
+     */
+    public final void setTitleFilter( String filter ) {
+        titleFilterProperty.set(filter);
+    }
+    
+    
+    
+    /***************************************************************************
+     *                                                                         *
+     * Stylesheet Handling                                                     *
+     *                                                                         *
+     **************************************************************************/
+
+    private static final String DEFAULT_STYLE_CLASS = "property-sheet"; //$NON-NLS-1$
+    
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/RangeSlider.java b/controlsfx/src/main/java/org/controlsfx/control/RangeSlider.java
new file mode 100644
index 0000000..b60462a
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/RangeSlider.java
@@ -0,0 +1,1102 @@
+/**
+ * Copyright (c) 2013, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control;
+
+import impl.org.controlsfx.skin.RangeSliderSkin;
+import org.controlsfx.tools.Utils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.DoublePropertyBase;
+import javafx.beans.property.IntegerProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.css.CssMetaData;
+import javafx.css.PseudoClass;
+import javafx.css.StyleOrigin;
+import javafx.css.Styleable;
+import javafx.css.StyleableBooleanProperty;
+import javafx.css.StyleableDoubleProperty;
+import javafx.css.StyleableIntegerProperty;
+import javafx.css.StyleableObjectProperty;
+import javafx.css.StyleableProperty;
+import javafx.geometry.Orientation;
+import javafx.scene.control.Control;
+import javafx.scene.control.Skin;
+import javafx.scene.control.Slider;
+
+import com.sun.javafx.css.converters.BooleanConverter;
+import com.sun.javafx.css.converters.EnumConverter;
+import com.sun.javafx.css.converters.SizeConverter;
+
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.util.StringConverter;
+
+/**
+ * The RangeSlider control is simply a JavaFX {@link Slider} control with support
+ * for two 'thumbs', rather than one. A thumb is the non-technical name for the
+ * draggable area inside the Slider / RangeSlider that allows for a value to be
+ * set. 
+ * 
+ * <p>Because the RangeSlider has two thumbs, it also has a few additional rules
+ * and user interactions:
+ * 
+ * <ol>
+ *   <li>The 'lower value' thumb can not move past the 'higher value' thumb.
+ *   <li>Whereas the {@link Slider} control only has one 
+ *       {@link Slider#valueProperty() value} property, the RangeSlider has a 
+ *       {@link #lowValueProperty() low value} and a 
+ *       {@link #highValueProperty() high value} property, not surprisingly 
+ *       represented by the 'low value' and 'high value' thumbs.
+ *   <li>The area between the low and high values represents the allowable range.
+ *       For example, if the low value is 2 and the high value is 8, then the
+ *       allowable range is between 2 and 8. 
+ *   <li>The allowable range area is rendered differently. This area is able to 
+ *       be dragged with mouse / touch input to allow for the entire range to
+ *       be modified. For example, following on from the previous example of the
+ *       allowable range being between 2 and 8, if the user drags the range bar
+ *       to the right, the low value will adjust to 3, and the high value 9, and
+ *       so on until the user stops adjusting. 
+ * </ol>
+ * 
+ * <h3>Screenshots</h3>
+ * Because the RangeSlider supports both horizontal and vertical 
+ * {@link #orientationProperty() orientation}, there are two screenshots below:
+ * 
+ * <table border="0" summary="Screenshot of RangeSlider orientation">
+ *   <tr>
+ *     <td width="75" valign="center"><strong>Horizontal:</strong></td>
+ *     <td><img src="rangeSlider-horizontal.png" alt="Screenshot of a horizontal RangeSlider"></td>
+ *   </tr>
+ *   <tr>
+ *     <td width="75" valign="top"><strong>Vertical:</strong></td>
+ *     <td><img src="rangeSlider-vertical.png" alt="Screenshot of a vertical RangeSlider"></td>
+ *   </tr>
+ * </table>
+ * 
+ * <h3>Code Samples</h3>
+ * Instantiating a RangeSlider is simple. The first decision is to decide whether
+ * a horizontal or a vertical track is more appropriate. By default RangeSlider
+ * instances are horizontal, but this can be changed by setting the 
+ * {@link #orientationProperty() orientation} property.
+ * 
+ * <p>Once the orientation is determined, the next most important decision is
+ * to determine what the {@link #minProperty() min} / {@link #maxProperty() max}
+ * and default {@link #lowValueProperty() low} / {@link #highValueProperty() high}
+ * values are. The min / max values represent the smallest and largest legal
+ * values for the thumbs to be set to, whereas the low / high values represent
+ * where the thumbs are currently, within the bounds of the min / max values.
+ * Because all four values are required in all circumstances, they are all
+ * required parameters to instantiate a RangeSlider: the constructor takes
+ * four doubles, representing min, max, lowValue and highValue (in that order).
+ * 
+ * <p>For example, here is a simple horizontal RangeSlider that has a minimum
+ * value of 0, a maximum value of 100, a low value of 10 and a high value of 90: 
+ * 
+ * <pre>{@code final RangeSlider hSlider = new RangeSlider(0, 100, 10, 90);}</pre>
+ * 
+ * <p>To configure the hSlider to look like the RangeSlider in the horizontal
+ * RangeSlider screenshot above only requires a few additional properties to be 
+ * set:
+ * 
+ * <pre>
+ * {@code
+ * final RangeSlider hSlider = new RangeSlider(0, 100, 10, 90);
+ * hSlider.setShowTickMarks(true);
+ * hSlider.setShowTickLabels(true);
+ * hSlider.setBlockIncrement(10);}</pre>
+ * 
+ * <p>To create a vertical slider, simply do the following:
+ * 
+ * <pre>
+ * {@code
+ * final RangeSlider vSlider = new RangeSlider(0, 200, 30, 150);
+ * vSlider.setOrientation(Orientation.VERTICAL);}</pre>
+ * 
+ * <p>This code creates a RangeSlider with a min value of 0, a max value of 200,
+ * a low value of 30, and a high value of 150.
+ * 
+ * @see Slider
+ */
+public class RangeSlider extends ControlsFXControl {
+    
+    /***************************************************************************
+     *                                                                         *
+     * Constructors                                                            *
+     *                                                                         *
+     **************************************************************************/
+    
+    /**
+     * Creates a new RangeSlider instance using default values of 0.0, 0.25, 0.75
+     * and 1.0 for min/lowValue/highValue/max, respectively. 
+     */
+    public RangeSlider() {
+        this(0, 1.0, 0.25, 0.75);
+    }
+
+    /**
+     * Instantiates a default, horizontal RangeSlider with the specified 
+     * min/max/low/high values.
+     * 
+     * @param min The minimum allowable value that the RangeSlider will allow.
+     * @param max The maximum allowable value that the RangeSlider will allow.
+     * @param lowValue The initial value for the low value in the RangeSlider.
+     * @param highValue The initial value for the high value in the RangeSlider.
+     */
+    public RangeSlider(double min, double max, double lowValue, double highValue) {
+        getStyleClass().setAll(DEFAULT_STYLE_CLASS);
+        
+        setMax(max);
+        setMin(min);
+        adjustValues();
+        setLowValue(lowValue);
+        setHighValue(highValue);
+    }
+        
+    /** {@inheritDoc} */
+    @Override public String getUserAgentStylesheet() {
+        return getUserAgentStylesheet(RangeSlider.class, "rangeslider.css");
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    @Override protected Skin<?> createDefaultSkin() {
+        return new RangeSliderSkin(this);
+    }
+  
+    
+    
+    /***************************************************************************
+     *                                                                         *
+     * New properties (over and above what is in Slider)                       *
+     *                                                                         *
+     **************************************************************************/
+    
+    // --- low value
+    /**
+     * The low value property represents the current position of the low value
+     * thumb, and is within the allowable range as specified by the
+     * {@link #minProperty() min} and {@link #maxProperty() max} properties. By
+     * default this value is 0.
+     */
+    public final DoubleProperty lowValueProperty() {
+        return lowValue;
+    }
+    private DoubleProperty lowValue = new SimpleDoubleProperty(this, "lowValue", 0.0D) { //$NON-NLS-1$
+        @Override protected void invalidated() {
+            adjustLowValues();
+        }
+    };
+    
+    /**
+     * Sets the low value for the range slider, which may or may not be clamped
+     * to be within the allowable range as specified by the
+     * {@link #minProperty() min} and {@link #maxProperty() max} properties.
+     */
+    public final void setLowValue(double d) {
+        lowValueProperty().set(d);
+    }
+
+    /**
+     * Returns the current low value for the range slider.
+     */
+    public final double getLowValue() {
+        return lowValue != null ? lowValue.get() : 0.0D;
+    }
+
+    
+    
+    // --- low value changing
+    /**
+     * When true, indicates the current low value of this RangeSlider is changing.
+     * It provides notification that the low value is changing. Once the low 
+     * value is computed, it is set back to false.
+     */
+    public final BooleanProperty lowValueChangingProperty() {
+        if (lowValueChanging == null) {
+            lowValueChanging = new SimpleBooleanProperty(this, "lowValueChanging", false); //$NON-NLS-1$
+        }
+        return lowValueChanging;
+    }
+    
+    private BooleanProperty lowValueChanging;
+
+    /**
+     * Call this when the low value is changing.
+     * @param value True if the low value is changing, false otherwise.
+     */
+    public final void setLowValueChanging(boolean value) {
+        lowValueChangingProperty().set(value);
+    }
+
+    /**
+     * Returns whether or not the low value of this RangeSlider is currently
+     * changing.
+     */
+    public final boolean isLowValueChanging() {
+        return lowValueChanging == null ? false : lowValueChanging.get();
+    }
+
+    
+    // --- high value
+    /**
+     * The high value property represents the current position of the high value
+     * thumb, and is within the allowable range as specified by the
+     * {@link #minProperty() min} and {@link #maxProperty() max} properties. By
+     * default this value is 100.
+     */
+    public final DoubleProperty highValueProperty() {
+        return highValue;
+    }
+    private DoubleProperty highValue = new SimpleDoubleProperty(this, "highValue", 100D) { //$NON-NLS-1$
+        @Override protected void invalidated() {
+            adjustHighValues();
+        }
+        
+        @Override public Object getBean() {
+            return RangeSlider.this;
+        }
+
+        @Override public String getName() {
+            return "highValue"; //$NON-NLS-1$
+        }
+    };
+    
+    /**
+     * Sets the high value for the range slider, which may or may not be clamped
+     * to be within the allowable range as specified by the
+     * {@link #minProperty() min} and {@link #maxProperty() max} properties.
+     */
+    public final void setHighValue(double d) {
+        if (!highValueProperty().isBound()) highValueProperty().set(d);
+    }
+
+    /**
+     * Returns the current high value for the range slider.
+     */
+    public final double getHighValue() {
+        return highValue != null ? highValue.get() : 100D;
+    }
+
+    
+
+    // --- high value changing
+    /**
+     * When true, indicates the current high value of this RangeSlider is changing.
+     * It provides notification that the high value is changing. Once the high 
+     * value is computed, it is set back to false.
+     */
+    public final BooleanProperty highValueChangingProperty() {
+        if (highValueChanging == null) {
+            highValueChanging = new SimpleBooleanProperty(this, "highValueChanging", false); //$NON-NLS-1$
+        }
+        return highValueChanging;
+    }
+    private BooleanProperty highValueChanging;
+
+    /**
+     * Call this when high low value is changing.
+     * @param value True if the high value is changing, false otherwise.
+     */
+    public final void setHighValueChanging(boolean value) {
+        highValueChangingProperty().set(value);
+    }
+
+    /**
+     * Returns whether or not the high value of this RangeSlider is currently
+     * changing.
+     */
+    public final boolean isHighValueChanging() {
+        return highValueChanging == null ? false : highValueChanging.get();
+    }
+    
+    private final ObjectProperty<StringConverter<Number>> tickLabelFormatter = new SimpleObjectProperty<>();
+    
+    /**
+     * Gets the value of the property tickLabelFormatter.
+     * @return the value of the property tickLabelFormatter.
+     */
+    public final StringConverter<Number> getLabelFormatter(){
+        return tickLabelFormatter.get();
+    }
+    
+    /**
+     * Sets the value of the property tickLabelFormatter.
+     * @param value 
+     */
+    public final void setLabelFormatter(StringConverter<Number> value){
+        tickLabelFormatter.set(value);
+    }
+    /**
+     * StringConverter used to format tick mark labels. If null a default will be used.
+     * @return a Property containing the StringConverter.
+     */
+    public final ObjectProperty<StringConverter<Number>> labelFormatterProperty(){
+        return tickLabelFormatter;
+    }
+    
+    /***************************************************************************
+     *                                                                         *
+     * New public API                                                          *
+     *                                                                         *
+     **************************************************************************/
+    
+    /**
+     * Increments the {@link #lowValueProperty() low value} by the 
+     * {@link #blockIncrementProperty() block increment} amount.
+     */
+    public void incrementLowValue() {
+        adjustLowValue(getLowValue() + getBlockIncrement());
+    }
+
+    /**
+     * Decrements the {@link #lowValueProperty() low value} by the 
+     * {@link #blockIncrementProperty() block increment} amount.
+     */
+    public void decrementLowValue() {
+        adjustLowValue(getLowValue() - getBlockIncrement());
+    }
+    
+    /**
+     * Increments the {@link #highValueProperty() high value} by the 
+     * {@link #blockIncrementProperty() block increment} amount.
+     */
+    public void incrementHighValue() {
+        adjustHighValue(getHighValue() + getBlockIncrement());
+    }
+
+    /**
+     * Decrements the {@link #highValueProperty() high value} by the 
+     * {@link #blockIncrementProperty() block increment} amount.
+     */
+    public void decrementHighValue() {
+        adjustHighValue(getHighValue() - getBlockIncrement());
+    }
+    
+    /**
+     * Adjusts {@link #lowValueProperty() lowValue} to match <code>newValue</code>,
+     * or as closely as possible within the constraints imposed by the 
+     * {@link #minProperty() min} and {@link #maxProperty() max} properties. 
+     * This function also takes into account 
+     * {@link #snapToTicksProperty() snapToTicks}, which is the main difference 
+     * between <code>adjustLowValue</code> and 
+     * {@link #setLowValue(double) setLowValue}.
+     */
+    public void adjustLowValue(double newValue) {
+        double d1 = getMin();
+        double d2 = getMax();
+        if (d2 <= d1) {
+            // no-op
+        } else {
+            newValue = newValue >= d1 ? newValue : d1;
+            newValue = newValue <= d2 ? newValue : d2;
+            setLowValue(snapValueToTicks(newValue));
+        }
+    }
+
+    /**
+     * Adjusts {@link #highValueProperty() highValue} to match <code>newValue</code>,
+     * or as closely as possible within the constraints imposed by the 
+     * {@link #minProperty() min} and {@link #maxProperty() max} properties. 
+     * This function also takes into account 
+     * {@link #snapToTicksProperty() snapToTicks}, which is the main difference 
+     * between <code>adjustHighValue</code> and 
+     * {@link #setHighValue(double) setHighValue}.
+     */
+    public void adjustHighValue(double newValue) {
+        double d1 = getMin();
+        double d2 = getMax();
+        if (d2 <= d1) {
+            // no-op
+        } else {
+            newValue = newValue >= d1 ? newValue : d1;
+            newValue = newValue <= d2 ? newValue : d2;
+            setHighValue(snapValueToTicks(newValue));
+        }
+    }
+
+    
+    
+    /***************************************************************************
+     *                                                                         *
+     * Properties copied from Slider (and slightly edited)                     *
+     *                                                                         *
+     **************************************************************************/
+    
+    private DoubleProperty max;
+    
+    /**
+     * Sets the maximum value for this Slider.
+     * @param value 
+     */
+    public final void setMax(double value) {
+        maxProperty().set(value);
+    }
+
+    /**
+     * @return The maximum value of this slider. 100 is returned if
+     * the maximum value has never been set.
+     */
+    public final double getMax() {
+        return max == null ? 100 : max.get();
+    }
+
+    /**
+     * 
+     * @return A DoubleProperty representing the maximum value of this Slider. 
+     * This must be a value greater than {@link #minProperty() min}.
+     */
+    public final DoubleProperty maxProperty() {
+        if (max == null) {
+            max = new DoublePropertyBase(100) {
+                @Override protected void invalidated() {
+                    if (get() < getMin()) {
+                        setMin(get());
+                    }
+                    adjustValues();
+                }
+
+                @Override public Object getBean() {
+                    return RangeSlider.this;
+                }
+
+                @Override public String getName() {
+                    return "max"; //$NON-NLS-1$
+                }
+            };
+        }
+        return max;
+    }
+
+    private DoubleProperty min;
+    
+    /**
+     * Sets the minimum value for this Slider.
+     * @param value 
+     */
+    public final void setMin(double value) {
+        minProperty().set(value);
+    }
+
+    /**
+     * 
+     * @return the minimum value for this Slider. 0 is returned if the minimum
+     * has never been set.
+     */
+    public final double getMin() {
+        return min == null ? 0 : min.get();
+    }
+
+    /**
+     * 
+     * @return A DoubleProperty representing The minimum value of this Slider. 
+     * This must be a value less than {@link #maxProperty() max}.
+     */
+    public final DoubleProperty minProperty() {
+        if (min == null) {
+            min = new DoublePropertyBase(0) {
+                @Override protected void invalidated() {
+                    if (get() > getMax()) {
+                        setMax(get());
+                    }
+                    adjustValues();
+                }
+
+                @Override public Object getBean() {
+                    return RangeSlider.this;
+                }
+
+                @Override public String getName() {
+                    return "min"; //$NON-NLS-1$
+                }
+            };
+        }
+        return min;
+    }
+    
+    /**
+     * 
+     */
+    private BooleanProperty snapToTicks;
+    
+    /**
+     * Sets the value of SnapToTicks. 
+     * @see #snapToTicksProperty() 
+     * @param value 
+     */
+    public final void setSnapToTicks(boolean value) {
+        snapToTicksProperty().set(value);
+    }
+
+    /**
+     * 
+     * @return the value of SnapToTicks.
+     * @see #snapToTicksProperty() 
+     */
+    public final boolean isSnapToTicks() {
+        return snapToTicks == null ? false : snapToTicks.get();
+    }
+
+    /**
+     * Indicates whether the {@link #lowValueProperty()} value} / 
+     * {@link #highValueProperty()} value} of the {@code Slider} should always
+     * be aligned with the tick marks. This is honored even if the tick marks
+     * are not shown.
+     * @return A BooleanProperty.
+     */
+    public final BooleanProperty snapToTicksProperty() {
+        if (snapToTicks == null) {
+            snapToTicks = new StyleableBooleanProperty(false) {
+                @Override public CssMetaData<? extends Styleable, Boolean> getCssMetaData() {
+                    return RangeSlider.StyleableProperties.SNAP_TO_TICKS;
+                }
+
+                @Override public Object getBean() {
+                    return RangeSlider.this;
+                }
+
+                @Override public String getName() {
+                    return "snapToTicks"; //$NON-NLS-1$
+                }
+            };
+        }
+        return snapToTicks;
+    }
+    /**
+     * 
+     */
+    private DoubleProperty majorTickUnit;
+    
+    /**
+     * Sets the unit distance between major tick marks.
+     * @param value 
+     * @see #majorTickUnitProperty() 
+     */
+    public final void setMajorTickUnit(double value) {
+        if (value <= 0) {
+            throw new IllegalArgumentException("MajorTickUnit cannot be less than or equal to 0."); //$NON-NLS-1$
+        }
+        majorTickUnitProperty().set(value);
+    }
+
+    /**
+     * @see #majorTickUnitProperty() 
+     * @return The unit distance between major tick marks.
+     */
+    public final double getMajorTickUnit() {
+        return majorTickUnit == null ? 25 : majorTickUnit.get();
+    }
+
+    /**
+     * The unit distance between major tick marks. For example, if
+     * the {@link #minProperty() min} is 0 and the {@link #maxProperty() max} is 100 and the
+     * {@link #majorTickUnitProperty() majorTickUnit} is 25, then there would be 5 tick marks: one at
+     * position 0, one at position 25, one at position 50, one at position
+     * 75, and a final one at position 100.
+     * <p>
+     * This value should be positive and should be a value less than the
+     * span. Out of range values are essentially the same as disabling
+     * tick marks.
+     * 
+     * @return A DoubleProperty
+     */
+    public final DoubleProperty majorTickUnitProperty() {
+        if (majorTickUnit == null) {
+            majorTickUnit = new StyleableDoubleProperty(25) {
+                @Override public void invalidated() {
+                    if (get() <= 0) {
+                        throw new IllegalArgumentException("MajorTickUnit cannot be less than or equal to 0."); //$NON-NLS-1$
+                    }
+                }
+                
+                @Override public CssMetaData<? extends Styleable, Number> getCssMetaData() {
+                    return StyleableProperties.MAJOR_TICK_UNIT;
+                }
+
+                @Override public Object getBean() {
+                    return RangeSlider.this;
+                }
+
+                @Override public String getName() {
+                    return "majorTickUnit"; //$NON-NLS-1$
+                }
+            };
+        }
+        return majorTickUnit;
+    }
+    /**
+     * 
+     */
+    private IntegerProperty minorTickCount;
+    
+    /**
+     * Sets the number of minor ticks to place between any two major ticks.
+     * @param value 
+     * @see #minorTickCountProperty() 
+     */
+    public final void setMinorTickCount(int value) {
+        minorTickCountProperty().set(value);
+    }
+
+    /**
+     * @see #minorTickCountProperty() 
+     * @return The number of minor ticks to place between any two major ticks.
+     */
+    public final int getMinorTickCount() {
+        return minorTickCount == null ? 3 : minorTickCount.get();
+    }
+
+    /**
+     * The number of minor ticks to place between any two major ticks. This
+     * number should be positive or zero. Out of range values will disable
+     * disable minor ticks, as will a value of zero.
+     * @return An InterProperty
+     */
+    public final IntegerProperty minorTickCountProperty() {
+        if (minorTickCount == null) {
+            minorTickCount = new StyleableIntegerProperty(3) {
+                @Override public CssMetaData<? extends Styleable, Number> getCssMetaData() {
+                    return RangeSlider.StyleableProperties.MINOR_TICK_COUNT;
+                }
+
+                @Override public Object getBean() {
+                    return RangeSlider.this;
+                }
+
+                @Override public String getName() {
+                    return "minorTickCount"; //$NON-NLS-1$
+                }
+            };
+        }
+        return minorTickCount;
+    }
+    /**
+     *
+     */
+    private DoubleProperty blockIncrement;
+    
+    /**
+     * Sets the amount by which to adjust the slider if the track of the slider is
+     * clicked.
+     * @param value 
+     * @see #blockIncrementProperty() 
+     */
+    public final void setBlockIncrement(double value) {
+        blockIncrementProperty().set(value);
+    }
+
+    /**
+     * @see #blockIncrementProperty() 
+     * @return The amount by which to adjust the slider if the track of the slider is
+     * clicked.
+     */
+    public final double getBlockIncrement() {
+        return blockIncrement == null ? 10 : blockIncrement.get();
+    }
+
+    /**
+     *  The amount by which to adjust the slider if the track of the slider is
+     * clicked. This is used when manipulating the slider position using keys. If
+     * {@link #snapToTicksProperty() snapToTicks} is true then the nearest tick mark to the adjusted
+     * value will be used.
+     * @return A DoubleProperty
+     */
+    public final DoubleProperty blockIncrementProperty() {
+        if (blockIncrement == null) {
+            blockIncrement = new StyleableDoubleProperty(10) {
+                @Override public CssMetaData<? extends Styleable, Number> getCssMetaData() {
+                    return RangeSlider.StyleableProperties.BLOCK_INCREMENT;
+                }
+
+                @Override public Object getBean() {
+                    return RangeSlider.this;
+                }
+
+                @Override public String getName() {
+                    return "blockIncrement"; //$NON-NLS-1$
+                }
+            };
+        }
+        return blockIncrement;
+    }
+    
+    /**
+     * 
+     */
+    private ObjectProperty<Orientation> orientation;
+    
+    /**
+     * Sets the orientation of the Slider.
+     * @param value 
+     */
+    public final void setOrientation(Orientation value) {
+        orientationProperty().set(value);
+    }
+
+    /**
+     * 
+     * @return The orientation of the Slider. {@link Orientation#HORIZONTAL} is 
+     * returned by default.
+     */
+    public final Orientation getOrientation() {
+        return orientation == null ? Orientation.HORIZONTAL : orientation.get();
+    }
+
+    /**
+     * The orientation of the {@code Slider} can either be horizontal
+     * or vertical.
+     * @return An Objectproperty representing the orientation of the Slider.
+     */
+    public final ObjectProperty<Orientation> orientationProperty() {
+        if (orientation == null) {
+            orientation = new StyleableObjectProperty<Orientation>(Orientation.HORIZONTAL) {
+                @Override protected void invalidated() {
+                    final boolean vertical = (get() == Orientation.VERTICAL);
+                    pseudoClassStateChanged(VERTICAL_PSEUDOCLASS_STATE, vertical);
+                    pseudoClassStateChanged(HORIZONTAL_PSEUDOCLASS_STATE, ! vertical);
+                }
+                
+                @Override public CssMetaData<? extends Styleable, Orientation> getCssMetaData() {
+                    return RangeSlider.StyleableProperties.ORIENTATION;
+                }
+
+                @Override public Object getBean() {
+                    return RangeSlider.this;
+                }
+
+                @Override public String getName() {
+                    return "orientation"; //$NON-NLS-1$
+                }
+            };
+        }
+        return orientation;
+    }
+    
+    private BooleanProperty showTickLabels;
+    
+    /**
+     * Sets whether labels of tick marks should be shown or not.
+     * @param value 
+     */
+    public final void setShowTickLabels(boolean value) {
+        showTickLabelsProperty().set(value);
+    }
+
+    /**
+     * @return whether labels of tick marks are being shown.
+     */
+    public final boolean isShowTickLabels() {
+        return showTickLabels == null ? false : showTickLabels.get();
+    }
+
+    /**
+     * Indicates that the labels for tick marks should be shown. Typically a
+     * {@link Skin} implementation will only show labels if
+     * {@link #showTickMarksProperty() showTickMarks} is also true.
+     * @return A BooleanProperty
+     */
+    public final BooleanProperty showTickLabelsProperty() {
+        if (showTickLabels == null) {
+            showTickLabels = new StyleableBooleanProperty(false) {
+                @Override public CssMetaData<? extends Styleable, Boolean> getCssMetaData() {
+                    return RangeSlider.StyleableProperties.SHOW_TICK_LABELS;
+                }
+
+                @Override public Object getBean() {
+                    return RangeSlider.this;
+                }
+
+                @Override public String getName() {
+                    return "showTickLabels"; //$NON-NLS-1$
+                }
+            };
+        }
+        return showTickLabels;
+    }
+    /**
+     * 
+     */
+    private BooleanProperty showTickMarks;
+    
+    /**
+     * Specifies whether the {@link Skin} implementation should show tick marks.
+     * @param value 
+     */
+    public final void setShowTickMarks(boolean value) {
+        showTickMarksProperty().set(value);
+    }
+
+    /**
+     * 
+     * @return whether the {@link Skin} implementation should show tick marks.
+     */
+    public final boolean isShowTickMarks() {
+        return showTickMarks == null ? false : showTickMarks.get();
+    }
+
+    /**
+     * @return A BooleanProperty that specifies whether the {@link Skin} 
+     * implementation should show tick marks.
+     */
+    public final BooleanProperty showTickMarksProperty() {
+        if (showTickMarks == null) {
+            showTickMarks = new StyleableBooleanProperty(false) {
+                @Override public CssMetaData<? extends Styleable, Boolean> getCssMetaData() {
+                    return RangeSlider.StyleableProperties.SHOW_TICK_MARKS;
+                }
+
+                @Override public Object getBean() {
+                    return RangeSlider.this;
+                }
+
+                @Override public String getName() {
+                    return "showTickMarks"; //$NON-NLS-1$
+                }
+            };
+        }
+        return showTickMarks;
+    }
+    
+
+    
+     /***************************************************************************
+     *                                                                         *
+     * Private methods                                                         *
+     *                                                                         *
+     **************************************************************************/    
+    
+    /**
+     * Ensures that min is always < max, that value is always
+     * somewhere between the two, and that if snapToTicks is set then the
+     * value will always be set to align with a tick mark.
+     */
+    private void adjustValues() {
+        adjustLowValues();
+        adjustHighValues();
+    }
+
+    private void adjustLowValues() {
+        /**
+         * We first look if the LowValue is between the min and max.
+         */
+        if (getLowValue() < getMin() || getLowValue() > getMax()) {
+            double value = Utils.clamp(getMin(), getLowValue(), getMax());
+            setLowValue(value);
+        /**
+         * If the LowValue seems right, we check if it's not superior to
+         * HighValue ONLY if the highValue itself is right. Because it may
+         * happen that the highValue has not yet been computed and is
+         * wrong, and therefore force the lowValue to change in a wrong way
+         * which may end up in an infinite loop.
+         */
+        } else if (getLowValue() >= getHighValue() && (getHighValue() >= getMin() && getHighValue() <= getMax())) {
+            double value = Utils.clamp(getMin(), getLowValue(), getHighValue());
+            setLowValue(value);
+        }
+    }
+    
+    private double snapValueToTicks(double d) {
+        double d1 = d;
+        if (isSnapToTicks()) {
+            double d2 = 0.0D;
+            if (getMinorTickCount() != 0) {
+                d2 = getMajorTickUnit() / (double) (Math.max(getMinorTickCount(), 0) + 1);
+            } else {
+                d2 = getMajorTickUnit();
+            }
+            int i = (int) ((d1 - getMin()) / d2);
+            double d3 = (double) i * d2 + getMin();
+            double d4 = (double) (i + 1) * d2 + getMin();
+            d1 = Utils.nearest(d3, d1, d4);
+        }
+        return Utils.clamp(getMin(), d1, getMax());
+    }
+
+    private void adjustHighValues() {
+        if (getHighValue() < getMin() || getHighValue() > getMax()) {
+            setHighValue(Utils.clamp(getMin(), getHighValue(), getMax()));
+        } else if (getHighValue() < getLowValue() && (getLowValue() >= getMin() && getLowValue() <= getMax())) {
+            setHighValue(Utils.clamp(getLowValue(), getHighValue(), getMax()));
+        }
+    }
+
+    
+    
+    /**************************************************************************
+    *                                                                         *
+    * Stylesheet Handling                                                     *
+    *                                                                         *
+    **************************************************************************/
+    
+    private static final String DEFAULT_STYLE_CLASS = "range-slider"; //$NON-NLS-1$
+    
+    private static class StyleableProperties {
+        private static final CssMetaData<RangeSlider,Number> BLOCK_INCREMENT =
+            new CssMetaData<RangeSlider,Number>("-fx-block-increment", //$NON-NLS-1$
+                SizeConverter.getInstance(), 10.0) {
+
+            @Override public boolean isSettable(RangeSlider n) {
+                return n.blockIncrement == null || !n.blockIncrement.isBound();
+            }
+
+            @SuppressWarnings("unchecked")
+            @Override public StyleableProperty<Number> getStyleableProperty(RangeSlider n) {
+                return (StyleableProperty<Number>)n.blockIncrementProperty();
+            }
+        };
+        
+        private static final CssMetaData<RangeSlider,Boolean> SHOW_TICK_LABELS =
+            new CssMetaData<RangeSlider,Boolean>("-fx-show-tick-labels", //$NON-NLS-1$
+                BooleanConverter.getInstance(), Boolean.FALSE) {
+
+            @Override public boolean isSettable(RangeSlider n) {
+                return n.showTickLabels == null || !n.showTickLabels.isBound();
+            }
+
+            @SuppressWarnings("unchecked")
+            @Override public StyleableProperty<Boolean> getStyleableProperty(RangeSlider n) {
+                return (StyleableProperty<Boolean>)n.showTickLabelsProperty();
+            }
+        };
+                    
+        private static final CssMetaData<RangeSlider,Boolean> SHOW_TICK_MARKS =
+            new CssMetaData<RangeSlider,Boolean>("-fx-show-tick-marks", //$NON-NLS-1$
+                BooleanConverter.getInstance(), Boolean.FALSE) {
+
+            @Override public boolean isSettable(RangeSlider n) {
+                return n.showTickMarks == null || !n.showTickMarks.isBound();
+            }
+
+            @SuppressWarnings("unchecked")
+            @Override public StyleableProperty<Boolean> getStyleableProperty(RangeSlider n) {
+                return (StyleableProperty<Boolean>)n.showTickMarksProperty();
+            }
+        };
+            
+        private static final CssMetaData<RangeSlider,Boolean> SNAP_TO_TICKS =
+            new CssMetaData<RangeSlider,Boolean>("-fx-snap-to-ticks", //$NON-NLS-1$
+                BooleanConverter.getInstance(), Boolean.FALSE) {
+
+            @Override public boolean isSettable(RangeSlider n) {
+                return n.snapToTicks == null || !n.snapToTicks.isBound();
+            }
+
+            @SuppressWarnings("unchecked")
+            @Override public StyleableProperty<Boolean> getStyleableProperty(RangeSlider n) {
+                return (StyleableProperty<Boolean>)n.snapToTicksProperty();
+            }
+        };
+        
+        private static final CssMetaData<RangeSlider,Number> MAJOR_TICK_UNIT =
+            new CssMetaData<RangeSlider,Number>("-fx-major-tick-unit", //$NON-NLS-1$
+                SizeConverter.getInstance(), 25.0) {
+
+            @Override public boolean isSettable(RangeSlider n) {
+                return n.majorTickUnit == null || !n.majorTickUnit.isBound();
+            }
+
+            @SuppressWarnings("unchecked")
+            @Override public StyleableProperty<Number> getStyleableProperty(RangeSlider n) {
+                return (StyleableProperty<Number>)n.majorTickUnitProperty();
+            }
+        };
+        
+        private static final CssMetaData<RangeSlider,Number> MINOR_TICK_COUNT =
+            new CssMetaData<RangeSlider,Number>("-fx-minor-tick-count", //$NON-NLS-1$
+                SizeConverter.getInstance(), 3.0) {
+
+            @SuppressWarnings("deprecation")
+            @Override public void set(RangeSlider node, Number value, StyleOrigin origin) {
+                super.set(node, value.intValue(), origin);
+            } 
+            
+            @Override public boolean isSettable(RangeSlider n) {
+                return n.minorTickCount == null || !n.minorTickCount.isBound();
+            }
+
+            @SuppressWarnings("unchecked")
+            @Override public StyleableProperty<Number> getStyleableProperty(RangeSlider n) {
+                return (StyleableProperty<Number>)n.minorTickCountProperty();
+            }
+        };
+        
+        private static final CssMetaData<RangeSlider,Orientation> ORIENTATION =
+            new CssMetaData<RangeSlider,Orientation>("-fx-orientation", //$NON-NLS-1$
+                new EnumConverter<>(Orientation.class), 
+                Orientation.HORIZONTAL) {
+
+            @Override public Orientation getInitialValue(RangeSlider node) {
+                // A vertical Slider should remain vertical 
+                return node.getOrientation();
+            }
+
+            @Override public boolean isSettable(RangeSlider n) {
+                return n.orientation == null || !n.orientation.isBound();
+            }
+
+            @SuppressWarnings("unchecked")
+            @Override public StyleableProperty<Orientation> getStyleableProperty(RangeSlider n) {
+                return (StyleableProperty<Orientation>)n.orientationProperty();
+            }
+        };
+
+        private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
+        static {
+            final List<CssMetaData<? extends Styleable, ?>> styleables = 
+                    new ArrayList<>(Control.getClassCssMetaData());
+            styleables.add(BLOCK_INCREMENT);
+            styleables.add(SHOW_TICK_LABELS);
+            styleables.add(SHOW_TICK_MARKS);
+            styleables.add(SNAP_TO_TICKS);
+            styleables.add(MAJOR_TICK_UNIT);
+            styleables.add(MINOR_TICK_COUNT);
+            styleables.add(ORIENTATION);
+
+            STYLEABLES = Collections.unmodifiableList(styleables);
+        }
+    }
+    
+
+    /**
+     * @return The CssMetaData associated with this class, which may include the
+     * CssMetaData of its super classes.
+     */
+    public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
+        return StyleableProperties.STYLEABLES;
+    }
+
+    private static final PseudoClass VERTICAL_PSEUDOCLASS_STATE =
+            PseudoClass.getPseudoClass("vertical"); //$NON-NLS-1$
+    private static final PseudoClass HORIZONTAL_PSEUDOCLASS_STATE =
+            PseudoClass.getPseudoClass("horizontal"); //$NON-NLS-1$
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/Rating.java b/controlsfx/src/main/java/org/controlsfx/control/Rating.java
new file mode 100644
index 0000000..a5f1f34
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/Rating.java
@@ -0,0 +1,317 @@
+/**
+ * Copyright (c) 2013, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control;
+
+import impl.org.controlsfx.skin.RatingSkin;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.IntegerProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.beans.property.SimpleIntegerProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.geometry.Orientation;
+import javafx.scene.control.Skin;
+
+/**
+ * A control for allowing users to provide a rating. This control supports
+ * {@link #partialRatingProperty() partial ratings} (i.e. not whole numbers and
+ * dependent upon where the user clicks in the control) and 
+ * {@link #updateOnHoverProperty() updating the rating on hover}. Read on for
+ * more examples!
+ * 
+ * <h3>Examples</h3>
+ * It can be hard to appreciate some of the features of the Rating control, so
+ * hopefully the following screenshots will help. Firstly, here is what the 
+ * standard (horizontal) Rating control looks like when it has five stars, and a 
+ * rating of two:
+ * 
+ * <br>
+ * <center>
+ * <img src="rating-horizontal.png" alt="Screenshot of horizontal Rating">
+ * </center>
+ * 
+ * <p>To create a Rating control that looks like this is simple:
+ * 
+ * <pre>
+ * {@code 
+ * final Rating rating = new Rating();}</pre>
+ * 
+ * <p>This creates a default horizontal rating control. To create a vertical 
+ * Rating control, simply change the orientation, as such:
+ * 
+ * <pre>
+ * {@code 
+ * final Rating rating = new Rating();
+ * rating.setOrientation(Orientation.VERTICAL);}</pre>
+ * 
+ * <p>The end result of making this one line change is shown in the screenshot
+ * below:
+ * 
+ * <br>
+ * <center>
+ * <img src="rating-vertical.png" alt="Screenshot of vertical Rating">
+ * </center>
+ * 
+ * <p>One of the features of the Rating control is that it doesn't just allow
+ * for 'integer' ratings: it also allows for the user to click anywhere within
+ * the rating area to set a 'float' rating. This is hard to describe, but easy 
+ * to show in a picture:
+ * 
+ * <br>
+ * <center>
+ * <img src="rating-partial.png" alt="Screenshot of partial Rating">
+ * </center>
+ * 
+ * <p>In essence, in the screenshot above, the user clicked roughly in the 
+ * middle of the third star. This results in a rating of approximately 2.44.
+ * To enable {@link #partialRatingProperty() partial ratings}, simply do the
+ * following when instantiating the Rating control:
+ * 
+ * <pre>
+ * {@code 
+ * final Rating rating = new Rating();
+ * rating.setPartialRating(true);}</pre>
+ * 
+ * <p>So far all of the Rating controls demonstrated above have
+ * required the user to click on the stars to register their rating. This may not
+ * be the preferred user interaction - often times the preferred approach is to
+ * simply allow for the rating to be registered by the user hovering their mouse
+ * over the rating stars. This mode is also supported by the Rating control,
+ * using the {@link #updateOnHoverProperty() update on hover} property, as such:
+ * 
+ * <pre>
+ * {@code 
+ * final Rating rating = new Rating();
+ * rating.setUpdateOnHover(true);}</pre>
+ * 
+ * <p>It is also allowable to have a Rating control that both updates on hover
+ * and allows for partial values: the 'golden fill' of the default graphics will
+ * automatically follow the users mouse as they move it along the Rating scale.
+ * To enable this, just set both properties to true.
+ */
+public class Rating extends ControlsFXControl {
+    
+    /***************************************************************************
+     * 
+     * Constructors
+     * 
+     **************************************************************************/
+    
+    /**
+     * Creates a default instance with a minimum rating of 0 and a maximum 
+     * rating of 5.
+     */
+    public Rating() {
+        this(5);
+    }
+    
+    /**
+     * Creates a default instance with a minimum rating of 0 and a maximum rating
+     * as provided by the argument.
+     * 
+     * @param max The maximum allowed rating value.
+     */
+    public Rating(int max) {
+        this(max, -1);
+    }
+    
+    /**
+     * Creates a Rating instance with a minimum rating of 0, a maximum rating
+     * as provided by the {@code max} argument, and a current rating as provided
+     * by the {@code rating} argument.
+     * 
+     * @param max The maximum allowed rating value.
+     */
+    public Rating(int max, int rating) {
+        getStyleClass().setAll("rating"); //$NON-NLS-1$
+        
+        setMax(max);
+        setRating(rating == -1 ? (int) Math.floor(max / 2.0) : rating);
+    }
+    
+    
+    
+    /***************************************************************************
+     * 
+     * Overriding public API
+     * 
+     **************************************************************************/
+    
+    /** {@inheritDoc} */
+    @Override protected Skin<?> createDefaultSkin() {
+        return new RatingSkin(this);
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getUserAgentStylesheet() {
+        return getUserAgentStylesheet(Rating.class, "rating.css");
+    }
+    
+    /***************************************************************************
+     * 
+     * Properties
+     * 
+     **************************************************************************/
+    
+    // --- Rating
+    /**
+     * The current rating value.
+     */
+    public final DoubleProperty ratingProperty() {
+        return rating;
+    }
+    private DoubleProperty rating = new SimpleDoubleProperty(this, "rating", 3); //$NON-NLS-1$
+    
+    /**
+     * Sets the current rating value.
+     */
+    public final void setRating(double value) {
+       ratingProperty().set(value);
+    }
+    
+    /**
+     * Returns the current rating value.
+     */
+    public final double getRating() {
+        return rating == null ? 3 : rating.get();
+    }
+
+    
+    // --- Max
+    /**
+     * The maximum-allowed rating value.
+     */
+    public final IntegerProperty maxProperty() {
+        return max;
+    }
+    private IntegerProperty max = new SimpleIntegerProperty(this, "max", 5); //$NON-NLS-1$
+    
+    /**
+     * Sets the maximum-allowed rating value.
+     */
+    public final void setMax(int value) {
+       maxProperty().set(value);
+    }
+
+    /**
+     * Returns the maximum-allowed rating value.
+     */
+    public final int getMax() {
+        return max == null ? 5 : max.get();
+    }
+    
+    
+    // --- Orientation
+    /**
+     * The {@link Orientation} of the {@code Rating} - this can either be 
+     * horizontal or vertical.
+     */
+    public final ObjectProperty<Orientation> orientationProperty() {
+        if (orientation == null) {
+            orientation = new SimpleObjectProperty<>(this, "orientation", Orientation.HORIZONTAL); //$NON-NLS-1$
+        }
+        return orientation;
+    }
+    private ObjectProperty<Orientation> orientation;
+    
+    /**
+     * Sets the {@link Orientation} of the {@code Rating} - this can either be 
+     * horizontal or vertical.
+     */
+    public final void setOrientation(Orientation value) {
+        orientationProperty().set(value);
+    };
+    
+    /**
+     * Returns the {@link Orientation} of the {@code Rating} - this can either 
+     * be horizontal or vertical.
+     */
+    public final Orientation getOrientation() {
+        return orientation == null ? Orientation.HORIZONTAL : orientation.get();
+    }
+
+    
+    // --- partial rating
+    /**
+     * If true this allows for users to set a rating as a floating point value.
+     * In other words, the range of the rating 'stars' can be thought of as a
+     * range between [0, max], and whereever the user clicks will be calculated
+     * as the new rating value. If this is false the more typical approach is used
+     * where the selected 'star' is used as the rating.
+     */
+    public final BooleanProperty partialRatingProperty() {
+        return partialRating;
+    }
+    private BooleanProperty partialRating = new SimpleBooleanProperty(this, "partialRating", false); //$NON-NLS-1$
+    
+    /**
+     * Sets whether {@link #partialRatingProperty() partial rating} support is
+     * enabled or not.
+     */
+    public final void setPartialRating(boolean value) {
+        partialRatingProperty().set(value);
+    }
+    
+    /**
+     * Returns whether {@link #partialRatingProperty() partial rating} support is
+     * enabled or not.
+     */
+    public final boolean isPartialRating() {
+        return partialRating == null ? false : partialRating.get();
+    }
+
+    
+    // --- update on hover
+    /**
+     * If true this allows for the {@link #ratingProperty() rating property} to
+     * be updated simply by the user hovering their mouse over the control. If
+     * false the user is required to click on their preferred rating to register
+     * the new rating with this control.
+     */
+    public final BooleanProperty updateOnHoverProperty() {
+        return updateOnHover;
+    }
+    private BooleanProperty updateOnHover = new SimpleBooleanProperty(this, "updateOnHover", false); //$NON-NLS-1$
+    
+    /**
+     * Sets whether {@link #updateOnHoverProperty() update on hover} support is
+     * enabled or not.
+     */
+    public final void setUpdateOnHover(boolean value) {
+        updateOnHoverProperty().set(value);
+    }
+    
+    /**
+     * Returns whether {@link #updateOnHoverProperty() update on hover} support is
+     * enabled or not.
+     */
+    public final boolean isUpdateOnHover() {
+        return updateOnHover == null ? false : updateOnHover.get();
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/SegmentedButton.java b/controlsfx/src/main/java/org/controlsfx/control/SegmentedButton.java
new file mode 100644
index 0000000..b27f81a
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/SegmentedButton.java
@@ -0,0 +1,241 @@
+/**
+ * Copyright (c) 2013, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control;
+
+import impl.org.controlsfx.skin.SegmentedButtonSkin;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.scene.control.Skin;
+import javafx.scene.control.ToggleButton;
+import javafx.scene.control.ToggleGroup;
+
+/**
+ * The SegmentedButton is a simple control that forces together a group of
+ * {@link ToggleButton} instances such that they appear as one collective button
+ * (with sub-buttons), rather than as individual buttons. This is better 
+ * clarified with a picture:
+ * 
+ * <br>
+ * <center>
+ * <img src="segmentedButton.png" alt="Screenshot of SegmentedButton">
+ * </center>
+ * 
+ * <h3>Code Samples</h3>
+ * 
+ * <p>There is very little API on this control, you essentially create
+ * {@link ToggleButton} instances as per usual (and don't bother putting them
+ * into a {@link ToggleGroup}, as this is done by the SegmentedButton itself), and then 
+ * you place these buttons inside the {@link #getButtons() buttons list}. The 
+ * long-hand way to code this is as follows:
+ * 
+ * <pre>
+ * {@code 
+ * ToggleButton b1 = new ToggleButton("day");
+ * ToggleButton b2 = new ToggleButton("week");
+ * ToggleButton b3 = new ToggleButton("month");
+ * ToggleButton b4 = new ToggleButton("year");
+ *       
+ * SegmentedButton segmentedButton = new SegmentedButton();    
+ * segmentedButton.getButtons().addAll(b1, b2, b3, b4);}</pre>
+ * 
+ * <p>A slightly shorter way of doing this is to pass the ToggleButton instances
+ * into the varargs constructor, as such:
+ * 
+ * <pre>{@code SegmentedButton segmentedButton = new SegmentedButton(b1, b2, b3, b4);}</pre>
+ * 
+ * <h3>Custom ToggleGroup</h3>
+ * <p>It is possible to configure the ToggleGroup, which is used internally.
+ * By setting the ToggleGroup to null, the control will allow multiple selections.
+ * 
+ * <pre>
+ * {@code 
+ * SegmentedButton segmentedButton = new SegmentedButton();
+ * segmentedButton.setToggleGroup(null);
+ * }</pre>
+ *  
+ * <h3>Alternative Styling</h3>
+ * <p>As is visible in the screenshot at the top of this class documentation, 
+ * there are two different styles supported by the SegmentedButton control.
+ * Firstly, there is the default style based on the JavaFX Modena look. The 
+ * alternative style is what is currently referred to as the 'dark' look. To 
+ * enable this functionality, simply do the following:
+ * 
+ * <pre>
+ * {@code
+ * SegmentedButton segmentedButton = new SegmentedButton();   
+ * segmentedButton.getStyleClass().add(SegmentedButton.STYLE_CLASS_DARK);
+ * }</pre>
+ *
+ * <h3>Resizable Range</h3>
+ * <p>By default, the maximum width and height of a SegmentedButton match its
+ * preferred width and height. Thus, the SegmentedButton only fills the area
+ * which is necessary to display the contained buttons. To change this behavior,
+ * the following code can be used:
+ *
+ * <pre>
+ * {@code
+ * SegmentedButton segmentedButton = new SegmentedButton();
+ * segmentedButton.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
+ * }</pre>
+ * 
+ * @see ToggleButton
+ * @see ToggleGroup
+ */
+public class SegmentedButton extends ControlsFXControl {
+    
+    /**************************************************************************
+     * 
+     * Static fields
+     * 
+     *************************************************************************/
+    
+    /**
+     * An alternative styling for the segmented button, with a darker pressed 
+     * color which stands out more than the default modena styling. Refer to
+     * the class documentation for details on how to use (and screenshots), but
+     * in short, simply do the following to get the dark styling:
+     * 
+     * <pre>
+     * {@code
+     * SegmentedButton segmentedButton = new SegmentedButton();   
+     * segmentedButton.getStyleClass().add(SegmentedButton.STYLE_CLASS_DARK);
+     * }</pre>
+     */
+    public static final String STYLE_CLASS_DARK = "dark"; //$NON-NLS-1$
+    
+    
+    
+    /**************************************************************************
+     * 
+     * Private fields
+     * 
+     *************************************************************************/
+    
+    private final ObservableList<ToggleButton> buttons;
+    private final ObjectProperty<ToggleGroup> toggleGroup = new SimpleObjectProperty<>(new ToggleGroup());
+    
+    /**************************************************************************
+     * 
+     * Constructors
+     * 
+     *************************************************************************/
+    
+    /**
+     * Creates a default SegmentedButton instance with no buttons.
+     */
+    public SegmentedButton() {
+        this((ObservableList<ToggleButton>)null);
+    }
+    
+    /**
+     * Creates a default SegmentedButton instance with the provided buttons
+     * inserted into it.
+     * 
+     * @param buttons A varargs array of buttons to add into the SegmentedButton
+     *      instance.
+     */
+    public SegmentedButton(ToggleButton... buttons) {
+        this(buttons == null ? 
+                FXCollections.<ToggleButton>observableArrayList() : 
+                FXCollections.observableArrayList(buttons));
+    }
+    
+    /**
+     * Creates a default SegmentedButton instance with the provided buttons
+     * inserted into it.
+     * 
+     * @param buttons A list of buttons to add into the SegmentedButton instance.
+     */
+    public SegmentedButton(ObservableList<ToggleButton> buttons) {
+        getStyleClass().add("segmented-button"); //$NON-NLS-1$
+        this.buttons = buttons == null ? FXCollections.<ToggleButton>observableArrayList() : buttons;
+        
+        // Fix for Issue #87:
+        // https://bitbucket.org/controlsfx/controlsfx/issue/87/segmentedbutton-keyboard-focus-traversal
+        setFocusTraversable(false);
+    }
+    
+
+    
+    
+    /**************************************************************************
+     * 
+     * Public API
+     * 
+     *************************************************************************/
+    
+    /** {@inheritDoc} */
+    @Override protected Skin<?> createDefaultSkin() {
+        return new SegmentedButtonSkin(this);
+    }
+    
+    /**
+     * Returns the list of buttons that this SegmentedButton will draw together
+     * into one 'grouped button'. It is possible to modify this list to add or
+     * remove {@link ToggleButton} instances, as shown in the javadoc 
+     * documentation for this class.
+     */
+    public final ObservableList<ToggleButton> getButtons() {
+        return buttons;
+    }
+    
+    /**
+     * @return Property of the ToggleGroup used internally 
+     */
+    public ObjectProperty<ToggleGroup> toggleGroupProperty() {
+        return this.toggleGroup;
+    }
+
+    /**
+     * @return ToggleGroup used internally 
+     */
+    public ToggleGroup getToggleGroup() {
+        return this.toggleGroupProperty().getValue();
+    }
+
+    /**
+     * @param toggleGroup ToggleGroup to be used internally 
+     */
+    public void setToggleGroup(final ToggleGroup toggleGroup) {
+        this.toggleGroupProperty().setValue(toggleGroup);
+    }
+    
+    
+    /**************************************************************************
+     * 
+     * CSS
+     * 
+     *************************************************************************/
+    
+    /** {@inheritDoc} */
+    @Override public String getUserAgentStylesheet() {
+        return getUserAgentStylesheet(SegmentedButton.class, "segmentedbutton.css");
+    }
+
+}
\ No newline at end of file
diff --git a/controlsfx/src/main/java/org/controlsfx/control/SnapshotView.java b/controlsfx/src/main/java/org/controlsfx/control/SnapshotView.java
new file mode 100644
index 0000000..6a9f480
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/SnapshotView.java
@@ -0,0 +1,1733 @@
+/**
+ * Copyright (c) 2014, 2015, ControlsFX
+ * All rights reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control;
+
+import static javafx.beans.binding.Bindings.and;
+import static javafx.beans.binding.Bindings.isNotNull;
+import static javafx.beans.binding.Bindings.notEqual;
+import impl.org.controlsfx.skin.SnapshotViewSkin;
+import impl.org.controlsfx.tools.rectangle.Rectangles2D;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.Property;
+import javafx.beans.property.ReadOnlyBooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.MapChangeListener;
+import javafx.collections.ObservableMap;
+import javafx.css.CssMetaData;
+import javafx.css.StyleConverter;
+import javafx.css.Styleable;
+import javafx.css.StyleableDoubleProperty;
+import javafx.css.StyleableObjectProperty;
+import javafx.css.StyleableProperty;
+import javafx.geometry.Bounds;
+import javafx.geometry.Point2D;
+import javafx.geometry.Rectangle2D;
+import javafx.scene.Node;
+import javafx.scene.SnapshotParameters;
+import javafx.scene.control.Control;
+import javafx.scene.control.Skin;
+import javafx.scene.image.WritableImage;
+import javafx.scene.layout.Pane;
+import javafx.scene.paint.Color;
+import javafx.scene.paint.Paint;
+
+/**
+ * A {@code SnapshotView} is a control which allows the user to select an area of a node in the typical manner used by
+ * picture editors and crate snapshots of the selection.
+ * <p>
+ * While holding the left mouse key down, a rectangular selection can be drawn. This selection can be moved, resized in
+ * eight cardinal directions and removed. Additionally, the selection's ratio can be fixed in which case the user's
+ * resizing will be limited such that the ratio is always upheld.
+ * <p>
+ * The area where the selection is possible is either this entire control or limited to the displayed node.
+ * 
+ * <h3>Screenshots</h3>
+ * <center><img src="snapshotView.png" alt="Screenshot of SnapshotView"></center>
+ * 
+ * <h3>Code Samples</h3>
+ * The following snippet creates a new instance with the ControlsFX logo loaded from the web, sets a selected area and
+ * fixes its ratio:
+ * 
+ * <pre>
+ * ImageView controlsFxView = new ImageView(
+ *         "http://cache.fxexperience.com/wp-content/uploads/2013/05/ControlsFX.png");
+ * SnapshotView snapshotView = new SnapshotView(controlsFxView);
+ * snapshotView.setSelection(33, 50, 100, 100);
+ * snapshotView.setFixedSelectionRatio(1); // (this is actually the default value)
+ * snapshotView.setSelectionRatioFixed(true);
+ * </pre>
+ * 
+ * <h3>Functionality Overview</h3>
+ * 
+ * This is just a vague overview. The linked properties provide a more detailed explanation.
+ * 
+ * <h4>Node</h4>
+ * 
+ * The node which this control displays is held by the {@link #nodeProperty() node} property.
+ * 
+ * <h4>Selection</h4>
+ * 
+ * There are several properties which interact to manage and indicate the selection.
+ * 
+ * <h5>State</h5>
+ * <ul>
+ * <li>the selection is held by the {@link #selectionProperty() selection} property
+ * <li>the {@link #hasSelectionProperty() hasSelection} property indicates whether a selection exists
+ * <li>the {@link #selectionActiveProperty() selectionActive} property indicates whether the current selection is active
+ * (it is only displayed if it is); by default this property is updated by this control which is determined by the
+ * {@link #selectionActivityManagedProperty() selectionActivityManaged} property
+ * </ul>
+ * 
+ * <h5>Interaction</h5>
+ * <ul>
+ * <li>if the selection is changing due to the user interacting with the control, this is indicated by the
+ * {@link #selectionChangingProperty() selectionChanging} property
+ * <li>whether the user can select any area of the control or only one above the node is determined by the
+ * {@link #selectionAreaBoundaryProperty() selectionAreaBoundary} property
+ * <li>with the {@link #selectionMouseTransparentProperty() selectionMouseTransparent} property the control can be made
+ * mouse transparent so the user can interact with the displayed node
+ * <li>the selection's ratio of width to height can be fixed with the {@link #selectionRatioFixedProperty()
+ * selectionRatioFixed} and the {@link #fixedSelectionRatioProperty() fixedSelectionRatio} properties
+ * </ul>
+ * 
+ * <h5>Visualization</h5>
+ * <ul>
+ * <li> {@link #selectionAreaFillProperty() selectionAreaFill} property for the selected area's paint
+ * <li> {@link #selectionBorderPaintProperty() selectionBorderPaint} property for the selection border's paint
+ * <li> {@link #selectionBorderWidthProperty() selectionBorderWidth} property for the selection border's width
+ * <li> {@link #unselectedAreaFillProperty() unselectedAreaFill} property for the area outside of the selection
+ * <li> {@link #unselectedAreaBoundaryProperty() unselectedAreaBoundary} property which defined what the unselected area
+ * covers
+ * </ul>
+ */
+public class SnapshotView extends ControlsFXControl {
+
+    /**
+     * The maximal divergence between a selection's ratio and the {@link #fixedSelectionRatioProperty()
+     * fixedselectionRatio} for the selection to still have the correct ratio (see {@link #hasCorrectRatio(Rectangle2D)
+     * hasCorrectRatio}).
+     * <p>
+     * The divergence is expressed relative to the {@code fixedselectionRatio}.
+     */
+    public static final double MAX_SELECTION_RATIO_DIVERGENCE = 1e-6;
+
+    /**
+     * The key of the {@link #getProperties() property} which is used to update {@link #selectionChangingProperty()
+     * selectionChanging}.
+     */
+    public static final String SELECTION_CHANGING_PROPERTY_KEY =
+            SnapshotView.class.getCanonicalName() + ".selection_changing"; //$NON-NLS-1$
+
+    /* ************************************************************************
+     *                                                                         * 
+     * Attributes & Properties                                                 * 
+     *                                                                         * 
+     **************************************************************************/
+
+    // NODE
+
+    /**
+     * @see #nodeProperty()
+     */
+    private final ObjectProperty<Node> node;
+
+    // SELECTION
+
+    /**
+     * @see #selectionProperty()
+     */
+    private final ObjectProperty<Rectangle2D> selection;
+
+    /**
+     * @see #hasSelectionProperty()
+     */
+    private final BooleanProperty hasSelection;
+
+    /**
+     * @see #selectionActiveProperty()
+     */
+    private final BooleanProperty selectionActive;
+
+    /**
+     * @see #selectionChangingProperty()
+     */
+    private final BooleanProperty selectionChanging;
+
+    /**
+     * @see #selectionRatioFixedProperty()
+     */
+    private final BooleanProperty selectionRatioFixed;
+
+    /**
+     * @see #fixedSelectionRatioProperty()
+     */
+    private final DoubleProperty fixedSelectionRatio;
+
+    // META
+
+    /**
+     * @see #selectionAreaBoundaryProperty()
+     */
+    private final ObjectProperty<Boundary> selectionAreaBoundary;
+
+    /**
+     * @see #selectionActivityManagedProperty()
+     */
+    private final BooleanProperty selectionActivityManaged;
+
+    /**
+     * @see #selectionMouseTransparentProperty()
+     */
+    private final BooleanProperty selectionMouseTransparent;
+
+    // VISUALIZATION
+
+    /**
+     * @see #unselectedAreaBoundaryProperty()
+     */
+    private final ObjectProperty<Boundary> unselectedAreaBoundary;
+
+    /**
+     * @see #selectionBorderPaintProperty()
+     */
+    private final ObjectProperty<Paint> selectionBorderPaint;
+
+    /**
+     * @see #selectionBorderWidthProperty()
+     */
+    private final DoubleProperty selectionBorderWidth;
+
+    /**
+     * @see #selectionAreaFillProperty()
+     */
+    private final ObjectProperty<Paint> selectionAreaFill;
+
+    /**
+     * @see #unselectedAreaFillProperty()
+     */
+    private final ObjectProperty<Paint> unselectedAreaFill;
+
+    /* ************************************************************************
+     *                                                                         * 
+     * Construction                                                            * 
+     *                                                                         * 
+     **************************************************************************/
+
+    /**
+     * Creates a new SnapshotView.
+     */
+    public SnapshotView() {
+        getStyleClass().setAll(DEFAULT_STYLE_CLASS);
+
+        // NODE
+        node = new SimpleObjectProperty<>(this, "node"); //$NON-NLS-1$
+
+        // SELECTION
+        selection = new SimpleObjectProperty<Rectangle2D>(this, "selection") { //$NON-NLS-1$
+            @Override
+            public void set(Rectangle2D selection) {
+                if (!isSelectionValid(selection)) {
+                    throw new IllegalArgumentException("The selection \"" + selection + "\" is invalid. " + //$NON-NLS-1$ //$NON-NLS-2$
+                            "Check the comment on 'SnapshotView.selectionProperty()' " + //$NON-NLS-1$
+                            "for all criteria a selection must fulfill."); //$NON-NLS-1$
+                }
+                super.set(selection);
+            }
+        };
+        hasSelection = new SimpleBooleanProperty(this, "hasSelection", false); //$NON-NLS-1$
+        hasSelection.bind(and(isNotNull(selection), notEqual(Rectangle2D.EMPTY, selection)));
+        selectionActive = new SimpleBooleanProperty(this, "selectionActive", false); //$NON-NLS-1$
+        selectionChanging = new SimpleBooleanProperty(this, "selectionChanging", false); //$NON-NLS-1$
+
+        selectionRatioFixed = new SimpleBooleanProperty(this, "selectionRatioFixed", false); //$NON-NLS-1$
+        fixedSelectionRatio = new SimpleDoubleProperty(this, "fixedSelectionRatio", 1) { //$NON-NLS-1$
+            @Override
+            public void set(double newValue) {
+                if (newValue <= 0) {
+                    throw new IllegalArgumentException("The fixed selection ratio must be positive."); //$NON-NLS-1$
+                }
+                super.set(newValue);
+            }
+        };
+
+        // META
+        selectionAreaBoundary = createStylableObjectProperty(
+                this, "selectionAreaBoundary", Boundary.CONTROL, Css.SELECTION_AREA_BOUNDARY); //$NON-NLS-1$
+        selectionActivityManaged = new SimpleBooleanProperty(this, "selectionActivityManaged", true); //$NON-NLS-1$
+        selectionMouseTransparent = new SimpleBooleanProperty(this, "selectionMouseTransparent", false); //$NON-NLS-1$
+
+        // VISUALIZATION
+        unselectedAreaBoundary = createStylableObjectProperty(
+                this, "unselectedAreaBoundary", Boundary.CONTROL, Css.UNSELECTED_AREA_BOUNDARY); //$NON-NLS-1$
+        selectionBorderPaint = createStylableObjectProperty(
+                this, "selectionBorderPaint", Color.WHITESMOKE, Css.SELECTION_BORDER_PAINT); //$NON-NLS-1$
+        selectionBorderWidth = createStylableDoubleProperty(
+                this, "selectionBorderWidth", 2.5, Css.SELECTION_BORDER_WIDTH); //$NON-NLS-1$
+        selectionAreaFill = createStylableObjectProperty(
+                this, "selectionAreaFill", Color.TRANSPARENT, Css.SELECTION_AREA_FILL); //$NON-NLS-1$
+        unselectedAreaFill = createStylableObjectProperty(
+                this, "unselectedAreaFill", new Color(0, 0, 0, 0.5), Css.UNSELECTED_AREA_FILL); //$NON-NLS-1$
+
+        addStateUpdatingListeners();
+        // update selection when resizing
+        new SelectionSizeUpdater().enableResizing();
+    }
+
+    /**
+     * Adds listeners to the properties which update the control's state.
+     */
+    private void addStateUpdatingListeners() {
+        // update the selection activity state when the selection is set
+        selection.addListener((o, oldValue, newValue) -> updateSelectionActivityState());
+
+        // ratio
+        selectionRatioFixed.addListener((o, oldValue, newValue) -> {
+            boolean valueChangedToTrue = !oldValue && newValue;
+            if (valueChangedToTrue) {
+                fixSelectionRatio();
+            }
+        });
+        fixedSelectionRatio.addListener((o, oldValue, newValue) -> {
+            if (isSelectionRatioFixed()) {
+                fixSelectionRatio();
+            }
+        });
+
+        // set selection changing according to the values set in the property map
+        listenToProperty(
+                getProperties(), SELECTION_CHANGING_PROPERTY_KEY, (Boolean value) -> selectionChanging.set(value));
+    }
+
+    /**
+     * Listens to the specified properties. When a pair with the specified key is added, it is processed. If the value
+     * has the correct type, it is given to the specified consumer. Even if the type does not match, it is removed from
+     * the map.
+     * 
+     * @param properties
+     *            the {@link ObservableMap} which contains the properties; typically {@link Control#getProperties()}
+     * @param key
+     *            the key for whose value is listened
+     * @param processValue
+     *            the {@link Consumer} for the new value
+     */
+    private static <T> void listenToProperty(
+            ObservableMap<Object, Object> properties, Object key, Consumer<T> processValue) {
+
+        Objects.requireNonNull(properties, "The argument 'properties' must not be null."); //$NON-NLS-1$
+        Objects.requireNonNull(key, "The argument 'key' must not be null."); //$NON-NLS-1$
+        Objects.requireNonNull(processValue, "The argument 'processValue' must not be null."); //$NON-NLS-1$
+
+        @SuppressWarnings("unchecked")
+        MapChangeListener<Object, Object> listener = change -> {
+            boolean addedForKey =
+                    change.wasAdded() && Objects.equals(key, change.getKey());
+            if (addedForKey) {
+                // give the value to the consumer if it has the correct type
+                try {
+                    // note that this cast does nothing except to calm the compiler
+                    // (hence the warning which had to be suppressed)
+                    T newValue = (T) change.getValueAdded();
+                    // this is where the actual exception is created
+                    processValue.accept(newValue);
+                } catch (ClassCastException e) {
+                    // the value was of the wrong type so it can't be processed by the consumer
+                    // -> do nothing
+                }
+                // remove the value from the properties map
+                properties.remove(key);
+            }
+        };
+
+        properties.addListener(listener);
+    }
+
+    /**
+     * Creates a new SnapshotView using the specified node.
+     * 
+     * @param node
+     *            the node to show after construction
+     */
+    public SnapshotView(Node node) {
+        this();
+        setNode(node);
+    }
+
+    /* ************************************************************************
+     *                                                                         * 
+     * Public Methods                                                          * 
+     *                                                                         * 
+     **************************************************************************/
+
+    /**
+     * Transforms the {@link #selectionProperty() selection} to node coordinates by calling
+     * {@link #transformToNodeCoordinates(Rectangle2D) transformToNodeCoordinates}.
+     * 
+     * @return a {@link Rectangle2D} which expresses the selection in the node's coordinates
+     * @throws IllegalStateException
+     *             if {@link #nodeProperty() node} is {@code null} or {@link #hasSelection() hasSelection} is
+     *             {@code false}
+     * @see #transformToNodeCoordinates(Rectangle2D)
+     */
+    public Rectangle2D transformSelectionToNodeCoordinates() {
+        if (!hasSelection()) {
+            throw new IllegalStateException(
+                    "The selection can not be transformed if it does not exist (check 'hasSelection()')."); //$NON-NLS-1$
+        }
+
+        return transformToNodeCoordinates(getSelection());
+    }
+
+    /**
+     * Transforms the specified area's coordinates to coordinates relative to the node. (The node's coordinate system
+     * has its origin in the upper left corner of the node.)
+     * 
+     * @param area
+     *            the {@link Rectangle2D} which will be transformed (must not be {@code null}); its coordinates will be
+     *            interpreted relative to the control (like the {@link #selectionProperty() selection})
+     * @return a {@link Rectangle2D} with the same width and height as the specified {@code area} but with coordinates
+     *         which are relative to the current {@link #nodeProperty() node}
+     * @throws IllegalStateException
+     *             if {@link #nodeProperty() node} is {@code null}
+     */
+    public Rectangle2D transformToNodeCoordinates(Rectangle2D area) throws IllegalStateException {
+        Objects.requireNonNull(area, "The argument 'area' must not be null."); //$NON-NLS-1$
+        if (getNode() == null) {
+            throw new IllegalStateException(
+                    "The selection can not be transformed if the node is null (check 'getNode()')."); //$NON-NLS-1$
+        }
+
+        // get the offset from the node's bounds
+        Bounds nodeBounds = getNode().getBoundsInParent();
+        double xOffset = nodeBounds.getMinX();
+        double yOffset = nodeBounds.getMinY();
+
+        // the coordinates of the transformed selection
+        double minX = area.getMinX() - xOffset;
+        double minY = area.getMinY() - yOffset;
+
+        return new Rectangle2D(minX, minY, area.getWidth(), area.getHeight());
+    }
+
+    /**
+     * Creates a snapshot of the selected area of the node.
+     * 
+     * @return the {@link WritableImage} that holds the rendered selection
+     * @throws IllegalStateException
+     *             if {@link #nodeProperty() node} is {@code null} or {@link #hasSelection() hasSelection} is
+     *             {@code false}
+     * @see Node#snapshot
+     */
+    public WritableImage createSnapshot() throws IllegalStateException {
+        // make sure the node and the selection exist
+        if (getNode() == null) {
+            throw new IllegalStateException("No snapshot can be created if the node is null (check 'getNode()')."); //$NON-NLS-1$
+        }
+        if (!hasSelection()) {
+            throw new IllegalStateException(
+                    "No snapshot can be created if there is no selection (check 'hasSelection()')."); //$NON-NLS-1$
+        }
+
+        SnapshotParameters parameters = new SnapshotParameters();
+        parameters.setViewport(getSelection());
+        return createSnapshot(parameters);
+    }
+
+    /**
+     * Creates a snapshot of the node with the specified parameters.
+     * 
+     * @param parameters
+     *            the {@link SnapshotParameters} used for the snapshot (must not be {@code null}); the viewport will be
+     *            interpreted relative to this control (like the {@link #selectionProperty() selection})
+     * @return the {@link WritableImage} that holds the rendered viewport
+     * @throws IllegalStateException
+     *             if {@link #nodeProperty() node} is {@code null}
+     * @see Node#snapshot
+     */
+    public WritableImage createSnapshot(SnapshotParameters parameters) throws IllegalStateException {
+        // make sure the node and the snapshot parameters exist
+        Objects.requireNonNull(parameters, "The argument 'parameters' must not be null."); //$NON-NLS-1$
+        if (getNode() == null) {
+            throw new IllegalStateException("No snapshot can be created if the node is null (check 'getNode()')."); //$NON-NLS-1$
+        }
+
+        // take the snapshot
+        return getNode().snapshot(parameters, null);
+    }
+
+    /* ************************************************************************
+     *                                                                         * 
+     * Model State                                                             * 
+     *                                                                         * 
+     **************************************************************************/
+
+    /**
+     * Updates the {@link #selectionActiveProperty() selectionActive} property if the
+     * {@link #selectionActivityManagedProperty() selectionActivityManaged} property indicates that it is managed by
+     * this control.
+     */
+    private void updateSelectionActivityState() {
+        boolean userManaged = !isSelectionActivityManaged();
+        if (userManaged) {
+            return;
+        }
+
+        boolean selectionActive = getSelection() != null && getSelection() != Rectangle2D.EMPTY;
+        setSelectionActive(selectionActive);
+    }
+
+    /**
+     * Resizes the current selection (if it exists) to the {@link #fixedSelectionRatioProperty() fixedSelectionRatio}.
+     */
+    private void fixSelectionRatio() {
+        boolean noSelectionToFix = getNode() == null || !hasSelection();
+        if (noSelectionToFix) {
+            return;
+        }
+
+        Rectangle2D selectionBounds = getSelectionBounds();
+        Rectangle2D resizedSelection = Rectangles2D.fixRatioWithinBounds(
+                getSelection(), getFixedSelectionRatio(), selectionBounds);
+
+        selection.set(resizedSelection);
+    }
+
+    /**
+     * 
+     * @return the bounds of the current selection according to the {@link #selectionAreaBoundaryProperty()
+     *         selectionAreaBoundary}.
+     */
+    private Rectangle2D getSelectionBounds() {
+        Boundary boundary = getSelectionAreaBoundary();
+        switch (boundary) {
+        case CONTROL:
+            return new Rectangle2D(0, 0, getWidth(), getHeight());
+        case NODE:
+            return Rectangles2D.fromBounds(getNode().getBoundsInParent());
+        default:
+            throw new IllegalArgumentException("The boundary '" + boundary + "' is not fully implemented yet."); //$NON-NLS-1$ //$NON-NLS-2$
+        }
+    }
+
+    /**
+     * Checks whether the specified selection is valid. This includes checking whether the selection is in bounds and
+     * has the correct ratio (if the ratio is fixed).
+     * 
+     * @param selection
+     *            the selection to check as a {@link Rectangle2D}
+     * @return {@code true} if the selection is valid; {@code false} otherwise
+     */
+    private boolean isSelectionValid(Rectangle2D selection) {
+        // empty selections are valid
+        boolean emptySelection = selection == null || selection == Rectangle2D.EMPTY;
+        if (emptySelection) {
+            return true;
+        }
+
+        // check values
+        if (!valuesFinite(selection)) {
+            return false;
+        }
+
+        // check bounds
+        if (!inBounds(selection)) {
+            return false;
+        }
+
+        // check ratio
+        if (!hasCorrectRatio(selection)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Indicates whether the specified selection has only finite values (e.g. width and height).
+     * 
+     * @param selection
+     *            the selection as a {@link Rectangle2D}
+     * @return {@code true} if the selection has only finite values.
+     */
+    private static boolean valuesFinite(Rectangle2D selection) {
+        return Double.isFinite(selection.getMinX()) && Double.isFinite(selection.getMinY()) &&
+                Double.isFinite(selection.getWidth()) && Double.isFinite(selection.getHeight());
+    }
+
+    /**
+     * Indicates whether the specified selection is inside the bounds determined by the
+     * {@link #selectionAreaBoundaryProperty() selectionAreaBoundary} property.
+     * 
+     * @param selection
+     *            the non-null and non-empty selection as a {@link Rectangle2D}
+     * @return {@code true} if the selection is fully contained in the bounds; otherwise {@code false}
+     */
+    private boolean inBounds(Rectangle2D selection) {
+        Boundary boundary = getSelectionAreaBoundary();
+        switch (boundary) {
+        case CONTROL:
+            return inBounds(selection, getBoundsInLocal());
+        case NODE:
+            if (getNode() == null) {
+                return false;
+            } else {
+                return inBounds(selection, getNode().getBoundsInParent());
+            }
+        default:
+            throw new IllegalArgumentException("The boundary '" + boundary + "' is not fully implemented yet."); //$NON-NLS-1$ //$NON-NLS-2$
+        }
+    }
+
+    /**
+     * Indicates whether the specified selection is inside the specified bounds.
+     * 
+     * @param selection
+     *            the selection as a {@link Rectangle2D}
+     * @param bounds
+     *            the {@link Bounds} to check the selection against
+     * @return {@code true} if the selection is fully contained in the bounds; otherwise {@code false}
+     */
+    private static boolean inBounds(Rectangle2D selection, Bounds bounds) {
+        return bounds.getMinX() <= selection.getMinX() && bounds.getMinY() <= selection.getMinY() &&
+                selection.getMaxX() <= bounds.getMaxX() && selection.getMaxY() <= bounds.getMaxY();
+    }
+
+    /**
+     * Indicates whether the specified selection has the correct ratio (which depends on whether the ratio is even
+     * {@link #selectionRatioFixedProperty() fixed}).
+     * 
+     * @param selection
+     *            the selection to check as a {@link Rectangle2D}
+     * @return {@code true} if the selection has the correct ratio.
+     */
+    private boolean hasCorrectRatio(Rectangle2D selection) {
+        if (!isSelectionRatioFixed()) {
+            return true;
+        }
+
+        double ratio = selection.getWidth() / selection.getHeight();
+        // compute the divergence relative to the fixed selection ratio
+        double ratioDivergence = Math.abs(1 - ratio / getFixedSelectionRatio());
+        return ratioDivergence <= MAX_SELECTION_RATIO_DIVERGENCE;
+    }
+
+    /* ************************************************************************
+     *                                                                         * 
+     * Style Sheet & Skin Handling                                             * 
+     *                                                                         * 
+     **************************************************************************/
+
+    /**
+     * The name of the style class used in CSS for instances of this class.
+     */
+    private static final String DEFAULT_STYLE_CLASS = "snapshot-view"; //$NON-NLS-1$
+
+    /** {@inheritDoc} */
+    @Override
+    public String getUserAgentStylesheet() {
+        return getUserAgentStylesheet(SnapshotView.class, "snapshot-view.css"); //$NON-NLS-1$
+    }
+
+    /**
+     * Creates a {@link StyleableDoubleProperty} with the specified arguments.
+     * 
+     * @param bean
+     *            the {@link Property#getBean() bean} the created property belongs to
+     * @param name
+     *            the property's {@link Property#getName() name}
+     * @param initialValue
+     *            the property's initial value
+     * @param cssMetaData
+     *            the {@link CssMetaData} for the created property
+     * @return a {@link StyleableDoubleProperty}
+     */
+    private static StyleableDoubleProperty createStylableDoubleProperty(
+            Object bean, String name, double initialValue, CssMetaData<? extends Styleable, Number> cssMetaData) {
+
+        return new StyleableDoubleProperty(initialValue) {
+
+            @Override
+            public Object getBean() {
+                return bean;
+            }
+
+            @Override
+            public String getName() {
+                return name;
+            }
+
+            @Override
+            public CssMetaData<? extends Styleable, Number> getCssMetaData() {
+                return cssMetaData;
+            }
+
+        };
+    }
+
+    /**
+     * Creates a {@link StyleableObjectProperty} with the specified arguments.
+     * 
+     * @param bean
+     *            the {@link Property#getBean() bean} the created property belongs to
+     * @param name
+     *            the property's {@link Property#getName() name}
+     * @param initialValue
+     *            the property's initial value
+     * @param cssMetaData
+     *            the {@link CssMetaData} for the created property
+     * @return a {@link StyleableObjectProperty}
+     */
+    private static <T> StyleableObjectProperty<T> createStylableObjectProperty(
+            Object bean, String name, T initialValue, CssMetaData<? extends Styleable, T> cssMetaData) {
+
+        return new StyleableObjectProperty<T>(initialValue) {
+
+            @Override
+            public Object getBean() {
+                return bean;
+            }
+
+            @Override
+            public String getName() {
+                return name;
+            }
+
+            @Override
+            public CssMetaData<? extends Styleable, T> getCssMetaData() {
+                return cssMetaData;
+            }
+
+        };
+    }
+
+    /**
+     * Creates an instance of {@link CssMetaData} with the specified arguments.
+     * 
+     * @param getProperty
+     *            a function from the {@link Styleable} which owns the styled property to the property styled by the
+     *            returned {@code CssMetaData}
+     * @param cssPropertyName
+     *            the name by which the styled property is referenced in CSS files
+     * @param styleConverter
+     *            the {@link StyleConverter} used to convert the CSS parsed value to a Java object
+     * @return an instance of {@link CssMetaData}
+     */
+    private static <S extends Styleable, T> CssMetaData<S, T> createCssMetaData(
+            Function<S, Property<T>> getProperty, String cssPropertyName, StyleConverter<?, T> styleConverter) {
+
+        return new CssMetaData<S, T>(cssPropertyName, styleConverter) {
+
+            @Override
+            public boolean isSettable(S styleable) {
+                final Property<T> property = getProperty.apply(styleable);
+                return property != null && !property.isBound();
+            }
+
+            @Override
+            @SuppressWarnings("unchecked")
+            public StyleableProperty<T> getStyleableProperty(S styleable) {
+                return (StyleableProperty<T>) getProperty.apply(styleable);
+            }
+        };
+    }
+
+    /**
+     * The class which holds this control's {@link CssMetaData} for the different {@link StyleableProperty
+     * StyleableProperties}.
+     */
+    @SuppressWarnings({ "javadoc", "unchecked" })
+    private static class Css {
+
+        public static final CssMetaData<SnapshotView, Boundary> SELECTION_AREA_BOUNDARY =
+                createCssMetaData(
+                        snapshotView -> snapshotView.selectionAreaBoundary, "-fx-selection-area-boundary", //$NON-NLS-1$
+                        (StyleConverter<?, Boundary>) StyleConverter.getEnumConverter(Boundary.class));
+
+        public static final CssMetaData<SnapshotView, Boundary> UNSELECTED_AREA_BOUNDARY =
+                createCssMetaData(
+                        snapshotView -> snapshotView.unselectedAreaBoundary, "-fx-unselected-area-boundary", //$NON-NLS-1$
+                        (StyleConverter<?, Boundary>) StyleConverter.getEnumConverter(Boundary.class));
+
+        public static final CssMetaData<SnapshotView, Paint> SELECTION_BORDER_PAINT =
+                createCssMetaData(
+                        snapshotView -> snapshotView.selectionBorderPaint, "-fx-selection-border-paint", //$NON-NLS-1$
+                        StyleConverter.getPaintConverter());
+
+        public static final CssMetaData<SnapshotView, Number> SELECTION_BORDER_WIDTH =
+                createCssMetaData(
+                        snapshotView -> snapshotView.selectionBorderWidth, "-fx-selection-border-width", //$NON-NLS-1$
+                        StyleConverter.getSizeConverter());
+
+        public static final CssMetaData<SnapshotView, Paint> SELECTION_AREA_FILL =
+                createCssMetaData(
+                        snapshotView -> snapshotView.selectionAreaFill, "-fx-selection-area-fill", //$NON-NLS-1$
+                        StyleConverter.getPaintConverter());
+
+        public static final CssMetaData<SnapshotView, Paint> UNSELECTED_AREA_FILL =
+                createCssMetaData(
+                        snapshotView -> snapshotView.unselectedAreaFill, "-fx-unselected-area-fill", //$NON-NLS-1$
+                        StyleConverter.getPaintConverter());
+
+        /**
+         * The {@link CssMetaData} associated with this class, which includes the {@code CssMetaData} of its super
+         * classes.
+         */
+        public static final List<CssMetaData<? extends Styleable, ?>> CSS_META_DATA;
+
+        static {
+            final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Control.getClassCssMetaData());
+            styleables.add(SELECTION_AREA_BOUNDARY);
+            styleables.add(UNSELECTED_AREA_BOUNDARY);
+            styleables.add(SELECTION_BORDER_PAINT);
+            styleables.add(SELECTION_BORDER_WIDTH);
+            styleables.add(SELECTION_AREA_FILL);
+            styleables.add(UNSELECTED_AREA_FILL);
+            CSS_META_DATA = Collections.unmodifiableList(styleables);
+        }
+    }
+
+    /**
+     * @return the {@link CssMetaData} associated with this class, which includes the {@code CssMetaData} of its super
+     *         classes
+     */
+    public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
+        return Css.CSS_META_DATA;
+    }
+
+    @Override
+    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
+        return getClassCssMetaData();
+    }
+
+    @Override
+    protected Skin<?> createDefaultSkin() {
+        return new SnapshotViewSkin(this);
+    }
+
+    /* ************************************************************************
+     *                                                                         * 
+     * Property Access                                                         * 
+     *                                                                         * 
+     **************************************************************************/
+
+    // NODE
+
+    /**
+     * The {@link Node} which will be displayed in the center of this control.
+     * <p>
+     * The node's {@link Node#boundsInParentProperty() boundsInParent} show its relative position inside this control.
+     * Since the {@link #selectionProperty() selection} property also uses this control as its reference coordinate
+     * system, the bounds can be used to compute which area of the node is selected.
+     * <p>
+     * If this control or the node behaves strangely when resized, try embedding the original node in a {@link Pane} and
+     * setting the pane here.
+     * 
+     * @return the property holding the displayed node
+     */
+    public final ObjectProperty<Node> nodeProperty() {
+        return node;
+    }
+
+    /**
+     * @return the displayed node
+     * @see #nodeProperty()
+     */
+    public final Node getNode() {
+        return nodeProperty().get();
+    }
+
+    /**
+     * @param node
+     *            the node to display
+     * @see #nodeProperty()
+     */
+    public final void setNode(Node node) {
+        nodeProperty().set(node);
+    }
+
+    // SELECTION
+
+    /**
+     * The current selection as a {@link Rectangle2D}. As such an instance is immutable a new one must be set to chane
+     * the selection.
+     * <p>
+     * The rectangle's coordinates are interpreted relative to this control. The top left corner is the origin (0, 0)
+     * and the lower right corner is ({@link #widthProperty() width}, {@link #heightProperty() height}). It is
+     * guaranteed that the selection always lies within these bounds. If the control is resized, so is the selection. If
+     * a selection which violates these bounds is set, an {@link IllegalArgumentException} is thrown.
+     * <p>
+     * The same is true if the {@link #selectionAreaBoundaryProperty() selectionAreaBoundary} is set to {@code NODE} but
+     * with the stricter condition that the selection must lie within the {@link #nodeProperty() node}'s
+     * {@link Node#boundsInParentProperty() boundsInParent}.
+     * <p>
+     * If the selection ratio is {@link #selectionRatioFixedProperty() fixed}, any new selection must have the
+     * {@link #fixedSelectionRatioProperty() fixedSelectionRatio}. Otherwise, an {@code IllegalArgumentException} is
+     * thrown.
+     * <p>
+     * An {@code IllegalArgumentException} is also thrown if not all of the selection's values (e.g. width and height)
+     * are finite.
+     * <p>
+     * The selection might be {@code null} or {@link Rectangle2D#EMPTY} in which case no selection is displayed and
+     * {@link #hasSelectionProperty() hasSelection} is {@code false}.
+     * 
+     * @return the property holding the current selection
+     * @see #hasSelectionProperty()
+     */
+    public final ObjectProperty<Rectangle2D> selectionProperty() {
+        return selection;
+    }
+
+    /**
+     * @return the current selection
+     * @see #selectionProperty()
+     */
+    public final Rectangle2D getSelection() {
+        return selectionProperty().get();
+    }
+
+    /**
+     * @param selection
+     *            the new selection
+     * @throws IllegalArgumentException
+     *             if the selection is out of the bounds defined by the {@link #selectionAreaBoundaryProperty()
+     *             selectionAreaBoundary} or the selection ratio is {@link #selectionRatioFixedProperty() fixed} and the
+     *             new selection does not have the {@link #fixedSelectionRatioProperty() fixedSelectionRatio}.
+     * @see #selectionProperty()
+     */
+    public final void setSelection(Rectangle2D selection) {
+        selectionProperty().set(selection);
+    }
+
+    /**
+     * Creates a new {@link Rectangle2D} from the specified arguments and sets it as the new
+     * {@link #selectionProperty() selection}. It will have ({@code upperLeftX}, {@code upperLeftY}) as its upper left
+     * point and span {@code width} to the right and {@code height} down.
+     * 
+     * @param upperLeftX
+     *            the x coordinate of the selection's upper left point
+     * @param upperLeftY
+     *            the y coordinate of the selection's upper left point
+     * @param width
+     *            the selection's width
+     * @param height
+     *            the selection's height
+     * @throws IllegalArgumentException
+     *             if the selection is out of the bounds defined by the {@link #selectionAreaBoundaryProperty()
+     *             selectionAreaBoundary} or the selection ratio is {@link #selectionRatioFixedProperty() fixed} and the
+     *             new selection does not have the {@link #fixedSelectionRatioProperty() fixedSelectionRatio}.
+     * @see #selectionProperty()
+     * 
+     */
+    public final void setSelection(double upperLeftX, double upperLeftY, double width, double height) {
+        selectionProperty().set(new Rectangle2D(upperLeftX, upperLeftY, width, height));
+    }
+
+    /**
+     * Indicates whether there currently is a selection. This will be {@code false} if the {@link #selectionProperty()
+     * selection} property holds {@code null} or {@link Rectangle2D#EMPTY} .
+     * 
+     * @return a property indicating whether there currently is a selection
+     */
+    public final ReadOnlyBooleanProperty hasSelectionProperty() {
+        return hasSelection;
+    }
+
+    /**
+     * @return whether there currently is a selection
+     * @see #hasSelectionProperty()
+     */
+    public final boolean hasSelection() {
+        return hasSelectionProperty().get();
+    }
+
+    /**
+     * Indicates whether the selection is currently active. Only an active selection will be displayed by the control.
+     * <p>
+     * See {@link #selectionActivityManagedProperty() selectionActivityManaged} for documentation on how this property
+     * might be changed by this control.
+     * 
+     * @return the property indicating whether the selection is active
+     */
+    public final BooleanProperty selectionActiveProperty() {
+        return selectionActive;
+    }
+
+    /**
+     * @return whether the selection is active
+     * @see #selectionActiveProperty()
+     */
+    public final boolean isSelectionActive() {
+        return selectionActiveProperty().get();
+    }
+
+    /**
+     * @param selectionActive
+     *            the new selection active status
+     * @see #selectionActiveProperty()
+     */
+    public final void setSelectionActive(boolean selectionActive) {
+        selectionActiveProperty().set(selectionActive);
+    }
+
+    /**
+     * Indicates whether the {@link #selectionProperty() selection} is currently changing due to user interaction with
+     * the control. It will be set to {@code true} when changing the selection begins and set to {@code false} when it
+     * ends.
+     * <p>
+     * If a selection is set by the code using this control (e.g. by calling {@link #setSelection(Rectangle2D)
+     * setSelection}) this property does not change its value.
+     * 
+     * @return a property indicating whether the selection is changing by user interaction
+     */
+    public final ReadOnlyBooleanProperty selectionChangingProperty() {
+        return selectionChanging;
+    }
+
+    /**
+     * @return whether the selection is changing by user interaction
+     * @see #selectionChangingProperty()
+     */
+    public final boolean isSelectionChanging() {
+        return selectionChangingProperty().get();
+    }
+
+    /**
+     * Indicates whether the ratio of the {@link #selectionProperty() selection} is fixed.
+     * <p>
+     * By default this property is {@code false} and the user interacting with this control can make arbitrary
+     * selections with any ratio of width to height. If it is {@code true}, the user is limited to making selections
+     * with the ratio defined by the {@link #fixedSelectionRatioProperty() fixedSelectionRatio} property. If the ratio
+     * is fixed and a selection with a different ratio is set, an {@link IllegalArgumentException} is thrown.
+     * <p>
+     * If a selection exists and this property is set to {@code true}, the selection is immediately resized to the
+     * currently set ratio.
+     * 
+     * @defaultValue {@code false}
+     * @return the property indicating whether the selection ratio is fixed
+     */
+    public final BooleanProperty selectionRatioFixedProperty() {
+        return selectionRatioFixed;
+    }
+
+    /**
+     * @return whether the selection ratio is fixed
+     * @see #selectionRatioFixedProperty()
+     */
+    public final boolean isSelectionRatioFixed() {
+        return selectionRatioFixedProperty().get();
+    }
+
+    /**
+     * @param selectionRatioFixed
+     *            whether the selection ratio will be fixed
+     * @see #selectionRatioFixedProperty()
+     */
+    public final void setSelectionRatioFixed(boolean selectionRatioFixed) {
+        selectionRatioFixedProperty().set(selectionRatioFixed);
+    }
+
+    /**
+     * The value to which the selection ratio is fixed. The ratio is defined as {@code width / height} and its value
+     * must be strictly positive.
+     * <p>
+     * If {@link #selectionRatioFixedProperty() selectionRatioFixed} is {@code true}, this ratio will be upheld by all
+     * changes made by user interaction with this control. If the ratio is fixed and a selection is set by code (e.g. by
+     * calling {@link #setSelection(Rectangle2D) setSelection}), this ratio is checked and if violated an
+     * {@link IllegalArgumentException} is thrown.
+     * <p>
+     * If a selection exists and {@code selectionRatioFixed} is set to {@code true}, the selection is immediately
+     * resized to this ratio. Similarly, if a selection exists and its ratio is fixed, setting a new value resizes the
+     * selection to the new ratio.
+     * 
+     * @defaultValue 1.0
+     * @return a property containing the fixed selection ratio
+     */
+    public final DoubleProperty fixedSelectionRatioProperty() {
+        return fixedSelectionRatio;
+    }
+
+    /**
+     * @return the fixedSelectionRatio, which will always be a strictly positive value
+     * @see #fixedSelectionRatioProperty()
+     */
+    public final double getFixedSelectionRatio() {
+        return fixedSelectionRatioProperty().get();
+    }
+
+    /**
+     * @param fixedSelectionRatio
+     *            the fixed selection ratio to set
+     * @throws IllegalArgumentException
+     *             if {@code fixedSelectionRatio} is not strictly positive
+     * @see #fixedSelectionRatioProperty()
+     */
+    public final void setFixedSelectionRatio(double fixedSelectionRatio) {
+        fixedSelectionRatioProperty().set(fixedSelectionRatio);
+    }
+
+    // META
+
+    /**
+     * Indicates which {@link Boundary} is set for the area the user can select.
+     * <p>
+     * By default the user can select any area of the control. If this should be limited to the area over the displayed
+     * node instead, this property can be set to {@link Boundary#NODE NODE}. If the value is changed from
+     * {@code CONTROL} to {@code NODE} a possibly existing selection is resized accordingly.
+     * <p>
+     * If the boundary is set to {@code NODE}, this is also respected when a new {@link #selectionProperty() selection}
+     * is set. This means the condition for the new selection's coordinates is made stricter and setting a selection out
+     * of the node's bounds (instead of only out of the control's bounds) throws an {@link IllegalArgumentException}.
+     * <p>
+     * Note that this does <b>not</b> change the reference coordinate system! The selection's coordinates are still
+     * interpreted relative to the {@link #nodeProperty() node}'s {@link Node#boundsInParentProperty() boundsInParent}.
+     * 
+     * @defaultValue {@link Boundary#CONTROL CONTROL}
+     * @return the property indicating the {@link Boundary} for the area the user can select
+     */
+    public final ObjectProperty<Boundary> selectionAreaBoundaryProperty() {
+        return selectionAreaBoundary;
+    }
+
+    /**
+     * @return the {@link Boundary} for the area the user can select
+     */
+    public final Boundary getSelectionAreaBoundary() {
+        return selectionAreaBoundaryProperty().get();
+    }
+
+    /**
+     * @param selectionAreaBoundary
+     *            the new {@link Boundary} for the area the user can select
+     */
+    public final void setSelectionAreaBoundary(Boundary selectionAreaBoundary) {
+        selectionAreaBoundaryProperty().set(selectionAreaBoundary);
+    }
+
+    /**
+     * Indicates whether the value of the {@link #selectionActiveProperty() selectionActive} property is managed by this
+     * control.
+     * <p>
+     * If this property is set to {@code true} (which is the default) this control will update the
+     * {@code selectionActive} property immediately after a new selection is set: if the new selection is {@code null}
+     * or {@link Rectangle2D#EMPTY}, it will be set to {@code false}; otherwise to {@code true}.
+     * <p>
+     * If this property is {@code false} this control will never change {@code selectionActive}'s value. In this case it
+     * must be managed by the using code but it is possible to unidirectionally bind it to another property without this
+     * control interfering.
+     * 
+     * @defaultValue {@code true}
+     * @return the property indicating whether the value of the {@link #selectionActiveProperty() selectionActive}
+     *         property is managed by this control
+     */
+    public final BooleanProperty selectionActivityManagedProperty() {
+        return selectionActivityManaged;
+    }
+
+    /**
+     * @return whether the selection activity is managed by this control
+     * @see #selectionActivityManagedProperty()
+     */
+    public final boolean isSelectionActivityManaged() {
+        return selectionActivityManagedProperty().get();
+    }
+
+    /**
+     * @param selectionActivityManaged
+     *            whether the selection activity will be managed by this control
+     * @see #selectionActivityManagedProperty()
+     */
+    public final void setSelectionActivityManaged(boolean selectionActivityManaged) {
+        selectionActivityManagedProperty().set(selectionActivityManaged);
+    }
+
+    /**
+     * Indicates whether the overlay which displays the selection is mouse transparent.
+     * <p>
+     * By default all mouse events are captured by this control and used to interact with the selection. If this
+     * property is set to {@code true}, this behavior changes and the user is able to interact with the displayed
+     * {@link #nodeProperty() node}.
+     * 
+     * @defaultValue {@code false}
+     * @return the property indicating whether the selection is mouse transparent
+     */
+    public final BooleanProperty selectionMouseTransparentProperty() {
+        return selectionMouseTransparent;
+    }
+
+    /**
+     * @return whether the selection is mouse transparent
+     * @see #selectionMouseTransparentProperty()
+     */
+    public final boolean isSelectionMouseTransparent() {
+        return selectionMouseTransparentProperty().get();
+    }
+
+    /**
+     * @param selectionMouseTransparent
+     *            whether the selection will be mouse transparent
+     * @see #selectionMouseTransparentProperty()
+     */
+    public final void setSelectionMouseTransparent(boolean selectionMouseTransparent) {
+        selectionMouseTransparentProperty().set(selectionMouseTransparent);
+    }
+
+    // VISUALIZATION
+
+    /**
+     * Indicates which {@link Boundary} is set for the visualization of the unselected area (i.e. the area outside of
+     * the selection rectangle).
+     * <p>
+     * If it is set to {@link Boundary#CONTROL CONTROL} (which is the default), the unselected area covers the whole
+     * control.
+     * <p>
+     * If it is set to {@link Boundary#NODE NODE}, the area only covers the displayed {@link #nodeProperty() node}. In
+     * most cases this only makes sense if the {@link #selectionAreaBoundaryProperty() selectionAreaBoundary} is also
+     * set to {@code NODE}.
+     * 
+     * @defaultValue {@link Boundary#CONTROL}
+     * @return the property defining the {@link Boundary} of the unselected area
+     */
+    public final ObjectProperty<Boundary> unselectedAreaBoundaryProperty() {
+        return unselectedAreaBoundary;
+    }
+
+    /**
+     * @return the {@link Boundary} for the unselected area
+     * @see #unselectedAreaBoundaryProperty()
+     */
+    public final Boundary getUnselectedAreaBoundary() {
+        return unselectedAreaBoundaryProperty().get();
+    }
+
+    /**
+     * @param unselectedAreaBoundary
+     *            the new {@link Boundary} for the unselected area
+     * @see #unselectedAreaBoundaryProperty()
+     */
+    public final void setUnselectedAreaBoundary(Boundary unselectedAreaBoundary) {
+        unselectedAreaBoundaryProperty().set(unselectedAreaBoundary);
+    }
+
+    /**
+     * Determines the visualization of the selection's border.
+     * 
+     * @defaultValue {@link Color#WHITESMOKE}
+     * @return the property holding the {@link Paint} of the selection border
+     * @see #selectionBorderWidthProperty()
+     */
+    public final ObjectProperty<Paint> selectionBorderPaintProperty() {
+        return selectionBorderPaint;
+    }
+
+    /**
+     * @return the {@link Paint} of the selection border
+     * @see #selectionBorderPaintProperty()
+     */
+    public final Paint getSelectionBorderPaint() {
+        return selectionBorderPaintProperty().get();
+    }
+
+    /**
+     * @param selectionBorderPaint
+     *            the new {@link Paint} of the selection border
+     * @see #selectionBorderPaintProperty()
+     */
+    public final void setSelectionBorderPaint(Paint selectionBorderPaint) {
+        selectionBorderPaintProperty().set(selectionBorderPaint);
+    }
+
+    /**
+     * Determines the width of the selection's border. The border is always painted to the outside of the selected area,
+     * i.e. the selected area is never covered by the border.
+     * 
+     * @defaultValue 2.5
+     * @return the property defining the selection border's width
+     * @see #selectionBorderPaintProperty()
+     * @see javafx.scene.shape.Shape#strokeWidthProperty() Shape.strokeWidthProperty()
+     */
+    public final DoubleProperty selectionBorderWidthProperty() {
+        return selectionBorderWidth;
+    }
+
+    /**
+     * @return the selection border width
+     * @see #selectionBorderWidthProperty()
+     */
+    public final double getSelectionBorderWidth() {
+        return selectionBorderWidthProperty().get();
+    }
+
+    /**
+     * @param selectionBorderWidth
+     *            the selection border width to set
+     * @see #selectionBorderWidthProperty()
+     */
+    public final void setSelectionBorderWidth(double selectionBorderWidth) {
+        selectionBorderWidthProperty().set(selectionBorderWidth);
+    }
+
+    /**
+     * Determines the visualization of the selected area.
+     * 
+     * @defaultValue {@link Color#TRANSPARENT}
+     * @return the property holding the {@link Paint} of the selected area
+     */
+    public final ObjectProperty<Paint> selectionAreaFillProperty() {
+        return selectionAreaFill;
+    }
+
+    /**
+     * @return the {@link Paint} of the selected area
+     * @see #selectionAreaFillProperty()
+     */
+    public final Paint getSelectionAreaFill() {
+        return selectionAreaFillProperty().get();
+    }
+
+    /**
+     * @param selectionAreaFill
+     *            the new {@link Paint} of the selected area
+     * @see #selectionAreaFillProperty()
+     */
+    public final void setSelectionAreaFill(Paint selectionAreaFill) {
+        selectionAreaFillProperty().set(selectionAreaFill);
+    }
+
+    /**
+     * Determines the visualization of the area outside of the selection.
+     * 
+     * @defaultValue {@link Color#BLACK black} with {@link Color#getOpacity() opacity} 0.5
+     * @return the property holding the {@link Paint} of the area outside of the selection
+     */
+    public final ObjectProperty<Paint> unselectedAreaFillProperty() {
+        return unselectedAreaFill;
+    }
+
+    /**
+     * @return the {@link Paint} of the area outside of the selection
+     * @see #unselectedAreaFillProperty()
+     */
+    public final Paint getUnselectedAreaFill() {
+        return unselectedAreaFillProperty().get();
+    }
+
+    /**
+     * @param unselectedAreaFill
+     *            the new {@link Paint} of the area outside of the selection
+     * @see #unselectedAreaFillProperty()
+     */
+    public final void setUnselectedAreaFill(Paint unselectedAreaFill) {
+        unselectedAreaFillProperty().set(unselectedAreaFill);
+    }
+
+    /* ************************************************************************
+     *                                                                         *
+     * Inner Classes                                                           *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * The {@link SnapshotView#selectionAreaBoundaryProperty() selectionArea}, in which the user can create a selection,
+     * and the {@link SnapshotView#unselectedAreaBoundaryProperty() unselectedArea}, in which the unselected area is
+     * visualized, are limited to a certain area of the control. This area's boundary is represented by this enum.
+     *
+     */
+    public static enum Boundary {
+
+        /**
+         * The boundary is this control's bound.
+         */
+        CONTROL,
+
+        /**
+         * The boundary is the displayed node's bound.
+         */
+        NODE,
+
+    }
+
+    /**
+     * Updates the size of the {@link SnapshotView#selectionProperty() selection} whenever necessary. This is the case
+     * if the {@link SnapshotView#selectionAreaBoundaryProperty() selectionAreaBoundary} is set to
+     * {@link Boundary#CONTROL CONTROL} and the control is resized or when it is set to {@link Boundary#NODE NODE} and
+     * the node is changed or resized.
+     *
+     */
+    private class SelectionSizeUpdater {
+
+        /*
+         * If the 'selectionAreaBoundary' is set to 'CONTROL', the selection is only updated when the control changes
+         * its width or height. If it is set to 'NODE', the selection is resized whenever the node or its
+         * 'boundsInParent' change.
+         * For both cases methods exist which resize the selection. The listeners which call those methods are only
+         * added to the corresponding properties when the matching boundary is selected.
+         */
+
+        // CONTROL
+
+        /**
+         * Calls {@link #resizeSelectionToNewControlWidth(ObservableValue, Number, Number)
+         * updateSelectionToNewControlWidth} whenever the control's width changes.
+         */
+        private final ChangeListener<Number> resizeSelectionToNewControlWidthListener;
+
+        /**
+         * Calls {@link #resizeSelectionToNewControlHeight(ObservableValue, Number, Number)
+         * updateSelectionToNewControlWidth} whenever the control's height changes.
+         */
+        private final ChangeListener<Number> resizeSelectionToNewControlHeightListener;
+
+        // NODE
+
+        /**
+         * Calls {@link #updateSelectionToNewNode(ObservableValue, Node, Node) updateSelectionToNewNode} whenever a new
+         * {@link SnapshotView#nodeProperty() node} is set.
+         */
+        private final ChangeListener<Node> updateSelectionToNodeListener;
+
+        /**
+         * Calls {@link #resizeSelectionToNewNodeBounds(ObservableValue, Bounds, Bounds) updateSelectionToNewNodeBounds}
+         * whenever the node's {@link Node#boundsInParentProperty() boundsInParent} change.
+         */
+        private final ChangeListener<Bounds> resizeSelectionToNewNodeBoundsListener;
+
+        // CONSTRUCTION
+
+        /**
+         * Creates a new selection size updater.
+         */
+        public SelectionSizeUpdater() {
+            // create listeners which point to methods
+            resizeSelectionToNewControlWidthListener = this::resizeSelectionToNewControlWidth;
+            resizeSelectionToNewControlHeightListener = this::resizeSelectionToNewControlHeight;
+            updateSelectionToNodeListener = this::updateSelectionToNewNode;
+            resizeSelectionToNewNodeBoundsListener = this::resizeSelectionToNewNodeBounds;
+        }
+
+        // ENABLE RESIZING
+
+        /**
+         * Enables resizing of the control.
+         */
+        public void enableResizing() {
+            // only resize if the selection is not null
+            enableResizingForBoundary(getSelectionAreaBoundary());
+            selectionAreaBoundary.addListener((o, oldBoundary, newBoundary) -> enableResizingForBoundary(newBoundary));
+        }
+
+        /**
+         * Enables resizing for the specified boundary.
+         * 
+         * @param boundary
+         *            the {@link Boundary} for which the control will be resized.
+         */
+        private void enableResizingForBoundary(Boundary boundary) {
+            switch (boundary) {
+            case CONTROL:
+                enableResizingForControl();
+                break;
+            case NODE:
+                enableResizingForNode();
+                break;
+            default:
+                throw new IllegalArgumentException("The boundary '" + boundary + "' is not fully implemented yet."); //$NON-NLS-1$ //$NON-NLS-2$
+            }
+        }
+
+        /**
+         * Enables resizing if the {@link SnapshotView#selectionAreaBoundary selectionAreaBoundary} is
+         * {@link Boundary#CONTROL CONTROL}.
+         */
+        private void enableResizingForControl() {
+            // remove listeners for node and its bounds
+            node.removeListener(updateSelectionToNodeListener);
+            if (getNode() != null) {
+                getNode().boundsInParentProperty().removeListener(resizeSelectionToNewNodeBoundsListener);
+            }
+
+            // add listener for the control's size
+            widthProperty().addListener(resizeSelectionToNewControlWidthListener);
+            heightProperty().addListener(resizeSelectionToNewControlHeightListener);
+
+            resizeSelectionFromNodeToControl();
+        }
+
+        /**
+         * Enables resizing if the {@link SnapshotView#selectionAreaBoundary selectionAreaBoundary} is
+         * {@link Boundary#NODE NODE}.
+         */
+        private void enableResizingForNode() {
+            // remove listeners for the control's size
+            widthProperty().removeListener(resizeSelectionToNewControlWidthListener);
+            heightProperty().removeListener(resizeSelectionToNewControlHeightListener);
+
+            // add listener for the node's bounds and for new nodes
+            if (getNode() != null) {
+                getNode().boundsInParentProperty().addListener(resizeSelectionToNewNodeBoundsListener);
+            }
+            node.addListener(updateSelectionToNodeListener);
+
+            resizeSelectionFromControlToNode();
+        }
+
+        // RESIZE TO CONTROL
+
+        /**
+         * Resizes the current {@link SnapshotView#selectionProperty() selection} from the node's to the control's
+         * bounds.
+         */
+        private void resizeSelectionFromNodeToControl() {
+            if (getNode() == null) {
+                setSelection(null);
+            } else {
+                // transform the selection from the control's to the node's bounds
+                Rectangle2D controlBounds = new Rectangle2D(0, 0, getWidth(), getHeight());
+                Rectangle2D nodeBounds = Rectangles2D.fromBounds(getNode().getBoundsInParent());
+                resizeSelectionToNewBounds(nodeBounds, controlBounds);
+            }
+        }
+
+        /**
+         * Resizes the current {@link SnapshotView#selectionProperty() selection} from the control's specified old width
+         * to its specified new width.
+         * <p>
+         * Designed to be used as a lambda method reference.
+         * 
+         * @param o
+         *            the {@link ObservableValue} which changed its value
+         * @param oldWidth
+         *            the control's old width
+         * @param newWidth
+         *            the control's new width
+         */
+        private void resizeSelectionToNewControlWidth(
+                @SuppressWarnings("unused") ObservableValue<? extends Number> o, Number oldWidth, Number newWidth) {
+
+            Rectangle2D oldBounds = new Rectangle2D(0, 0, oldWidth.doubleValue(), getHeight());
+            Rectangle2D newBounds = new Rectangle2D(0, 0, newWidth.doubleValue(), getHeight());
+            resizeSelectionToNewBounds(oldBounds, newBounds);
+        }
+
+        /**
+         * Resizes the current {@link SnapshotView#selectionProperty() selection} from the control's specified old
+         * height to its specified new height.
+         * <p>
+         * Designed to be used as a lambda method reference.
+         * 
+         * @param o
+         *            the {@link ObservableValue} which changed its value
+         * @param oldHeight
+         *            the control's old height
+         * @param newHeight
+         *            the control's new height
+         */
+        private void resizeSelectionToNewControlHeight(
+                @SuppressWarnings("unused") ObservableValue<? extends Number> o, Number oldHeight, Number newHeight) {
+
+            Rectangle2D oldBounds = new Rectangle2D(0, 0, getWidth(), oldHeight.doubleValue());
+            Rectangle2D newBounds = new Rectangle2D(0, 0, getWidth(), newHeight.doubleValue());
+            resizeSelectionToNewBounds(oldBounds, newBounds);
+        }
+
+        // RESIZE TO NODE
+
+        /**
+         * Resizes the current {@link SnapshotView#selectionProperty() selection} from the control's to the node's
+         * bounds
+         */
+        private void resizeSelectionFromControlToNode() {
+            if (getNode() == null) {
+                setSelection(null);
+            } else {
+                // transform the selection from the control's to the node's bounds
+                Rectangle2D controlBounds = new Rectangle2D(0, 0, getWidth(), getHeight());
+                Rectangle2D nodeBounds = Rectangles2D.fromBounds(getNode().getBoundsInParent());
+                resizeSelectionToNewBounds(controlBounds, nodeBounds);
+            }
+        }
+
+        /**
+         * Moves the {@link #resizeSelectionToNewNodeBoundsListener} from the specified old to the specified new node's
+         * {@link Node#boundsInParentProperty() boundsInParent} property and resizes the current
+         * {@link SnapshotView#selectionProperty() selection} from the old to the new node's bounds.
+         * <p>
+         * Designed to be used as a lambda method reference.
+         * 
+         * @param o
+         *            the {@link ObservableValue} which changed its value
+         * @param oldNode
+         *            the old node
+         * @param newNode
+         *            the new node
+         */
+        private void updateSelectionToNewNode(
+                @SuppressWarnings("unused") ObservableValue<? extends Node> o, Node oldNode, Node newNode) {
+
+            // move the bounds listener from the old to the new node
+            if (oldNode != null) {
+                oldNode.boundsInParentProperty().removeListener(resizeSelectionToNewNodeBoundsListener);
+            }
+            if (newNode != null) {
+                newNode.boundsInParentProperty().addListener(resizeSelectionToNewNodeBoundsListener);
+            }
+
+            // update selection
+            if (oldNode == null || newNode == null) {
+                // if one of the nodes is null, set no selection
+                setSelection(null);
+            } else {
+                // transform the current selection
+                resizeSelectionToNewNodeBounds(null, oldNode.getBoundsInParent(), newNode.getBoundsInParent());
+            }
+        }
+
+        /**
+         * Resizes the current {@link SnapshotView#selectionProperty() selection} from the specified old to the
+         * specified new bounds of the {@link SnapshotView#nodeProperty() node}.
+         * 
+         * @param o
+         *            the {@link ObservableValue} which changed its value
+         * @param oldBounds
+         *            the node's old bounds
+         * @param newBounds
+         *            the node's new bounds
+         */
+        private void resizeSelectionToNewNodeBounds(
+                @SuppressWarnings("unused") ObservableValue<? extends Bounds> o, Bounds oldBounds, Bounds newBounds) {
+
+            resizeSelectionToNewBounds(Rectangles2D.fromBounds(oldBounds), Rectangles2D.fromBounds(newBounds));
+        }
+
+        // GENERAL RESIZING
+
+        /**
+         * If this control {@link SnapshotView#hasSelection() has a selection} it is resized from the specified old to
+         * the specified new bounds.
+         * 
+         * @param oldBounds
+         *            the {@link SnapshotView#selectionProperty() selection}'s old bounds as a {@link Rectangle2D}
+         * @param newBounds
+         *            the {@link SnapshotView#selectionProperty() selection}'s new bounds as a {@link Rectangle2D}
+         */
+        private void resizeSelectionToNewBounds(Rectangle2D oldBounds, Rectangle2D newBounds) {
+            if (!hasSelection()) {
+                return;
+            }
+
+            Rectangle2D newSelection = transformSelectionToNewBounds(getSelection(), oldBounds, newBounds);
+            if (isSelectionValid(newSelection)) {
+                setSelection(newSelection);
+            } else {
+                setSelection(null);
+            }
+        }
+
+        /**
+         * Returns a new selection which is a transformation of the specified old selection. The transformation is such
+         * that the new selection's "relative position" in the specified new bounds is the same as the old selection's
+         * relative position in the specified old bounds.
+         * <p>
+         * Here, "relative position" is a representation of the selection where the coordinates of its upper left point
+         * and its width and height are expressed in a percentage of its bounds. Those percentages are the same for
+         * "old selection in old bounds" and "returned selection in new bounds"
+         * 
+         * @param oldSelection
+         *            the selection to be transformed as a {@link Rectangle2D}
+         * @param oldBounds
+         *            the {@code oldSelection}'s old bounds as a {@link Rectangle2D}
+         * @param newBounds
+         *            the {@code oldSelection}'s new bounds as a {@link Rectangle2D}
+         * @return s {@link Rectangle2D} which is the transformation of the old selection to the new bounds
+         */
+        private Rectangle2D transformSelectionToNewBounds(
+                Rectangle2D oldSelection, Rectangle2D oldBounds, Rectangle2D newBounds) {
+
+            Point2D newSelectionCenter = computeNewSelectionCenter(oldSelection, oldBounds, newBounds);
+
+            double widthRatio = newBounds.getWidth() / oldBounds.getWidth();
+            double heightRatio = newBounds.getHeight() / oldBounds.getHeight();
+
+            if (isSelectionRatioFixed()) {
+                double newArea = (oldSelection.getWidth() * widthRatio) * (oldSelection.getHeight() * heightRatio);
+                double ratio = getFixedSelectionRatio();
+                return Rectangles2D.forCenterAndAreaAndRatioWithinBounds(newSelectionCenter, newArea, ratio, newBounds);
+            } else {
+                double newWidth = oldSelection.getWidth() * widthRatio;
+                double newHeight = oldSelection.getHeight() * heightRatio;
+                return Rectangles2D.forCenterAndSize(newSelectionCenter, newWidth, newHeight);
+            }
+        }
+
+        /**
+         * Computes a point with the same relative position in the specified new bounds as the specified old selection's
+         * center point in the specified old bounds. (See
+         * {@link #transformSelectionToNewBounds(Rectangle2D, Rectangle2D, Rectangle2D) transformSelectionToNewBounds}
+         * for a definition of "relative position").
+         * 
+         * @param oldSelection
+         *            the selection whose center point is the base for the returned center point as a
+         *            {@link Rectangle2D}
+         * @param oldBounds
+         *            the bounds of the old selection as a {@link Rectangle2D}
+         * @param newBounds
+         *            the bounds for the new selection as a {@link Rectangle2D}
+         * @return a {@link Point2D} with the same relative position in the new bounds as the old selection's center
+         *         point in the old bounds
+         */
+        private Point2D computeNewSelectionCenter(Rectangle2D oldSelection, Rectangle2D oldBounds, Rectangle2D newBounds) {
+
+            Point2D oldSelectionCenter = Rectangles2D.getCenterPoint(oldSelection);
+            Point2D oldBoundsCenter = Rectangles2D.getCenterPoint(oldBounds);
+            Point2D oldSelectionCenterOffset = oldSelectionCenter.subtract(oldBoundsCenter);
+
+            double widthRatio = newBounds.getWidth() / oldBounds.getWidth();
+            double heightRatio = newBounds.getHeight() / oldBounds.getHeight();
+
+            Point2D newSelectionCenterOffset = new Point2D(
+                    oldSelectionCenterOffset.getX() * widthRatio, oldSelectionCenterOffset.getY() * heightRatio);
+            Point2D newBoundsCenter = Rectangles2D.getCenterPoint(newBounds);
+            Point2D newSelectionCenter = newBoundsCenter.add(newSelectionCenterOffset);
+
+            return newSelectionCenter;
+        }
+
+    }
+
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/StatusBar.java b/controlsfx/src/main/java/org/controlsfx/control/StatusBar.java
new file mode 100644
index 0000000..40273de
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/StatusBar.java
@@ -0,0 +1,211 @@
+/**
+ * Copyright (c) 2014, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control;
+
+import static impl.org.controlsfx.i18n.Localization.asKey;
+import static impl.org.controlsfx.i18n.Localization.localize;
+
+import impl.org.controlsfx.skin.StatusBarSkin;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.scene.Node;
+import javafx.scene.control.ProgressBar;
+import javafx.scene.control.Skin;
+
+/**
+ * The StatusBar control is normally placed at the bottom of a window. It is
+ * used to display various types of application status information. This can be
+ * a text message, the progress of a task, or any other kind of status (e.g. red
+ * / green / yellow lights). By default the status bar contains a label for
+ * displaying plain text and a progress bar (see {@link ProgressBar}) for long
+ * running tasks. Additional controls / nodes can be placed on the left and
+ * right sides (see {@link #getLeftItems()} and {@link #getRightItems()}).
+ * 
+ * <h3>Screenshots</h3> 
+ * The picture below shows the default appearance of the StatusBar control: 
+ * <center><img src="statusbar.png" alt="Screenshot of StatusBar"></center> 
+ * 
+ * <br>
+ * The following picture shows the status bar reporting progress of a task:
+ * <center><img src="statusbar-progress.png" alt="Screenshot of StatusBar 
+ * reporting progress of a task"></center> 
+ * 
+ * <br>
+ * The last picture shows the status bar reporting progress, along with a couple 
+ * of extra items added to the left and right areas of the bar: 
+ * <center><img src="statusbar-items.png" alt="Screenshot of StatusBar
+ * reporting progress, along with a couple of extra items"></center>
+ * 
+ * <h3>Code Sample</h3>
+ * 
+ * <pre>
+ * StatusBar statusBar = new StatusBar();
+ * statusBar.getLeftItems().add(new Button("Info"));
+ * statusBar.setProgress(.5);
+ * </pre>
+ */
+public class StatusBar extends ControlsFXControl {
+
+    /**
+     * Constructs a new status bar control.
+     */
+    public StatusBar() {
+        getStyleClass().add("status-bar"); //$NON-NLS-1$
+    }
+
+    @Override
+    protected Skin<?> createDefaultSkin() {
+        return new StatusBarSkin(this);
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getUserAgentStylesheet() {
+        return getUserAgentStylesheet(StatusBar.class, "statusbar.css");
+    }
+    
+    private final StringProperty text = new SimpleStringProperty(this, "text", //$NON-NLS-1$
+            localize(asKey("statusbar.ok"))); //$NON-NLS-1$
+
+    /**
+     * The property used for storing the text message shown by the status bar.
+     * 
+     * @return the text message property
+     */
+    public final StringProperty textProperty() {
+        return text;
+    }
+
+    /**
+     * Sets the value of the {@link #textProperty()}.
+     * 
+     * @param text the text shown by the label control inside the status bar
+     */
+    public final void setText(String text) {
+        textProperty().set(text);
+    }
+
+    /**
+     * Returns the value of the {@link #textProperty()}. 
+     * 
+     * @return the text currently shown by the status bar
+     */
+    public final String getText() {
+        return textProperty().get();
+    }
+
+    private final ObjectProperty<Node> graphic = new SimpleObjectProperty<>(
+            this, "graphic"); //$NON-NLS-1$
+
+    /**
+     * The property used to store a graphic node that can be displayed by the 
+     * status label inside the status bar control.
+     * 
+     * @return the property used for storing a graphic node
+     */
+    public final ObjectProperty<Node> graphicProperty() {
+        return graphic;
+    }
+
+    /**
+     * Returns the value of the {@link #graphicProperty()}.
+     * 
+     * @return the graphic node shown by the label inside the status bar
+     */
+    public final Node getGraphic() {
+        return graphicProperty().get();
+    }
+
+    /**
+     * Sets the value of {@link #graphicProperty()}.
+     * 
+     * @param node the graphic node shown by the label inside the status bar
+     */
+    public final void setGraphic(Node node) {
+        graphicProperty().set(node);
+    }
+
+    private final ObservableList<Node> leftItems = FXCollections
+            .observableArrayList();
+
+    /**
+     * Returns the list of items / nodes that will be shown to the left of the status label.
+     * 
+     * @return the items on the left-hand side of the status bar
+     */
+    public final ObservableList<Node> getLeftItems() {
+        return leftItems;
+    }
+
+    private final ObservableList<Node> rightItems = FXCollections
+            .observableArrayList();
+
+    /**
+     * Returns the list of items / nodes that will be shown to the right of the status label.
+     * 
+     * @return the items on the left-hand side of the status bar
+     */
+    public final ObservableList<Node> getRightItems() {
+        return rightItems;
+    }
+
+    private final DoubleProperty progress = new SimpleDoubleProperty(this,
+            "progress"); //$NON-NLS-1$
+
+    /**
+     * The property used to store the progress, a value between 0 and 1. A negative
+     * value causes the progress bar to show an indeterminate state.
+     * 
+     * @return the property used to store the progress of a task
+     */
+    public final DoubleProperty progressProperty() {
+        return progress;
+    }
+
+    /**
+     * Sets the value of the {@link #progressProperty()}.
+     * 
+     * @param progress the new progress value
+     */
+    public final void setProgress(double progress) {
+        progressProperty().set(progress);
+    }
+
+    /**
+     * Returns the value of {@link #progressProperty()}.
+     * 
+     * @return the current progress value
+     */
+    public final double getProgress() {
+        return progressProperty().get();
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/TaskProgressView.java b/controlsfx/src/main/java/org/controlsfx/control/TaskProgressView.java
new file mode 100644
index 0000000..839d4ce
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/TaskProgressView.java
@@ -0,0 +1,158 @@
+/**
+ * Copyright (c) 2014, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control;
+
+import impl.org.controlsfx.skin.TaskProgressViewSkin;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import javafx.concurrent.Task;
+import javafx.concurrent.WorkerStateEvent;
+import javafx.event.EventHandler;
+import javafx.scene.Node;
+import javafx.scene.control.Skin;
+import javafx.util.Callback;
+
+/**
+ * The task progress view is used to visualize the progress of long running
+ * tasks. These tasks are created via the {@link Task} class. This view
+ * manages a list of such tasks and displays each one of them with their
+ * name, progress, and update messages.<p>
+ * An optional graphic factory can be set to place a graphic in each row.
+ * This allows the user to more easily distinguish between different types
+ * of tasks.
+ *
+ * <h3>Screenshots</h3>
+ * The picture below shows the default appearance of the task progress view
+ * control:
+ * <center><img src="task-monitor.png" alt="Screenshot of TaskProgressView"></center>
+ *
+ * <h3>Code Sample</h3>
+ * <pre>
+ * TaskProgressView<MyTask> view = new TaskProgressView<>();
+ * view.setGraphicFactory(task -> return new ImageView("db-access.png"));
+ * view.getTasks().add(new MyTask());
+ * </pre>
+ */
+public class TaskProgressView<T extends Task<?>> extends ControlsFXControl {
+
+    /**
+     * Constructs a new task progress view.
+     */
+    public TaskProgressView() {
+        getStyleClass().add("task-progress-view");
+
+        EventHandler<WorkerStateEvent> taskHandler = evt -> {
+            if (evt.getEventType().equals(
+                    WorkerStateEvent.WORKER_STATE_SUCCEEDED)
+                    || evt.getEventType().equals(
+                            WorkerStateEvent.WORKER_STATE_CANCELLED)
+                    || evt.getEventType().equals(
+                            WorkerStateEvent.WORKER_STATE_FAILED)) {
+                getTasks().remove(evt.getSource());
+            }
+        };
+
+        getTasks().addListener(new ListChangeListener<Task<?>>() {
+            @Override
+            public void onChanged(Change<? extends Task<?>> c) {
+                while (c.next()) {
+                    if (c.wasAdded()) {
+                        for (Task<?> task : c.getAddedSubList()) {
+                            task.addEventHandler(WorkerStateEvent.ANY,
+                                    taskHandler);
+                        }
+                    } else if (c.wasRemoved()) {
+                        for (Task<?> task : c.getRemoved()) {
+                            task.removeEventHandler(WorkerStateEvent.ANY,
+                                    taskHandler);
+                        }
+                    }
+                }
+            }
+        });
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getUserAgentStylesheet() {
+        return getUserAgentStylesheet(TaskProgressView.class, "taskprogressview.css");
+    }
+    
+    @Override
+    protected Skin<?> createDefaultSkin() {
+        return new TaskProgressViewSkin<>(this);
+    }
+
+    private final ObservableList<T> tasks = FXCollections
+            .observableArrayList();
+
+    /**
+     * Returns the list of tasks currently monitored by this view.
+     *
+     * @return the monitored tasks
+     */
+    public final ObservableList<T> getTasks() {
+        return tasks;
+    }
+
+    private ObjectProperty<Callback<T, Node>> graphicFactory;
+
+    /**
+     * Returns the property used to store an optional callback for creating
+     * custom graphics for each task.
+     *
+     * @return the graphic factory property
+     */
+    public final ObjectProperty<Callback<T, Node>> graphicFactoryProperty() {
+        if (graphicFactory == null) {
+            graphicFactory = new SimpleObjectProperty<Callback<T, Node>>(
+                    this, "graphicFactory");
+        }
+
+        return graphicFactory;
+    }
+
+    /**
+     * Returns the value of {@link #graphicFactoryProperty()}.
+     *
+     * @return the optional graphic factory
+     */
+    public final Callback<T, Node> getGraphicFactory() {
+        return graphicFactory == null ? null : graphicFactory.get();
+    }
+
+    /**
+     * Sets the value of {@link #graphicFactoryProperty()}.
+     *
+     * @param factory an optional graphic factory
+     */
+    public final void setGraphicFactory(Callback<T, Node> factory) {
+        graphicFactoryProperty().set(factory);
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/ToggleSwitch.java b/controlsfx/src/main/java/org/controlsfx/control/ToggleSwitch.java
new file mode 100644
index 0000000..d380ead
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/ToggleSwitch.java
@@ -0,0 +1,171 @@
+/**
+ * Copyright (c) 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.controlsfx.control;
+
+import impl.org.controlsfx.skin.ToggleSwitchSkin;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.BooleanPropertyBase;
+import javafx.css.PseudoClass;
+import javafx.event.ActionEvent;
+import javafx.scene.control.Labeled;
+import javafx.scene.control.Skin;
+
+/**
+ * Much like a Toggle Button this control allows the user to toggle between one of two states. It has been popularized
+ * in touch based devices where its usage is particularly useful because unlike a checkbox the finger touch of a user
+ * doesn't obscure the control.
+ *
+ * <p> Shown below is a screenshot of the ToggleSwitch control in its on and off state:
+ * <br>
+ * <center>
+ * <img src="ToggleSwitch.png" alt="Screenshot of ToggleSwitch">
+ * </center>
+ */
+public class ToggleSwitch extends Labeled
+{
+
+    /***************************************************************************
+     *                                                                         *
+     * Constructors                                                            *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * Creates a toggle switch with empty string for its label.
+     */
+    public ToggleSwitch() {
+        initialize();
+    }
+
+    /**
+     * Creates a toggle switch with the specified label.
+     *
+     * @param text The label string of the control.
+     */
+    public ToggleSwitch(String text) {
+        super(text);
+        initialize();
+    }
+
+    private void initialize() {
+        getStyleClass().setAll(DEFAULT_STYLE_CLASS);
+    }
+
+    /***************************************************************************
+     *                                                                         *
+     * Properties                                                              *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * Indicates whether this ToggleSwitch is selected.
+     */
+    private BooleanProperty selected;
+
+    /**
+     * Sets the selected value of this Toggle Switch
+     */
+    public final void setSelected(boolean value) {
+        selectedProperty().set(value);
+    }
+
+    /**
+     * Returns whether this Toggle Switch is selected
+     */
+    public final boolean isSelected() {
+        return selected == null ? false : selected.get();
+    }
+
+    /**
+     * Returns the selected property
+     */
+    public final BooleanProperty selectedProperty() {
+        if (selected == null) {
+            selected = new BooleanPropertyBase() {
+                @Override protected void invalidated() {
+                    final Boolean v = get();
+                    pseudoClassStateChanged(PSEUDO_CLASS_SELECTED, v);
+//                    accSendNotification(Attribute.SELECTED);
+                }
+
+                @Override
+                public Object getBean() {
+                    return ToggleSwitch.this;
+                }
+
+                @Override
+                public String getName() {
+                    return "selected";
+                }
+            };
+        }
+        return selected;
+    }
+
+
+    /***************************************************************************
+     *                                                                         *
+     * Methods                                                                 *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * Toggles the state of the {@code ToggleSwitch}. The {@code ToggleSwitch} will cycle through
+     * the selected and unselected states.
+     */
+    public void fire() {
+        if (!isDisabled()) {
+            setSelected(!isSelected());
+            fireEvent(new ActionEvent());
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override protected Skin<?> createDefaultSkin() {
+        return new ToggleSwitchSkin(this);
+    }
+
+
+    /***************************************************************************
+     *                                                                         *
+     * Stylesheet Handling                                                     *
+     *                                                                         *
+     **************************************************************************/
+
+    private static final String DEFAULT_STYLE_CLASS = "toggle-switch";
+
+    private static final PseudoClass PSEUDO_CLASS_SELECTED =
+            PseudoClass.getPseudoClass("selected");
+
+    /** {@inheritDoc} */
+    @Override
+    public String getUserAgentStylesheet() {
+        return ToggleSwitch.class.getResource("toggleswitch.css").toExternalForm();
+    }
+
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/action/Action.java b/controlsfx/src/main/java/org/controlsfx/control/action/Action.java
new file mode 100644
index 0000000..04d8439
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/action/Action.java
@@ -0,0 +1,430 @@
+/**
+ * Copyright (c) 2013, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.action;
+
+import impl.org.controlsfx.i18n.Localization;
+import impl.org.controlsfx.i18n.SimpleLocalizedStringProperty;
+
+import java.util.function.Consumer;
+
+import javafx.beans.NamedArg;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.collections.ObservableMap;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.scene.Node;
+import javafx.scene.control.Button;
+import javafx.scene.control.Menu;
+import javafx.scene.control.MenuItem;
+import javafx.scene.input.KeyCombination;
+
+/**
+ * A base class for Action API. 
+ * 
+ * <h3>What is an Action?</h3>
+ * An action in JavaFX can be used to separate functionality and state from a 
+ * control. For example, if you have two or more controls that perform the same 
+ * function (e.g. one in a {@link Menu} and another on a toolbar), consider 
+ * using an Action object to implement the function. An Action object provides 
+ * centralized handling of the state of action-event-firing components such as 
+ * buttons, menu items, etc. The state that an action can handle includes text, 
+ * graphic, long text (i.e. tooltip text), and disabled.
+ */
+public class Action implements EventHandler<ActionEvent> {
+    
+    /**************************************************************************
+     * 
+     * Private fields
+     * 
+     **************************************************************************/
+    
+    private boolean locked = false;
+    
+    private Consumer<ActionEvent> eventHandler;
+    
+    
+    /**************************************************************************
+     * 
+     * Constructors
+     * 
+     **************************************************************************/
+    
+    public Action(@NamedArg("text") String text) {
+        this(text, null);
+    }
+    
+    public Action(Consumer<ActionEvent> eventHandler) {
+        this("", eventHandler); //$NON-NLS-1$
+    }
+   
+    /**
+     * Creates a new AbstractAction instance with the given String set as the 
+     * {@link #textProperty() text} value, as well as the {@code Consumer<ActionEvent>}
+     * set to be called when the action event is fired.
+     *  
+     * @param text The string to display in the text property of controls such
+     *      as {@link Button#textProperty() Button}.
+     * @param eventHandler This will be called when the ActionEvent is fired.
+     */
+    public Action(@NamedArg("text") String text, Consumer<ActionEvent> eventHandler) {
+        setText(text);
+        setEventHandler(eventHandler);
+        getStyleClass().add( "action" ); // this class will be added to all bound controls
+    }
+    
+    protected void lock() {
+    	locked = true;
+    }
+    
+    
+    /**************************************************************************
+     * 
+     * Properties
+     * 
+     **************************************************************************/
+    
+    // --- style
+    /**
+     * A string representation of the CSS style associated with this
+     * Action instance and passed to related UI controls. 
+     * This is analogous to the "style" attribute of an
+     * HTML element. Note that, like the HTML style attribute, this
+     * variable contains style properties and values and not the
+     * selector portion of a style rule.
+     * <p>
+     * Parsing this style might not be supported on some limited
+     * platforms. It is recommended to use a standalone CSS file instead.
+     */
+    private StringProperty style;
+    public final void setStyle(String value) { styleProperty().set(value); }
+    public final String getStyle() { return style == null ? "" : style.get(); } //$NON-NLS-1$
+    public final StringProperty styleProperty() {
+        if (style == null) {
+            style = new SimpleStringProperty(this, "style") { //$NON-NLS-1$
+            	@Override
+            	public void set(String style) {
+            		if (locked) throw new UnsupportedOperationException("The action is immutable, property change support is disabled."); //$NON-NLS-1$
+            		super.set(style);
+            	}
+            };
+        }
+        return style;
+    }
+
+
+    // --- Style class
+    private final ObservableList<String> styleClass = FXCollections.observableArrayList();
+    /**
+     * A list of String identifiers which can be used to logically group
+     * Nodes, specifically for an external style engine. This variable is
+     * analogous to the "class" attribute on an HTML element and, as such,
+     * each element of the list is a style class to which this Node belongs.
+     *
+     * @see <a href="http://www.w3.org/TR/css3-selectors/#class-html">CSS3 class selectors</a>
+     */
+    public ObservableList<String> getStyleClass() {
+        return styleClass;
+    }    
+    
+    
+    // --- selected 
+    private final BooleanProperty selectedProperty = new SimpleBooleanProperty(this, "selected") { //$NON-NLS-1$
+    	@Override public void set(boolean selected) {
+    		if (locked) throw new UnsupportedOperationException("The action is immutable, property change support is disabled."); //$NON-NLS-1$
+    		super.set(selected);
+    	}
+    };
+    
+    /**
+     * Represents action's selected state. 
+     * Usually bound to selected state of components such as Toggle Buttons, CheckBOxes  etc
+     *
+     * @return An observable {@link BooleanProperty} that represents the current
+     *      selected state, and which can be observed for changes.
+     */
+    public final BooleanProperty selectedProperty() {
+    	return selectedProperty;
+    }
+    
+    /**
+     * Selected state of the Action.
+     * @return The selected state of this action.
+     */
+    public final boolean isSelected() {
+    	return selectedProperty.get();
+    }
+    
+    /**
+     * Sets selected state of the Action
+     * @param selected
+     */
+    public final void setSelected( boolean selected ) {
+    	selectedProperty.set(selected);
+    }
+    
+    
+    // --- text
+    private final StringProperty textProperty = new SimpleLocalizedStringProperty(this, "text"){ //$NON-NLS-1$
+    	@Override public void set(String value) {
+    		if ( locked ) throw new RuntimeException("The action is immutable, property change support is disabled."); //$NON-NLS-1$
+    		super.set(value);
+    	}
+    };
+    
+    /**
+     * The text to show to the user.
+     * 
+     * @return An observable {@link StringProperty} that represents the current
+     *      text for this property, and which can be observed for changes.
+     */
+    public final StringProperty textProperty() {
+        return textProperty;
+    }
+    
+    /**
+     * 
+     * @return the text of the Action.
+     */
+    public final String getText() {
+        return textProperty.get();
+    }
+
+    /**
+     * Sets the text of the Action.
+     * @param value 
+     */
+    public final void setText(String value) {
+        textProperty.set(value);
+    }
+    
+    
+    // --- disabled
+    private final BooleanProperty disabledProperty = new SimpleBooleanProperty(this, "disabled"){ //$NON-NLS-1$
+        @Override public void set(boolean value) {
+    		if ( locked ) throw new RuntimeException("The action is immutable, property change support is disabled."); //$NON-NLS-1$
+    		super.set(value);
+    	}
+    };
+    
+    /**
+     * This represents whether the action should be available to the end user,
+     * or whether it should appeared 'grayed out'.
+     * 
+     * @return An observable {@link BooleanProperty} that represents the current
+     *      disabled state for this property, and which can be observed for 
+     *      changes.
+     */
+    public final BooleanProperty disabledProperty() {
+        return disabledProperty;
+    }
+    
+    /**
+     * 
+     * @return whether the action is available to the end user,
+     * or whether it should appeared 'grayed out'.
+     */
+    public final boolean isDisabled() {
+        return disabledProperty.get();
+    }
+    
+    /**
+     * Sets whether the action should be available to the end user,
+     * or whether it should appeared 'grayed out'.
+     * @param value 
+     */
+    public final void setDisabled(boolean value) {
+        disabledProperty.set(value);
+    }
+
+    
+    // --- longText
+    private final StringProperty longTextProperty = new SimpleLocalizedStringProperty(this, "longText"){ //$NON-NLS-1$
+        @Override public void set(String value) {
+    		if ( locked ) throw new RuntimeException("The action is immutable, property change support is disabled."); //$NON-NLS-1$
+    		super.set(value);
+
+    	}
+    };
+    
+    /**
+     * The longer form of the text to show to the user (e.g. on a 
+     * {@link Button}, it is usually a tooltip that should be shown to the user 
+     * if their mouse hovers over this action).
+     * 
+     * @return An observable {@link StringProperty} that represents the current
+     *      long text for this property, and which can be observed for changes.
+     */
+    public final StringProperty longTextProperty() {
+        return longTextProperty;
+    }
+    
+    /**
+     * @see #longTextProperty() 
+     * @return The longer form of the text to show to the user
+     */
+    public final String getLongText() {
+        return Localization.localize(longTextProperty.get());
+    }
+    
+    /**
+     * Sets the longer form of the text to show to the user
+     * @param value 
+     * @see #longTextProperty() 
+     */
+    public final void setLongText(String value) {
+        longTextProperty.set(value);
+    }
+    
+    
+    // --- graphic
+    private final ObjectProperty<Node> graphicProperty = new SimpleObjectProperty<Node>(this, "graphic"){ //$NON-NLS-1$
+        @Override public void set(Node value) {
+    		if ( locked ) throw new RuntimeException("The action is immutable, property change support is disabled."); //$NON-NLS-1$
+    		super.set(value);
+
+    	}
+    };
+    
+    /**
+     * The graphic that should be shown to the user in relation to this action.
+     * 
+     * @return An observable {@link ObjectProperty} that represents the current
+     *      graphic for this property, and which can be observed for changes.
+     */
+    public final ObjectProperty<Node> graphicProperty() {
+        return graphicProperty;
+    }
+    
+    /**
+     * 
+     * @return The graphic that should be shown to the user in relation to this action.
+     */
+    public final Node getGraphic() {
+        return graphicProperty.get();
+    }
+    
+    /**
+     * Sets the graphic that should be shown to the user in relation to this action.
+     * @param value 
+     */
+    public final void setGraphic(Node value) {
+        graphicProperty.set(value);
+    }
+    
+    
+    // --- accelerator
+    private final ObjectProperty<KeyCombination> acceleratorProperty = new SimpleObjectProperty<KeyCombination>(this, "accelerator"){ //$NON-NLS-1$
+        @Override public void set(KeyCombination value) {
+    		if ( locked ) throw new RuntimeException("The action is immutable, property change support is disabled."); //$NON-NLS-1$
+    		super.set(value);
+
+    	}
+    };
+    
+    /**
+     * The accelerator {@link KeyCombination} that should be used for this action,
+     * if it is used in an applicable UI control (most notably {@link MenuItem}).
+     * 
+     * @return An observable {@link ObjectProperty} that represents the current
+     *      accelerator for this property, and which can be observed for changes.
+     */
+    public final ObjectProperty<KeyCombination> acceleratorProperty() {
+        return acceleratorProperty;
+    }
+    
+    /**
+     * 
+     * @return The accelerator {@link KeyCombination} that should be used for this action,
+     * if it is used in an applicable UI control
+     */
+    public final KeyCombination getAccelerator() {
+        return acceleratorProperty.get();
+    }
+    
+    /**
+     * Sets the accelerator {@link KeyCombination} that should be used for this action,
+     * if it is used in an applicable UI control
+     * @param value 
+     */
+    public final void setAccelerator(KeyCombination value) {
+        acceleratorProperty.set(value);
+    }
+    
+    
+    // --- properties
+    private ObservableMap<Object, Object> props;
+    
+    /**
+     * Returns an observable map of properties on this Action for use primarily
+     * by application developers.
+     *
+     * @return An observable map of properties on this Action for use primarily
+     * by application developers
+     */
+    public final synchronized ObservableMap<Object, Object> getProperties() {
+    	if ( props == null ) props = FXCollections.observableHashMap();
+    	return props;
+    }
+    
+    protected Consumer<ActionEvent> getEventHandler() {
+		return eventHandler;
+	}
+    
+    protected void setEventHandler(Consumer<ActionEvent> eventHandler) {
+		this.eventHandler = eventHandler;
+	}
+    
+    /**************************************************************************
+     * 
+     * Public API
+     * 
+     **************************************************************************/
+    
+    /** 
+     * Defers to the {@code Consumer<ActionEvent>} passed in to the Action constructor.
+     */
+    @Override public final void handle(ActionEvent event) {
+        if (eventHandler != null && !isDisabled()) {
+            eventHandler.accept(event);
+        }
+    }
+    
+//    public void bind(ButtonBase button) {
+//        ActionUtils.configureButton(this, button);
+//    }
+//    
+//    public void bind(MenuItem menuItem) {
+//        ActionUtils.configureMenuItem(this, menuItem);
+//    }
+}
\ No newline at end of file
diff --git a/controlsfx/src/main/java/org/controlsfx/control/action/ActionCheck.java b/controlsfx/src/main/java/org/controlsfx/control/action/ActionCheck.java
new file mode 100644
index 0000000..b59ed06
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/action/ActionCheck.java
@@ -0,0 +1,21 @@
+package org.controlsfx.control.action;
+
+import javafx.scene.control.Button;
+import javafx.scene.control.CheckMenuItem;
+import javafx.scene.control.MenuItem;
+import javafx.scene.control.ToggleButton;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks the {@link Action} or a method annotated with {@link ActionProxy} to let action engine know
+ * that {@link ToggleButton} or {@link CheckMenuItem} has to be bound to the action
+ * instead of standard {@link Button} and {@link MenuItem}
+ */
+ at Target( { ElementType.TYPE, ElementType.METHOD } )
+ at Retention(RetentionPolicy.RUNTIME)
+public @interface ActionCheck {
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/action/ActionGroup.java b/controlsfx/src/main/java/org/controlsfx/control/action/ActionGroup.java
new file mode 100644
index 0000000..06ab1f0
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/action/ActionGroup.java
@@ -0,0 +1,167 @@
+/**
+ * Copyright (c) 2013, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.action;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.scene.Node;
+import javafx.scene.control.ContextMenu;
+import javafx.scene.control.MenuBar;
+import javafx.scene.control.ToolBar;
+
+/**
+ * An ActionGroup (unsurprisingly) groups together zero or more {@link Action} 
+ * instances, allowing for more complex controls like {@link ToolBar}, 
+ * {@link MenuBar} and {@link ContextMenu} to be automatically generated from
+ * the collection of actions inside the ActionGroup. For your convenience,
+ * there are a number of utility methods that do precisely this in the 
+ * {@link ActionUtils} class.
+ * 
+ * <h3>Code Examples</h3>
+ * <p>Consider the following code example (note that DummyAction is a fake class
+ * that extends from (and implements) {@link Action}):
+ * 
+ * <pre>
+ * {@code
+ * // Firstly, create a list of Actions
+ * Collection<? extends Action> actions = Arrays.asList(
+ *     new ActionGroup("Group 1",  new DummyAction("Action 1.1"), 
+ *                                 new DummyAction("Action 2.1") ),
+ *     new ActionGroup("Group 2",  new DummyAction("Action 2.1"), 
+ *                                 new ActionGroup("Action 2.2", new DummyAction("Action 2.2.1"), 
+ *                                                               new DummyAction("Action 2.2.2")),
+ *                                 new DummyAction("Action 2.3") ),
+ *     new ActionGroup("Group 3",  new DummyAction("Action 3.1"), 
+ *                                 new DummyAction("Action 3.2") )
+ *   );
+ *   
+ *   // Use the ActionUtils class to create UI controls from these actions, e.g:
+ *   MenuBar menuBar = ActionUtils.createMenuBar(actions);
+ *   
+ *   ToolBar toolBar = ActionUtils.createToolBar(actions);
+ *   
+ *   Label context = new Label("Right-click to see the context menu");
+ *   context.setContextMenu(ActionUtils.createContextMenu(actions));  
+ * }</pre>
+ * 
+ * <p>The end result of running the code above is shown in the screenshots below
+ * (hopefully it goes without saying that within the 'Group 1', 'Group 2' and 
+ * 'Group 3' options are the 'Action 1.1', etc actions that have been specified
+ * in the code above):
+ *
+ * <table border="0" summary="ActionGroup Screenshots">
+ *   <tr>
+ *     <td width="75" valign="center"><strong>MenuBar:</strong></td>
+ *     <td><img src="actionGroup-menubar.png" alt="Screenshot of ActionGroup in a MenuBar"></td>
+ *   </tr>
+ *   <tr>
+ *     <td width="75" valign="center"><strong>ToolBar:</strong></td>
+ *     <td><img src="actionGroup-toolbar.png" alt="Screenshot of ActionGroup in a ToolBar"></td>
+ *   </tr>
+ *   <tr>
+ *   <td width="75" valign="top"><strong>ContextMenu:</strong></td>
+ *     <td><img src="actionGroup-contextmenu.png" alt="Screenshot of ActionGroup in a ContextMenu"></td>
+ *   </tr>
+ * </table>
+ * 
+ * @see Action
+ * @see ActionUtils
+ */
+public class ActionGroup extends Action {
+    
+    /**
+     * Creates an ActionGroup with the given text as the name of the {@link Action}, 
+     * and zero or more Actions as members of this ActionGroup. Note that it is
+     * legitimate to pass in zero Actions to this constructor, and to later 
+     * set the actions directly into the {@link #getActions() actions} list.
+     * 
+     * @param text The {@link Action#textProperty() text} of this {@link Action}.
+     * @param actions Zero or more actions to insert into this ActionGroup.
+     */
+    public ActionGroup(String text, Action... actions) {
+    	 this(text, Arrays.asList(actions));
+    }
+    
+    /**
+     * Creates an ActionGroup with the given text as the name of the {@link Action}, 
+     * and collection of Actions as members of this ActionGroup. 
+     * 
+     * @param text The {@link Action#textProperty() text} of this {@link Action}.
+     * @param actions Collection of actions to insert into this ActionGroup.
+     */
+    public ActionGroup(String text, Collection<Action> actions) {
+        super(text);
+        getActions().addAll(actions);
+    }
+    
+    /**
+     * Creates an ActionGroup with the given text as the name of the {@link Action}, 
+     * and zero or more Actions as members of this ActionGroup. Note that it is
+     * legitimate to pass in zero Actions to this constructor, and to later 
+     * set the actions directly into the {@link #getActions() actions} list.
+     * 
+     * @param text The {@link Action#textProperty() text} of this {@link Action}.
+     * @param icon The {@link Action#graphicProperty() image} of this {@link Action}.
+     * @param actions Zero or more actions to insert into this ActionGroup.
+     */
+    public ActionGroup(String text, Node icon, Action... actions) {
+    	 this( text, icon, Arrays.asList(actions));
+    }
+    
+    /**
+     * Creates an ActionGroup with the given text as the name of the {@link Action},
+     * and collection of Actions as members of this ActionGroup. .
+     * 
+     * @param text The {@link Action#textProperty() text} of this {@link Action}.
+     * @param icon The {@link Action#graphicProperty() image} of this {@link Action}.
+     * @param actions Collection of actions to insert into this ActionGroup.
+     */
+    public ActionGroup(String text, Node icon, Collection<Action> actions) {
+        super(text);
+        setGraphic(icon);
+        getActions().addAll(actions);
+    }
+
+    // --- actions
+    private final ObservableList<Action> actions = FXCollections.<Action> observableArrayList();
+
+    /**
+     * The list of {@link Action} instances that exist within this ActionGroup.
+     * This list may be modified, as shown in the class documentation.
+     */
+    public final ObservableList<Action> getActions() {
+        return actions;
+    }
+    
+    @Override public String toString() {
+        return getText();
+    }
+    
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/action/ActionMap.java b/controlsfx/src/main/java/org/controlsfx/control/action/ActionMap.java
new file mode 100644
index 0000000..a8e8380
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/action/ActionMap.java
@@ -0,0 +1,224 @@
+/**
+ * Copyright (c) 2013, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.action;
+
+import javafx.event.ActionEvent;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.*;
+
+/**
+ * Action Map provides an ability to create an action map of any object.
+ * Attempts to convert methods annotated with {@link ActionProxy} to {@link Action}. 
+ * 
+ * <h3>Code Example</h3>
+ * Here's a very simple example of how to use ActionMap to register a class (in
+ * this class it is the application class itself), and to then retrieve actions
+ * out of the ActionMap (via the static {@link ActionMap#action(String)} method:
+ * <br>
+ * 
+ * <pre>
+ * public class ActionMapDemo extends Application {
+ *     public ActionMapDemo() {
+ *         ActionMap.register(this);
+ *         Action action11 = ActionMap.action("action11");
+ *         Button actionButton = ActionUtils.createButton(action11);
+ *     }
+ *  
+ *     @ActionProxy(text="Action 1.1", graphic="start.png", accelerator="ctrl+shift+T")
+ *     private void action11() {
+ *         System.out.println( "Action 1.1 is executed");
+ *     }
+ * }
+ * </pre>
+ * 
+ * If you require more control over the creation of the Action objects, you can either set the
+ * global ActionFactory by calling ActionMap.setActionFactory() and/or you can use the factory 
+ * property on individual @ActionProxy declarations to set the factory on a case-by-case basis.
+ * 
+ * @see ActionProxy
+ * @see Action
+ */
+public class ActionMap {
+
+    private static AnnotatedActionFactory actionFactory = new DefaultActionFactory();
+    
+    private static final Map<String, AnnotatedAction> actions = new HashMap<>();
+    
+
+    private ActionMap() {
+        // no-op
+    }
+
+
+    /**
+     * Returns the action factory used by ActionMap to construct AnnotatedAction instances. By default, this
+     * is an instance of {@link DefaultActionFactory}.
+     */
+    public static AnnotatedActionFactory getActionFactory() {
+        return actionFactory;
+    }
+
+    /**
+     * Sets the action factory used by ActionMap to construct AnnotatedAction instances. This factory can be overridden on
+     * a case-by-case basis by specifying a factory class in {@link ActionProxy#factory()}
+     */
+    public static void setActionFactory( AnnotatedActionFactory factory ) {
+        Objects.requireNonNull( factory );
+        actionFactory = factory;
+    }
+    
+    
+    
+    
+    /**
+     * Attempts to convert target's methods annotated with {@link ActionProxy} to {@link Action}s.
+     * Three types of methods are currently converted: parameter-less methods, 
+     * methods with one parameter of type {@link ActionEvent}, and methods with two parameters
+     * ({@link ActionEvent}, {@link Action}).
+     * 
+     * Note that this method supports safe re-registration of a given instance or of another instance of the
+     * same class that has already been registered. If another instance of the same class is registered, then
+     * those actions will now be associated with the new instance. The first instance is implicitly unregistered.
+     * 
+     * Actions are registered with their id or method name if id is not defined.
+     * 
+     * @param target object to work on
+     * @throws IllegalStateException if a method with unsupported parameters is annotated with {@link ActionProxy}.
+     */
+    public static void register(Object target) {
+
+        for (Method method : target.getClass().getDeclaredMethods()) {
+            // Only process methods that have the ActionProxy annotation
+            Annotation[] annotations = method.getAnnotationsByType(ActionProxy.class);
+            if (annotations.length == 0) {
+                continue;
+            }
+            
+            // Only process methods that have
+            // a) no parameters OR 
+            // b) one parameter of type ActionEvent OR
+            // c) two parameters (ActionEvent, Action)
+            int paramCount = method.getParameterCount();
+            Class[] paramTypes = method.getParameterTypes();
+            
+            if (paramCount > 2) {
+                throw new IllegalArgumentException( String.format( "Method %s has too many parameters", method.getName() ) );
+            }
+            
+            if (paramCount == 1 && !ActionEvent.class.isAssignableFrom( paramTypes[0] )) {
+                throw new IllegalArgumentException( String.format( "Method %s -- single parameter must be of type ActionEvent", method.getName() ) );
+            }
+            
+            if (paramCount == 2 && (!ActionEvent.class.isAssignableFrom( paramTypes[0] ) || 
+                                    !Action.class.isAssignableFrom( paramTypes[1] ))) {
+                throw new IllegalArgumentException( String.format( "Method %s -- parameters must be of types (ActionEvent, Action)", method.getName() ) );
+            }
+            
+            ActionProxy annotation = (ActionProxy) annotations[0];
+
+            AnnotatedActionFactory factory = determineActionFactory( annotation );
+            AnnotatedAction action = factory.createAction( annotation, method, target );
+
+            String id = annotation.id().isEmpty() ? method.getName() : annotation.id();
+            actions.put( id, action );
+        }
+    }
+    
+    
+    
+    
+    private static AnnotatedActionFactory determineActionFactory( ActionProxy annotation ) {
+        // Default to using the global action factory
+        AnnotatedActionFactory factory = actionFactory;
+        
+        // If an action-factory has been specified on this specific ActionProxy, then
+        // instantiate it and use it instead.
+        String factoryClassName = annotation.factory();
+        if (!factoryClassName.isEmpty()) {
+            try {
+                Class factoryClass = Class.forName( factoryClassName );
+                factory = (AnnotatedActionFactory) factoryClass.newInstance();
+
+            } catch (ClassNotFoundException ex) {
+                throw new IllegalArgumentException( String.format( "Action proxy refers to non-existant factory class %s", factoryClassName ), ex );
+
+            } catch (InstantiationException | IllegalAccessException ex) {
+                throw new IllegalStateException( String.format( "Unable to instantiate action factory class %s", factoryClassName ), ex );
+            }
+        }
+        
+        return factory;
+    }
+    
+    
+    /**
+     * Removes all the actions associated with target object from the action map.
+     * @param target object to work on
+     */
+    public static void unregister(Object target) {
+        if ( target != null ) {
+            Iterator<Map.Entry<String, AnnotatedAction>> entryIter = actions.entrySet().iterator();
+            while (entryIter.hasNext()) {
+                Map.Entry<String, AnnotatedAction> entry = entryIter.next();
+                
+                Object actionTarget = entry.getValue().getTarget();
+                
+                if (actionTarget == null || actionTarget == target) {
+                    entryIter.remove();
+                }
+            }
+        }
+    }
+
+    /**
+     * Returns action by its id.
+     * @param id action id
+     * @return action or null if id was not found
+     */
+    public static Action action(String id) {
+        return actions.get(id);
+    }
+
+    /**
+     * Returns collection of actions by ids. Useful to create {@link ActionGroup}s.
+     * Ids starting with "---" are converted to {@link ActionUtils#ACTION_SEPARATOR}.
+     * Incorrect ids are ignored. 
+     * @param ids action ids
+     * @return collection of actions
+     */
+    public static Collection<Action> actions(String... ids) {
+        List<Action> result = new ArrayList<>();
+        for( String id: ids ) {
+            if ( id.startsWith("---")) result.add(ActionUtils.ACTION_SEPARATOR); //$NON-NLS-1$
+            Action action = action(id);
+            if ( action != null ) result.add(action);
+        }
+        return result;
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/action/ActionProxy.java b/controlsfx/src/main/java/org/controlsfx/control/action/ActionProxy.java
new file mode 100644
index 0000000..30f9064
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/action/ActionProxy.java
@@ -0,0 +1,138 @@
+/**
+ * Copyright (c) 2013, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.action;
+
+import javafx.event.ActionEvent;
+import org.controlsfx.glyphfont.Glyph;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * An annotation to allow conversion of class methods to {@link Action} instances.
+ * 
+ * <p>The following steps are required to use {@link ActionProxy} annotations:
+ * 
+ * <ol>
+ * <li>Annotate your methods with the {@link ActionProxy} annotation. For example:
+ * <pre>{@code @ActionProxy(text="Action 1.1", graphic=imagePath, accelerator="ctrl+shift+T")
+ * private void action11() {
+ *     System.out.println("Action 1.1 is executed");
+ * }}</pre>
+ * 
+ * <p>The ActionProxy annotation is designed to work with three types of methods: 
+ * <ol>
+ *     <li>Methods with no parameters, 
+ *     <li>Methods with one parameter of type {@link ActionEvent}.
+ *     <li>Methods that take both an {@link ActionEvent} and an {@link Action}.
+ * </ol> 
+ * 
+ * <p>The ActionProxy annotation {@link #graphic()} property supports different node types:
+ * <ol>
+ *     <li>Images,
+ *     <li>Glyph fonts.
+ * </ol>
+ * 
+ * <p>The ability for ActionProxy to support glyph fonts is part of the ControlsFX
+ * {@link Glyph} API. For more information on how to specify
+ * images and glyph fonts, refer to the {@link ActionProxy#graphic()} method.
+ * <br><br></li>
+ * 
+ * <li>Register your class in the global {@link ActionMap}, preferably in the 
+ * class constructor:  
+ * <pre>{@code ActionMap.register(this); }</pre> 
+ * 
+ * Immediately after that actions will be created according to the provided 
+ * annotations and are accessible from {@link ActionMap}, which provides several 
+ * convenience methods to access actions by id. Refer to the {@link ActionMap}
+ * class for more details on how to use it.</li>
+ *
+ * <p>{@link ActionCheck} annotation is supported on the same method where @ActionProxy is applied}</p>
+ * </ol>
+ * 
+ * @see Action
+ * @see ActionMap
+ */
+ at Target(ElementType.METHOD)
+ at Retention(RetentionPolicy.RUNTIME)
+public @interface ActionProxy {
+    
+    /**
+     * By default the method name that this annotation is applied to, but if not
+     * null then this ID is what you use when requesting the {@link Action} out
+     * of the {@link ActionMap} when using the {@link ActionMap#action(String)}
+     * method.
+     */
+	String id() default "";
+	
+	/**
+	 * The text that should be set in {@link Action#textProperty()}.
+	 */
+    String text();
+    
+    /**
+     * The graphic that should be set in {@link Action#graphicProperty()}.
+     * 
+     * <p>The graphic can be either image (local path or url) or font glyph. 
+     * 
+     * <p>Because a graphic can come from multiple sources, a simple protocol 
+     * prefix is used to designate the type. Currently supported prefixes are 
+     * '<code>image></code>' and '<code>font></code>'. Default protocol is 
+     * '<code>image></code>'. 
+     * 
+     * <p>The following are the examples of different graphic nodes:
+	 * <pre>
+	 * @ActionProxy(text="Teacher", graphic="http://icons.iconarchive.com/icons/custom-icon-design/mini-3/16/teacher-male-icon.png")
+	 * @ActionProxy(text="Security", graphic="/org/controlsfx/samples/security-low.png")
+	 * @ActionProxy(text="Security", graphic="image>/org/controlsfx/samples/security-low.png")
+	 * @ActionProxy(text="Star", graphic="font>FontAwesome|STAR")
+	 * </pre>     
+	 * 
+	 */
+    String graphic() default "";
+    
+    /**
+     * The text that should be set in {@link Action#longTextProperty()}.
+     */
+    String longText() default "";
+    
+    /**
+     * Accepts string values such as "ctrl+shift+T" to represent the keyboard
+     * shortcut for this action. By default this is empty if there is no keyboard
+     * shortcut desired for this action.
+     */
+    String accelerator() default "";
+    
+    /**
+     * The full class-name of a class that implements {@link AnnotatedActionFactory}. {@link ActionMap} will
+     * use this class to instantiate the {@link AnnotatedAction} associated with this method, rather than
+     * using its own action factory.
+     */
+    String factory() default "";
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/action/ActionUtils.java b/controlsfx/src/main/java/org/controlsfx/control/action/ActionUtils.java
new file mode 100644
index 0000000..d7169dc
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/action/ActionUtils.java
@@ -0,0 +1,890 @@
+/**
+ * Copyright (c) 2013, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.action;
+
+import javafx.beans.InvalidationListener;
+import javafx.beans.binding.ObjectBinding;
+import javafx.beans.binding.StringBinding;
+import javafx.beans.binding.When;
+import javafx.collections.FXCollections;
+import javafx.collections.ListChangeListener;
+import javafx.collections.MapChangeListener;
+import javafx.collections.ObservableList;
+import javafx.css.Styleable;
+import javafx.scene.Node;
+import javafx.scene.control.*;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.VBox;
+import org.controlsfx.control.SegmentedButton;
+import org.controlsfx.tools.Duplicatable;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+
+/**
+ * Convenience class for users of the {@link Action} API. Primarily this class
+ * is used to conveniently create UI controls from a given Action (this is
+ * necessary for now as there is no built-in support for Action in JavaFX
+ * UI controls at present).
+ *
+ * <p>Some of the methods in this class take a {@link Collection} of
+ * {@link Action actions}. In these cases, it is likely they are designed to
+ * work with {@link ActionGroup action groups}. For examples on how to work with
+ * these methods, refer to the {@link ActionGroup} class documentation.
+ *
+ * @see Action
+ * @see ActionGroup
+ */
+ at SuppressWarnings("deprecation")
+public class ActionUtils {
+
+    /***************************************************************************
+     *                                                                         *
+     * Constructors                                                            *
+     *                                                                         *
+     **************************************************************************/
+
+    private ActionUtils() {
+        // no-op
+    }
+
+    /***************************************************************************
+     *                                                                         *
+     * Action API                                                              *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * Action text behavior.
+     * Defines uniform action's text behavior for multi-action controls such as toolbars and menus
+     */
+    public enum ActionTextBehavior {
+        /**
+         * Text is shown as usual on related control
+         */
+        SHOW,
+
+        /**
+         * Text is not shown on the related control
+         */
+        HIDE,
+    }
+
+
+    /**
+     * Takes the provided {@link Action} and returns a {@link Button} instance
+     * with all relevant properties bound to the properties of the Action.
+     *
+     * @param action The {@link Action} that the {@link Button} should bind to.
+     * @param textBehavior Defines {@link ActionTextBehavior}
+     * @return A {@link Button} that is bound to the state of the provided
+     *      {@link Action}
+     */
+    public static Button createButton(final Action action, final ActionTextBehavior textBehavior) {
+        return configure(new Button(), action, textBehavior);
+    }
+
+    /**
+     * Takes the provided {@link Action} and returns a {@link Button} instance
+     * with all relevant properties bound to the properties of the Action.
+     *
+     * @param action The {@link Action} that the {@link Button} should bind to.
+     * @return A {@link Button} that is bound to the state of the provided
+     *      {@link Action}
+     */
+    public static Button createButton(final Action action) {
+        return configure(new Button(), action, ActionTextBehavior.SHOW);
+    }
+
+    /**
+     * Takes the provided {@link Action} and binds the relevant properties to
+     * the supplied {@link Button}. This allows for the use of Actions
+     * within custom Button subclasses.
+     *
+     * @param action The {@link Action} that the {@link Button} should bind to.
+     * @param button The {@link ButtonBase} that the {@link Action} should be bound to.
+     * @return The {@link ButtonBase} that was bound to the {@link Action}.
+     */
+    public static ButtonBase configureButton(final Action action, ButtonBase button) {
+        return configure(button, action, ActionTextBehavior.SHOW);
+    }
+
+    /**
+     * Removes all bindings and listeners which were added when the supplied
+     * {@link ButtonBase} was bound to an {@link Action} via one of the methods
+     * of this class.
+     *
+     * @param button a {@link ButtonBase} that was bound to an {@link Action}
+     */
+    public static void unconfigureButton(ButtonBase button) {
+        unconfigure(button);
+    }
+	    
+    /**
+     * Takes the provided {@link Action} and returns a {@link MenuButton} instance
+     * with all relevant properties bound to the properties of the Action.
+     *
+     * @param action The {@link Action} that the {@link MenuButton} should bind to.
+     * @param textBehavior Defines {@link ActionTextBehavior}
+     * @return A {@link MenuButton} that is bound to the state of the provided
+     *      {@link Action}
+     */
+    public static MenuButton createMenuButton(final Action action, final ActionTextBehavior textBehavior) {
+        return configure(new MenuButton(), action, textBehavior);
+    }
+
+    /**
+     * Takes the provided {@link Action} and returns a {@link MenuButton} instance
+     * with all relevant properties bound to the properties of the Action.
+     *
+     * @param action The {@link Action} that the {@link MenuButton} should bind to.
+     * @return A {@link MenuButton} that is bound to the state of the provided
+     *      {@link Action}
+     */
+    public static MenuButton createMenuButton(final Action action) {
+        return configure(new MenuButton(), action, ActionTextBehavior.SHOW);
+    }
+
+    /**
+     * Takes the provided {@link Action} and returns a {@link Hyperlink} instance
+     * with all relevant properties bound to the properties of the Action.
+     *
+     * @param action The {@link Action} that the {@link Hyperlink} should bind to.
+     * @return A {@link Hyperlink} that is bound to the state of the provided
+     *      {@link Action}
+     */
+    public static Hyperlink createHyperlink(final Action action) {
+        return configure(new Hyperlink(), action, ActionTextBehavior.SHOW);
+    }
+
+    /**
+     * Takes the provided {@link Action} and returns a {@link ToggleButton} instance
+     * with all relevant properties bound to the properties of the Action.
+     *
+     * @param action The {@link Action} that the {@link ToggleButton} should bind to.
+     * @param textBehavior Defines {@link ActionTextBehavior}
+     * @return A {@link ToggleButton} that is bound to the state of the provided
+     *      {@link Action}
+     */
+    public static ToggleButton createToggleButton(final Action action, final ActionTextBehavior textBehavior ) {
+        return configure(new ToggleButton(), action, textBehavior);
+    }
+
+    /**
+     * Takes the provided {@link Action} and returns a {@link ToggleButton} instance
+     * with all relevant properties bound to the properties of the Action.
+     *
+     * @param action The {@link Action} that the {@link ToggleButton} should bind to.
+     * @return A {@link ToggleButton} that is bound to the state of the provided
+     *      {@link Action}
+     */
+    public static ToggleButton createToggleButton( final Action action ) {
+        return createToggleButton( action, ActionTextBehavior.SHOW );
+    }
+
+    /**
+     * Takes the provided {@link Collection} of {@link Action}  and returns a {@link SegmentedButton} instance
+     * with all relevant properties bound to the properties of the actions.
+     *
+     * @param actions The {@link Collection} of {@link Action} that the {@link SegmentedButton} should bind to.
+     * @param textBehavior Defines {@link ActionTextBehavior}
+     * @return A {@link SegmentedButton} that is bound to the state of the provided {@link Action}s
+     */
+    public static SegmentedButton createSegmentedButton(final ActionTextBehavior textBehavior, Collection<? extends Action> actions) {
+        ObservableList<ToggleButton> buttons = FXCollections.observableArrayList();
+        for( Action a: actions ) {
+            buttons.add( createToggleButton(a,textBehavior));
+        }
+        return new SegmentedButton( buttons );
+    }
+
+    /**
+     * Takes the provided {@link Collection} of {@link Action}  and returns a {@link SegmentedButton} instance
+     * with all relevant properties bound to the properties of the actions.
+     *
+     * @param actions The {@link Collection} of {@link Action} that the {@link SegmentedButton} should bind to.
+     * @return A {@link SegmentedButton} that is bound to the state of the provided {@link Action}s
+     */
+    public static SegmentedButton createSegmentedButton(Collection<? extends Action> actions) {
+        return createSegmentedButton( ActionTextBehavior.SHOW, actions);
+    }
+
+    /**
+     * Takes the provided varargs array of {@link Action}  and returns a {@link SegmentedButton} instance
+     * with all relevant properties bound to the properties of the actions.
+     *
+     * @param actions A varargs array of {@link Action} that the {@link SegmentedButton} should bind to.
+     * @param textBehavior Defines {@link ActionTextBehavior}
+     * @return A {@link SegmentedButton} that is bound to the state of the provided {@link Action}s
+     */
+    public static SegmentedButton createSegmentedButton(ActionTextBehavior textBehavior, Action... actions) {
+        return createSegmentedButton(textBehavior, Arrays.asList(actions));
+    }
+
+    /**
+     * Takes the provided varargs array of {@link Action}  and returns a {@link SegmentedButton} instance
+     * with all relevant properties bound to the properties of the actions.
+     *
+     * @param actions A varargs array of {@link Action} that the {@link SegmentedButton} should bind to.
+     * @return A {@link SegmentedButton} that is bound to the state of the provided {@link Action}s
+     */
+    public static SegmentedButton createSegmentedButton(Action... actions) {
+        return createSegmentedButton(ActionTextBehavior.SHOW, Arrays.asList(actions));
+    }
+
+
+
+    /**
+     * Takes the provided {@link Action} and returns a {@link CheckBox} instance
+     * with all relevant properties bound to the properties of the Action.
+     *
+     * @param action The {@link Action} that the {@link CheckBox} should bind to.
+     * @return A {@link CheckBox} that is bound to the state of the provided
+     *      {@link Action}
+     */
+    public static CheckBox createCheckBox(final Action action) {
+        return configure(new CheckBox(), action, ActionTextBehavior.SHOW);
+    }
+
+    /**
+     * Takes the provided {@link Action} and returns a {@link RadioButton} instance
+     * with all relevant properties bound to the properties of the Action.
+     *
+     * @param action The {@link Action} that the {@link RadioButton} should bind to.
+     * @return A {@link RadioButton} that is bound to the state of the provided
+     *      {@link Action}
+     */
+    public static RadioButton createRadioButton(final Action action) {
+        return configure(new RadioButton(), action, ActionTextBehavior.SHOW);
+    }
+
+    /**
+     * Takes the provided {@link Action} and returns a {@link MenuItem} instance
+     * with all relevant properties bound to the properties of the Action.
+     *
+     * @param action The {@link Action} that the {@link MenuItem} should bind to.
+     * @return A {@link MenuItem} that is bound to the state of the provided
+     *      {@link Action}
+     */
+    public static MenuItem createMenuItem(final Action action) {
+
+        MenuItem menuItem =
+            action.getClass().isAnnotationPresent(ActionCheck.class)? new CheckMenuItem(): new MenuItem();
+
+        return configure( menuItem, action);
+    }
+
+    public static MenuItem configureMenuItem(final Action action, MenuItem menuItem) {
+        return configure(menuItem, action);
+    }
+
+    /**
+     * Removes all bindings and listeners which were added when the supplied
+     * {@link MenuItem} was bound to an {@link Action} via one of the methods
+     * of this class.
+     *
+     * @param menuItem a {@link MenuItem} that was bound to an {@link Action}
+     */
+    public static void unconfigureMenuItem(final MenuItem menuItem) {
+        unconfigure(menuItem);
+    }
+    
+    /**
+     * Takes the provided {@link Action} and returns a {@link Menu} instance
+     * with all relevant properties bound to the properties of the Action.
+     *
+     * @param action The {@link Action} that the {@link Menu} should bind to.
+     * @return A {@link Menu} that is bound to the state of the provided
+     *      {@link Action}
+     */
+    public static Menu createMenu(final Action action) {
+        return configure(new Menu(), action);
+    }
+
+    /**
+     * Takes the provided {@link Action} and returns a {@link CheckMenuItem} instance
+     * with all relevant properties bound to the properties of the Action.
+     *
+     * @param action The {@link Action} that the {@link CheckMenuItem} should bind to.
+     * @return A {@link CheckMenuItem} that is bound to the state of the provided
+     *      {@link Action}
+     */
+    public static CheckMenuItem createCheckMenuItem(final Action action) {
+        return configure(new CheckMenuItem(), action);
+    }
+
+    /**
+     * Takes the provided {@link Action} and returns a {@link RadioMenuItem} instance
+     * with all relevant properties bound to the properties of the Action.
+     *
+     * @param action The {@link Action} that the {@link RadioMenuItem} should bind to.
+     * @return A {@link RadioMenuItem} that is bound to the state of the provided
+     *      {@link Action}
+     */
+    public static RadioMenuItem createRadioMenuItem(final Action action) {
+        return configure(new RadioMenuItem(action.textProperty().get()), action);
+    }
+
+
+
+    /***************************************************************************
+     *                                                                         *
+     * ActionGroup API                                                         *
+     *                                                                         *
+     **************************************************************************/
+
+
+    /**
+     * Action representation of the generic separator. Adding this action anywhere in the
+     * action tree serves as indication that separator has be created in its place.
+     * See {@link ActionGroup} for example of action tree creation
+     */
+    public static Action ACTION_SEPARATOR = new Action(null, null) {
+        @Override public String toString() {
+            return "Separator";  //$NON-NLS-1$
+        }
+    };
+
+    public static Action ACTION_SPAN = new Action(null, null) {
+        @Override public String toString() {
+            return "Span";  //$NON-NLS-1$
+        }
+    };
+
+
+
+    /**
+     * Takes the provided {@link Collection} of {@link Action} (or subclasses,
+     * such as {@link ActionGroup}) instances and returns a {@link ToolBar}
+     * populated with appropriate {@link Node nodes} bound to the provided
+     * {@link Action actions}.
+     *
+     * @param actions The {@link Action actions} to place on the {@link ToolBar}.
+     * @param textBehavior defines {@link ActionTextBehavior}
+     * @return A {@link ToolBar} that contains {@link Node nodes} which are bound
+     *      to the state of the provided {@link Action}
+     */
+    public static ToolBar createToolBar(Collection<? extends Action> actions, ActionTextBehavior textBehavior) {
+        return updateToolBar( new ToolBar(), actions, textBehavior );
+    }
+
+    /**
+     * Takes the provided {@link Collection} of {@link Action} (or subclasses,
+     * such as {@link ActionGroup}) instances and returns provided {@link ToolBar}
+     * populated with appropriate {@link Node nodes} bound to the provided
+     * {@link Action actions}. Previous toolbar content is removed
+     *
+     * @param toolbar The {@link ToolBar toolbar} to update
+     * @param actions The {@link Action actions} to place on the {@link ToolBar}.
+     * @param textBehavior defines {@link ActionTextBehavior}
+     * @return A {@link ToolBar} that contains {@link Node nodes} which are bound
+     *      to the state of the provided {@link Action}
+     */
+    public static ToolBar updateToolBar( ToolBar toolbar, Collection<? extends Action> actions, ActionTextBehavior textBehavior) {
+        toolbar.getItems().clear();
+        for (Action action : actions) {
+            if ( action instanceof ActionGroup ) {
+                MenuButton menu = createMenuButton( action, textBehavior );
+                menu.setFocusTraversable(false);
+                menu.getItems().addAll( toMenuItems( ((ActionGroup)action).getActions()));
+                toolbar.getItems().add(menu);
+            } else if ( action == ACTION_SEPARATOR ) {
+                toolbar.getItems().add( new Separator());
+            } else if ( action == ACTION_SPAN ) {
+                Pane span = new Pane();
+                HBox.setHgrow(span, Priority.ALWAYS);
+                VBox.setVgrow(span, Priority.ALWAYS);
+                toolbar.getItems().add(span);
+            } else if ( action == null ) {
+                //no-op
+            } else {
+
+                ButtonBase button;
+                if ( action.getClass().getAnnotation(ActionCheck.class) != null ) {
+                    button = createToggleButton(action, textBehavior);
+                } else {
+                    button = createButton(action, textBehavior);
+                }
+                button.setFocusTraversable(false);
+                toolbar.getItems().add(button);
+            }
+        }
+
+        return toolbar;
+    }
+
+    /**
+     * Takes the provided {@link Collection} of {@link Action} (or subclasses,
+     * such as {@link ActionGroup}) instances and returns a {@link MenuBar}
+     * populated with appropriate {@link Node nodes} bound to the provided
+     * {@link Action actions}.
+     *
+     * @param actions The {@link Action actions} to place on the {@link MenuBar}.
+     * @return A {@link MenuBar} that contains {@link Node nodes} which are bound
+     *      to the state of the provided {@link Action}
+     */
+    public static MenuBar createMenuBar(Collection<? extends Action> actions) {
+        return updateMenuBar(new MenuBar(), actions);
+    }
+
+    /**
+     * Takes the provided {@link Collection} of {@link Action} (or subclasses,
+     * such as {@link ActionGroup}) instances and updates a {@link MenuBar}
+     * populated with appropriate {@link Node nodes} bound to the provided
+     * {@link Action actions}. Previous MenuBar content is removed.
+     *
+     * @param menuBar The {@link MenuBar menuBar} to update
+     * @param actions The {@link Action actions} to place on the {@link MenuBar}.
+     * @return A {@link MenuBar} that contains {@link Node nodes} which are bound
+     *      to the state of the provided {@link Action}
+     */
+    public static MenuBar updateMenuBar( MenuBar menuBar, Collection<? extends Action> actions) {
+        menuBar.getMenus().clear();
+        for (Action action : actions) {
+
+            if ( action == ACTION_SEPARATOR || action == ACTION_SPAN ) continue;
+
+            Menu menu = createMenu( action );
+
+            if ( action instanceof ActionGroup ) {
+                menu.getItems().addAll( toMenuItems( ((ActionGroup)action).getActions()));
+            } else if ( action == null ) {
+                //no-op
+            }
+
+            menuBar.getMenus().add(menu);
+        }
+
+        return menuBar;
+    }
+
+    /**
+     * Takes the provided {@link Collection} of {@link Action} (or subclasses,
+     * such as {@link ActionGroup}) instances and returns a {@link ButtonBar}
+     * populated with appropriate {@link Node nodes} bound to the provided
+     * {@link Action actions}.
+     *
+     * @param actions The {@link Action actions} to place on the {@link ButtonBar}.
+     * @return A {@link ButtonBar} that contains {@link Node nodes} which are bound
+     *      to the state of the provided {@link Action}
+     */
+    public static ButtonBar createButtonBar(Collection<? extends Action> actions) {
+        return updateButtonBar( new ButtonBar(), actions);
+    }
+
+    /**
+     * Takes the provided {@link Collection} of {@link Action} (or subclasses,
+     * such as {@link ActionGroup}) instances and updates a {@link ButtonBar}
+     * populated with appropriate {@link Node nodes} bound to the provided
+     * {@link Action actions}. Previous content of button bar is removed
+     *
+     * @param buttonBar The {@link ButtonBar buttonBar} to update
+     * @param actions The {@link Action actions} to place on the {@link ButtonBar}.
+     * @return A {@link ButtonBar} that contains {@link Node nodes} which are bound
+     *      to the state of the provided {@link Action}
+     */
+    public static ButtonBar updateButtonBar( ButtonBar buttonBar, Collection<? extends Action> actions) {
+        buttonBar.getButtons().clear();
+        for (Action action : actions) {
+            if ( action instanceof ActionGroup ) {
+                // no-op
+            } else if ( action == ACTION_SPAN || action == ACTION_SEPARATOR || action == null ) {
+                // no-op
+            } else {
+                buttonBar.getButtons().add(createButton(action, ActionTextBehavior.SHOW));
+            }
+        }
+
+        return buttonBar;
+    }
+
+    /**
+     * Takes the provided {@link Collection} of {@link Action} (or subclasses,
+     * such as {@link ActionGroup}) instances and returns a {@link ContextMenu}
+     * populated with appropriate {@link Node nodes} bound to the provided
+     * {@link Action actions}.
+     *
+     * @param actions The {@link Action actions} to place on the {@link ContextMenu}.
+     * @return A {@link ContextMenu} that contains {@link Node nodes} which are bound
+     *      to the state of the provided {@link Action}
+     */
+    public static ContextMenu createContextMenu(Collection<? extends Action> actions) {
+        return updateContextMenu(new ContextMenu(), actions);
+    }
+
+    /**
+     * Takes the provided {@link Collection} of {@link Action} (or subclasses,
+     * such as {@link ActionGroup}) instances and updates a {@link ContextMenu}
+     * populated with appropriate {@link Node nodes} bound to the provided
+     * {@link Action actions}. Previous content of context menu is removed
+     *
+     * @param menu The {@link ContextMenu menu} to update
+     * @param actions The {@link Action actions} to place on the {@link ContextMenu}.
+     * @return A {@link ContextMenu} that contains {@link Node nodes} which are bound
+     *      to the state of the provided {@link Action}
+     */
+    public static ContextMenu updateContextMenu(ContextMenu menu, Collection<? extends Action> actions) {
+        menu.getItems().clear();
+        menu.getItems().addAll(toMenuItems(actions));
+        return menu;
+    }
+
+
+    /***************************************************************************
+     *                                                                         *
+     * Private implementation                                                  *
+     *                                                                         *
+     **************************************************************************/
+
+    private static Collection<MenuItem> toMenuItems( Collection<? extends Action> actions ) {
+
+        Collection<MenuItem> items = new ArrayList<>();
+
+        for (Action action : actions) {
+
+            if ( action instanceof ActionGroup ) {
+
+                Menu menu = createMenu( action );
+                menu.getItems().addAll( toMenuItems( ((ActionGroup)action).getActions()));
+                items.add(menu);
+
+            } else if ( action == ACTION_SEPARATOR ) {
+
+                items.add( new SeparatorMenuItem());
+
+            } else if ( action == null || action == ACTION_SPAN) {
+                // no-op
+            } else {
+
+                items.add( createMenuItem(action));
+
+            }
+
+        }
+
+        return items;
+
+    }
+
+    private static Node copyNode( Node node ) {
+        if ( node instanceof ImageView ) {
+            return new ImageView( ((ImageView)node).getImage());
+        } else if ( node instanceof Duplicatable<?> ) {
+            return (Node) ((Duplicatable<?>)node).duplicate();
+        } else {
+            return null;
+        }
+    }
+
+    // Carry over action style classes changes to the styleable
+    // Binding as not a good solution since it wipes out existing styleable classes
+    private static void bindStyle(final Styleable styleable, final Action action ) {
+        styleable.getStyleClass().addAll( action.getStyleClass() );
+        action.getStyleClass().addListener(new ListChangeListener<String>() {
+            @Override
+            public void onChanged(Change<? extends String> c) {
+                while(c.next()) {
+                    if (c.wasRemoved()) {
+                        styleable.getStyleClass().removeAll(c.getRemoved());
+                    }
+                    if (c.wasAdded()) {
+                        styleable.getStyleClass().addAll(c.getAddedSubList());
+                    }
+                }
+            }
+        });
+    }
+
+    private static <T extends ButtonBase> T configure(final T btn, final Action action, final ActionTextBehavior textBehavior) {
+        if (action == null) {
+            throw new NullPointerException("Action can not be null"); //$NON-NLS-1$
+        }
+
+        // button bind to action properties
+
+        bindStyle(btn,action);
+
+        //btn.textProperty().bind(action.textProperty());
+        if ( textBehavior == ActionTextBehavior.SHOW ) {
+            btn.textProperty().bind(action.textProperty());
+        }
+        btn.disableProperty().bind(action.disabledProperty());
+
+
+        btn.graphicProperty().bind(new ObjectBinding<Node>() {
+            { bind(action.graphicProperty()); }
+
+            @Override protected Node computeValue() {
+                return copyNode(action.graphicProperty().get());
+            }
+
+            @Override
+            public void removeListener(InvalidationListener listener) {
+                super.removeListener(listener);
+                unbind(action.graphicProperty());
+            }
+        });
+
+
+        // add all the properties of the action into the button, and set up
+        // a listener so they are always copied across
+        btn.getProperties().putAll(action.getProperties());
+        action.getProperties().addListener(new ButtonPropertiesMapChangeListener<>(btn, action));
+
+        // tooltip requires some special handling (i.e. don't have one when
+        // the text property is null
+        btn.tooltipProperty().bind(new ObjectBinding<Tooltip>() {
+            private Tooltip tooltip = new Tooltip();
+            private StringBinding textBinding = new When(action.longTextProperty().isEmpty()).then(action.textProperty()).otherwise(action.longTextProperty());
+
+            {
+                bind(textBinding);
+                tooltip.textProperty().bind(textBinding);
+            }
+
+            @Override protected Tooltip computeValue() {
+                String longText =  textBinding.get();
+                return longText == null || textBinding.get().isEmpty() ? null : tooltip;
+            }
+
+            @Override
+            public void removeListener(InvalidationListener listener) {
+                super.removeListener(listener);
+                unbind(action.longTextProperty());
+                tooltip.textProperty().unbind();
+            }
+        });
+
+
+
+        // Handle the selected state of the button if it is of the applicable type
+
+        if ( btn instanceof ToggleButton ) {
+            ((ToggleButton)btn).selectedProperty().bindBidirectional(action.selectedProperty());
+        }
+
+        // Just call the execute method on the action itself when the action
+        // event occurs on the button
+        btn.setOnAction(action);
+
+        return btn;
+    }
+
+    private static void unconfigure(final ButtonBase btn) {
+        if (btn == null || !(btn.getOnAction() instanceof Action)) {
+            return;
+        }
+
+        Action action = (Action) btn.getOnAction();
+
+        btn.styleProperty().unbind();
+        btn.textProperty().unbind();
+        btn.disableProperty().unbind();
+        btn.graphicProperty().unbind();
+
+        action.getProperties().removeListener(new ButtonPropertiesMapChangeListener<>(btn, action));
+
+        btn.tooltipProperty().unbind();
+
+        if (btn instanceof ToggleButton) {
+            ((ToggleButton) btn).selectedProperty().unbindBidirectional(action.selectedProperty());
+        }
+
+        btn.setOnAction(null);
+    }
+    
+    private static <T extends MenuItem> T configure(final T menuItem, final Action action) {
+        if (action == null) {
+            throw new NullPointerException("Action can not be null"); //$NON-NLS-1$
+        }
+
+        // button bind to action properties
+        bindStyle(menuItem,action);
+
+        menuItem.textProperty().bind(action.textProperty());
+        menuItem.disableProperty().bind(action.disabledProperty());
+        menuItem.acceleratorProperty().bind(action.acceleratorProperty());
+
+        menuItem.graphicProperty().bind(new ObjectBinding<Node>() {
+            { bind(action.graphicProperty()); }
+
+            @Override protected Node computeValue() {
+                return copyNode( action.graphicProperty().get());
+            }
+
+            @Override
+            public void removeListener(InvalidationListener listener) {
+                super.removeListener(listener);
+                unbind(action.graphicProperty());
+            }
+        });
+
+
+        // add all the properties of the action into the button, and set up
+        // a listener so they are always copied across
+        menuItem.getProperties().putAll(action.getProperties());
+        action.getProperties().addListener(new MenuItemPropertiesMapChangeListener<>(menuItem, action));
+
+        // Handle the selected state of the menu item if it is a
+        // CheckMenuItem or RadioMenuItem
+
+        if ( menuItem instanceof RadioMenuItem ) {
+            ((RadioMenuItem)menuItem).selectedProperty().bindBidirectional(action.selectedProperty());
+        } else if ( menuItem instanceof CheckMenuItem ) {
+            ((CheckMenuItem)menuItem).selectedProperty().bindBidirectional(action.selectedProperty());
+        }
+
+        // Just call the execute method on the action itself when the action
+        // event occurs on the button
+        menuItem.setOnAction(action);
+
+        return menuItem;
+    }
+
+    private static void unconfigure(final MenuItem menuItem) {
+        if (menuItem == null || !(menuItem.getOnAction() instanceof Action)) {
+            return;
+        }
+
+        Action action = (Action) menuItem.getOnAction();
+
+        menuItem.styleProperty().unbind();
+        menuItem.textProperty().unbind();
+        menuItem.disableProperty().unbind();
+        menuItem.acceleratorProperty().unbind();
+        menuItem.graphicProperty().unbind();
+
+        action.getProperties().removeListener(new MenuItemPropertiesMapChangeListener<>(menuItem, action));
+
+        if (menuItem instanceof RadioMenuItem) {
+            ((RadioMenuItem) menuItem).selectedProperty().unbindBidirectional(action.selectedProperty());
+        } else if (menuItem instanceof CheckMenuItem) {
+            ((CheckMenuItem) menuItem).selectedProperty().unbindBidirectional(action.selectedProperty());
+        }
+
+        menuItem.setOnAction(null);
+    }
+
+    private static class ButtonPropertiesMapChangeListener<T extends ButtonBase> implements MapChangeListener<Object, Object> {
+
+        private final WeakReference<T> btnWeakReference;
+        private final Action action;
+
+        private ButtonPropertiesMapChangeListener(T btn, Action action) {
+            btnWeakReference = new WeakReference<>(btn);
+            this.action = action;
+        }
+
+        @Override public void onChanged(MapChangeListener.Change<?, ?> change) {
+            T btn = btnWeakReference.get();
+            if (btn == null) {
+                action.getProperties().removeListener(this);
+            } else {
+                btn.getProperties().clear();
+                btn.getProperties().putAll(action.getProperties());
+            }
+        }
+
+        @Override
+        public boolean equals(Object otherObject) {
+            if (this == otherObject) {
+                return true;
+            }
+            if (otherObject == null || getClass() != otherObject.getClass()) {
+                return false;
+            }
+
+            ButtonPropertiesMapChangeListener<?> otherListener = (ButtonPropertiesMapChangeListener<?>) otherObject;
+
+            T btn = btnWeakReference.get();
+            ButtonBase otherBtn = otherListener.btnWeakReference.get();
+            if (btn != null ? !btn.equals(otherBtn) : otherBtn != null) {
+                return false;
+            }
+            return action.equals(otherListener.action);
+        }
+
+        @Override
+        public int hashCode() {
+            T btn = btnWeakReference.get();
+            int result = btn != null ? btn.hashCode() : 0;
+            result = 31 * result + action.hashCode();
+            return result;
+        }
+    }
+
+    private static class MenuItemPropertiesMapChangeListener<T extends MenuItem> implements MapChangeListener<Object, Object> {
+
+        private final WeakReference<T> menuItemWeakReference;
+        private final Action action;
+
+        private MenuItemPropertiesMapChangeListener(T menuItem, Action action) {
+            menuItemWeakReference = new WeakReference<>(menuItem);
+            this.action = action;
+        }
+
+        @Override public void onChanged(MapChangeListener.Change<?, ?> change) {
+            T menuItem = menuItemWeakReference.get();
+            if (menuItem == null) {
+                action.getProperties().removeListener(this);
+            } else {
+                menuItem.getProperties().clear();
+                menuItem.getProperties().putAll(action.getProperties());
+            }
+        }
+
+        @Override
+        public boolean equals(Object otherObject) {
+            if (this == otherObject) {
+                return true;
+            }
+            if (otherObject == null || getClass() != otherObject.getClass()) {
+                return false;
+            }
+
+            MenuItemPropertiesMapChangeListener<?> otherListener = (MenuItemPropertiesMapChangeListener<?>) otherObject;
+
+            T menuItem = menuItemWeakReference.get();
+            MenuItem otherMenuItem = otherListener.menuItemWeakReference.get();
+            return menuItem != null ? menuItem.equals(otherMenuItem) : otherMenuItem == null && action.equals(otherListener.action);
+
+        }
+
+        @Override
+        public int hashCode() {
+            T menuItem = menuItemWeakReference.get();
+            int result = menuItem != null ? menuItem.hashCode() : 0;
+            result = 31 * result + action.hashCode();
+            return result;
+        }
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/action/AnnotatedAction.java b/controlsfx/src/main/java/org/controlsfx/control/action/AnnotatedAction.java
new file mode 100644
index 0000000..b667cda
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/action/AnnotatedAction.java
@@ -0,0 +1,115 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.action;
+
+import java.lang.ref.WeakReference;
+import java.lang.reflect.Method;
+import java.util.Objects;
+import javafx.event.ActionEvent;
+
+
+
+/**
+ * An action that invokes a method that has been annotated with {@link ActionProxy}. These actions are created via
+ * {@link ActionMap#register(java.lang.Object)}, which delegates the actual instantiation to an {@link AnnotatedActionFactory}.
+ * 
+ * Note that this class maintains a WeakReference to the supplied target object, so the existence of an 
+ * AnnotatedAction instance will not prevent the target from being garbage-collected.
+ */
+public class AnnotatedAction extends Action {
+
+    private final Method method;
+    private final WeakReference<Object> target;
+
+    /**
+     * Instantiates an action that will call the specified method on the specified target.
+     */
+    public AnnotatedAction(String text, Method method, Object target) {
+        super(text);
+        Objects.requireNonNull( method );
+        Objects.requireNonNull( target );
+        
+        setEventHandler(this::handleAction);
+        
+        this.method = method;
+        this.method.setAccessible(true);
+        this.target = new WeakReference( target );
+    }
+    
+    /**
+     * Returns the target object (the object on which the annotated method will be called).
+     * 
+     * @return The target object, or null if the target object has been garbage-collected.
+     */
+    public Object getTarget() {
+        return target.get();
+    }
+
+    /**
+     * Handle the action-event by invoking the annotated method on the target object. If an exception is
+     * thrown, then the default implementation of this method will call handleActionException().
+     */
+    protected void handleAction(ActionEvent ae) {
+        try {
+            Object actionTarget = getTarget();
+            if (actionTarget == null) {
+                throw new IllegalStateException( "Action target object is no longer reachable" );
+            }
+            
+            int paramCount =  method.getParameterCount(); 
+            if ( paramCount == 0 ) {
+                method.invoke(actionTarget);
+                
+            } else if ( paramCount == 1) {
+                method.invoke(actionTarget, ae);
+                
+            } else if ( paramCount == 2) {
+                method.invoke(actionTarget, ae, this);
+            }
+        } catch (Throwable e) {
+            handleActionException( ae, e );
+        }
+    }
+    
+    
+    /**
+     * Called if the annotated method throws an exception when invoked. The default implementation of this method simply prints
+     * the stack trace of the specified exception.
+     */
+    protected void handleActionException( ActionEvent ae, Throwable ex ) {
+        ex.printStackTrace();
+    }
+    
+    
+    /**
+     * Overridden to return the text of this action.
+     */
+    @Override
+    public String toString() {
+        return getText();
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/action/AnnotatedActionFactory.java b/controlsfx/src/main/java/org/controlsfx/control/action/AnnotatedActionFactory.java
new file mode 100644
index 0000000..9c083de
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/action/AnnotatedActionFactory.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.action;
+
+import java.lang.reflect.Method;
+
+
+/**
+ * Defines the interface used by {@link ActionMap} for creating instances of {@link AnnotatedAction}.
+ */
+public interface AnnotatedActionFactory {
+
+    /**
+     * Create an {@link AnnotatedAction} instance.
+     * 
+     * @param annotation The annotation specified on the method.
+     * @param method The method to be invoked when an action is fired.
+     * @param target The target object on which the method will be invoked.
+     * @return An {@link AnnotatedAction} instance.
+     */
+    AnnotatedAction createAction( ActionProxy annotation, Method method, Object target );
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/action/AnnotatedCheckAction.java b/controlsfx/src/main/java/org/controlsfx/control/action/AnnotatedCheckAction.java
new file mode 100644
index 0000000..5b5dfd3
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/action/AnnotatedCheckAction.java
@@ -0,0 +1,19 @@
+package org.controlsfx.control.action;
+
+import java.lang.reflect.Method;
+
+ at ActionCheck
+public class AnnotatedCheckAction extends AnnotatedAction {
+
+    /**
+     * Instantiates an action that will call the specified method on the specified target.
+     * This action is marked  with @ActionCheck
+     *
+     * @param text
+     * @param method
+     * @param target
+     */
+    public AnnotatedCheckAction(String text, Method method, Object target) {
+        super(text, method, target);
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/action/DefaultActionFactory.java b/controlsfx/src/main/java/org/controlsfx/control/action/DefaultActionFactory.java
new file mode 100644
index 0000000..3dc3d31
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/action/DefaultActionFactory.java
@@ -0,0 +1,114 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.action;
+
+import javafx.scene.Node;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.input.KeyCombination;
+import org.controlsfx.glyphfont.Glyph;
+
+import java.lang.reflect.Method;
+
+/**
+ * The default {@link AnnotatedActionFactory} to be used when no alternative has been specified. This class creates
+ * instances of {@link AnnotatedAction}.
+ */
+public class DefaultActionFactory implements AnnotatedActionFactory {
+
+    /**
+     * Create an {@link AnnotatedAction}. This method is called by {@link ActionMap#register(java.lang.Object)}.
+     * 
+     * @param annotation The annotation specified on the method.
+     * @param method The method to be invoked when an action is fired.
+     * @param target The target object on which the method will be invoked.
+     * @return An {@link AnnotatedAction} instance.
+     */
+    @Override
+    public AnnotatedAction createAction( ActionProxy annotation, Method method, Object target ) {
+        AnnotatedAction action;
+        if ( method.isAnnotationPresent(ActionCheck.class)) {
+            action = new AnnotatedCheckAction(annotation.text(), method, target);
+        } else {
+            action = new AnnotatedAction(annotation.text(), method, target);
+        }
+        
+        configureAction( annotation, action );
+        
+        return action;
+    }
+    
+    
+    /**
+     * Configures the newly-created action before it is returned to {@link ActionMap}. Subclasses can override this method
+     * to change configuration behavior.
+     * 
+     * @param annotation The annotation specified on the method.
+     * @param action The newly-created action.
+     */
+    protected void configureAction( ActionProxy annotation, AnnotatedAction action ) {
+        Node graphic = resolveGraphic(annotation);
+        action.setGraphic(graphic);
+        
+        // set long text / tooltip
+        String longText = annotation.longText().trim();
+        if ( graphic != null ) {
+            action.setLongText(longText);
+        }
+        
+        // set accelerator
+        String acceleratorText = annotation.accelerator().trim();
+        if (!acceleratorText.isEmpty()) {
+            action.setAccelerator(KeyCombination.keyCombination(acceleratorText));
+        }
+
+    }
+    
+    
+    /**
+     * Resolve the graphical representation of this action. The default implementation of this method implements the protocol described
+     * in {@link ActionProxy#graphic()}, but subclasses can override this method to provide alternative behavior.
+     * 
+     * @param annotation The annotation specified on the method.
+     * @return A JavaFX Node for the graphic associated with this action.
+     */
+    protected Node resolveGraphic( ActionProxy annotation ) {
+        String graphicDef = annotation.graphic().trim();
+        if ( !graphicDef.isEmpty()) {
+            
+            String[] def = graphicDef.split("\\>");  // cannot use ':' because it used in urls //$NON-NLS-1$
+            if ( def.length == 1 ) return new ImageView(new Image(def[0]));
+            switch (def[0]) {
+               case "font"    : return Glyph.create(def[1]);   //$NON-NLS-1$
+               case "image"   : return new ImageView(new Image(def[1])); //$NON-NLS-1$
+               default: throw new IllegalArgumentException( String.format("Unknown ActionProxy graphic protocol: %s", def[0])); //$NON-NLS-1$
+            }
+        }
+        return null;
+    }
+    
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/action/package-info.java b/controlsfx/src/main/java/org/controlsfx/control/action/package-info.java
new file mode 100644
index 0000000..bcb828d
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/action/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * A package containing the {@link org.controlsfx.control.action.Action} API, as well
+ * as the {@link org.controlsfx.control.action.Action} convenience subclass. 
+ * Refer to these two classes for the necessary details on what actions are in
+ * the JavaFX context.
+ */
+package org.controlsfx.control.action;
\ No newline at end of file
diff --git a/controlsfx/src/main/java/org/controlsfx/control/cell/ColorGridCell.java b/controlsfx/src/main/java/org/controlsfx/control/cell/ColorGridCell.java
new file mode 100644
index 0000000..7317ccf
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/cell/ColorGridCell.java
@@ -0,0 +1,82 @@
+/**
+ * Copyright (c) 2013, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.cell;
+
+import javafx.scene.control.ContentDisplay;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Rectangle;
+
+import org.controlsfx.control.GridCell;
+import org.controlsfx.control.GridView;
+
+/**
+ * A {@link GridCell} that can be used to show coloured rectangles inside the 
+ * {@link GridView} control.
+ *
+ * @see GridView
+ */
+public class ColorGridCell extends GridCell<Color> {
+	
+	private Rectangle colorRect;
+	
+	private static final boolean debug = false;
+
+	/**
+	 * Creates a default ColorGridCell instance.
+	 */
+    public ColorGridCell() {
+		getStyleClass().add("color-grid-cell"); //$NON-NLS-1$
+		
+		colorRect = new Rectangle();
+		colorRect.setStroke(Color.BLACK);
+		colorRect.heightProperty().bind(heightProperty());
+		colorRect.widthProperty().bind(widthProperty());   
+		setGraphic(colorRect);
+		
+		if (debug) {
+		    setContentDisplay(ContentDisplay.TEXT_ONLY);
+		}
+	}
+	
+    /**
+     * {@inheritDoc}
+     */
+	@Override protected void updateItem(Color item, boolean empty) {
+	    super.updateItem(item, empty);
+	    
+	    if (empty) {
+	        setGraphic(null);
+	    } else {
+	        colorRect.setFill(item);
+	        setGraphic(colorRect);
+	    }
+	    
+	    if (debug) {
+	        setText(getIndex() + ""); //$NON-NLS-1$
+	    }
+	}
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/cell/ImageGridCell.java b/controlsfx/src/main/java/org/controlsfx/control/cell/ImageGridCell.java
new file mode 100644
index 0000000..7703f30
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/cell/ImageGridCell.java
@@ -0,0 +1,85 @@
+/**
+ * Copyright (c) 2013, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.cell;
+
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+
+import org.controlsfx.control.GridCell;
+import org.controlsfx.control.GridView;
+
+/**
+ * A {@link GridCell} that can be used to show images inside the 
+ * {@link GridView} control.
+ *
+ * @see GridView
+ */
+public class ImageGridCell extends GridCell<Image> {
+	
+    private final ImageView imageView;
+    
+    private final boolean preserveImageProperties;
+    
+    
+    /**
+     * Creates a default ImageGridCell instance, which will preserve image properties
+     */
+    public ImageGridCell() {
+        this(true);
+    }
+    
+    /**
+     * Create ImageGridCell instance
+     * @param preserveImageProperties if set to true will preserve image aspect ratio and smoothness
+     */
+	public ImageGridCell( boolean preserveImageProperties ) {
+		getStyleClass().add("image-grid-cell"); //$NON-NLS-1$
+		
+		this.preserveImageProperties = preserveImageProperties;
+		imageView = new ImageView();
+        imageView.fitHeightProperty().bind(heightProperty());
+        imageView.fitWidthProperty().bind(widthProperty());
+	}
+	
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override protected void updateItem(Image item, boolean empty) {
+	    super.updateItem(item, empty);
+	    
+	    if (empty) {
+	        setGraphic(null);
+	    } else {
+	        if (preserveImageProperties) {
+    	        imageView.setPreserveRatio(item.isPreserveRatio());
+    	        imageView.setSmooth( item.isSmooth());
+	        }
+	        imageView.setImage(item);
+	        setGraphic(imageView);
+	    }
+	}
+}
\ No newline at end of file
diff --git a/controlsfx/src/main/java/org/controlsfx/control/cell/MediaImageCell.java b/controlsfx/src/main/java/org/controlsfx/control/cell/MediaImageCell.java
new file mode 100644
index 0000000..4564872
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/cell/MediaImageCell.java
@@ -0,0 +1,106 @@
+/**
+ * Copyright (c) 2013, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.cell;
+
+import javafx.scene.media.Media;
+import javafx.scene.media.MediaPlayer;
+import javafx.scene.media.MediaView;
+
+import org.controlsfx.control.GridCell;
+import org.controlsfx.control.GridView;
+
+/**
+ * A {@link GridCell} that can be used to show media (i.e. movies) inside the 
+ * {@link GridView} control.
+ *
+ * @see GridView
+ */
+public class MediaImageCell extends GridCell<Media> {
+	
+	private MediaPlayer mediaPlayer;
+	private final MediaView mediaView;
+	
+	/**
+	 * Creates a default MediaGridCell instance.
+	 */
+	public MediaImageCell() {
+		getStyleClass().add("media-grid-cell"); //$NON-NLS-1$
+		
+		mediaView = new MediaView();
+		mediaView.setMediaPlayer(mediaPlayer);
+        mediaView.fitHeightProperty().bind(heightProperty());
+        mediaView.fitWidthProperty().bind(widthProperty());
+        mediaView.setMediaPlayer(mediaPlayer);
+	}
+	
+	/**
+	 * Pauses the media player inside this cell.
+	 */
+	public void pause() {
+		if(mediaPlayer != null) {
+			mediaPlayer.pause();
+		}
+	}
+	
+	/**
+     * Starts playing the media player inside this cell.
+     */
+	public void play() {
+		if(mediaPlayer != null) {
+			mediaPlayer.play();
+		}
+	}
+	
+	/**
+     * Stops playing the media player inside this cell.
+     */
+	public void stop() {
+		if(mediaPlayer != null) {
+			mediaPlayer.stop();
+		}
+	}
+	
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override protected void updateItem(Media item, boolean empty) {
+	    super.updateItem(item, empty);
+	    
+	    getChildren().clear();
+        if (mediaPlayer != null) {
+            mediaPlayer.stop();
+        }
+	    
+	    if (empty) {
+	        setGraphic(null);
+	    } else {
+	        mediaPlayer = new MediaPlayer(item);
+	        mediaView.setMediaPlayer(mediaPlayer);
+	        setGraphic(mediaView);
+	    }
+	}
+}
\ No newline at end of file
diff --git a/controlsfx/src/main/java/org/controlsfx/control/cell/package-info.java b/controlsfx/src/main/java/org/controlsfx/control/cell/package-info.java
new file mode 100644
index 0000000..4305006
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/cell/package-info.java
@@ -0,0 +1,9 @@
+/**
+ * A package containing a number of useful cell-related classes that do not
+ * exist in the base JavaFX distribution, many related to the new 
+ * {@link org.controlsfx.control.GridView GridView} control offered in 
+ * ControlsFX.
+ * 
+ * @see org.controlsfx.control.GridView
+ */
+package org.controlsfx.control.cell;
\ No newline at end of file
diff --git a/controlsfx/src/main/java/org/controlsfx/control/decoration/Decoration.java b/controlsfx/src/main/java/org/controlsfx/control/decoration/Decoration.java
new file mode 100644
index 0000000..d5ff986
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/decoration/Decoration.java
@@ -0,0 +1,92 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.decoration;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javafx.scene.Node;
+
+/**
+ * Decoration is an abstract class used by the ControlsFX {@link Decorator} class
+ * for adding and removing decorations on a node. ControlsFX
+ * ships with pre-built decorations, including {@link GraphicDecoration} and
+ * {@link StyleClassDecoration}.
+ * 
+ * <p>To better understand how to use the ControlsFX decoration API in your 
+ * application, refer to the code samples and explanations in {@link Decorator}.
+ * 
+ * @see Decorator
+ * @see GraphicDecoration
+ * @see StyleClassDecoration
+ */
+public abstract class Decoration {
+    
+    private volatile Map<String,Object> properties;
+    
+    /**
+     * Instantiates a default Decoration instance (obviously only callable by
+     * subclasses).
+     */
+    protected Decoration() {
+        // no-op
+    }
+    
+	/**
+     * This method decorates the given 
+     * target node with the relevant decorations, returning any 'decoration node' 
+     * that needs to be added to the scenegraph (although this can be null). When
+     * the returned Node is null, this indicates that the decoration will be 
+     * handled internally by the decoration (which is preferred, as the default
+     * implementation is not ideal in most circumstances).
+     * 
+     * <p>When the boolean parameter is false, this method removes the decoration 
+     * from the given target node, always returning null.
+     * 
+     * @param targetNode The node to decorate.
+     * @return The decoration, but null is a valid return value.
+     */
+    public abstract Node applyDecoration(Node targetNode);
+    
+    /**
+     * This method removes the decoration from the given target node.
+     * 
+     * @param targetNode The node to undecorate.
+     */
+    public abstract void removeDecoration(Node targetNode);
+    
+    /**
+     * Custom decoration properties
+     * @return decoration properties
+     */
+    public synchronized final Map<String,Object> getProperties() {
+        if (properties == null) {
+            properties = new HashMap<>();
+        }
+    	return properties;
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/decoration/Decorator.java b/controlsfx/src/main/java/org/controlsfx/control/decoration/Decorator.java
new file mode 100644
index 0000000..0743593
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/decoration/Decorator.java
@@ -0,0 +1,241 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.decoration;
+
+import impl.org.controlsfx.ImplUtils;
+import impl.org.controlsfx.skin.DecorationPane;
+
+import java.util.*;
+import java.util.function.Consumer;
+
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.scene.Node;
+import javafx.scene.Parent;
+import javafx.scene.Scene;
+import javafx.scene.control.TextField;
+
+/**
+ * The Decorator class is responsible for accessing decorations for a given node.
+ * Through this class you may therefore add and remove decorations as desired.
+ * 
+ * <h3>Code Example</h3>
+ * <p>Say you have a {@link TextField} that you want to decorate. You would simply
+ * do the following:
+ * 
+ * <pre>
+ * {@code 
+ * TextField textfield = new TextField();
+ * Node decoration = ... // could be an ImageView or any Node!
+ * Decorator.addDecoration(textfield, new GraphicDecoration(decoration, Pos.CENTER_RIGHT));}
+ * </pre>
+ * 
+ * <p>Similarly, if we wanted to add a CSS style class (e.g. because we have some 
+ * css that knows to make the 'warning' style class turn the TextField a lovely
+ * shade of bright red, we would simply do the following:
+ * 
+ * <pre>
+ * {@code 
+ * TextField textfield = new TextField();
+ * Decorator.addDecoration(textfield, new StyleClassDecoration("warning");}
+ * </pre>
+ * 
+ * @see Decoration
+ */
+public class Decorator {
+    
+    
+    /***************************************************************************
+     *                                                                         *
+     * Static fields                                                           *
+     *                                                                         *
+     **************************************************************************/
+
+    private final static String DECORATIONS_PROPERTY_KEY = "$org.controlsfx.decorations$"; //$NON-NLS-1$
+
+    
+    
+    /***************************************************************************
+     *                                                                         *
+     * Constructors                                                            *
+     *                                                                         *
+     **************************************************************************/
+    
+    private Decorator() {
+        // no op
+    }
+    
+    
+    
+    /***************************************************************************
+     *                                                                         *
+     * Static API                                                              *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * Adds the given decoration to the given node.
+     * @param target The node to add the decoration to.
+     * @param decoration The decoration to add to the node.
+     */
+    public static final void addDecoration(Node target, Decoration decoration) {
+        getDecorations(target, true).add(decoration);
+        updateDecorationsOnNode(target, FXCollections.observableArrayList(decoration), null);
+    }
+
+    /**
+     * Removes the given decoration from the given node.
+     * @param target The node to remove the decoration from.
+     * @param decoration The decoration to remove from the node.
+     */
+    public static final void removeDecoration(Node target, Decoration decoration) {
+        getDecorations(target, true).remove(decoration);
+        updateDecorationsOnNode(target, null, FXCollections.observableArrayList(decoration));
+    }
+    
+    /**
+     * Removes all the decorations that have previously been set on the given node.
+     * @param target The node from which all previously set decorations should be removed.
+     */
+    public static final void removeAllDecorations(Node target) {
+        List<Decoration> decorations = getDecorations(target, true);
+        List<Decoration> removed = FXCollections.observableArrayList(decorations);
+        
+        target.getProperties().remove(DECORATIONS_PROPERTY_KEY);
+        
+        updateDecorationsOnNode(target, null, removed);
+    }
+    
+    /**
+     * Returns all the currently set decorations for the given node.
+     * @param target The node for which all currently set decorations are required.
+     * @return An ObservableList of the currently set decorations for the given node.
+     */
+    public static final ObservableList<Decoration> getDecorations(Node target) {
+        return getDecorations(target, false);
+    }
+
+    
+    
+    /***************************************************************************
+     *                                                                         *
+     * Implementation                                                          *
+     *                                                                         *
+     **************************************************************************/
+    
+    private static final ObservableList<Decoration> getDecorations(Node target, boolean createIfAbsent) {
+        @SuppressWarnings("unchecked")
+        ObservableList<Decoration> decorations = (ObservableList<Decoration>) target.getProperties().get(DECORATIONS_PROPERTY_KEY);
+        if (decorations == null && createIfAbsent) {
+            decorations = FXCollections.observableArrayList();
+            target.getProperties().put(DECORATIONS_PROPERTY_KEY, decorations);
+        }
+        return decorations;
+    }
+    
+    private static void updateDecorationsOnNode(Node target, List<Decoration> added, List<Decoration> removed) {
+        // find a DecorationPane parent and notify it that a node has updated
+        // decorations
+        getDecorationPane(target, (pane) -> pane.updateDecorationsOnNode(target, added, removed));
+    }
+    
+    private static List<Scene> currentlyInstallingScenes = new ArrayList<>();
+    private static Map<Scene, List<Consumer<DecorationPane>>> pendingTasksByScene = new HashMap<>();
+    
+    private static void getDecorationPane(Node target, Consumer<DecorationPane> task) {
+        // find a DecorationPane parent and notify it that a node has updated
+        // decorations. If a DecorationPane doesn't exist, we install it into
+        // the scene. If a Scene does not exist, we add a listener to try again
+        // when a scene is available.
+        
+        DecorationPane pane = getDecorationPaneInParentHierarchy(target);
+        
+        if (pane != null) {
+            task.accept(pane);
+        } else {
+            // install decoration pane
+            final Consumer<Scene> sceneConsumer = scene -> {
+                if (currentlyInstallingScenes.contains(scene)) {
+                    List<Consumer<DecorationPane>> pendingTasks = pendingTasksByScene.get(scene);
+                    if (pendingTasks == null) {
+                        pendingTasks = new LinkedList<>();
+                        pendingTasksByScene.put(scene, pendingTasks);
+                    }
+                    pendingTasks.add(task);
+                    return;
+                }
+                
+                DecorationPane _pane = getDecorationPaneInParentHierarchy(target);
+                if (_pane == null) {
+                    currentlyInstallingScenes.add(scene);
+                    _pane = new DecorationPane();
+                    Node oldRoot = scene.getRoot();
+                    ImplUtils.injectAsRootPane(scene, _pane, true);
+                    _pane.setRoot(oldRoot);
+                    currentlyInstallingScenes.remove(scene);
+                }
+                
+                task.accept(_pane);
+                final List<Consumer<DecorationPane>> pendingTasks = pendingTasksByScene.remove(scene);
+                if (pendingTasks != null) {
+                    for (Consumer<DecorationPane> pendingTask : pendingTasks) {
+                        pendingTask.accept(_pane);
+                    }
+                }
+            };
+            
+            Scene scene = target.getScene();
+            if (scene != null) {
+                sceneConsumer.accept(scene);
+            } else {
+                // install listener to try again later
+                InvalidationListener sceneListener = new InvalidationListener() {
+                    @Override public void invalidated(Observable o) {
+                        if (target.getScene() != null) {
+                            target.sceneProperty().removeListener(this);
+                            sceneConsumer.accept(target.getScene());
+                        }
+                    }
+                };
+                target.sceneProperty().addListener(sceneListener);
+            }
+        }
+    }
+    
+    private static DecorationPane getDecorationPaneInParentHierarchy(Node target) {
+        Parent p = target.getParent();
+        while (p != null) {
+            if (p instanceof DecorationPane) {
+                return (DecorationPane) p;
+            }
+            p = p.getParent();
+        }
+        return null;
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/decoration/GraphicDecoration.java b/controlsfx/src/main/java/org/controlsfx/control/decoration/GraphicDecoration.java
new file mode 100644
index 0000000..ca521ad
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/decoration/GraphicDecoration.java
@@ -0,0 +1,193 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.decoration;
+
+import impl.org.controlsfx.ImplUtils;
+
+import java.util.List;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+
+import javafx.geometry.Bounds;
+import javafx.geometry.Pos;
+import javafx.scene.Node;
+import javafx.scene.Parent;
+import javafx.scene.image.ImageView;
+
+/**
+ * GraphicDecoration is a {@link Decoration} designed to show a graphic (be it
+ * an image loaded via an {@link ImageView} or an arbitrarily complex 
+ * scenegraph in its own right) on top of a given node. GraphicDecoration is
+ * applied as part of the ControlsFX {@link Decorator} API - refer to the 
+ * {@link Decorator} javadoc for more details.
+ * 
+ * @see Decoration
+ * @see Decorator
+ */
+public class GraphicDecoration extends Decoration {
+
+    private final Node decorationNode;
+    private final Pos pos;
+    private final double xOffset;
+    private final double yOffset;
+
+    /**
+     * Constructs a new GraphicDecoration with the given decoration node to be 
+     * applied to any node that has this decoration applied to it. By default
+     * the decoration node will be applied in the top-left corner of the node.
+     * 
+     * @param decorationNode The decoration node to apply to any node that has this 
+     *      decoration applied to it 
+     */
+    public GraphicDecoration(Node decorationNode) {
+        this(decorationNode, Pos.TOP_LEFT);
+    }
+    
+    /**
+     * Constructs a new GraphicDecoration with the given decoration node to be 
+     * applied to any node that has this decoration applied to it, in the location
+     * provided by the {@link Pos position} argument.
+     * 
+     * @param decorationNode The decoration node to apply to any node that has this 
+     *      decoration applied to it 
+     * @param position The location to position the decoration node relative to the 
+     *      node that is being decorated.
+     */
+    public GraphicDecoration(Node decorationNode, Pos position) {
+        this(decorationNode, position, 0, 0);
+    }
+    
+    /**
+     * Constructs a new GraphicDecoration with the given decoration node to be 
+     * applied to any node that has this decoration applied to it, in the location
+     * provided by the {@link Pos position} argument, with the given xOffset and
+     * yOffset values used to adjust the position.
+     * 
+     * @param decorationNode The decoration node to apply to any node that has this 
+     *      decoration applied to it 
+     * @param position The location to position the decoration node relative to the 
+     *      node that is being decorated.
+     * @param xOffset The amount of movement to apply to the decoration node in the
+     *      x direction (i.e. left and right).
+     * @param yOffset The amount of movement to apply to the decoration node in the
+     *      y direction (i.e. up and down). 
+     */
+    public GraphicDecoration(Node decorationNode, Pos position, double xOffset, double yOffset) {
+        this.decorationNode = decorationNode;
+        this.decorationNode.setManaged(false);
+        this.pos = position;
+        this.xOffset = xOffset;
+        this.yOffset = yOffset;
+    }
+    
+    /** {@inheritDoc} */
+    @Override public Node applyDecoration(Node targetNode) {
+        List<Node> targetNodeChildren = ImplUtils.getChildren((Parent)targetNode, true);
+        updateGraphicPosition(targetNode);
+        if (!targetNodeChildren.contains(decorationNode)) {
+            targetNodeChildren.add(decorationNode);
+        }
+        return null;
+    }
+    
+    /** {@inheritDoc} */
+    @Override public void removeDecoration(Node targetNode) {
+        List<Node> targetNodeChildren = ImplUtils.getChildren((Parent)targetNode, true);
+        
+        if (targetNodeChildren.contains(decorationNode)) {
+            targetNodeChildren.remove(decorationNode);
+        }
+    }
+    
+    private void updateGraphicPosition(Node targetNode) {
+        final double decorationNodeWidth = decorationNode.prefWidth(-1);
+        final double decorationNodeHeight = decorationNode.prefHeight(-1);
+        
+        Bounds targetBounds = targetNode.getLayoutBounds();
+        double x = targetBounds.getMinX();
+        double y = targetBounds.getMinY();
+
+        double targetWidth = targetBounds.getWidth();
+        if (targetWidth <= 0) {
+            targetWidth = targetNode.prefWidth(-1);
+        }
+        
+        double targetHeight = targetBounds.getHeight();
+        if (targetHeight <= 0) {
+            targetHeight = targetNode.prefHeight(-1);
+        }
+
+        /**
+         * If both targetWidth and targetHeight are equal to 0, this means the
+         * targetNode has not been laid out so we can put a listener in order to
+         * catch when the layout will be updated, and then we will place our
+         * decorationNode to the proper position.
+         */
+        if (targetWidth <= 0 && targetHeight <= 0) {
+            targetNode.layoutBoundsProperty().addListener(new ChangeListener<Bounds>() {
+
+                @Override
+                public void changed(ObservableValue<? extends Bounds> observable, Bounds oldValue, Bounds newValue) {
+                    targetNode.layoutBoundsProperty().removeListener(this);
+                    updateGraphicPosition(targetNode);
+                }
+            });
+        }
+        
+        // x
+        switch (pos.getHpos()) {
+        	case CENTER: 
+        		x += targetWidth/2 - decorationNodeWidth / 2.0;
+        		break;
+        	case LEFT: 
+        		x -= decorationNodeWidth / 2.0;
+        		break;
+        	case RIGHT:
+        		x += targetWidth - decorationNodeWidth / 2.0;
+        		break;
+        }
+        
+        // y
+        switch (pos.getVpos()) {
+        	case CENTER: 
+        		y += targetHeight/2 - decorationNodeHeight / 2.0;
+        		break;
+        	case TOP: 
+        		y -= decorationNodeHeight / 2.0;
+        		break;
+        	case BOTTOM:
+        		y += targetHeight - decorationNodeWidth / 2.0;
+        		break;
+        	case BASELINE: 
+        		y += targetNode.getBaselineOffset() - decorationNode.getBaselineOffset() - decorationNodeHeight / 2.0;
+        		break;
+        }
+        
+        decorationNode.setLayoutX(x + xOffset);
+        decorationNode.setLayoutY(y + yOffset);
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/decoration/StyleClassDecoration.java b/controlsfx/src/main/java/org/controlsfx/control/decoration/StyleClassDecoration.java
new file mode 100644
index 0000000..5e86b8b
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/decoration/StyleClassDecoration.java
@@ -0,0 +1,81 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.decoration;
+
+import java.util.List;
+
+import javafx.scene.Node;
+
+/**
+ * StyleClassDecoration is a {@link Decoration} designed to add a CSS style class
+ * to a node (for example, to show a warning style when the field is incorrectly 
+ * set). StyleClassDecoration is applied as part of the ControlsFX {@link Decorator} 
+ * API - refer to the {@link Decorator} javadoc for more details.
+ * 
+ * @see Decoration
+ * @see Decorator
+ */
+public class StyleClassDecoration extends Decoration {
+
+    private final String[] styleClasses;
+
+    /**
+     * Constructs a new StyleClassDecoration with the given var-args array of 
+     * style classes set to be applied to any node that has this decoration 
+     * applied to it.
+     * 
+     * @param styleClass A var-args array of style classes to apply to any node.
+     * @throws IllegalArgumentException if the styleClass varargs array is null or empty.
+     */
+    public StyleClassDecoration(String... styleClass) {
+        if (styleClass == null || styleClass.length == 0) {
+            throw new IllegalArgumentException("var-arg style class array must not be null or empty"); //$NON-NLS-1$
+        }
+        this.styleClasses = styleClass;
+    }
+
+    /** {@inheritDoc} */
+    @Override public Node applyDecoration(Node targetNode) {
+        final List<String> styleClassList = targetNode.getStyleClass();
+        
+        for (String styleClass : styleClasses) {
+            if (styleClassList.contains(styleClass)) {
+                continue;
+            }
+    
+            styleClassList.add(styleClass);
+        }
+            
+        // no decoration node, so return null
+        return null;
+    }
+    
+    /** {@inheritDoc} */
+    @Override public void removeDecoration(Node targetNode) {
+        targetNode.getStyleClass().removeAll(styleClasses);
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/decoration/package-info.java b/controlsfx/src/main/java/org/controlsfx/control/decoration/package-info.java
new file mode 100644
index 0000000..da9c058
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/decoration/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * A package containing decoration-related API (that is, API to allow for users
+ * to 'decorate' nodes with additional nodes or css decorations.
+ */
+package org.controlsfx.control.decoration;
\ No newline at end of file
diff --git a/controlsfx/src/main/java/org/controlsfx/control/package-info.java b/controlsfx/src/main/java/org/controlsfx/control/package-info.java
new file mode 100644
index 0000000..f22fac2
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * A package containing a number of useful controls-related classes that do not
+ * exist in the base JavaFX distribution.
+ */
+package org.controlsfx.control;
\ No newline at end of file
diff --git a/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/Grid.java b/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/Grid.java
new file mode 100644
index 0000000..53ac9e8
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/Grid.java
@@ -0,0 +1,200 @@
+/**
+ * Copyright (c) 2013, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.spreadsheet;
+
+import java.util.Collection;
+import javafx.collections.ObservableList;
+import javafx.event.EventHandler;
+import javafx.event.EventType;
+import org.controlsfx.control.spreadsheet.SpreadsheetView.SpanType;
+
+/**
+ * That class holds some {@link SpreadsheetCell} in order
+ * to be used by the {@link SpreadsheetView}.
+ * 
+ * A Grid is used by {@link SpreadsheetView} to represent the data to show on
+ * screen. A default implementation is provided by {@link GridBase}, but for 
+ * more custom purposes (e.g. loading data on demand), this Grid interface may
+ * prove useful.
+ * 
+ * <p>A Grid at its essence consists of rows and columns. Critical to the 
+ * SpreadsheetView is that the {@link #getRowCount() row count} and 
+ * {@link #getColumnCount() column count} are accurately returned when requested
+ * (even if the data returned by {@link #getRows()} is not all fully loaded into
+ * memory). 
+ * 
+ * <p>Whilst the {@link #getRows()} return type may appear confusing, it is 
+ * actually quite logical when you think about it: {@link #getRows()} returns an
+ * ObservableList of ObservableList of {@link SpreadsheetCell} instances. In other
+ * words, this is your classic 2D collection, where the outer ObservableList
+ * can be thought of as the rows, and the inner ObservableList as the columns
+ * within each row. Therefore, if you are wanting to iterate through all columns
+ * in every row of the grid, you would do something like this:
+ * 
+ * 
+ * <h3> Code Sample </h3>
+ * <pre>
+ * Grid grid = ...
+ * for (int row = 0; row < grid.getRowCount(); row++) {
+ *     for (int column = 0; column < grid.getColumnCount(); column++) {
+ *         SpreadsheetCell<?> cell = getRows().get(row).get(column);
+ *         doStuff(cell);
+ *     }
+ * }
+ * 
+ * </pre>
+ * 
+ * @see SpreadsheetView
+ * @see GridBase
+ * @see SpreadsheetCell
+ */
+public interface Grid {
+    /**
+     * This value may be returned from {@link #getRowHeight(int) } in order to
+     * let the system compute the best row height.
+     */
+    public static final double AUTOFIT = -1;
+    
+    /**
+     * @return how many rows are inside the grid.
+     */
+    public int getRowCount();
+    
+    /**
+     * @return how many columns are inside the grid.
+     */
+    public int getColumnCount();
+    
+    /**
+     * Return an ObservableList of ObservableList of {@link SpreadsheetCell}
+     * instances. Refer to the {@link Grid} class javadoc for more detail.
+     * @return an ObservableList of ObservableList of {@link SpreadsheetCell}
+     * instances
+     */
+    public ObservableList<ObservableList<SpreadsheetCell>> getRows();
+
+    /**
+     * Change the value situated at the intersection if possible.
+     * Verification and conversion of the value should be done before 
+     * with {@link SpreadsheetCellType#match(Object)}
+     * and {@link SpreadsheetCellType#convertValue(Object)}.
+     * @param row
+     * @param column
+     * @param value
+     */
+    public void setCellValue(int row, int column, Object value);
+    
+    /**
+     * Return the {@link SpanType} for a given cell row/column intersection.
+     * @param spv
+     * @param row
+     * @param column
+     * @return the {@link SpanType} for a given cell row/column intersection.
+     */
+    public SpanType getSpanType(final SpreadsheetView spv, final int row, final int column);
+    
+    /**
+     * Return the height of a row. {@link #AUTOFIT } can be returned in order to
+     * let the system compute the best row height.
+     *
+     * @param row
+     * @return the height of a row.
+     */
+    public double getRowHeight(int row);
+
+    /**
+     * Return true if the specified row is resizable.
+     * @param row
+     * @return true if the specified row is resizable.
+     */
+    public boolean isRowResizable(int row);
+    
+    /**
+     * Returns an ObservableList of string to display in the row headers.
+     * 
+     * @return an ObservableList of string to display in the row headers.
+     */
+    public ObservableList<String> getRowHeaders();
+    
+    /**
+     * Returns an ObservableList of string to display in the column headers.
+     * 
+     * @return an ObservableList of string to display in the column headers.
+     */
+    public ObservableList<String> getColumnHeaders();
+    
+    /**
+     * Span in row the cell situated at rowIndex and colIndex by the number
+     * count
+     * 
+     * @param count
+     * @param rowIndex
+     * @param colIndex
+     */
+    public void spanRow(int count, int rowIndex, int colIndex);
+    
+    /**
+     * Span in column the cell situated at rowIndex and colIndex by the number
+     * count
+     * 
+     * @param count
+     * @param rowIndex
+     * @param colIndex
+     */
+    public void spanColumn(int count, int rowIndex, int colIndex);
+    
+    /**
+     * This method sets the rows used by the grid, and updates the rowCount.
+     * @param rows
+     */
+    public void setRows(Collection<ObservableList<SpreadsheetCell>> rows);
+    
+    /**
+     * Registers an event handler to this Grid. The Grid class allows 
+     * registration of listeners which will be notified as a {@link SpreadsheetCell}'s value 
+     * will change.
+     *
+     * @param <E>
+     * @param eventType the type of the events to receive by the handler
+     * @param eventHandler the handler to register
+     * @throws NullPointerException if the event type or handler is null
+     */
+    public <E extends GridChange> void addEventHandler(EventType<E> eventType, EventHandler<E> eventHandler);
+    
+    /**
+     * Unregisters a previously registered event handler from this Grid. One
+     * handler might have been registered for different event types, so the
+     * caller needs to specify the particular event type from which to
+     * unregister the handler.
+     *
+     * @param <E>
+     * @param eventType the event type from which to unregister
+     * @param eventHandler the handler to unregister
+     * @throws NullPointerException if the event type or handler is null
+     */
+    public <E extends GridChange> void removeEventHandler(EventType<E> eventType, EventHandler<E> eventHandler);
+}
\ No newline at end of file
diff --git a/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/GridBase.java b/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/GridBase.java
new file mode 100644
index 0000000..16183e2
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/GridBase.java
@@ -0,0 +1,414 @@
+/**
+ * Copyright (c) 2013, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.spreadsheet;
+
+import com.sun.javafx.event.EventHandlerManager;
+import impl.org.controlsfx.spreadsheet.GridViewSkin;
+import java.util.BitSet;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import javafx.beans.Observable;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.event.Event;
+import javafx.event.EventDispatchChain;
+import javafx.event.EventHandler;
+import javafx.event.EventTarget;
+import javafx.event.EventType;
+import javafx.util.Callback;
+import org.controlsfx.control.spreadsheet.SpreadsheetView.SpanType;
+
+/**
+ * A base implementation of the {@link Grid} interface.
+ * 
+ * <h3>Row Height</h3>
+ * 
+ * You can specify some row height for some of your rows at the beginning.
+ * You have to use the method {@link #setRowHeightCallback(javafx.util.Callback) }
+ * in order to specify a Callback that will give you the index of the row, and you 
+ * will give back the height of the row.
+ * <br>
+ * If you just have a {@link Map} available, you can use the {@link MapBasedRowHeightFactory}
+ * that will construct the Callback for you.
+
+* The default height is 24.0.
+ * 
+ * <h3>Cell values</h3>
+ * <p>
+ * If you want to change the value of a cell, you have to go through the API
+ * with {@link #setCellValue(int, int, Object)}. This method will verify that
+ * the value is corresponding to the {@link SpreadsheetCellType} of the cell and
+ * try to convert it if possible. It will also fire a {@link GridChange} event
+ * in order to notify all listeners that a value has changed. <br>
+ * <p>
+ * If you want to listen to those changes, you can use the
+ * {@link #addEventHandler(EventType, EventHandler)} and
+ * {@link #removeEventHandler(EventType, EventHandler)} methods. <br>
+ * A basic listener for implementing a undo/redo in the SpreadsheetView could be
+ * like that:
+ * 
+ * <pre>
+ * Grid grid = ...;
+ * Stack<GridChange> undoStack = ...;
+ * grid.addEventHandler(GridChange.GRID_CHANGE_EVENT, new EventHandler<GridChange>() {
+ *         
+ *         public void handle(GridChange change) {
+ *                 undoStack.push(change);
+ *             }
+ *         });
+ * 
+ * </pre>
+ * 
+ * 
+ * @see Grid
+ * @see GridChange
+ */
+public class GridBase implements Grid, EventTarget {
+
+    /***************************************************************************
+     * 
+     * Private Fields
+     * 
+     **************************************************************************/
+    private final ObservableList<ObservableList<SpreadsheetCell>> rows;
+
+    private int rowCount;
+    private int columnCount;
+    private Callback<Integer, Double> rowHeightFactory;
+    private final BooleanProperty locked;
+    private final EventHandlerManager eventHandlerManager = new EventHandlerManager(this);
+    private final ObservableList<String> rowsHeader;
+    private final ObservableList<String> columnsHeader;
+    private BitSet resizableRow;
+
+    /***************************************************************************
+     * 
+     * Constructor
+     * 
+     **************************************************************************/
+
+    /**
+     * Creates a grid with a fixed number of rows and columns.
+     * 
+     * @param rowCount
+     * @param columnCount
+     */
+    public GridBase(int rowCount, int columnCount) {
+        this.rowCount = rowCount;
+        this.columnCount = columnCount;
+        rowsHeader = FXCollections.observableArrayList();
+        columnsHeader = FXCollections.observableArrayList();
+        locked = new SimpleBooleanProperty(false);
+        rowHeightFactory = new MapBasedRowHeightFactory(new HashMap<>());
+        rows = FXCollections.observableArrayList();
+        rows.addListener((Observable observable) -> {
+            setRowCount(rows.size());
+        });
+        resizableRow = new BitSet(rowCount);
+        resizableRow.set(0, rowCount, true);
+    }
+
+    /***************************************************************************
+     * 
+     * Public Methods (Inherited from Grid)
+     * 
+     **************************************************************************/
+
+    /** {@inheritDoc} */
+    @Override
+    public ObservableList<ObservableList<SpreadsheetCell>> getRows() {
+        return rows;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setCellValue(int row, int column, Object value) {
+        if (row < rowCount && column < columnCount && !isLocked()) {
+            SpreadsheetCell cell = getRows().get(row).get(column);
+            Object previousItem = cell.getItem();
+            Object convertedValue = cell.getCellType().convertValue(value);
+            cell.setItem(convertedValue);
+            if (!java.util.Objects.equals(previousItem, cell.getItem())) {
+                GridChange cellChange = new GridChange(row, column, previousItem, convertedValue);
+                Event.fireEvent(this, cellChange);
+            }
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int getRowCount() {
+        return rowCount;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int getColumnCount() {
+        return columnCount;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public SpanType getSpanType(final SpreadsheetView spv, final int row, final int column) {
+        if (row < 0 || column < 0 || row >= rowCount || column >= columnCount) {
+            return SpanType.NORMAL_CELL;
+        }
+        
+        final SpreadsheetCell cell =  getRows().get(row).get(column);
+
+        final int cellColumn = cell.getColumn();
+        final int cellRow = cell.getRow();
+        final int cellRowSpan = cell.getRowSpan();
+
+        if (cellColumn == column && cellRow == row && cellRowSpan == 1) {
+            return SpanType.NORMAL_CELL;
+        }
+
+        final int cellColumnSpan = cell.getColumnSpan();
+        /**
+         * This is a consuming operation so we place it after the normal_cell
+         * case since this is the most typical case.
+         */
+        final GridViewSkin skin = spv.getCellsViewSkin();
+        final boolean containsRowMinusOne = skin == null ? true : skin.containsRow(row - 1);
+        if (containsRowMinusOne && cellColumnSpan > 1 && cellColumn != column && cellRowSpan > 1
+                && cellRow != row) {
+            return SpanType.BOTH_INVISIBLE;
+        } else if (cellRowSpan > 1 && cellColumn == column) {
+            if ((cellRow == row || !containsRowMinusOne)) {
+                return SpanType.ROW_VISIBLE;
+            } else {
+                return SpanType.ROW_SPAN_INVISIBLE;
+            }
+        } else if (cellColumnSpan > 1 && cellColumn != column && (cellRow == row || !containsRowMinusOne)) {
+            return SpanType.COLUMN_SPAN_INVISIBLE;
+        } else {
+            return SpanType.NORMAL_CELL;
+        }
+    }
+
+    /**
+     * Return the height of a row. 
+     * It will first look into the {@link Callback}provided at the
+     * initialization. If nothing's found, {@link #AUTOFIT} will be returned.
+     * @param row
+     * @return the height of a row.
+     */
+    @Override
+    public double getRowHeight(int row) {
+        return rowHeightFactory.call((Integer) row);
+    }
+
+    /***************************************************************************
+     * 
+     * Public Methods
+     * 
+     **************************************************************************/
+
+    /**
+     * Set a new {@link Callback} for this grid in order to specify height of
+     * each row.
+     * 
+     * @param rowHeight
+     */
+    public void setRowHeightCallback(Callback<Integer, Double> rowHeight) {
+        this.rowHeightFactory = rowHeight;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public ObservableList<String> getRowHeaders() {
+        return rowsHeader;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public ObservableList<String> getColumnHeaders() {
+        return columnsHeader;
+    }
+
+    /**
+     * Return a BooleanProperty associated with the locked grid state. It means
+     * that the Grid is in a read-only mode and that no SpreadsheetCell can be
+     * modified, no regards for their own
+     * {@link SpreadsheetCell#isEditable()} state.
+     * 
+     * @return a BooleanProperty associated with the locked grid state.
+     */
+    public BooleanProperty lockedProperty() {
+        return locked;
+    }
+
+    /**
+     * Return whether this Grid id locked or not.
+     * 
+     * @return whether this Grid id locked or not.
+     */
+    public boolean isLocked() {
+        return locked.get();
+    }
+
+    /**
+     * Lock or unlock this Grid.
+     * 
+     * @param lock
+     */
+    public void setLocked(Boolean lock) {
+        locked.setValue(lock);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void spanRow(int count, int rowIndex, int colIndex) {
+        if (count <= 0 || count > rowCount || rowIndex >= rowCount || colIndex >= columnCount) {
+            return;
+        }
+        final SpreadsheetCell cell = rows.get(rowIndex).get(colIndex);
+        final int colSpan = cell.getColumnSpan();
+        final int rowSpan = count;
+        cell.setRowSpan(rowSpan);
+        for (int row = rowIndex; row < rowIndex + rowSpan && row < rowCount; ++row) {
+            for (int col = colIndex; col < colIndex + colSpan && col < columnCount; ++col) {
+                if (row != rowIndex || col != colIndex) {
+                    rows.get(row).set(col, cell);
+                }
+            }
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void spanColumn(int count, int rowIndex, int colIndex) {
+        if (count <= 0 || count > columnCount || rowIndex >= rowCount || colIndex >= columnCount) {
+            return;
+        }
+        final SpreadsheetCell cell = rows.get(rowIndex).get(colIndex);
+        final int colSpan = count;
+        final int rowSpan = cell.getRowSpan();
+        cell.setColumnSpan(colSpan);
+        for (int row = rowIndex; row < rowIndex + rowSpan && row < rowCount; ++row) {
+            for (int col = colIndex; col < colIndex + colSpan && col < columnCount; ++col) {
+                if (row != rowIndex || col != colIndex) {
+                    rows.get(row).set(col, cell);
+                }
+            }
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setRows(Collection<ObservableList<SpreadsheetCell>> rows) {
+        this.rows.clear();
+        this.rows.addAll(rows);
+
+        setRowCount(rows.size());
+        setColumnCount(rowCount == 0 ? 0 : this.rows.get(0).size());
+    }
+
+    /**
+     * Sets the resizable state of all rows. If a bit is set to true in the
+     * BitSet, it means the row is resizable.
+     *
+     * The {@link BitSet#length() } must be equal to the {@link #getRowCount() }
+     *
+     * @param resizableRow
+     */
+    public void setResizableRows(BitSet resizableRow) {
+        this.resizableRow = resizableRow;
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    public boolean isRowResizable(int row) {
+        return resizableRow.get(row);
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    public <E extends GridChange> void addEventHandler(EventType<E> eventType, EventHandler<E> eventHandler) {
+        eventHandlerManager.addEventHandler(eventType, eventHandler);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public <E extends GridChange> void removeEventHandler(EventType<E> eventType, EventHandler<E> eventHandler) {
+        eventHandlerManager.removeEventHandler(eventType, eventHandler);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public EventDispatchChain buildEventDispatchChain(EventDispatchChain tail) {
+        return tail.append(eventHandlerManager);
+    }
+
+    /***************************************************************************
+     * 
+     * Private implementation
+     * 
+     **************************************************************************/
+
+    /**
+     * Set a new rowCount for the grid.
+     * 
+     * @param rowCount
+     */
+    private void setRowCount(int rowCount) {
+        this.rowCount = rowCount;
+    }
+
+    /**
+     * Set a new columnCount for the grid.
+     * 
+     * @param columnCount
+     */
+    private void setColumnCount(int columnCount) {
+        this.columnCount = columnCount;
+    }
+
+    /**
+     * This class serves as a bridge between row height Callback needed by the
+     * GridBase and a Map<Integer,Double> that one could have (each Integer
+     * specify a row index and its associated height).
+     */
+    public static class MapBasedRowHeightFactory implements Callback<Integer, Double> {
+        private final Map<Integer, Double> rowHeightMap;
+
+        public MapBasedRowHeightFactory(Map<Integer, Double> rowHeightMap) {
+            this.rowHeightMap = rowHeightMap;
+        }
+
+        @Override
+        public Double call(Integer index) {
+            Double value = rowHeightMap.get(index);
+            return value == null ? AUTOFIT : value;
+        }
+
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/GridChange.java b/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/GridChange.java
new file mode 100644
index 0000000..c8ee2ff
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/GridChange.java
@@ -0,0 +1,126 @@
+/**
+ * Copyright (c) 2013, ControlsFX 
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met: *
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer. * Redistributions in binary
+ * form must reproduce the above copyright notice, this list of conditions and
+ * the following disclaimer in the documentation and/or other materials provided
+ * with the distribution. * Neither the name of ControlsFX, any associated
+ * website, nor the names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior written
+ * permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.spreadsheet;
+
+import java.io.Serializable;
+
+import javafx.event.Event;
+import javafx.event.EventType;
+
+/**
+ * This class represents a single change happening in a {@link Grid}.
+ * 
+ * @see Grid
+ * @see GridBase
+ */
+public class GridChange extends Event implements Serializable {
+
+    /**
+     * This is the event used by {@link GridChange}.
+     */
+    public static final EventType<GridChange> GRID_CHANGE_EVENT = new EventType<>(Event.ANY, "GridChange"); //$NON-NLS-1$
+
+    /**
+     * *************************************************************************
+     * * Static field * *
+     ************************************************************************* 
+     */
+    private static final long serialVersionUID = 210644901287223524L;
+
+    /**
+     * *************************************************************************
+     * * Private Fields * *
+     ************************************************************************* 
+     */
+    private final int row;
+    private final int column;
+    private final Object oldValue;
+    private final Object newValue;
+
+    /**
+     * *************************************************************************
+     * * Constructor *
+     ************************************************************************* 
+     */
+    /**
+     * Constructor of a GridChange when a change inside a
+     * {@link SpreadsheetCell} is happening.
+     * 
+     * @param row
+     * @param column
+     * @param oldValue
+     * @param newValue
+     */
+    public GridChange(int row, int column, Object oldValue, Object newValue) {
+        super(GRID_CHANGE_EVENT);
+        this.row = row;
+        this.column = column;
+        this.oldValue = oldValue;
+        this.newValue = newValue;
+    }
+
+    /**
+     * *************************************************************************
+     * * Public methods * *
+     ************************************************************************* 
+     */
+    /**
+     * Return the row number of this change.
+     * 
+     * @return the row number of this change.
+     */
+    public int getRow() {
+        return row;
+    }
+
+    /**
+     * Return the column number of this change.
+     * 
+     * @return the column number of this change.
+     */
+    public int getColumn() {
+        return column;
+    }
+
+    /**
+     * Return the value before the change.
+     * 
+     * @return the value before the change.
+     */
+    public Object getOldValue() {
+        return oldValue;
+    }
+
+    /**
+     * Return the value after the change.
+     * 
+     * @return the value after the change.
+     */
+    public Object getNewValue() {
+        return newValue;
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/Picker.java b/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/Picker.java
new file mode 100644
index 0000000..0c2762d
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/Picker.java
@@ -0,0 +1,93 @@
+/**
+ * Copyright (c) 2014, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.spreadsheet;
+
+import java.util.Collection;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+
+/**
+ *
+ * Pickers can display some Images next to the headers. <br>
+ * You can specify the image by providing custom StyleClass :<br>
+ * 
+ * <pre>
+ * .picker-label{
+ *   -fx-graphic: url("add.png"); 
+ *   -fx-background-color: white;
+ *   -fx-padding: 0 0 0 0;
+ * }
+ * </pre>
+ * 
+ * The {@link #onClick() } method does nothing by default, so you can override it
+ * if you want to execute a custom action when the user will click on your Picker.
+ * 
+ * <h3>Visual:</h3> <center><img src="pickers.PNG" alt="Screenshot of Picker"></center>
+ * 
+ */
+public class Picker {
+
+    private final ObservableList<String> styleClass = FXCollections.observableArrayList();
+
+    /**
+     * Default constructor, the default "picker-label" styleClass is applied.
+     */
+    public Picker() {
+        this("picker-label"); //$NON-NLS-1$
+    }
+
+    /**
+     * Initialize this Picker with the style classes provided.
+     * @param styleClass 
+     */
+    public Picker(String... styleClass) {
+        this.styleClass.addAll(styleClass);
+    }
+    
+    /**
+     * Initialize this Picker with the style classes provided.
+     * @param styleClass 
+     */
+    public Picker(Collection<String> styleClass) {
+        this.styleClass.addAll(styleClass);
+    }
+
+
+    /**
+     * @return the style class of this picker.
+     */
+    public final ObservableList<String> getStyleClass() {
+        return styleClass;
+    }
+
+    /**
+     * This method will be called whenever the user clicks on this picker.
+     */
+    public void onClick(){
+        //no-op by default
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/SpreadsheetCell.java b/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/SpreadsheetCell.java
new file mode 100644
index 0000000..29bb9ad
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/SpreadsheetCell.java
@@ -0,0 +1,333 @@
+/**
+ * Copyright (c) 2014, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.spreadsheet;
+
+import java.util.Optional;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.ReadOnlyStringProperty;
+import javafx.beans.property.StringProperty;
+import javafx.collections.ObservableSet;
+import javafx.event.Event;
+import javafx.event.EventHandler;
+import javafx.event.EventType;
+import javafx.scene.Node;
+
+/**
+ *
+ * Interface of the cells used in the {@link SpreadsheetView}.
+ * 
+ * See {@link SpreadsheetCellBase} for a complete and detailed documentation.
+ * @see SpreadsheetCellBase
+ */
+public interface SpreadsheetCell {
+    /**
+     * This EventType can be used with an {@link EventHandler} in order to catch
+     * when the editable state of a SpreadsheetCell is changed.
+     */
+    public static final EventType EDITABLE_EVENT_TYPE = new EventType("EditableEventType"); //$NON-NLS-1$
+    
+    /**
+     * This EventType can be used with an {@link EventHandler} in order to catch
+     * when the wrap text state of a SpreadsheetCell is changed.
+     */
+    public static final EventType WRAP_EVENT_TYPE = new EventType("WrapTextEventType"); //$NON-NLS-1$
+    
+    /**
+     * This EventType can be used with an {@link EventHandler} in order to catch
+     * when a corner state of a SpreadsheetCell is changed.
+     */
+    public static final EventType CORNER_EVENT_TYPE = new EventType("CornerEventType"); //$NON-NLS-1$
+
+    /**
+     * This enum states the four different corner available for positioning 
+     * some elements in a cell.
+     */
+    public static enum CornerPosition {
+
+        TOP_LEFT ,
+        TOP_RIGHT ,
+        BOTTOM_RIGHT ,
+        BOTTOM_LEFT 
+    }
+    
+    /**
+     * Verify that the upcoming cell value can be set to the current cell. This
+     * is currently used by the Copy/Paste.
+     *
+     * @param cell
+     * @return true if the upcoming cell value can be set to the current cell.
+     */
+    public boolean match(SpreadsheetCell cell);
+
+    /**
+     * Sets the value of the property Item. This should be used only at
+     * initialization. Prefer {@link Grid#setCellValue(int, int, Object)} after
+     * because it will compute correctly the modifiedCell. If
+     * {@link #isEditable()} return false, nothing is done.
+     *
+     * @param value
+     */
+    public void setItem(Object value);
+
+    /**
+     * Return the value contained in the cell.
+     *
+     * @return the value contained in the cell.
+     */
+    public Object getItem();
+
+    /**
+     * The item property represents the currently-set value inside this
+     * SpreadsheetCell instance.
+     *
+     * @return the item property which contains the value.
+     */
+    public ObjectProperty<Object> itemProperty();
+
+    /**
+     * Return if this cell can be edited or not.
+     *
+     * @return true if this cell is editable.
+     */
+    public boolean isEditable();
+
+    /**
+     * Change the editable state of this cell
+     *
+     * @param editable
+     */
+    public void setEditable(boolean editable);
+    
+    /**
+     * If a run of text exceeds the width of the Labeled, then this variable
+     * indicates whether the text should wrap onto another line.
+     *
+     * @return the value of wrapText property.
+     */
+    public boolean isWrapText();
+
+    /**
+     * If a run of text exceeds the width of the Labeled, then this variable
+     * indicates whether the text should wrap onto another line.
+     * @param wrapText
+     */
+    public void setWrapText(boolean wrapText);
+
+    /**
+     * A string representation of the CSS style associated with this specific
+     * Node. This is analogous to the "style" attribute of an HTML element. Note
+     * that, like the HTML style attribute, this variable contains style
+     * properties and values and not the selector portion of a style rule.
+     *
+     * @param style
+     */
+    public void setStyle(String style);
+    
+    /**
+     * A string representation of the CSS style associated with this specific
+     * Node. This is analogous to the "style" attribute of an HTML element. Note
+     * that, like the HTML style attribute, this variable contains style
+     * properties and values and not the selector portion of a style rule.
+     *
+     * @return The inline CSS style associated with this Node. If this Node does
+     * not have an inline style, an empty String is returned.
+     */
+    public String getStyle();
+    
+    /**
+     * A string representation of the CSS style associated with this specific
+     * Node. This is analogous to the "style" attribute of an HTML element. Note
+     * that, like the HTML style attribute, this variable contains style
+     * properties and values and not the selector portion of a style rule.
+     *
+     * @return a string representation of the CSS style
+     */
+    public StringProperty styleProperty();
+    
+    /**
+     * This activate the given cornerPosition.
+     * @param position
+     */
+    public void activateCorner(CornerPosition position);
+    
+    /**
+     * This deactivate the given cornerPosition.
+     * @param position
+     */
+    public void deactivateCorner(CornerPosition position);
+
+    /**
+     * 
+     * @param position
+     * @return the current state of a specific corner.
+     */
+    public boolean isCornerActivated(CornerPosition position);
+    
+    /**
+     * The {@link StringProperty} linked with the format.
+     *
+     * @return The {@link StringProperty} linked with the format state.
+     */
+    public StringProperty formatProperty();
+
+    /**
+     * Return the format of this cell or an empty string if no format has been
+     * specified.
+     *
+     * @return Return the format of this cell or an empty string if no format
+     * has been specified.
+     */
+    public String getFormat();
+
+    /**
+     * Set a new format for this Cell. You can specify how to represent the
+     * value in the cell.
+     *
+     * @param format
+     */
+    public void setFormat(String format);
+
+    /**
+     * Return the StringProperty of the representation of the value.
+     *
+     * @return the StringProperty of the representation of the value.
+     */
+    public ReadOnlyStringProperty textProperty();
+
+    /**
+     * Return the String representation currently used for display in the
+     * {@link SpreadsheetView}.
+     *
+     * @return text representation of the value.
+     */
+    public String getText();
+
+    /**
+     * Return the {@link SpreadsheetCellType} of this particular cell.
+     *
+     * @return the {@link SpreadsheetCellType} of this particular cell.
+     */
+    public SpreadsheetCellType getCellType();
+
+    /**
+     * Return the row of this cell.
+     *
+     * @return the row of this cell.
+     */
+    public int getRow();
+
+    /**
+     * Return the column of this cell.
+     *
+     * @return the column of this cell.
+     */
+    public int getColumn();
+
+    /**
+     * Return how much this cell is spanning in row, 1 is normal.
+     *
+     * @return how much this cell is spanning in row, 1 is normal.
+     */
+    public int getRowSpan();
+
+    /**
+     * Sets how much this cell is spanning in row. See {@link SpreadsheetCell}
+     * description for information. You should use
+     * {@link Grid#spanRow(int, int, int)} instead of using this method
+     * directly.
+     *
+     * @param rowSpan
+     */
+    public void setRowSpan(int rowSpan);
+
+    /**
+     * Return how much this cell is spanning in column, 1 is normal.
+     *
+     * @return how much this cell is spanning in column, 1 is normal.
+     */
+    public int getColumnSpan();
+
+    /**
+     * Sets how much this cell is spanning in column. See
+     * {@link SpreadsheetCell} description for information. You should use
+     * {@link Grid#spanColumn(int, int, int)} instead of using this method
+     * directly.
+     *
+     * @param columnSpan
+     */
+    public void setColumnSpan(int columnSpan);
+
+    /**
+     * Return an ObservableList of String of all the style class associated with
+     * this cell. You can easily modify its appearance by adding a style class
+     * (previously set in CSS).
+     *
+     * @return an ObservableList of String of all the style class
+     */
+    public ObservableSet<String> getStyleClass();
+
+    /**
+     * @return an ObjectProperty wrapping a Node for the graphic.
+     */
+    public ObjectProperty<Node> graphicProperty();
+
+    /**
+     * Set a graphic for this cell to display aside with the text.
+     *
+     * @param graphic
+     */
+    public void setGraphic(Node graphic);
+
+    /**
+     * Return the graphic node associated with this cell. Return null if nothing
+     * has been associated.
+     *
+     * @return the graphic node associated with this cell.
+     */
+    public Node getGraphic();
+    
+    /**
+     * @return the tooltip associated with this SpreadsheetCell.
+     */
+    public Optional<String> getTooltip();
+    
+    /**
+     * Registers an event handler to this SpreadsheetCell. 
+     * @param eventType the type of the events to receive by the handler
+     * @param eventHandler the handler to register
+     * @throws NullPointerException if the event type or handler is null
+     */
+    public void addEventHandler(EventType<Event> eventType, EventHandler<Event> eventHandler);
+    
+    /**
+     * Unregisters a previously registered event handler from this SpreadsheetCell. 
+     * @param eventType the event type from which to unregister
+     * @param eventHandler the handler to unregister
+     * @throws NullPointerException if the event type or handler is null
+     */
+    public void removeEventHandler(EventType<Event> eventType, EventHandler<Event> eventHandler);
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/SpreadsheetCellBase.java b/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/SpreadsheetCellBase.java
new file mode 100644
index 0000000..f13634f
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/SpreadsheetCellBase.java
@@ -0,0 +1,643 @@
+/**
+ * Copyright (c) 2013, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.spreadsheet;
+
+import com.sun.javafx.event.EventHandlerManager;
+import java.util.Objects;
+import java.util.Optional;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.ReadOnlyStringProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableSet;
+import javafx.event.Event;
+import javafx.event.EventDispatchChain;
+import javafx.event.EventHandler;
+import javafx.event.EventTarget;
+import javafx.event.EventType;
+import javafx.scene.Node;
+import javafx.scene.image.ImageView;
+
+/**
+ * The SpreadsheetCells serve as model for the {@link SpreadsheetView}. <br>
+ * You will provide them when constructing a {@link Grid}.
+ * 
+ * <br>
+ * <h3>SpreadsheetCell Types</h3> Each SpreadsheetCell has its own
+ * {@link SpreadsheetCellType} which has its own {@link SpreadsheetCellEditor}
+ * in order to control very closely the possible modifications.
+ * 
+ * <p>
+ * Different {@link SpreadsheetCellType SpreadsheetCellTypes} are available
+ * depending on the data you want to represent in your {@link SpreadsheetView}.
+ * You can use the different static method provided in
+ * {@link SpreadsheetCellType} in order to create the specialized
+ * SpreadsheetCell that suits your need.
+ * 
+ * 
+ * <br>
+ * 
+ * <p>
+ * If you want to create a SpreadsheetCell of your own, you simply have to
+ * use one of the provided constructor. Usually you will let your {@link SpreadsheetCellType}
+ * create the cells. For example 
+ * {@link SpreadsheetCellType.StringType#createCell(int, int, int, int, java.lang.String) }.
+ * You will also have to provide a custom {@link SpreadsheetCellEditor}.
+ * 
+ * <h2>Configuration</h2>
+ * You will have to indicate the coordinates of the cell together with the
+ * {@link #setRowSpan(int) row} and {@link #setColumnSpan(int) column} span. You
+ * can specify if you want the cell to be editable or not using
+ * {@link #setEditable(boolean)}. Be advised that a cell with a rowSpan means
+ * that the cell will replace all the cells situated in the rowSpan range. Same
+ * with the column span. 
+ * <br>
+ * So the best way to handle spanning is to fill your grid
+ * with unique cells, and then call at the end {@link GridBase#spanColumn(int, int, int)}
+ * or {@link GridBase#spanRow(int, int, int)}. These methods will handle the span
+ * for you.
+ * 
+ * <br>
+ * 
+ * <h3>Format</h3>
+ * Your cell can have its very own format. If you want to display some dates
+ * with different format, you just have to create a unique
+ * {@link SpreadsheetCellType} and then specify for each cell their format with
+ * {@link #setFormat(String)}. You will then have the guaranty that all your
+ * cells will have a LocalDate as a value, but the value will be displayed
+ * differently for each cell. This will also guaranty that copy/paste and other
+ * operation will be compatible since every cell will share the same
+ * {@link SpreadsheetCellType}. <br>
+ * Here an example : <br>
+ * 
+ * 
+ * <pre>
+ * SpreadsheetCell cell = SpreadsheetCellType.DATE.createCell(row, column, rowSpan, colSpan,
+ *         LocalDate.now().plusDays((int) (Math.random() * 10))); // Random value
+ * // given here
+ * final double random = Math.random();
+ * if (random < 0.25) {
+ *     cell.setFormat("EEEE d");
+ * } else if (random < 0.5) {
+ *     cell.setFormat("dd/MM :YY");
+ * } else {
+ *     cell.setFormat("dd/MM/YYYY");
+ * }
+ * </pre>
+ * 
+ * <center><img src="dateFormat.PNG" alt="SpreadsheetCellBase with custom format"></center>
+ * 
+ * <h3>Graphic</h3>
+ * Each cell can have a graphic to display next to the text in the cells. Just
+ * use the {@link #setGraphic(Node)} in order to specify the graphic you want.
+ * If you specify an {@link ImageView}, the SpreadsheetView will try to resize it in
+ * order to fit the space available in the cell.
+ * 
+ * For example :
+ * 
+ * <pre>
+ * cell.setGraphic(new ImageView(new Image(getClass().getResourceAsStream("icons/exclamation.png"))));
+ * </pre>
+ * 
+ * <center><img src="graphicNodeToCell.png" alt="SpreadsheetCellBase with graphic"></center> <br>
+ * In addition to that, you can also specify another graphic property to your
+ * cell with {@link #activateCorner(org.controlsfx.control.spreadsheet.SpreadsheetCell.CornerPosition) }.
+ * This allow you to activate or deactivate some graphics on the cell in every 
+ * corner. Right now it's a little red triangle but you can modify this in your CSS by
+ * using the "<b>cell-corner</b>" style class.
+ * 
+ * <pre>
+ * .cell-corner.top-left{
+ *     -fx-background-color: red;
+ *     -fx-shape : "M 0 0 L 1 0 L 0 1 z";
+ * }
+ * </pre>
+ * 
+ * <center><img src="triangleCell.PNG" alt="SpreadsheetCellBase with a styled cell-corner"></center>
+ * 
+ * 
+ * <br>
+ * You can also customize the tooltip of your SpreadsheetCell by specifying one
+ * with {@link #setTooltip(java.lang.String) }.
+ * 
+ * <h3>Style with CSS</h3>
+ * You can style your cell by specifying some styleClass with
+ * {@link #getStyleClass()}. You just have to create and custom that class in
+ * your CSS stylesheet associated with your {@link SpreadsheetView}. Also note
+ * that all {@link SpreadsheetCell} have a "<b>spreadsheet-cell</b>" styleClass
+ * added by default. Here is a example :<br>
+ * 
+ * <pre>
+ * cell.getStyleClass().add("row_header");
+ * </pre>
+ * 
+ * And in the CSS:
+ * 
+ * <pre>
+ *  .spreadsheet-cell.row_header{
+ *     -fx-background-color: #b4d4ad ;
+ *     -fx-background-insets: 0, 0 1 1 0;
+ *     -fx-alignment: center;
+ * }
+ * </pre>
+ * 
+ * <h3>Examples</h3>
+ * Here is an example that uses all the pre-built {@link SpreadsheetCellType}
+ * types. The generation is random here so you will want to replace the logic to
+ * suit your needs.
+ * 
+ * <pre>
+ * private SpreadsheetCell<?> generateCell(int row, int column, int rowSpan, int colSpan) {
+ *     List<String> stringListTextCell = Arrays.asList("Shanghai","Paris","New York City","Bangkok","Singapore","Johannesburg","Berlin","Wellington","London","Montreal");
+ *     final double random = Math.random();
+ *     if (random < 0.10) {
+ *         List<String> stringList = Arrays.asList("China","France","New Zealand","United States","Germany","Canada");
+ *         cell = SpreadsheetCellType.LIST(stringList).createCell(row, column, rowSpan, colSpan, stringList.get((int) (Math.random() * 6)));
+ *     } else if (random >= 0.10 && random < 0.25) {
+ *         cell = SpreadsheetCellType.STRING.createCell(row, column, rowSpan, colSpan,stringListTextCell.get((int)(Math.random()*10)));
+ *     } else if (random >= 0.25 && random < 0.75) {
+ *         cell = SpreadsheetCellType.DOUBLE.createCell(row, column, rowSpan, colSpan,(double)Math.round((Math.random()*100)*100)/100);
+ *     } else {
+ *         cell = SpreadsheetCellType.DATE.createCell(row, column, rowSpan, colSpan, LocalDate.now().plusDays((int)(Math.random()*10)));
+ *     }
+ *     return cell;
+ * }
+ * </pre>
+ * 
+ * @see SpreadsheetView
+ * @see SpreadsheetCellEditor
+ * @see SpreadsheetCellType
+ */
+public class SpreadsheetCellBase implements SpreadsheetCell, EventTarget{
+
+    /***************************************************************************
+     * 
+     * Private Fields
+     * 
+     **************************************************************************/
+
+    //The Bit position for the editable Property.
+    private static final int EDITABLE_BIT_POSITION = 4;
+    private static final int WRAP_BIT_POSITION = 5;
+    private final SpreadsheetCellType type;
+    private final int row;
+    private final int column;
+    private int rowSpan;
+    private int columnSpan;
+    private final StringProperty format;
+    private final StringProperty text;
+    private final StringProperty styleProperty;
+    private final ObjectProperty<Node> graphic;
+    private String tooltip;
+    /**
+     * This variable handles all boolean values of this SpreadsheetCell inside
+     * its bits. Instead of using regular boolean, we use that int so that we 
+     * can reduce memory usage to the bare minimum.
+     */
+    private int propertyContainer = 0;
+    private final EventHandlerManager eventHandlerManager = new EventHandlerManager(this);
+
+    private ObservableSet<String> styleClass;
+
+    /***************************************************************************
+     * 
+     * Constructor
+     * 
+     **************************************************************************/
+
+    /**
+     * Constructs a SpreadsheetCell with the given configuration.
+     * Use the {@link SpreadsheetCellType#OBJECT} type.
+     * @param row
+     * @param column
+     * @param rowSpan
+     * @param columnSpan
+     */
+    public SpreadsheetCellBase(final int row, final int column, final int rowSpan, final int columnSpan) {
+        this(row, column, rowSpan, columnSpan, SpreadsheetCellType.OBJECT);
+    }
+
+    /**
+     * Constructs a SpreadsheetCell with the given configuration.
+     * 
+     * @param row
+     * @param column
+     * @param rowSpan
+     * @param columnSpan
+     * @param type
+     */
+    public SpreadsheetCellBase(final int row, final int column, final int rowSpan, final int columnSpan,
+            final SpreadsheetCellType<?> type) {
+        this.row = row;
+        this.column = column;
+        this.rowSpan = rowSpan;
+        this.columnSpan = columnSpan;
+        this.type = type;
+        text = new SimpleStringProperty(""); //$NON-NLS-1$
+        format = new SimpleStringProperty(""); //$NON-NLS-1$
+        graphic = new SimpleObjectProperty<>();
+        format.addListener(new ChangeListener<String>() {
+            @Override
+            public void changed(ObservableValue<? extends String> arg0, String arg1, String arg2) {
+                updateText();
+            }
+        });
+        //Editable is true at the initialisation
+        setEditable(true);
+        getStyleClass().add("spreadsheet-cell"); //$NON-NLS-1$
+        styleProperty = new SimpleStringProperty();
+    }
+
+   /***************************************************************************
+     * 
+     * Public Methods
+     * 
+     **************************************************************************/
+    
+    /** {@inheritDoc} */
+    @Override
+    public boolean match(SpreadsheetCell cell) {
+        return type.match(cell);
+    }
+
+    // --- item
+    private final ObjectProperty<Object> item = new SimpleObjectProperty<Object>(this, "item") { //$NON-NLS-1$
+        @Override
+        protected void invalidated() {
+            updateText();
+        }
+    };
+
+   /** {@inheritDoc} */
+    @Override
+    public final void setItem(Object value) {
+        if (isEditable())
+            item.set(value);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public final Object getItem() {
+        return item.get();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public final ObjectProperty<Object> itemProperty() {
+        return item;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public final boolean isEditable() {
+        return isSet(EDITABLE_BIT_POSITION);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public final void setEditable(boolean editable) {
+        if(setMask(editable, EDITABLE_BIT_POSITION)){
+            Event.fireEvent(this, new Event(EDITABLE_EVENT_TYPE));
+        }
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    public boolean isWrapText(){
+        return isSet(WRAP_BIT_POSITION);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setWrapText(boolean wrapText) {
+        if (setMask(wrapText, WRAP_BIT_POSITION)) {
+            Event.fireEvent(this, new Event(WRAP_EVENT_TYPE));
+        }
+    }
+
+   /** {@inheritDoc} */
+    @Override
+    public final StringProperty formatProperty() {
+        return format;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public final String getFormat() {
+        return format.get();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public final void setFormat(String format) {
+        formatProperty().set(format);
+        updateText();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public final ReadOnlyStringProperty textProperty() {
+        return text;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public final String getText() {
+        return text.get();
+    }
+
+   /** {@inheritDoc} */
+    @Override
+    public final SpreadsheetCellType getCellType() {
+        return type;
+    }
+
+   /** {@inheritDoc} */
+    @Override
+    public final int getRow() {
+        return row;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public final int getColumn() {
+        return column;
+    }
+
+   /** {@inheritDoc} */
+    @Override
+    public final int getRowSpan() {
+        return rowSpan;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public final void setRowSpan(int rowSpan) {
+        this.rowSpan = rowSpan;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public final int getColumnSpan() {
+        return columnSpan;
+    }
+
+   /** {@inheritDoc} */
+    @Override
+    public final void setColumnSpan(int columnSpan) {
+        this.columnSpan = columnSpan;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public final ObservableSet<String> getStyleClass() {
+        if (styleClass == null) {
+            styleClass = FXCollections.observableSet();
+        }
+        return styleClass;
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    public void setStyle(String style){
+        styleProperty.set(style);
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    public String getStyle(){
+        return styleProperty.get();
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    public StringProperty styleProperty(){
+        return styleProperty;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public ObjectProperty<Node> graphicProperty() {
+        return graphic;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setGraphic(Node graphic) {
+        this.graphic.set(graphic);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Node getGraphic() {
+        return graphic.get();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Optional<String> getTooltip() {
+        return Optional.ofNullable(tooltip);
+    }
+    
+    /**
+     * Set a new tooltip for this cell.
+     * @param tooltip 
+     */
+    public void setTooltip(String tooltip){
+        this.tooltip = tooltip;
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    public void activateCorner(CornerPosition position) {
+        if(setMask(true, getCornerBitNumber(position))){
+            Event.fireEvent(this, new Event(CORNER_EVENT_TYPE));
+        }
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    public void deactivateCorner(CornerPosition position) {
+        if(setMask(false, getCornerBitNumber(position))){
+             Event.fireEvent(this, new Event(CORNER_EVENT_TYPE));
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isCornerActivated(CornerPosition position) {
+        return isSet(getCornerBitNumber(position));
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    public EventDispatchChain buildEventDispatchChain(EventDispatchChain tail) {
+        return tail.append(eventHandlerManager);
+    }
+    
+    /***************************************************************************
+     * 
+     * Overridden Methods
+     * 
+     **************************************************************************/
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+        return "cell[" + row + "][" + column + "]" + rowSpan + "-" + columnSpan; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public final boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (!(obj instanceof SpreadsheetCell))
+            return false;
+
+        final SpreadsheetCell otherCell = (SpreadsheetCell) obj;
+        return otherCell.getRow() == row && otherCell.getColumn() == column
+                && Objects.equals(otherCell.getText(), getText())
+                && rowSpan == otherCell.getRowSpan()
+                && columnSpan == otherCell.getColumnSpan()
+                && Objects.equals(getStyleClass(), otherCell.getStyleClass());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public final int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + column;
+        result = prime * result + row;
+        result = prime * result + rowSpan;
+        result = prime * result + columnSpan;
+        result = prime * result + Objects.hashCode(getText());
+        result = prime * result + Objects.hashCode(getStyleClass());
+        return result;
+    }
+    
+    /**
+     * Registers an event handler to this SpreadsheetCell. The SpreadsheetCell class allows 
+     * registration of listeners which will be notified when a corner state of
+     * the editable state of this SpreadsheetCell have changed.
+     *
+     * @param eventType the type of the events to receive by the handler
+     * @param eventHandler the handler to register
+     * @throws NullPointerException if the event type or handler is null
+     */
+    @Override
+    public void addEventHandler(EventType<Event> eventType, EventHandler<Event> eventHandler) {
+         eventHandlerManager.addEventHandler(eventType, eventHandler);
+    }
+
+    /**
+     * Unregisters a previously registered event handler from this SpreadsheetCell. One
+     * handler might have been registered for different event types, so the
+     * caller needs to specify the particular event type from which to
+     * unregister the handler.
+     *
+     * @param eventType the event type from which to unregister
+     * @param eventHandler the handler to unregister
+     * @throws NullPointerException if the event type or handler is null
+     */
+    @Override
+    public void removeEventHandler(EventType<Event> eventType, EventHandler<Event> eventHandler) {
+         eventHandlerManager.removeEventHandler(eventType, eventHandler);
+    }
+    
+    /***************************************************************************
+     * 
+     * Private Implementation
+     * 
+     **************************************************************************/
+
+    /**
+     * Update the text for the SpreadsheetView.
+     */
+    @SuppressWarnings("unchecked")
+    private void updateText() {
+        if(getItem() == null){
+            text.setValue(""); //$NON-NLS-1$
+        }else if (!("").equals(getFormat())) { //$NON-NLS-1$
+            text.setValue(type.toString(getItem(), getFormat()));
+        } else {
+            text.setValue(type.toString(getItem()));
+        }
+    }
+
+    /**
+     * Return the Bit position for each corner.
+     * @param position
+     * @return 
+     */
+    private int getCornerBitNumber(CornerPosition position) {
+        switch (position) {
+            case TOP_LEFT:
+                return 0;
+
+            case TOP_RIGHT:
+                return 1;
+
+            case BOTTOM_RIGHT:
+                return 2;
+
+            case BOTTOM_LEFT:
+            default:
+                return 3;
+        }
+    }
+
+    /**
+     * Set the specified bit position at the value specified by flag.
+     * @param flag
+     * @param position
+     * @return whether a change has really occured.
+     */
+    private boolean setMask(boolean flag, int position) {
+        int oldCorner = propertyContainer;
+        if (flag) {
+            propertyContainer |= (1 << position);
+        } else {
+            propertyContainer &= ~(1 << position);
+        }
+        return propertyContainer != oldCorner;
+    }
+
+    /**
+     * @param mask
+     * @param position
+     * @return whether the specified bit position is true.
+     */
+    private boolean isSet(int position) {
+        return (propertyContainer & (1 << position)) != 0;
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/SpreadsheetCellEditor.java b/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/SpreadsheetCellEditor.java
new file mode 100644
index 0000000..790a031
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/SpreadsheetCellEditor.java
@@ -0,0 +1,926 @@
+/**
+ * Copyright (c) 2013, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.spreadsheet;
+
+import impl.org.controlsfx.i18n.Localization;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.NumberFormat;
+import java.text.ParseException;
+import java.text.ParsePosition;
+import java.time.LocalDate;
+import java.util.List;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.event.EventHandler;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.Control;
+import javafx.scene.control.DatePicker;
+import javafx.scene.control.IndexRange;
+import javafx.scene.control.TextArea;
+import javafx.scene.control.TextField;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyEvent;
+import javafx.util.StringConverter;
+
+/**
+ * 
+ * SpreadsheetCellEditor are used by {@link SpreadsheetCellType} and
+ * {@link SpreadsheetCell} in order to control how each value will be entered. <br>
+ * 
+ * <h3>General behavior:</h3> Editors will be displayed if the user double-click
+ * in an editable cell ( see {@link SpreadsheetCell#setEditable(boolean)} ). <br>
+ * If the user does anything outside the editor, the editor <b> will be forced
+ * </b> to try to commit the edition and close itself. If the value is not
+ * valid, the editor will cancel the value and close itself. The editor is just
+ * here to allow communication between the user and the {@link SpreadsheetView}.
+ * It will just be given a value, and it will just give back another one after.
+ * The policy regarding validation of a given value is defined in
+ * {@link SpreadsheetCellType#match(Object)}.
+ * 
+ * If the value doesn't meet the requirements when saving the cell, nothing
+ * happens and the editor keeps editing. <br>
+ * You can abandon a current modification by pressing "esc" key. <br>
+ * 
+ * You can specify a maximum height to your spreadsheetCellEditor with {@link #getMaxHeight()
+ * }. This can be used in order to control the display of your editor. If they
+ * should grow or not in a big cell. (for example a {@link TextAreaEditor} want
+ * to grow with the cell in order to take full space for display.
+ * <br>
+ * <h3>Specific behavior:</h3> This class offers some static classes in order to
+ * create a {@link SpreadsheetCellEditor}. Here are their properties: <br>
+ * 
+ * <ul>
+ * <li> {@link StringEditor}: Basic {@link TextField}, can accept all data and
+ * save it as a string.</li>
+ * <li> {@link ListEditor}: Display a {@link ComboBox} with the different values.
+ * </li>
+ * <li> {@link DoubleEditor}: Display a {@link TextField} which accepts only
+ * double value. If the entered value is incorrect, the background will turn red
+ * so that the user will know in advance if the data will be saved or not.</li>
+ * <li> {@link IntegerEditor}: Display a {@link TextField} which accepts only
+ * Integer value. If the entered value is incorrect, the background will turn red
+ * so that the user will know in advance if the data will be saved or not.</li>
+ * <li> {@link DateEditor}: Display a {@link DatePicker}.</li>
+ * <li> {@link ObjectEditor}: Display a {@link TextField} , accept an Object.</li>
+ * </ul>
+ * 
+ * <br>
+ * <h3>Creating your editor:</h3> You can of course create your own
+ * {@link SpreadsheetCellEditor} for displaying other controls.<br>
+ * 
+ * You just have to override the four abstract methods. <b>Remember</b> that you
+ * will never call those methods directly. They will be called by the
+ * {@link SpreadsheetView} when needed.
+ * <ul>
+ * <li> {@link #startEdit(Object)}: You will configure your control with the
+ * given value which is {@link SpreadsheetCell#getItem()} converted to an
+ * object. You do not instantiate your control here, you do it in the
+ * constructor.</li>
+ * <li> {@link #getEditor()}: You will return which control you're using (for
+ * display).</li>
+ * <li> {@link #getControlValue()}: You will return the value inside your editor
+ * in order to submit it for validation.</li>
+ * <li> {@link #end()}: When editing is finished, you can properly close your own
+ * control.</li>
+ * </ul>
+ * <br>
+ * Keep in mind that you will interact only with {@link #endEdit(boolean)} where
+ * a <b>true</b> value means you want to commit, and a <b>false</b> means you
+ * want to cancel. The {@link SpreadsheetView} will handle all the rest for you
+ * and call your methods at the right moment. <br>
+ * 
+ * <h3>Use case :</h3> <center><img src="editorScheme.png" alt="Use case of SpreadsheetCellEditor"></center>
+ * 
+ * <h3>Visual:</h3>
+ * <table style="border: 1px solid gray;" summary="Screenshots of various SpreadsheetCellEditor">
+ * <tr>
+ * <td valign="center" style="text-align:right;"><strong>String</strong></td>
+ * <td><center><img src="textEditor.png" alt="Screenshot of SpreadsheetCellEditor.StringEditor"></center></td>
+ * </tr>
+ * <tr>
+ * <td valign="center" style="text-align:right;"><strong>List</strong></td>
+ * <td><center><img src="listEditor.png" alt="Screenshot of SpreadsheetCellEditor.ListEditor"></center></td>
+ * </tr>
+ * <tr>
+ * <td valign="center" style="text-align:right;"><strong>Double</strong></td>
+ * <td><center><img src="doubleEditor.png" alt="Screenshot of SpreadsheetCellEditor.DoubleEditor"></center></td>
+ * </tr>
+ * <tr>
+ * <td valign="center" style="text-align:right;"><strong>Date</strong></td>
+ * <td><center><img src="dateEditor.png" alt="Screenshot of SpreadsheetCellEditor.DateEditor"></center></td>
+ * </tr>
+ * </table>
+ * 
+ * 
+ * @see SpreadsheetView
+ * @see SpreadsheetCell
+ * @see SpreadsheetCellType
+ */
+public abstract class SpreadsheetCellEditor {
+    private static final double MAX_EDITOR_HEIGHT = 50.0;
+    
+    private static final DecimalFormat decimalFormat = new DecimalFormat("#.##########"); //$NON-NLS-1$
+    SpreadsheetView view;
+
+    /***************************************************************************
+     * * Constructor * *
+     **************************************************************************/
+
+    /**
+     * Construct the SpreadsheetCellEditor.
+     * 
+     * @param view
+     */
+    public SpreadsheetCellEditor(SpreadsheetView view) {
+        this.view = view;
+    }
+
+    /***************************************************************************
+     * * Public Final Methods * *
+     **************************************************************************/
+    /**
+     * Whenever you want to stop the edition, you call that method.<br>
+     * True means you're trying to commit the value, then
+     * {@link SpreadsheetCellType#convertValue(Object)} will be called in order
+     * to verify that the value is correct.<br>
+     * False means you're trying to cancel the value and it will be follow by
+     * {@link #end()}.<br>
+     * See SpreadsheetCellEditor description
+     * 
+     * @param b
+     *            true means commit, false means cancel
+     */
+    public final void endEdit(boolean b) {
+        view.getCellsViewSkin().getSpreadsheetCellEditorImpl().endEdit(b);
+    }
+
+    /***************************************************************************
+     * * Public Abstract Methods * *
+     **************************************************************************/
+    /**
+     * This method will be called when edition start.<br>
+     * You will then do all the configuration of your editor.
+     * 
+     * @param item
+     */
+    public abstract void startEdit(Object item);
+
+    /**
+     * Return the control used for controlling the input. This is called at the
+     * beginning in order to display your control in the cell.
+     * 
+     * @return the control used.
+     */
+    public abstract Control getEditor();
+
+    /**
+     * Return the value within your editor as a string. This will be used by the
+     * {@link SpreadsheetCellType#convertValue(Object)} in order to compute
+     * whether the value is valid regarding the {@link SpreadsheetCellType}
+     * policy.
+     * 
+     * @return the value within your editor as a string.
+     */
+    public abstract String getControlValue();
+
+    /**
+     * This method will be called at the end of edition.<br>
+     * You will be offered the possibility to do the configuration post editing.
+     */
+    public abstract void end();
+
+    /***************************************************************************
+     * * Public Methods * *
+     **************************************************************************/
+    /**
+     * Return the maximum height of the editor. 
+     * @return 50 by default.
+     */
+    public double getMaxHeight(){
+        return MAX_EDITOR_HEIGHT;
+    }
+    
+    /**
+     * A {@link SpreadsheetCellEditor} for
+     * {@link SpreadsheetCellType.ObjectType} typed cells. It displays a
+     * {@link TextField} where the user can type different values.
+     */
+    public static class ObjectEditor extends SpreadsheetCellEditor {
+
+        /***************************************************************************
+         * * Private Fields * *
+         **************************************************************************/
+        private final TextField tf;
+
+        /***************************************************************************
+         * * Constructor * *
+         **************************************************************************/
+        /**
+        * Constructor for the ObjectEditor..
+        * @param view The SpreadsheetView
+        */
+        public ObjectEditor(SpreadsheetView view) {
+            super(view);
+            tf = new TextField();
+        }
+
+        /***************************************************************************
+         * * Public Methods * *
+         **************************************************************************/
+        @Override
+        public void startEdit(Object value) {
+            if (value instanceof String) {
+                tf.setText(value.toString());
+            }
+            attachEnterEscapeEventHandler();
+
+            tf.requestFocus();
+            tf.end();
+        }
+
+        @Override
+        public String getControlValue() {
+            return tf.getText();
+        }
+
+        @Override
+        public void end() {
+            tf.setOnKeyPressed(null);
+        }
+
+        @Override
+        public TextField getEditor() {
+            return tf;
+        }
+
+        /***************************************************************************
+         * * Private Methods * *
+         **************************************************************************/
+
+        private void attachEnterEscapeEventHandler() {
+            tf.setOnKeyPressed(new EventHandler<KeyEvent>() {
+                @Override
+                public void handle(KeyEvent t) {
+                    if (t.getCode() == KeyCode.ENTER) {
+                        endEdit(true);
+                    } else if (t.getCode() == KeyCode.ESCAPE) {
+                        endEdit(false);
+                    }
+                }
+            });
+        }
+    }
+
+    /**
+     * A {@link SpreadsheetCellEditor} for
+     * {@link SpreadsheetCellType.StringType} typed cells. It displays a
+     * {@link TextField} where the user can type different values.
+     */
+    public static class StringEditor extends SpreadsheetCellEditor {
+        /***************************************************************************
+         * * Private Fields * *
+         **************************************************************************/
+        private final TextField tf;
+
+        /***************************************************************************
+         * * Constructor * *
+         **************************************************************************/
+        /**
+         * Constructor for the StringEditor.
+         * @param view The SpreadsheetView
+         */
+        public StringEditor(SpreadsheetView view) {
+            super(view);
+            tf = new TextField();
+        }
+
+        /***************************************************************************
+         * * Public Methods * *
+         **************************************************************************/
+        @Override
+        public void startEdit(Object value) {
+
+            if (value instanceof String || value == null) {
+                tf.setText((String) value);
+            }
+            attachEnterEscapeEventHandler();
+
+            tf.requestFocus();
+            tf.selectAll();
+        }
+
+        @Override
+        public String getControlValue() {
+            return tf.getText();
+        }
+
+        @Override
+        public void end() {
+            tf.setOnKeyPressed(null);
+        }
+
+        @Override
+        public TextField getEditor() {
+            return tf;
+        }
+
+        /***************************************************************************
+         * * Private Methods * *
+         **************************************************************************/
+
+        private void attachEnterEscapeEventHandler() {
+            tf.setOnKeyPressed(new EventHandler<KeyEvent>() {
+                @Override
+                public void handle(KeyEvent t) {
+                    if (t.getCode() == KeyCode.ENTER) {
+                        endEdit(true);
+                    } else if (t.getCode() == KeyCode.ESCAPE) {
+                        endEdit(false);
+                    }
+                }
+            });
+        }
+    }
+
+    
+    /**
+     * A {@link SpreadsheetCellEditor} for
+     * {@link SpreadsheetCellType.StringType} typed cells. It displays a
+     * {@link TextField} where the user can type different values.
+     */
+    public static class TextAreaEditor extends SpreadsheetCellEditor {
+
+        /**
+         * *************************************************************************
+         * * Private Fields * *
+         * ************************************************************************
+         */
+        private final TextArea textArea;
+
+        /**
+         * *************************************************************************
+         * * Constructor * *
+         * ************************************************************************
+         */
+        /**
+         * Constructor for the StringEditor.
+         *
+         * @param view The SpreadsheetView
+         */
+        public TextAreaEditor(SpreadsheetView view) {
+            super(view);
+            textArea = new TextArea();
+            textArea.setWrapText(true);
+            //The textArea is not respecting the maxHeight if we are not setting the min..
+            textArea.minHeightProperty().bind(textArea.maxHeightProperty());
+        }
+
+        /**
+         * *************************************************************************
+         * * Public Methods * *
+         * ************************************************************************
+         */
+        @Override
+        public void startEdit(Object value) {
+            if (value instanceof String || value == null) {
+                textArea.setText((String) value);
+            }
+            attachEnterEscapeEventHandler();
+
+            textArea.requestFocus();
+            textArea.selectAll();
+        }
+
+        @Override
+        public String getControlValue() {
+            return textArea.getText();
+        }
+
+        @Override
+        public void end() {
+            textArea.setOnKeyPressed(null);
+        }
+
+        @Override
+        public TextArea getEditor() {
+            return textArea;
+        }
+
+        @Override
+        public double getMaxHeight() {
+            return Double.MAX_VALUE;
+        }
+        
+        /**
+         * *************************************************************************
+         * * Private Methods * *
+         * ************************************************************************
+         */
+        private void attachEnterEscapeEventHandler() {
+            
+            textArea.setOnKeyPressed(new EventHandler<KeyEvent>() {
+                @Override
+                public void handle(KeyEvent keyEvent) {
+                    if (keyEvent.getCode() == KeyCode.ENTER) {
+                        if (keyEvent.isShiftDown()) {
+                            //if shift is down, we insert a new line.
+                            textArea.replaceSelection("\n"); //$NON-NLS-1$
+                        } else {
+                            endEdit(true);
+                        }
+                    } else if (keyEvent.getCode() == KeyCode.ESCAPE) {
+                        endEdit(false);
+                    }else if(keyEvent.getCode() == KeyCode.TAB){
+                        if (keyEvent.isShiftDown()) {
+                            //if shift is down, we insert a tab.
+                            textArea.replaceSelection("\t"); //$NON-NLS-1$
+                            keyEvent.consume();
+                        } else {
+                            endEdit(true);
+                        }
+                    }
+                }
+            });
+        }
+    }
+    
+    /**
+     * A {@link SpreadsheetCellEditor} for
+     * {@link SpreadsheetCellType.DoubleType} typed cells. It displays a
+     * {@link TextField} where the user can type different numbers. Only numbers
+     * will be stored. <br>
+     * Moreover, the {@link TextField} will turn red if the value currently
+     * entered if incorrect.
+     */
+    public static class DoubleEditor extends SpreadsheetCellEditor {
+
+        /***************************************************************************
+         * * private Fields * *
+         **************************************************************************/
+        private final TextField tf;
+
+        /***************************************************************************
+         * * Constructor * *
+         **************************************************************************/
+        /**
+         * Constructor for the DoubleEditor.
+         * @param view The SpreadsheetView.
+         */
+        public DoubleEditor(SpreadsheetView view) {
+            super(view);
+            tf = new TextField() {
+                
+                @Override
+                public void insertText(int index, String text) {
+                    String fixedText = fixText(text);
+                    super.insertText(index, fixedText);
+                }
+
+                @Override
+                public void replaceText(int start, int end, String text) {
+                    String fixedText = fixText(text);
+                    super.replaceText(start, end, fixedText);
+                }
+
+                @Override
+                public void replaceText(IndexRange range, String text) {
+                    replaceText(range.getStart(), range.getEnd(), text);
+                }
+
+                private String fixText(String text) {
+                    DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(Localization.getLocale());
+                    text = text.replace(' ', '\u00a0');//$NON-NLS-1$
+                    return text.replaceAll("\\.", Character.toString(symbols.getDecimalSeparator()));//$NON-NLS-1$
+                }
+            };
+        }
+
+        /***************************************************************************
+         * * Public Methods * *
+         **************************************************************************/
+        /** {@inheritDoc} */
+        @Override
+        public void startEdit(Object value) {
+            if (value instanceof Double) {
+                //We want to set the text in its proper form regarding the Locale.
+                decimalFormat.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(Localization.getLocale()));
+                tf.setText(((Double) value).isNaN() ? "" : decimalFormat.format(value)); //$NON-NLS-1$
+            } else {
+                tf.setText(null);
+            }
+
+            tf.getStyleClass().removeAll("error"); //$NON-NLS-1$
+            attachEnterEscapeEventHandler();
+
+            tf.requestFocus();
+            tf.selectAll();
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void end() {
+            tf.setOnKeyPressed(null);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public TextField getEditor() {
+            return tf;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public String getControlValue() {
+            NumberFormat format = NumberFormat.getInstance(Localization.getLocale());
+            ParsePosition parsePosition = new ParsePosition(0);
+            if (tf.getText() != null) {
+                Number number = format.parse(tf.getText(), parsePosition);
+                if (number != null && parsePosition.getIndex() == tf.getText().length()) {
+                    return String.valueOf(number.doubleValue());
+                }
+            }
+            return tf.getText();
+        }
+
+        /***************************************************************************
+         * * Private Methods * *
+         **************************************************************************/
+
+        private void attachEnterEscapeEventHandler() {
+            tf.setOnKeyPressed(new EventHandler<KeyEvent>() {
+                @Override
+                public void handle(KeyEvent t) {
+                    if (t.getCode() == KeyCode.ENTER) {
+                        try {
+                            if (tf.getText().equals("")) { //$NON-NLS-1$
+                                endEdit(true);
+                            } else {
+                                tryParsing();
+                                endEdit(true);
+                            }
+                        } catch (Exception e) {
+                        }
+
+                    } else if (t.getCode() == KeyCode.ESCAPE) {
+                        endEdit(false);
+                    }
+                }
+            });
+            
+            tf.setOnKeyReleased(new EventHandler<KeyEvent>() {
+                @Override
+                public void handle(KeyEvent t) {
+                    try {
+                        if (tf.getText().equals("")) { //$NON-NLS-1$
+                            tf.getStyleClass().removeAll("error"); //$NON-NLS-1$
+                        } else {
+                            tryParsing();
+                            tf.getStyleClass().removeAll("error"); //$NON-NLS-1$
+                        }
+                    } catch (Exception e) {
+                        tf.getStyleClass().add("error"); //$NON-NLS-1$
+                    }
+                }
+            });
+        }
+        
+        private void tryParsing() throws ParseException {
+            NumberFormat format = NumberFormat.getNumberInstance(Localization.getLocale());
+            ParsePosition parsePosition = new ParsePosition(0);
+            format.parse(tf.getText(), parsePosition);
+            if (parsePosition.getIndex() != tf.getText().length()) {
+                throw new ParseException("Invalid input", parsePosition.getIndex());
+            }
+        }
+    }
+
+    /**
+     * A {@link SpreadsheetCellEditor} for
+     * {@link SpreadsheetCellType.DoubleType} typed cells. It displays a
+     * {@link TextField} where the user can type different numbers. Only numbers
+     * will be stored. <br>
+     * Moreover, the {@link TextField} will turn red if the value currently
+     * entered if incorrect.
+     */
+    public static class IntegerEditor extends SpreadsheetCellEditor {
+
+        /***************************************************************************
+         * * Private Fields * *
+         **************************************************************************/
+        private final TextField tf;
+
+        /***************************************************************************
+         * * Constructor * *
+         **************************************************************************/
+        /**
+         * Constructor for the IntegerEditor.
+         * @param view the SpreadsheetView
+         */
+        public IntegerEditor(SpreadsheetView view) {
+            super(view);
+            tf = new TextField();
+        }
+
+        /***************************************************************************
+         * * Public Methods * *
+         **************************************************************************/
+        /** {@inheritDoc} */
+        @Override
+        public void startEdit(Object value) {
+            if (value instanceof Integer) {
+                tf.setText(Integer.toString((Integer) value));
+            } else {
+                tf.setText(null);
+            }
+
+            tf.getStyleClass().removeAll("error"); //$NON-NLS-1$
+            attachEnterEscapeEventHandler();
+
+            tf.requestFocus();
+            tf.selectAll();
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void end() {
+            tf.setOnKeyPressed(null);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public TextField getEditor() {
+            return tf;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public String getControlValue() {
+            return tf.getText();
+        }
+
+        /***************************************************************************
+         * * Private Methods * *
+         **************************************************************************/
+
+        private void attachEnterEscapeEventHandler() {
+            tf.setOnKeyPressed(new EventHandler<KeyEvent>() {
+                @Override
+                public void handle(KeyEvent t) {
+                    if (t.getCode() == KeyCode.ENTER) {
+                        try {
+                            if (tf.getText().equals("")) { //$NON-NLS-1$
+                                endEdit(true);
+                            } else {
+                                Integer.parseInt(tf.getText());
+                                endEdit(true);
+                            }
+                        } catch (Exception e) {
+                        }
+
+                    } else if (t.getCode() == KeyCode.ESCAPE) {
+                        endEdit(false);
+                    }
+                }
+            });
+            tf.setOnKeyReleased(new EventHandler<KeyEvent>() {
+                @Override
+                public void handle(KeyEvent t) {
+                    try {
+                        if (tf.getText().equals("")) { //$NON-NLS-1$
+                            tf.getStyleClass().removeAll("error"); //$NON-NLS-1$
+                        } else {
+                            Integer.parseInt(tf.getText());
+                            tf.getStyleClass().removeAll("error"); //$NON-NLS-1$
+                        }
+                    } catch (Exception e) {
+                        tf.getStyleClass().add("error"); //$NON-NLS-1$
+                    }
+                }
+            });
+        }
+    }
+
+    /**
+     * 
+     * A {@link SpreadsheetCellEditor} for {@link SpreadsheetCellType.ListType}
+     * typed cells. It displays a {@link ComboBox} where the user can choose a
+     * value.
+     */
+    public static class ListEditor<R> extends SpreadsheetCellEditor {
+        /***************************************************************************
+         * * Private Fields * *
+         **************************************************************************/
+        private final List<String> itemList;
+        private final ComboBox<String> cb;
+        private String originalValue;
+
+        /***************************************************************************
+         * * Constructor * *
+         **************************************************************************/
+
+        /**
+         * Constructor for the ListEditor.
+         * @param view The SpreadsheetView
+         * @param itemList The items to display in the editor.
+         */
+        public ListEditor(SpreadsheetView view, final List<String> itemList) {
+            super(view);
+            this.itemList = itemList;
+            cb = new ComboBox<String>();
+            cb.setVisibleRowCount(5);
+        }
+
+        /***************************************************************************
+         * * Public Methods * *
+         **************************************************************************/
+
+        /** {@inheritDoc} */
+        @Override
+        public void startEdit(Object value) {
+            if (value instanceof String) {
+                originalValue = value.toString();
+            } else {
+                originalValue = null;
+            }
+            ObservableList<String> items = FXCollections.observableList(itemList);
+            cb.setItems(items);
+            cb.setValue(originalValue);
+
+            attachEnterEscapeEventHandler();
+            cb.show();
+            cb.requestFocus();
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void end() {
+            cb.setOnKeyPressed(null);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public ComboBox<String> getEditor() {
+            return cb;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public String getControlValue() {
+            return cb.getSelectionModel().getSelectedItem();
+        }
+
+        /***************************************************************************
+         * * Private Methods * *
+         **************************************************************************/
+
+        private void attachEnterEscapeEventHandler() {
+
+            cb.setOnKeyPressed(new EventHandler<KeyEvent>() {
+                @Override
+                public void handle(KeyEvent t) {
+                    if (t.getCode() == KeyCode.ESCAPE) {
+                        cb.setValue(originalValue);
+                        endEdit(false);
+                    } else if (t.getCode() == KeyCode.ENTER) {
+                        endEdit(true);
+                    }
+                }
+            });
+        }
+    }
+
+    /**
+     * A {@link SpreadsheetCellEditor} for {@link SpreadsheetCellType.DateType}
+     * typed cells. It displays a {@link DatePicker} where the user can choose a
+     * date through a visual calendar.
+     */
+    public static class DateEditor extends SpreadsheetCellEditor {
+
+        /***************************************************************************
+         * * Private Fields * *
+         **************************************************************************/
+        private final DatePicker datePicker;
+        private EventHandler<KeyEvent> eh;
+        private ChangeListener<LocalDate> cl;
+        /**
+         * This is needed because "endEdit" will call our "end" method too late
+         * when pressing enter, so several "endEdit" will be called. So this
+         * prevent that to happen.
+         */
+        private boolean ending = false;
+
+        /***************************************************************************
+         * * Constructor * *
+         **************************************************************************/
+        /**
+         * Constructor for the DateEditor.
+         * @param view the SpreadsheetView
+         * @param converter A Converter for converting a date to a String.
+         */
+        public DateEditor(SpreadsheetView view, StringConverter<LocalDate> converter) {
+            super(view);
+            datePicker = new DatePicker();
+            datePicker.setConverter(converter);
+        }
+
+        /***************************************************************************
+         * * Public Methods * *
+         **************************************************************************/
+        /** {@inheritDoc} */
+        @Override
+        public void startEdit(Object value) {
+            if (value instanceof LocalDate) {
+                datePicker.setValue((LocalDate) value);
+            }
+            attachEnterEscapeEventHandler();
+            datePicker.show();
+            datePicker.getEditor().requestFocus();
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void end() {
+            if (datePicker.isShowing()) {
+                datePicker.hide();
+            }
+            datePicker.removeEventFilter(KeyEvent.KEY_PRESSED, eh);
+            datePicker.valueProperty().removeListener(cl);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public DatePicker getEditor() {
+            return datePicker;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public String getControlValue() {
+            return datePicker.getEditor().getText();
+        }
+
+        /***************************************************************************
+         * * Private Methods * *
+         **************************************************************************/
+
+        private void attachEnterEscapeEventHandler() {
+            /**
+             * We need to add an EventFilter because otherwise the DatePicker
+             * will block "escape" and "enter". But when "enter" is hit, we need
+             * to runLater the commit because the value has not yet hit the
+             * DatePicker itself.
+             */
+            eh = new EventHandler<KeyEvent>() {
+                @Override
+                public void handle(KeyEvent t) {
+                    if (t.getCode() == KeyCode.ENTER) {
+                        ending = true;
+                        endEdit(true);
+                        ending = false;
+                    } else if (t.getCode() == KeyCode.ESCAPE) {
+                        endEdit(false);
+                    }
+                }
+            };
+
+            datePicker.addEventFilter(KeyEvent.KEY_PRESSED, eh);
+
+            cl = new ChangeListener<LocalDate>() {
+                @Override
+                public void changed(ObservableValue<? extends LocalDate> arg0, LocalDate arg1, LocalDate arg2) {
+                    if (!ending)
+                        endEdit(true);
+                }
+            };
+            datePicker.valueProperty().addListener(cl);
+        }
+
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/SpreadsheetCellType.java b/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/SpreadsheetCellType.java
new file mode 100644
index 0000000..6412af6
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/SpreadsheetCellType.java
@@ -0,0 +1,858 @@
+/**
+ * Copyright (c) 2013, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.spreadsheet;
+
+import java.text.DecimalFormat;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+
+import javafx.util.StringConverter;
+import javafx.util.converter.DefaultStringConverter;
+import javafx.util.converter.DoubleStringConverter;
+import javafx.util.converter.IntegerStringConverter;
+
+/**
+ * When instantiating a {@link SpreadsheetCell}, its SpreadsheetCellType will
+ * specify which values the cell can accept as user input, and which
+ * {@link SpreadsheetCellEditor} it will use to receive user input. <br>
+ * Different static methods are provided in order to give you access to basic
+ * types, and to create {@link SpreadsheetCell} easily:
+ * <ul>
+ * <li><b>String</b>: Accessible with
+ * {@link SpreadsheetCellType.StringType#createCell(int, int, int, int, String)}
+ * .</li>
+ * <li><b>List</b>: Accessible with
+ * {@link SpreadsheetCellType.ListType#createCell(int, int, int, int, String)}.</li>
+ * <li><b>Double</b>: Accessible with
+ * {@link SpreadsheetCellType.DoubleType#createCell(int, int, int, int, Double)}
+ * .</li>
+ * <li><b>Integer</b>: Accessible with
+ * {@link SpreadsheetCellType.IntegerType#createCell(int, int, int, int, Integer)}
+ * .</li>
+ * <li><b>Date</b>: Accessible with
+ * {@link SpreadsheetCellType.DateType#createCell(int, int, int, int, LocalDate)}
+ * .</li>
+ * </ul>
+ * 
+ * <h3>Value verification</h3> You can specify two levels of verification in your
+ * types. <br>
+ * <ul>
+ * <li>The first one is defined by {@link #match(Object)}. It is the first level
+ * that tells whether or not the given value should be accepted or not. Trying
+ * to set a String into a Double will return false for example. This method will
+ * be use by the {@link SpreadsheetView} when trying to set values for example.
+ * <br>
+ * </li>
+ * <li>The second level is defined by {@link #isError(Object)}. This is more
+ * subtle and allow you to tell whether the given value is coherent or not
+ * regarding the policy you gave. You can just make a {@link SpreadsheetCell}
+ * call this method when its value has changed in order to react accordingly if
+ * the value is in error. (see example below).</li>
+ * </ul>
+ * <h3>Converter</h3> You will have to specify a converter for your type. It
+ * will handle all the conversion between your real value type (Double, Integer,
+ * LocalDate etc) and its string representation for the cell. <br>
+ * You can either use a pre-built {@link StringConverter} or our
+ * {@link StringConverterWithFormat}. This one just add one method (
+ * {@link StringConverterWithFormat#toStringFormat(Object, String)} which will
+ * convert your value with a String format (found in
+ * {@link SpreadsheetCell#getFormat()}).
+ * 
+ * <h3>Example</h3> You can create several types which are using the same
+ * editor. Suppose you want to handle Double values. You will implement the
+ * {@link #createEditor(SpreadsheetView)} method and use the
+ * {@link SpreadsheetCellEditor.DoubleEditor}. <br>
+ * 
+ * Then for each type you will provide your own policy in {@link #match(Object)}
+ * and in {@link #isError(Object)}, which most of the time will use your
+ * {@link #converter}. <br>
+ * 
+ * Here is an example of how to create a {@link StringConverterWithFormat} :
+ * 
+ * 
+ * 
+ * <pre>
+ * 
+ * StringConverterWithFormat specialConverter = new StringConverterWithFormat<Double>(new DoubleStringConverter()) {
+ *            @Override
+ *             public String toString(Double item) {
+ *                  //We just redirect to the other method.
+ *                 return toStringFormat(item, "");
+ *             }
+ * 
+ *             @Override
+ *             public String toStringFormat(Double item, String format) {
+ *                 if (item == null || Double.isNaN(item)) {
+ *                     return missingLabel; // For example return something else that an empty cell.
+ *                 } else{
+ *                     if (!("").equals(format) && !Double.isNaN(item)) {
+ *                     //We format here the value
+ *                         return new DecimalFormat(format).format(item);
+ *                     } else {
+ *                     //We call the DoubleStringConverter that we gave in argument
+ *                         return myConverter.toString(item);
+ *                     }
+ *                 }
+ *             }
+ * 
+ *            @Override
+ *             public Double fromString(String str) {
+ *                 if (str == null || str.isEmpty()) {
+ *                     return Double.NaN;
+ *                 } else {
+ *                     try {
+ *                         //Just returning the value
+ *                         Double myDouble = Double.parseDouble(str);
+ *                         return myDouble;
+ * 
+ *                     } catch (NumberFormatException e) {
+ *                         return myConverter.fromString(str);
+ *                     }
+ *                 }
+ *             }
+ *         }
+ * 
+ * </pre>
+ * 
+ * And then suppose you only want to accept double values between 0 and 100, and
+ * that a value superior to 10 is abnormal. <br>
+ * 
+ * <pre>
+ * @Override
+ * public boolean isError(Object value) {
+ *     if (value instanceof Double) {
+ *         if ((Double) value > 0 && (Double) value < 10) {
+ *             return false;
+ *         }
+ *         return true;
+ *     }
+ *     return true;
+ * }
+ * 
+ * @Override
+ * public boolean match(Object value) {
+ *     if (value instanceof Double) {
+ *         return true;
+ *     } else {
+ *         try {
+ *             Double convertedValue = converter.fromString(value == null ? null : value.toString());
+ *             if (convertedValue >= 0 && convertedValue <= 100)
+ *                 return true;
+ *             else
+ *                 return false;
+ *         } catch (Exception e) {
+ *             return false;
+ *         }
+ *     }
+ * }
+ * </pre>
+ * 
+ * @see SpreadsheetView
+ * @see SpreadsheetCellEditor
+ * @see SpreadsheetCell
+ */
+public abstract class SpreadsheetCellType<T> {
+    /** An instance of converter from string to cell type. */
+    protected StringConverter<T> converter;
+
+    /**
+     * Default constructor.
+     */
+    public SpreadsheetCellType() {
+
+    }
+
+    /**
+     * Constructor with the StringConverter directly provided.
+     * 
+     * @param converter
+     *            The converter to use
+     */
+    public SpreadsheetCellType(StringConverter<T> converter) {
+        this.converter = converter;
+    }
+
+    /**
+     * Creates an editor for this type of cells.
+     * 
+     * @param view
+     *            the spreadsheet that will own this editor
+     * @return the editor instance
+     */
+    public abstract SpreadsheetCellEditor createEditor(SpreadsheetView view);
+
+    /**
+     * Return a string representation of the given item for the
+     * {@link SpreadsheetView} to display using the inner
+     * {@link SpreadsheetCellType#converter} and the specified format.
+     * 
+     * @param object
+     * @param format
+     * @return a string representation of the given item.
+     */
+    public String toString(T object, String format) {
+        return toString(object);
+    }
+
+    /**
+     * Return a string representation of the given item for the
+     * {@link SpreadsheetView} to display using the inner
+     * {@link SpreadsheetCellType#converter}.
+     * 
+     * @param object
+     * @return a string representation of the given item.
+     */
+    public abstract String toString(T object);
+
+    /**
+     * Verify that the upcoming value can be set to the current cell. This is
+     * the first level of verification to prevent affecting a text to a double
+     * or a double to a date. For closer verification, use
+     * {@link #isError(Object)}.
+     * 
+     * @param value
+     *            the value to test
+     * @return true if it matches.
+     */
+    public abstract boolean match(Object value);
+
+    /**
+     * Returns true if the value is an error regarding the specification of its
+     * type.
+     * 
+     * @param value
+     * @return true if the value is an error.
+     */
+    public boolean isError(Object value) {
+        return false;
+    }
+
+    /**
+     *
+     * @return true if this SpreadsheetCellType accepts Objects to be dropped on
+     * the {@link SpreadsheetCell}. Currently only Files can be dropped. If
+     * accepted, prepare to receive them in {@link #match(java.lang.Object) }
+     * and {@link #convertValue(java.lang.Object) }.
+     */
+    public boolean acceptDrop() {
+        return false;
+    }
+    
+    /**
+     * This method will be called when a commit is happening.<br>
+     * This method will try to convert the value, be sure to call
+     * {@link #match(Object)} before to see if this method will succeed.
+     * 
+     * @param value
+     * @return null if not valid or the correct value otherwise.
+     */
+    public abstract T convertValue(Object value);
+
+    /**
+     * The {@link SpreadsheetCell} {@link Object} type instance.
+     */
+    public static final SpreadsheetCellType<Object> OBJECT = new ObjectType();
+
+    /**
+     * The {@link SpreadsheetCell} {@link Object} type base class.
+     */
+    public static class ObjectType extends SpreadsheetCellType<Object> {
+
+        public ObjectType() {
+            this(new StringConverterWithFormat<Object>() {
+                @Override
+                public Object fromString(String arg0) {
+                    return arg0;
+                }
+
+                @Override
+                public String toString(Object arg0) {
+                    return arg0 == null ? "" : arg0.toString(); //$NON-NLS-1$
+                }
+            });
+        }
+
+        public ObjectType(StringConverterWithFormat<Object> converter) {
+            super(converter);
+        }
+
+        @Override
+        public String toString() {
+            return "object"; //$NON-NLS-1$
+        }
+
+        @Override
+        public boolean match(Object value) {
+            return true;
+        }
+
+        /**
+        * Creates a cell that hold an Object at the specified position, with the
+        * specified row/column span.
+        * 
+        * @param row
+        *            row number
+        * @param column
+        *            column number
+        * @param rowSpan
+        *            rowSpan (1 is normal)
+        * @param columnSpan
+        *            ColumnSpan (1 is normal)
+        * @param value
+        *            the value to display
+        * @return a {@link SpreadsheetCell}
+        */
+        public SpreadsheetCell createCell(final int row, final int column, final int rowSpan, final int columnSpan,
+                final Object value) {
+            SpreadsheetCell cell = new SpreadsheetCellBase(row, column, rowSpan, columnSpan, this);
+            cell.setItem(value);
+            return cell;
+        }
+
+        @Override
+        public SpreadsheetCellEditor createEditor(SpreadsheetView view) {
+            return new SpreadsheetCellEditor.ObjectEditor(view);
+        }
+
+        @Override
+        public Object convertValue(Object value) {
+            return value;
+        }
+
+        @Override
+        public String toString(Object item) {
+            return converter.toString(item);
+        }
+
+    };
+
+    /**
+     * The {@link SpreadsheetCell} {@link String} type instance.
+     */
+    public static final StringType STRING = new StringType();
+
+    /**
+     * The {@link SpreadsheetCell} {@link String} type base class.
+     */
+    public static class StringType extends SpreadsheetCellType<String> {
+
+        public StringType() {
+            this(new DefaultStringConverter());
+        }
+
+        public StringType(StringConverter<String> converter) {
+            super(converter);
+        }
+
+        @Override
+        public String toString() {
+            return "string"; //$NON-NLS-1$
+        }
+
+        @Override
+        public boolean match(Object value) {
+            return true;
+        }
+
+        /**
+        * Creates a cell that hold a String at the specified position, with the
+        * specified row/column span.
+        * 
+        * @param row
+        *            row number
+        * @param column
+        *            column number
+        * @param rowSpan
+        *            rowSpan (1 is normal)
+        * @param columnSpan
+        *            ColumnSpan (1 is normal)
+        * @param value
+        *            the value to display
+        * @return a {@link SpreadsheetCell}
+        */
+        public SpreadsheetCell createCell(final int row, final int column, final int rowSpan, final int columnSpan,
+                final String value) {
+            SpreadsheetCell cell = new SpreadsheetCellBase(row, column, rowSpan, columnSpan, this);
+            cell.setItem(value);
+            return cell;
+        }
+
+        @Override
+        public SpreadsheetCellEditor createEditor(SpreadsheetView view) {
+            return new SpreadsheetCellEditor.StringEditor(view);
+        }
+
+        @Override
+        public String convertValue(Object value) {
+            String convertedValue = converter.fromString(value == null ? null : value.toString());
+            if (convertedValue == null || convertedValue.equals("")) { //$NON-NLS-1$
+                return null;
+            }
+            return convertedValue;
+        }
+
+        @Override
+        public String toString(String item) {
+            return converter.toString(item);
+        }
+
+    };
+
+    /**
+     * The {@link SpreadsheetCell} {@link Double} type instance.
+     */
+    public static final DoubleType DOUBLE = new DoubleType();
+
+    /**
+     * The {@link SpreadsheetCell} {@link Double} type base class.
+     */
+    public static class DoubleType extends SpreadsheetCellType<Double> {
+
+        public DoubleType() {
+
+            this(new StringConverterWithFormat<Double>(new DoubleStringConverter()) {
+                @Override
+                public String toString(Double item) {
+                    return toStringFormat(item, ""); //$NON-NLS-1$
+                }
+
+                @Override
+                public Double fromString(String str) {
+                    if (str == null || str.isEmpty() || "NaN".equals(str)) { //$NON-NLS-1$
+                        return Double.NaN;
+                    } else {
+                        return myConverter.fromString(str);
+                    }
+                }
+
+                @Override
+                public String toStringFormat(Double item, String format) {
+                    try {
+                        if (item == null || Double.isNaN(item)) {
+                            return ""; //$NON-NLS-1$
+                        } else {
+                            return new DecimalFormat(format).format(item);
+                        }
+                    } catch (Exception ex) {
+                        return myConverter.toString(item);
+                    }
+                }
+            });
+        }
+
+        public DoubleType(StringConverter<Double> converter) {
+            super(converter);
+        }
+
+        @Override
+        public String toString() {
+            return "double"; //$NON-NLS-1$
+        }
+
+        /**
+        * Creates a cell that hold a Double at the specified position, with the
+        * specified row/column span.
+        * 
+        * @param row
+        *            row number
+        * @param column
+        *            column number
+        * @param rowSpan
+        *            rowSpan (1 is normal)
+        * @param columnSpan
+        *            ColumnSpan (1 is normal)
+        * @param value
+        *            the value to display
+        * @return a {@link SpreadsheetCell}
+        */
+        public SpreadsheetCell createCell(final int row, final int column, final int rowSpan, final int columnSpan,
+                final Double value) {
+            SpreadsheetCell cell = new SpreadsheetCellBase(row, column, rowSpan, columnSpan, this);
+            cell.setItem(value);
+            return cell;
+        }
+
+        @Override
+        public SpreadsheetCellEditor createEditor(SpreadsheetView view) {
+            return new SpreadsheetCellEditor.DoubleEditor(view);
+        }
+
+        @Override
+        public boolean match(Object value) {
+            if (value instanceof Double)
+                return true;
+            else {
+                try {
+                    converter.fromString(value == null ? null : value.toString());
+                    return true;
+                } catch (Exception e) {
+                    return false;
+                }
+            }
+        }
+
+        @Override
+        public Double convertValue(Object value) {
+            if (value instanceof Double)
+                return (Double) value;
+            else {
+                try {
+                    return converter.fromString(value == null ? null : value.toString());
+                } catch (Exception e) {
+                    return null;
+                }
+            }
+        }
+
+        @Override
+        public String toString(Double item) {
+            return converter.toString(item);
+        }
+
+        @Override
+        public String toString(Double item, String format) {
+            return ((StringConverterWithFormat<Double>) converter).toStringFormat(item, format);
+        }
+    };
+
+    /**
+     * The {@link SpreadsheetCell} {@link Integer} type instance.
+     */
+    public static final IntegerType INTEGER = new IntegerType();
+
+    /**
+     * The {@link SpreadsheetCell} {@link Integer} type base class.
+     */
+    public static class IntegerType extends SpreadsheetCellType<Integer> {
+
+        public IntegerType() {
+            this(new IntegerStringConverter() {
+                @Override
+                public String toString(Integer item) {
+                    if (item == null || Double.isNaN(item)) {
+                        return ""; //$NON-NLS-1$
+                    } else {
+                        return super.toString(item);
+                    }
+                }
+
+                @Override
+                public Integer fromString(String str) {
+                    if (str == null || str.isEmpty() || "NaN".equals(str)) { //$NON-NLS-1$
+                        return null;
+                    } else {
+                        // We try to integrate Double if possible by truncating
+                        // them
+                        try {
+                            Double temp = Double.parseDouble(str);
+                            return temp.intValue();
+                        } catch (Exception e) {
+                            return super.fromString(str);
+                        }
+                    }
+                }
+            });
+        }
+
+        public IntegerType(IntegerStringConverter converter) {
+            super(converter);
+        }
+
+        @Override
+        public String toString() {
+            return "Integer"; //$NON-NLS-1$
+        }
+
+        /**
+        * Creates a cell that hold a Integer at the specified position, with the
+        * specified row/column span.
+        * 
+        * @param row
+        *            row number
+        * @param column
+        *            column number
+        * @param rowSpan
+        *            rowSpan (1 is normal)
+        * @param columnSpan
+        *            ColumnSpan (1 is normal)
+        * @param value
+        *            the value to display
+        * @return a {@link SpreadsheetCell}
+        */
+        public SpreadsheetCell createCell(final int row, final int column, final int rowSpan, final int columnSpan,
+                final Integer value) {
+            SpreadsheetCell cell = new SpreadsheetCellBase(row, column, rowSpan, columnSpan, this);
+            cell.setItem(value);
+            return cell;
+        }
+
+        @Override
+        public SpreadsheetCellEditor createEditor(SpreadsheetView view) {
+            return new SpreadsheetCellEditor.IntegerEditor(view);
+        }
+
+        @Override
+        public boolean match(Object value) {
+            if (value instanceof Integer)
+                return true;
+            else {
+                try {
+                    converter.fromString(value == null ? null : value.toString());
+                    return true;
+                } catch (Exception e) {
+                    return false;
+                }
+            }
+        }
+
+        @Override
+        public Integer convertValue(Object value) {
+            if (value instanceof Integer)
+                return (Integer) value;
+            else {
+                try {
+                    return converter.fromString(value == null ? null : value.toString());
+                } catch (Exception e) {
+                    return null;
+                }
+            }
+        }
+
+        @Override
+        public String toString(Integer item) {
+            return converter.toString(item);
+        }
+    };
+
+    /**
+     * Creates a {@link ListType}.
+     * 
+     * @param items
+     *            the list items
+     * @return the instance
+     */
+    public static final ListType LIST(final List<String> items) {
+        return new ListType(items);
+    }
+
+    /**
+     * The {@link SpreadsheetCell} {@link List} type base class.
+     */
+    public static class ListType extends SpreadsheetCellType<String> {
+        protected final List<String> items;
+
+        public ListType(final List<String> items) {
+            super(new DefaultStringConverter() {
+                @Override
+                public String fromString(String str) {
+                    if (str != null && items.contains(str)) {
+                        return str;
+                    } else {
+                        return null;
+                    }
+                }
+
+            });
+            this.items = items;
+        }
+
+        @Override
+        public String toString() {
+            return "list"; //$NON-NLS-1$
+        }
+
+        /**
+        * Creates a cell that hold a String at the specified position, with the
+        * specified row/column span.
+        * 
+        * @param row
+        *            row number
+        * @param column
+        *            column number
+        * @param rowSpan
+        *            rowSpan (1 is normal)
+        * @param columnSpan
+        *            ColumnSpan (1 is normal)
+        * @param value
+        *            the value to display
+        * @return a {@link SpreadsheetCell}
+        */
+        public SpreadsheetCell createCell(final int row, final int column, final int rowSpan, final int columnSpan,
+                String value) {
+            SpreadsheetCell cell = new SpreadsheetCellBase(row, column, rowSpan, columnSpan, this);
+            if (items != null && items.size() > 0) {
+                if (value != null && items.contains(value)) {
+                    cell.setItem(value);
+                } else {
+                    cell.setItem(items.get(0));
+                }
+            }
+            return cell;
+        }
+
+        @Override
+        public SpreadsheetCellEditor createEditor(SpreadsheetView view) {
+            return new SpreadsheetCellEditor.ListEditor<>(view, items);
+        }
+
+        @Override
+        public boolean match(Object value) {
+            if (value instanceof String && items.contains(value.toString()))
+                return true;
+            else
+                return items.contains(value == null ? null : value.toString());
+        }
+
+        @Override
+        public String convertValue(Object value) {
+            return converter.fromString(value == null ? null : value.toString());
+        }
+
+        @Override
+        public String toString(String item) {
+            return converter.toString(item);
+        }
+    }
+
+    /**
+     * The {@link SpreadsheetCell} {@link LocalDate} type instance.
+     */
+    public static final DateType DATE = new DateType();
+
+    /**
+     * The {@link SpreadsheetCell} {@link LocalDate} type base class.
+     */
+    public static class DateType extends SpreadsheetCellType<LocalDate> {
+
+        /**
+         * Creates a new DateType.
+         */
+        public DateType() {
+            this(new StringConverterWithFormat<LocalDate>() {
+                @Override
+                public String toString(LocalDate item) {
+                    return toStringFormat(item, ""); //$NON-NLS-1$
+                }
+
+                @Override
+                public LocalDate fromString(String str) {
+                    try {
+                        return LocalDate.parse(str);
+                    } catch (Exception e) {
+                        return null;
+                    }
+                }
+
+                @Override
+                public String toStringFormat(LocalDate item, String format) {
+                    if (("").equals(format) && item != null) { //$NON-NLS-1$
+                        return item.toString();
+                    } else if (item != null) {
+                        return item.format(DateTimeFormatter.ofPattern(format));
+                    } else {
+                        return ""; //$NON-NLS-1$
+                    }
+                }
+            });
+        }
+
+        public DateType(StringConverter<LocalDate> converter) {
+            super(converter);
+        }
+
+        @Override
+        public String toString() {
+            return "date"; //$NON-NLS-1$
+        }
+
+        /**
+        * Creates a cell that hold a LocalDate at the specified position, with the
+        * specified row/column span.
+        * 
+        * @param row
+        *            row number
+        * @param column
+        *            column number
+        * @param rowSpan
+        *            rowSpan (1 is normal)
+        * @param columnSpan
+        *            ColumnSpan (1 is normal)
+        * @param value
+        *            the value to display
+        * @return a {@link SpreadsheetCell}
+        */
+        public SpreadsheetCell createCell(final int row, final int column, final int rowSpan, final int columnSpan,
+                final LocalDate value) {
+            SpreadsheetCell cell = new SpreadsheetCellBase(row, column, rowSpan, columnSpan, this);
+            cell.setItem(value);
+            return cell;
+        }
+
+        @Override
+        public SpreadsheetCellEditor createEditor(SpreadsheetView view) {
+            return new SpreadsheetCellEditor.DateEditor(view, converter);
+        }
+
+        @Override
+        public boolean match(Object value) {
+            if (value instanceof LocalDate)
+                return true;
+            else {
+                try {
+                    LocalDate temp = converter.fromString(value == null ? null : value.toString());
+                    return temp != null;
+                } catch (Exception e) {
+                    return false;
+                }
+            }
+        }
+
+        @Override
+        public LocalDate convertValue(Object value) {
+            if (value instanceof LocalDate)
+                return (LocalDate) value;
+            else {
+                try {
+                    return converter.fromString(value == null ? null : value.toString());
+                } catch (Exception e) {
+                    return null;
+                }
+            }
+        }
+
+        @Override
+        public String toString(LocalDate item) {
+            return converter.toString(item);
+        }
+
+        @Override
+        public String toString(LocalDate item, String format) {
+            return ((StringConverterWithFormat<LocalDate>) converter).toStringFormat(item, format);
+        }
+    }
+}
\ No newline at end of file
diff --git a/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/SpreadsheetColumn.java b/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/SpreadsheetColumn.java
new file mode 100644
index 0000000..bf45a1e
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/SpreadsheetColumn.java
@@ -0,0 +1,372 @@
+/**
+ * Copyright (c) 2013, 2016 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.spreadsheet;
+
+import static impl.org.controlsfx.i18n.Localization.asKey;
+import static impl.org.controlsfx.i18n.Localization.localize;
+import impl.org.controlsfx.spreadsheet.CellView;
+import java.util.List;
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.ReadOnlyDoubleProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.ObservableList;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.scene.control.ContextMenu;
+import javafx.scene.control.MenuItem;
+import javafx.scene.control.TableColumn;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.stage.WindowEvent;
+import org.controlsfx.tools.Utils;
+
+/**
+ * A {@link SpreadsheetView} is made up of a number of {@link SpreadsheetColumn}
+ * instances.
+ * 
+ * <h3>Configuration</h3> SpreadsheetColumns are instantiated by the
+ * {@link SpreadsheetView} itself, so there is no public constructor for this
+ * class. To access the available columns, you need to call
+ * {@link SpreadsheetView#getColumns()}.
+ * 
+ * <p>
+ * SpreadsheetColumn gives you the ability to modify some aspects of the column,
+ * for example the {@link #setPrefWidth(double) width} or
+ * {@link #setResizable(boolean) resizability} of the column.
+ * 
+ * <p>
+ * You have the ability to fix this column at the left of the SpreadsheetView by
+ * calling {@link #setFixed(boolean)}. But you are strongly advised to check if
+ * it is possible with {@link #isColumnFixable()} before calling
+ * {@link #setFixed(boolean)}. Take a look at the {@link SpreadsheetView}
+ * description to understand the fixing constraints.
+ * 
+ * <p>
+ * If the column can be fixed, a {@link ContextMenu} will appear if the user right-clicks on it. 
+ * If not, nothing will appear and the user will not have the possibility to fix it.
+ * 
+ * <h3>Screenshot</h3>
+ * The column <b>A</b> is fixed and is covering column <b>B</b> and partially
+ * column <b>C</b>. The context menu is being shown and offers the possibility
+ * to unfix the column.
+ * 
+ * <br>
+ * <br>
+ * <center><img src="fixedColumn.png" alt="Screenshot of SpreadsheetColumn"></center>
+ * 
+ * @see SpreadsheetView
+ */
+public final class SpreadsheetColumn {
+
+    /***************************************************************************
+     * * Private Fields * *
+     **************************************************************************/
+    private final SpreadsheetView spreadsheetView;
+    final TableColumn<ObservableList<SpreadsheetCell>, SpreadsheetCell> column;
+    private final boolean canFix;
+    private final Integer indexColumn;
+    private MenuItem fixItem;
+
+    /***************************************************************************
+     * * Constructor * *
+     **************************************************************************/
+    /**
+     * Creates a new SpreadsheetColumn.
+     * 
+     * @param column
+     * @param spreadsheetView
+     * @param indexColumn
+     */
+    SpreadsheetColumn(final TableColumn<ObservableList<SpreadsheetCell>, SpreadsheetCell> column,
+            final SpreadsheetView spreadsheetView, final Integer indexColumn, Grid grid) {
+        this.spreadsheetView = spreadsheetView;
+        this.column = column;
+        column.setMinWidth(0);
+        this.indexColumn = indexColumn;
+        canFix = initCanFix(grid);
+
+        // The contextMenu creation must be on the JFX thread
+        CellView.getValue(() -> {
+            column.setContextMenu(getColumnContextMenu());
+        });
+
+        // When changing frozen fixed columns, we need to update the ContextMenu.
+        spreadsheetView.fixingColumnsAllowedProperty().addListener(new ChangeListener<Boolean>() {
+
+            @Override
+            public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
+                CellView.getValue(() -> {
+                    column.setContextMenu(getColumnContextMenu());
+                });
+            }
+        });
+
+        // When ColumnsHeaders are changing, we update the text
+        grid.getColumnHeaders().addListener(new InvalidationListener() {
+            @Override
+            public void invalidated(Observable arg0) {
+                List<String> columnsHeader = spreadsheetView.getGrid().getColumnHeaders();
+                if (columnsHeader.size() <= indexColumn) {
+                    setText(Utils.getExcelLetterFromNumber(indexColumn));
+                } else if (!columnsHeader.get(indexColumn).equals(getText())) {
+                    setText(columnsHeader.get(indexColumn));
+                }
+            }
+        });
+
+        // When changing rows, we re-calculate if this columns can be fixed.
+        grid.getRows().addListener(new InvalidationListener() {
+            @Override
+            public void invalidated(Observable arg0) {
+                initCanFix(grid);
+            }
+        });
+    }
+
+    /***************************************************************************
+     * * Public Methods * *
+     **************************************************************************/
+
+    /**
+     * Return whether this column is fixed or not.
+     * 
+     * @return true if this column is fixed.
+     */
+    public boolean isFixed() {
+        return spreadsheetView.getFixedColumns().contains(this);
+    }
+
+    /**
+     * Fix this column to the left if possible, although it is recommended that
+     * you call {@link #isColumnFixable()} before trying to fix a column.
+     *
+     * If you want to fix several columns (because of a span for example), add
+     * all the columns directly in {@link SpreadsheetView#getFixedColumns() }.
+     * Always use {@link SpreadsheetView#areSpreadsheetColumnsFixable(java.util.List)
+     * } before.
+     *
+     * @param fixed
+     */
+    public void setFixed(boolean fixed) {
+        if (fixed) {
+            spreadsheetView.getFixedColumns().add(this);
+        } else {
+            spreadsheetView.getFixedColumns().removeAll(this);
+        }
+    }
+
+    /**
+     * Set the width of this column.
+     * 
+     * @param width
+     */
+    public void setPrefWidth(double width) {
+        width = Math.ceil(width);
+        if (column.getPrefWidth() == width && column.getWidth() != width) {
+            column.impl_setWidth(width);
+        } else {
+            column.setPrefWidth(width);
+        }
+        spreadsheetView.columnWidthSet(indexColumn);
+    }
+
+    /**
+     * Return the actual width of the column.
+     *
+     * @return the actual width of the column
+     */
+    public double getWidth() {
+        return column.getWidth();
+    }
+
+    /**
+     * Return the Property related to the actual width of the column.
+     *
+     * @return
+     */
+    public final ReadOnlyDoubleProperty widthProperty() {
+        return column.widthProperty();
+    }
+
+    /**
+     * Set the minimum width for this SpreadsheetColumn.
+     *
+     * @param value
+     */
+    public final void setMinWidth(double value) {
+        column.setMinWidth(value);
+    }
+
+    /**
+     * Return the minimum width for this SpreadsheetColumn.
+     *
+     * @return
+     */
+    public final double getMinWidth() {
+        return column.getMinWidth();
+    }
+
+    /**
+     * Return the Property related to the minimum width of this
+     * SpreadsheetColumn.
+     *
+     * @return
+     */
+    public final DoubleProperty minWidthProperty() {
+        return column.minWidthProperty();
+    }
+
+    /**
+     * Return the Property related to the maximum width of this
+     * SpreadsheetColumn.
+     *
+     * @return
+     */
+    public final DoubleProperty maxWidthProperty() {
+        return column.maxWidthProperty();
+    }
+
+    /**
+     * Set the maximum width for this SpreadsheetColumn.
+     *
+     * @param value
+     */
+    public final void setMaxWidth(double value) {
+        column.setMaxWidth(value);
+    }
+
+    /**
+     * Return the maximum width for this SpreadsheetColumn.
+     *
+     * @return
+     */
+    public final double getMaxWidth() {
+        return column.getMaxWidth();
+    }
+    /**
+     * If this column can be resized by the user
+     * 
+     * @param b
+     */
+    public void setResizable(boolean b) {
+        column.setResizable(b);
+    }
+
+    /**
+     * If the column is resizable, it will compute the optimum width for all the
+     * visible cells to be visible.
+     */
+    public void fitColumn() {
+        if (column.isResizable() && spreadsheetView.getCellsViewSkin() != null) {
+            spreadsheetView.getCellsViewSkin().resize(column, 100);
+        }
+    }
+
+    /**
+     * Indicate whether this column can be fixed or not. Call that method before
+     * calling {@link #setFixed(boolean)} or adding an item to
+     * {@link SpreadsheetView#getFixedColumns()}.
+     *
+     * A column cannot be fixed alone if any cell inside the column has a column
+     * span superior to one.
+     *
+     * @return true if this column is fixable.
+     */
+    public boolean isColumnFixable() {
+        return canFix && spreadsheetView.isFixingColumnsAllowed();
+    }
+
+    /***************************************************************************
+     * * Private Methods * *
+     **************************************************************************/
+    private void setText(String text) {
+        column.setText(text);
+    }
+
+    private String getText() {
+        return column.getText();
+    }
+
+    /**
+     * Generate a context Menu in order to fix/unfix some column It is shown
+     * when right-clicking on the column header
+     * 
+     * @return a context menu.
+     */
+    private ContextMenu getColumnContextMenu() {
+        if (isColumnFixable()) {
+            final ContextMenu contextMenu = new ContextMenu();
+
+            this.fixItem = new MenuItem(localize(asKey("spreadsheet.column.menu.fix"))); //$NON-NLS-1$
+            contextMenu.setOnShowing(new EventHandler<WindowEvent>() {
+
+                @Override
+                public void handle(WindowEvent event) {
+                    if (!isFixed()) {
+                        fixItem.setText(localize(asKey("spreadsheet.column.menu.fix"))); //$NON-NLS-1$
+                    } else {
+                        fixItem.setText(localize(asKey("spreadsheet.column.menu.unfix"))); //$NON-NLS-1$
+                }
+                }
+            });
+            fixItem.setGraphic(new ImageView(new Image(getClass().getResourceAsStream("pinSpreadsheetView.png")))); //$NON-NLS-1$
+            fixItem.setOnAction(new EventHandler<ActionEvent>() {
+                @Override
+                public void handle(ActionEvent arg0) {
+                    if (!isFixed()) {
+                        setFixed(true);
+                    } else {
+                        setFixed(false);
+                    }
+                }
+            });
+            contextMenu.getItems().addAll(fixItem);
+
+            return contextMenu;
+        } else {
+            return new ContextMenu();
+        }
+    }
+
+    /**
+     * Verify that you can fix this column. 
+     * 
+     * @return if it's fixable.
+     */
+    private boolean initCanFix(Grid grid) {
+        for (ObservableList<SpreadsheetCell> row : grid.getRows()) {
+            int columnSpan = row.get(indexColumn).getColumnSpan();
+            if (columnSpan > 1) {
+                return false;
+            }
+        }
+        return true;
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/SpreadsheetView.java b/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/SpreadsheetView.java
new file mode 100644
index 0000000..0cb176f
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/SpreadsheetView.java
@@ -0,0 +1,1897 @@
+/**
+ * Copyright (c) 2013, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.spreadsheet;
+
+import static impl.org.controlsfx.i18n.Localization.asKey;
+import static impl.org.controlsfx.i18n.Localization.localize;
+import impl.org.controlsfx.spreadsheet.CellView;
+import impl.org.controlsfx.spreadsheet.FocusModelListener;
+import impl.org.controlsfx.spreadsheet.GridViewSkin;
+import impl.org.controlsfx.spreadsheet.RectangleSelection.GridRange;
+import impl.org.controlsfx.spreadsheet.RectangleSelection.SelectionRange;
+import impl.org.controlsfx.spreadsheet.SpreadsheetGridView;
+import impl.org.controlsfx.spreadsheet.SpreadsheetHandle;
+import impl.org.controlsfx.spreadsheet.TableViewSpanSelectionModel;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javafx.application.Platform;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.ReadOnlyBooleanProperty;
+import javafx.beans.property.ReadOnlyObjectProperty;
+import javafx.beans.property.ReadOnlyObjectWrapper;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.beans.value.WeakChangeListener;
+import javafx.collections.FXCollections;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import javafx.collections.ObservableMap;
+import javafx.event.ActionEvent;
+import javafx.event.Event;
+import javafx.event.EventHandler;
+import javafx.event.EventType;
+import javafx.event.WeakEventHandler;
+import javafx.scene.Node;
+import javafx.scene.control.ContextMenu;
+import javafx.scene.control.Control;
+import javafx.scene.control.Menu;
+import javafx.scene.control.MenuItem;
+import javafx.scene.control.ScrollBar;
+import javafx.scene.control.SelectionMode;
+import javafx.scene.control.Skin;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TablePosition;
+import javafx.scene.control.TableView;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.input.Clipboard;
+import javafx.scene.input.ClipboardContent;
+import javafx.scene.input.DataFormat;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyCodeCombination;
+import javafx.scene.input.KeyCombination;
+import javafx.scene.input.KeyEvent;
+import javafx.stage.WindowEvent;
+import javafx.util.Pair;
+import org.controlsfx.tools.Utils;
+
+/**
+ * The SpreadsheetView is a control similar to the JavaFX {@link TableView}
+ * control but with different functionalities and use cases. The aim is to have
+ * a powerful grid where data can be written and retrieved.
+ * 
+ * <h3>Features</h3>
+ * <ul>
+ * <li>Cells can span in row and in column.</li>
+ * <li>Rows can be fixed to the top of the {@link SpreadsheetView} so that they
+ * are always visible on screen.</li>
+ * <li>Columns can be fixed to the left of the {@link SpreadsheetView} so that
+ * they are always visible on screen.</li>
+ * <li>A row header can be switched on in order to display the row number.</li>
+ * <li>Rows can be resized just like columns with click & drag.</li>
+ * <li>Both row and column header can be visible or invisible.</li>
+ * <li>Selection of several cells can be made with a click and drag.</li>
+ * <li>A copy/paste context menu is accessible with a right-click. The usual
+ * shortcuts are also working.</li>
+ * <li>{@link Picker} can be placed above column header or to the side of the
+ * row header.</li>
+ * </ul>
+ * 
+ * <br>
+ * 
+ * <h3>Fixing Rows and Columns</h3> 
+ * <br>
+ * You can fix some rows and some columns by right-clicking on their header. A
+ * context menu will appear if it's possible to fix them. When fixed, the label
+ * header will then be in italic and the background will turn to dark grey.
+ * <br>
+ * You have also the possibility to fix them manually by adding and removing
+ * items from {@link #getFixedRows()} and {@link #getFixedColumns()}. But you
+ * are strongly advised to check if it's possible to do so with
+ * {@link SpreadsheetColumn#isColumnFixable()} for the fixed columns and with
+ * {@link #isRowFixable(int)} for the fixed rows.
+ * <br>
+ *
+ * A set of rows cannot be fixed if any cell inside these rows has a row span
+ * superior to the number of fixed rows. Likewise, a set of columns cannot be
+ * fixed if any cell inside these columns has a column span superior to the
+ * number of fixed columns.
+ * 
+ * <br><br>
+ * If you want to fix several rows or columns together, and they have a span
+ * inside, you can call {@link #areRowsFixable(java.util.List) } or  {@link #areSpreadsheetColumnsFixable(java.util.List)
+ * }
+ * to verify if you can fix them. Be sure to add them all in once otherwise the
+ * system will detect that a span is going out of bounds and will throw an
+ * exception.
+ *
+ * Calling those methods prior
+ * every move will ensure that no exception will be thrown.
+ * <br><br>
+ * You have also the possibility to deactivate these possibilities. For example,
+ * you force some row/column to be fixed and then the user cannot change the 
+ * settings. 
+ * <br>
+ * 
+ * <h3>Headers</h3>
+ * <br>
+ * You can also access and toggle header's visibility by using the methods
+ * provided like {@link #setShowRowHeader(boolean) } or {@link #setShowColumnHeader(boolean)
+ * }.
+ * 
+ * <br>
+ * Users can double-click on a column header will resize the column to the best
+ * size in order to fully see each cell in it. Same rule apply for row header.
+ * Also note that double-clicking on the little space between two row or two
+ * columns (when resizable) will also work just like Excel.
+ * 
+ * <h3>Pickers</h3>
+ * <br>
+ * 
+ * You can show some little images next to the headers. They will appear on the 
+ * left of the VerticalHeader and on top on the HorizontalHeader. They are called
+ * "picker" because they were used originally for picking a row or a column to 
+ * insert in the SpreadsheetView.
+ * <br>
+ * But you can do anything you want with it. Simply put a row or a column index
+ * in {@link #getRowPickers() } and {@link #getColumnPickers() } along with an
+ * instance of {@link Picker}. You can override the {@link Picker#onClick() }
+ * method in order to react when the user click on the picker.
+ * <br>
+ * The pickers will appear on the top of the column's header and on the left of 
+ * the row's header.
+ * <br>
+ * 
+ * <h3>Copy pasting</h3> You can copy any cell you want and paste it elsewhere.
+ * Be aware that only the value inside will be pasted, not the style nor the
+ * type. Thus the value you're trying to paste must be compatible with the
+ * {@link SpreadsheetCellType} of the receiving cell. Pasting a Double into a
+ * String will work but the reverse operation will not. 
+ * <br>
+ * See {@link SpreadsheetCellType} <i>Value Verification</i> documentation for more 
+ * information.
+ * <br>
+ * A unique cell or a selection can be copied and pasted.
+ * 
+ * <br>
+ * <br>
+ * <h3>Code Samples</h3> Just like the {@link TableView}, you instantiate the
+ * underlying model, a {@link Grid}. You will create some rows filled with {@link SpreadsheetCell}.
+ * 
+ * <br>
+ * <br>
+ * 
+ * <pre>
+ * int rowCount = 15;
+ *     int columnCount = 10;
+ *     GridBase grid = new GridBase(rowCount, columnCount);
+ *     
+ *     ObservableList<ObservableList<SpreadsheetCell>> rows = FXCollections.observableArrayList();
+ *     for (int row = 0; row < grid.getRowCount(); ++row) {
+ *         final ObservableList<SpreadsheetCell> list = FXCollections.observableArrayList();
+ *         for (int column = 0; column < grid.getColumnCount(); ++column) {
+ *             list.add(SpreadsheetCellType.STRING.createCell(row, column, 1, 1,"value"));
+ *         }
+ *         rows.add(list);
+ *     }
+ *     grid.setRows(rows);
+ *
+ *     SpreadsheetView spv = new SpreadsheetView(grid);
+ *     
+ * </pre>
+ * 
+ * At that moment you can span some of the cells with the convenient method
+ * provided by the grid. Then you just need to instantiate the SpreadsheetView. <br>
+ * <h3>Visual:</h3> <center><img src="spreadsheetView.png" alt="Screenshot of SpreadsheetView"></center>
+ * 
+ * @see SpreadsheetCell
+ * @see SpreadsheetCellBase
+ * @see SpreadsheetColumn
+ * @see Grid
+ * @see GridBase
+ * @see Picker
+ */
+public class SpreadsheetView extends Control{
+
+    /***************************************************************************
+     * * Static Fields * *
+     **************************************************************************/
+
+    /**
+     * The SpanType describes in which state each cell can be. When a spanning
+     * is occurring, one cell is becoming larger and the others are becoming
+     * invisible. Thus, that particular cell is masking the others. <br>
+     * <br>
+     * But the SpanType cannot be known in advance because it's evolving for
+     * each cell during the lifetime of the {@link SpreadsheetView}. Suppose you
+     * have a cell spanning in row, the first one is in a ROW_VISIBLE state, and
+     * all the other below are in a ROW_SPAN_INVISIBLE state. But if the user is
+     * scrolling down, the first will go out of sight. At that moment, the
+     * second cell is switching from ROW_SPAN_INVISIBLE state to ROW_VISIBLE
+     * state. <br>
+     * <br>
+     * 
+     * <center><img src="spanType.png" alt="Screenshot of SpreadsheetView.SpanType"></center>
+     * Refer to {@link SpreadsheetView} for more information.
+     */
+    public static enum SpanType {
+
+        /**
+         * Visible cell, can be a unique cell (no span) or the first one inside
+         * a column spanning cell.
+         */
+        NORMAL_CELL,
+
+        /**
+         * Invisible cell because a cell in a NORMAL_CELL state on the left is
+         * covering it.
+         */
+        COLUMN_SPAN_INVISIBLE,
+
+        /**
+         * Invisible cell because a cell in a ROW_VISIBLE state on the top is
+         * covering it.
+         */
+        ROW_SPAN_INVISIBLE,
+
+        /** Visible Cell but has some cells below in a ROW_SPAN_INVISIBLE state. */
+        ROW_VISIBLE,
+
+        /**
+         * Invisible cell situated in diagonal of a cell in a ROW_VISIBLE state.
+         */
+        BOTH_INVISIBLE;
+    }
+
+    /**
+     * Default width of the VerticalHeader.
+     */
+    private static final double DEFAULT_ROW_HEADER_WIDTH = 30.0;
+    /***************************************************************************
+     * * Private Fields * *
+     **************************************************************************/
+
+    private final SpreadsheetGridView cellsView;// The main cell container.
+    private SimpleObjectProperty<Grid> gridProperty = new SimpleObjectProperty<>();
+    private DataFormat fmt;
+    
+    private final ObservableList<Integer> fixedRows = FXCollections.observableArrayList();
+    private final ObservableList<SpreadsheetColumn> fixedColumns = FXCollections.observableArrayList();
+
+    private final BooleanProperty fixingRowsAllowedProperty = new SimpleBooleanProperty(true);
+    private final BooleanProperty fixingColumnsAllowedProperty = new SimpleBooleanProperty(true);
+
+    private final BooleanProperty showColumnHeader = new SimpleBooleanProperty(true, "showColumnHeader", true); //$NON-NLS-1$
+    private final BooleanProperty showRowHeader = new SimpleBooleanProperty(true, "showRowHeader", true); //$NON-NLS-1$
+
+    private BitSet rowFix; // Compute if we can fix the rows or not.
+
+    private final ObservableMap<Integer, Picker> rowPickers = FXCollections.observableHashMap();
+
+    private final ObservableMap<Integer, Picker> columnPickers = FXCollections.observableHashMap();
+
+    // Properties needed by the SpreadsheetView and managed by the skin (source
+    // is the VirtualFlow)
+    private ObservableList<SpreadsheetColumn> columns = FXCollections.observableArrayList();
+    private Map<SpreadsheetCellType<?>, SpreadsheetCellEditor> editors = new IdentityHashMap<>();
+    private final SpreadsheetViewSelectionModel selectionModel;
+    
+    /**
+     * The vertical header width, just for the Label, not the Pickers.
+     */
+    private final DoubleProperty rowHeaderWidth = new SimpleDoubleProperty(DEFAULT_ROW_HEADER_WIDTH);
+
+    /**
+     * Since the default with applied to TableColumn is 80. If a user sets a
+     * width of 80, the column will be detected as having the default with and
+     * therefore will be requested to be autosized. In order to prevent that, we
+     * must detect which columns has been specifically set and which not. With
+     * that BitSet, we are able to make the difference between a "default" 80
+     * width applied by the system, and a 80 width applid by a user.
+     */
+    private final BitSet columnWidthSet = new BitSet();
+    // The handle that bridges with implementation.
+    final SpreadsheetHandle handle = new SpreadsheetHandle() {
+        
+        @Override
+        protected SpreadsheetView getView() {
+            return SpreadsheetView.this;
+        }
+
+        @Override
+        protected GridViewSkin getCellsViewSkin() {
+            return SpreadsheetView.this.getCellsViewSkin();
+        }
+
+        @Override
+        protected SpreadsheetGridView getGridView() {
+            return SpreadsheetView.this.getCellsView();
+        }
+
+        @Override
+        protected boolean isColumnWidthSet(int indexColumn) {
+            return columnWidthSet.get(indexColumn);
+        }
+    };
+
+    /**
+     * @return the inner table view skin
+     */
+    final GridViewSkin getCellsViewSkin() {
+        return (GridViewSkin) (cellsView.getSkin());
+    }
+
+    /**
+     * @return the inner table view
+     */
+    final SpreadsheetGridView getCellsView() {
+        return cellsView;
+    }
+    
+    /**
+     * Used by {@link SpreadsheetColumn} internally in order to specify if a
+     * column width has been set by the user.
+     *
+     * @param indexColumn
+     */
+    void columnWidthSet(int indexColumn) {
+        columnWidthSet.set(indexColumn);
+    }
+
+    /***************************************************************************
+     * * Constructor * *
+     **************************************************************************/
+
+    /**
+     * This constructor will generate sample Grid with 100 rows and 15 columns.
+     * All cells are typed as String (see {@link SpreadsheetCellType#STRING}).
+     */
+    public SpreadsheetView(){
+        this(getSampleGrid());
+        for(SpreadsheetColumn column: getColumns()){
+            column.setPrefWidth(100);
+        }
+    }
+    
+    /**
+     * Creates a SpreadsheetView control with the {@link Grid} specified.
+     *
+     * @param grid The Grid that contains the items to be rendered
+     */
+    public SpreadsheetView(final Grid grid) {
+        super();
+        //We want to recompute the rectangleHeight when a fixedRow is resized.
+        addEventHandler(RowHeightEvent.ROW_HEIGHT_CHANGE, (RowHeightEvent event) -> {
+            if(getFixedRows().contains(event.getRow()) && getCellsViewSkin() != null){
+                getCellsViewSkin().computeFixedRowHeight();
+            }
+        });
+        getStyleClass().add("SpreadsheetView"); //$NON-NLS-1$
+        // anonymous skin
+        setSkin(new Skin<SpreadsheetView>() {
+            @Override
+            public Node getNode() {
+                return SpreadsheetView.this.getCellsView();
+            }
+
+            @Override
+            public SpreadsheetView getSkinnable() {
+                return SpreadsheetView.this;
+            }
+
+            @Override
+            public void dispose() {
+                // no-op
+            }
+        });
+
+        this.cellsView = new SpreadsheetGridView(handle);
+        getChildren().add(cellsView);
+        
+        /**
+         * Add a listener to the selection model in order to edit the spanned
+         * cells when clicked
+         */
+        TableViewSpanSelectionModel tableViewSpanSelectionModel = new TableViewSpanSelectionModel(this,cellsView);
+        cellsView.setSelectionModel(tableViewSpanSelectionModel);
+        tableViewSpanSelectionModel.setCellSelectionEnabled(true);
+        tableViewSpanSelectionModel.setSelectionMode(SelectionMode.MULTIPLE);
+        selectionModel = new SpreadsheetViewSelectionModel(this, tableViewSpanSelectionModel);
+
+        /**
+         * Set the focus model to track keyboard change and redirect focus on
+         * spanned cells
+         */
+        // We add a listener on the focus model in order to catch when we are on
+        // a hidden cell
+        cellsView.getFocusModel().focusedCellProperty()
+                .addListener((ChangeListener<TablePosition>) (ChangeListener<?>) new FocusModelListener(this,cellsView));
+
+        /**
+         * Keyboard action, maybe use an accelerator
+         */
+        cellsView.setOnKeyPressed(keyPressedHandler);
+        
+        /**
+         * ContextMenu handling.
+         */
+        this.contextMenuProperty().addListener(new WeakChangeListener<>(contextMenuChangeListener));
+        // The contextMenu creation must be on the JFX thread
+        CellView.getValue(() -> {
+            setContextMenu(getSpreadsheetViewContextMenu());
+        });
+
+        setGrid(grid);
+        setEditable(true);
+        
+        // Listeners & handlers
+        fixedRows.addListener(fixedRowsListener);
+        fixedColumns.addListener(fixedColumnsListener);
+    }
+    /***************************************************************************
+     * * Public Methods * *
+     **************************************************************************/
+
+    /**
+     * Set a new Grid for the SpreadsheetView. This will be called by default by
+     * {@link #SpreadsheetView(Grid)}. So this is useful when you want to
+     * refresh your SpreadsheetView with a new model. This will keep the state
+     * of your SpreadsheetView (position of the bar, number of fixedRows etc).
+     * 
+     * @param grid the new Grid
+     */
+    public final void setGrid(Grid grid) {
+        if(grid == null){
+            return;
+        }
+        // Reactivate that after
+//        verifyGrid(grid);
+        gridProperty.set(grid);
+        initRowFix(grid);
+
+        /**
+         * We need to verify that the previous fixedRows are still compatible
+         * with our new model
+         */
+
+        List<Integer> newFixedRows = new ArrayList<>();
+        for (Integer rowFixed : getFixedRows()) {
+            if (isRowFixable(rowFixed)) {
+                newFixedRows.add(rowFixed);
+            }
+        }
+        getFixedRows().setAll(newFixedRows);
+
+        /**
+         * We need to store the index of the fixedColumns and clear then because
+         * we will keep reference to SpreadsheetColumn that no longer exist.
+         */
+        List<Integer> columnsFixed = new ArrayList<>();
+        for (SpreadsheetColumn column : getFixedColumns()) {
+            columnsFixed.add(getColumns().indexOf(column));
+        }
+        getFixedColumns().clear();
+
+        /**
+         * We try to save the width of the column as we save the height of our rows so that we preserve the state.
+         */
+        List<Double> widthColumns = new ArrayList<>();
+        for (SpreadsheetColumn column : columns) {
+            widthColumns.add(column.getWidth());
+        }
+        //We need to update the focused cell afterwards
+        Pair<Integer, Integer> focusedPair = null;
+        TablePosition focusedCell = cellsView.getFocusModel().getFocusedCell();
+        if (focusedCell != null && focusedCell.getRow() != -1 && focusedCell.getColumn() != -1) {
+            focusedPair = new Pair(focusedCell.getRow(), focusedCell.getColumn());
+        }
+
+        final Pair<Integer, Integer> finalPair = focusedPair;
+        
+        if (grid.getRows() != null) {
+            final ObservableList<ObservableList<SpreadsheetCell>> observableRows = FXCollections
+                    .observableArrayList(grid.getRows());
+            cellsView.getItems().clear();
+            cellsView.setItems(observableRows);
+
+            final int columnCount = grid.getColumnCount();
+            columns.clear();
+            for (int columnIndex = 0; columnIndex < columnCount; ++columnIndex) {
+                final SpreadsheetColumn spreadsheetColumn = new SpreadsheetColumn(getTableColumn(grid, columnIndex), this, columnIndex, grid);
+                if(widthColumns.size() > columnIndex){
+                    spreadsheetColumn.setPrefWidth(widthColumns.get(columnIndex));
+                }
+                columns.add(spreadsheetColumn);
+                // We verify if this column was fixed before and try to re-fix
+                // it.
+                if (columnsFixed.contains((Integer) columnIndex) && spreadsheetColumn.isColumnFixable()) {
+                    spreadsheetColumn.setFixed(true);
+                }
+            }
+        }
+        
+        List<Pair<Integer, Integer>> selectedCells = new ArrayList<>();
+        for (TablePosition position : getSelectionModel().getSelectedCells()) {
+            selectedCells.add(new Pair<>(position.getRow(), position.getColumn()));
+        }
+        
+        
+        /**
+         * Since the TableView is added to the sceneGraph, it's not possible to
+         * modify the columns in another thread. We normally should call
+         * Platform.runLater() and exit. But in this particular case, we need to
+         * add the tableColumn right now. So that when we exit this "setGrid"
+         * method, we are sure we can manipulate all the elements.
+         *
+         * We also try to be smart here when we already have some columns in
+         * order to re-use them and minimize the time used to add/remove
+         * columns.
+         */
+        Runnable runnable = () -> {
+            if (cellsView.getColumns().size() > grid.getColumnCount()) {
+                cellsView.getColumns().remove(grid.getColumnCount(), cellsView.getColumns().size());
+            } else if (cellsView.getColumns().size() < grid.getColumnCount()) {
+                for (int i = cellsView.getColumns().size(); i < grid.getColumnCount(); ++i) {
+                    cellsView.getColumns().add(columns.get(i).column);
+                }
+            }
+            ((TableViewSpanSelectionModel) cellsView.getSelectionModel()).verifySelectedCells(selectedCells);
+            //Just like the selected cell we update the focused cell.
+            if(finalPair != null && finalPair.getKey() < getGrid().getRowCount() && finalPair.getValue() < getGrid().getColumnCount()){
+                cellsView.getFocusModel().focus(finalPair.getKey(), cellsView.getColumns().get(finalPair.getValue()));
+            }
+        };
+        
+        if (Platform.isFxApplicationThread()) {
+            runnable.run();
+        } else {
+            try {
+                FutureTask future = new FutureTask(runnable, null);
+                Platform.runLater(future);
+                future.get();
+            } catch (InterruptedException | ExecutionException ex) {
+                Logger.getLogger(SpreadsheetView.class.getName()).log(Level.SEVERE, null, ex);
+            }
+        }
+    }
+
+    /**
+     * Return a {@link TablePosition} of cell being currently edited.
+     * 
+     * @return a {@link TablePosition} of cell being currently edited.
+     */
+    public TablePosition<ObservableList<SpreadsheetCell>, ?> getEditingCell() {
+        return cellsView.getEditingCell();
+    }
+    
+    /**
+     * Represents the current cell being edited, or null if there is no cell
+     * being edited.
+     *
+     * @return the current cell being edited, or null if there is no cell being
+     * edited.
+     */
+    public ReadOnlyObjectProperty<TablePosition<ObservableList<SpreadsheetCell>, ?>> editingCellProperty() {
+        return cellsView.editingCellProperty();
+    }
+
+    /**
+     * Return an ObservableList of the {@link SpreadsheetColumn} used. This list
+     * is filled automatically by the SpreadsheetView. Adding and removing
+     * columns should be done in the model {@link Grid}.
+     *
+     * @return An ObservableList of the {@link SpreadsheetColumn}
+     */
+    public final ObservableList<SpreadsheetColumn> getColumns() {
+        return columns;
+    }
+
+    /**
+     * Return the model Grid used by the SpreadsheetView
+     * 
+     * @return the model Grid used by the SpreadsheetView
+     */
+    public final Grid getGrid() {
+        return gridProperty.get();
+    }
+
+    /**
+     * Return a {@link ReadOnlyObjectProperty} containing the current Grid
+     * used in the SpreadsheetView.
+     * @return a {@link ReadOnlyObjectProperty}.
+     */
+    public final ReadOnlyObjectProperty<Grid> gridProperty() {
+        return gridProperty;
+    }
+
+    /**
+     * You can fix or unfix a row by modifying this list. Call
+     * {@link #isRowFixable(int)} before trying to fix a row. See
+     * {@link SpreadsheetView} description for information.
+     *
+     * @return an ObservableList of integer representing the fixedRows.
+     */
+    public ObservableList<Integer> getFixedRows() {
+        return fixedRows;
+    }
+
+    /**
+     * Indicate whether a row can be fixed or not. Call that method before
+     * adding an item with {@link #getFixedRows()} .
+     *
+     * A row cannot be fixed alone if any cell inside the row has a row span
+     * superior to one.
+     *
+     * @param row
+     * @return true if the row can be fixed.
+     */
+    public boolean isRowFixable(int row) {
+        return row >= 0 && row < rowFix.size() && isFixingRowsAllowed() ? rowFix.get(row) : false;
+    }
+    
+    /**
+     * Indicates whether a List of rows can be fixed or not.
+     *
+     * A set of rows cannot be fixed if any cell inside these rows has a row
+     * span superior to the number of fixed rows.
+     *
+     * @param list
+     * @return true if the List of row can be fixed together.
+     */
+    public boolean areRowsFixable(List<? extends Integer> list) {
+        if(list == null || list.isEmpty() || !isFixingRowsAllowed()){
+            return false;
+        }
+        final Grid grid = getGrid();
+        final int rowCount = grid.getRowCount();
+        final ObservableList<ObservableList<SpreadsheetCell>> rows = grid.getRows();
+        for (Integer row : list) {
+            if (row == null || row < 0 || row >= rowCount) {
+                return false;
+            }
+            //If this row is not fixable, we need to identify the maximum span
+            if (!isRowFixable(row)) {
+                int maxSpan = 1;
+                List<SpreadsheetCell> gridRow = rows.get(row);
+                for (SpreadsheetCell cell : gridRow) {
+                    //If the original row is not within this range, there is not need to look deeper.
+                    if (!list.contains(cell.getRow())) {
+                        return false;
+                    }
+                    //We only want to consider the original cell.
+                    if (cell.getRowSpan() > maxSpan && cell.getRow() == row) {
+                        maxSpan = cell.getRowSpan();
+                    }
+                }
+                //Then we need to verify that all rows within that span are fixed.
+                int count = row + maxSpan - 1;
+                for (int index = row + 1; index <= count; ++index) {
+                    if (!list.contains(index)) {
+                        return false;
+                    }
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Return whether change to Fixed rows are allowed.
+     *
+     * @return whether change to Fixed rows are allowed.
+     */
+    public boolean isFixingRowsAllowed() {
+        return fixingRowsAllowedProperty.get();
+    }
+
+    /**
+     * If set to true, user will be allowed to fix and unfix the rows.
+     *
+     * @param b
+     */
+    public void setFixingRowsAllowed(boolean b) {
+        fixingRowsAllowedProperty.set(b);
+    }
+
+    /**
+     * Return the Boolean property associated with the allowance of fixing or
+     * unfixing some rows.
+     *
+     * @return the Boolean property associated with the allowance of fixing or
+     * unfixing some rows.
+     */
+    public ReadOnlyBooleanProperty fixingRowsAllowedProperty() {
+        return fixingRowsAllowedProperty;
+    }
+
+    /**
+     * You can fix or unfix a column by modifying this list. Call
+     * {@link SpreadsheetColumn#isColumnFixable()} on the column before adding
+     * an item.
+     *
+     * @return an ObservableList of the fixed columns.
+     */
+    public ObservableList<SpreadsheetColumn> getFixedColumns() {
+        return fixedColumns;
+    }
+
+    /**
+     * Indicate whether this column can be fixed or not. If you have a
+     * {@link SpreadsheetColumn}, call
+     * {@link SpreadsheetColumn#isColumnFixable()} on it directly. Call that
+     * method before adding an item with {@link #getFixedColumns()} .
+     *
+     * @param columnIndex
+     * @return true if the column if fixable
+     */
+    public boolean isColumnFixable(int columnIndex) {
+        return columnIndex >= 0 && columnIndex < getColumns().size() && isFixingColumnsAllowed()
+                ? getColumns().get(columnIndex).isColumnFixable() : false;
+    }
+
+    /**
+     * Indicates whether a List of {@link SpreadsheetColumn} can be fixed or
+     * not.
+     *
+     * A set of columns cannot be fixed if any cell inside these columns has a
+     * column span superior to the number of fixed columns.
+     *
+     * @param list
+     * @return true if the List of columns can be fixed together.
+     */
+    public boolean areSpreadsheetColumnsFixable(List<? extends SpreadsheetColumn> list) {
+        List<Integer> newList = new ArrayList<>();
+        for (SpreadsheetColumn column : list) {
+            if (column != null) {
+                newList.add(columns.indexOf(column));
+            }
+        }
+        return areColumnsFixable(newList);
+    }
+
+    /**
+     * This method is the same as {@link #areSpreadsheetColumnsFixable(java.util.List)
+     * } but is using a List of {@link SpreadsheetColumn} indexes.
+     *
+     * A set of columns cannot be fixed if any cell inside these columns has a
+     * column span superior to the number of fixed columns.
+     *
+     * @param list
+     * @return true if the List of columns can be fixed together.
+     */
+    public boolean areColumnsFixable(List<? extends Integer> list) {
+        if (list == null || list.isEmpty() || !isFixingRowsAllowed()) {
+            return false;
+        }
+        final Grid grid = getGrid();
+        final int columnCount = grid.getColumnCount();
+        final ObservableList<ObservableList<SpreadsheetCell>> rows = grid.getRows();
+        for (Integer columnIndex : list) {
+            if (columnIndex == null || columnIndex < 0 || columnIndex >= columnCount) {
+                return false;
+            }
+            //If this column is not fixable, we need to identify the maximum span
+            if (!isColumnFixable(columnIndex)) {
+                int maxSpan = 1;
+                SpreadsheetCell cell;
+                for (List<SpreadsheetCell> row : rows) {
+                    cell = row.get(columnIndex);
+                    //If the original column is not within this range, there is not need to look deeper.
+                    if (!list.contains(cell.getColumn())) {
+                        return false;
+                    }
+                    //We only want to consider the original cell.
+                    if (cell.getColumnSpan() > maxSpan && cell.getColumn() == columnIndex) {
+                        maxSpan = cell.getColumnSpan();
+                    }
+                }
+                //Then we need to verify that all columns within that span are fixed.
+                int count = columnIndex + maxSpan - 1;
+                for (int index = columnIndex + 1; index <= count; ++index) {
+                    if (!list.contains(index)) {
+                        return false;
+                    }
+                }
+            }
+        }
+        return true;
+    }
+    
+    /**
+     * Return whether change to Fixed columns are allowed.
+     *
+     * @return whether change to Fixed columns are allowed.
+     */
+    public boolean isFixingColumnsAllowed() {
+        return fixingColumnsAllowedProperty.get();
+    }
+
+    /**
+     * If set to true, user will be allowed to fix and unfix the columns.
+     *
+     * @param b
+     */
+    public void setFixingColumnsAllowed(boolean b) {
+        fixingColumnsAllowedProperty.set(b);
+    }
+
+    /**
+     * Return the Boolean property associated with the allowance of fixing or
+     * unfixing some columns.
+     *
+     * @return the Boolean property associated with the allowance of fixing or
+     * unfixing some columns.
+     */
+    public ReadOnlyBooleanProperty fixingColumnsAllowedProperty() {
+        return fixingColumnsAllowedProperty;
+    }
+
+    /**
+     * Activate and deactivate the Column Header
+     *
+     * @param b
+     */
+    public final void setShowColumnHeader(final boolean b) {
+        showColumnHeader.setValue(b);
+    }
+
+    /**
+     * Return if the Column Header is showing.
+     *
+     * @return a boolean telling whether the column Header is shown
+     */
+    public final boolean isShowColumnHeader() {
+        return showColumnHeader.get();
+    }
+
+    /**
+     * BooleanProperty associated with the column Header.
+     *
+     * @return the BooleanProperty associated with the column Header.
+     */
+    public final BooleanProperty showColumnHeaderProperty() {
+        return showColumnHeader;
+    }
+
+    /**
+     * Activate and deactivate the Row Header.
+     *
+     * @param b
+     */
+    public final void setShowRowHeader(final boolean b) {
+        showRowHeader.setValue(b);
+    }
+
+    /**
+     * Return if the row Header is showing.
+     *
+     * @return a boolean telling if the row Header is being shown
+     */
+    public final boolean isShowRowHeader() {
+        return showRowHeader.get();
+    }
+
+    /**
+     * BooleanProperty associated with the row Header.
+     *
+     * @return the BooleanProperty associated with the row Header.
+     */
+    public final BooleanProperty showRowHeaderProperty() {
+        return showRowHeader;
+    }
+
+    /**
+     * This DoubleProperty represents the with of the rowHeader. This is just
+     * representing the width of the Labels, not the pickers.
+     *
+     * @return A DoubleProperty.
+     */
+    public final DoubleProperty rowHeaderWidthProperty(){
+        return rowHeaderWidth;
+    }
+    
+    /**
+     * Specify a new width for the row header.
+     *
+     * @param value
+     */
+    public final void setRowHeaderWidth(double value){
+        rowHeaderWidth.setValue(value);
+    }
+    
+    /**
+     *
+     * @return the current width of the row header.
+     */
+    public final double getRowHeaderWidth(){
+        return rowHeaderWidth.get();
+    }
+    
+    /**
+     * @return An ObservableMap with the row index as key and the Picker as a
+     * value.
+     */
+    public ObservableMap<Integer, Picker> getRowPickers() {
+        return rowPickers;
+    }
+
+    /**
+     * @return An ObservableMap with the column index as key and the Picker as a
+     * value.
+     */
+    public ObservableMap<Integer, Picker> getColumnPickers() {
+        return columnPickers;
+    }
+
+    /**
+     * This method will compute the best height for each line. That is to say
+     * a height where each content of each cell could be fully visible.\n
+     * Use this method wisely because it can degrade performance on great grid.
+     */
+    public void resizeRowsToFitContent() {
+        if (getCellsViewSkin() != null) {
+            getCellsViewSkin().resizeRowsToFitContent();
+        }
+    }
+    
+    /**
+     * This method will first apply {@link #resizeRowsToFitContent() } and then
+     * take the highest height and apply it to every row.\n
+     * Just as {@link #resizeRowsToFitContent() }, this method can be degrading
+     * your performance on great grid.
+     */
+    public void resizeRowsToMaximum(){
+        if (getCellsViewSkin() != null) {
+            getCellsViewSkin().resizeRowsToMaximum();
+        }
+    }
+    
+    /**
+     * This method will wipe all changes made to the row's height and set all row's
+     * height back to their default height defined in the model Grid.
+     */
+    public void resizeRowsToDefault() {
+        if (getCellsViewSkin() != null) {
+            getCellsViewSkin().resizeRowsToDefault();
+        }
+    }
+    
+    /**
+     * @param row
+     * @return the height of a particular row of the SpreadsheetView.
+     */
+    public double getRowHeight(int row) {
+        //Sometime, the skin is not initialised yet..
+        if (getCellsViewSkin() == null) {
+            return getGrid().getRowHeight(row);
+        } else {
+            return getCellsViewSkin().getRowHeight(row);
+        }
+    }
+    
+    /**
+     * Return the selectionModel used by the SpreadsheetView. 
+     * 
+     * @return {@link SpreadsheetViewSelectionModel}
+     */
+    public SpreadsheetViewSelectionModel getSelectionModel() {
+        return selectionModel;
+    }
+    
+    /**
+     * Scrolls the SpreadsheetView so that the given row is visible.
+     * @param row 
+     */
+    public void scrollToRow(int row){
+        cellsView.scrollTo(row);
+    }
+    
+    /**
+     * Same method as {@link ScrollBar#setValue(double) } on the verticalBar.
+     *
+     * @param value
+     */
+    public void setVBarValue(double value) {
+        if (getCellsViewSkin() == null) {
+            Platform.runLater(() -> {
+                setVBarValue(value);
+            });
+            return;
+        }
+        getCellsViewSkin().getVBar().setValue(value);
+    }
+
+    /**
+     * Same method as {@link ScrollBar#setValue(double) } on the verticalBar.
+     *
+     * @param value
+     */
+    public void setHBarValue(double value) {
+        setHBarValue(value,0);
+    }
+    
+    private void setHBarValue(double value, int attempt) {
+        if(attempt > 10){
+            return;
+        }
+        if (getCellsViewSkin() == null) {
+            final int newAttempt = ++attempt;
+            Platform.runLater(() -> {
+                setHBarValue(value, newAttempt);
+            });
+            return;
+        }
+        getCellsViewSkin().setHbarValue(value);
+    }
+
+    /**
+     * Return the value of the vertical scrollbar. See {@link ScrollBar#getValue()
+     * }
+     *
+     * @return
+     */
+    public double getVBarValue() {
+        if (getCellsViewSkin() != null && getCellsViewSkin().getVBar() != null) {
+            return getCellsViewSkin().getVBar().getValue();
+        }
+        return 0.0;
+    }
+
+    /**
+     * Return the value of the horizontal scrollbar. See {@link ScrollBar#getValue()
+     * }
+     *
+     * @return
+     */
+    public double getHBarValue() {
+        if (getCellsViewSkin() != null && getCellsViewSkin().getHBar() != null) {
+            return getCellsViewSkin().getHBar().getValue();
+        }
+        return 0.0;
+    }
+    
+    /**
+     * Scrolls the SpreadsheetView so that the given {@link SpreadsheetColumn} is visible.
+     * @param column 
+     */
+    public void scrollToColumn(SpreadsheetColumn column){
+        cellsView.scrollToColumn(column.column);
+    }
+    
+    /**
+     *
+     * Scrolls the SpreadsheetView so that the given column index is visible.
+     *
+     * @param columnIndex
+     *
+     */
+    public void scrollToColumnIndex(int columnIndex) {
+        cellsView.scrollToColumnIndex(columnIndex);
+    }
+
+    /**
+     * Return the editor associated with the CellType. (defined in
+     * {@link SpreadsheetCellType#createEditor(SpreadsheetView)}. FIXME Maybe
+     * keep the editor references inside the SpreadsheetCellType
+     * 
+     * @param cellType
+     * @return the editor associated with the CellType.
+     */
+    public final Optional<SpreadsheetCellEditor> getEditor(SpreadsheetCellType<?> cellType) {
+        if(cellType == null){
+            return Optional.empty();
+        }
+        SpreadsheetCellEditor cellEditor = editors.get(cellType);
+        if (cellEditor == null) {
+            cellEditor = cellType.createEditor(this);
+            if(cellEditor == null){
+                return Optional.empty();
+            }
+            editors.put(cellType, cellEditor);
+        }
+        return Optional.of(cellEditor);
+    }
+
+    /**
+     * Sets the value of the property editable.
+     * 
+     * @param b
+     */
+    public final void setEditable(final boolean b) {
+        cellsView.setEditable(b);
+    }
+
+    /**
+     * Gets the value of the property editable.
+     * 
+     * @return a boolean telling if the SpreadsheetView is editable.
+     */
+    public final boolean isEditable() {
+        return cellsView.isEditable();
+    }
+
+    /**
+     * Specifies whether this SpreadsheetView is editable - only if the
+     * SpreadsheetView, and the {@link SpreadsheetCell} within it are both
+     * editable will a {@link SpreadsheetCell} be able to go into its editing
+     * state.
+     * 
+     * @return the BooleanProperty associated with the editableProperty.
+     */
+    public final BooleanProperty editableProperty() {
+        return cellsView.editableProperty();
+    }
+
+    /**
+     * This Node is shown to the user when the SpreadsheetView has no content to show.
+     */
+    public final ObjectProperty<Node> placeholderProperty() {
+        return cellsView.placeholderProperty();
+    }
+
+    /**
+     * Sets the value of the placeholder property
+     *
+     * @param placeholder the node to show when the SpreadsheetView has no content to show.
+     */
+    public final void setPlaceholder(final Node placeholder) {
+        cellsView.setPlaceholder(placeholder);
+    }
+
+    /**
+     * Gets the value of the placeholder property.
+     *
+     * @return the Node used as a placeholder that is shown when the SpreadsheetView has no content to show.
+     */
+    public final Node getPlaceholder() {
+        return cellsView.getPlaceholder();
+    }
+
+    
+    /***************************************************************************
+     * COPY / PASTE METHODS
+     **************************************************************************/
+    
+    /**
+     * Put the current selection into the ClipBoard. This can be overridden by
+     * developers for custom behavior.
+     */
+    public void copyClipboard() {
+        checkFormat();
+
+        final ArrayList<GridChange> list = new ArrayList<>();
+        final ObservableList<TablePosition> posList = getSelectionModel().getSelectedCells();
+
+        for (final TablePosition<?, ?> p : posList) {
+            SpreadsheetCell cell = getGrid().getRows().get(p.getRow()).get(p.getColumn());
+            // Using SpreadsheetCell change to stock the information
+            // FIXME a dedicated class should be used
+            /**
+             * We need to add every cell contained in a span otherwise the
+             * rectangles computed when pasting will be wrong.
+             */
+            for (int row = 0; row < cell.getRowSpan(); ++row) {
+                for (int col = 0; col < cell.getColumnSpan(); ++col) {
+                    try {
+                        new ObjectOutputStream(new ByteArrayOutputStream()).writeObject(cell.getItem());
+                        list.add(new GridChange(cell.getRow() + row, cell.getColumn() + col, null, cell.getItem() == null ? null : cell.getItem()));
+                    } catch (IOException exception) {
+                        list.add(new GridChange(cell.getRow() + row, cell.getColumn() + col, null, cell.getItem() == null ? null : cell.getItem().toString()));
+                    }
+                }
+            }
+        }
+        final ClipboardContent content = new ClipboardContent();
+        content.put(fmt, list);
+        Clipboard.getSystemClipboard().setContent(content);
+    }
+
+    /**
+     * Paste one value from the clipboard over the whole selection.
+     * @param change 
+     */
+    private void pasteOneValue(GridChange change) {
+        for (TablePosition position : getSelectionModel().getSelectedCells()) {
+            tryPasteCell(position.getRow(), position.getColumn(), change.getNewValue());
+        }
+    }
+
+    /**
+     * Try to paste the given value into the given position.
+     * @param row
+     * @param column
+     * @param value 
+     */
+    private void tryPasteCell(int row, int column, Object value) {
+        final SpanType type = getSpanType(row, column);
+        if (type == SpanType.NORMAL_CELL || type == SpanType.ROW_VISIBLE) {
+            SpreadsheetCell cell = getGrid().getRows().get(row).get(column);
+            boolean succeed = cell.getCellType().match(value);
+            if (succeed) {
+                getGrid().setCellValue(cell.getRow(), cell.getColumn(),
+                        cell.getCellType().convertValue(value));
+            }
+        }
+    }
+
+    /**
+     * Try to paste the values given into the selection. If both selection are
+     * rectangles and the number of rows of the source is equal of the numbers
+     * of rows of the target AND number of columns of the target is a multiple
+     * of the number of columns of the source, then we can paste.
+     *
+     * Same goes if we invert the rows and columns.
+     * @param list
+     */
+    private void pasteMixedValues(ArrayList<GridChange> list) {
+        SelectionRange sourceSelectionRange = new SelectionRange();
+        sourceSelectionRange.fillGridRange(list);
+
+        //It means we have a rectangle.
+        if (sourceSelectionRange.getRange() != null) {
+            SelectionRange targetSelectionRange = new SelectionRange();
+            targetSelectionRange.fill(cellsView.getSelectionModel().getSelectedCells());
+            if (targetSelectionRange.getRange() != null) {
+                //If both selection are rectangle
+                GridRange sourceRange = sourceSelectionRange.getRange();
+                GridRange targetRange = targetSelectionRange.getRange();
+                int sourceRowGap = sourceRange.getBottom() - sourceRange.getTop() + 1;
+                int targetRowGap = targetRange.getBottom() - targetRange.getTop() + 1;
+
+                int sourceColumnGap = sourceRange.getRight() - sourceRange.getLeft() + 1;
+                int targetColumnGap = targetRange.getRight() - targetRange.getLeft() + 1;
+
+                final int offsetRow = targetRange.getTop() - sourceRange.getTop();
+                final int offsetCol = targetRange.getLeft() - sourceRange.getLeft();
+
+                //If the numbers of rows are the same and the targetColumnGap is a multiple of sourceColumnGap
+                if ((sourceRowGap == targetRowGap || targetRowGap == 1) && (targetColumnGap % sourceColumnGap) == 0) {
+                    for (final GridChange change : list) {
+                        int row = change.getRow() + offsetRow;
+                        int column = change.getColumn() + offsetCol;
+                        do {
+                            if (row < getGrid().getRowCount() && column < getGrid().getColumnCount()
+                                    && row >= 0 && column >= 0) {
+                                tryPasteCell(row, column, change.getNewValue());
+                            }
+                        } while ((column = column + sourceColumnGap) <= targetRange.getRight());
+                    }
+                    //If the numbers of columns are the same and the targetRowGap is a multiple of sourceRowGap
+                } else if ((sourceColumnGap == targetColumnGap || targetColumnGap == 1) && (targetRowGap % sourceRowGap) == 0) {
+                    for (final GridChange change : list) {
+
+                        int row = change.getRow() + offsetRow;
+                        int column = change.getColumn() + offsetCol;
+                        do {
+                            if (row < getGrid().getRowCount() && column < getGrid().getColumnCount()
+                                    && row >= 0 && column >= 0) {
+                                tryPasteCell(row, column, change.getNewValue());
+                            }
+                        } while ((row = row + sourceRowGap) <= targetRange.getBottom());
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * If we have several source values to paste into one cell, we do it.
+     *
+     * @param list
+     */
+    private void pasteSeveralValues(ArrayList<GridChange> list) {
+        // TODO algorithm very bad
+        int minRow = getGrid().getRowCount();
+        int minCol = getGrid().getColumnCount();
+        int maxRow = 0;
+        int maxCol = 0;
+        for (final GridChange p : list) {
+            final int tempcol = p.getColumn();
+            final int temprow = p.getRow();
+            if (tempcol < minCol) {
+                minCol = tempcol;
+            }
+            if (tempcol > maxCol) {
+                maxCol = tempcol;
+            }
+            if (temprow < minRow) {
+                minRow = temprow;
+            }
+            if (temprow > maxRow) {
+                maxRow = temprow;
+            }
+        }
+
+        final TablePosition<?, ?> p = cellsView.getFocusModel().getFocusedCell();
+
+        final int offsetRow = p.getRow() - minRow;
+        final int offsetCol = p.getColumn() - minCol;
+        final int rowCount = getGrid().getRowCount();
+        final int columnCount = getGrid().getColumnCount();
+        int row;
+        int column;
+
+        for (final GridChange change : list) {
+            row = change.getRow() + offsetRow;
+            column = change.getColumn() + offsetCol;
+            if (row < rowCount && column < columnCount
+                    && row >= 0 && column >= 0) {
+                tryPasteCell(row, column, change.getNewValue());
+            }
+        }
+    }
+    
+    /**
+     * Try to paste the clipBoard to the specified position. Try to paste the
+     * current selection into the Grid. If the two contents are not matchable,
+     * then it's not pasted. This can be overridden by developers for custom
+     * behavior.
+     */
+    public void pasteClipboard() {
+        // FIXME Maybe move editableProperty to the model..
+        List<TablePosition> selectedCells = cellsView.getSelectionModel().getSelectedCells();
+        if (!isEditable() || selectedCells.isEmpty()) {
+            return;
+        }
+
+        checkFormat();
+        final Clipboard clipboard = Clipboard.getSystemClipboard();
+        if (clipboard.getContent(fmt) != null) {
+
+            @SuppressWarnings("unchecked")
+            final ArrayList<GridChange> list = (ArrayList<GridChange>) clipboard.getContent(fmt);
+            if (list.size() == 1) {
+                pasteOneValue(list.get(0));
+            } else if (selectedCells.size() > 1) {
+                pasteMixedValues(list);
+            } else {
+                pasteSeveralValues(list);
+            }
+            // To be improved
+        } else if (clipboard.hasString()) {
+            // final TablePosition<?,?> p =
+            // cellsView.getFocusModel().getFocusedCell();
+            //
+            // SpreadsheetCell stringCell =
+            // SpreadsheetCellType.STRING.createCell(0, 0, 1, 1,
+            // clipboard.getString());
+            // getGrid().getRows().get(p.getRow()).get(p.getColumn()).match(stringCell);
+
+        }
+    }
+
+    /**
+     * Create a menu on rightClick with two options: Copy/Paste This can be
+     * overridden by developers for custom behavior.
+     * 
+     * @return the ContextMenu to use.
+     */
+    public ContextMenu getSpreadsheetViewContextMenu() {
+        final ContextMenu contextMenu = new ContextMenu();
+
+        final MenuItem copyItem = new MenuItem(localize(asKey("spreadsheet.view.menu.copy"))); //$NON-NLS-1$
+        copyItem.setGraphic(new ImageView(new Image(SpreadsheetView.class
+                .getResourceAsStream("copySpreadsheetView.png")))); //$NON-NLS-1$
+        copyItem.setAccelerator(new KeyCodeCombination(KeyCode.C, KeyCombination.SHORTCUT_DOWN));
+        copyItem.setOnAction(new EventHandler<ActionEvent>() {
+            @Override
+            public void handle(ActionEvent e) {
+                copyClipboard();
+            }
+        });
+
+        final MenuItem pasteItem = new MenuItem(localize(asKey("spreadsheet.view.menu.paste"))); //$NON-NLS-1$
+        pasteItem.setGraphic(new ImageView(new Image(SpreadsheetView.class
+                .getResourceAsStream("pasteSpreadsheetView.png")))); //$NON-NLS-1$
+        pasteItem.setAccelerator(new KeyCodeCombination(KeyCode.V, KeyCombination.SHORTCUT_DOWN));
+        pasteItem.setOnAction(new EventHandler<ActionEvent>() {
+            @Override
+            public void handle(ActionEvent e) {
+                pasteClipboard();
+            }
+        });
+        
+        final Menu cornerMenu = new Menu(localize(asKey("spreadsheet.view.menu.comment"))); //$NON-NLS-1$
+        cornerMenu.setGraphic(new ImageView(new Image(SpreadsheetView.class
+                .getResourceAsStream("comment.png")))); //$NON-NLS-1$
+
+        final MenuItem topLeftItem = new MenuItem(localize(asKey("spreadsheet.view.menu.comment.top-left"))); //$NON-NLS-1$
+        topLeftItem.setOnAction(new EventHandler<ActionEvent>() {
+
+            @Override
+            public void handle(ActionEvent t) {
+                TablePosition<ObservableList<SpreadsheetCell>, ?> pos = cellsView.getFocusModel().getFocusedCell();
+                SpreadsheetCell cell = getGrid().getRows().get(pos.getRow()).get(pos.getColumn());
+                cell.activateCorner(SpreadsheetCell.CornerPosition.TOP_LEFT);
+                }
+        });
+        final MenuItem topRightItem = new MenuItem(localize(asKey("spreadsheet.view.menu.comment.top-right"))); //$NON-NLS-1$
+        topRightItem.setOnAction(new EventHandler<ActionEvent>() {
+
+            @Override
+            public void handle(ActionEvent t) {
+                TablePosition<ObservableList<SpreadsheetCell>, ?> pos = cellsView.getFocusModel().getFocusedCell();
+                SpreadsheetCell cell = getGrid().getRows().get(pos.getRow()).get(pos.getColumn());
+                cell.activateCorner(SpreadsheetCell.CornerPosition.TOP_RIGHT);
+            }
+        });
+        final MenuItem bottomRightItem = new MenuItem(localize(asKey("spreadsheet.view.menu.comment.bottom-right"))); //$NON-NLS-1$
+        bottomRightItem.setOnAction(new EventHandler<ActionEvent>() {
+
+            @Override
+            public void handle(ActionEvent t) {
+                TablePosition<ObservableList<SpreadsheetCell>, ?> pos = cellsView.getFocusModel().getFocusedCell();
+                SpreadsheetCell cell = getGrid().getRows().get(pos.getRow()).get(pos.getColumn());
+                cell.activateCorner(SpreadsheetCell.CornerPosition.BOTTOM_RIGHT);
+            }
+        });
+        final MenuItem bottomLeftItem = new MenuItem(localize(asKey("spreadsheet.view.menu.comment.bottom-left"))); //$NON-NLS-1$
+        bottomLeftItem.setOnAction(new EventHandler<ActionEvent>() {
+
+            @Override
+            public void handle(ActionEvent t) {
+                TablePosition<ObservableList<SpreadsheetCell>, ?> pos = cellsView.getFocusModel().getFocusedCell();
+                SpreadsheetCell cell = getGrid().getRows().get(pos.getRow()).get(pos.getColumn());
+                cell.activateCorner(SpreadsheetCell.CornerPosition.BOTTOM_LEFT);
+            }
+        });
+
+        cornerMenu.getItems().addAll(topLeftItem, topRightItem, bottomRightItem, bottomLeftItem);
+        
+        contextMenu.getItems().addAll(copyItem, pasteItem, cornerMenu);
+        return contextMenu;
+    }
+
+    /**
+     * This method is called when pressing the "delete" key on the
+     * SpreadsheetView. This will erase the values of selected cells. This can
+     * be overridden by developers for custom behavior.
+     */
+    public void deleteSelectedCells() {
+        for (TablePosition<ObservableList<SpreadsheetCell>, ?> position : getSelectionModel().getSelectedCells()) {
+            getGrid().setCellValue(position.getRow(), position.getColumn(), null);
+        }
+    }
+    
+     /**
+     * Return the {@link SpanType} of a cell, this is a shorcut for 
+     * {@link Grid#getSpanType(org.controlsfx.control.spreadsheet.SpreadsheetView, int, int) }.
+     * 
+     * @param row
+     * @param column
+     * @return The {@link SpanType} of a cell
+     */
+    public SpanType getSpanType(final int row, final int column) {
+        if (getGrid() == null) {
+            return SpanType.NORMAL_CELL;
+        }
+        return getGrid().getSpanType(this, row, column);
+    }
+
+    /***************************************************************************
+     * * Private/Protected Implementation * *
+     **************************************************************************/
+
+    /**
+     * This is called when setting a Grid. The main idea is to re-use
+     * TableColumn if possible. Because we can have a great amount of time spent
+     * in com.sun.javafx.css.StyleManager.forget when removing lots of columns
+     * and adding new ones. So if we already have some, we can just re-use them
+     * so we avoid doign all the fuss with the TableColumns.
+     *
+     * @param grid
+     * @param columnIndex
+     * @return
+     */
+    private TableColumn<ObservableList<SpreadsheetCell>, SpreadsheetCell> getTableColumn(Grid grid, int columnIndex) {
+
+        TableColumn<ObservableList<SpreadsheetCell>, SpreadsheetCell> column;
+
+        String columnHeader = grid.getColumnHeaders().size() > columnIndex ? grid
+                .getColumnHeaders().get(columnIndex) : Utils.getExcelLetterFromNumber(columnIndex);
+
+        if (columnIndex < cellsView.getColumns().size()) {
+            column = (TableColumn<ObservableList<SpreadsheetCell>, SpreadsheetCell>) cellsView.getColumns().get(columnIndex);
+            column.setText(columnHeader);
+        } else {
+            column = new TableColumn<>(columnHeader);
+
+            column.setEditable(true);
+            // We don't want to sort the column
+            column.setSortable(false);
+
+            column.impl_setReorderable(false);
+
+            // We assign a DataCell for each Cell needed (MODEL).
+            column.setCellValueFactory((TableColumn.CellDataFeatures<ObservableList<SpreadsheetCell>, SpreadsheetCell> p) -> {
+                if (columnIndex >= p.getValue().size()) {
+                    return null;
+                }
+                return new ReadOnlyObjectWrapper<>(p.getValue().get(columnIndex));
+            });
+            // We create a SpreadsheetCell for each DataCell in order to
+            // specify how to represent the DataCell(VIEW)
+            column.setCellFactory((TableColumn<ObservableList<SpreadsheetCell>, SpreadsheetCell> p) -> new CellView(handle));
+        }
+        return column;
+    }
+    
+    /**
+     * This static method creates a sample Grid with 100 rows and 15 columns.
+     * All cells are typed as String.
+     *
+     * @return the sample Grid
+     * @see SpreadsheetCellType#STRING
+     */
+    private static Grid getSampleGrid() {
+        GridBase gridBase = new GridBase(100, 15);
+        List<ObservableList<SpreadsheetCell>> rows = FXCollections.observableArrayList();
+
+        for (int row = 0; row < gridBase.getRowCount(); ++row) {
+            ObservableList<SpreadsheetCell> currentRow = FXCollections.observableArrayList();
+            for (int column = 0; column < gridBase.getColumnCount(); ++column) {
+                currentRow.add(SpreadsheetCellType.STRING.createCell(row, column, 1, 1, "toto"));
+            }
+            rows.add(currentRow);
+        }
+        gridBase.setRows(rows);
+        return gridBase;
+    }
+    
+    private void initRowFix(Grid grid) {
+        ObservableList<ObservableList<SpreadsheetCell>> rows = grid.getRows();
+        rowFix = new BitSet(rows.size());
+        rows:
+        for (int r = 0; r < rows.size(); ++r) {
+            ObservableList<SpreadsheetCell> row = rows.get(r);
+            for (SpreadsheetCell cell : row) {
+                if (cell.getRowSpan() > 1) {
+                    continue rows;
+                }
+            }
+            rowFix.set(r);
+        }
+    }
+    
+    /**
+     * Verify that the grid is well-formed. Can be quite time-consuming I guess
+     * so I would like it not to be compulsory..
+     * 
+     * @param grid
+     */
+    private void verifyGrid(Grid grid) {
+        verifyColumnSpan(grid);
+    }
+
+    private void verifyColumnSpan(Grid grid) {
+        for (int i = 0; i < grid.getRows().size(); ++i) {
+            ObservableList<SpreadsheetCell> row = grid.getRows().get(i);
+            int count = 0;
+            for (int j = 0; j < row.size(); ++j) {
+                if (row.get(j).getColumnSpan() == 1) {
+                    ++count;
+                } else if (row.get(j).getColumnSpan() > 1) {
+                    ++count;
+                    SpreadsheetCell currentCell = row.get(j);
+                    for (int k = j + 1; k < currentCell.getColumn() + currentCell.getColumnSpan(); ++k) {
+                        if (!row.get(k).equals(currentCell)) {
+                            throw new IllegalStateException("\n At row " + i + " and column " + j //$NON-NLS-1$ //$NON-NLS-2$
+                                    + ": this cell is in the range of a columnSpan but is different. \n" //$NON-NLS-1$
+                                    + "Every cell in a range of a ColumnSpan must be of the same instance."); //$NON-NLS-1$
+                        }
+                        ++count;
+                        ++j;
+                    }
+                } else {
+                    throw new IllegalStateException("\n At row " + i + " and column " + j //$NON-NLS-1$ //$NON-NLS-2$
+                            + ": this cell has a negative columnSpan"); //$NON-NLS-1$
+                }
+            }
+            if (count != grid.getColumnCount()) {
+                throw new IllegalStateException("The row" + i //$NON-NLS-1$
+                        + " has a number of cells different of the columnCount declared in the grid."); //$NON-NLS-1$
+            }
+        }
+    }
+
+    private void checkFormat() {
+        if ((fmt = DataFormat.lookupMimeType("SpreadsheetView")) == null) { //$NON-NLS-1$
+            fmt = new DataFormat("SpreadsheetView"); //$NON-NLS-1$
+        }
+    }
+
+    /**
+     * ********************************************************************* *
+     * private listeners
+     * ********************************************************************
+     */
+
+    private final ListChangeListener<Integer> fixedRowsListener = new ListChangeListener<Integer>() {
+        @Override
+        public void onChanged(ListChangeListener.Change<? extends Integer> c) {
+            while (c.next()) {
+                if (c.wasAdded()) {
+                    List<? extends Integer> newRows = c.getAddedSubList();
+                    if(!areRowsFixable(newRows)){
+                        throw new IllegalArgumentException(computeReason(newRows));
+                    }
+                    FXCollections.sort(fixedRows);
+                }
+                
+                if(c.wasRemoved()){
+                    //Handle this case.
+                }
+            }
+        }
+    };
+
+        private String computeReason(List<? extends Integer> list) {
+        String reason = "\n A row cannot be fixed. \n"; //$NON-NLS-1$
+
+        for (Integer row : list) {
+            //If this row is not fixable, we need to identify the maximum span
+            if (!isRowFixable(row)) {
+
+                int maxSpan = 1;
+                List<SpreadsheetCell> gridRow = getGrid().getRows().get(row);
+                for (SpreadsheetCell cell : gridRow) {
+                    if(!list.contains(cell.getRow())){
+                        reason += "The row " + row + " is inside a row span and the starting row " + cell.getRow() + " is not fixed.\n"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+                    }
+                    //We only want to consider the original cell.
+                    if (cell.getRowSpan() > maxSpan && cell.getRow() == row) {
+                        maxSpan = cell.getRowSpan();
+                    }
+                }
+                //Then we need to verify that all rows within that span are fixed.
+                int count = row + maxSpan - 1;
+                for (int index = row + 1; index < count; ++index) {
+                    if (!list.contains(index)) {
+                        reason += "One cell on the row " + row + " has a row span of " + maxSpan + ". " //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+                                + "But the row " + index + " contained within that span is not fixed.\n"; //$NON-NLS-1$ //$NON-NLS-2$
+                    }
+                }
+            }
+        }
+        return reason;
+    }
+
+    private final ListChangeListener<SpreadsheetColumn> fixedColumnsListener = new ListChangeListener<SpreadsheetColumn>() {
+        @Override
+        public void onChanged(ListChangeListener.Change<? extends SpreadsheetColumn> c) {
+            while (c.next()) {
+                if (c.wasAdded()) {
+                    List<? extends SpreadsheetColumn> newColumns = c.getAddedSubList();
+                    if (!areSpreadsheetColumnsFixable(newColumns)) {
+                        List<Integer> newList = new ArrayList<>();
+                        for (SpreadsheetColumn column : newColumns) {
+                            if (column != null) {
+                                newList.add(columns.indexOf(column));
+                            }
+                        }
+                        throw new IllegalArgumentException(computeReason(newList));
+                    }
+                }
+            }
+        }
+
+        private String computeReason(List<Integer> list) {
+
+            String reason = "\n This column cannot be fixed."; //$NON-NLS-1$
+            final ObservableList<ObservableList<SpreadsheetCell>> rows = getGrid().getRows();
+            for (Integer columnIndex : list) {
+                //If this row is not fixable, we need to identify the maximum span
+                if (!isColumnFixable(columnIndex)) {
+                    int maxSpan = 1;
+                    SpreadsheetCell cell;
+                    for (List<SpreadsheetCell> row : rows) {
+                        cell = row.get(columnIndex);
+                        //If the original column is not within this range, there is not need to look deeper.
+                        if (!list.contains(cell.getColumn())) {
+                            reason += "The column " + columnIndex + " is inside a column span and the starting column " + cell.getColumn() + " is not fixed.\n"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+                        }
+                        //We only want to consider the original cell.
+                        if (cell.getColumnSpan() > maxSpan && cell.getColumn() == columnIndex) {
+                            maxSpan = cell.getColumnSpan();
+                        }
+                    }
+                    //Then we need to verify that all columns within that span are fixed.
+                    int count = columnIndex + maxSpan - 1;
+                    for (int index = columnIndex + 1; index < count; ++index) {
+                        if (!list.contains(index)) {
+                            reason += "One cell on the column " + columnIndex + " has a column span of " + maxSpan + ". " //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+                                    + "But the column " + index + " contained within that span is not fixed.\n"; //$NON-NLS-1$ //$NON-NLS-2$
+                        }
+                    }
+                }
+            }
+            return reason;
+        }
+    };
+
+    private final ChangeListener<ContextMenu> contextMenuChangeListener = new ChangeListener<ContextMenu>() {
+        
+        @Override
+        public void changed(ObservableValue<? extends ContextMenu> arg0, ContextMenu oldContextMenu, final ContextMenu newContextMenu) {
+            if(oldContextMenu !=null){
+                oldContextMenu.setOnShowing(null);
+            }
+            if(newContextMenu != null){
+                newContextMenu.setOnShowing(new WeakEventHandler<>(hideContextMenuEventHandler));
+            }
+        }
+    };
+    
+    private final EventHandler<WindowEvent> hideContextMenuEventHandler = new EventHandler<WindowEvent>() {
+        @Override
+        public void handle(WindowEvent arg0) {
+            // We don't want to open a contextMenu when editing
+            // because editors
+            // have their own contextMenu
+            if (getEditingCell() != null) {
+                // We're being reactive but we want to be pro-active
+                // so we may need a work-around.
+                Platform.runLater(()->{
+                    getContextMenu().hide();
+                });
+            }
+        }
+    };
+    
+    private final EventHandler<KeyEvent> keyPressedHandler = (KeyEvent keyEvent) -> {
+        TablePosition<ObservableList<SpreadsheetCell>, ?> position = getSelectionModel().getFocusedCell();
+        // Go to the next row only if we're not editing
+        if (getEditingCell() == null && KeyCode.ENTER.equals(keyEvent.getCode())) {
+            if (position != null) {
+                if(keyEvent.isShiftDown()){
+                    getSelectionModel().clearAndSelectPreviousCell();
+                }else{
+                    getSelectionModel().clearAndSelectNextCell();
+                }
+                //We consume the event because we don't want to go in edition
+                keyEvent.consume();
+            }
+            getCellsViewSkin().scrollHorizontally();
+            // Go to next cell
+        } else if (getEditingCell() == null && KeyCode.TAB.equals(keyEvent.getCode())) {
+            if (position != null) {
+                if (keyEvent.isShiftDown()) {
+                    getSelectionModel().clearAndSelectLeftCell();
+                } else {
+                    getSelectionModel().clearAndSelectRightCell();
+                }
+            }
+            //We consume the event because we don't want to loose focus
+            keyEvent.consume();
+            getCellsViewSkin().scrollHorizontally();
+            // We want to erase values when delete key is pressed.
+        } else if (KeyCode.DELETE.equals(keyEvent.getCode())) {
+            deleteSelectedCells();
+            /**
+             * We want NOT to go in edition if we're pressing SHIFT and if we're
+             * using the navigation keys. But we still want the user to go in
+             * edition with SHIFT and some letters for example if he wants a
+             * capital letter.
+             * FIXME Add a test to prevent the Shift fail case.
+             */
+        }else if (keyEvent.getCode() != KeyCode.SHIFT && !keyEvent.isShortcutDown() 
+                && !keyEvent.getCode().isNavigationKey() 
+                && keyEvent.getCode() != KeyCode.ESCAPE) {
+            getCellsView().edit(position.getRow(), position.getTableColumn());
+        }
+    };
+    
+    /**
+     * This event is thrown on the SpreadsheetView when the user resize a row
+     * with its mouse.
+     */
+    public static class RowHeightEvent extends Event {
+
+        /**
+         * This is the event used by {@link RowHeightEvent}.
+         */
+        public static final EventType<RowHeightEvent> ROW_HEIGHT_CHANGE = new EventType<>(Event.ANY, "RowHeightChange"); //$NON-NLS-1$
+
+        private final int row;
+        private final double height;
+
+        public RowHeightEvent(int row, double height) {
+            super(ROW_HEIGHT_CHANGE);
+            this.row = row;
+            this.height = height;
+        }
+
+        /**
+         * Return the row index that has been resized.
+         * @return the row index that has been resized.
+         */
+        public int getRow() {
+            return row;
+        }
+
+        /**
+         * Return the new height for this row.
+         * @return the new height for this row.
+         */
+        public double getHeight() {
+            return height;
+        }
+    }
+    
+    /**
+     * This event is thrown on the SpreadsheetView when the user resize a column
+     * with its mouse.
+     */
+    public static class ColumnWidthEvent extends Event {
+
+        /**
+         * This is the event used by {@link ColumnWidthEvent}.
+         */
+        public static final EventType<ColumnWidthEvent> COLUMN_WIDTH_CHANGE = new EventType<>(Event.ANY, "ColumnWidthChange"); //$NON-NLS-1$
+
+        private final int column;
+        private final double width;
+
+        public ColumnWidthEvent(int column, double width) {
+            super(COLUMN_WIDTH_CHANGE);
+            this.column = column;
+            this.width = width;
+        }
+
+        /**
+         * Return the column index that has been resized.
+         * @return the column index that has been resized.
+         */
+        public int getColumn() {
+            return column;
+        }
+
+        /**
+         * Return the new width for this column.
+         * @return the new width for this column.
+         */
+        public double getWidth() {
+            return width;
+        }
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/SpreadsheetViewSelectionModel.java b/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/SpreadsheetViewSelectionModel.java
new file mode 100644
index 0000000..9561180
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/SpreadsheetViewSelectionModel.java
@@ -0,0 +1,237 @@
+/**
+ * Copyright (c) 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.spreadsheet;
+
+import impl.org.controlsfx.spreadsheet.FocusModelListener;
+import impl.org.controlsfx.spreadsheet.TableViewSpanSelectionModel;
+import java.util.Arrays;
+import java.util.List;
+import javafx.collections.ObservableList;
+import javafx.scene.control.SelectionMode;
+import javafx.scene.control.TablePosition;
+import javafx.scene.control.TableView;
+import javafx.util.Pair;
+
+/**
+ *
+ * This class provides basic support for common interaction on the
+ * {@link SpreadsheetView}.
+ *
+ * Due to the complexity induced by cell's span, it is not possible to give a
+ * full access to selectionModel like in the {@link TableView}.
+ */
+public class SpreadsheetViewSelectionModel {
+
+    private final TableViewSpanSelectionModel selectionModel;
+    private final SpreadsheetView spv;
+
+    SpreadsheetViewSelectionModel(SpreadsheetView spv, TableViewSpanSelectionModel selectionModel) {
+        this.spv = spv;
+        this.selectionModel = selectionModel;
+    }
+    
+    /**
+     * Clears all selection, and then selects the cell at the given row/column intersection.
+     * @param row
+     * @param column 
+     */
+    public final void clearAndSelect(int row, SpreadsheetColumn column) {
+        selectionModel.clearAndSelect(row, column.column);
+    }
+    
+    /**
+     * Selects the cell at the given row/column intersection.
+     * @param row
+     * @param column 
+     */
+    public final void select(int row, SpreadsheetColumn column) {
+        selectionModel.select(row,column.column);
+    }
+    
+    /**
+     * Clears the selection model of all selected indices.
+     */
+    public final void clearSelection() {
+        selectionModel.clearSelection();
+    }
+    
+    /**
+     * A read-only ObservableList representing the currently selected cells in this SpreadsheetView. 
+     * @return A read-only ObservableList.
+     */
+    public final ObservableList<TablePosition> getSelectedCells() {
+        return selectionModel.getSelectedCells();
+    }
+    
+    /**
+     * Select all the possible cells.
+     */
+    public final void selectAll() {
+        selectionModel.selectAll();
+    }
+    
+    /**
+     * Return the position of the cell that has current focus. 
+     * @return the position of the cell that has current focus. 
+     */
+    public final TablePosition getFocusedCell(){
+        return selectionModel.getTableView().getFocusModel().getFocusedCell();
+    }
+    
+    /**
+     * Causes the cell at the given index to receive the focus.
+     * @param row The row index of the item to give focus to.
+     * @param column The column of the item to give focus to. Can be null.
+     */
+    public final void focus(int row, SpreadsheetColumn column){
+        selectionModel.getTableView().getFocusModel().focus(row, column.column);
+    }
+    
+    /**
+     * Specifies the selection mode to use in this selection model. The
+     * selection mode specifies how many items in the underlying data model can
+     * be selected at any one time. By default, the selection mode is
+     * {@link SelectionMode#MULTIPLE}.
+     *
+     * @param value
+     */
+    public final void setSelectionMode(SelectionMode value) {
+        selectionModel.setSelectionMode(value);
+    }
+    
+    /**
+     * Return the selectionMode currently used.
+     *
+     * @return the selectionMode currently used.
+     */
+    public SelectionMode getSelectionMode() {
+        return selectionModel.getSelectionMode();
+    }
+    
+    
+    /**
+     * Use this method to select discontinuous cells.
+     *
+     * The {@link Pair} must contain the row index as key and the column index
+     * as value. This is useful when you want to select a great amount of cell
+     * because it will be more efficient than calling
+     * {@link #select(int, org.controlsfx.control.spreadsheet.SpreadsheetColumn) }.
+     *
+     * @param selectedCells
+     */
+    public void selectCells(List<Pair<Integer, Integer>> selectedCells) {
+        selectionModel.verifySelectedCells(selectedCells);
+    }
+    
+    /**
+     * Use this method to select discontinuous cells.
+     *
+     * The {@link Pair} must contain the row index as key and the column index
+     * as value. This is useful when you want to select a great amount of cell
+     * because it will be more efficient than calling
+     * {@link #select(int, org.controlsfx.control.spreadsheet.SpreadsheetColumn) }.
+     * @param selectedCells
+     */
+    public void selectCells(Pair<Integer, Integer>... selectedCells) {
+        selectionModel.verifySelectedCells(Arrays.asList(selectedCells));
+    }
+    
+    /**
+     * Selects the cells in the range (minRow, minColumn) to (maxRow, maxColumn), inclusive.
+     * @param minRow
+     * @param minColumn
+     * @param maxRow
+     * @param maxColumn 
+     */
+    public void selectRange(int minRow, SpreadsheetColumn minColumn, int maxRow, SpreadsheetColumn maxColumn) {
+        selectionModel.selectRange(minRow, minColumn.column, maxRow, maxColumn.column);
+    }
+    
+    /**
+     * Clear the current selection and select the cell on the left of the
+     * current focused cell. If the cell is the first one on a row, the last
+     * cell of the preceding row is selected.
+     */
+    public void clearAndSelectLeftCell() {
+        TablePosition<ObservableList<SpreadsheetCell>, ?> position = getFocusedCell();
+        int row = position.getRow();
+        int column = position.getColumn();
+        column -= 1;
+        if (column < 0) {
+            if (row == 0) {
+                column++;
+            } else {
+                column = spv.getGrid().getColumnCount() - 1;
+                row--;
+            }
+        }
+        clearAndSelect(row, spv.getColumns().get(column));
+    }
+
+    /**
+     * Clear the current selection and select the cell on the right of the
+     * current focused cell. If the cell is the last one on a row, the first
+     * cell of the next row is selected.
+     */
+    public void clearAndSelectRightCell() {
+        TablePosition<ObservableList<SpreadsheetCell>, ?> position = getFocusedCell();
+        int row = position.getRow();
+        int column = position.getColumn();
+        column += 1;
+        if (column >= spv.getColumns().size()) {
+            if (row == spv.getGrid().getRowCount() - 1) {
+                column--;
+            } else {
+                column = 0;
+                row++;
+            }
+        }
+        clearAndSelect(row, spv.getColumns().get(column));
+    }
+
+    /**
+     * Clear the current selection and select the cell on the previous row.
+     */
+    public void clearAndSelectPreviousCell() {
+        TablePosition<ObservableList<SpreadsheetCell>, ?> position = getFocusedCell();
+        int nextRow = FocusModelListener.getPreviousRowNumber(position, selectionModel.getTableView());
+        if (nextRow >= 0) {
+            clearAndSelect(nextRow, spv.getColumns().get(position.getColumn()));
+        }
+    }
+
+    /**
+     * Clear the current selection and select the cell on the next row.
+     */
+    public void clearAndSelectNextCell() {
+        TablePosition<ObservableList<SpreadsheetCell>, ?> position = getFocusedCell();
+        int nextRow = FocusModelListener.getNextRowNumber(position, selectionModel.getTableView());
+        if (nextRow < spv.getGrid().getRowCount()) {
+            clearAndSelect(nextRow, spv.getColumns().get(position.getColumn()));
+        }
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/StringConverterWithFormat.java b/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/StringConverterWithFormat.java
new file mode 100644
index 0000000..d0b4d41
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/StringConverterWithFormat.java
@@ -0,0 +1,77 @@
+/**
+ * Copyright (c) 2013, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.spreadsheet;
+
+import javafx.util.StringConverter;
+
+/**
+ * This class is used by some of the {@link SpreadsheetCellType} in order to use
+ * a specific format.<br>
+ * 
+ * Since the format is specified in the {@link SpreadsheetCell}, we need a
+ * converter which provide a runtime method {@link #toStringFormat(Object, String)}.<br>
+ * 
+ * This class provide two constructors:
+ * <ul>
+ * <li>A default one where you implement the three abstract methods.</li>
+ * <li>Another one which takes another StringConverter. This is useful when you just want to implement 
+ * the {@link #toStringFormat(Object, String)} and let the other converter handle the other methods.</li>
+ * </ul>
+ * 
+ * @see SpreadsheetCellType
+ * 
+ * @param <T>
+ */
+public abstract class StringConverterWithFormat<T> extends StringConverter<T> {
+
+    protected StringConverter<T> myConverter;
+
+    /**
+     * Default constructor.
+     */
+    public StringConverterWithFormat() {
+        super();
+    }
+
+    /**
+     * This constructor allow to use another StringConverter. 
+     * @param specificStringConverter
+     */
+    public StringConverterWithFormat(StringConverter<T> specificStringConverter) {
+        myConverter = specificStringConverter;
+    }
+
+    /**
+     * Converts the object provided into its string form with the specified format.
+     * @param value
+     * @param format
+     * @return a string containing the converted value with the specified format.
+     */
+    public String toStringFormat(T value, String format) {
+        return toString(value);
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/package-info.java b/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/package-info.java
new file mode 100644
index 0000000..15373ae
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/spreadsheet/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * A package containing model and view related classes used by the 
+ * {@link org.controlsfx.control.spreadsheet.SpreadsheetView} control.
+ */
+package org.controlsfx.control.spreadsheet;
\ No newline at end of file
diff --git a/controlsfx/src/main/java/org/controlsfx/control/table/TableFilter.java b/controlsfx/src/main/java/org/controlsfx/control/table/TableFilter.java
new file mode 100644
index 0000000..f2672cc
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/table/TableFilter.java
@@ -0,0 +1,225 @@
+/**
+ * Copyright (c) 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.table;
+
+import impl.org.controlsfx.table.ColumnFilter;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.collections.transformation.FilteredList;
+import javafx.collections.transformation.SortedList;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableView;
+
+import java.util.Optional;
+import java.util.function.BiPredicate;
+import java.util.stream.Collectors;
+import java.util.stream.*;
+
+/**Applies a filtering control to a provided {@link TableView} instance. 
+ * The filter will be applied immediately on construction, and 
+ * can be made visible by right-clicking the desired column to filter on. 
+ *<br><br>
+ *<b>Features</b><br>
+ *-Convenient filter control holds a checklist of distinct items to include/exclude, much like an Excel filter.<br>
+ *-New/removed records will be captured by the filter control and reflect new or removed values from checklist.
+ *-Filters on more than one column are combined to only display mutually inclusive records on the client's TableView.
+ * @param <T>
+ */
+public final class TableFilter<T> {
+    
+    private final TableView<T> tableView;
+    private final ObservableList<T> backingList;
+    private final FilteredList<T> filteredList;
+
+    private final ObservableList<ColumnFilter<T,?>> columnFilters = FXCollections.observableArrayList();
+
+
+    /**
+     * Use TableFilter.forTableView() factory and leverage Builder
+     */
+    @Deprecated
+    public TableFilter(TableView<T> tableView) {
+        this(tableView,false);
+    }
+
+    private TableFilter(TableView<T> tableView, boolean isLazy) {
+        this.tableView = tableView;
+        backingList = tableView.getItems();
+        filteredList = new FilteredList<>(new SortedList<>(backingList));
+        SortedList<T> sortedControlList = new SortedList<>(this.filteredList);
+
+        filteredList.setPredicate(v -> true);
+
+        sortedControlList.comparatorProperty().bind(tableView.comparatorProperty());
+        tableView.setItems(sortedControlList);
+
+        applyForAllColumns(isLazy);
+        tableView.getStylesheets().add("/impl/org/controlsfx/table/tablefilter.css");
+
+        if (!isLazy) {
+            columnFilters.forEach(ColumnFilter::initialize);
+        }
+    }
+
+    /**
+     * Allows specifying a different behavior for the search box on the TableFilter.
+     * By default, the contains() method on a String is used to evaluate the search box input to qualify the distinct filter values.
+     * But you can specify a different behavior by providing a simple BiPredicate argument to this method.
+     * The BiPredicate argument allows you take the input value and target value and use a lambda to evaluate a boolean.
+     * For instance, you can implement a comparison by assuming the input value is a regular expression, and call matches()
+     * on the target value to see if it aligns to the pattern.
+     * @param searchStrategy
+     */
+    public void setSearchStrategy(BiPredicate<String,String> searchStrategy) {
+        columnFilters.forEach(cf -> cf.setSearchStrategy(searchStrategy));
+    }
+    /**
+     * Returns the backing {@link ObservableList} originally provided to the constructor.
+     * @return ObservableList
+     */
+    public ObservableList<T> getBackingList() { 
+        return backingList;
+    }
+    /**
+     * Returns the {@link FilteredList} used by this TableFilter and is backing the {@link TableView}. 
+     * @return FilteredList
+     */
+    public FilteredList<T> getFilteredList() { 
+        return filteredList;
+    }
+    /** 
+     * @treatAsPrivate
+     */
+    private void applyForAllColumns(boolean isLazy) {
+        columnFilters.setAll(tableView.getColumns().stream().flatMap(this::extractNestedColumns)
+                .map(c -> new ColumnFilter<>(this, c)).collect(Collectors.toList()));
+    }
+    private <S> Stream<TableColumn<T,?>> extractNestedColumns(TableColumn<T,S> tableColumn) {
+        if (tableColumn.getColumns().size() == 0) {
+            return Stream.of(tableColumn);
+        } else {
+            return tableColumn.getColumns().stream().flatMap(this::extractNestedColumns);
+        }
+    }
+
+    /**
+     * Programmatically selects value for the specified TableColumn
+     */
+    public void selectValue(TableColumn<?,?> column, Object value) {
+        columnFilters.stream().filter(c -> c.getTableColumn() == column)
+                .forEach(c -> c.selectValue(value));
+    }
+    /**
+     * Programmatically unselects value for the specified TableColumn
+     */
+    public void unselectValue(TableColumn<?,?> column, Object value) {
+        columnFilters.stream().filter(c -> c.getTableColumn() == column)
+                .forEach(c -> c.unselectValue(value));
+    }
+
+    /**
+     * Programmatically selects all values for the specified TableColumn
+
+     */
+    public void selectAllValues(TableColumn<?,?> column) {
+        columnFilters.stream().filter(c -> c.getTableColumn() == column)
+                .forEach(ColumnFilter::selectAllValues);
+    }
+
+    /**
+     * Programmatically unselect all values for the specified TableColumn
+     */
+    public void unSelectAllValues(TableColumn<?,?> column) {
+        columnFilters.stream().filter(c -> c.getTableColumn() == column)
+                .forEach(ColumnFilter::unSelectAllValues);
+    }
+    public void executeFilter() {
+        if (columnFilters.stream().filter(ColumnFilter::isFiltered).findAny().isPresent()) {
+            filteredList.setPredicate(item -> !columnFilters.stream()
+                    .filter(cf -> !cf.evaluate(item))
+                    .findAny().isPresent());
+        }
+        else {
+            resetFilter();
+        }
+    }
+    public void resetFilter() {
+        filteredList.setPredicate(item -> true);
+    }
+    /** 
+     * @treatAsPrivate
+     */
+    public TableView<T> getTableView() {
+        return tableView;
+    }
+    /** 
+     * @treatAsPrivate
+     */
+    public ObservableList<ColumnFilter<T,?>> getColumnFilters() {
+        return columnFilters;
+    }
+    /** 
+     * @treatAsPrivate
+     */
+    public Optional<ColumnFilter<T,?>> getColumnFilter(TableColumn<T,?> tableColumn) {
+        return columnFilters.stream().filter(f -> f.getTableColumn().equals(tableColumn)).findAny();
+    }
+    public boolean isDirty() {
+        return columnFilters.stream().filter(ColumnFilter::isFiltered).findAny().isPresent();
+    }
+
+    /**
+     * Returns a TableFilter.Builder to configure a TableFilter on the specified TableView. Call apply() to initialize and return the TableFilter
+     * @param tableView
+     * @param <T>
+     */
+    public static <T> Builder<T> forTableView(TableView<T> tableView) {
+        return new Builder<T>(tableView);
+    }
+
+    /**
+     * A Builder for a TableFilter against a specified TableView
+     * @param <T>
+     */
+    public static final class Builder<T> {
+
+        private final TableView<T> tableView;
+        private volatile boolean lazyInd = false;
+
+        private Builder(TableView<T> tableView) {
+            this.tableView = tableView;
+        }
+        public Builder<T> lazy(boolean isLazy) {
+            this.lazyInd = isLazy;
+            return this;
+        }
+        public TableFilter<T> apply() {
+            return new TableFilter<>(tableView, lazyInd);
+        }
+    }
+    
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/table/TableRowExpanderColumn.java b/controlsfx/src/main/java/org/controlsfx/control/table/TableRowExpanderColumn.java
new file mode 100644
index 0000000..c7af39f
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/table/TableRowExpanderColumn.java
@@ -0,0 +1,325 @@
+/**
+ * Copyright (c) 2016 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.table;
+
+import impl.org.controlsfx.skin.ExpandableTableRowSkin;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.geometry.Insets;
+import javafx.scene.Node;
+import javafx.scene.control.*;
+import javafx.util.Callback;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * The TableRowExpanderColumn enables a TableView to provide an expandable editor below each table row.
+ * The column itself contains a toggle button that on click will show an editor for the current row right below the
+ * columns. Example:
+ *
+ * <pre>
+ * TableRowExpanderColumn<Customer> expander = new TableRowExpanderColumn<>(param -> {
+ *     HBox editor = new HBox(10);
+ *     TextField text = new TextField(param.getValue().getName());
+ *     Button save = new Button("Save customer");
+ *     save.setOnAction(event -> {
+ *         save();
+ *         param.toggleExpanded();
+ *     });
+ *     editor.getChildren().addAll(text, save);
+ *     return editor;
+ * });
+ *
+ * tableView.getColumns().add(expander);
+ * </pre>
+ *
+ * You can provide a custom cellFactory to customize the toggle button. A typical custom toggle cell implementation
+ * would look like this:
+ *
+ * <pre>
+ * public class MyCustomToggleCell<S> extends TableCell<S, Boolean> {
+ *     private Button button = new Button();
+ *
+ *     public MyCustomToggleCell(TableRowExpanderColumn<S> column) {
+ *         button.setOnAction(event -> column.toggleExpanded(getIndex()));
+ *     }
+ *
+ *     protected void updateItem(Boolean expanded, boolean empty) {
+ *         super.updateItem(expanded, empty);
+ *         if (expanded == null || empty) {
+ *             setGraphic(null);
+ *         } else {
+ *             button.setText(expanded ? "Collapse" : "Expand");
+ *             setGraphic(button);
+ *         }
+ *     }
+ * }
+ * </pre>
+ *
+ * The custom toggle cell utilizes the {@link TableRowExpanderColumn#toggleExpanded(int)} method to toggle
+ * the row expander instead of param.toggleExpanded() like the editor does.
+ *
+ * @param <S> The item type of the TableView
+ */
+public final class TableRowExpanderColumn<S> extends TableColumn<S, Boolean> {
+    private static final String STYLE_CLASS = "expander-column";
+    private static final String EXPANDER_BUTTON_STYLE_CLASS = "expander-button";
+
+    private final Map<S, Node> expandedNodeCache = new HashMap<>();
+    private final Map<S, BooleanProperty> expansionState = new HashMap<>();
+    private Callback<TableRowDataFeatures<S>, Node> expandedNodeCallback;
+
+    /**
+     * Returns a Boolean property that can be used to manipulate the expanded state for a row
+     * corresponding to the given item value.
+     *
+     * @param item The item corresponding to a table row
+     * @return The boolean property
+     */
+    public BooleanProperty getExpandedProperty(S item) {
+        BooleanProperty value = expansionState.get(item);
+        if (value == null) {
+            value = new SimpleBooleanProperty(item, "expanded", false) {
+                /**
+                 * When the expanded state change we refresh the tableview.
+                 * If the expanded state changes to false we remove the cached expanded node.
+                 */
+                @Override
+                protected void invalidated() {
+                    getTableView().refresh();
+                    if (!getValue()) expandedNodeCache.remove(getBean());
+                }
+            };
+            expansionState.put(item, value);
+        }
+        return value;
+    }
+
+    /**
+     * Get or create and cache the expanded node for a given item.
+     *
+     * @param tableRow The table row, used to find the item index
+     * @return The expanded node for the given item
+     */
+    public Node getOrCreateExpandedNode(TableRow<S> tableRow) {
+        int index = tableRow.getIndex();
+        if (index > -1 && index < getTableView().getItems().size()) {
+            S item = getTableView().getItems().get(index);
+            Node node = expandedNodeCache.get(item);
+            if (node == null) {
+                node = expandedNodeCallback.call(new TableRowDataFeatures<>(tableRow, this, item));
+                expandedNodeCache.put(item, node);
+            }
+            return node;
+        }
+        return null;
+    }
+
+    /**
+     * Return the expanded node for the given item, if it exists.
+     *
+     * @param item The item corresponding to a table row
+     * @return The expanded node, if it exists.
+     */
+    public Node getExpandedNode(S item) {
+        return expandedNodeCache.get(item);
+    }
+
+    /**
+     * Create a row expander column that can be added to the TableView list of columns.
+     *
+     * The expandedNodeCallback is expected to return a Node representing the editor that should appear below the
+     * table row when the toggle button within the expander column is clicked.
+     *
+     * Once this column is assigned to a TableView, it will automatically install a custom row factory for the TableView
+     * so that it can configure a TableRow with the {@link impl.org.controlsfx.skin.ExpandableTableRowSkin}. It is within the skin that the actual
+     * rendering of the expanded node occurs.
+     *
+     * @see TableRowExpanderColumn
+     * @see TableRowDataFeatures
+     *
+     * @param expandedNodeCallback
+     */
+    public TableRowExpanderColumn(Callback<TableRowDataFeatures<S>, Node> expandedNodeCallback) {
+        this.expandedNodeCallback = expandedNodeCallback;
+
+        getStyleClass().add(STYLE_CLASS);
+        setCellValueFactory(param -> getExpandedProperty(param.getValue()));
+        setCellFactory(param -> new ToggleCell());
+        installRowFactoryOnTableViewAssignment();
+    }
+
+    /**
+     * Install the row factory on the TableView when this column is assigned to a TableView.
+     */
+    private void installRowFactoryOnTableViewAssignment() {
+        tableViewProperty().addListener((observable, oldValue, newValue) -> {
+            if (newValue != null) {
+                getTableView().setRowFactory(param -> new TableRow<S>() {
+                    @Override
+                    protected Skin<?> createDefaultSkin() {
+                        return new ExpandableTableRowSkin<>(this, TableRowExpanderColumn.this);
+                    }
+                });
+            }
+        });
+    }
+
+    /**
+     * The default toggle cell creates a button with a + or - sign as the text,
+     * depending on the expanded state of the row it represents.
+     *
+     * You can use this as a starting point to implement a custom toggle cell.
+     */
+    private final class ToggleCell extends TableCell<S, Boolean> {
+        private Button button = new Button();
+
+        public ToggleCell() {
+            button.setFocusTraversable(false);
+            button.getStyleClass().add(EXPANDER_BUTTON_STYLE_CLASS);
+            button.setPrefSize(16, 16);
+            button.setPadding(new Insets(0));
+            button.setOnAction(event -> toggleExpanded(getIndex()));
+        }
+
+        @Override
+        protected void updateItem(Boolean expanded, boolean empty) {
+            super.updateItem(expanded, empty);
+            if (expanded == null || empty) {
+                setGraphic(null);
+            } else {
+                button.setText(expanded ? "-" : "+");
+                setGraphic(button);
+            }
+        }
+    }
+
+    /**
+     * Toggle the expanded state of the row at the given index.
+     *
+     * @param index The index of the row you want to toggle expansion for.
+     */
+    public void toggleExpanded(int index) {
+        BooleanProperty expanded = (BooleanProperty) getCellObservableValue(index);
+        expanded.setValue(!expanded.getValue());
+    }
+
+    /**
+     * This object is passed to the expanded node callback when it is time to create a Node to represent the
+     * expanded editor of a certain row. The most important method is {@link #getValue()}} which returns the
+     * object represented by the current row.
+     *
+     * Further more, the {@link #expandedProperty()} returns a boolean property indicating the current expansion
+     * state of the current row. You can use this, or the {@link #toggleExpanded()} method to toggle and inspect
+     * the expanded state of the row, for example if you want an action inside the row editor to contract the editor.
+     *
+     * @param <S> The type of items in the TableView
+     */
+    public static final class TableRowDataFeatures<S> {
+        private TableRow<S> tableRow;
+        private TableRowExpanderColumn<S> tableColumn;
+        private BooleanProperty expandedProperty;
+        private S value;
+
+        public TableRowDataFeatures(TableRow<S> tableRow, TableRowExpanderColumn<S> tableColumn, S value) {
+            this.tableRow = tableRow;
+            this.tableColumn = tableColumn;
+            this.expandedProperty = (BooleanProperty) tableColumn.getCellObservableValue(tableRow.getIndex());
+            this.value = value;
+        }
+
+        /**
+         * Return the current TableRow. It is safe to assume that the index returned by {@link TableRow#getIndex()} is
+         * correct as long as you use it for the initial node creation. It is not safe to trust the result of this call
+         * at any later time, for example in a button action within the row editor.
+         *
+         * @return The current TableRow
+         */
+        public TableRow<S> getTableRow() {
+            return tableRow;
+        }
+
+        /**
+         * Return the TableColumn which contains the toggle button. Normally you would not need to use this directly,
+         * but rather consult the {@link #expandedProperty()} for inspection and mutation of the toggled state of this row.
+         *
+         * @return The TableColumn which contains the toggle button
+         */
+        public TableRowExpanderColumn<S> getTableColumn() {
+            return tableColumn;
+        }
+
+        /**
+         * The expanded property can be used to inspect or mutate the toggled state of this row editor. You can also
+         * listen for changes to it's state if needed.
+         *
+         * @return The expanded property
+         */
+        public BooleanProperty expandedProperty() {
+            return expandedProperty;
+        }
+
+        /**
+         * Toggle the expanded state of this row editor.
+         */
+        public void toggleExpanded() {
+            BooleanProperty expanded = expandedProperty();
+            expanded.setValue(!expanded.getValue());
+        }
+
+        /**
+         * Returns a boolean indicating if the current row is expanded or not
+         *
+         * @return A boolean indicating the expanded state of the current editor
+         */
+        public Boolean isExpanded() {
+            return expandedProperty().getValue();
+        }
+
+        /**
+         * Set the expanded state. This will update the {@link #expandedProperty()} accordingly.
+         *
+         * @param expanded Wheter the row editor should be expanded or not
+         */
+        public void setExpanded(Boolean expanded) {
+            expandedProperty().setValue(expanded);
+        }
+
+        /**
+         * The value represented by the current table row. It is important that the value has valid equals/hashCode
+         * methods, as the row value is used to keep track of the node editor for each row.
+         *
+         * @return The value represented by the current table row
+         */
+        public S getValue() {
+            return value;
+        }
+
+    }
+
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/table/TableViewUtils.java b/controlsfx/src/main/java/org/controlsfx/control/table/TableViewUtils.java
new file mode 100644
index 0000000..94a020e
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/table/TableViewUtils.java
@@ -0,0 +1,138 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.table;
+
+import java.lang.reflect.Field;
+import java.util.function.Consumer;
+
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.collections.ObservableList;
+import javafx.scene.Node;
+import javafx.scene.control.ContextMenu;
+import javafx.scene.control.Control;
+import javafx.scene.control.MenuItem;
+import javafx.scene.control.Skin;
+import javafx.scene.control.TableView;
+import javafx.scene.control.TreeTableView;
+
+import com.sun.javafx.scene.control.skin.TableHeaderRow;
+import com.sun.javafx.scene.control.skin.TableViewSkin;
+import com.sun.javafx.scene.control.skin.TableViewSkinBase;
+
+/**
+ * A utility class for API revolving around the JavaFX {@link TableView} and
+ * {@link TreeTableView} controls.
+ */
+// not public as not ready for 8.20.7
+final class TableViewUtils {
+
+    /**
+     * Call this method to be able to programatically manipulate the 
+     * {@link TableView#tableMenuButtonVisibleProperty() TableView menu button}
+     * (assuming it is visible). This allows developers to, for example, add in
+     * new {@link MenuItem}.
+     */
+    public static void modifyTableMenu(final TableView<?> tableView, final Consumer<ContextMenu> consumer) {
+        modifyTableMenu((Control)tableView, consumer);
+    }
+    
+    /**
+     * Call this method to be able to programatically manipulate the 
+     * {@link TreeTableView#tableMenuButtonVisibleProperty() TreeTableView menu button}
+     * (assuming it is visible). This allows developers to, for example, add in
+     * new {@link MenuItem}.
+     */
+    public static void modifyTableMenu(final TreeTableView<?> treeTableView, final Consumer<ContextMenu> consumer) {
+        modifyTableMenu((Control)treeTableView, consumer);
+    }
+    
+    private static void modifyTableMenu(final Control control, final Consumer<ContextMenu> consumer) {
+        if (control.getScene() == null) {
+            control.sceneProperty().addListener(new InvalidationListener() {
+                @Override public void invalidated(Observable o) {
+                    control.sceneProperty().removeListener(this);
+                    modifyTableMenu(control, consumer);
+                }
+            });
+            
+            return;
+        }
+        
+        Skin<?> skin = control.getSkin();
+        if (skin == null) {
+            control.skinProperty().addListener(new InvalidationListener() {
+                @Override public void invalidated(Observable o) {
+                    control.skinProperty().removeListener(this);
+                    modifyTableMenu(control, consumer);
+                }
+            });
+            
+            return;
+        }
+
+        doModify(skin, consumer);
+    }
+
+    private static void doModify(Skin<?> skin, Consumer<ContextMenu> consumer) {
+        if (! (skin instanceof TableViewSkinBase)) return;
+
+        TableViewSkin<?> tableSkin = (TableViewSkin<?>)skin;
+        TableHeaderRow headerRow = getHeaderRow(tableSkin);
+        if (headerRow == null) return;
+
+        ContextMenu contextMenu = getContextMenu(headerRow);
+        consumer.accept(contextMenu);        
+    }
+
+    private static TableHeaderRow getHeaderRow(TableViewSkin<?> tableSkin) {
+        ObservableList<Node> children = tableSkin.getChildren();
+        for (int i = 0, max = children.size(); i < max; i++) {
+            Node child = children.get(i);
+            if (child instanceof TableHeaderRow) return (TableHeaderRow) child;
+        }
+        return null;
+    }
+
+    private static ContextMenu getContextMenu(TableHeaderRow headerRow) {
+        try {
+            Field privateContextMenuField = TableHeaderRow.class.getDeclaredField("columnPopupMenu"); //$NON-NLS-1$
+            privateContextMenuField.setAccessible(true);
+            ContextMenu contextMenu = (ContextMenu) privateContextMenuField.get(headerRow);
+            return contextMenu;
+        } catch (IllegalArgumentException ex) {
+            ex.printStackTrace();
+        } catch (IllegalAccessException ex) {
+            ex.printStackTrace();
+        } catch (NoSuchFieldException ex) {
+            ex.printStackTrace();
+        } catch (SecurityException ex) {
+            ex.printStackTrace();
+        }
+        return null;
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/table/model/JavaFXTableModel.java b/controlsfx/src/main/java/org/controlsfx/control/table/model/JavaFXTableModel.java
new file mode 100644
index 0000000..b595123
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/table/model/JavaFXTableModel.java
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.table.model;
+
+import javafx.scene.control.TableView;
+
+/**
+ *
+ */
+//not public as not ready for 8.20.7
+interface JavaFXTableModel<T> {
+    public T getValueAt(int rowIndex, int columnIndex);
+    
+    public void setValueAt(T value, int rowIndex, int columnIndex);
+    
+    public int getRowCount();
+    
+    public int getColumnCount();
+    
+    public String getColumnName(int columnIndex);
+    
+    public void sort(TableView<TableModelRow<T>> table);
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/table/model/JavaFXTableModels.java b/controlsfx/src/main/java/org/controlsfx/control/table/model/JavaFXTableModels.java
new file mode 100644
index 0000000..734e123
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/table/model/JavaFXTableModels.java
@@ -0,0 +1,100 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.table.model;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableColumn.SortType;
+import javafx.scene.control.TableView;
+
+import javax.swing.RowSorter.SortKey;
+import javax.swing.SortOrder;
+import javax.swing.table.TableModel;
+import javax.swing.table.TableRowSorter;
+
+/**
+ *
+ */
+//not public as not ready for 8.20.7
+class JavaFXTableModels {
+    
+    /**
+     * Swing
+     */
+    public static <S> JavaFXTableModel<S> wrap(final TableModel tableModel) {
+        
+        return new JavaFXTableModel<S>() {
+            final TableRowSorter<TableModel> sorter;
+            
+            {
+                sorter = new TableRowSorter<>(tableModel);
+            }
+            
+            @SuppressWarnings("unchecked")
+            @Override public S getValueAt(int rowIndex, int columnIndex) {
+                return (S) tableModel.getValueAt(sorter.convertRowIndexToView(rowIndex), columnIndex);
+            }
+
+            @Override public void setValueAt(S value, int rowIndex, int columnIndex) {
+                tableModel.setValueAt(value, rowIndex, columnIndex);
+            }
+
+            @Override public int getRowCount() {
+                return tableModel.getRowCount();
+            }
+
+            @Override public int getColumnCount() {
+                return tableModel.getColumnCount();
+            }
+
+            @Override public String getColumnName(int columnIndex) {
+                return tableModel.getColumnName(columnIndex);
+            }
+            
+            @Override public void sort(TableView<TableModelRow<S>> table) {
+                List<SortKey> sortKeys = new ArrayList<>();
+                
+                for (TableColumn<TableModelRow<S>, ?> column : table.getSortOrder()) {
+                    final int columnIndex = table.getVisibleLeafIndex(column);
+                    final SortType sortType = column.getSortType();
+                    SortOrder sortOrder = sortType == SortType.ASCENDING ? SortOrder.ASCENDING :
+                                          sortType == SortType.DESCENDING ? SortOrder.DESCENDING :
+                                          SortOrder.UNSORTED;
+                    SortKey sortKey = new SortKey(columnIndex, sortOrder);
+                    sortKeys.add(sortKey);
+                    
+                    sorter.setComparator(columnIndex, column.getComparator());
+                }
+                
+                sorter.setSortKeys(sortKeys);
+                sorter.sort();
+            }
+        };
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/table/model/TableModelRow.java b/controlsfx/src/main/java/org/controlsfx/control/table/model/TableModelRow.java
new file mode 100644
index 0000000..0eaa638
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/table/model/TableModelRow.java
@@ -0,0 +1,60 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.table.model;
+
+/**
+ */
+class TableModelRow<S> {
+    private final int columnCount;
+    private final JavaFXTableModel<S> tableModel;
+    private final int row;
+
+    TableModelRow(JavaFXTableModel<S> tableModel, int row) {
+        this.row = row;
+        this.tableModel = tableModel;
+        this.columnCount = tableModel.getColumnCount();
+    }
+    
+    public Object get(int column) {
+        return column < 0 || column >= columnCount ? null : this.tableModel.getValueAt(row, column);
+    }
+
+    @Override public String toString() {
+        String text = "Row " + row + ": [ "; //$NON-NLS-1$ //$NON-NLS-2$
+        
+        for (int col = 0; col < columnCount; col++) {
+            text += get(col);
+            
+            if (col < (columnCount - 1)) {
+                text += ", "; //$NON-NLS-1$
+            }
+        }
+        
+        text += " ]"; //$NON-NLS-1$
+        return text;
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/table/model/TableModelTableView.java b/controlsfx/src/main/java/org/controlsfx/control/table/model/TableModelTableView.java
new file mode 100644
index 0000000..596a8d5
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/table/model/TableModelTableView.java
@@ -0,0 +1,66 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.table.model;
+
+import com.sun.javafx.scene.control.ReadOnlyUnbackedObservableList;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableView;
+
+/**
+ *
+ */
+//not public as not ready for 8.20.7
+class TableModelTableView<S> extends TableView<TableModelRow<S>> {
+
+    public TableModelTableView(final JavaFXTableModel<S> tableModel) {
+        // create a dummy items list of the appropriate size, where the returned
+        // value is the index of the row
+        setItems(new ReadOnlyUnbackedObservableList<TableModelRow<S>>() {
+            @Override public TableModelRow<S> get(int row) {
+                if (row < 0 || row >= tableModel.getRowCount()) return null;
+                TableModelRow<S> backingRow = new TableModelRow<>(tableModel, row);
+                return backingRow;
+            }
+
+            @Override public int size() {
+                return tableModel.getRowCount();
+            }
+        });
+        
+        setSortPolicy(table -> {
+            tableModel.sort(table);
+            return true;
+        });
+        
+        // create columns from the table model
+        for (int i = 0; i < tableModel.getColumnCount(); i++) {
+            TableColumn<TableModelRow<S>,?> column = new TableColumn<>(tableModel.getColumnName(i));
+            column.setCellValueFactory(new TableModelValueFactory<>(tableModel, i));
+            getColumns().add(column);
+        }
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/table/model/TableModelValueFactory.java b/controlsfx/src/main/java/org/controlsfx/control/table/model/TableModelValueFactory.java
new file mode 100644
index 0000000..bc098b9
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/table/model/TableModelValueFactory.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.table.model;
+
+import javafx.beans.property.ReadOnlyObjectWrapper;
+import javafx.beans.value.ObservableValue;
+import javafx.scene.control.TableColumn;
+import javafx.util.Callback;
+import javafx.scene.control.TableColumn.CellDataFeatures;
+
+/**
+ *
+ */
+//not public as not ready for 8.20.7
+class TableModelValueFactory<S, T> implements Callback<CellDataFeatures<TableModelRow<S>, T>, ObservableValue<T>> {
+    @SuppressWarnings("unused")
+    private final JavaFXTableModel<S> _tableModel;
+    private final int _columnIndex;
+
+    public TableModelValueFactory(JavaFXTableModel<S> tableModel, int columnIndex) {
+        _tableModel = tableModel;
+        _columnIndex = columnIndex;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override public ObservableValue<T> call(TableColumn.CellDataFeatures<TableModelRow<S>, T> cdf) {
+        TableModelRow<S> row = cdf.getValue();
+        T valueAt = (T) row.get(_columnIndex);
+        return valueAt instanceof ObservableValue ? ((ObservableValue<T>) valueAt) : new ReadOnlyObjectWrapper<>(valueAt);
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/textfield/AutoCompletionBinding.java b/controlsfx/src/main/java/org/controlsfx/control/textfield/AutoCompletionBinding.java
new file mode 100644
index 0000000..f851ea0
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/textfield/AutoCompletionBinding.java
@@ -0,0 +1,558 @@
+/**
+ * Copyright (c) 2014, 2016 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.textfield;
+
+import com.sun.javafx.event.EventHandlerManager;
+import impl.org.controlsfx.skin.AutoCompletePopup;
+import impl.org.controlsfx.skin.AutoCompletePopupSkin;
+import javafx.application.Platform;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.ObjectPropertyBase;
+import javafx.concurrent.Task;
+import javafx.event.*;
+import javafx.scene.Node;
+import javafx.scene.control.ListView;
+import javafx.scene.control.Skin;
+import javafx.util.Callback;
+import javafx.util.StringConverter;
+
+import java.util.Collection;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.IntegerProperty;
+
+/**
+ * The AutoCompletionBinding is the abstract base class of all auto-completion bindings.
+ * This class is the core logic for the auto-completion feature but highly customizable.
+ * 
+ * <p>To use the autocompletion functionality, refer to the {@link TextFields} class.
+ *
+ * The popup size can be modified through its {@link #setVisibleRowCount(int) }
+ * for the height and all the usual methods for the width.
+ * 
+ * @param <T> Model-Type of the suggestions
+ * @see TextFields
+ */
+public abstract class AutoCompletionBinding<T> implements EventTarget {
+
+
+    /***************************************************************************
+     *                                                                         *
+     * Private fields                                                          *
+     *                                                                         *
+     **************************************************************************/
+    private final Node completionTarget;
+    private final AutoCompletePopup<T> autoCompletionPopup;
+    private final Object suggestionsTaskLock = new Object();
+
+    private FetchSuggestionsTask suggestionsTask = null;
+    private Callback<ISuggestionRequest, Collection<T>> suggestionProvider = null;
+    private boolean ignoreInputChanges = false;
+    private long delay = 250;
+
+    /***************************************************************************
+     *                                                                         *
+     * Constructors                                                            *
+     *                                                                         *
+     **************************************************************************/
+
+
+    /**
+     * Creates a new AutoCompletionBinding
+     * 
+     * @param completionTarget The target node to which auto-completion shall be added
+     * @param suggestionProvider The strategy to retrieve suggestions 
+     * @param converter The converter to be used to convert suggestions to strings 
+     */
+    protected AutoCompletionBinding(Node completionTarget, 
+            Callback<ISuggestionRequest, Collection<T>> suggestionProvider,
+            StringConverter<T> converter){
+
+        this.completionTarget = completionTarget;
+        this.suggestionProvider = suggestionProvider;
+        this.autoCompletionPopup = new AutoCompletePopup<>();
+        this.autoCompletionPopup.setConverter(converter);
+
+        autoCompletionPopup.setOnSuggestion(sce -> {
+            try{
+                setIgnoreInputChanges(true);
+                completeUserInput(sce.getSuggestion());
+                fireAutoCompletion(sce.getSuggestion());
+                hidePopup();
+            }finally{
+                // Ensure that ignore is always set back to false
+                setIgnoreInputChanges(false);
+            }
+        });
+    }
+
+    /***************************************************************************
+     *                                                                         *
+     * Public API                                                              *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * Specifies whether the PopupWindow should be hidden when an unhandled
+     * escape key is pressed while the popup has focus.
+     *
+     * @param value
+     */
+    public void setHideOnEscape(boolean value) {
+        autoCompletionPopup.setHideOnEscape(value);
+    }
+
+    /**
+     * Set the current text the user has entered
+     * @param userText
+     */
+    public final void setUserInput(String userText){
+        if(!isIgnoreInputChanges()){
+            onUserInputChanged(userText);
+        }
+    }
+
+    /**
+     * Sets the delay in ms between a key press and the suggestion popup being displayed.
+     *
+     * @param delay
+     */
+    public final void setDelay(long delay) {
+        this.delay = delay;
+    }
+
+    /**
+     * Gets the target node for auto completion
+     * @return the target node for auto completion
+     */
+    public Node getCompletionTarget(){
+        return completionTarget;
+    }
+
+    /**
+     * Disposes the binding.
+     */
+    public abstract void dispose();
+
+
+    /**
+     * Set the maximum number of rows to be visible in the popup when it is
+     * showing.
+     *
+     * @param value
+     */
+    public final void setVisibleRowCount(int value) {
+        autoCompletionPopup.setVisibleRowCount(value);
+    }
+
+    /**
+     * Return the maximum number of rows to be visible in the popup when it is
+     * showing.
+     *
+     * @return the maximum number of rows to be visible in the popup when it is
+     * showing.
+     */
+    public final int getVisibleRowCount() {
+        return autoCompletionPopup.getVisibleRowCount();
+    }
+
+    /**
+     * Return an property representing the maximum number of rows to be visible
+     * in the popup when it is showing.
+     *
+     * @return an property representing the maximum number of rows to be visible
+     * in the popup when it is showing.
+     */
+    public final IntegerProperty visibleRowCountProperty() {
+        return autoCompletionPopup.visibleRowCountProperty();
+    }
+    
+    /**
+     * Sets the prefWidth of the popup.
+     *
+     * @param value
+     */
+    public final void setPrefWidth(double value) {
+        autoCompletionPopup.setPrefWidth(value);
+    }
+
+    /**
+     * Return the pref width of the popup.
+     *
+     * @return the pref width of the popup.
+     */
+    public final double getPrefWidth() {
+        return autoCompletionPopup.getPrefWidth();
+    }
+
+    /**
+     * Return the property associated with the pref width.
+     * @return 
+     */
+    public final DoubleProperty prefWidthProperty() {
+        return autoCompletionPopup.prefWidthProperty();
+    }
+
+    /**
+     * Sets the minWidth of the popup.
+     *
+     * @param value
+     */
+    public final void setMinWidth(double value) {
+        autoCompletionPopup.setMinWidth(value);
+    }
+
+    /**
+     * Return the min width of the popup.
+     *
+     * @return the min width of the popup.
+     */
+    public final double getMinWidth() {
+        return autoCompletionPopup.getMinWidth();
+    }
+
+    /**
+     * Return the property associated with the min width.
+     * @return 
+     */
+    public final DoubleProperty minWidthProperty() {
+        return autoCompletionPopup.minWidthProperty();
+    }
+
+    /**
+     * Sets the maxWidth of the popup.
+     *
+     * @param value
+     */
+    public final void setMaxWidth(double value) {
+        autoCompletionPopup.setMaxWidth(value);
+    }
+
+    /**
+     * Return the max width of the popup.
+     *
+     * @return the max width of the popup.
+     */
+    public final double getMaxWidth() {
+        return autoCompletionPopup.getMaxWidth();
+    }
+
+    /**
+     * Return the property associated with the max width.
+     * @return 
+     */
+    public final DoubleProperty maxWidthProperty() {
+        return autoCompletionPopup.maxWidthProperty();
+    }
+
+    /***************************************************************************
+     *                                                                         *
+     * Protected methods                                                       *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * Complete the current user-input with the provided completion.
+     * Sub-classes have to provide a concrete implementation.
+     * @param completion
+     */
+    protected abstract void completeUserInput(T completion);
+
+
+    /**
+     * Show the auto completion popup
+     */
+    protected void showPopup(){
+        autoCompletionPopup.show(completionTarget);
+        selectFirstSuggestion(autoCompletionPopup);
+    }
+
+    /**
+     * Hide the auto completion targets
+     */
+    protected void hidePopup(){
+        autoCompletionPopup.hide();
+    }
+
+    protected void fireAutoCompletion(T completion){
+        Event.fireEvent(this, new AutoCompletionEvent<>(completion));
+    }
+
+
+    /***************************************************************************
+     *                                                                         *
+     * Private methods                                                         *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * Selects the first suggestion (if any), so the user can choose it
+     * by pressing enter immediately.
+     */
+    private void selectFirstSuggestion(AutoCompletePopup<?> autoCompletionPopup){
+        Skin<?> skin = autoCompletionPopup.getSkin();
+        if(skin instanceof AutoCompletePopupSkin){
+            AutoCompletePopupSkin<?> au = (AutoCompletePopupSkin<?>)skin;
+            ListView<?> li = (ListView<?>)au.getNode();
+            if(li.getItems() != null && !li.getItems().isEmpty()){
+                li.getSelectionModel().select(0);
+            }
+        }
+    }
+
+    /**
+     * Occurs when the user text has changed and the suggestions require an update
+     * @param userText
+     */
+    private final void onUserInputChanged(final String userText){
+        synchronized (suggestionsTaskLock) {
+            if(suggestionsTask != null && suggestionsTask.isRunning()){
+                // cancel the current running task
+                suggestionsTask.cancel(); 
+            }
+            // create a new fetcher task
+            suggestionsTask = new FetchSuggestionsTask(userText, delay);
+            new Thread(suggestionsTask).start();
+        }
+    }
+
+    /**
+     * Shall changes to the user input be ignored?
+     * @return
+     */
+    private boolean isIgnoreInputChanges(){
+        return ignoreInputChanges;
+    }
+
+    /**
+     * If IgnoreInputChanges is set to true, all changes to the user input are
+     * ignored. This is primary used to avoid self triggering while
+     * auto completing.
+     * @param state
+     */
+    private void setIgnoreInputChanges(boolean state){
+        ignoreInputChanges = state;
+    }
+
+    /***************************************************************************
+     *                                                                         *
+     * Inner classes and interfaces                                            *
+     *                                                                         *
+     **************************************************************************/
+
+
+    /**
+     * Represents a suggestion fetch request
+     *
+     */
+    public static interface ISuggestionRequest {
+        /**
+         * Is this request canceled?
+         * @return {@code true} if the request is canceled, otherwise {@code false}
+         */
+        public boolean isCancelled();
+
+        /**
+         * Get the user text to which suggestions shall be found
+         * @return {@link String} containing the user text
+         */
+        public String getUserText();
+    }
+
+
+
+    /**
+     * This task is responsible to fetch suggestions asynchronous
+     * by using the current defined suggestionProvider
+     *
+     */
+    private class FetchSuggestionsTask extends Task<Void> implements ISuggestionRequest {
+        private final String userText;
+        private final long delay;
+
+        public FetchSuggestionsTask(String userText, long delay){
+            this.userText = userText;
+            this.delay = delay;
+        }
+
+        @Override
+        protected Void call() throws Exception {
+            Callback<ISuggestionRequest, Collection<T>> provider = suggestionProvider;
+            if(provider != null){
+            	long startTime = System.currentTimeMillis();
+                final Collection<T> fetchedSuggestions = provider.call(this);
+                long sleepTime = startTime + delay - System.currentTimeMillis();
+                if (sleepTime > 0 && !isCancelled()) {
+                	Thread.sleep(sleepTime);
+                }
+                if(!isCancelled()){
+                    Platform.runLater(() -> {
+                        if(fetchedSuggestions != null && !fetchedSuggestions.isEmpty()){
+                            autoCompletionPopup.getSuggestions().setAll(fetchedSuggestions);
+                            showPopup();
+                        }else{
+                            // No suggestions found, so hide the popup
+                            hidePopup();
+                        }
+                    });
+                }
+            }else {
+                // No suggestion provider
+                hidePopup();
+            }
+            return null;
+        }
+
+        @Override
+        public String getUserText() {
+            return userText;
+        }
+    }
+
+    /***************************************************************************
+     *                                                                         *
+     * Events                                                                  *
+     *                                                                         *
+     **************************************************************************/
+
+
+    // --- AutoCompletionEvent
+
+    /**
+     * Represents an Event which is fired after an auto completion.
+     */
+    @SuppressWarnings("serial")
+    public static class AutoCompletionEvent<TE> extends Event {
+
+        /**
+         * The event type that should be listened to by people interested in 
+         * knowing when an auto completion has been performed.
+         */
+        @SuppressWarnings("rawtypes")
+        public static final EventType<AutoCompletionEvent> AUTO_COMPLETED = new EventType<>("AUTO_COMPLETED"); //$NON-NLS-1$
+
+        private final TE completion;
+
+        /**
+         * Creates a new event that can subsequently be fired.
+         */
+        public AutoCompletionEvent(TE completion) {
+            super(AUTO_COMPLETED);
+            this.completion = completion;
+        }
+
+        /**
+         * Returns the chosen completion.
+         */
+        public TE getCompletion() {
+            return completion;
+        }
+    }
+
+
+    private ObjectProperty<EventHandler<AutoCompletionEvent<T>>> onAutoCompleted;
+
+    /**
+     * Set a event handler which is invoked after an auto completion.
+     * @param value
+     */
+    public final void setOnAutoCompleted(EventHandler<AutoCompletionEvent<T>> value) {
+        onAutoCompletedProperty().set( value);
+    }
+
+    public final EventHandler<AutoCompletionEvent<T>> getOnAutoCompleted() {
+        return onAutoCompleted == null ? null : onAutoCompleted.get();
+    }
+
+    public final ObjectProperty<EventHandler<AutoCompletionEvent<T>>> onAutoCompletedProperty() {
+        if (onAutoCompleted == null) {
+            onAutoCompleted = new ObjectPropertyBase<EventHandler<AutoCompletionEvent<T>>>() {
+                @SuppressWarnings({ "rawtypes", "unchecked" })
+                @Override protected void invalidated() {
+                    eventHandlerManager.setEventHandler(
+                            AutoCompletionEvent.AUTO_COMPLETED,
+                            (EventHandler<AutoCompletionEvent>)(Object)get());
+                }
+
+                @Override
+                public Object getBean() {
+                    return AutoCompletionBinding.this;
+                }
+
+                @Override
+                public String getName() {
+                    return "onAutoCompleted"; //$NON-NLS-1$
+                }
+            };
+        }
+        return onAutoCompleted;
+    }
+
+
+    /***************************************************************************
+     *                                                                         *
+     * EventTarget Implementation                                              *
+     *                                                                         *
+     **************************************************************************/
+
+    final EventHandlerManager eventHandlerManager = new EventHandlerManager(this);
+
+    /**
+     * Registers an event handler to this EventTarget. The handler is called when the
+     * menu item receives an {@code Event} of the specified type during the bubbling
+     * phase of event delivery.
+     *
+     * @param <E> the specific event class of the handler
+     * @param eventType the type of the events to receive by the handler
+     * @param eventHandler the handler to register
+     * @throws NullPointerException if the event type or handler is null
+     */
+    public <E extends Event> void addEventHandler(EventType<E> eventType, EventHandler<E> eventHandler) {
+        eventHandlerManager.addEventHandler(eventType, eventHandler);
+    }
+
+    /**
+     * Unregisters a previously registered event handler from this EventTarget. One
+     * handler might have been registered for different event types, so the
+     * caller needs to specify the particular event type from which to
+     * unregister the handler.
+     *
+     * @param <E> the specific event class of the handler
+     * @param eventType the event type from which to unregister
+     * @param eventHandler the handler to unregister
+     * @throws NullPointerException if the event type or handler is null
+     */
+    public <E extends Event> void removeEventHandler(EventType<E> eventType, EventHandler<E> eventHandler) {
+        eventHandlerManager.removeEventHandler(eventType, eventHandler);
+    }
+
+    /** {@inheritDoc} */
+    @Override public EventDispatchChain buildEventDispatchChain(EventDispatchChain tail) {
+        return tail.prepend(eventHandlerManager);
+    }
+
+
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/textfield/CustomPasswordField.java b/controlsfx/src/main/java/org/controlsfx/control/textfield/CustomPasswordField.java
new file mode 100644
index 0000000..0265dd6
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/textfield/CustomPasswordField.java
@@ -0,0 +1,171 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.textfield;
+
+import impl.org.controlsfx.skin.CustomTextFieldSkin;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.scene.Node;
+import javafx.scene.control.PasswordField;
+import javafx.scene.control.Skin;
+
+/**
+ * A base class for people wanting to customize a {@link PasswordField} to contain nodes
+ * inside the input field area itself, without being on top of the users typed-in text.
+ * 
+ * <p>Whilst not exactly the same, refer to the {@link CustomTextField} javadoc
+ * for a screenshot and more detail. The obvious difference is that of course
+ * the CustomPasswordField masks the input from users, but in all other ways
+ * is equivalent to {@link CustomTextField}.
+ * 
+ * @see CustomPasswordField
+ * @see TextFields
+ */
+public class CustomPasswordField extends PasswordField {
+
+    /**************************************************************************
+     * 
+     * Private fields
+     * 
+     **************************************************************************/
+    
+
+    
+    
+    /**************************************************************************
+     * 
+     * Constructors
+     * 
+     **************************************************************************/
+    
+    /**
+     * Instantiates a default CustomPasswordField.
+     */
+    public CustomPasswordField() {
+        getStyleClass().addAll("custom-text-field", "custom-password-field"); //$NON-NLS-1$ //$NON-NLS-2$
+    }
+
+    
+    
+    /**************************************************************************
+     * 
+     * Properties
+     * 
+     **************************************************************************/
+    
+    // --- left
+    private ObjectProperty<Node> left = new SimpleObjectProperty<>(this, "left"); //$NON-NLS-1$
+    
+    /**
+     * Property representing the {@link Node} that is placed on the left of
+     * the password field.
+     * @return An ObjectProperty.
+     */
+    public final ObjectProperty<Node> leftProperty() {
+        return left;
+    }
+    
+    /**
+     * 
+     * @return The {@link Node} that is placed on the left of
+     * the password field.
+     */
+    public final Node getLeft() {
+        return left.get();
+    }
+    
+    /**
+     * Sets the {@link Node} that is placed on the left of
+     * the password field.
+     * @param value 
+     */
+    public final void setLeft(Node value) {
+        left.set(value);
+    }
+    
+    
+    // --- right
+    private ObjectProperty<Node> right = new SimpleObjectProperty<>(this, "right"); //$NON-NLS-1$
+    
+    /**
+     * Property representing the {@link Node} that is placed on the right of
+     * the password field.
+     * @return An ObjectProperty.
+     */
+    public final ObjectProperty<Node> rightProperty() {
+        return right;
+    }
+    
+    /**
+     * 
+     * @return The {@link Node} that is placed on the right of
+     * the password field.
+     */
+    public final Node getRight() {
+        return right.get();
+    }
+    
+    /**
+     * Sets the {@link Node} that is placed on the right of
+     * the password field.
+     * @param value 
+     */
+    public final void setRight(Node value) {
+        right.set(value);
+    }
+    
+    
+    
+    /**************************************************************************
+     * 
+     * Public API
+     * 
+     **************************************************************************/
+    
+    /**
+     * {@inheritDoc}
+     */
+    @Override protected Skin<?> createDefaultSkin() {
+        return new CustomTextFieldSkin(this) {
+            @Override public ObjectProperty<Node> leftProperty() {
+                return CustomPasswordField.this.leftProperty();
+            }
+            
+            @Override public ObjectProperty<Node> rightProperty() {
+                return CustomPasswordField.this.rightProperty();
+            }
+        };
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    @Override public String getUserAgentStylesheet() {
+        return CustomTextField.class.getResource("customtextfield.css").toExternalForm(); //$NON-NLS-1$
+    }
+    
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/textfield/CustomTextField.java b/controlsfx/src/main/java/org/controlsfx/control/textfield/CustomTextField.java
new file mode 100644
index 0000000..5eb1ce6
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/textfield/CustomTextField.java
@@ -0,0 +1,178 @@
+/**
+ * Copyright (c) 2013, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.textfield;
+
+import impl.org.controlsfx.skin.CustomTextFieldSkin;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.scene.Node;
+import javafx.scene.control.Skin;
+import javafx.scene.control.TextField;
+
+/**
+ * A base class for people wanting to customize a {@link TextField} to contain nodes
+ * inside the text field itself, without being on top of the users typed-in text.
+ * 
+ * <h3>Screenshot</h3>
+ * <p>The following screenshot is taken from the HelloControlsFX sample application,
+ * and shows a normal TextField, with a {@link TextFields#createClearableTextField() clearable text field},
+ * followed by three CustomTextFields. Note what happens with long text input - 
+ * it is prevented from going beneath the left and right graphics. Of course, if 
+ * the keyboard caret moves to the right, the text will become visible, but this
+ * is because it will all scroll to the left (as is the case in a normal {@link TextField}).
+ * 
+ * <br>
+ * <center>
+ * <img src="customTextField.png" alt="Screenshot of CustomTextField">
+ * </center>
+ * 
+ * @see TextFields
+ * @see CustomPasswordField
+ */
+public class CustomTextField extends TextField {
+
+    /**************************************************************************
+     * 
+     * Private fields
+     * 
+     **************************************************************************/
+    
+
+    
+    
+    /**************************************************************************
+     * 
+     * Constructors
+     * 
+     **************************************************************************/
+    
+    /**
+     * Instantiates a default CustomTextField.
+     */
+    public CustomTextField() {
+        getStyleClass().add("custom-text-field"); //$NON-NLS-1$
+    }
+
+    
+    
+    /**************************************************************************
+     * 
+     * Properties
+     * 
+     **************************************************************************/
+    
+    // --- left
+    private ObjectProperty<Node> left = new SimpleObjectProperty<>(this, "left"); //$NON-NLS-1$
+    
+    /**
+     * 
+     * @return An ObjectProperty wrapping the {@link Node} that is placed 
+     * on the left ofthe text field.
+     */
+    public final ObjectProperty<Node> leftProperty() {
+        return left;
+    }
+    
+    /**
+     * 
+     * @return the {@link Node} that is placed on the left of
+     * the text field.
+     */
+    public final Node getLeft() {
+        return left.get();
+    }
+    
+    /**
+     * Sets the {@link Node} that is placed on the left of
+     * the text field.
+     * @param value 
+     */
+    public final void setLeft(Node value) {
+        left.set(value);
+    }
+    
+    
+    // --- right
+    private ObjectProperty<Node> right = new SimpleObjectProperty<>(this, "right"); //$NON-NLS-1$
+    
+    /**
+     * Property representing the {@link Node} that is placed on the right of
+     * the text field.
+     * @return An ObjectProperty.
+     */
+    public final ObjectProperty<Node> rightProperty() {
+        return right;
+    }
+    
+    /**
+     * 
+     * @return The {@link Node} that is placed on the right of
+     * the text field.
+     */
+    public final Node getRight() {
+        return right.get();
+    }
+    
+    /**
+     * Sets the {@link Node} that is placed on the right of
+     * the text field.
+     * @param value 
+     */
+    public final void setRight(Node value) {
+        right.set(value);
+    }
+    
+    
+    
+    /**************************************************************************
+     * 
+     * Public API
+     * 
+     **************************************************************************/
+    
+    /**
+     * {@inheritDoc}
+     */
+    @Override protected Skin<?> createDefaultSkin() {
+        return new CustomTextFieldSkin(this) {
+            @Override public ObjectProperty<Node> leftProperty() {
+                return CustomTextField.this.leftProperty();
+            }
+            
+            @Override public ObjectProperty<Node> rightProperty() {
+                return CustomTextField.this.rightProperty();
+            }
+        };
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    @Override public String getUserAgentStylesheet() {
+        return CustomTextField.class.getResource("customtextfield.css").toExternalForm(); //$NON-NLS-1$
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/control/textfield/TextFields.java b/controlsfx/src/main/java/org/controlsfx/control/textfield/TextFields.java
new file mode 100644
index 0000000..3f8d7ab
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/textfield/TextFields.java
@@ -0,0 +1,190 @@
+/**
+ * Copyright (c) 2014, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.textfield;
+
+import impl.org.controlsfx.autocompletion.AutoCompletionTextFieldBinding;
+import impl.org.controlsfx.autocompletion.SuggestionProvider;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import javafx.animation.FadeTransition;
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.beans.property.ObjectProperty;
+import javafx.scene.Cursor;
+import javafx.scene.Node;
+import javafx.scene.control.PasswordField;
+import javafx.scene.control.TextField;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.StackPane;
+import javafx.util.Callback;
+import javafx.util.Duration;
+import javafx.util.StringConverter;
+
+import org.controlsfx.control.textfield.AutoCompletionBinding.ISuggestionRequest;
+
+/**
+ * A class containing useful customizations for the JavaFX {@link TextField}.
+ * Note that this class is experimental and the API may change in future 
+ * releases. Note also that this class makes use of the {@link CustomTextField}
+ * class.
+ * 
+ * @see CustomTextField
+ */
+public class TextFields {
+    private static final Duration FADE_DURATION = Duration.millis(350);
+
+    private TextFields() {
+        // no-op
+    }
+
+    /***************************************************************************
+     *                                                                         *
+     * Search fields                                                           *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * Creates a TextField that shows a clear button inside the TextField (on
+     * the right hand side of it) when text is entered by the user.
+     */
+    public static TextField createClearableTextField() {
+        CustomTextField inputField = new CustomTextField();
+        setupClearButtonField(inputField, inputField.rightProperty());
+        return inputField;
+    }
+    
+    /**
+     * Creates a PasswordField that shows a clear button inside the PasswordField
+     * (on the right hand side of it) when text is entered by the user.
+     */
+    public static PasswordField createClearablePasswordField() {
+        CustomPasswordField inputField = new CustomPasswordField();
+        setupClearButtonField(inputField, inputField.rightProperty());
+        return inputField;
+    }
+    
+    private static void setupClearButtonField(TextField inputField, ObjectProperty<Node> rightProperty) {
+        inputField.getStyleClass().add("clearable-field"); //$NON-NLS-1$
+
+        Region clearButton = new Region();
+        clearButton.getStyleClass().addAll("graphic"); //$NON-NLS-1$
+        StackPane clearButtonPane = new StackPane(clearButton);
+        clearButtonPane.getStyleClass().addAll("clear-button"); //$NON-NLS-1$
+        clearButtonPane.setOpacity(0.0);
+        clearButtonPane.setCursor(Cursor.DEFAULT);
+        clearButtonPane.setOnMouseReleased(e -> inputField.clear());
+        clearButtonPane.managedProperty().bind(inputField.editableProperty());
+        clearButtonPane.visibleProperty().bind(inputField.editableProperty());
+
+        rightProperty.set(clearButtonPane);
+
+        final FadeTransition fader = new FadeTransition(FADE_DURATION, clearButtonPane);
+        fader.setCycleCount(1);
+
+        inputField.textProperty().addListener(new InvalidationListener() {
+            @Override public void invalidated(Observable arg0) {
+                String text = inputField.getText();
+                boolean isTextEmpty = text == null || text.isEmpty();
+                boolean isButtonVisible = fader.getNode().getOpacity() > 0;
+
+                if (isTextEmpty && isButtonVisible) {
+                    setButtonVisible(false);
+                } else if (!isTextEmpty && !isButtonVisible) {
+                    setButtonVisible(true);
+                }
+            }
+
+            private void setButtonVisible( boolean visible ) {
+                fader.setFromValue(visible? 0.0: 1.0);
+                fader.setToValue(visible? 1.0: 0.0);
+                fader.play();
+            }
+        });
+    }
+
+    /***************************************************************************
+     *                                                                         *
+     * Auto-completion                                                         *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * Create a new auto-completion binding between the given textField and the 
+     * given suggestion provider.
+     * 
+     * The {@link TextFields} API has some suggestion-provider builder methods 
+     * for simple use cases.
+     * 
+     * @param textField The {@link TextField} to which auto-completion shall be added
+     * @param suggestionProvider A suggestion-provider strategy to use
+     * @param converter The converter to be used to convert suggestions to strings
+     */
+	public static <T> AutoCompletionBinding<T> bindAutoCompletion(TextField textField,
+			Callback<ISuggestionRequest, Collection<T>> suggestionProvider,
+			StringConverter<T> converter) {
+		return new AutoCompletionTextFieldBinding<>(textField,
+				suggestionProvider, converter);
+	}    
+    
+    /**
+     * Create a new auto-completion binding between the given textField and the 
+     * given suggestion provider.
+     * 
+     * The {@link TextFields} API has some suggestion-provider builder methods 
+     * for simple use cases.
+     * 
+     * @param textField The {@link TextField} to which auto-completion shall be added
+     * @param suggestionProvider A suggestion-provider strategy to use
+     * @return The AutoCompletionBinding
+     */
+    public static <T> AutoCompletionBinding<T> bindAutoCompletion(TextField textField, 
+    		Callback<ISuggestionRequest, Collection<T>> suggestionProvider){
+        return new AutoCompletionTextFieldBinding<>(textField, suggestionProvider);
+    }
+
+    /**
+     * Create a new auto-completion binding between the given {@link TextField} 
+     * using the given auto-complete suggestions
+     * 
+     * @param textField The {@link TextField} to which auto-completion shall be added
+     * @param possibleSuggestions Possible auto-complete suggestions
+     * @return The AutoCompletionBinding
+     */
+	public static <T> AutoCompletionBinding<T> bindAutoCompletion(
+			TextField textField, @SuppressWarnings("unchecked") T... possibleSuggestions) {
+		return bindAutoCompletion(textField, Arrays.asList(possibleSuggestions));
+	}
+    
+	public static <T> AutoCompletionBinding<T> bindAutoCompletion(
+			TextField textField, Collection<T> possibleSuggestions) {
+		return new AutoCompletionTextFieldBinding<>(textField,
+				SuggestionProvider.create(possibleSuggestions));
+	}
+}
+
diff --git a/controlsfx/src/main/java/org/controlsfx/control/textfield/package-info.java b/controlsfx/src/main/java/org/controlsfx/control/textfield/package-info.java
new file mode 100644
index 0000000..f7985d8
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/control/textfield/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * A package containing a number of useful classes related to text input.
+ */
+package org.controlsfx.control.textfield;
\ No newline at end of file
diff --git a/controlsfx/src/main/java/org/controlsfx/dialog/CommandLinksDialog.java b/controlsfx/src/main/java/org/controlsfx/dialog/CommandLinksDialog.java
new file mode 100644
index 0000000..4d2e0e5
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/dialog/CommandLinksDialog.java
@@ -0,0 +1,298 @@
+/**
+ * Copyright (c) 2014, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.dialog;
+
+import static impl.org.controlsfx.i18n.Localization.getString;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javafx.beans.binding.DoubleBinding;
+import javafx.collections.ListChangeListener;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.geometry.VPos;
+import javafx.scene.Node;
+import javafx.scene.control.Button;
+import javafx.scene.control.ButtonBar.ButtonData;
+import javafx.scene.control.ButtonType;
+import javafx.scene.control.Dialog;
+import javafx.scene.control.DialogPane;
+import javafx.scene.control.Label;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.Priority;
+
+public class CommandLinksDialog extends Dialog<ButtonType> {
+    
+    public static class CommandLinksButtonType {
+        private final ButtonType buttonType;
+        private final String longText;
+        private final Node graphic;
+        private boolean isHidden = false;
+        
+        public CommandLinksButtonType(String text, boolean isDefault ) {
+            this(new ButtonType(text, buildButtonData(isDefault)), null);
+        }
+        
+        public CommandLinksButtonType(String text, String longText, boolean isDefault) {
+            this(new ButtonType(text, buildButtonData(isDefault)), longText, null);
+        }
+        
+        public CommandLinksButtonType(String text, String longText, Node graphic, boolean isDefault) {
+            this(new ButtonType(text, buildButtonData(isDefault)), longText, graphic);
+        }
+        
+        private CommandLinksButtonType(ButtonType buttonType) {
+            this(buttonType, null);
+        }
+        
+        private CommandLinksButtonType(ButtonType buttonType, String longText) {
+            this(buttonType, longText, null);
+        }
+        
+        private CommandLinksButtonType(ButtonType buttonType, String longText, Node graphic) {
+            this.buttonType = buttonType;
+            this.longText = longText;
+            this.graphic = graphic;
+            
+        }
+        
+        private static ButtonData buildButtonData( boolean isDeafault) {
+        	return isDeafault? ButtonData.OK_DONE :ButtonData.OTHER;
+        }
+        
+        private static CommandLinksButtonType buildHiddenCancelLink() {
+            CommandLinksButtonType link = new CommandLinksButtonType(new ButtonType("",ButtonData.CANCEL_CLOSE));
+            link.isHidden = true;
+            return link;
+        }
+        
+        public ButtonType getButtonType() {
+            return buttonType;
+        }
+        
+        public Node getGraphic() {
+            return graphic;
+        }
+        
+        public String getLongText() {
+            return longText;
+        }
+    }
+    
+    
+    private final static int gapSize = 10;
+    
+    private final Map<ButtonType, CommandLinksButtonType> typeMap;
+    
+    private Label contentTextLabel;
+    
+    private GridPane grid = new GridPane() {
+        @Override protected double computePrefWidth(double height) {
+            boolean isDefault = true;
+            double pw = 0;
+
+            for (ButtonType buttonType : getDialogPane().getButtonTypes()) {
+                Button button = (Button) getDialogPane().lookupButton(buttonType);
+                double buttonPrefWidth = button.getGraphic().prefWidth(-1);
+                
+                if (isDefault) {
+                    pw = buttonPrefWidth;
+                    isDefault = false;
+                } else {
+                    pw = Math.min(pw, buttonPrefWidth);
+                }
+            }
+            return pw + gapSize;
+        }
+
+        @Override protected double computePrefHeight(double width) {
+            double ph = getDialogPane().getHeader() == null ? 0 : 10;
+
+            for (ButtonType buttonType : getDialogPane().getButtonTypes()) {
+                Button button = (Button) getDialogPane().lookupButton(buttonType);
+                ph += button.prefHeight(width) + gapSize;
+            }
+            
+            // TODO remove magic number
+            return ph * 1.2;
+        }
+    };
+    
+    public CommandLinksDialog(CommandLinksButtonType... links) {
+        this(Arrays.asList(links));
+    }
+    
+    public CommandLinksDialog(List<CommandLinksButtonType> links) {
+        this.grid.setHgap(gapSize);
+        this.grid.setVgap(gapSize);
+        this.grid.getStyleClass().add("container"); //$NON-NLS-1$
+        
+        final DialogPane dialogPane = new DialogPane() {
+            @Override protected Node createButtonBar() {
+                return null;
+            }
+            
+            @Override protected Node createButton(ButtonType buttonType) {
+                return createCommandLinksButton(buttonType);
+            }
+        }; 
+        setDialogPane(dialogPane);
+        
+        setTitle(getString("Dialog.info.title")); //$NON-NLS-1$
+        dialogPane.getStyleClass().add("command-links-dialog"); //$NON-NLS-1$
+        dialogPane.getStylesheets().add(getClass().getResource("dialogs.css").toExternalForm()); //$NON-NLS-1$
+        dialogPane.getStylesheets().add(getClass().getResource("commandlink.css").toExternalForm()); //$NON-NLS-1$
+        
+        // create a map from ButtonType -> CommandLinkButtonType, and put the 
+        // ButtonType values into the dialog pane
+        
+        typeMap = new HashMap<>();
+        for (CommandLinksButtonType link : links) { 
+            addLinkToDialog(dialogPane,link);
+        }
+        addLinkToDialog(dialogPane,CommandLinksButtonType.buildHiddenCancelLink());
+        
+        updateGrid();
+        dialogPane.getButtonTypes().addListener((ListChangeListener<? super ButtonType>)c -> updateGrid());
+        
+        contentTextProperty().addListener(o -> updateContentText());
+    }
+    
+    private void addLinkToDialog(DialogPane dialogPane, CommandLinksButtonType link) {
+         typeMap.put(link.getButtonType(), link); 
+         dialogPane.getButtonTypes().add(link.getButtonType()); 
+    }
+    
+    private void updateContentText() {
+        String contentText = getDialogPane().getContentText();
+        grid.getChildren().remove(contentTextLabel);
+        if (contentText != null && ! contentText.isEmpty()) {
+            if (contentTextLabel != null) {
+                contentTextLabel.setText(contentText);
+            } else {
+                contentTextLabel = new Label(getDialogPane().getContentText());
+                contentTextLabel.getStyleClass().add("command-link-message"); //$NON-NLS-1$
+            }
+            grid.add(contentTextLabel, 0, 0);
+        }
+    }
+    
+    private void updateGrid() {
+        grid.getChildren().clear();
+        
+        // add the message to the top of the dialog
+        updateContentText();
+        
+        // then build all the buttons
+        int row = 1;
+        for (final ButtonType buttonType : getDialogPane().getButtonTypes()) {
+            if (buttonType == null) continue; 
+
+            final Button button = (Button)getDialogPane().lookupButton(buttonType);   
+
+            GridPane.setHgrow(button, Priority.ALWAYS);
+            GridPane.setVgrow(button, Priority.ALWAYS);
+            grid.add(button, 0, row++);
+        }
+
+//        // last button gets some extra padding (hacky)
+//        GridPane.setMargin(buttons.get(buttons.size() - 1), new Insets(0,0,10,0));
+
+        getDialogPane().setContent(grid);
+        getDialogPane().requestLayout();
+    }
+    
+    private Button createCommandLinksButton(ButtonType buttonType) {
+        // look up the CommandLinkButtonType for the given ButtonType
+        CommandLinksButtonType commandLink = typeMap.getOrDefault(buttonType, new CommandLinksButtonType(buttonType));
+        
+        
+        // put the content inside a button
+        final Button button = new Button();
+        button.getStyleClass().addAll("command-link-button"); //$NON-NLS-1$
+        button.setMaxHeight(Double.MAX_VALUE);
+        button.setMaxWidth(Double.MAX_VALUE);
+        button.setAlignment(Pos.CENTER_LEFT);
+        
+        final ButtonData buttonData = buttonType.getButtonData();
+        button.setDefaultButton(buttonData != null && buttonData.isDefaultButton());
+        button.setOnAction(ae -> setResult(buttonType));
+
+        final Label titleLabel = new Label(commandLink.getButtonType().getText() );
+        titleLabel.minWidthProperty().bind(new DoubleBinding() {
+            {
+                bind(titleLabel.prefWidthProperty());
+            }
+
+            @Override protected double computeValue() {
+                return titleLabel.getPrefWidth() + 400;
+            }
+        });
+        titleLabel.getStyleClass().addAll("line-1"); //$NON-NLS-1$
+        titleLabel.setWrapText(true);
+        titleLabel.setAlignment(Pos.TOP_LEFT);
+        GridPane.setVgrow(titleLabel, Priority.NEVER);
+
+        Label messageLabel = new Label(commandLink.getLongText() );
+        messageLabel.getStyleClass().addAll("line-2"); //$NON-NLS-1$
+        messageLabel.setWrapText(true);
+        messageLabel.setAlignment(Pos.TOP_LEFT);
+        messageLabel.setMaxHeight(Double.MAX_VALUE);
+        GridPane.setVgrow(messageLabel, Priority.SOMETIMES);
+
+        Node commandLinkImage = commandLink.getGraphic();
+        Node view = commandLinkImage == null ? 
+                new ImageView(CommandLinksDialog.class.getResource("arrow-green-right.png").toExternalForm()) :  //$NON-NLS-1$
+                commandLinkImage;
+        Pane graphicContainer = new Pane(view);
+        graphicContainer.getStyleClass().add("graphic-container"); //$NON-NLS-1$
+        GridPane.setValignment(graphicContainer, VPos.TOP);
+        GridPane.setMargin(graphicContainer, new Insets(0,10,0,0));
+
+        GridPane grid = new GridPane();
+        grid.minWidthProperty().bind(titleLabel.prefWidthProperty());
+        grid.setMaxHeight(Double.MAX_VALUE);
+        grid.setMaxWidth(Double.MAX_VALUE);
+        grid.getStyleClass().add("container"); //$NON-NLS-1$
+        grid.add(graphicContainer, 0, 0, 1, 2);
+        grid.add(titleLabel, 1, 0);
+        grid.add(messageLabel, 1, 1);
+
+        button.setGraphic(grid);
+        button.minWidthProperty().bind(titleLabel.prefWidthProperty());
+        
+        if (commandLink.isHidden) {
+            button.setVisible(false);
+            button.setPrefHeight(1);
+        }
+        return button;
+    }    
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/dialog/DialogUtils.java b/controlsfx/src/main/java/org/controlsfx/dialog/DialogUtils.java
new file mode 100644
index 0000000..21fdd4c
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/dialog/DialogUtils.java
@@ -0,0 +1,61 @@
+/**
+ * Copyright (c) 2014 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.dialog;
+
+import javafx.scene.control.ButtonType;
+import javafx.scene.control.Dialog;
+import javafx.scene.control.DialogPane;
+import javafx.scene.control.ButtonBar.ButtonData;
+
+// package scope
+class DialogUtils {
+
+    static void forcefullyHideDialog(javafx.scene.control.Dialog<?> dialog) {
+        // for the dialog to be able to hide, we need a cancel button,
+        // so lets put one in now and then immediately call hide, and then
+        // remove the button again (if necessary).
+        DialogPane dialogPane = dialog.getDialogPane();
+        if (containsCancelButton(dialog)) {
+            dialog.hide();
+            return;
+        }
+        
+        dialogPane.getButtonTypes().add(ButtonType.CANCEL); 
+        dialog.hide();
+        dialogPane.getButtonTypes().remove(ButtonType.CANCEL);
+    }
+    
+    static boolean containsCancelButton(Dialog<?> dialog) {
+        DialogPane dialogPane = dialog.getDialogPane();
+        for (ButtonType type : dialogPane.getButtonTypes()) {
+            if (type.getButtonData() == ButtonData.CANCEL_CLOSE) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/dialog/ExceptionDialog.java b/controlsfx/src/main/java/org/controlsfx/dialog/ExceptionDialog.java
new file mode 100644
index 0000000..816b513
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/dialog/ExceptionDialog.java
@@ -0,0 +1,84 @@
+/**
+ * Copyright (c) 2014 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.dialog;
+
+import static impl.org.controlsfx.i18n.Localization.getString;
+import static impl.org.controlsfx.i18n.Localization.localize;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import javafx.scene.control.ButtonType;
+import javafx.scene.control.Dialog;
+import javafx.scene.control.DialogPane;
+import javafx.scene.control.Label;
+import javafx.scene.control.TextArea;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.Priority;
+
+public class ExceptionDialog extends Dialog<ButtonType> {
+
+    public ExceptionDialog(final Throwable exception) {
+        final DialogPane dialogPane = getDialogPane();
+        
+        setTitle(getString("exception.dlg.title")); //$NON-NLS-1$
+        dialogPane.setHeaderText(getString("exception.dlg.header")); //$NON-NLS-1$
+        dialogPane.getStyleClass().add("exception-dialog"); //$NON-NLS-1$
+        dialogPane.getStylesheets().add(ProgressDialog.class.getResource("dialogs.css").toExternalForm()); //$NON-NLS-1$
+        dialogPane.getButtonTypes().addAll(ButtonType.OK);
+        
+        // --- content
+        String contentText = getContentText();
+        dialogPane.setContent(new Label(contentText != null && ! contentText.isEmpty() ? 
+                contentText : exception.getMessage()));
+        
+        // --- expandable content
+        StringWriter sw = new StringWriter();
+        PrintWriter pw = new PrintWriter(sw);
+        exception.printStackTrace(pw);
+        String exceptionText = sw.toString();
+        
+        Label label = new Label( localize(getString("exception.dlg.label"))); //$NON-NLS-1$
+
+        TextArea textArea = new TextArea(exceptionText);
+        textArea.setEditable(false);
+        textArea.setWrapText(true);
+        
+        textArea.setMaxWidth(Double.MAX_VALUE);
+        textArea.setMaxHeight(Double.MAX_VALUE);
+        GridPane.setVgrow(textArea, Priority.ALWAYS);
+        GridPane.setHgrow(textArea, Priority.ALWAYS);
+        
+        GridPane root = new GridPane();
+        root.setMaxWidth(Double.MAX_VALUE);
+        root.add(label, 0, 0);
+        root.add(textArea, 0, 1);
+        
+        
+        dialogPane.setExpandableContent(root);
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/dialog/FontSelectorDialog.java b/controlsfx/src/main/java/org/controlsfx/dialog/FontSelectorDialog.java
new file mode 100644
index 0000000..f725ae1
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/dialog/FontSelectorDialog.java
@@ -0,0 +1,367 @@
+/**
+ * Copyright (c) 2014 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.dialog;
+
+import static impl.org.controlsfx.i18n.Localization.asKey;
+import static impl.org.controlsfx.i18n.Localization.localize;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Predicate;
+
+import javafx.application.Platform;
+import javafx.beans.binding.DoubleBinding;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.FXCollections;
+import javafx.collections.transformation.FilteredList;
+import javafx.geometry.Pos;
+import javafx.scene.control.ButtonType;
+import javafx.scene.control.Dialog;
+import javafx.scene.control.DialogPane;
+import javafx.scene.control.Label;
+import javafx.scene.control.ListCell;
+import javafx.scene.control.ListView;
+import javafx.scene.layout.ColumnConstraints;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.RowConstraints;
+import javafx.scene.layout.StackPane;
+import javafx.scene.shape.Rectangle;
+import javafx.scene.text.Font;
+import javafx.scene.text.FontPosture;
+import javafx.scene.text.FontWeight;
+import javafx.scene.text.Text;
+import javafx.util.Callback;
+
+public class FontSelectorDialog extends Dialog<Font> {
+    
+    private FontPanel fontPanel;
+
+    public FontSelectorDialog(Font defaultFont) {
+        fontPanel = new FontPanel();
+        fontPanel.setFont(defaultFont);
+        
+        setResultConverter(dialogButton -> dialogButton == ButtonType.OK ? fontPanel.getFont() : null);
+                
+        final DialogPane dialogPane = getDialogPane();
+        
+        setTitle(localize(asKey("font.dlg.title"))); //$NON-NLS-1$
+        dialogPane.setHeaderText(localize(asKey("font.dlg.header"))); //$NON-NLS-1$
+        dialogPane.getStyleClass().add("font-selector-dialog"); //$NON-NLS-1$
+        dialogPane.getStylesheets().add(FontSelectorDialog.class.getResource("dialogs.css").toExternalForm()); //$NON-NLS-1$
+        dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
+        dialogPane.setContent(fontPanel);
+    }
+    
+
+    
+    /**************************************************************************
+     * 
+     * Support classes
+     * 
+     **************************************************************************/
+
+    /**
+     * Font style as combination of font weight and font posture. 
+     * Weight does not have to be there (represented by null)
+     * Posture is required, null posture is converted to REGULAR
+     */
+    private static class FontStyle implements Comparable<FontStyle> {
+
+        private FontPosture posture; 
+        private FontWeight weight;
+
+        public FontStyle( FontWeight weight, FontPosture posture ) {
+            this.posture = posture == null? FontPosture.REGULAR: posture;
+            this.weight = weight;
+        }
+
+        public FontStyle() {
+            this( null, null);
+        }
+
+        public FontStyle(String styles) {
+            this();
+            String[] fontStyles = (styles == null? "": styles.trim().toUpperCase()).split(" "); //$NON-NLS-1$ //$NON-NLS-2$
+            for ( String style: fontStyles) {
+                FontWeight w = FontWeight.findByName(style);
+                if ( w != null ) {
+                    weight = w;
+                } else {
+                    FontPosture p = FontPosture.findByName(style);
+                    if ( p != null ) posture = p;
+                }
+            }
+        }
+
+        public FontStyle(Font font) {
+            this( font.getStyle());
+        }
+
+        public FontPosture getPosture() {
+            return posture;
+        }
+
+        public FontWeight getWeight() {
+            return weight;
+        }
+
+
+        @Override public int hashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + ((posture == null) ? 0 : posture.hashCode());
+            result = prime * result + ((weight == null) ? 0 : weight.hashCode());
+            return result;
+        }
+
+        @Override public boolean equals(Object that) {
+            if (this == that)
+                return true;
+            if (that == null)
+                return false;
+            if (getClass() != that.getClass())
+                return false;
+            FontStyle other = (FontStyle) that;
+            if (posture != other.posture)
+                return false;
+            if (weight != other.weight)
+                return false;
+            return true;
+        }
+
+        private static String makePretty(Object o) {
+            String s = o == null? "": o.toString(); //$NON-NLS-1$
+            if ( !s.isEmpty()) { 
+                s = s.replace("_", " "); //$NON-NLS-1$ //$NON-NLS-2$
+                s = s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase();
+            }
+            return s;
+        }
+
+        @Override public String toString() {
+            return String.format("%s %s", makePretty(weight), makePretty(posture) ).trim(); //$NON-NLS-1$
+        }
+
+        private <T extends Enum<T>> int compareEnums( T e1, T e2) {
+            if ( e1 == e2 ) return 0;
+            if ( e1 == null ) return -1;
+            if ( e2 == null ) return 1;
+            return e1.compareTo(e2);
+        }
+
+        @Override public int compareTo(FontStyle fs) {
+            int result = compareEnums(weight,fs.weight);
+            return ( result != 0 )? result: compareEnums(posture,fs.posture);
+        }
+
+    }    
+
+
+    private static class FontPanel extends GridPane {
+        private static final double HGAP = 10;
+        private static final double VGAP = 5;
+
+        private static final Predicate<Object> MATCH_ALL = new Predicate<Object>() {
+            @Override public boolean test(Object t) {
+                return true;
+            }
+        };
+
+        private static final Double[] fontSizes = new Double[] {8d,9d,11d,12d,14d,16d,18d,20d,22d,24d,26d,28d,36d,48d,72d};
+
+        private static List<FontStyle> getFontStyles( String fontFamily ) {
+            Set<FontStyle> set = new HashSet<>();
+            for (String f : Font.getFontNames(fontFamily)) {
+                set.add(new FontStyle(f.replace(fontFamily, ""))); //$NON-NLS-1$
+            }
+
+            List<FontStyle> result =  new ArrayList<>(set);
+            Collections.sort(result);
+            return result;
+
+        }
+
+
+        private final FilteredList<String> filteredFontList = new FilteredList<>(FXCollections.observableArrayList(Font.getFamilies()), MATCH_ALL);
+        private final FilteredList<FontStyle> filteredStyleList = new FilteredList<>(FXCollections.<FontStyle>observableArrayList(), MATCH_ALL);
+        private final FilteredList<Double> filteredSizeList = new FilteredList<>(FXCollections.observableArrayList(fontSizes), MATCH_ALL);
+
+        private final ListView<String> fontListView = new ListView<>(filteredFontList);
+        private final ListView<FontStyle> styleListView = new ListView<>(filteredStyleList);
+        private final ListView<Double> sizeListView = new ListView<>(filteredSizeList);
+        private final Text sample = new Text(localize(asKey("font.dlg.sample.text"))); //$NON-NLS-1$
+
+        public FontPanel() {
+            setHgap(HGAP);
+            setVgap(VGAP);
+            setPrefSize(500, 300);
+            setMinSize(500, 300);
+
+            ColumnConstraints c0 = new ColumnConstraints();
+            c0.setPercentWidth(60);
+            ColumnConstraints c1 = new ColumnConstraints();
+            c1.setPercentWidth(25);
+            ColumnConstraints c2 = new ColumnConstraints();
+            c2.setPercentWidth(15);
+            getColumnConstraints().addAll(c0, c1, c2);
+
+            RowConstraints r0 = new RowConstraints();
+            r0.setVgrow(Priority.NEVER);
+            RowConstraints r1 = new RowConstraints();
+            r1.setVgrow(Priority.NEVER);
+            RowConstraints r2 = new RowConstraints();
+            r2.setFillHeight(true);
+            r2.setVgrow(Priority.NEVER);
+            RowConstraints r3 = new RowConstraints();
+            r3.setPrefHeight(250);
+            r3.setVgrow(Priority.NEVER);
+            getRowConstraints().addAll(r0, r1, r2, r3);
+
+            // layout hello.dialog
+            add(new Label(localize(asKey("font.dlg.font.label"))), 0, 0); //$NON-NLS-1$
+            //            fontSearch.setMinHeight(Control.USE_PREF_SIZE);
+            //            add( fontSearch, 0, 1);
+            add(fontListView, 0, 1);
+            fontListView.setCellFactory(new Callback<ListView<String>, ListCell<String>>() {
+                @Override public ListCell<String> call(ListView<String> listview) {
+                    return new ListCell<String>() {
+                        @Override protected void updateItem(String family, boolean empty) {
+                            super.updateItem(family, empty);
+
+                            if (! empty) {
+                                setFont(Font.font(family));
+                                setText(family);
+                            } else {
+                                setText(null);
+                            }
+                        }
+                    };
+                }
+            });
+
+
+            ChangeListener<Object> sampleRefreshListener = new ChangeListener<Object>() {
+                @Override public void changed(ObservableValue<? extends Object> arg0, Object arg1, Object arg2) {
+                    refreshSample();
+                }
+            };
+
+            fontListView.selectionModelProperty().get().selectedItemProperty().addListener( new ChangeListener<String>() {
+
+                @Override public void changed(ObservableValue<? extends String> arg0, String arg1, String arg2) {
+                    String fontFamily = listSelection(fontListView);
+                    styleListView.setItems(FXCollections.<FontStyle>observableArrayList(getFontStyles(fontFamily)));       
+                    refreshSample();
+                }});
+
+            add( new Label(localize(asKey("font.dlg.style.label"))), 1, 0); //$NON-NLS-1$
+            //            postureSearch.setMinHeight(Control.USE_PREF_SIZE);
+            //            add( postureSearch, 1, 1);
+            add(styleListView, 1, 1);
+            styleListView.selectionModelProperty().get().selectedItemProperty().addListener(sampleRefreshListener);
+
+            add( new Label(localize(asKey("font.dlg.size.label"))), 2, 0); //$NON-NLS-1$
+            //            sizeSearch.setMinHeight(Control.USE_PREF_SIZE);
+            //            add( sizeSearch, 2, 1);
+            add(sizeListView, 2, 1);
+            sizeListView.selectionModelProperty().get().selectedItemProperty().addListener(sampleRefreshListener);
+
+            final double height = 45;
+            final DoubleBinding sampleWidth = new DoubleBinding() {
+                {
+                    bind(fontListView.widthProperty(), styleListView.widthProperty(), sizeListView.widthProperty());
+                }
+
+                @Override protected double computeValue() {
+                    return fontListView.getWidth() + styleListView.getWidth() + sizeListView.getWidth() + 3 * HGAP;
+                }
+            };
+            StackPane sampleStack = new StackPane(sample);
+            sampleStack.setAlignment(Pos.CENTER_LEFT);
+            sampleStack.setMinHeight(height);
+            sampleStack.setPrefHeight(height);
+            sampleStack.setMaxHeight(height);
+            sampleStack.prefWidthProperty().bind(sampleWidth);
+            Rectangle clip = new Rectangle(0, height);
+            clip.widthProperty().bind(sampleWidth);
+            sampleStack.setClip(clip);
+            add(sampleStack, 0, 3, 1, 3);
+        }
+
+        public void setFont(final Font font) {
+            final Font _font = font == null ? Font.getDefault() : font;
+            if (_font != null) {
+                selectInList( fontListView,  _font.getFamily() );
+                selectInList( styleListView, new FontStyle(_font));
+                selectInList( sizeListView, _font.getSize() );
+            }
+        }
+
+        public Font getFont() {
+            try {
+                FontStyle style = listSelection(styleListView);
+                if ( style == null ) {
+                    return Font.font(
+                            listSelection(fontListView),
+                            listSelection(sizeListView));
+
+                } else { 
+                    return Font.font(
+                            listSelection(fontListView),
+                            style.getWeight(),
+                            style.getPosture(),
+                            listSelection(sizeListView));
+                }
+
+            } catch( Throwable ex ) {
+                return null;
+            }
+        }
+
+        private void refreshSample() {
+            sample.setFont(getFont());
+        }
+
+        private <T> void selectInList( final ListView<T> listView, final T selection ) {
+            Platform.runLater(new Runnable() {
+                @Override public void run() {
+                    listView.scrollTo(selection);
+                    listView.getSelectionModel().select(selection);
+                }
+            });
+        }
+
+        private <T> T listSelection(final ListView<T> listView) {
+            return listView.selectionModelProperty().get().getSelectedItem();
+        }
+    }  
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/dialog/LoginDialog.java b/controlsfx/src/main/java/org/controlsfx/dialog/LoginDialog.java
new file mode 100644
index 0000000..018f748
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/dialog/LoginDialog.java
@@ -0,0 +1,152 @@
+/**
+ * Copyright (c) 2014, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.dialog;
+
+import static impl.org.controlsfx.i18n.Localization.getString;
+import javafx.application.Platform;
+import javafx.scene.control.Button;
+import javafx.scene.control.ButtonBar.ButtonData;
+import javafx.scene.control.ButtonType;
+import javafx.scene.control.Dialog;
+import javafx.scene.control.DialogPane;
+import javafx.scene.control.Label;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.VBox;
+import javafx.util.Callback;
+import javafx.util.Pair;
+
+import org.controlsfx.control.textfield.CustomPasswordField;
+import org.controlsfx.control.textfield.CustomTextField;
+import org.controlsfx.control.textfield.TextFields;
+import org.controlsfx.validation.ValidationSupport;
+import org.controlsfx.validation.Validator;
+
+public class LoginDialog extends Dialog<Pair<String,String>> {
+    
+    private final ButtonType loginButtonType;
+    private final CustomTextField txUserName;
+    private final CustomPasswordField txPassword;
+
+    @SuppressWarnings("deprecation")
+    public LoginDialog(final Pair<String,String> initialUserInfo, final Callback<Pair<String,String>, Void> authenticator) {
+        final DialogPane dialogPane = getDialogPane();
+        
+        setTitle(getString("login.dlg.title")); //$NON-NLS-1$
+        dialogPane.setHeaderText(getString("login.dlg.header")); //$NON-NLS-1$
+        dialogPane.getStyleClass().add("login-dialog"); //$NON-NLS-1$
+        dialogPane.getStylesheets().add(LoginDialog.class.getResource("dialogs.css").toExternalForm()); //$NON-NLS-1$
+        dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
+        
+        
+        
+        
+        
+        txUserName = (CustomTextField) TextFields.createClearableTextField();
+        
+        txUserName.setLeft(new ImageView(LoginDialog.class.getResource("/org/controlsfx/dialog/user.png").toExternalForm())); //$NON-NLS-1$
+        
+        txPassword = (CustomPasswordField) TextFields.createClearablePasswordField();
+        txPassword.setLeft(new ImageView(LoginDialog.class.getResource("/org/controlsfx/dialog/lock.png").toExternalForm())); //$NON-NLS-1$
+        
+        Label lbMessage= new Label("");  //$NON-NLS-1$
+        lbMessage.getStyleClass().addAll("message-banner"); //$NON-NLS-1$
+        lbMessage.setVisible(false);
+        lbMessage.setManaged(false);
+        
+        final VBox content = new VBox(10);
+        content.getChildren().add(lbMessage);
+        content.getChildren().add(txUserName);
+        content.getChildren().add(txPassword);
+        
+        dialogPane.setContent(content);
+        
+        loginButtonType = new javafx.scene.control.ButtonType(getString("login.dlg.login.button"), ButtonData.OK_DONE); //$NON-NLS-1$
+        dialogPane.getButtonTypes().addAll(loginButtonType);
+        Button loginButton = (Button) dialogPane.lookupButton(loginButtonType);
+        loginButton.setOnAction(actionEvent -> {
+            try {
+                if (authenticator != null ) {
+                    authenticator.call(new Pair<>(txUserName.getText(), txPassword.getText()));
+                }
+                lbMessage.setVisible(false);
+                lbMessage.setManaged(false);
+                hide();
+//                dlg.setResult(this);
+            } catch( Throwable ex ) {
+                lbMessage.setVisible(true);
+                lbMessage.setManaged(true);
+                lbMessage.setText(ex.getMessage());
+//                sizeToScene();
+//                dlg.shake();
+                ex.printStackTrace();
+            }
+        });
+        
+//        final Dialog dlg = buildDialog(Type.LOGIN);
+//        dlg.setContent(content);
+        
+//        dlg.setResizable(false);
+//        dlg.setIconifiable(false);
+//        if ( dlg.getGraphic() == null ) { 
+//            dlg.setGraphic( new ImageView( DialogResources.getImage("login.icon")));
+//        }
+//        dlg.getActions().setAll(actionLogin, ACTION_CANCEL);
+        String userNameCation = getString("login.dlg.user.caption"); //$NON-NLS-1$
+        String passwordCaption = getString("login.dlg.pswd.caption"); //$NON-NLS-1$
+        txUserName.setPromptText(userNameCation);
+        txUserName.setText(initialUserInfo == null ? "" : initialUserInfo.getKey()); //$NON-NLS-1$
+        txPassword.setPromptText(passwordCaption);
+        txPassword.setText(new String(initialUserInfo == null ? "" : initialUserInfo.getValue())); //$NON-NLS-1$
+
+        ValidationSupport validationSupport = new ValidationSupport();
+        Platform.runLater( () -> {
+            String requiredFormat = "'%s' is required"; //$NON-NLS-1$
+            validationSupport.registerValidator(txUserName, Validator.createEmptyValidator( String.format( requiredFormat, userNameCation )));
+            validationSupport.registerValidator(txPassword, Validator.createEmptyValidator(String.format( requiredFormat, passwordCaption )));
+//            loginButton.disabledProperty().bind(validationSupport.invalidProperty());
+            txUserName.requestFocus();
+        } );
+        
+        
+        setResultConverter(dialogButton -> dialogButton == loginButtonType ? 
+                new Pair<>(txUserName.getText(), txPassword.getText()) : null);
+
+//        return Optional.ofNullable( 
+//                dlg.show() == actionLogin? 
+//                        new Pair<String,String>(txUserName.getText(), txPassword.getText()): 
+//                        null);
+    }
+    
+
+    
+    /**************************************************************************
+     * 
+     * Support classes
+     * 
+     **************************************************************************/
+
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/dialog/ProgressDialog.java b/controlsfx/src/main/java/org/controlsfx/dialog/ProgressDialog.java
new file mode 100644
index 0000000..f57707e
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/dialog/ProgressDialog.java
@@ -0,0 +1,215 @@
+/**
+ * Copyright (c) 2014, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.dialog;
+
+import static impl.org.controlsfx.i18n.Localization.getString;
+import javafx.application.Platform;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.concurrent.Worker;
+import javafx.concurrent.Worker.State;
+import javafx.geometry.Insets;
+import javafx.scene.control.Dialog;
+import javafx.scene.control.DialogPane;
+import javafx.scene.control.Label;
+import javafx.scene.control.ProgressBar;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.VBox;
+
+public class ProgressDialog extends Dialog<Void> {
+    
+
+    public ProgressDialog(final Worker<?> worker) {
+        if (worker != null
+                && (worker.getState() == State.CANCELLED
+                || worker.getState() == State.FAILED
+                || worker.getState() == State.SUCCEEDED)) {
+            return;
+        }
+        setResultConverter(dialogButton -> null);
+                
+        final DialogPane dialogPane = getDialogPane();
+        
+        setTitle(getString("progress.dlg.title")); //$NON-NLS-1$
+        dialogPane.setHeaderText(getString("progress.dlg.header")); //$NON-NLS-1$
+        dialogPane.getStyleClass().add("progress-dialog"); //$NON-NLS-1$
+        dialogPane.getStylesheets().add(ProgressDialog.class.getResource("dialogs.css").toExternalForm()); //$NON-NLS-1$
+        
+        final Label progressMessage = new Label();
+        progressMessage.textProperty().bind(worker.messageProperty());
+
+        final WorkerProgressPane content = new WorkerProgressPane(this);
+        content.setMaxWidth(Double.MAX_VALUE);
+        content.setWorker(worker);
+        
+        VBox vbox = new VBox(10, progressMessage, content);
+        vbox.setMaxWidth(Double.MAX_VALUE);
+        vbox.setPrefSize(300, 100);
+        /**
+         * The content Text cannot be set before the constructor and since we
+         * set a Content Node, the contentText will not be shown. If we want to
+         * let the user display a content text, we must recreate it.
+         */
+        Label contentText = new Label();
+        contentText.setWrapText(true);
+        vbox.getChildren().add(0, contentText);
+        contentText.textProperty().bind(dialogPane.contentTextProperty());
+        dialogPane.setContent(vbox);
+    }
+    
+
+    
+    /**************************************************************************
+     * 
+     * Support classes
+     * 
+     **************************************************************************/
+
+    /**
+     * The WorkerProgressPane takes a {@link Dialog} and a {@link Worker}
+     * and links them together so the dialog is shown or hidden depending
+     * on the state of the worker.  The WorkerProgressPane also includes
+     * a progress bar that is automatically bound to the progress property
+     * of the worker.  The way in which the WorkerProgressPane shows and
+     * hides its worker's dialog is consistent with the expected behavior
+     * for {@link #showWorkerProgress(Worker)}.
+     */
+    private static class WorkerProgressPane extends Region {
+        private Worker<?> worker;
+
+        private boolean dialogVisible = false;
+        private boolean cancelDialogShow = false;
+
+        private ChangeListener<Worker.State> stateListener = new ChangeListener<Worker.State>() {
+            @Override public void changed(ObservableValue<? extends State> observable, State old, State value) {
+                switch(value) {
+                    case CANCELLED:
+                    case FAILED:
+                    case SUCCEEDED:
+                        if(!dialogVisible) {
+                            cancelDialogShow = true;
+                            end();
+                        } else if(old == State.SCHEDULED || old == State.RUNNING) {
+                            end();
+                        }
+                        break;
+                    case SCHEDULED:
+                        begin();
+                        break;
+                    default: //no-op     
+                }
+            }
+        };
+
+        public final void setWorker(final Worker<?> newWorker) { 
+            if (newWorker != worker) {
+                if (worker != null) {
+                    worker.stateProperty().removeListener(stateListener);
+                    end();
+                }
+
+                worker = newWorker;
+
+                if (newWorker != null) {
+                    newWorker.stateProperty().addListener(stateListener);
+                    if (newWorker.getState() == Worker.State.RUNNING || newWorker.getState() == Worker.State.SCHEDULED) {
+                        // It is already running
+                        begin();
+                    }
+                }
+            }
+        }
+
+        // If the progress indicator changes, then we need to re-initialize
+        // If the worker changes, we need to re-initialize
+
+        private final ProgressDialog dialog;
+        private final ProgressBar progressBar;
+        
+        public WorkerProgressPane(ProgressDialog dialog) {
+            this.dialog = dialog;
+            
+            this.progressBar = new ProgressBar();
+            progressBar.setMaxWidth(Double.MAX_VALUE);
+            getChildren().add(progressBar);
+            
+            if (worker != null) {
+                progressBar.progressProperty().bind(worker.progressProperty());
+            }
+        }
+
+        private void begin() {
+            // Platform.runLater needs to be used to show the dialog because
+            // the call begin() is going to be occurring when the worker is
+            // notifying state listeners about changes.  If Platform.runLater
+            // is not used, the call to show() will cause the worker to get
+            // blocked during notification and it will prevent the worker
+            // from performing any additional notification for state changes.
+            //
+            // Sine the dialog is hidden as a result of a change in worker
+            // state, calling show() without wrapping it in Platform.runLater
+            // will cause the progress dialog to run forever when the dialog
+            // is attached to workers that start out with a state of READY.
+            //
+            // This also creates a case where the worker's state can change
+            // to finished before the dialog is shown, resulting in an
+            // an attempt to hide the dialog before it is shown.  It's
+            // necessary to track whether or not this occurs, so flags are
+            // set to indicate if the dialog is visible and if if the call
+            // to show should still be allowed.
+            cancelDialogShow = false;
+
+            Platform.runLater(() -> {
+                if(!cancelDialogShow) {
+                    progressBar.progressProperty().bind(worker.progressProperty());
+                    dialogVisible = true;
+                    dialog.show();
+                }
+            });
+        }
+
+        private void end() {
+            progressBar.progressProperty().unbind();
+            dialogVisible = false;
+            DialogUtils.forcefullyHideDialog(dialog);
+        }
+
+        @Override protected void layoutChildren() {
+            if (progressBar != null) {
+                Insets insets = getInsets();
+                double w = getWidth() - insets.getLeft() - insets.getRight();
+                double h = getHeight() - insets.getTop() - insets.getBottom();
+
+                double prefH = progressBar.prefHeight(-1);
+                double x = insets.getLeft() + (w - w) / 2.0;
+                double y = insets.getTop() + (h - prefH) / 2.0;
+
+                progressBar.resizeRelocate(x, y, w, prefH);
+            }
+        }
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/dialog/Wizard.java b/controlsfx/src/main/java/org/controlsfx/dialog/Wizard.java
new file mode 100644
index 0000000..ee6c080
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/dialog/Wizard.java
@@ -0,0 +1,737 @@
+/**
+ * Copyright (c) 2014, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.dialog;
+
+import static impl.org.controlsfx.i18n.Localization.asKey;
+import static impl.org.controlsfx.i18n.Localization.localize;
+import impl.org.controlsfx.ImplUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Optional;
+import java.util.Stack;
+import java.util.function.BooleanSupplier;
+
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.collections.ObservableMap;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.geometry.Rectangle2D;
+import javafx.scene.Node;
+import javafx.scene.control.Button;
+import javafx.scene.control.ButtonBar.ButtonData;
+import javafx.scene.control.ButtonType;
+import javafx.scene.control.Dialog;
+import javafx.scene.control.DialogPane;
+import javafx.scene.layout.Pane;
+import javafx.stage.Screen;
+import javafx.stage.Window;
+
+import org.controlsfx.tools.ValueExtractor;
+import org.controlsfx.validation.ValidationSupport;
+
+/**
+ * <p>The API for creating multi-page Wizards, based on JavaFX {@link Dialog} API.<br> 
+ * Wizard can be setup in following few steps:</p>
+ * <ul>
+ *    <li>Design wizard pages by inheriting them from {@link WizardPane}</li>
+ *    <li>Define wizard flow by implementing {@link Wizard.Flow}</li>
+ *    <li>Create and instance of the Wizard and assign flow to it</li>
+ *    <li>Execute the wizard using showAndWait method</li>
+ *    <li>Values can be extracted from settings map by calling getSettings 
+ * </ul>    
+ * <p>For simple, linear wizards, the {@link LinearFlow} can be used. 
+ * It is a flow based on a collection of wizard pages. Here is the example:</p>
+ * 
+ * <pre>{@code // Create pages. Here for simplicity we just create and instance of WizardPane.
+ * WizardPane page1 = new WizardPane(); 
+ * WizardPane page2 = new WizardPane(); 
+ * WizardPane page2 = new WizardPane(); 
+ * 
+ * // create wizard
+ * Wizard wizard = new Wizard();
+ * 
+ * // create and assign the flow
+ * wizard.setFlow(new LinearFlow(page1, page2, page3));
+ *     
+ * // show wizard and wait for response
+ * wizard.showAndWait().ifPresent(result -> {
+ *     if (result == ButtonType.FINISH) {
+ *         System.out.println("Wizard finished, settings: " + wizard.getSettings());
+ *     }
+ * });}</pre>
+ * 
+ * <p>For more complex wizard flows we suggest to create a custom ones, describing page traversal logic. 
+ * Here is a simplified example: </p>
+ * 
+ * <pre>{@code Wizard.Flow branchingFlow = new Wizard.Flow() {
+ *     public Optional<WizardPane> advance(WizardPane currentPage) {
+ *         return Optional.of(getNext(currentPage));
+ *     }
+ *
+ *     public boolean canAdvance(WizardPane currentPage) {
+ *         return currentPage != page3;
+ *     }
+ *          
+ *     private WizardPane getNext(WizardPane currentPage) {
+ *         if ( currentPage == null ) {
+ *             return page1;
+ *         } else if ( currentPage == page1) {
+ *             // skipNextPage() does not exist - this just represents that you
+ *             // can add a conditional statement here to change the page.
+ *             return page1.skipNextPage()? page3: page2;
+ *         } else {
+ *             return page3;
+ *         }
+ *     }
+ * };}</pre>
+ */
+public class Wizard {
+    
+    
+    /**************************************************************************
+     * 
+     * Private fields
+     * 
+     **************************************************************************/
+    
+    private Dialog<ButtonType> dialog;
+    
+    private final ObservableMap<String, Object> settings = FXCollections.observableHashMap();
+    
+    private final Stack<WizardPane> pageHistory = new Stack<>(); 
+    
+    private Optional<WizardPane> currentPage = Optional.empty();
+
+    private final BooleanProperty invalidProperty = new SimpleBooleanProperty(false);
+
+    // Read settings activated by default for backward compatibility
+    private final BooleanProperty readSettingsProperty = new SimpleBooleanProperty(true);
+    
+    private final ButtonType BUTTON_PREVIOUS = new ButtonType(localize(asKey("wizard.previous.button")), ButtonData.BACK_PREVIOUS); //$NON-NLS-1$
+    private final EventHandler<ActionEvent> BUTTON_PREVIOUS_ACTION_HANDLER = actionEvent -> {
+        actionEvent.consume();
+        currentPage = Optional.ofNullable( pageHistory.isEmpty()? null: pageHistory.pop() );
+        updatePage(dialog,false);
+    };
+    
+    private final ButtonType BUTTON_NEXT = new ButtonType(localize(asKey("wizard.next.button")), ButtonData.NEXT_FORWARD); //$NON-NLS-1$
+    private final EventHandler<ActionEvent> BUTTON_NEXT_ACTION_HANDLER = actionEvent -> {
+        actionEvent.consume();
+        currentPage.ifPresent(page->pageHistory.push(page));
+        currentPage = getFlow().advance(currentPage.orElse(null));
+        updatePage(dialog,true);
+    };
+    
+    private final StringProperty titleProperty = new SimpleStringProperty(); 
+    
+    
+    /**************************************************************************
+     * 
+     * Constructors
+     * 
+     **************************************************************************/
+    
+    /**
+     * Creates an instance of the wizard without an owner.
+     */
+    public Wizard() {
+        this(null);
+    }
+    
+    /**
+     * Creates an instance of the wizard with the given owner.
+     * @param owner The object from which the owner window is deduced (typically
+     *      this is a Node, but it may also be a Scene or a Stage). 
+     */
+    public Wizard(Object owner) {
+        this(owner, ""); //$NON-NLS-1$
+    }
+    
+    /**
+     * Creates an instance of the wizard with the given owner and title.
+     * 
+     * @param owner The object from which the owner window is deduced (typically
+     *      this is a Node, but it may also be a Scene or a Stage). 
+     * @param title The wizard title.
+     */
+    public Wizard(Object owner, String title) {
+    	
+        invalidProperty.addListener( (o, ov, nv) -> validateActionState());
+        
+        dialog = new Dialog<>();
+        dialog.titleProperty().bind(this.titleProperty);
+        setTitle(title);
+        
+        Window window = null;
+        if ( owner instanceof Window) { 
+        	window = (Window)owner;
+        } else if ( owner instanceof Node ) {
+        	window = ((Node)owner).getScene().getWindow();
+        }
+        
+        dialog.initOwner(window);
+    }
+    
+    
+    
+    /**************************************************************************
+     * 
+     * Public API
+     * 
+     **************************************************************************/
+    
+//    /**
+//     * Shows the wizard but does not wait for a user response (in other words,
+//     * this brings up a non-blocking dialog). Users of this API must either
+//     * poll the {@link #resultProperty() result property}, or else add a listener
+//     * to the result property to be informed of when it is set.
+//     */
+//    public final void show() {
+//        dialog.show();
+//    }
+    
+    /**
+     * Shows the wizard and waits for the user response (in other words, brings 
+     * up a blocking dialog, with the returned value the users input).
+     * 
+     * @return An {@link Optional} that contains the result.
+     */
+    public final Optional<ButtonType> showAndWait() {
+        return dialog.showAndWait();
+    }
+
+    /**
+     * @return {@link Dialog#resultProperty()} of the {@link Dialog} representing this {@link Wizard}.
+     */
+    public final ObjectProperty<ButtonType> resultProperty() {
+            return dialog.resultProperty();
+    }
+    
+    /**
+     * The settings map is the place where all data from pages is kept once the 
+     * user moves on from the page, assuming there is a {@link ValueExtractor} 
+     * that is capable of extracting a value out of the various fields on the page. 
+     */
+    public final ObservableMap<String, Object> getSettings() {
+        return settings;
+    }
+    
+    
+    
+    /**************************************************************************
+     * 
+     * Properties
+     * 
+     **************************************************************************/
+    
+    // --- title
+    
+    /**
+     * Return the titleProperty of the wizard.
+     */
+    public final StringProperty titleProperty() {
+        return titleProperty;
+    }  
+    
+    /**
+     * Return the title of the wizard.
+     */
+    public final String getTitle() {
+    	return titleProperty.get();
+    }
+    
+    /**
+     * Change the Title of the wizard.
+     * @param title
+     */
+    public final void setTitle( String title ) {
+    	titleProperty.set(title);
+    }
+    
+    // --- flow
+    /**
+     * The {@link Flow} property represents the flow of pages in the wizard. 
+     */
+    private ObjectProperty<Flow> flow = new SimpleObjectProperty<Flow>(new LinearFlow()) {
+        @Override protected void invalidated() {
+            updatePage(dialog,false);
+        }
+        
+        @Override public void set(Flow flow) {
+        	super.set(flow);
+        	pageHistory.clear();
+        	if ( flow != null ) {
+        		currentPage = flow.advance(currentPage.orElse(null));
+        		updatePage(dialog,true);
+        	}
+        };
+    };
+    
+    public final ObjectProperty<Flow> flowProperty() {
+        return flow;
+    }
+    
+    /**
+     * Returns the currently set {@link Flow}, which represents the flow of 
+     * pages in the wizard. 
+     */
+    public final Flow getFlow() {
+        return flow.get();
+    }
+    
+    /**
+     * Sets the {@link Flow}, which represents the flow of pages in the wizard. 
+     */
+    public final void setFlow(Flow flow) {
+        this.flow.set(flow);
+    }
+    
+    
+    // --- Properties
+    private static final Object USER_DATA_KEY = new Object();
+    
+    // A map containing a set of properties for this Wizard
+    private ObservableMap<Object, Object> properties;
+
+    /**
+      * Returns an observable map of properties on this Wizard for use primarily
+      * by application developers - not to be confused with the 
+      * {@link #getSettings()} map that represents the values entered by the user
+      * into the wizard.
+      *
+      * @return an observable map of properties on this Wizard for use primarily
+      * by application developers
+     */
+     public final ObservableMap<Object, Object> getProperties() {
+        if (properties == null) {
+            properties = FXCollections.observableMap(new HashMap<>());
+        }
+        return properties;
+    }
+    
+    /**
+     * Tests if this Wizard has properties.
+     * @return true if this Wizard has properties.
+     */
+     public boolean hasProperties() {
+        return properties != null && !properties.isEmpty();
+    }
+
+     
+    // --- UserData
+    /**
+     * Convenience method for setting a single Object property that can be
+     * retrieved at a later date. This is functionally equivalent to calling
+     * the getProperties().put(Object key, Object value) method. This can later
+     * be retrieved by calling {@link #getUserData()}.
+     *
+     * @param value The value to be stored - this can later be retrieved by calling
+     *          {@link #getUserData()}.
+     */
+    public void setUserData(Object value) {
+        getProperties().put(USER_DATA_KEY, value);
+    }
+
+    /**
+     * Returns a previously set Object property, or null if no such property
+     * has been set using the {@link #setUserData(Object)} method.
+     *
+     * @return The Object that was previously set, or null if no property
+     *          has been set or if null was set.
+     */
+    public Object getUserData() {
+        return getProperties().get(USER_DATA_KEY);
+    }
+    
+    /**
+     * Sets the value of the property {@code invalid}.
+     * 
+     * @param invalid The new validation state
+     *  {@link #invalidProperty() }
+     */
+    public final void setInvalid(boolean invalid) {
+        invalidProperty.set(invalid);
+    }
+    
+    /**
+     * Gets the value of the property {@code invalid}.
+     * 
+     * @return The validation state
+     * @see #invalidProperty() 
+     */
+    public final boolean isInvalid() {
+        return invalidProperty.get();
+    }
+    
+    /**
+     * Property for overriding the individual validation state of this {@link Wizard}.
+     * Setting {@code invalid} to true will disable the next/finish Button and the user
+     * will not be able to advance to the next page of the {@link Wizard}. Setting
+     * {@code invalid} to false will enable the next/finish Button. <br>
+     * <br>
+     * For example you can use the {@link ValidationSupport#invalidProperty()} of a
+     * page and bind it to the {@code invalid} property: <br>
+     * {@code
+     * wizard.invalidProperty().bind(page.validationSupport.invalidProperty());
+     * }
+     * 
+     * @return The validation state property
+     */
+    public final BooleanProperty invalidProperty() {
+        return invalidProperty;
+    }
+    
+    /**
+     * Sets the value of the property {@code readSettings}.
+     * 
+     * @param readSettings The new read-settings state
+     * @see #readSettingsProperty()
+     */
+    public final void setReadSettings(boolean readSettings) {
+        readSettingsProperty.set(readSettings);
+    }
+    
+    /**
+     * Gets the value of the property {@code readSettings}.
+     * 
+     * @return The read-settings state
+     * @see #readSettingsProperty()
+     */
+    public final boolean isReadSettings() {
+        return readSettingsProperty.get();
+    }
+    
+    /**
+    * Property for overriding the individual read-settings state of this {@link Wizard}.
+    * Setting {@code readSettings} to true will enable the value extraction for this
+    * {@link Wizard}. Setting {@code readSettings} to false will disable the value
+    * extraction for this {@link Wizard}.
+    *
+    * @return The readSettings state property
+    */
+    public final BooleanProperty readSettingsProperty() {
+        return readSettingsProperty;
+    }
+    
+    
+    
+    /**************************************************************************
+     * 
+     * Private implementation
+     * 
+     **************************************************************************/
+    
+    private void updatePage(Dialog<ButtonType> dialog, boolean advancing) {
+        Flow flow = getFlow();
+        if (flow == null) {
+            return;
+        }
+        
+        Optional<WizardPane> prevPage = Optional.ofNullable( pageHistory.isEmpty()? null: pageHistory.peek()); 
+        prevPage.ifPresent( page -> {
+	        // if we are going forward in the wizard, we read in the settings 
+	        // from the page and store them in the settings map.
+	        // If we are going backwards, we do nothing
+                // This is only performed if readSettings is true.
+	        if (advancing && isReadSettings()) {
+	        	readSettings(page);
+	        }
+	        
+	        // give the previous wizard page a chance to update the pages list
+	        // based on the settings it has received
+	        page.onExitingPage(this);
+        });
+        
+        currentPage.ifPresent(currentPage -> {
+            // put in default actions
+            List<ButtonType> buttons = currentPage.getButtonTypes();
+            if (! buttons.contains(BUTTON_PREVIOUS)) {
+                buttons.add(BUTTON_PREVIOUS);
+                Button button = (Button)currentPage.lookupButton(BUTTON_PREVIOUS);
+                button.addEventFilter(ActionEvent.ACTION, BUTTON_PREVIOUS_ACTION_HANDLER);
+            }
+            if (! buttons.contains(BUTTON_NEXT)) {
+                buttons.add(BUTTON_NEXT);
+                Button button = (Button)currentPage.lookupButton(BUTTON_NEXT);
+                button.addEventFilter(ActionEvent.ACTION, BUTTON_NEXT_ACTION_HANDLER);
+            }
+            if (! buttons.contains(ButtonType.FINISH)) buttons.add(ButtonType.FINISH);
+            if (! buttons.contains(ButtonType.CANCEL)) buttons.add(ButtonType.CANCEL);
+                
+            // then give user a chance to modify the default actions
+            currentPage.onEnteringPage(this);
+            
+            // Remove from DecorationPane which has been created by e.g. validation
+            if (currentPage.getParent() != null && currentPage.getParent() instanceof Pane) {
+                Pane parentOfCurrentPage = (Pane) currentPage.getParent();
+                parentOfCurrentPage.getChildren().remove(currentPage);
+            }
+            
+            // Get current position and size
+            double previousX = dialog.getX();
+            double previousY = dialog.getY();
+            double previousWidth = dialog.getWidth();
+            double previousHeight = dialog.getHeight();
+            // and then switch to the new pane
+            dialog.setDialogPane(currentPage);
+            // Resize Wizard to new page
+            Window wizard = currentPage.getScene().getWindow();
+            wizard.sizeToScene();
+            // Center resized Wizard to previous position
+            
+            
+            if (!Double.isNaN(previousX) && !Double.isNaN(previousY)) {
+                double newWidth = dialog.getWidth();
+                double newHeight = dialog.getHeight();
+                int newX = (int) (previousX + (previousWidth / 2.0) - (newWidth / 2.0));
+                int newY = (int) (previousY + (previousHeight / 2.0) - (newHeight / 2.0));
+
+                ObservableList<Screen> screens = Screen.getScreensForRectangle(previousX, previousY, 1, 1);
+                Screen screen = screens.isEmpty() ? Screen.getPrimary() : screens.get(0);
+                Rectangle2D scrBounds = screen.getBounds();
+                int minX = (int)Math.round(scrBounds.getMinX());
+                int maxX = (int)Math.round(scrBounds.getMaxX());
+                int minY = (int)Math.round(scrBounds.getMinY());
+                int maxY = (int)Math.round(scrBounds.getMaxY());
+                if(newX + newWidth > maxX) {
+                    newX = maxX - (int)Math.round(newWidth);
+                }
+                if(newY + newHeight > maxY) {
+                    newY = maxY - (int)Math.round(newHeight);
+                }                
+                if(newX < minX) {
+                    newX = minX;
+                }
+                if(newY < minY) {
+                    newY = minY;
+                }
+
+                dialog.setX(newX);
+                dialog.setY(newY);
+            }
+        });
+        
+        validateActionState();
+    }
+    
+    private void validateActionState() {
+        final List<ButtonType> currentPaneButtons = dialog.getDialogPane().getButtonTypes();
+        
+        if (getFlow().canAdvance(currentPage.orElse(null))) {
+            currentPaneButtons.remove(ButtonType.FINISH);
+        } else {
+            currentPaneButtons.remove(BUTTON_NEXT);
+        }
+
+        validateButton( BUTTON_PREVIOUS, () -> pageHistory.isEmpty());
+        validateButton( BUTTON_NEXT,     () -> invalidProperty.get());
+        validateButton( ButtonType.FINISH,     () -> invalidProperty.get());
+
+    }
+    
+    // Functional design allows to delay condition evaluation until it is actually needed 
+    private void validateButton( ButtonType buttonType, BooleanSupplier condition) {
+    	Button btn = (Button)dialog.getDialogPane().lookupButton(buttonType);
+        if ( btn != null ) {
+            Node focusOwner = (btn.getScene() != null) ? btn.getScene().getFocusOwner() : null;
+            btn.setDisable(condition.getAsBoolean());
+            if(focusOwner != null) {
+                focusOwner.requestFocus();
+            }
+        }
+    }
+    
+    private int settingCounter;
+    private void readSettings(WizardPane page) {
+        // for now we cannot know the structure of the page, so we just drill down
+        // through the entire scenegraph (from page.content down) until we get
+        // to the leaf nodes. We stop only if we find a node that is a
+        // ValueContainer (either by implementing the interface), or being 
+        // listed in the internal valueContainers map.
+        
+        settingCounter = 0;
+        checkNode(page.getContent());
+    }
+    
+    private boolean checkNode(Node n) {
+        boolean success = readSetting(n);
+        
+        if (success) {
+            // we've added the setting to the settings map and we should stop drilling deeper
+            return true;
+        } else {
+            /**
+             * go into children of this node (if possible) and see if we can get
+             * a value from them (recursively) We use reflection to fix
+             * https://bitbucket.org/controlsfx/controlsfx/issue/412 .
+             */
+            List<Node> children = ImplUtils.getChildren(n, true);
+            
+            // we're doing a depth-first search, where we stop drilling down
+            // once we hit a successful read
+            boolean childSuccess = false;
+            for (Node child : children) {
+                childSuccess |= checkNode(child);
+            }
+            return childSuccess;
+        }
+    }
+    
+    private boolean readSetting(Node n) {
+        if (n == null) {
+            return false;
+        }
+        
+        Object setting = ValueExtractor.getValue(n);
+        
+        if (setting != null) {
+            // save it into the settings map.
+            // if the node has an id set, we will use that as the setting name
+            String settingName = n.getId();
+            
+            // but if the id is not set, we will use a generic naming scheme
+            if (settingName == null || settingName.isEmpty()) {
+                settingName = "page_" /*+ previousPageIndex*/ + ".setting_" + settingCounter;  //$NON-NLS-1$ //$NON-NLS-2$
+            }
+            
+            getSettings().put(settingName, setting);
+            
+            settingCounter++;
+        }
+        
+        return setting != null;
+    }
+    
+    
+    
+    /**************************************************************************
+     * 
+     * Support classes
+     * 
+     **************************************************************************/
+    
+    
+    /**
+     * Represents the page flow of the wizard. It defines only methods required 
+     * to move forward in the wizard logic, as backward movement is automatically 
+     * handled by wizard itself, using internal page history.   
+     */
+    public interface Flow {
+    	
+    	/** 
+    	 * Advances the wizard to the next page if possible.
+    	 * 
+    	 * @param currentPage The current wizard page
+    	 * @return {@link Optional} value containing the next wizard page. 
+    	 */
+    	Optional<WizardPane> advance(WizardPane currentPage);
+    	
+    	/**
+    	 * Check if advancing to the next page is possible
+    	 * 
+    	 * @param currentPage The current wizard page
+    	 * @return true if it is possible to advance to the next page, false otherwise.
+    	 */
+    	boolean canAdvance(WizardPane currentPage);
+    }
+    
+    
+    /**
+     * LinearFlow is an implementation of the {@link Wizard.Flow} interface,
+     * designed to support the most common type of wizard flow - namely, a linear 
+     * wizard page flow (i.e. through all pages in the order that they are specified).
+     * Therefore, this {@link Flow} implementation simply traverses a collections of
+     * {@link WizardPane WizardPanes}.
+     * 
+     * <p>For example of how to use this API, please refer to the {@link Wizard} 
+     * documentation</p>
+     * 
+     * @see Wizard
+     * @see WizardPane 
+     */
+    public static class LinearFlow implements Wizard.Flow {
+
+        private final List<WizardPane> pages;
+
+        /**
+         * Creates a new LinearFlow instance that will allow for stepping through
+         * the given collection of {@link WizardPane} instances.
+         */
+        public LinearFlow( Collection<WizardPane> pages ) {
+            this.pages = new ArrayList<>(pages);
+        }
+
+        /**
+         * Creates a new LinearFlow instance that will allow for stepping through
+         * the given varargs array of {@link WizardPane} instances.
+         */
+        public LinearFlow( WizardPane... pages ) {
+            this( Arrays.asList(pages));
+        }
+
+        /** {@inheritDoc} */
+        @Override public Optional<WizardPane> advance(WizardPane currentPage) {
+            int pageIndex = pages.indexOf(currentPage);
+            return Optional.ofNullable( pages.get(++pageIndex) );
+        }
+
+        /** {@inheritDoc} */
+        @Override public boolean canAdvance(WizardPane currentPage) {
+            int pageIndex = pages.indexOf(currentPage);
+            return pages.size()-1 > pageIndex; 
+        }
+    }
+    
+    
+    
+    /**************************************************************************
+     * 
+     * Methods for the sake of unit tests
+     * 
+     **************************************************************************/
+    
+    /**
+     * @return The {@link Dialog} representing this {@link Wizard}. <br>
+     *         This is actually for {@link Dialog} reading-purposes, e.g.
+     *         unit testing the {@link DialogPane} content.
+     */
+    Dialog<ButtonType> getDialog() {
+        return dialog;
+    }
+    
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/dialog/WizardPane.java b/controlsfx/src/main/java/org/controlsfx/dialog/WizardPane.java
new file mode 100644
index 0000000..4379b33
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/dialog/WizardPane.java
@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2014, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.dialog;
+
+import javafx.scene.control.DialogPane;
+
+/**
+ * WizardPane is the base class for all wizard pages. The API is essentially
+ * the {@link DialogPane}, with the addition of convenience methods related
+ * to {@link #onEnteringPage(Wizard) entering} and 
+ * {@link #onExitingPage(Wizard) exiting} the page.
+ */
+public class WizardPane extends DialogPane {
+
+    /**
+     * Creates an instance of wizard pane.
+     */
+    public WizardPane() {
+        getStylesheets().add(Wizard.class.getResource("wizard.css").toExternalForm());
+        getStyleClass().add("wizard-pane");
+    }
+
+    /**
+     * Called on entering a page. This is a good place to read values from wizard settings 
+     * and assign them to controls on the page
+     * @param wizard which page will be used on
+     */
+    public void onEnteringPage(Wizard wizard) {
+        // no-op
+    }
+    
+    /**
+     * Called on existing the page. 
+     * This is a good place to read values from page controls and store them in wizard settings
+     * @param wizard which page was used on
+     */
+    public void onExitingPage(Wizard wizard) {
+        // no-op
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/dialog/package-info.java b/controlsfx/src/main/java/org/controlsfx/dialog/package-info.java
new file mode 100644
index 0000000..0d1019c
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/dialog/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * A package containing a powerful (yet easy to use) dialogs API for showing
+ * modal dialogs in JavaFX-based applications. 
+ */
+package org.controlsfx.dialog;
\ No newline at end of file
diff --git a/controlsfx/src/main/java/org/controlsfx/glyphfont/FontAwesome.java b/controlsfx/src/main/java/org/controlsfx/glyphfont/FontAwesome.java
new file mode 100644
index 0000000..1404e26
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/glyphfont/FontAwesome.java
@@ -0,0 +1,723 @@
+/**
+ * Copyright (c) 2013,2014 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.glyphfont;
+
+import java.io.InputStream;
+import java.util.Arrays;
+
+
+/**
+ * Defines a {@link GlyphFont} for the FontAwesome font set (see
+ * <a href="http://fortawesome.github.io/Font-Awesome/">the FontAwesome website</a>
+ * for more details). Note that at present the FontAwesome font is not distributed
+ * with ControlsFX, and is, by default, instead loaded from a CDN at runtime.
+ *
+ * <p>To use FontAwesome (or indeed any glyph font) in your JavaFX application,
+ * you firstly have to get access to the FontAwesome glyph font. You do this by
+ * doing the following:
+ *
+ * <pre>GlyphFont fontAwesome = GlyphFontRegistry.font("FontAwesome");</pre>
+ *
+ * <p>This code works because all glyph fonts are found dynamically at runtime
+ * by the {@link GlyphFontRegistry} class, so you can simply request the font
+ * set you want from there.
+ *
+ * <p>Once the font set has been loaded, you can simply start creating
+ * {@link Glyph} nodes and place them in your user interface. For example:
+ *
+ * <pre>new Button("", fontAwesome.create(\uf013).fontColor(Color.RED));</pre>
+ *
+ * <p>Of course, this requires you to know that <code>\uf013</code> maps to
+ * a 'gear' icon, which is not always intuitive (especially when you re-read the
+ * code in the future). A simpler approach is to do the following:
+ *
+ * <pre>new Button("", fontAwesome.create(FontAwesome.Glyph.GEAR));</pre>
+ * or
+ * <pre>new Button("", fontAwesome.create("GEAR"));</pre>
+ *
+ * It is possible to achieve the same result without creating a reference to icon font by simply using
+ * {@link org.controlsfx.glyphfont.Glyph} constructor
+ *
+ * <pre>new Button("", new Glyph("FontAwesome","GEAR");</pre>
+ *
+ * You can use the above Glyph class also in FXML and set the
+ * fontFamily and icon property there.
+ *
+ * @see GlyphFont
+ * @see GlyphFontRegistry
+ * @see Glyph
+ */
+public class FontAwesome extends GlyphFont {
+
+    private static String fontName = "FontAwesome"; //$NON-NLS-1$
+
+
+    /**
+     * The individual glyphs offered by the FontAwesome font.
+     */
+    public static enum Glyph implements INamedCharacter {
+
+        ADJUST('\uf042'),
+        ADN('\uf170'),
+        ALIGN_CENTER('\uf037'),
+        ALIGN_JUSTIFY('\uf039'),
+        ALIGN_LEFT('\uf036'),
+        ALIGN_RIGHT('\uf038'),
+        AMBULANCE('\uf0F9'),
+        ANCHOR('\uf13D'),
+        ANDROID('\uf17B'),
+        ANGELLIST('\uf209'),
+        ANGLE_DOUBLE_DOWN('\uf103'),
+        ANGLE_DOUBLE_LEFT('\uf100'),
+        ANGLE_DOUBLE_RIGHT('\uf101'),
+        ANGLE_DOUBLE_UP('\uf102'),
+        ANGLE_DOWN('\uf107'),
+        ANGLE_LEFT('\uf104'),
+        ANGLE_RIGHT('\uf105'),
+        ANGLE_UP('\uf106'),
+        APPLE('\uf179'),
+        ARCHIVE('\uf187'),
+        AREA_CHART('\uf1FE'),
+        ARROW_CIRCLE_DOWN('\uf0AB'),
+        ARROW_CIRCLE_LEFT('\uf0A8'),
+        ARROW_CIRCLE_O_DOWN('\uf01A'),
+        ARROW_CIRCLE_O_LEFT('\uf190'),
+        ARROW_CIRCLE_O_RIGHT('\uf18E'),
+        ARROW_CIRCLE_O_UP('\uf01B'),
+        ARROW_CIRCLE_RIGHT('\uf0A9'),
+        ARROW_CIRCLE_UP('\uf0AA'),
+        ARROW_DOWN('\uf063'),
+        ARROW_LEFT('\uf060'),
+        ARROW_RIGHT('\uf061'),
+        ARROW_UP('\uf062'),
+        ARROWS('\uf047'),
+        ARROWS_ALT('\uf0B2'),
+        ARROWS_H('\uf07E'),
+        ARROWS_V('\uf07D'),
+        ASTERISK('\uf069'),
+        AT('\uf1FA'),
+        AUTOMOBILE('\uf1B9'),
+        BACKWARD('\uf04A'),
+        BAN('\uf05E'),
+        BANK('\uf19C'),
+        BAR_CHART('\uf080'),
+        BAR_CHART_O('\uf080'),
+        BARCODE('\uf02A'),
+        BARS('\uf0C9'),
+        BED('\uf236'),
+        BEER('\uf0FC'),
+        BEHANCE('\uf1B4'),
+        BEHANCE_SQUARE('\uf1B5'),
+        BELL('\uf0F3'),
+        BELL_O('\uf0A2'),
+        BELL_SLASH('\uf1F6'),
+        BELL_SLASH_O('\uf1F7'),
+        BICYCLE('\uf206'),
+        BINOCULARS('\uf1E5'),
+        BIRTHDAY_CAKE('\uf1FD'),
+        BITBUCKET('\uf171'),
+        BITBUCKET_SQUARE('\uf172'),
+        BITCOIN('\uf15A'),
+        BOLD('\uf032'),
+        BOLT('\uf0E7'),
+        BOMB('\uf1E2'),
+        BOOK('\uf02D'),
+        BOOKMARK('\uf02E'),
+        BOOKMARK_ALT('\uf097'),
+        BRIEFCASE('\uf0B1'),
+        BTC('\uf15A'),
+        BUG('\uf188'),
+        BUILDING('\uf1AD'),
+        BUILDING_ALT('\uf0F7'),
+        BULLHORN('\uf0A1'),
+        BULLSEYE('\uf140'),
+        BUS('\uf207'),
+        BUYSELLADS('\uf20D'),
+        CAB('\uf1BA'),
+        CALCULATOR('\uf1EC'),
+        CALENDAR('\uf073'),
+        CALENDAR_ALT('\uf133'),
+        CAMERA('\uf030'),
+        CAMERA_RETRO('\uf083'),
+        CAR('\uf1B9'),
+        CARET_DOWN('\uf0D7'),
+        CARET_LEFT('\uf0D9'),
+        CARET_RIGHT('\uf0DA'),
+        CARET_SQUARE_ALT_DOWN('\uf150'),
+        CARET_SQUARE_ALT_LEFT('\uf191'),
+        CARET_SQUARE_ALT_RIGHT('\uf152'),
+        CARET_SQUARE_ALT_UP('\uf151'),
+        CARET_UP('\uf0D8'),
+        CART_ARROW_DOWN('\uf218'),
+        CART_PLUS('\uf217'),
+        CC('\uf20A'),
+        CC_AMEX('\uf1F3'),
+        CC_DISCOVER('\uf1F2'),
+        CC_MASTERCARD('\uf1F1'),
+        CC_PAYPAL('\uf1F4'),
+        CC_STRIPE('\uf1F5'),
+        CC_VISA('\uf1F0'),
+        CERTIFICATE('\uf0A3'),
+        CHAIN('\uf0C1'),
+        CHAIN_BROKEN('\uf127'),
+        CHECK('\uf00C'),
+        CHECK_CIRCLE('\uf058'),
+        CHECK_CIRCLE_ALT('\uf05D'),
+        CHECK_SQUARE('\uf14A'),
+        CHECK_SQUARE_ALT('\uf046'),
+        CHEVRON_CIRCLE_DOWN('\uf13A'),
+        CHEVRON_CIRCLE_LEFT('\uf137'),
+        CHEVRON_CIRCLE_RIGHT('\uf138'),
+        CHEVRON_CIRCLE_UP('\uf139'),
+        CHEVRON_DOWN('\uf078'),
+        CHEVRON_LEFT('\uf053'),
+        CHEVRON_RIGHT('\uf054'),
+        CHEVRON_UP('\uf077'),
+        CHILD('\uf1AE'),
+        CIRCLE('\uf111'),
+        CIRCLE_ALT('\uf10C'),
+        CIRCLE_ALT_NOTCH('\uf1CE'),
+        CIRCLE_THIN('\uf1DB'),
+        CLIPBOARD('\uf0EA'),
+        CLOCK_ALT('\uf017'),
+        CLOSE('\uf00D'),
+        CLOUD('\uf0C2'),
+        CLOUD_DOWNLOAD('\uf0ED'),
+        CLOUD_UPLOAD('\uf0EE'),
+        CNY('\uf157'),
+        CODE('\uf121'),
+        CODE_FORK('\uf126'),
+        CODEPEN('\uf1CB'),
+        COFFEE('\uf0F4'),
+        COG('\uf013'),
+        COGS('\uf085'),
+        COLUMNS('\uf0DB'),
+        COMMENT('\uf075'),
+        COMMENT_ALT('\uf0E5'),
+        COMMENTS('\uf086'),
+        COMMENTS_ALT('\uf0E6'),
+        COMPASS('\uf14E'),
+        COMPRESS('\uf066'),
+        CONNECTDEVELOP('\uf20E'),
+        COPY('\uf0C5'),
+        COPYRIGHT('\uf1F9'),
+        CREDIT_CARD('\uf09D'),
+        CROP('\uf125'),
+        CROSSHAIRS('\uf05B'),
+        CSS3('\uf13C'),
+        CUBE('\uf1B2'),
+        CUBES('\uf1B3'),
+        CUT('\uf0C4'),
+        CUTLERY('\uf0F5'),
+        DASHBOARD('\uf0E4'),
+        DASHCUBE('\uf210'),
+        DATABASE('\uf1C0'),
+        DEDENT('\uf03B'),
+        DELICIOUS('\uf1A5'),
+        DESKTOP('\uf108'),
+        DEVIANTART('\uf1BD'),
+        DIAMOND('\uf219'),
+        DIGG('\uf1A6'),
+        DOLLAR('\uf155'),
+        DOT_CIRCLE_ALT('\uf192'),
+        DOWNLOAD('\uf019'),
+        DRIBBBLE('\uf17D'),
+        DROPBOX('\uf16B'),
+        DRUPAL('\uf1A9'),
+        EDIT('\uf044'),
+        EJECT('\uf052'),
+        ELLIPSIS_H('\uf141'),
+        ELLIPSIS_V('\uf142'),
+        EMPIRE('\uf1D1'),
+        ENVELOPE('\uf0E0'),
+        ENVELOPE_ALT('\uf003'),
+        ENVELOPE_SQUARE('\uf199'),
+        ERASER('\uf12D'),
+        EUR('\uf153'),
+        EURO('\uf153'),
+        EXCHANGE('\uf0EC'),
+        EXCLAMATION('\uf12A'),
+        EXCLAMATION_CIRCLE('\uf06A'),
+        EXCLAMATION_TRIANGLE('\uf071'),
+        EXPAND('\uf065'),
+        EXTERNAL_LINK('\uf08E'),
+        EXTERNAL_LINK_SQUARE('\uf14C'),
+        EYE('\uf06E'),
+        EYE_SLASH('\uf070'),
+        EYEDROPPER('\uf1FB'),
+        FACEBOOK('\uf09A'),
+        FACEBOOK_F('\uf09A'),
+        FACEBOOK_ALTFFICIAL('\uf230'),
+        FACEBOOK_SQUARE('\uf082'),
+        FAST_BACKWARD('\uf049'),
+        FAST_FORWARD('\uf050'),
+        FAX('\uf1AC'),
+        FEMALE('\uf182'),
+        FIGHTER_JET('\uf0FB'),
+        FILE('\uf15B'),
+        FILE_ARCHIVE_ALT('\uf1C6'),
+        FILE_AUDIO_ALT('\uf1C7'),
+        FILE_CODE_ALT('\uf1C9'),
+        FILE_EXCEL_ALT('\uf1C3'),
+        FILE_IMAGE_ALT('\uf1C5'),
+        FILE_MOVIE_ALT('\uf1C8'),
+        FILE_ALT('\uf016'),
+        FILE_PDF_ALT('\uf1C1'),
+        FILE_PHOTO_ALT('\uf1C5'),
+        FILE_PICTURE_ALT('\uf1C5'),
+        FILE_POWERPOINT_ALT('\uf1C4'),
+        FILE_SOUND_ALT('\uf1C7'),
+        FILE_TEXT('\uf15C'),
+        FILE_TEXT_ALT('\uf0F6'),
+        FILE_VIDEO_ALT('\uf1C8'),
+        FILE_WORD_ALT('\uf1C2'),
+        FILE_ZIP_ALT('\uf1C6'),
+        FILES_ALT('\uf0C5'),
+        FILM('\uf008'),
+        FILTER('\uf0B0'),
+        FIRE('\uf06D'),
+        FIRE_EXTINGUISHER('\uf134'),
+        FLAG('\uf024'),
+        FLAG_CHECKERED('\uf11E'),
+        FLAG_ALT('\uf11D'),
+        FLASH('\uf0E7'),
+        FLASK('\uf0C3'),
+        FLICKR('\uf16E'),
+        FLOPPY_ALT('\uf0C7'),
+        FOLDER('\uf07B'),
+        FOLDER_ALT('\uf114'),
+        FOLDER_OPEN('\uf07C'),
+        FOLDER_OPEN_ALT('\uf115'),
+        FONT('\uf031'),
+        FORUMBEE('\uf211'),
+        FORWARD('\uf04E'),
+        FOURSQUARE('\uf180'),
+        FROWN_ALT('\uf119'),
+        FUTBOL_ALT('\uf1E3'),
+        GAMEPAD('\uf11B'),
+        GAVEL('\uf0E3'),
+        GBP('\uf154'),
+        GE('\uf1D1'),
+        GEAR('\uf013'),
+        GEARS('\uf085'),
+        GENDERLESS('\uf1DB'),
+        GIFT('\uf06B'),
+        GIT('\uf1D3'),
+        GIT_SQUARE('\uf1D2'),
+        GITHUB('\uf09B'),
+        GITHUB_ALT('\uf113'),
+        GITHUB_SQUARE('\uf092'),
+        GITTIP('\uf184'),
+        GLASS('\uf000'),
+        GLOBE('\uf0AC'),
+        GOOGLE('\uf1A0'),
+        GOOGLE_PLUS('\uf0D5'),
+        GOOGLE_PLUS_SQUARE('\uf0D4'),
+        GOOGLE_WALLET('\uf1EE'),
+        GRADUATION_CAP('\uf19D'),
+        GRATIPAY('\uf184'),
+        GROUP('\uf0C0'),
+        H_SQUARE('\uf0FD'),
+        HACKER_NEWS('\uf1D4'),
+        HAND_ALT_DOWN('\uf0A7'),
+        HAND_ALT_LEFT('\uf0A5'),
+        HAND_ALT_RIGHT('\uf0A4'),
+        HAND_ALT_UP('\uf0A6'),
+        HDD_ALT('\uf0A0'),
+        HEADER('\uf1DC'),
+        HEADPHONES('\uf025'),
+        HEART('\uf004'),
+        HEART_ALT('\uf08A'),
+        HEARTBEAT('\uf21E'),
+        HISTORY('\uf1DA'),
+        HOME('\uf015'),
+        HOSPITAL_ALT('\uf0F8'),
+        HOTEL('\uf236'),
+        HTML5('\uf13B'),
+        ILS('\uf20B'),
+        IMAGE('\uf03E'),
+        INBOX('\uf01C'),
+        INDENT('\uf03C'),
+        INFO('\uf129'),
+        INFO_CIRCLE('\uf05A'),
+        INR('\uf156'),
+        INSTAGRAM('\uf16D'),
+        INSTITUTION('\uf19C'),
+        IOXHOST('\uf208'),
+        ITALIC('\uf033'),
+        JOOMLA('\uf1AA'),
+        JPY('\uf157'),
+        JSFIDDLE('\uf1CC'),
+        KEY('\uf084'),
+        KEYBOARD_ALT('\uf11C'),
+        KRW('\uf159'),
+        LANGUAGE('\uf1AB'),
+        LAPTOP('\uf109'),
+        LASTFM('\uf202'),
+        LASTFM_SQUARE('\uf203'),
+        LEAF('\uf06C'),
+        LEANPUB('\uf212'),
+        LEGAL('\uf0E3'),
+        LEMON_ALT('\uf094'),
+        LEVEL_DOWN('\uf149'),
+        LEVEL_UP('\uf148'),
+        LIFE_BOUY('\uf1CD'),
+        LIFE_BUOY('\uf1CD'),
+        LIFE_RING('\uf1CD'),
+        LIFE_SAVER('\uf1CD'),
+        LIGHTBULB_ALT('\uf0EB'),
+        LINE_CHART('\uf201'),
+        LINK('\uf0C1'),
+        LINKEDIN('\uf0E1'),
+        LINKEDIN_SQUARE('\uf08C'),
+        LINUX('\uf17C'),
+        LIST('\uf03A'),
+        LIST_ALT('\uf022'),
+        LIST_OL('\uf0CB'),
+        LIST_UL('\uf0CA'),
+        LOCATION_ARROW('\uf124'),
+        LOCK('\uf023'),
+        LONG_ARROW_DOWN('\uf175'),
+        LONG_ARROW_LEFT('\uf177'),
+        LONG_ARROW_RIGHT('\uf178'),
+        LONG_ARROW_UP('\uf176'),
+        MAGIC('\uf0D0'),
+        MAGNET('\uf076'),
+        MAIL_FORWARD('\uf064'),
+        MAIL_REPLY('\uf112'),
+        MAIL_REPLY_ALL('\uf122'),
+        MALE('\uf183'),
+        MAP_MARKER('\uf041'),
+        MARS('\uf222'),
+        MARS_DOUBLE('\uf227'),
+        MARS_STROKE('\uf229'),
+        MARS_STROKE_H('\uf22B'),
+        MARS_STROKE_V('\uf22A'),
+        MAXCDN('\uf136'),
+        MEANPATH('\uf20C'),
+        MEDIUM('\uf23A'),
+        MEDKIT('\uf0FA'),
+        MEH_ALT('\uf11A'),
+        MERCURY('\uf223'),
+        MICROPHONE('\uf130'),
+        MICROPHONE_SLASH('\uf131'),
+        MINUS('\uf068'),
+        MINUS_CIRCLE('\uf056'),
+        MINUS_SQUARE('\uf146'),
+        MINUS_SQUARE_ALT('\uf147'),
+        MOBILE('\uf10B'),
+        MOBILE_PHONE('\uf10B'),
+        MONEY('\uf0D6'),
+        MOON_ALT('\uf186'),
+        MORTAR_BOARD('\uf19D'),
+        MOTORCYCLE('\uf21C'),
+        MUSIC('\uf001'),
+        NAVICON('\uf0C9'),
+        NEUTER('\uf22C'),
+        NEWSPAPER_ALT('\uf1EA'),
+        OPENID('\uf19B'),
+        OUTDENT('\uf03B'),
+        PAGELINES('\uf18C'),
+        PAINT_BRUSH('\uf1FC'),
+        PAPER_PLANE('\uf1D8'),
+        PAPER_PLANE_ALT('\uf1D9'),
+        PAPERCLIP('\uf0C6'),
+        PARAGRAPH('\uf1DD'),
+        PASTE('\uf0EA'),
+        PAUSE('\uf04C'),
+        PAW('\uf1B0'),
+        PAYPAL('\uf1ED'),
+        PENCIL('\uf040'),
+        PENCIL_SQUARE('\uf14B'),
+        PENCIL_SQUARE_ALT('\uf044'),
+        PHONE('\uf095'),
+        PHONE_SQUARE('\uf098'),
+        PHOTO('\uf03E'),
+        PICTURE_ALT('\uf03E'),
+        PIE_CHART('\uf200'),
+        PIED_PIPER('\uf1A7'),
+        PIED_PIPER_ALT('\uf1A8'),
+        PINTEREST('\uf0D2'),
+        PINTEREST_P('\uf231'),
+        PINTEREST_SQUARE('\uf0D3'),
+        PLANE('\uf072'),
+        PLAY('\uf04B'),
+        PLAY_CIRCLE('\uf144'),
+        PLAY_CIRCLE_ALT('\uf01D'),
+        PLUG('\uf1E6'),
+        PLUS('\uf067'),
+        PLUS_CIRCLE('\uf055'),
+        PLUS_SQUARE('\uf0FE'),
+        PLUS_SQUARE_ALT('\uf196'),
+        POWER_OFF('\uf011'),
+        PRINT('\uf02F'),
+        PUZZLE_PIECE('\uf12E'),
+        QQ('\uf1D6'),
+        QRCODE('\uf029'),
+        QUESTION('\uf128'),
+        QUESTION_CIRCLE('\uf059'),
+        QUOTE_LEFT('\uf10D'),
+        QUOTE_RIGHT('\uf10E'),
+        RA('\uf1D0'),
+        RANDOM('\uf074'),
+        REBEL('\uf1D0'),
+        RECYCLE('\uf1B8'),
+        REDDIT('\uf1A1'),
+        REDDIT_SQUARE('\uf1A2'),
+        REFRESH('\uf021'),
+        REMOVE('\uf00D'),
+        RENREN('\uf18B'),
+        REORDER('\uf0C9'),
+        REPEAT('\uf01E'),
+        REPLY('\uf112'),
+        REPLY_ALL('\uf122'),
+        RETWEET('\uf079'),
+        RMB('\uf157'),
+        ROAD('\uf018'),
+        ROCKET('\uf135'),
+        ROTATE_LEFT('\uf0E2'),
+        ROTATE_RIGHT('\uf01E'),
+        ROUBLE('\uf158'),
+        RSS('\uf09E'),
+        RSS_SQUARE('\uf143'),
+        RUB('\uf158'),
+        RUBLE('\uf158'),
+        RUPEE('\uf156'),
+        SAVE('\uf0C7'),
+        SCISSORS('\uf0C4'),
+        SEARCH('\uf002'),
+        SEARCH_MINUS('\uf010'),
+        SEARCH_PLUS('\uf00E'),
+        SELLSY('\uf213'),
+        SEND('\uf1D8'),
+        SEND_ALT('\uf1D9'),
+        SERVER('\uf233'),
+        SHARE('\uf064'),
+        SHARE_ALT('\uf1E0'),
+        SHARE_ALT_SQUARE('\uf1E1'),
+        SHARE_SQUARE('\uf14D'),
+        SHARE_SQUARE_ALT('\uf045'),
+        SHEKEL('\uf20B'),
+        SHEQEL('\uf20B'),
+        SHIELD('\uf132'),
+        SHIP('\uf21A'),
+        SHIRTSINBULK('\uf214'),
+        SHOPPING_CART('\uf07A'),
+        SIGN_IN('\uf090'),
+        SIGN_OUT('\uf08B'),
+        SIGNAL('\uf012'),
+        SIMPLYBUILT('\uf215'),
+        SITEMAP('\uf0E8'),
+        SKYATLAS('\uf216'),
+        SKYPE('\uf17E'),
+        SLACK('\uf198'),
+        SLIDERS('\uf1DE'),
+        SLIDESHARE('\uf1E7'),
+        SMILE_ALT('\uf118'),
+        SOCCER_BALL_ALT('\uf1E3'),
+        SORT('\uf0DC'),
+        SORT_ALPHA_ASC('\uf15D'),
+        SORT_ALPHA_DESC('\uf15E'),
+        SORT_AMOUNT_ASC('\uf160'),
+        SORT_AMOUNT_DESC('\uf161'),
+        SORT_ASC('\uf0DE'),
+        SORT_DESC('\uf0DD'),
+        SORT_DOWN('\uf0DD'),
+        SORT_NUMERIC_ASC('\uf162'),
+        SORT_NUMERIC_DESC('\uf163'),
+        SORT_UP('\uf0DE'),
+        SOUNDCLOUD('\uf1BE'),
+        SPACE_SHUTTLE('\uf197'),
+        SPINNER('\uf110'),
+        SPOON('\uf1B1'),
+        SPOTIFY('\uf1BC'),
+        SQUARE('\uf0C8'),
+        SQUARE_ALT('\uf096'),
+        STACK_EXCHANGE('\uf18D'),
+        STACK_OVERFLOW('\uf16C'),
+        STAR('\uf005'),
+        STAR_HALF('\uf089'),
+        STAR_HALF_EMPTY('\uf123'),
+        STAR_HALF_FULL('\uf123'),
+        STAR_HALF_ALT('\uf123'),
+        STAR_ALT('\uf006'),
+        STEAM('\uf1B6'),
+        STEAM_SQUARE('\uf1B7'),
+        STEP_BACKWARD('\uf048'),
+        STEP_FORWARD('\uf051'),
+        STETHOSCOPE('\uf0F1'),
+        STOP('\uf04D'),
+        STREET_VIEW('\uf21D'),
+        STRIKETHROUGH('\uf0CC'),
+        STUMBLEUPON('\uf1A4'),
+        STUMBLEUPON_CIRCLE('\uf1A3'),
+        SUBSCRIPT('\uf12C'),
+        SUBWAY('\uf239'),
+        SUITCASE('\uf0F2'),
+        SUN_ALT('\uf185'),
+        SUPERSCRIPT('\uf12B'),
+        SUPPORT('\uf1CD'),
+        TABLE('\uf0CE'),
+        TABLET('\uf10A'),
+        TACHOMETER('\uf0E4'),
+        TAG('\uf02B'),
+        TAGS('\uf02C'),
+        TASKS('\uf0AE'),
+        TAXI('\uf1BA'),
+        TENCENT_WEIBO('\uf1D5'),
+        TERMINAL('\uf120'),
+        TEXT_HEIGHT('\uf034'),
+        TEXT_WIDTH('\uf035'),
+        TH('\uf00A'),
+        TH_LARGE('\uf009'),
+        TH_LIST('\uf00B'),
+        THUMB_TACK('\uf08D'),
+        THUMBS_DOWN('\uf165'),
+        THUMBS_ALT_DOWN('\uf088'),
+        THUMBS_ALT_UP('\uf087'),
+        THUMBS_UP('\uf164'),
+        TICKET('\uf145'),
+        TIMES('\uf00D'),
+        TIMES_CIRCLE('\uf057'),
+        TIMES_CIRCLE_ALT('\uf05C'),
+        TINT('\uf043'),
+        TOGGLE_DOWN('\uf150'),
+        TOGGLE_LEFT('\uf191'),
+        TOGGLE_OFF('\uf204'),
+        TOGGLE_ON('\uf205'),
+        TOGGLE_RIGHT('\uf152'),
+        TOGGLE_UP('\uf151'),
+        TRAIN('\uf238'),
+        TRANSGENDER('\uf224'),
+        TRANSGENDER_ALT('\uf225'),
+        TRASH('\uf1F8'),
+        TRASH_ALT('\uf014'),
+        TREE('\uf1BB'),
+        TRELLO('\uf181'),
+        TROPHY('\uf091'),
+        TRUCK('\uf0D1'),
+        TRY('\uf195'),
+        TTY('\uf1E4'),
+        TUMBLR('\uf173'),
+        TUMBLR_SQUARE('\uf174'),
+        TURKISH_LIRA('\uf195'),
+        TWITCH('\uf1E8'),
+        TWITTER('\uf099'),
+        TWITTER_SQUARE('\uf081'),
+        UMBRELLA('\uf0E9'),
+        UNDERLINE('\uf0CD'),
+        UNDO('\uf0E2'),
+        UNIVERSITY('\uf19C'),
+        UNLINK('\uf127'),
+        UNLOCK('\uf09C'),
+        UNLOCK_ALT('\uf13E'),
+        UNSORTED('\uf0DC'),
+        UPLOAD('\uf093'),
+        USD('\uf155'),
+        USER('\uf007'),
+        USER_MD('\uf0F0'),
+        USER_PLUS('\uf234'),
+        USER_SECRET('\uf21B'),
+        USER_TIMES('\uf235'),
+        USERS('\uf0C0'),
+        VENUS('\uf221'),
+        VENUS_DOUBLE('\uf226'),
+        VENUS_MARS('\uf228'),
+        VIACOIN('\uf237'),
+        VIDEO_CAMERA('\uf03D'),
+        VIMEO_SQUARE('\uf194'),
+        VINE('\uf1CA'),
+        VK('\uf189'),
+        VOLUME_DOWN('\uf027'),
+        VOLUME_OFF('\uf026'),
+        VOLUME_UP('\uf028'),
+        WARNING('\uf071'),
+        WECHAT('\uf1D7'),
+        WEIBO('\uf18A'),
+        WEIXIN('\uf1D7'),
+        WHATSAPP('\uf232'),
+        WHEELCHAIR('\uf193'),
+        WIFI('\uf1EB'),
+        WINDOWS('\uf17A'),
+        WON('\uf159'),
+        WORDPRESS('\uf19A'),
+        WRENCH('\uf0AD'),
+        XING('\uf168'),
+        XING_SQUARE('\uf169'),
+        YAHOO('\uf19E'),
+        YELP('\uf1E9'),
+        YEN('\uf157'),
+        YOUTUBE('\uf167'),
+        YOUTUBE_PLAY('\uf16A'),
+        YOUTUBE_SQUARE('\uf166');
+
+        private final char ch;
+
+        /**
+         * Creates a named Glyph mapped to the given character
+         * @param ch
+         */
+        Glyph( char ch ) {
+            this.ch = ch;
+        }
+
+        @Override
+        public char getChar() {
+            return ch;
+        }
+    };
+
+    /**
+     * Do not call this constructor directly - instead access the
+     * {@link FontAwesome.Glyph} public static enumeration method to create the glyph nodes), or
+     * use the {@link GlyphFontRegistry} class to get access.
+     *
+     * Note: Do not remove this public constructor since it is used by the service loader!
+     */
+    public FontAwesome() {
+        this("http://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.3.0/fonts/fontawesome-webfont.ttf"); //$NON-NLS-1$
+    }
+
+    /**
+     * Creates a new FontAwesome instance which uses the provided font source.
+     * @param url
+     */
+    public FontAwesome(String url){
+        super(fontName, 14, url, true);
+        registerAll(Arrays.asList(Glyph.values()));
+    }
+
+    /**
+     * Creates a new FontAwesome instance which uses the provided font source.
+     * @param is
+     */
+    public FontAwesome(InputStream is){
+        super(fontName, 14, is, true);
+        registerAll(Arrays.asList(Glyph.values()));
+    }
+
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/glyphfont/Glyph.java b/controlsfx/src/main/java/org/controlsfx/glyphfont/Glyph.java
new file mode 100644
index 0000000..8bb8c72
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/glyphfont/Glyph.java
@@ -0,0 +1,350 @@
+/**
+ * Copyright (c) 2013, 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.glyphfont;
+
+import java.util.Optional;
+
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.ObservableList;
+import javafx.scene.Node;
+import javafx.scene.control.Label;
+import javafx.scene.paint.*;
+import javafx.scene.text.Font;
+
+import org.controlsfx.control.action.Action;
+import org.controlsfx.tools.Duplicatable;
+
+/**
+ * Represents one glyph from the font.
+ * The glyph is actually a label showing one character from the specified font. It can be used as 'graphic' on any UI
+ * control or {@link Action}. It can also directly be used in FXML.
+ *
+ * Examples:
+ *
+ * <pre>{@code
+ * new Button("", new Glyph("FontAwesome", "BEER"))
+ * }</pre>
+ *
+ * <pre>{@code
+ * new Button("", new Glyph("FontAwesome", FontAwesome.Glyph.BEER))
+ * }</pre>
+ *
+ * Thy Glyph-Class also offers a fluent API to customize the look of the Glyph.
+ * For example, you can set the color {@link #color(javafx.scene.paint.Color)} or
+ * also add effects such as {@link #useHoverEffect()}
+ *
+ * <p>An ability to retrieve glyph node by combination of font name and glyph name
+ * extends to the {@link org.controlsfx.control.action.ActionProxy} graphic attribute, where the "font>"
+ * prefix should be used. For more information see {@link org.controlsfx.control.action.ActionProxy}.
+ *
+ */
+public class Glyph extends Label implements Duplicatable<Glyph> {
+
+    /***************************************************************************
+     *                                                                         *
+     * Static creators                                                         *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * Retrieve glyph by font name and glyph name using one string
+     * where font name an glyph name are separated by pipe.
+     *
+     * @param fontAndGlyph The font and glyph separated by a pipe. Example: "FontAwesome|STAR"
+     * @return A instance of a Glyph node
+     */
+    public static Glyph create(String fontAndGlyph) {
+        String[] args = fontAndGlyph.split("\\|"); //$NON-NLS-1$
+        return new Glyph(args[0], args[1]);
+    }
+
+
+    /***************************************************************************
+     *                                                                         *
+     * Private fields                                                          *
+     *                                                                         *
+     **************************************************************************/
+
+    public final static String DEFAULT_CSS_CLASS = "glyph-font";    //$NON-NLS-1$
+    public final static String STYLE_GRADIENT = "gradient";         //$NON-NLS-1$
+    public final static String STYLE_HOVER_EFFECT = "hover-effect"; //$NON-NLS-1$
+
+    private final ObjectProperty<Object> icon = new SimpleObjectProperty<>();
+
+    /***************************************************************************
+     *                                                                         *
+     * Constructors                                                            *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * Empty Constructor (used by FXML)
+     */
+    public Glyph(){
+        getStyleClass().add(DEFAULT_CSS_CLASS);
+
+        icon.addListener(x -> updateIcon());
+        fontProperty().addListener(x -> updateIcon());
+    }
+
+    /**
+     * Creates a new Glyph
+     * @param fontFamily The family name of the font. Example: "FontAwesome"
+     * @param unicode The Unicode character of the glyph
+     */
+    public Glyph(String fontFamily, char unicode) {
+        this();
+        setFontFamily(fontFamily);
+        setTextUnicode(unicode);
+    }
+
+    /**
+     * Creates a new Glyph
+     * @param fontFamily The family name of the font. Example: "FontAwesome"
+     * @param icon The icon - which can be the name (String) or Enum value.
+     *             Example: FontAwesome.Glyph.BEER
+     */
+    public Glyph(String fontFamily, Object icon) {
+        this();
+        setFontFamily(fontFamily);
+        setIcon(icon);
+    }
+
+    /***************************************************************************
+     *                                                                         *
+     * Public API                                                              *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * Sets the glyph icon font family
+     * @param fontFamily A font family name
+     * @return Returns this instance for fluent API
+     */
+    public Glyph fontFamily(String fontFamily){
+        setFontFamily(fontFamily);
+        return this;
+    }
+
+    /**
+     * Sets the glyph color
+     * @param color
+     * @return Returns this instance for fluent API
+     */
+    public Glyph color(Color color){
+        setColor(color);
+        return this;
+    }
+
+    /**
+     * Sets glyph size
+     * @param size
+     * @return Returns this instance for fluent API
+     */
+    public Glyph size(double size) {
+        setFontSize(size);
+        return this;
+    }
+
+    /**
+     * Sets glyph size using size factor based on default font size
+     * @param factor
+     * @return Returns this instance for fluent API
+     */
+    public Glyph sizeFactor(int factor) {
+    	Optional.ofNullable(GlyphFontRegistry.font(getFont().getFamily())).ifPresent( glyphFont ->{
+    		setFontSize(glyphFont.getDefaultSize()* (factor < 1? 1: factor));
+    	});
+    	return this;
+    }
+
+    
+
+    /**
+     * Adds the hover effect style
+     * @return Returns this instance for fluent API
+     */
+    public Glyph useHoverEffect(){
+        this.getStyleClass().add(Glyph.STYLE_HOVER_EFFECT);
+        return this;
+    }
+
+    /**
+     * Adds the gradient effect style
+     * @return Returns this instance for fluent API
+     */
+    public Glyph useGradientEffect(){
+
+        if(getTextFill() instanceof Color){
+            Color currentColor = (Color)getTextFill();
+
+            /*
+             TODO
+             Do this in code:
+            -fx-text-fill: linear-gradient(to bottom, derive(-fx-text-fill,20%) 10%, derive(-fx-text-fill,-40%) 80%);
+             */
+            Stop[] stops = new Stop[] { new Stop(0, Color.BLACK), new Stop(1, currentColor)};
+            LinearGradient lg1 = new LinearGradient(0, 0, 1, 0, true, CycleMethod.NO_CYCLE, stops);
+            setTextFill(lg1);
+        }
+
+        this.getStyleClass().add(Glyph.STYLE_GRADIENT);
+        return this;
+    }
+
+
+    /**
+     * Allows glyph duplication. Since in the JavaFX scenegraph it is not possible to insert the same
+     * {@link Node} in multiple locations at the same time, this method allows for glyph reuse in several places
+     */
+    @Override public Glyph duplicate() {
+        Paint color = getTextFill();
+        Object icon = getIcon();
+        ObservableList<String> classes = getStyleClass();
+        return new Glyph(){{
+            setIcon(icon);
+            setTextFill(color);
+            getStyleClass().addAll(classes);
+        }}
+                .fontFamily(getFontFamily())
+                .size(getFontSize());
+    }
+
+    /***************************************************************************
+     *                                                                         *
+     * Properties                                                              *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * Sets the font family of this glyph
+     * Font size is reset to default glyph font size
+     */
+    public void setFontFamily(String family){
+        if( !getFont().getFamily().equals(family)){
+        	Optional.ofNullable(GlyphFontRegistry.font(family)).ifPresent( glyphFont -> {
+        		glyphFont.ensureFontIsLoaded(); // Make sure font is loaded 
+        		Font newFont = Font.font(family, glyphFont.getDefaultSize()); // Reset to default font size
+                setFont(newFont);
+        	});
+        }
+    }
+
+    /**
+     * Gets the font family of this glyph
+     */
+    public String getFontFamily(){
+        return getFont().getFamily();
+    }
+
+    /**
+     * Sets the font size of this glyph
+     */
+    public void setFontSize(double size){
+        Font newFont = Font.font(getFont().getFamily(), size);
+        setFont(newFont);
+    }
+
+    /**
+     * Gets the font size of this glyph
+     */
+    public double getFontSize(){
+        return getFont().getSize();
+    }
+
+    /**
+     * Set the Color of this Glyph
+     */
+    public void setColor(Color color){
+        setTextFill(color);
+    }
+
+    /**
+     * The icon name property.
+     *
+     * This must either be a Glyph-Name (either string or enum value) known by the GlyphFontRegistry.
+     * Alternatively, you can directly submit a unicode character here.
+     */
+    public ObjectProperty<Object> iconProperty(){
+        return icon;
+    }
+
+    /**
+     * Set the icon to display.
+     * @param iconValue This can either be the Glyph-Name, Glyph-Enum Value or a unicode character representing the sign.
+     */
+    public void setIcon(Object iconValue){
+        icon.set(iconValue);
+    }
+
+    public Object getIcon(){
+        return icon.get();
+    }
+
+    /***************************************************************************
+     *                                                                         *
+     * Private methods                                                         *
+     *                                                                         *
+     **************************************************************************/
+
+
+    /**
+     * This updates the text with the correct unicode value
+     * so that the desired icon is displayed.
+     */
+    private void updateIcon(){
+
+        Object iconValue = getIcon();
+
+        if(iconValue != null) {
+            if(iconValue instanceof Character){
+                setTextUnicode((Character)iconValue);
+            }else {
+                GlyphFont glyphFont = GlyphFontRegistry.font(getFontFamily());
+                if (glyphFont != null) {
+                    String name = iconValue.toString();
+                    Character unicode = glyphFont.getCharacter(name);
+                    if (unicode != null) {
+                        setTextUnicode(unicode);
+                    } else {
+                        // Could not find a icon with this name
+                        setText(name);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Sets the given char as text
+     * @param unicode
+     */
+    private void setTextUnicode(char unicode){
+        setText(String.valueOf(unicode));
+    }
+}
\ No newline at end of file
diff --git a/controlsfx/src/main/java/org/controlsfx/glyphfont/GlyphFont.java b/controlsfx/src/main/java/org/controlsfx/glyphfont/GlyphFont.java
new file mode 100644
index 0000000..ea355cd
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/glyphfont/GlyphFont.java
@@ -0,0 +1,245 @@
+/**
+ * Copyright (c) 2013, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.glyphfont;
+
+import com.sun.javafx.css.StyleManager;
+import javafx.scene.text.Font;
+
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ *  Represents a glyph font, which can be loaded locally or from a specified URL.
+ *  {@link Glyph}s can be created easily using specified character defined in the
+ *  font. For example, \uf013 in FontAwesome is used to represent
+ *  a gear icon.
+ *
+ *  <p>To simplify glyph customization, methods can be chained, for example:
+ *
+ *  <pre>
+ *  Glyph glyph = fontAwesome.create('\uf013').size(28).color(Color.RED); //GEAR
+ *  </pre>
+ *
+ *  <p>Here's a screenshot of two font packs being used to render images into
+ *  JavaFX Button controls:
+ *
+ * <br>
+ * <center><img src="glyphFont.png" alt="Screenshot of GlyphFont"></center>
+ */
+public class GlyphFont {
+
+    static {
+        StyleManager.getInstance().addUserAgentStylesheet(
+                GlyphFont.class.getResource("glyphfont.css").toExternalForm()); //$NON-NLS-1$
+    }
+
+    /***************************************************************************
+     *                                                                         *
+     * Private fields                                                          *
+     *                                                                         *
+     **************************************************************************/
+
+    private final Map<String, Character> namedGlyphs = new HashMap<>();
+    private final Runnable fontLoader;
+    private final String fontName;
+    private final double defaultSize;
+
+    private boolean  fontLoaded = false;
+
+
+    /***************************************************************************
+     *                                                                         *
+     * Constructors                                                            *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * Loads glyph font from specified {@link InputStream}
+     * @param fontName glyph font name
+     * @param defaultSize default font size
+     * @param in input stream to load the font from
+     */
+    public GlyphFont( String fontName, int defaultSize, final InputStream in) {
+        this(fontName, defaultSize, in, false);
+    }
+
+    /**
+     * Load glyph font from specified URL.
+     * Example for a local file:
+     * "file:///C:/Users/Bob/Fonts/icomoon.ttf"
+     * "file:///Users/Bob/Fonts/icomoon.ttf"
+     *
+     * @param fontName glyph font name
+     * @param defaultSize default font size
+     * @param urlStr A URL to load the font from
+     */
+    public GlyphFont( String fontName, int defaultSize, final String urlStr) {
+        this(fontName, defaultSize, urlStr, false);
+    }
+
+    /**
+     * Loads glyph font from specified {@link InputStream}
+     * @param fontName glyph font name
+     * @param defaultSize default font size
+     * @param in input stream to load the font from
+     * @param lazyLoad If true, the font will only be loaded when accessed
+     */
+    public GlyphFont( String fontName, int defaultSize, final InputStream in, boolean lazyLoad) {
+        this(fontName, defaultSize, () -> {
+            Font.loadFont(in, -1);
+        }, lazyLoad);
+    }
+
+    /**
+     * Load glyph font from specified URL.
+     * Example for a local file:
+     * "file:///C:/Users/Bob/Fonts/icomoon.ttf"
+     * "file:///Users/Bob/Fonts/icomoon.ttf"
+     *
+     * @param fontName glyph font name
+     * @param defaultSize default font size
+     * @param urlStr A URL to load the font from
+     * @param lazyLoad If true, the font will only be loaded when accessed
+     */
+    public GlyphFont( String fontName, int defaultSize, final String urlStr, boolean lazyLoad) {
+        this(fontName, defaultSize, () -> {
+            Font.loadFont(urlStr, -1);
+        }, lazyLoad);
+    }
+
+    /**
+     * Creates a GlyphFont
+     * @param fontName
+     * @param defaultSize
+     * @param fontLoader
+     * @param lazyLoad
+     */
+    private GlyphFont(String fontName, int defaultSize, Runnable fontLoader, boolean lazyLoad){
+        this.fontName = fontName;
+        this.defaultSize = defaultSize;
+        this.fontLoader = fontLoader;
+
+        if(!lazyLoad){
+            ensureFontIsLoaded();
+        }
+    }
+
+    /***************************************************************************
+     *                                                                         *
+     * Public API                                                              *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * Returns font name
+     * @return font name
+     */
+    public String getName() {
+        return fontName;
+    }
+
+    /**
+     * Returns the default font size
+     * @return default font size
+     */
+    public double getDefaultSize() {
+        return defaultSize;
+    }
+
+
+    /**
+     * Creates an instance of {@link Glyph} using specified font character
+     * @param character font character
+     * @return instance of {@link Glyph}
+     */
+    public Glyph create(char character) {
+        return new Glyph(fontName, character);
+    }
+
+    /**
+     * Creates an instance of {@link Glyph} using glyph name
+     * @param glyphName glyph name
+     * @return glyph by its name or null if name is not found
+     */
+    public Glyph create(String glyphName) {
+        return new Glyph(fontName, glyphName);
+    }
+
+    /**
+     *  Creates an instance of {@link Glyph} using a known Glyph enum value
+     * @param glyph
+     */
+    public Glyph create(Enum<?> glyph) {
+        return new Glyph(fontName, glyph);
+    }
+
+    /**
+     * Returns the character code which is mapped to this Name.
+     * If no match is found, NULL is returned.
+     * @param glyphName
+     */
+    public Character getCharacter(String glyphName){
+        return namedGlyphs.get(glyphName.toUpperCase());
+    }
+
+
+    /**
+     * Registers all given characters with their name.
+     * @param namedCharacters
+     */
+    public void registerAll(Iterable<? extends INamedCharacter> namedCharacters){
+        for (INamedCharacter e:  namedCharacters) {
+            register(e.name(), e.getChar());
+        }
+    }
+
+    /**
+     * Registers the given name-character mapping
+     * @param name
+     * @param character
+     */
+    public void register(String name, Character character){
+        namedGlyphs.put(name.toUpperCase(), character);
+    }
+
+    /***************************************************************************
+     *                                                                         *
+     * Internal methods                                                        *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * Ensures that the font is loaded
+     */
+    synchronized void ensureFontIsLoaded(){
+        if ( !fontLoaded ) {
+            fontLoader.run();
+            fontLoaded = true;
+        }
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/glyphfont/GlyphFontRegistry.java b/controlsfx/src/main/java/org/controlsfx/glyphfont/GlyphFontRegistry.java
new file mode 100644
index 0000000..cc9d433
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/glyphfont/GlyphFontRegistry.java
@@ -0,0 +1,127 @@
+/**
+ * Copyright (c) 2013,2014 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.glyphfont;
+
+
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.ServiceLoader;
+
+/**
+ * The glyph font registry automatically registers available fonts using a
+ * {@link ServiceLoader} facility, however it is also possible to register
+ * glyph fonts manually using the provided
+ * {@link GlyphFontRegistry#register(GlyphFont)} method.
+ *
+ * <p>Once registered, fonts can be requested by name using the
+ * {@link GlyphFontRegistry#font(String)} method.
+ *
+ * Please refer to the {@link GlyphFont} documentation
+ * to learn how to use a font.
+ *
+ */
+public final class GlyphFontRegistry {
+
+    /***************************************************************************
+     *                                                                         *
+     * Private fields                                                          *
+     *                                                                         *
+     **************************************************************************/
+
+    private static Map<String, GlyphFont> fontMap = new HashMap<>();
+
+    /***************************************************************************
+     *                                                                         *
+     * Constructors                                                            *
+     *                                                                         *
+     **************************************************************************/
+
+    static {
+        // find all classes that implement GlyphFont and register them now
+        ServiceLoader<GlyphFont> loader = ServiceLoader.load(GlyphFont.class);
+        for (GlyphFont font : loader) {
+            GlyphFontRegistry.register(font);
+        }
+    }
+
+    /**
+     * Private constructor since static class
+     */
+    private GlyphFontRegistry() {
+        // no-op
+    }
+
+    /***************************************************************************
+     *                                                                         *
+     * Public API                                                              *
+     *                                                                         *
+     **************************************************************************/
+
+    /**
+     * Registers the specified font as default GlyphFont
+     * @param familyName The name of this font.
+     * @param uri The location where it can be loaded from.
+     * @param defaultSize The default font size
+     */
+    public static void register(String familyName, String uri, int defaultSize){
+        register(new GlyphFont(familyName, defaultSize, uri));
+    }
+
+    /**
+     * Registers the specified font as default GlyphFont
+     * @param familyName The name of this font.
+     * @param in Inputstream of the font data
+     * @param defaultSize The default font size
+     */
+    public static void register(String familyName, InputStream in, int defaultSize){
+        register(new GlyphFont(familyName, defaultSize, in));
+    }
+
+    /**
+     * Registers the specified font
+     * @param font
+     */
+    public static void register( GlyphFont font ) {
+        if (font != null ) {
+            fontMap.put( font.getName(), font );
+        }
+    }
+
+    /**
+     * Retrieve font by its family name
+     * @param familyName family name of the font
+     * @return font or null if not found
+     */
+    public static GlyphFont font( String familyName ) {
+        GlyphFont font = fontMap.get(familyName);
+        if(font != null) {
+            font.ensureFontIsLoaded();
+        }
+        return font;
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/glyphfont/INamedCharacter.java b/controlsfx/src/main/java/org/controlsfx/glyphfont/INamedCharacter.java
new file mode 100644
index 0000000..60b7069
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/glyphfont/INamedCharacter.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.glyphfont;
+
+/**
+ * Represents a named character.
+ * This interface is usually implemented by a Enum
+ * which holds all characters of a specific font.
+ */
+public interface INamedCharacter {
+    /**
+     * Gets the name of this character
+     */
+    String name();
+
+    /**
+     * Gets the character value
+     */
+    char getChar();
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/glyphfont/package-info.java b/controlsfx/src/main/java/org/controlsfx/glyphfont/package-info.java
new file mode 100644
index 0000000..a4cee16
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/glyphfont/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * A package containing a number of useful code related to loading and using
+ * font packs whose characters are actually images.
+ */
+package org.controlsfx.glyphfont;
\ No newline at end of file
diff --git a/controlsfx/src/main/java/org/controlsfx/property/BeanProperty.java b/controlsfx/src/main/java/org/controlsfx/property/BeanProperty.java
new file mode 100644
index 0000000..3415253
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/property/BeanProperty.java
@@ -0,0 +1,208 @@
+/**
+ * Copyright (c) 2013, 2015, 2016 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.property;
+
+import java.beans.FeatureDescriptor;
+import java.beans.PropertyDescriptor;
+import java.beans.PropertyVetoException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Optional;
+
+import org.controlsfx.control.PropertySheet;
+import org.controlsfx.control.PropertySheet.Item;
+import org.controlsfx.property.editor.PropertyEditor;
+
+import impl.org.controlsfx.i18n.Localization;
+import javafx.beans.value.ObservableValue;
+import javafx.scene.control.Alert;
+
+/**
+ * A convenience class for creating a {@link Item} for use in the
+ * {@link PropertySheet} control based on a property belonging to a
+ * JavaBean - simply provide a {@link PropertyDescriptor} and the rest will be
+ * taken care of automatically.
+ *
+ * @see Item
+ * @see PropertySheet
+ * @see PropertyDescriptor
+ */
+public class BeanProperty implements PropertySheet.Item {
+
+    /**
+     * Unique identifier to provide a custom category label within
+     * {@link PropertySheet.Item#getCategory()}.
+     *
+     * How to use it: with a PropertyDescriptor, provide the custom category
+     * through a a named attribute
+     * {@link FeatureDescriptor#setValue(String, Object)}.
+     *
+     * <pre>
+     * final PropertyDescriptor propertyDescriptor = new PropertyDescriptor("yourProperty", YourBean.class);
+     * propertyDescriptor.setDisplayName("Your Display Name");
+     * propertyDescriptor.setShortDescription("Your explanation about this property.");
+     * // then provide a custom category
+     * propertyDescriptor.setValue(BeanProperty.CATEGORY_LABEL_KEY, "Your custom category");
+     * </pre>
+     */
+    public static final String CATEGORY_LABEL_KEY = "propertysheet.item.category.label";
+
+    private final Object bean;
+    private final PropertyDescriptor beanPropertyDescriptor;
+    private final Method readMethod;
+    private boolean editable = true;
+    private Optional<ObservableValue<? extends Object>> observableValue = Optional.empty();
+
+    public BeanProperty(final Object bean, final PropertyDescriptor propertyDescriptor) {
+        this.bean = bean;
+        this.beanPropertyDescriptor = propertyDescriptor;
+        this.readMethod = propertyDescriptor.getReadMethod();
+        if (this.beanPropertyDescriptor.getWriteMethod() == null) {
+            this.setEditable(false);
+        }
+
+        this.findObservableValue();
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getName() {
+        return this.beanPropertyDescriptor.getDisplayName();
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getDescription() {
+        return this.beanPropertyDescriptor.getShortDescription();
+    }
+
+    /** {@inheritDoc} */
+    @Override public Class<?> getType() {
+        return this.beanPropertyDescriptor.getPropertyType();
+    }
+
+    /** {@inheritDoc} */
+    @Override public Object getValue() {
+        try {
+            return this.readMethod.invoke(this.bean);
+        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override public void setValue(final Object value) {
+        final Method writeMethod = this.beanPropertyDescriptor.getWriteMethod();
+        if ( writeMethod != null ) {
+            try {
+                writeMethod.invoke(this.bean, value);
+            } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
+                e.printStackTrace();
+            } catch (final Throwable e) {
+                if (e instanceof PropertyVetoException) {
+                    final Alert alert = new Alert(Alert.AlertType.ERROR);
+                    alert.setTitle(Localization.localize(Localization.asKey("bean.property.change.error.title")));//$NON-NLS-1$
+                    alert.setHeaderText(Localization.localize(Localization.asKey("bean.property.change.error.masthead")));//$NON-NLS-1$
+                    alert.setContentText(e.getLocalizedMessage());
+                    alert.showAndWait();
+                } else {
+                    throw e;
+                }
+            }
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getCategory() {
+        String category = (String) this.beanPropertyDescriptor.getValue(BeanProperty.CATEGORY_LABEL_KEY);
+
+        // fall back to default behavior if there is no category provided.
+        if (category == null) {
+            category = Localization.localize(Localization.asKey(this.beanPropertyDescriptor.isExpert()
+                    ? "bean.property.category.expert" : "bean.property.category.basic")); //$NON-NLS-1$ //$NON-NLS-2$
+        }
+        return category;
+    }
+
+    /**
+     * @return The object passed in to the constructor of the BeanProperty.
+     */
+    public Object getBean() {
+        return this.bean;
+    }
+
+    /**
+     * @return The {@link PropertyDescriptor} passed in to the constructor of
+     * the BeanProperty.
+     */
+    public PropertyDescriptor getPropertyDescriptor() {
+        return this.beanPropertyDescriptor;
+    }
+
+    /** {@inheritDoc} */
+    @SuppressWarnings({ "unchecked" })
+    @Override public Optional<Class<? extends PropertyEditor<?>>> getPropertyEditorClass() {
+
+        if ((this.beanPropertyDescriptor.getPropertyEditorClass() != null) &&
+                PropertyEditor.class.isAssignableFrom(this.beanPropertyDescriptor.getPropertyEditorClass())) {
+
+            return Optional.of((Class<PropertyEditor<?>>)this.beanPropertyDescriptor.getPropertyEditorClass());
+        }
+
+        return Item.super.getPropertyEditorClass();
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean isEditable() {
+        return this.editable;
+    }
+
+    /**
+     * @param editable Whether this property should be editable in the PropertySheet.
+     */
+    public void setEditable(final boolean editable) {
+        this.editable = editable;
+    }
+
+    /** {@inheritDoc} */
+    @Override public Optional<ObservableValue<? extends Object>> getObservableValue() {
+        return this.observableValue;
+    }
+
+    private void findObservableValue() {
+        try {
+            final String propName = this.beanPropertyDescriptor.getName() + "Property";
+            final Method m = this.getBean().getClass().getMethod(propName);
+            final Object val = m.invoke(this.getBean());
+            if ((val != null) && (val instanceof ObservableValue)) {
+                this.observableValue = Optional.of((ObservableValue<?>) val);
+            }
+        } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
+            //Logger.getLogger(BeanProperty.class.getName()).log(Level.SEVERE, null, ex);
+            // ignore it...
+        }
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/property/BeanPropertyUtils.java b/controlsfx/src/main/java/org/controlsfx/property/BeanPropertyUtils.java
new file mode 100644
index 0000000..b88093b
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/property/BeanPropertyUtils.java
@@ -0,0 +1,96 @@
+/**
+ * Copyright (c) 2013, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.property;
+
+import java.beans.BeanInfo;
+import java.beans.IntrospectionException;
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
+import java.util.function.Predicate;
+
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.event.EventHandler;
+
+import org.controlsfx.control.PropertySheet;
+import org.controlsfx.control.PropertySheet.Item;
+
+/**
+ * Convenience utility class for creating {@link PropertySheet} instances based
+ * on a JavaBean.
+ */
+public final class BeanPropertyUtils {
+
+    private BeanPropertyUtils() {
+        // no op
+    }
+
+    /**
+     * Given a JavaBean, this method will return a list of {@link Item} intances,
+     * which may be directly placed inside a {@link PropertySheet} (via its
+     * {@link PropertySheet#getItems() items list}.
+     * <p>
+     * This method will not return read-only properties.
+     * 
+     * @param bean The JavaBean that should be introspected and be editable via
+     *      a {@link PropertySheet}.
+     * @return A list of {@link Item} instances representing the properties of the
+     *      JavaBean.
+     */
+    public static ObservableList<Item> getProperties(final Object bean) {
+        return getProperties(bean, (p) -> {return true;} );
+    }
+    
+    /**
+     * Given a JavaBean, this method will return a list of {@link Item} intances,
+     * which may be directly placed inside a {@link PropertySheet} (via its
+     * {@link PropertySheet#getItems() items list}.
+     * 
+     * @param bean The JavaBean that should be introspected and be editable via
+     *      a {@link PropertySheet}.
+     * @param test Predicate to test whether the property should be included in the 
+     *      list of results.
+     * @return A list of {@link Item} instances representing the properties of the
+     *      JavaBean.
+     */
+    public static ObservableList<Item> getProperties(final Object bean, Predicate<PropertyDescriptor> test) {
+        ObservableList<Item> list = FXCollections.observableArrayList();
+        try {
+            BeanInfo beanInfo = Introspector.getBeanInfo(bean.getClass(), Object.class);
+            for (PropertyDescriptor p : beanInfo.getPropertyDescriptors()) {
+                if (test.test(p)) {
+                    list.add(new BeanProperty(bean, p));
+                }
+            }
+        } catch (IntrospectionException e) {
+            e.printStackTrace();
+        }
+
+        return list;
+    }
+    
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/property/editor/AbstractObjectField.java b/controlsfx/src/main/java/org/controlsfx/property/editor/AbstractObjectField.java
new file mode 100644
index 0000000..d6f3941
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/property/editor/AbstractObjectField.java
@@ -0,0 +1,91 @@
+/**
+ * Copyright (c) 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.property.editor;
+
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.property.StringProperty;
+import javafx.scene.Cursor;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.input.MouseButton;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.StackPane;
+
+import org.controlsfx.control.textfield.CustomTextField;
+
+// package-private for now...
+abstract class AbstractObjectField<T> extends HBox {
+
+    //TODO: Replace with CSS
+    private static final Image image = new Image(AbstractObjectField.class.getResource("/org/controlsfx/control/open-editor.png").toExternalForm()); //$NON-NLS-1$
+
+    private final CustomTextField textField = new CustomTextField();
+
+    private ObjectProperty<T> objectProperty = new SimpleObjectProperty<>();
+
+    public AbstractObjectField() {
+        super(1);
+        textField.setEditable(false);
+        textField.setFocusTraversable(false);
+
+        StackPane button = new StackPane(new ImageView(image));
+        button.setCursor(Cursor.DEFAULT);
+
+        button.setOnMouseReleased(e -> {
+            if ( MouseButton.PRIMARY == e.getButton() ) {
+                final T result = edit(objectProperty.get());
+                if (result != null) {
+                    objectProperty.set(result);
+                }
+            }
+        });
+
+        textField.setRight(button);
+        getChildren().add(textField);
+        HBox.setHgrow(textField, Priority.ALWAYS);
+
+        objectProperty.addListener((o, oldValue, newValue) -> textProperty().set(objectToString(newValue)));
+    }
+
+    protected StringProperty textProperty() {
+        return textField.textProperty();
+    }
+
+    public ObjectProperty<T> getObjectProperty() {
+        return objectProperty;
+    }
+
+    protected String objectToString(T object) {
+        return object == null ? "" : object.toString(); //$NON-NLS-1$
+    }
+
+    protected abstract Class<T> getType();
+
+    protected abstract T edit(T object);
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/property/editor/AbstractPropertyEditor.java b/controlsfx/src/main/java/org/controlsfx/property/editor/AbstractPropertyEditor.java
new file mode 100644
index 0000000..5a3cc80
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/property/editor/AbstractPropertyEditor.java
@@ -0,0 +1,139 @@
+/**
+ * Copyright (c) 2013, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.property.editor;
+
+import javafx.beans.value.ObservableValue;
+import javafx.scene.Node;
+
+import org.controlsfx.control.PropertySheet.Item;
+
+/**
+ * An abstract implementation of the {@link PropertyEditor} interface.
+ *
+ * @param <T> The type of the property being edited.
+ * @param <C> The type of Node that is used to edit this property.
+ */
+public abstract class AbstractPropertyEditor<T, C extends Node> implements PropertyEditor<T> {
+    
+    /**************************************************************************
+     * 
+     * Private fields
+     * 
+     **************************************************************************/
+
+    private final Item property;
+    private final C control;
+    private boolean suspendUpdate;
+    
+    
+    /**************************************************************************
+     * 
+     * Constructors
+     * 
+     **************************************************************************/
+    
+    /**
+     * Creates an editable AbstractPropertyEditor instance for the given property
+     * using the given editing control.
+     * 
+     * @param property The property that the instance is responsible for editing.
+     * @param control The control that is responsible for editing the property.
+     */
+    public AbstractPropertyEditor(Item property, C control) {
+        this(property, control, ! property.isEditable());
+    }
+    
+    /**
+     * Creates an AbstractPropertyEditor instance for the given property
+     * using the given editing control. It may be read-only or editable, based
+     * on the readonly boolean parameter being true or false.
+     * 
+     * @param property The property that the instance is responsible for editing.
+     * @param control The control that is responsible for editing the property.
+     * @param readonly Specifies whether the editor should allow input or not.
+     */
+    public AbstractPropertyEditor(Item property, C control, boolean readonly) {
+        this.control = control;
+        this.property = property;
+        if (! readonly) {
+            getObservableValue().addListener((ObservableValue<? extends Object> o, Object oldValue, Object newValue) -> {
+                if (! suspendUpdate) {
+                    suspendUpdate = true;
+                    AbstractPropertyEditor.this.property.setValue(getValue());
+                    suspendUpdate = false;
+                }
+            });
+            
+            if (property.getObservableValue().isPresent()) {
+                property.getObservableValue().get().addListener((ObservableValue<? extends Object> o, Object oldValue, Object newValue) -> {
+                    if (! suspendUpdate) {
+                        suspendUpdate = true;
+                        AbstractPropertyEditor.this.setValue((T) property.getValue());
+                        suspendUpdate = false;
+                    }
+                });
+            }
+            
+        }
+    }
+    
+    
+    
+    /**************************************************************************
+     * 
+     * Public API
+     * 
+     **************************************************************************/
+    
+    /**
+     * Returns an {@link ObservableValue} of the property that this property
+     * editor is responsible for editing. This is the editor's value, e.g. a 
+     * TextField's textProperty().
+     */
+    protected abstract ObservableValue<T> getObservableValue();
+    
+    /**
+     * Returns the property that this property editor is responsible for editing.
+     */
+    public final Item getProperty() {
+        return property;
+    }
+        
+    /**
+     * {@inheritDoc}
+     */
+    @Override public C getEditor() {
+        return control;
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    @Override public T getValue() {
+        return getObservableValue().getValue();
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/property/editor/DefaultPropertyEditorFactory.java b/controlsfx/src/main/java/org/controlsfx/property/editor/DefaultPropertyEditorFactory.java
new file mode 100644
index 0000000..36b483b
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/property/editor/DefaultPropertyEditorFactory.java
@@ -0,0 +1,115 @@
+/**
+ * Copyright (c) 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.property.editor;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.time.LocalDate;
+import java.util.Arrays;
+import java.util.Optional;
+
+import javafx.scene.paint.Color;
+import javafx.scene.paint.Paint;
+import javafx.scene.text.Font;
+import javafx.util.Callback;
+
+import org.controlsfx.control.PropertySheet;
+import org.controlsfx.control.PropertySheet.Item;
+
+/**
+ * A default implementation of the {@link Callback} type required by the
+ * {@link PropertySheet} 
+ * {@link PropertySheet#propertyEditorFactory() property editor factory}. By 
+ * default this is the implementation used by PropertySheet, but developers may
+ * choose to provide their own, or more likely, extend this implementation
+ * and override the {@link DefaultPropertyEditorFactory#call(org.controlsfx.control.PropertySheet.Item) } method to 
+ * add in support for additional editor types.
+ *
+ * @see PropertySheet
+ */
+public class DefaultPropertyEditorFactory implements Callback<Item, PropertyEditor<?>> {
+    
+    @Override public PropertyEditor<?> call(Item item) {
+        Class<?> type = item.getType();
+        
+        //TODO: add support for char and collection editors
+        
+        if (item.getPropertyEditorClass().isPresent()) {
+            Optional<PropertyEditor<?>> ed = Editors.createCustomEditor(item);
+            if (ed.isPresent()) return ed.get();
+        }
+        
+        if (/*type != null &&*/ type == String.class) {
+            return Editors.createTextEditor(item);  
+        }
+
+        if (/*type != null &&*/ isNumber(type)) {
+            return Editors.createNumericEditor(item);
+        }
+        
+        if (/*type != null &&*/(type == boolean.class || type == Boolean.class)) {
+            return Editors.createCheckEditor(item);
+        }
+
+        if (/*type != null &&*/type == LocalDate.class) {
+            return Editors.createDateEditor(item);
+        }
+        
+        if (/*type != null &&*/type == Color.class || type == Paint.class) {
+            return Editors.createColorEditor(item);
+        }
+
+        if (type != null && type.isEnum()) {
+            return Editors.createChoiceEditor(item, Arrays.<Object>asList(type.getEnumConstants()));
+        }
+        
+        if (/*type != null &&*/type == Font.class) {
+            return Editors.createFontEditor(item);
+        }
+        
+        return null; 
+    }
+    
+    private static Class<?>[] numericTypes = new Class[]{
+        byte.class, Byte.class,
+        short.class, Short.class,
+        int.class, Integer.class,
+        long.class, Long.class,
+        float.class, Float.class,
+        double.class, Double.class,
+        BigInteger.class, BigDecimal.class
+    };    
+    
+    // there should be better ways to do this
+    private static boolean isNumber(Class<?> type)  {
+        if ( type == null ) return false;
+        for (Class<?> cls : numericTypes) {
+            if ( type == cls ) return true;
+        }
+        return false;
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/property/editor/Editors.java b/controlsfx/src/main/java/org/controlsfx/property/editor/Editors.java
new file mode 100644
index 0000000..c742d49
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/property/editor/Editors.java
@@ -0,0 +1,232 @@
+/**
+ * Copyright (c) 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.property.editor;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.time.LocalDate;
+import java.util.Collection;
+import java.util.Optional;
+
+import javafx.application.Platform;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.StringProperty;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.FXCollections;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.ColorPicker;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.DatePicker;
+import javafx.scene.control.TextField;
+import javafx.scene.control.TextInputControl;
+import javafx.scene.paint.Color;
+import javafx.scene.text.Font;
+
+import org.controlsfx.control.PropertySheet;
+import org.controlsfx.control.PropertySheet.Item;
+import org.controlsfx.dialog.FontSelectorDialog;
+
+ at SuppressWarnings("deprecation")
+public class Editors {
+
+    private Editors() {
+        // no op
+    }
+    
+    public static final PropertyEditor<?> createTextEditor( Item property ) {
+    
+        return new AbstractPropertyEditor<String, TextField>(property, new TextField()) {
+
+            { enableAutoSelectAll(getEditor()); } 
+            
+            @Override protected StringProperty getObservableValue() {
+                return getEditor().textProperty();
+            }
+    
+            @Override public void setValue(String value) {
+                getEditor().setText(value);
+            }
+        };
+    }
+    
+    @SuppressWarnings("unchecked")
+    public static final PropertyEditor<?> createNumericEditor( Item property ) {
+
+    	return new AbstractPropertyEditor<Number, NumericField>(property, new NumericField( (Class<? extends Number>) property.getType())) {
+
+			private Class<? extends Number> sourceClass = (Class<? extends Number>) property.getType(); //Double.class;
+            
+            { enableAutoSelectAll(getEditor()); }
+
+            @Override protected ObservableValue<Number> getObservableValue() {
+                return getEditor().valueProperty();
+            }
+
+            @Override public Number getValue() {
+                try {
+                    return sourceClass.getConstructor(String.class).newInstance(getEditor().getText());
+                } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException
+                        | NoSuchMethodException | SecurityException e) {
+                    e.printStackTrace();
+                    return null;
+                }
+            }
+
+            @Override public void setValue(Number value) {
+                sourceClass = (Class<? extends Number>) value.getClass();
+                getEditor().setText(value.toString());
+            }
+            
+        };   
+    }
+    
+    public static final PropertyEditor<?> createCheckEditor( Item property ) {
+     
+        return new AbstractPropertyEditor<Boolean, CheckBox>(property, new CheckBox()) {
+
+            @Override protected BooleanProperty getObservableValue() {
+                return getEditor().selectedProperty();
+            }
+            
+            @Override public void setValue(Boolean value) {
+                getEditor().setSelected((Boolean)value);
+            }
+        };
+        
+    }
+    
+    public static final <T> PropertyEditor<?> createChoiceEditor( Item property, final Collection<T> choices ) {
+         
+        return new AbstractPropertyEditor<T, ComboBox<T>>(property, new ComboBox<T>()) {
+            
+            { getEditor().setItems(FXCollections.observableArrayList(choices)); }
+            
+            @Override protected ObservableValue<T> getObservableValue() {
+                return getEditor().getSelectionModel().selectedItemProperty();
+            }
+
+            @Override public void setValue(T value) {
+                getEditor().getSelectionModel().select(value);
+            }
+        };
+    }
+    
+    public static final PropertyEditor<?> createColorEditor( Item property ) {
+        return new AbstractPropertyEditor<Color, ColorPicker>(property, new ColorPicker()) {
+
+            @Override protected ObservableValue<Color> getObservableValue() {
+                return getEditor().valueProperty();
+            }
+
+            @Override public void setValue(Color value) {
+                getEditor().setValue((Color) value);
+            }
+        };
+    }
+    
+    
+    public static final PropertyEditor<?> createDateEditor( Item property ) {
+        return new AbstractPropertyEditor<LocalDate, DatePicker>(property, new DatePicker()) {
+            
+            //TODO: Provide date picker customization support
+            
+            @Override protected ObservableValue<LocalDate> getObservableValue() {
+                return getEditor().valueProperty();
+            }
+
+            @Override public void setValue(LocalDate value) {
+                getEditor().setValue((LocalDate) value);
+            }
+        };
+    }    
+    
+    public static final PropertyEditor<?> createFontEditor( Item property ) {
+        
+        return new AbstractPropertyEditor<Font, AbstractObjectField<Font>>(property, new AbstractObjectField<Font>() {
+                    @Override protected Class<Font> getType() {
+                        return Font.class;
+                    }
+                    
+                    @Override protected String objectToString(Font font) {
+                        return font == null? "": String.format("%s, %.1f", font.getName(), font.getSize()); //$NON-NLS-1$ //$NON-NLS-2$
+                    }
+        
+                    @Override protected Font edit(Font font) {
+                        FontSelectorDialog dlg = new FontSelectorDialog(font);
+                        Optional<Font> optionalFont = dlg.showAndWait();
+                        return optionalFont.get();
+                    }
+                }) {
+
+            @Override protected ObservableValue<Font> getObservableValue() {
+                return getEditor().getObjectProperty();
+            }
+
+            @Override public void setValue(Font value) {
+                getEditor().getObjectProperty().set(value);
+            }
+        };
+        
+    }
+    
+    /**
+     * Static method used to create an instance of the custom editor returned 
+     * via a call to {@link Item#getPropertyEditorClass() } 
+     * 
+     * The class returned must declare a constructor that takes a single 
+     * parameter of type PropertySheet.Item into which the parameter supplied 
+     * to this method will be passed.
+     * 
+     * @param property The {@link Item} that this editor will be 
+     * associated with.
+     * @return The {@link PropertyEditor} wrapped in an {@link Optional}
+     */
+    public static final Optional<PropertyEditor<?>> createCustomEditor(final Item property ) {
+        return property.getPropertyEditorClass().map(cls -> {
+            try {
+                Constructor<?> cn = cls.getConstructor(PropertySheet.Item.class);
+                if (cn != null) {
+                    return (PropertyEditor<?>) cn.newInstance(property);
+                }
+            } catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
+                ex.printStackTrace();
+            }
+            return null;
+        });
+    }
+    
+    private static void enableAutoSelectAll(final TextInputControl control) {
+        control.focusedProperty().addListener((ObservableValue<? extends Boolean> o, Boolean oldValue, Boolean newValue) -> {
+            if (newValue) {
+                Platform.runLater(() -> {
+                    control.selectAll();
+                });
+            }
+        });
+    }
+    
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/property/editor/NumericField.java b/controlsfx/src/main/java/org/controlsfx/property/editor/NumericField.java
new file mode 100644
index 0000000..c90f413
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/property/editor/NumericField.java
@@ -0,0 +1,150 @@
+/**
+ * Copyright (c) 2015 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.property.editor;
+
+import java.math.BigInteger;
+
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.beans.binding.NumberExpression;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.beans.property.SimpleLongProperty;
+import javafx.beans.value.ObservableValue;
+import javafx.scene.control.IndexRange;
+import javafx.scene.control.TextField;
+
+/*
+ * TODO replace this with proper API when it becomes available:
+ * https://javafx-jira.kenai.com/browse/RT-30881
+ */
+class NumericField extends TextField {
+
+    private final NumericValidator<? extends Number> value ;
+    		
+    public NumericField( Class<? extends Number> cls ) {
+    	
+    	if ( cls == byte.class || cls == Byte.class || cls == short.class || cls == Short.class ||
+    		 cls ==	int.class  || cls == Integer.class || cls == long.class || cls == Long.class ||
+    	     cls == BigInteger.class) {
+    		value = new LongValidator(this);
+    	} else {
+    		value = new DoubleValidator(this);
+    	}
+    	
+        textProperty().addListener(new InvalidationListener() {
+            @Override public void invalidated(Observable arg0) {
+                value.setValue(value.toNumber(getText()));
+            }
+        });
+        
+    }
+    
+    public final ObservableValue<Number> valueProperty() {
+        return value;
+    }
+
+    @Override public void replaceText(int start, int end, String text) {
+        if (replaceValid(start, end, text)) {
+            super.replaceText(start, end, text);
+        }
+    }
+
+    @Override public void replaceSelection(String text) {
+        IndexRange range = getSelection();
+        if (replaceValid(range.getStart(), range.getEnd(), text)) {
+            super.replaceSelection(text);
+        }
+    }
+
+    private Boolean replaceValid(int start, int end, String fragment) {
+        try {
+        	String newText = getText().substring(0, start) + fragment + getText().substring(end);
+        	if (newText.isEmpty()) return true; 
+			value.toNumber(newText);
+        	return true;
+        } catch( Throwable ex ) {
+        	return false;
+        }
+    }
+    
+    
+    private static abstract interface NumericValidator<T extends Number> extends NumberExpression {
+    	void setValue(Number num);
+    	T toNumber(String s);
+    	
+    }
+    
+    static class DoubleValidator extends SimpleDoubleProperty implements NumericValidator<Double>{
+    	
+    	private NumericField field;
+    	
+    	public DoubleValidator(NumericField field) {
+    		super(field, "value", 0.0); //$NON-NLS-1$
+    		this.field = field;
+		}
+    	
+    	@Override protected void invalidated() {
+            field.setText(Double.toString(get()));
+        }
+
+		@Override
+		public Double toNumber(String s) {
+			if ( s == null || s.trim().isEmpty() ) return 0d;
+	    	String d = s.trim();
+	    	if ( d.endsWith("f") || d.endsWith("d") || d.endsWith("F") || d.endsWith("D") ) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
+	    		throw new NumberFormatException("There should be no alpha symbols"); //$NON-NLS-1$
+	    	}
+	    	return new Double(d);
+		};
+		
+    }
+ 
+    
+    static class LongValidator extends SimpleLongProperty implements NumericValidator<Long>{
+    	
+    	private NumericField field;
+    	
+    	public LongValidator(NumericField field) {
+    		super(field, "value", 0l); //$NON-NLS-1$
+    		this.field = field;
+		}
+    	
+    	@Override protected void invalidated() {
+            field.setText(Long.toString(get()));
+        }
+
+		@Override
+		public Long toNumber(String s) {
+			if ( s == null || s.trim().isEmpty() ) return 0l;
+	    	String d = s.trim();
+	    	return new Long(d);
+		};
+		
+    }    
+    
+    
+}
\ No newline at end of file
diff --git a/controlsfx/src/main/java/org/controlsfx/property/editor/PropertyEditor.java b/controlsfx/src/main/java/org/controlsfx/property/editor/PropertyEditor.java
new file mode 100644
index 0000000..58dbf36
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/property/editor/PropertyEditor.java
@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2013, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.property.editor;
+
+import org.controlsfx.control.PropertySheet;
+
+import javafx.scene.Node;
+
+/**
+ * The base interface for all editors used by the {@link PropertySheet} control. 
+ *
+ * @param <T> The type of the property that the PropertyEditor is responsible
+ *      for editing.
+ */
+public interface PropertyEditor<T> {
+
+    /**
+     * Returns the editor responsible for editing this property.
+     */
+    public Node getEditor();
+
+    /**
+     * Returns the current value in the editor - this may not be the value of
+     * the property itself!
+     */
+    public T getValue();
+
+    /**
+     * Sets the value to display in the editor - this may not be the value of 
+     * the property itself - and the property value will not change!
+     */
+    public void setValue(T value);
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/property/editor/package-info.java b/controlsfx/src/main/java/org/controlsfx/property/editor/package-info.java
new file mode 100644
index 0000000..67fd7df
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/property/editor/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * A package containing a number of useful editor classes related to the 
+ * {@link org.controlsfx.control.PropertySheet} control.
+ */
+package org.controlsfx.property.editor;
\ No newline at end of file
diff --git a/controlsfx/src/main/java/org/controlsfx/property/package-info.java b/controlsfx/src/main/java/org/controlsfx/property/package-info.java
new file mode 100644
index 0000000..366b5b5
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/property/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * A package containing a number of useful classes related to the 
+ * {@link org.controlsfx.control.PropertySheet} control.
+ */
+package org.controlsfx.property;
\ No newline at end of file
diff --git a/controlsfx/src/main/java/org/controlsfx/tools/Borders.java b/controlsfx/src/main/java/org/controlsfx/tools/Borders.java
new file mode 100644
index 0000000..542c2f4
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/tools/Borders.java
@@ -0,0 +1,807 @@
+/**
+ * Copyright (c) 2013, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.tools;
+
+import javafx.geometry.Insets;
+import javafx.scene.Node;
+import javafx.scene.control.Label;
+import javafx.scene.layout.*;
+import javafx.scene.paint.Color;
+
+import javax.swing.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * A utility class that allows you to wrap JavaFX {@link Node Nodes} with a border,
+ * in a way somewhat analogous to the Swing {@link BorderFactory} (although with
+ * less options as a lot of what the Swing BorderFactory offers resulted in 
+ * ugly borders!).
+ * 
+ * <p>The Borders class provides a fluent API for specifying the properties of
+ * each border. It is possible to create multiple borders around a Node simply
+ * by continuing to call additional methods before you call the final 
+ * {@link Borders#build()} method. To use the Borders class, you simply call
+ * {@link Borders#wrap(Node)}, passing in the Node you wish to wrap the border(s)
+ * around. 
+ * 
+ * <h3>Examples</h3>
+ * <p>Firstly, lets wrap a JavaFX Button node with a simple line border that looks
+ * like the following:
+ * 
+ * <br>
+ * <center><img src="borders-lineBorder.png" alt="Screenshot of Borders.LineBorders"></center>
+ * 
+ * <p>Here's the code:</p>
+ * 
+ * <pre>
+ * {@code
+ * Button button = new Button("Hello World!");
+ * Node wrappedButton = Borders.wrap(button).lineBorder().buildAll();
+ * }</pre>
+ * 
+ * <p>Easy, isn't it!? You can make the border look a little nicer by replacing
+ * the line border with an {@link EtchedBorders etched border}. An etched border
+ * has a subtle inner (or outer) line that makes the border stand out a bit more,
+ * like this:
+ * 
+ * <br>
+ * <center><img src="borders-etchedBorder.png" alt="Screenshot of Borders.EtchedBorders"></center>
+ * 
+ * <p>Now that's one good looking border! Here's the code:</p>
+ * 
+ * <pre>
+ * {@code
+ * Button button = new Button("Hello World!");
+ * Node wrappedButton = Borders.wrap(button).etchedBorder().buildAll();
+ * }</pre>
+ * 
+ * <p>In some circumstances you want to have multiple borders. For example,
+ * you might two line borders. That's easy:
+ * 
+ * <br>
+ * <center><img src="borders-twoLines.png" alt="Screenshot of two Borders.LineBorders"></center>
+ * 
+ * <pre>
+ * {@code
+ * Node wrappedButton = Borders.wrap(button)
+ *     .lineBorder().color(Color.RED).build()
+ *     .lineBorder().color(Color.GREEN).build()
+ *     .build();
+ * }</pre>
+ * 
+ * <p>You simply chain the borders together, going from inside to outside!</p>
+ * 
+ * <p>Because of all the configuration options it isn't possible to list all the 
+ * functionality of all the border types, so refer to the rest of the javadocs 
+ * for inspiration.</p>
+ */
+public final class Borders {
+    
+    /**************************************************************************
+     * 
+     * Static fields
+     * 
+     **************************************************************************/
+    
+    private static final Color DEFAULT_BORDER_COLOR = Color.DARKGRAY;
+    
+    
+    
+    /**************************************************************************
+     * 
+     * Internal fields
+     * 
+     **************************************************************************/
+    
+    private final Node node;
+    private final List<Border> borders;
+    
+    
+    
+    /**************************************************************************
+     * 
+     * Fluent API entry method(s)
+     * 
+     **************************************************************************/
+    
+    public static Borders wrap(Node n) {
+        return new Borders(n);
+    }
+    
+    
+    
+    /**************************************************************************
+     * 
+     * Private Constructor
+     * 
+     **************************************************************************/
+    
+    private Borders(Node n) { 
+        this.node = n;
+        this.borders = new ArrayList<>();
+    }
+    
+    
+    
+    /**************************************************************************
+     * 
+     * Fluent API
+     * 
+     **************************************************************************/
+    
+    /**
+     * Often times it is useful to have a bit of whitespace around a Node, to 
+     * separate it from what it is next to. Call this method to begin building
+     * a border that will wrap the node with a given amount of whitespace 
+     * (which can vary between the top, right, bottom, and left sides).
+     */
+    public EmptyBorders emptyBorder() {
+        return new EmptyBorders(this);
+    }
+    
+    /**
+     * The etched border look is essentially equivalent to the {@link #lineBorder()}
+     * look, except rather than one line, there are two. What is commonly done in
+     * this circumstance is that one of the lines is a very light colour (commonly
+     * white), which gives a nice etched look. Refer to the API in {@link EtchedBorders}
+     * for more information.
+     */
+    public EtchedBorders etchedBorder() {
+        return new EtchedBorders(this);
+    }
+    
+    /**
+     * Creates a nice, simple border around the node. Note that there are many
+     * configuration options in {@link LineBorders}, so explore it carefully.
+     */
+    public LineBorders lineBorder() {
+        return new LineBorders(this);
+    }
+    
+    /**
+     * Allows for developers to develop custom {@link Border} implementations,
+     * and to wrap them around a Node. Note that of course this is mostly 
+     * redundant (as you could just call {@link Border#wrap(Node)} directly).
+     * The only benefit is if you're creating a compound border consisting of 
+     * multiple borders, and you want your custom border included as part of 
+     * this.
+     */
+    public Borders addBorder(Border border) {
+        borders.add(border);
+        return this;
+    }
+
+    /**
+     * Returns the original node wrapped in zero or more borders, as specified
+     * using the fluent API.
+     */
+    public Node build() {
+        // we iterate through the borders list in reverse order
+        Node bundle = node;
+        for (int i = borders.size() - 1; i >= 0; i--) {
+            Border border = borders.get(i);
+            bundle = border.wrap(bundle);
+        }
+        return bundle;
+    }
+    
+    
+    
+    /**************************************************************************
+     * 
+     * Support classes
+     * 
+     **************************************************************************/
+    
+    /**
+     * A fluent API that is only indirectly instantiable via the {@link Borders} 
+     * fluent API, and which allows for an {@link Borders#emptyBorder() empty border}
+     * to be wrapped around a given Node.
+     */
+    public class EmptyBorders {
+        private final Borders parent;
+        
+        private double top;
+        private double right;
+        private double bottom;
+        private double left;
+        
+        // private on purpose - this class is not directly instantiable.
+        private EmptyBorders(Borders parent) { 
+            this.parent = parent;
+        }
+        
+        /**
+         * Specifies that the wrapped Node should have the given padding around
+         * all four sides of itself.
+         */
+        public EmptyBorders padding(double padding) {
+            return padding(padding, padding, padding, padding);
+        }
+
+        /**
+         * Specifies that the wrapped Node should be wrapped with the given
+         * padding for each of its four sides, going in the order top, right,
+         * bottom, and finally left.
+         */
+        public EmptyBorders padding(double top, double right, double bottom, double left) {
+            this.top = top;
+            this.right = right;
+            this.bottom = bottom;
+            this.left = left;
+            return this;
+        }
+        
+        /**
+         * Builds the {@link Border} and {@link Borders#addBorder(Border) adds it}
+         * to the list of borders to wrap around the given Node (which will be
+         * constructed and returned when {@link Borders#build()} is called.
+         */
+        public Borders build() {
+            parent.addBorder(new StrokeBorder(null, buildStroke()));
+            return parent;
+        }
+        
+        /**
+         * A convenience method, this is equivalent to calling 
+         * {@link #build()} followed by {@link Borders#build()}. In other words,
+         * calling this will return the original Node wrapped in all its borders 
+         * specified.
+         */
+        public Node buildAll() {
+            build();
+            return parent.build();
+        }
+        
+        private BorderStroke buildStroke() {
+            return new BorderStroke(
+                null, 
+                BorderStrokeStyle.NONE,
+                null, 
+                new BorderWidths(top, right, bottom, left),
+                Insets.EMPTY);
+        }
+    }
+    
+    /**
+     * A fluent API that is only indirectly instantiable via the {@link Borders} 
+     * fluent API, and which allows for an {@link Borders#etchedBorder() etched border}
+     * to be wrapped around a given Node.
+     */
+    public class EtchedBorders {
+        private final Borders parent;
+        
+        private String title;
+        private boolean raised = false;
+        
+        private double outerTopPadding = 10;
+        private double outerRightPadding = 10;
+        private double outerBottomPadding = 10;
+        private double outerLeftPadding = 10;
+        
+        private double innerTopPadding = 15;
+        private double innerRightPadding = 15;
+        private double innerBottomPadding = 15;
+        private double innerLeftPadding = 15;
+        
+        private double topLeftRadius = 0;
+        private double topRightRadius = 0;
+        private double bottomRightRadius = 0;
+        private double bottomLeftRadius = 0;
+        
+        private Color highlightColor = DEFAULT_BORDER_COLOR;
+        private Color shadowColor = Color.WHITE;
+        
+        // private on purpose - this class is not directly instantiable.
+        private EtchedBorders(Borders parent) { 
+            this.parent = parent;
+        }
+        
+        /**
+         * Specifies the highlight colour to use in the etched border.
+         */
+        public EtchedBorders highlight(Color highlight) {
+            this.highlightColor = highlight;
+            return this;
+        }
+        
+        /**
+         * Specifies the shadow colour to use in the etched border.
+         */
+        public EtchedBorders shadow(Color shadow) {
+            this.shadowColor = shadow;
+            return this;
+        }
+        
+        /**
+         * Specifies the order in which the highlight and shadow colours are
+         * placed. A raised etched border has the shadow colour on the outside
+         * of the border, whereas a non-raised (or lowered) etched border has 
+         * the shadow colour on the inside of the border.
+         */
+        public EtchedBorders raised() {
+            raised = true;
+            return this;
+        }
+        
+        /**
+         * If desired, this specifies the title text to show in this border.
+         */
+        public EtchedBorders title(String title) {
+            this.title = title;
+            return this;
+        }
+        
+        /**
+         * Specifies the outer padding of the four lines of this border.
+         */
+        public EtchedBorders outerPadding(double padding) {
+            return outerPadding(padding, padding, padding, padding);
+        }
+        
+        /**
+         * Specifies that the line wrapping the node should have outer padding
+         * as specified, with each padding being independently configured, going 
+         * in the order top, right, bottom, and left.
+         */
+        public EtchedBorders outerPadding(double topPadding, double rightPadding, double bottomPadding, double leftPadding) {
+            this.outerTopPadding = topPadding;
+            this.outerRightPadding = rightPadding;
+            this.outerBottomPadding = bottomPadding;
+            this.outerLeftPadding = leftPadding;
+            
+            return this;
+        }
+        
+        /**
+         * Specifies the inner padding of the four lines of this border.
+         */
+        public EtchedBorders innerPadding(double padding) {
+            return innerPadding(padding, padding, padding, padding);
+        }
+        
+        /**
+         * Specifies that the line wrapping the node should have inner padding
+         * as specified, with each padding being independently configured, going 
+         * in the order top, right, bottom, and left.
+         */
+        public EtchedBorders innerPadding(double topPadding, double rightPadding, double bottomPadding, double leftPadding) {
+            this.innerTopPadding = topPadding;
+            this.innerRightPadding = rightPadding;
+            this.innerBottomPadding = bottomPadding;
+            this.innerLeftPadding = leftPadding;
+            
+            return this;
+        }
+        
+        /**
+         * Specifies the radius of the four corners of the lines of this border.
+         */
+        public EtchedBorders radius(double radius) {
+            return radius(radius, radius, radius, radius);
+        }
+        
+        /**
+         * Specifies that the etched line wrapping the node should have corner radii
+         * as specified, with each radius being independently configured, going 
+         * in the order top-left, top-right, bottom-right, and finally bottom-left.
+         */
+        public EtchedBorders radius(double topLeft, double topRight, double bottomRight, double bottomLeft) {
+            this.topLeftRadius = topLeft;
+            this.topRightRadius = topRight;
+            this.bottomRightRadius = bottomRight;
+            this.bottomLeftRadius = bottomLeft;
+            return this;
+        }
+        
+        /**
+         * Builds the {@link Border} and {@link Borders#addBorder(Border) adds it}
+         * to the list of borders to wrap around the given Node (which will be
+         * constructed and returned when {@link Borders#build()} is called.
+         */
+        public Borders build() {
+            Color inner = raised ? shadowColor : highlightColor;
+            Color outer = raised ? highlightColor : shadowColor;
+            BorderStroke innerStroke = new BorderStroke(
+                    inner, 
+                    BorderStrokeStyle.SOLID, 
+                    new CornerRadii(topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius, false), 
+                    new BorderWidths(1));
+            BorderStroke outerStroke = new BorderStroke(
+                    outer, 
+                    BorderStrokeStyle.SOLID, 
+                    new CornerRadii(topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius, false), 
+                    new BorderWidths(1), 
+                    new Insets(1));
+            
+            BorderStroke outerPadding = new EmptyBorders(parent)
+                .padding(outerTopPadding, outerRightPadding, outerBottomPadding, outerLeftPadding)
+                .buildStroke();
+            
+            BorderStroke innerPadding = new EmptyBorders(parent)
+                .padding(innerTopPadding, innerRightPadding, innerBottomPadding, innerLeftPadding)
+                .buildStroke();
+            
+            parent.addBorder(new StrokeBorder(null, outerPadding));
+            parent.addBorder(new StrokeBorder(title, innerStroke, outerStroke));
+            parent.addBorder(new StrokeBorder(null, innerPadding));
+            
+            return parent;
+        }
+        
+        /**
+         * A convenience method, this is equivalent to calling 
+         * {@link #build()} followed by {@link Borders#build()}. In other words,
+         * calling this will return the original Node wrapped in all its borders 
+         * specified.
+         */
+        public Node buildAll() {
+            build();
+            return parent.build();
+        }
+    }
+    
+    /**
+     * A fluent API that is only indirectly instantiable via the {@link Borders} 
+     * fluent API, and which allows for a {@link Borders#lineBorder() line border}
+     * to be wrapped around a given Node.
+     */
+    public class LineBorders {
+        private final Borders parent;
+        
+        private String title;
+        
+        private BorderStrokeStyle strokeStyle = BorderStrokeStyle.SOLID;
+        
+        private Color topColor = DEFAULT_BORDER_COLOR;
+        private Color rightColor = DEFAULT_BORDER_COLOR;
+        private Color bottomColor = DEFAULT_BORDER_COLOR;
+        private Color leftColor = DEFAULT_BORDER_COLOR;
+        
+        private double outerTopPadding = 10;
+        private double outerRightPadding = 10;
+        private double outerBottomPadding = 10;
+        private double outerLeftPadding = 10;
+        
+        private double innerTopPadding = 15;
+        private double innerRightPadding = 15;
+        private double innerBottomPadding = 15;
+        private double innerLeftPadding = 15;
+        
+        private double topThickness = 1;
+        private double rightThickness = 1;
+        private double bottomThickness = 1;
+        private double leftThickness = 1;
+
+        private double topLeftRadius = 0;
+        private double topRightRadius = 0;
+        private double bottomRightRadius = 0;
+        private double bottomLeftRadius = 0;
+        
+        // private on purpose - this class is not directly instantiable.
+        private LineBorders(Borders parent) { 
+            this.parent = parent;
+        }
+        
+        /**
+         * Specifies the colour to use for all four sides of this border.
+         */
+        public LineBorders color(Color color) {
+            return color(color, color, color, color);
+        }
+
+        /**
+         * Specifies that the wrapped Node should be wrapped with the given
+         * colours for each of its four sides, going in the order top, right,
+         * bottom, and finally left.
+         */
+        public LineBorders color(Color topColor, Color rightColor, Color bottomColor, Color leftColor) {
+            this.topColor = topColor;
+            this.rightColor = rightColor;
+            this.bottomColor = bottomColor;
+            this.leftColor = leftColor;
+            return this;
+        }
+        
+        /**
+         * Specifies which {@link BorderStrokeStyle} to use for this line border.
+         * By default this is {@link BorderStrokeStyle#SOLID}, but you can use
+         * any other style (such as {@link BorderStrokeStyle#DASHED}, 
+         * {@link BorderStrokeStyle#DOTTED}, or a custom style built using
+         * {@link BorderStrokeStyle#BorderStrokeStyle(javafx.scene.shape.StrokeType, javafx.scene.shape.StrokeLineJoin, javafx.scene.shape.StrokeLineCap, double, double, List)}.
+         */
+        public LineBorders strokeStyle(BorderStrokeStyle strokeStyle) {
+            this.strokeStyle = strokeStyle;
+            return this;
+        }
+        
+        /**
+         * Specifies the inner padding of the four lines of this border.
+         */
+        public LineBorders outerPadding(double padding) {
+            return outerPadding(padding, padding, padding, padding);
+        }
+        
+        /**
+         * Specifies that the line wrapping the node should have outer padding
+         * as specified, with each padding being independently configured, going 
+         * in the order top, right, bottom, and left.
+         */
+        public LineBorders outerPadding(double topPadding, double rightPadding, double bottomPadding, double leftPadding) {
+            this.outerTopPadding = topPadding;
+            this.outerRightPadding = rightPadding;
+            this.outerBottomPadding = bottomPadding;
+            this.outerLeftPadding = leftPadding;
+            
+            return this;
+        }
+        
+        /**
+         * Specifies the outer padding of the four lines of this border.
+         */
+        public LineBorders innerPadding(double padding) {
+            return innerPadding(padding, padding, padding, padding);
+        }
+        
+        /**
+         * Specifies that the line wrapping the node should have inner padding
+         * as specified, with each padding being independently configured, going 
+         * in the order top, right, bottom, and left.
+         */
+        public LineBorders innerPadding(double topPadding, double rightPadding, double bottomPadding, double leftPadding) {
+            this.innerTopPadding = topPadding;
+            this.innerRightPadding = rightPadding;
+            this.innerBottomPadding = bottomPadding;
+            this.innerLeftPadding = leftPadding;
+            
+            return this;
+        }
+        
+        /**
+         * Specifies the thickness of the line to use on all four sides of this
+         * border.
+         */
+        public LineBorders thickness(double thickness) {
+            return thickness(thickness, thickness, thickness, thickness);
+        }
+        
+        /**
+         * Specifies that the wrapped Node should be wrapped with the given
+         * line thickness for each of its four sides, going in the order top, right,
+         * bottom, and finally left.
+         */
+        public LineBorders thickness(double topThickness, double rightThickness, double bottomThickness, double leftThickness) {
+            this.topThickness = topThickness;
+            this.rightThickness = rightThickness;
+            this.bottomThickness = bottomThickness;
+            this.leftThickness = leftThickness;
+            return this;
+        }
+        
+        /**
+         * Specifies the radius of the four corners of the line of this border.
+         */
+        public LineBorders radius(double radius) {
+            return radius(radius, radius, radius, radius);
+        }
+        
+        /**
+         * Specifies that the line wrapping the node should have corner radii
+         * as specified, with each radius being independently configured, going 
+         * in the order top-left, top-right, bottom-right, and finally bottom-left.
+         */
+        public LineBorders radius(double topLeft, double topRight, double bottomRight, double bottomLeft) {
+            this.topLeftRadius = topLeft;
+            this.topRightRadius = topRight;
+            this.bottomRightRadius = bottomRight;
+            this.bottomLeftRadius = bottomLeft;
+            return this;
+        }
+        
+        /**
+         * If desired, this specifies the title text to show in this border.
+         */
+        public LineBorders title(String title) {
+            this.title = title;
+            return this;
+        }
+        
+        /**
+         * Builds the {@link Border} and {@link Borders#addBorder(Border) adds it}
+         * to the list of borders to wrap around the given Node (which will be
+         * constructed and returned when {@link Borders#build()} is called.
+         */
+        public Borders build() {
+            BorderStroke borderStroke = new BorderStroke(
+                    topColor, rightColor, bottomColor, leftColor, 
+                    strokeStyle, strokeStyle, strokeStyle, strokeStyle,  
+                    new CornerRadii(topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius, false), 
+                    new BorderWidths(topThickness, rightThickness, bottomThickness, leftThickness),
+                    null);
+            
+            BorderStroke outerPadding = new EmptyBorders(parent)
+                .padding(outerTopPadding, outerRightPadding, outerBottomPadding, outerLeftPadding)
+                .buildStroke();
+            
+            BorderStroke innerPadding = new EmptyBorders(parent)
+                .padding(innerTopPadding, innerRightPadding, innerBottomPadding, innerLeftPadding)
+                .buildStroke();
+            
+            parent.addBorder(new StrokeBorder(null, outerPadding));
+            parent.addBorder(new StrokeBorder(title, borderStroke));
+            parent.addBorder(new StrokeBorder(null, innerPadding));
+            
+            return parent;
+        }
+        
+        /**
+         * A convenience method, this is equivalent to calling 
+         * {@link #build()} followed by {@link Borders#build()}. In other words,
+         * calling this will return the original Node wrapped in all its borders 
+         * specified.
+         */
+        public Node buildAll() {
+            build();
+            return parent.build();
+        }
+    }
+    
+    
+    
+    /**************************************************************************
+     * 
+     * Support interfaces
+     * 
+     **************************************************************************/
+    
+    /**
+     * The public interface used by the {@link Borders} API to wrap nodes with
+     * zero or more Border implementations. ControlsFX ships with a few 
+     * Border implementations (current {@link EmptyBorders}, {@link LineBorders},
+     * and {@link EtchedBorders}). As noted in {@link Borders#addBorder(Border)},
+     * this interface is relatively pointless, unless you plan to wrap a node
+     * with multiple borders and you want to use a custom {@link Border} 
+     * implementation for at least one border. In this case, you can simply 
+     * call {@link Borders#addBorder(Border)} with your custom border, when 
+     * appropriate.
+     */
+    @FunctionalInterface
+    public static interface Border {
+        
+        /**
+         * Given a {@link Node}, this method should return a Node that contains
+         * the original Node and also has wrapped it with an appropriate border.
+         */
+        public Node wrap(Node n);
+    }
+    
+    
+    
+    /**************************************************************************
+     * 
+     * Private support classes
+     * 
+     **************************************************************************/
+    
+    // --- Border implementations
+    private static class StrokeBorder implements Border {
+        private static final int TITLE_PADDING = 3;
+        private static final double GAP_PADDING = TITLE_PADDING * 2 - 1;
+        
+        private final String title;
+        private final BorderStroke[] borderStrokes;
+
+        public StrokeBorder(String title, BorderStroke... borderStrokes) {
+            this.title = title;
+            this.borderStrokes = borderStrokes;
+        }
+
+        @Override public Node wrap(final Node n) {
+            StackPane pane = new StackPane() {
+                Label titleLabel;
+                
+                {
+                    // add in the node we are wrapping
+                    getChildren().add(n);
+                    
+                    
+                    // if the title string is set, then also add in the title label
+                    if (title != null) {
+                        titleLabel = new Label(title);
+    
+                        // give the text a bit of space on the left...
+                        titleLabel.setPadding(new Insets(0, 0, 0, TITLE_PADDING));
+                        getChildren().add(titleLabel);
+                    }
+                }
+
+                @Override protected void layoutChildren() {
+                    super.layoutChildren();
+
+                    if (titleLabel != null) {
+                        // layout the title label
+                        final double labelHeight = titleLabel.prefHeight(-1);
+                        final double labelWidth = titleLabel.prefWidth(labelHeight) + TITLE_PADDING;
+                        titleLabel.resize(labelWidth, labelHeight);
+                        titleLabel.relocate(TITLE_PADDING * 2, -labelHeight / 2.0 - 1);
+
+                        List<BorderStroke> newBorderStrokes = new ArrayList<>(2);
+
+                        // create a line gap for the title label
+                        for (BorderStroke bs : borderStrokes) {
+                            List<Double> dashList = new ArrayList<>();
+
+                            // Create a dash list for the line gap or add it at the beginning of an existing dash list. This gap should be wide enough for the title label.
+                            if (bs.getTopStyle().getDashArray().isEmpty())
+                                dashList.addAll(Arrays.asList(GAP_PADDING, labelWidth, Double.MAX_VALUE));
+                            else { // dash pattern exists
+                                // insert gap in existing dash pattern and multiply original pattern so that gap does not show more then once
+                                double origDashWidth = bs.getTopStyle().getDashArray().stream().mapToDouble(d -> d).sum();
+
+                                if (origDashWidth > GAP_PADDING) {
+                                    dashList.add(GAP_PADDING);
+                                    dashList.add(labelWidth);
+                                } else { // need to insert dash pattern before the gap
+                                    int no = (int) (GAP_PADDING / origDashWidth);
+
+                                    for (int i = 0; i < no; i++)
+                                        dashList.addAll(bs.getTopStyle().getDashArray());
+
+                                    if ((dashList.size() & 1) == 0) // if size is even number, add one more element because gap must be at odd index to be transparent
+                                        dashList.add(0d);
+
+                                    dashList.add(labelWidth + GAP_PADDING - no * origDashWidth);
+                                }
+
+                                for (int i = 0; i < (getWidth() - labelWidth - origDashWidth) / origDashWidth; i++)
+                                    dashList.addAll(bs.getTopStyle().getDashArray());
+                            }
+
+                            // create new border stroke style for the top border line with new dash list
+                            BorderStrokeStyle topStrokeStyle = new BorderStrokeStyle(
+                                bs.getTopStyle().getType(), bs.getTopStyle().getLineJoin(), bs.getTopStyle().getLineCap(),
+                                bs.getTopStyle().getMiterLimit(), bs.getTopStyle().getDashOffset(), dashList);
+
+                            // change existing border stroke to utilize new top border line stroke style
+                            newBorderStrokes.add(new BorderStroke(
+                                bs.getTopStroke(), bs.getRightStroke(), bs.getBottomStroke(), bs.getLeftStroke(),
+                                topStrokeStyle, bs.getRightStyle(), bs.getBottomStyle(), bs.getLeftStyle(),
+                                bs.getRadii(), bs.getWidths(), null));
+                        }
+
+                        setBorder(new javafx.scene.layout.Border(newBorderStrokes.toArray(new BorderStroke[newBorderStrokes.size()])));
+                    }
+                }
+            };
+
+            pane.setBorder(new javafx.scene.layout.Border(borderStrokes));
+            return pane;
+        }
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/tools/Duplicatable.java b/controlsfx/src/main/java/org/controlsfx/tools/Duplicatable.java
new file mode 100644
index 0000000..1fd50fd
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/tools/Duplicatable.java
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2013, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.tools;
+
+import javafx.scene.Node;
+
+/**
+ * An interface used in ControlsFX to represent something that can be duplicated,
+ * as in the JavaFX scenegraph it is not possible to insert the same 
+ * {@link Node} in multiple locations at the same time. Therefore, to work 
+ * around this the node may implement this interface to duplicate itself.
+ *
+ * @param <T> The node type
+ */
+ at FunctionalInterface
+public interface Duplicatable<T> {
+   T duplicate();
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/tools/Platform.java b/controlsfx/src/main/java/org/controlsfx/tools/Platform.java
new file mode 100644
index 0000000..3a0eae7
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/tools/Platform.java
@@ -0,0 +1,84 @@
+/**
+ * Copyright (c) 2013, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.tools;
+
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+
+/**
+ * Represents operating system with appropriate properties 
+ *
+ */
+public enum Platform {
+    
+    WINDOWS("windows"), //$NON-NLS-1$
+    OSX("mac"), //$NON-NLS-1$
+    UNIX("unix"), //$NON-NLS-1$
+    UNKNOWN(""); //$NON-NLS-1$
+    
+    private static Platform current = getCurrentPlatform();
+    
+    private String platformId;
+    
+    Platform( String platformId ) {
+        this.platformId = platformId;
+    }
+    
+    /**
+     * Returns platform id. Usually used to specify platform dependent styles
+     * @return platform id
+     */
+    public String getPlatformId() {
+        return platformId;
+    }
+    
+    /**
+     * @return the current OS.
+     */
+    public static Platform getCurrent() {
+        return current;
+    }
+    
+    private static Platform getCurrentPlatform() {
+        String osName = System.getProperty("os.name");
+        if ( osName.startsWith("Windows") ) return WINDOWS;        
+        if ( osName.startsWith("Mac") )     return OSX;
+        if ( osName.startsWith("SunOS") )   return UNIX;
+        if ( osName.startsWith("Linux") ) {
+            String javafxPlatform = AccessController.doPrivileged(new PrivilegedAction<String>() {
+                @Override
+                public String run() {
+                    return System.getProperty("javafx.platform");
+                }
+            });
+            if (! ( "android".equals(javafxPlatform) || "Dalvik".equals(System.getProperty("java.vm.name")) ) ) // if not Android
+                return UNIX;
+        }
+        return UNKNOWN;
+    }
+    
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/tools/SVGLoader.java b/controlsfx/src/main/java/org/controlsfx/tools/SVGLoader.java
new file mode 100644
index 0000000..c25ecb1
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/tools/SVGLoader.java
@@ -0,0 +1,227 @@
+/**
+ * Copyright (c) 2013, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.tools;
+
+import java.net.URL;
+
+import javafx.beans.value.ChangeListener;
+import javafx.concurrent.Worker.State;
+import javafx.geometry.Rectangle2D;
+import javafx.scene.Scene;
+import javafx.scene.SnapshotParameters;
+import javafx.scene.SnapshotResult;
+import javafx.scene.image.ImageView;
+import javafx.scene.image.WritableImage;
+import javafx.scene.paint.Color;
+import javafx.scene.web.WebEngine;
+import javafx.scene.web.WebView;
+import javafx.stage.Stage;
+import javafx.util.Callback;
+
+import com.sun.javafx.webkit.Accessor;
+import com.sun.webkit.WebPage;
+
+/**
+ * Convenience class that will attempt to load a given URL as an .svg file.
+ */
+class SVGLoader {
+    
+    private SVGLoader() {
+        // no-op
+    }
+
+    /**
+     * This method will attempt to load the given svgImage URL into an ImageView
+     * node that will be provided asynchronously via the provided 
+     * {@link Callback}, and it will be sized to the given prefWidth / prefHeight.
+     * 
+     * <p>Note that it is valid to pass in -1 to prefWidth and / or prefHeight as
+     * an indicator to the SVG loader. If both values are -1, the default width
+     * of the SVG will be used. If one of the values is -1, then the SVG will
+     * be sized to ensure that it remains proportional.
+     * 
+     * @param svgImage The image to load.
+     * @param prefWidth The preferred width of the image when loaded, or -1 if 
+     *      there is no preferred width.
+     * @param prefHeight The preferred height of the image when loaded, or -1 if 
+     *      there is no preferred height.
+     * @param callback The {@link Callback} that will be called when the SVG 
+     *      image is loaded, where the {@link ImageView} containing the rendered
+     *      image will be available.
+     */
+    public static void loadSVGImage(final URL svgImage, 
+                                    final double prefWidth, 
+                                    final double prefHeight, 
+                                    final Callback<ImageView, Void> callback) {
+        loadSVGImage(svgImage, prefWidth, prefHeight, callback, null);
+    }
+    
+    /**
+     * This method will attempt to load the given svgImage URL into the provided
+     * {@link WritableImage}, with the SVG scaled to fit the size of the
+     * WritableImage.
+     * 
+     * @param svgImage The image to load.
+     * @param outputImage The location to write the loaded image once it has 
+     *      been rendered (it will not happen synchronously).
+     * @throws NullPointerException The outputImage argument must be non-null.
+     */
+    public static void loadSVGImage(final URL svgImage, 
+                                    final WritableImage outputImage) {
+        if (outputImage == null) {
+            throw new NullPointerException("outputImage can not be null"); //$NON-NLS-1$
+        }
+        final double w = outputImage.getWidth();
+        final double h = outputImage.getHeight();
+        loadSVGImage(svgImage, w, h, null, outputImage);
+    }
+    
+    public static void loadSVGImage(final URL svgImage, 
+                                    final double prefWidth, 
+                                    final double prefHeight, 
+                                    final Callback<ImageView, Void> callback, 
+                                    final WritableImage outputImage) {
+        final WebView view = new WebView();
+        final WebEngine eng = view.getEngine();
+        
+        // using non-public API to ensure background transparency
+        final WebPage webPage = Accessor.getPageFor(eng);
+        webPage.setBackgroundColor(webPage.getMainFrame(), 0xffffff00);
+        webPage.setOpaque(webPage.getMainFrame(), false); 
+        // end of non-public API
+
+        // temporary scene / stage
+        final Scene scene = new Scene(view);
+        final Stage stage = new Stage();
+        stage.setScene(scene);
+        stage.setWidth(0);
+        stage.setHeight(0);
+        stage.setOpacity(0);
+        stage.show();
+        
+//        String svgString = readFile(svgImage);
+
+        String content =
+        "<html>" + //$NON-NLS-1$
+            "<body style=\"margin-top: 0px; margin-bottom: 30px; margin-left: 0px; margin-right: 0px; padding: 0;\">" + //$NON-NLS-1$
+//                "<div style=\"width: " + prefWidth + "; height: " + prefHeight + ";\">" +
+                    "<img id=\"svgImage\" style=\"display: block;float: top;\" width=\"" + prefWidth + "\" height=\"" + prefHeight + "\" src=\"" + svgImage.toExternalForm() + "\" />" + //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
+//                    svgString +
+//                "</div>" +
+            "</body>" + //$NON-NLS-1$
+        "</head>"; //$NON-NLS-1$
+                
+        
+        
+        eng.loadContent(content);
+        
+        eng.getLoadWorker().stateProperty().addListener(new ChangeListener<State>() {
+            @Override public void changed(javafx.beans.value.ObservableValue<? extends State> o, State oldValue, State newValue) {
+                if (newValue == State.SUCCEEDED) {
+                    
+//                    HTMLImageElement svgImageElement = (HTMLImageElement) getSvgDom(eng);
+//                    System.out.println(svgImageElement.getAttributes());
+                    
+                    final double svgWidth = prefWidth >= 0 ? prefWidth : getSvgWidth(eng);
+                    final double svgHeight = prefHeight >= 0 ? prefWidth : getSvgHeight(eng);
+                    
+                    SnapshotParameters params = new SnapshotParameters();
+                    params.setFill(Color.TRANSPARENT);
+                    params.setViewport(new Rectangle2D(0, 0, svgWidth, svgHeight));
+                    
+                    view.snapshot(new Callback<SnapshotResult, Void>() {
+                        @Override public Void call(SnapshotResult param) {
+                            WritableImage snapshot = param.getImage();
+                            ImageView image = new ImageView(snapshot);
+                            
+                            if (callback != null) {
+                                callback.call(image);
+                            }
+                            
+                            stage.hide();
+                            return null;
+                        }
+                    }, params, outputImage);
+                }
+            }
+        });
+    }
+    
+//    private static String readFile(URL url) {
+//        try {
+//            FileInputStream stream = new FileInputStream(new File(url.toURI()));
+//            try {
+//                FileChannel fc = stream.getChannel();
+//                MappedByteBuffer bb = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size());
+//                return Charset.defaultCharset().decode(bb).toString();
+//            }
+//            finally {
+//                stream.close();
+//            }
+//        } catch (Exception e) {
+//            e.printStackTrace();
+//        }
+//        return null;
+//    }
+    
+    private static double getSvgWidth(WebEngine webEngine) {
+        Object result = getSvgDomProperty(webEngine, "offsetWidth"); //$NON-NLS-1$
+        if (result instanceof Integer) {
+            return (Integer) result;
+        }
+        return -1;
+    }
+    
+    private static double getSvgHeight(WebEngine webEngine) {
+        Object result = getSvgDomProperty(webEngine, "offsetHeight"); //$NON-NLS-1$
+        if (result instanceof Integer) {
+            return (Integer) result;
+        }
+        return -1;
+    }
+    
+    private static Object getSvgDomProperty(final WebEngine webEngine, final String property) {
+        return webEngine.executeScript("document.getElementById('svgImage')." + property); //$NON-NLS-1$
+    }
+
+//    private static HTMLImageElement getSvgDom(WebEngine webEngine) {
+//        return (HTMLImageElement) webEngine.executeScript("document.getElementById('svgImage')");
+//    }
+//    
+//    private static void printDocument(Document doc, OutputStream out) throws IOException, TransformerException {
+//        TransformerFactory tf = TransformerFactory.newInstance();
+//        Transformer transformer = tf.newTransformer();
+//        transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
+//        transformer.setOutputProperty(OutputKeys.METHOD, "xml");
+//        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+//        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
+//        transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
+//
+//        transformer.transform(new DOMSource(doc), 
+//             new StreamResult(new OutputStreamWriter(out, "UTF-8")));
+//    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/tools/Utils.java b/controlsfx/src/main/java/org/controlsfx/tools/Utils.java
new file mode 100644
index 0000000..a7b236d
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/tools/Utils.java
@@ -0,0 +1,113 @@
+/**
+ * Copyright (c) 2014, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.tools;
+
+import java.util.Iterator;
+
+import javafx.scene.Node;
+import javafx.stage.PopupWindow;
+import javafx.stage.Window;
+
+public class Utils {
+
+    /**
+     * Will return a {@link Window} from an object if any can be found. {@code null}
+     * value can be given, the program will then try to find the focused window
+     * among those available.
+     * 
+     * @param owner the object whose window is to be found.
+     * @return the window of the given object.
+     */
+    public static Window getWindow(Object owner) throws IllegalArgumentException {
+        if (owner == null) {
+            Window window = null;
+            // lets just get the focused stage and show the dialog in there
+            @SuppressWarnings("deprecation")
+            Iterator<Window> windows = Window.impl_getWindows();
+            while (windows.hasNext()) {
+                window = windows.next();
+                if (window.isFocused() && !(window instanceof PopupWindow)) {
+                    break;
+                }
+            }
+            return window;
+        } else if (owner instanceof Window) {
+            return (Window) owner;
+        } else if (owner instanceof Node) {
+            return ((Node) owner).getScene().getWindow();
+        } else {
+            throw new IllegalArgumentException("Unknown owner: " + owner.getClass()); //$NON-NLS-1$
+        }
+    }
+    
+    /**
+     * Return a letter (just like Excel) associated with the number. When the
+     * number is under 26, a simple letter is returned. When the number is
+     * superior, concatenated letters are returned.
+     * 
+     * 
+     * For example: 0 -> A 1 -> B 26 -> AA 32 -> AG 45 -> AT
+     * 
+     * 
+     * @param number the number whose Excel Letter is to be found.
+     * @return a letter (like) associated with the number.
+     */
+    public static final String getExcelLetterFromNumber(int number) {
+        String letter = ""; //$NON-NLS-1$
+        // Repeatedly divide the number by 26 and convert the
+        // remainder into the appropriate letter.
+        while (number >= 0) {
+            final int remainder = number % 26;
+            letter = (char) (remainder + 'A') + letter;
+            number = number / 26 - 1;
+        }
+
+        return letter;
+    }
+	
+    /**
+     * Simple utility function which clamps the given value to be strictly
+     * between the min and max values.
+     */
+    public static double clamp(double min, double value, double max) {
+        if (value < min) return min;
+        if (value > max) return max;
+        return value;
+    }
+
+    /**
+     * Utility function which returns either {@code less} or {@code more}
+     * depending on which {@code value} is closer to. If {@code value}
+     * is perfectly between them, then either may be returned.
+     */
+    public static double nearest(double less, double value, double more) {
+        double lessDiff = value - less;
+        double moreDiff = more - value;
+        if (lessDiff < moreDiff) return less;
+        return more;
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/tools/ValueExtractor.java b/controlsfx/src/main/java/org/controlsfx/tools/ValueExtractor.java
new file mode 100644
index 0000000..b64b4ed
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/tools/ValueExtractor.java
@@ -0,0 +1,170 @@
+/**
+ * Copyright (c) 2014 ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.tools;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Predicate;
+
+import javafx.beans.value.ObservableValue;
+import javafx.collections.FXCollections;
+import javafx.scene.Node;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.ChoiceBox;
+import javafx.scene.control.ColorPicker;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.Control;
+import javafx.scene.control.DatePicker;
+import javafx.scene.control.ListView;
+import javafx.scene.control.MultipleSelectionModel;
+import javafx.scene.control.RadioButton;
+import javafx.scene.control.SelectionMode;
+import javafx.scene.control.Slider;
+import javafx.scene.control.TableView;
+import javafx.scene.control.TextInputControl;
+import javafx.scene.control.TreeTableView;
+import javafx.scene.control.TreeView;
+import javafx.util.Callback;
+
+public class ValueExtractor {
+	
+	private static class ObservableValueExtractor {
+
+        public final Predicate<Control> applicability;
+		public final Callback<Control, ObservableValue<?>> extraction;
+
+        public ObservableValueExtractor( Predicate<Control> applicability, Callback<Control, ObservableValue<?>> extraction ) {
+            this.applicability = Objects.requireNonNull(applicability);
+            this.extraction    = Objects.requireNonNull(extraction);
+        }
+
+    }
+
+    private static List<ObservableValueExtractor> extractors = FXCollections.observableArrayList(); 
+
+    /**
+     * Add "obervable value extractor" for custom controls.
+     * @param test applicability test
+     * @param extract extraction of observable value
+     */
+    public static void addObservableValueExtractor( Predicate<Control> test, Callback<Control, ObservableValue<?>> extract ) {
+        extractors.add( new ObservableValueExtractor(test, extract));
+    }
+
+    static {
+        addObservableValueExtractor( c -> c instanceof TextInputControl, c -> ((TextInputControl)c).textProperty());
+        addObservableValueExtractor( c -> c instanceof ComboBox,         c -> ((ComboBox<?>)c).valueProperty());
+        addObservableValueExtractor( c -> c instanceof ChoiceBox,        c -> ((ChoiceBox<?>)c).valueProperty());
+        addObservableValueExtractor( c -> c instanceof CheckBox,         c -> ((CheckBox)c).selectedProperty());
+        addObservableValueExtractor( c -> c instanceof Slider,           c -> ((Slider)c).valueProperty());
+        addObservableValueExtractor( c -> c instanceof ColorPicker,      c -> ((ColorPicker)c).valueProperty());
+        addObservableValueExtractor( c -> c instanceof DatePicker,       c -> ((DatePicker)c).valueProperty());
+
+        addObservableValueExtractor( c -> c instanceof ListView,         c -> ((ListView<?>)c).itemsProperty());
+        addObservableValueExtractor( c -> c instanceof TableView,        c -> ((TableView<?>)c).itemsProperty());
+
+        // FIXME: How to listen for TreeView changes???
+        //addObservableValueExtractor( c -> c instanceof TreeView,         c -> ((TreeView<?>)c).Property());
+    }
+	
+	
+	
+    public static final Optional<Callback<Control, ObservableValue<?>>> getObservableValueExtractor(final Control c) {
+        for( ObservableValueExtractor e: extractors ) {
+            if ( e.applicability.test(c)) return Optional.of(e.extraction);
+        }
+        return Optional.empty();
+    }
+    
+    
+    private static class NodeValueExtractor {
+
+        public final Predicate<Node> applicability;
+		public final Callback<Node, Object> extraction;
+
+        public NodeValueExtractor( Predicate<Node> applicability, Callback<Node, Object> extraction ) {
+            this.applicability = Objects.requireNonNull(applicability);
+            this.extraction    = Objects.requireNonNull(extraction);
+        }
+
+    }
+    
+
+    private static final List<NodeValueExtractor> valueExtractors = FXCollections.observableArrayList();
+    
+    static {
+        addValueExtractor( n -> n instanceof CheckBox,         cb -> ((CheckBox)cb).isSelected());
+        addValueExtractor( n -> n instanceof ChoiceBox,        cb -> ((ChoiceBox<?>)cb).getValue());
+        addValueExtractor( n -> n instanceof ComboBox,         cb -> ((ComboBox<?>)cb).getValue());
+        addValueExtractor( n -> n instanceof DatePicker,       dp -> ((DatePicker)dp).getValue());
+        addValueExtractor( n -> n instanceof RadioButton,      rb -> ((RadioButton)rb).isSelected());
+        addValueExtractor( n -> n instanceof Slider,           sl -> ((Slider)sl).getValue());
+        addValueExtractor( n -> n instanceof TextInputControl, ta -> ((TextInputControl)ta).getText());
+        
+        addValueExtractor( n -> n instanceof ListView, lv -> {
+            MultipleSelectionModel<?> sm = ((ListView<?>)lv).getSelectionModel();
+            return sm.getSelectionMode() == SelectionMode.MULTIPLE ? sm.getSelectedItems() : sm.getSelectedItem();
+        });
+        addValueExtractor( n -> n instanceof TreeView, tv -> {
+            MultipleSelectionModel<?> sm = ((TreeView<?>)tv).getSelectionModel();
+            return sm.getSelectionMode() == SelectionMode.MULTIPLE ? sm.getSelectedItems() : sm.getSelectedItem();
+        });
+        addValueExtractor( n -> n instanceof TableView, tv -> {
+            MultipleSelectionModel<?> sm = ((TableView<?>)tv).getSelectionModel();
+            return sm.getSelectionMode() == SelectionMode.MULTIPLE ? sm.getSelectedItems() : sm.getSelectedItem();
+        });
+        addValueExtractor( n -> n instanceof TreeTableView, tv -> {
+            MultipleSelectionModel<?> sm = ((TreeTableView<?>)tv).getSelectionModel();
+            return sm.getSelectionMode() == SelectionMode.MULTIPLE ? sm.getSelectedItems() : sm.getSelectedItem();
+        });
+    }
+    
+    private ValueExtractor() {
+        // no-op
+    }
+    
+    public static void addValueExtractor(Predicate<Node> test, Callback<Node, Object> extractor) {
+        valueExtractors.add(new NodeValueExtractor(test, extractor));
+    }
+    
+    /**
+     * Attempts to return a value for the given Node. This is done by checking
+     * the map of value extractors, contained within this class. This
+     * map contains value extractors for common UI controls, but more extractors
+     * can be added by calling {@link #addObservableValueExtractor(Predicate, Callback)}.
+     * 
+     * @param n The node from whom a value will hopefully be extracted.
+     * @return The value of the given node.
+     */
+    public static Object getValue(Node n) {
+    	for( NodeValueExtractor nve: valueExtractors ) {
+            if ( nve.applicability.test(n)) return nve.extraction.call(n);
+        }
+        return null;
+    }
+}
\ No newline at end of file
diff --git a/controlsfx/src/main/java/org/controlsfx/tools/package-info.java b/controlsfx/src/main/java/org/controlsfx/tools/package-info.java
new file mode 100644
index 0000000..af1fe9a
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/tools/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * A package containing a number of useful utility methods.
+ */
+package org.controlsfx.tools;
\ No newline at end of file
diff --git a/controlsfx/src/main/java/org/controlsfx/validation/Severity.java b/controlsfx/src/main/java/org/controlsfx/validation/Severity.java
new file mode 100644
index 0000000..ab51e3e
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/validation/Severity.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.validation;
+
+/**
+ * Defines severity of validation messages 
+ */
+public enum Severity {
+
+    ERROR,
+    WARNING
+
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/validation/SimpleValidationMessage.java b/controlsfx/src/main/java/org/controlsfx/validation/SimpleValidationMessage.java
new file mode 100644
index 0000000..314d67b
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/validation/SimpleValidationMessage.java
@@ -0,0 +1,95 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.validation;
+
+import javafx.scene.control.Control;
+
+/**
+ * Internal implementation of simple validation message 
+ */
+class SimpleValidationMessage implements ValidationMessage {
+
+    private final String text;
+    private final Severity severity;
+    private final Control target;
+
+    public SimpleValidationMessage( Control target, String text, Severity severity ) {
+        this.text = text;
+        this.severity = severity == null? Severity.ERROR: severity;
+        this.target = target;
+    }
+
+    @Override public Control getTarget() {
+        return target;
+    }
+
+    @Override public String getText() {
+        return text;
+    }
+
+    @Override public Severity getSeverity() {
+        return severity;
+    }
+
+    @Override public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result
+                + ((severity == null) ? 0 : severity.hashCode());
+        result = prime * result + ((target == null) ? 0 : target.hashCode());
+        result = prime * result + ((text == null) ? 0 : text.hashCode());
+        return result;
+    }
+
+    @Override public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        SimpleValidationMessage other = (SimpleValidationMessage) obj;
+        if (severity != other.severity)
+            return false;
+        if (target == null) {
+            if (other.target != null)
+                return false;
+        } else if (!target.equals(other.target))
+            return false;
+        if (text == null) {
+            if (other.text != null)
+                return false;
+        } else if (!text.equals(other.text))
+            return false;
+        return true;
+    }
+
+    @Override public String toString() {
+        return String.format("%s(%s)", severity, text); //$NON-NLS-1$
+    }
+
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/validation/ValidationMessage.java b/controlsfx/src/main/java/org/controlsfx/validation/ValidationMessage.java
new file mode 100644
index 0000000..4b836d6
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/validation/ValidationMessage.java
@@ -0,0 +1,91 @@
+/**
+ * Copyright (c) 2014, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.validation;
+
+import java.util.Comparator;
+
+import javafx.scene.control.Control;
+
+/**
+ * Interface to define basic contract for validation message  
+ */
+public interface ValidationMessage extends Comparable<ValidationMessage>{
+
+	public static final Comparator<ValidationMessage> COMPARATOR = new Comparator<ValidationMessage>() {
+
+		@Override
+		public int compare(ValidationMessage vm1, ValidationMessage vm2) {
+			if ( vm1 == vm2 ) return  0;
+			if ( vm1 == null) return  1;
+			if ( vm2 == null) return -1;
+			return vm1.compareTo(vm2);
+		}
+	};
+	
+    /**
+     * Message text
+     * @return message text
+     */
+    public String getText();
+
+    /**
+     * Message {@link Severity} 
+     * @return message severity
+     */
+    public Severity getSeverity();
+
+
+    /**
+     * Message target - {@link Control} which message is related to . 
+     * @return message target
+     */
+    public Control getTarget();
+
+    /**
+     * Factory method to create a simple error message 
+     * @param target message target
+     * @param text message text 
+     * @return error message
+     */
+    public static ValidationMessage error( Control target, String text ) {
+        return new SimpleValidationMessage(target, text, Severity.ERROR);
+    }
+
+    /**
+     * Factory method to create a simple warning message 
+     * @param target message target
+     * @param text message text 
+     * @return warning message
+     */
+    public static ValidationMessage warning( Control target, String text ) {
+        return new SimpleValidationMessage(target, text, Severity.WARNING);
+    }
+
+    @Override default public int compareTo(ValidationMessage msg) {
+        return msg == null || getTarget() != msg.getTarget() ? -1: getSeverity().compareTo(msg.getSeverity());
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/validation/ValidationResult.java b/controlsfx/src/main/java/org/controlsfx/validation/ValidationResult.java
new file mode 100644
index 0000000..460bb3f
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/validation/ValidationResult.java
@@ -0,0 +1,276 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.validation;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import javafx.scene.control.Control;
+
+/**
+ * Validation result. Can generally be thought of a collection of validation messages.
+ * Allows for quick an painless accumulation of the messages. 
+ * Also provides ability to combine validation results 
+ */
+public class ValidationResult {
+
+    private List<ValidationMessage> errors   = new ArrayList<>();
+    private List<ValidationMessage> warnings = new ArrayList<>();
+
+    /**
+     * Creates empty validation result
+     */
+    public ValidationResult() {}
+
+    /**
+     * Factory method to create validation result out of one message
+     * @param target validation target 
+     * @param text message text 
+     * @param severity message severity
+     * @param condition condition on which message will be added to validation result
+     * @return New instance of validation result
+     */
+    public static final ValidationResult fromMessageIf( Control target, String text, Severity severity, boolean condition ) {
+        return new ValidationResult().addMessageIf(target, text, severity, condition);
+    }
+
+    /**
+     * Factory method to create validation result out of one error
+     * @param target validation target 
+     * @param text message text 
+     * @param condition condition on which message will be added to validation result
+     * @return New instance of validation result
+     */
+    public static final ValidationResult fromErrorIf( Control target, String text, boolean condition ) {
+        return new ValidationResult().addErrorIf(target, text, condition);
+    }
+
+    /**
+     * Factory method to create validation result out of one warning
+     * @param target validation target 
+     * @param text message text 
+     * @param condition condition on which message will be added to validation result
+     * @return New instance of validation result
+     */
+    public static final ValidationResult fromWarningIf( Control target, String text, boolean condition ) {
+        return new ValidationResult().addWarningIf(target, text, condition);
+    }
+
+    /**
+     * Factory method to create validation result out of one error
+     * @param target validation target 
+     * @param text message text 
+     * @return New instance of validation result
+     */
+    public static final ValidationResult fromError( Control target, String text ) {
+        return fromMessages( ValidationMessage.error(target, text));
+    }
+
+    /**
+     * Factory method to create validation result out of one warning
+     * @param target validation target 
+     * @param text message text 
+     * @return New instance of validation result
+     */
+    public static final ValidationResult fromWarning( Control target, String text ) {
+        return fromMessages( ValidationMessage.warning(target, text));
+    }
+
+    /**
+     * Factory method to create validation result out of several messages
+     * @param messages
+     * @return New instance of validation result
+     */
+    public static final ValidationResult fromMessages( ValidationMessage... messages ) {
+        return new ValidationResult().addAll(messages);
+    }
+
+    /**
+     * Factory method to create validation result out of collection of messages
+     * @param messages
+     * @return New instance of validation result
+     */
+    public static final ValidationResult fromMessages( Collection<? extends ValidationMessage> messages ) {
+        return new ValidationResult().addAll(messages);
+    }
+
+    /**
+     * Factory method to create validation result out of several validation results
+     * @param results results
+     * @return New instance of validation result, combining all into one
+     */
+    public static final ValidationResult fromResults( ValidationResult... results ) {
+        return new ValidationResult().combineAll(results);
+    }
+
+    /**
+     * Factory method to create validation result out of collection of validation results
+     * @param results results
+     * @return New instance of validation result, combining all into one
+     */
+    public static final ValidationResult fromResults( Collection<ValidationResult> results ) {
+        return new ValidationResult().combineAll(results);
+    }
+
+    /**
+     * Creates a copy of validation result
+     * @return copy of validation result
+     */
+    public ValidationResult copy() {
+        return ValidationResult.fromMessages(getMessages());
+    }
+
+    /**
+     * Add one message to validation result
+     * @param message validation message
+     * @return updated validation result
+     */
+    public ValidationResult add( ValidationMessage message ) {
+
+        if ( message != null ) {
+            switch( message.getSeverity() ) {
+                case ERROR  : errors.add( message); break;
+                case WARNING: warnings.add(message); break;
+            }
+        }
+
+        return this;
+    }
+
+    /**
+     * Add one message to validation result with condition
+     * @param target validation target
+     * @param text message text
+     * @param severity message severity
+     * @param condition condition on which message will be added 
+     * @return updated validation result
+     */
+    public ValidationResult addMessageIf( Control target, String text, Severity severity, boolean condition) {
+        return  condition? add( new SimpleValidationMessage(target, text, severity)): this;
+    }
+
+    /**
+     * Add one error to validation result with condition
+     * @param target validation target
+     * @param text message text
+     * @param condition condition on which error will be added 
+     * @return updated validation result
+     */
+    public ValidationResult addErrorIf( Control target, String text, boolean condition) {
+        return addMessageIf(target,text,Severity.ERROR,condition);
+    }
+
+    /**
+     * Add one warning to validation result with condition
+     * @param target validation target
+     * @param text message text
+     * @param condition condition on which warning will be added 
+     * @return updated validation result
+     */
+    public ValidationResult addWarningIf( Control target, String text, boolean condition) {
+        return addMessageIf(target,text,Severity.WARNING,condition);
+    }
+
+    /**
+     * Add collection of validation messages
+     * @param messages
+     * @return updated validation result
+     */
+    public ValidationResult addAll( Collection<? extends ValidationMessage> messages ) {
+        messages.stream().forEach( msg-> add(msg));
+        return this;
+    }
+
+    /**
+     * Add several validation messages
+     * @param messages
+     * @return updated validation result
+     */
+    public ValidationResult addAll( ValidationMessage... messages ) {
+        return addAll(Arrays.asList(messages));
+    }
+
+    /**
+     * Combine validation result with another. This will create a new instance of combined validation result
+     * @param validationResult
+     * @return new instance of combined validation result
+     */
+    public ValidationResult combine( ValidationResult validationResult ) {
+        return validationResult == null? copy(): copy().addAll(validationResult.getMessages());
+    }
+
+    /**
+     * Combine validation result with others. This will create a new instance of combined validation result
+     * @param validationResults
+     * @return new instance of combined validation result
+     */
+    public ValidationResult combineAll( Collection<ValidationResult> validationResults ) {
+        return validationResults.stream().reduce(copy(), (x,r) -> {
+            return r == null? x: x.addAll(r.getMessages());
+        });
+    }
+
+    /**
+     * Combine validation result with others. This will create a new instance of combined validation result
+     * @param validationResults
+     * @return new instance of combined validation result
+     */
+    public ValidationResult combineAll( ValidationResult... validationResults ) {
+        return combineAll( Arrays.asList(validationResults));
+    }
+
+    /**
+     * Retrieve errors represented by validation result
+     * @return collection of errors
+     */
+    public Collection<ValidationMessage> getErrors() {
+        return Collections.unmodifiableList(errors);
+    }
+
+    /**
+     * Retrieve warnings represented by validation result
+     * @return collection of warnings
+     */
+    public Collection<ValidationMessage> getWarnings() {
+        return Collections.unmodifiableList(warnings);
+    }
+
+    /**
+     * Retrieve all messages represented by validation result
+     * @return collection of messages
+     */
+    public Collection<ValidationMessage> getMessages() {
+        List<ValidationMessage> messages = new ArrayList<>();
+        messages.addAll(errors);
+        messages.addAll(warnings);
+        return Collections.unmodifiableList(messages);
+    }
+
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/validation/ValidationSupport.java b/controlsfx/src/main/java/org/controlsfx/validation/ValidationSupport.java
new file mode 100644
index 0000000..bd70c06
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/validation/ValidationSupport.java
@@ -0,0 +1,329 @@
+/**
+ * Copyright (c) 2014, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.validation;
+
+import java.util.Collections;
+import java.util.Optional;
+import java.util.Set;
+import java.util.WeakHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+import javafx.application.Platform;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.ReadOnlyBooleanProperty;
+import javafx.beans.property.ReadOnlyObjectProperty;
+import javafx.beans.property.ReadOnlyObjectWrapper;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.FXCollections;
+import javafx.collections.MapChangeListener;
+import javafx.collections.ObservableMap;
+import javafx.collections.ObservableSet;
+import javafx.scene.control.Control;
+import javafx.util.Callback;
+
+import org.controlsfx.tools.ValueExtractor;
+import org.controlsfx.validation.decoration.GraphicValidationDecoration;
+import org.controlsfx.validation.decoration.ValidationDecoration;
+
+/**
+ * Provides validation support for UI components. The idea is create an instance of this class the component group, usually a panel.<br>
+ * Once created, {@link Validator}s can be registered for components, to provide the validation:
+ * 
+ *     <pre>
+ *        ValidationSupport validationSupport = new ValidationSupport();
+ *        validationSupport.registerValidator(textField, Validator.createEmptyValidator("Text is required"));
+ *        validationSupport.registerValidator(combobox, Validator.createEmptyValidator( "ComboBox Selection required"));
+ *        validationSupport.registerValidator(checkBox, (Control c, Boolean newValue) ->
+ *        	    ValidationResult.fromErrorIf( c, "Checkbox should be checked", !newValue)
+ *         );
+ *     </pre>
+ *     
+ *  validationResultProperty provides an ability to react on overall validation result changes:
+ *  <pre>
+ *     validationSupport.validationResultProperty().addListener( (o, oldValue, newValue) ->
+        	 messageList.getItems().setAll(newValue.getMessages()));
+ *  </pre>   
+ *  
+ *  Standard JavaFX UI controls are supported out of the box. There is also an ability to add support for custom controls.
+ *  To do that "observable value extractor" should be added for specific controls. Such "extractor" consists of two functional interfaces:
+ *  a {@link Predicate} to check the applicability of the control and a {@link Callback} to extract control's observable value. 
+ *  Here is an sample of internal registration of such "extractor" for  a few  controls :
+ *  <pre>
+ *     ValueExtractor.addObservableValueExtractor( c -> c instanceof TextInputControl, c -> ((TextInputControl)c).textProperty());
+ *     ValueExtractor.addObservableValueExtractor( c -> c instanceof ComboBox,         c -> ((ComboBox<?>)c).getValue());
+ *  </pre>
+ *   
+ */
+public class ValidationSupport {
+    
+
+    private static final String CTRL_REQUIRED_FLAG    = "$org.controlsfx.validation.required$"; //$NON-NLS-1$
+    
+    /**
+     * Set control's required flag
+     * @param c control
+     * @param required flag
+     */
+    public static void setRequired( Control c, boolean required ) {
+        c.getProperties().put(CTRL_REQUIRED_FLAG, required);
+    }
+
+    /**
+     * Check control's required flag
+     * @param c control
+     * @return true if required 
+     */
+    public static boolean isRequired( Control c ) {
+        Object value = c.getProperties().get(CTRL_REQUIRED_FLAG);
+        return value instanceof Boolean? (Boolean)value: false;
+    }
+
+    private ObservableSet<Control> controls = FXCollections.observableSet();
+    private ObservableMap<Control,ValidationResult> validationResults = 
+            FXCollections.observableMap(new WeakHashMap<>());
+
+    
+    private AtomicBoolean dataChanged = new AtomicBoolean(false);
+    
+    /**
+     * Creates validation support instance. <br>
+     * If initial decoration is desired invoke {@link #initInitialDecoration()}.
+     */
+    public ValidationSupport() {
+
+        validationResultProperty().addListener( (o, oldValue, validationResult) -> {
+            invalidProperty.set(!validationResult.getErrors().isEmpty());
+            redecorate();
+        });
+    	
+        // notify validation result observers
+        validationResults.addListener( (MapChangeListener.Change<? extends Control, ? extends ValidationResult> change) ->
+        	validationResultProperty.set(ValidationResult.fromResults(validationResults.values()))
+        );
+
+    }
+    
+    /**
+     * Activates the initial decoration of validated controls. <br>
+     * By default the decoration will only be applied after the first change of one validated controls value.
+     */
+    public void initInitialDecoration() {
+        dataChanged.set(true);
+        redecorate();
+    }
+
+    /**
+     * Redecorates all known components
+     * Only decorations related to validation are affected
+     */
+    // TODO needs optimization
+    public void redecorate() {
+        Optional<ValidationDecoration> odecorator = Optional.ofNullable(getValidationDecorator());
+        for (Control target : getRegisteredControls()) {
+            odecorator.ifPresent( decorator -> {
+            	decorator.removeDecorations(target);
+                decorator.applyRequiredDecoration(target);
+                if ( dataChanged.get() && isErrorDecorationEnabled()) {
+                	getHighestMessage(target).ifPresent(msg -> decorator.applyValidationDecoration(msg));
+                }
+            });
+        }
+    }
+    
+    private BooleanProperty errorDecorationEnabledProperty = new SimpleBooleanProperty(true) {
+    	protected void invalidated() {
+    		redecorate();
+    	};
+    };
+    
+    public BooleanProperty errorDecorationEnabledProperty() {
+    	return errorDecorationEnabledProperty;
+    }
+    
+    public void setErrorDecorationEnabled(boolean enabled) {
+		errorDecorationEnabledProperty.set(enabled);
+	}
+    
+    private boolean isErrorDecorationEnabled() {
+    	return errorDecorationEnabledProperty.get();
+	}
+    
+    
+
+    private ReadOnlyObjectWrapper<ValidationResult> validationResultProperty = 
+            new ReadOnlyObjectWrapper<>();
+
+
+    /**
+     * Retrieves current validation result
+     * @return validation result
+     */
+    public ValidationResult getValidationResult() {
+        return validationResultProperty.get();
+    }
+
+    /**
+     * Can be used to track validation result changes 
+     * @return The Validation result property.
+     */
+    public ReadOnlyObjectProperty<ValidationResult> validationResultProperty() {
+        return validationResultProperty.getReadOnlyProperty();
+    }
+
+    private BooleanProperty invalidProperty = new SimpleBooleanProperty();
+
+    /**
+     * Returns current validation state. 
+     * @return true if there is at least one error
+     */
+    public Boolean isInvalid() {
+        return invalidProperty.get();
+    }
+
+    /**
+     * Validation state property 
+     * @return validation state property
+     */
+    public ReadOnlyBooleanProperty invalidProperty() {
+        return invalidProperty;
+    }
+
+
+    private ObjectProperty<ValidationDecoration> validationDecoratorProperty =
+            new SimpleObjectProperty<ValidationDecoration>(this, "validationDecorator", new GraphicValidationDecoration()) { //$NON-NLS-1$
+        @Override protected void invalidated() {
+            // when the decorator changes, rerun the decoration to update the visuals immediately.
+            redecorate();
+        }
+    };
+
+    /**
+     * @return The Validation decorator property
+     */
+    public ObjectProperty<ValidationDecoration> validationDecoratorProperty() {
+        return validationDecoratorProperty;
+    }
+
+    /**
+     * Returns current validation decorator
+     * @return current validation decorator or null if none
+     */
+    public ValidationDecoration getValidationDecorator() {
+        return validationDecoratorProperty.get();
+    }
+
+    /**
+     * Sets new validation decorator
+     * @param decorator new validation decorator. Null value is valid - no decoration will occur
+     */
+    public void setValidationDecorator( ValidationDecoration decorator ) {
+        validationDecoratorProperty.set(decorator);
+    }
+
+
+    /**
+     * Registers {@link Validator} for specified control with additional possiblity to mark control as required or not.
+     * @param c control to validate
+     * @param required true if controls should be required
+     * @param validator {@link Validator} to be used
+     * @return true if registration is successful
+     */
+    @SuppressWarnings("unchecked")
+    public <T> boolean registerValidator( final Control c, boolean required, final Validator<T> validator  ) {
+    	
+    	Optional.ofNullable(c).ifPresent( ctrl -> {
+    		ctrl.getProperties().addListener( new MapChangeListener<Object,Object>(){
+
+				@Override
+				public void onChanged(
+						javafx.collections.MapChangeListener.Change<? extends Object, ? extends Object> change) {
+					
+					if ( CTRL_REQUIRED_FLAG.equals(change.getKey())) {
+						redecorate();
+					}
+				}
+
+    		});
+    	});
+    	
+        setRequired( c, required );
+
+        return ValueExtractor.getObservableValueExtractor(c).map( e -> {
+
+            ObservableValue<T> observable = (ObservableValue<T>) e.call(c);
+
+            Consumer<T> updateResults = value -> { 
+                Platform.runLater(() -> validationResults.put(c, validator.apply(c, value)));
+            };
+
+            controls.add(c);
+
+            observable.addListener( (o,oldValue,newValue) -> {
+            	dataChanged.set(true);
+            	updateResults.accept(newValue);
+            });
+            updateResults.accept(observable.getValue());
+
+            return e;
+
+        }).isPresent();
+    }
+
+    /**
+     * Registers {@link Validator} for specified control and makes control required
+     * @param c control to validate
+     * @param validator {@link Validator} to be used
+     * @return true if registration is successful
+     */
+    public <T> boolean registerValidator( final Control c, final Validator<T> validator  ) {
+        return registerValidator(c, true, validator);
+    }
+
+    /**
+     * Returns currently registered controls
+     * @return set of currently registered controls
+     */
+    public Set<Control> getRegisteredControls() {
+        return Collections.unmodifiableSet(controls);
+    }
+
+    /**
+     * Returns optional highest severity message for a control
+     * @param target control
+     * @return Optional highest severity message for a control
+     */
+    public Optional<ValidationMessage> getHighestMessage(Control target) {
+    	return Optional.ofNullable(validationResults.get(target)).flatMap( result ->
+    	   result.getMessages().stream().max(ValidationMessage.COMPARATOR)   
+    	);
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/validation/Validator.java b/controlsfx/src/main/java/org/controlsfx/validation/Validator.java
new file mode 100644
index 0000000..3a7aeaa
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/validation/Validator.java
@@ -0,0 +1,158 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.validation;
+
+import java.util.Collection;
+import java.util.function.BiFunction;
+import java.util.function.Predicate;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import javafx.scene.control.Control;
+
+/**
+ * Interface defining the contract for validation of specific component
+ * This interface is a {@link BiFunction} which when given the control and its current value 
+ * computes the validation result 
+ * 
+ * @param <T> type of the controls value
+ */
+public interface Validator<T> extends BiFunction<Control, T, ValidationResult> {
+    
+    /**
+     * Combines the given validators into a single Validator instance. 
+     * @param validators the validators to combine
+     * @return a Validator instance
+     */
+    @SafeVarargs
+    static <T> Validator<T> combine(Validator<T>... validators) {
+        return (control, value) -> Stream.of(validators)
+            .map(validator -> validator.apply(control, value))
+            .collect(Collectors.reducing(new ValidationResult(), ValidationResult::combine));
+    }
+
+    /**
+     * Factory method to create a validator, which checks if value exists. 
+     * @param message text of a message to be created if value is invalid
+     * @param severity severity of a message to be created if value is invalid
+     * @return new validator
+     */
+    public static <T> Validator<T> createEmptyValidator(final String message, final Severity severity) {
+        return (c, value) -> {
+            boolean condition = value instanceof String ? value.toString().trim().isEmpty() : value == null;
+            return ValidationResult.fromMessageIf(c, message, severity, condition);
+        };
+    }
+
+    /**
+     * Factory method to create a validator, which checks if value exists. 
+     * Error is created if not if value does not exist 
+     * @param message of a error to be created if value is invalid
+     * @return new validator
+     */
+    public static <T> Validator<T> createEmptyValidator(final String message) {
+        return createEmptyValidator(message, Severity.ERROR);
+    }
+
+    /**
+     * Factory method to create a validator, which if value exists in the provided collection. 
+     * @param values text of a message to be created if value is not found
+     * @param severity severity of a message to be created if value is found
+     * @return new validator
+     */
+    public static <T> Validator<T> createEqualsValidator(final String message, final Severity severity, final Collection<T> values) {
+        return (c, value) -> ValidationResult.fromMessageIf(c,message,severity, !values.contains(value));
+    }
+
+    /**
+     * Factory method to create a validator, which checks if value exists in the provided collection. 
+     * Error is created if not found 
+     * @param message text of a error to be created if value is not found
+     * @param values
+     * @return new validator
+     */
+    public static <T> Validator<T> createEqualsValidator(final String message, final Collection<T> values) {
+        return createEqualsValidator(message, Severity.ERROR, values);
+    }
+    
+    /**
+     * Factory method to create a validator, which evaluates the value validity with a given predicate.
+     * Error is created if the evaluation is <code>false</code>.
+     * @param message text of a message to be created if value is invalid
+     * @param predicate the predicate to be used for the value validity evaluation.
+     * @return new validator
+     */
+    static <T> Validator<T> createPredicateValidator(Predicate<T> predicate, String message) {
+        return createPredicateValidator(predicate, message, Severity.ERROR);
+    }
+    
+    /**
+     * Factory method to create a validator, which evaluates the value validity with a given predicate.
+     * Error is created if the evaluation is <code>false</code>.
+     * @param message text of a message to be created if value is invalid
+     * @param predicate the predicate to be used for the value validity evaluation.
+     * @param severity severity of a message to be created if value is invalid
+     * @return new validator
+     */
+    static <T> Validator<T> createPredicateValidator(Predicate<T> predicate, String message, Severity severity) {
+        return (control, value) -> ValidationResult.fromMessageIf(
+            control, message,
+            severity,
+            predicate.test(value) == false);
+    }
+
+    /**
+     * Factory method to create a validator, which checks the value against a given regular expression.
+     * Error is created if the value is <code>null</code> or the value does not match the pattern.
+     * @param message text of a message to be created if value is invalid
+     * @param regex the regular expression the value has to match
+     * @param severity severity of a message to be created if value is invalid
+     * @return new validator
+     */
+    public static Validator<String> createRegexValidator(final String message, final String regex, final Severity severity) {
+        return (c, value) -> {
+            boolean condition = value == null ? true : !Pattern.matches(regex, value);
+            return ValidationResult.fromMessageIf(c, message, severity, condition);
+        };
+    }
+    
+    /**
+     * Factory method to create a validator, which checks the value against a given regular expression.
+     * Error is created if the value is <code>null</code> or the value does not match the pattern.
+     * @param message text of a message to be created if value is invalid
+     * @param regex the regular expression the value has to match
+     * @param severity severity of a message to be created if value is invalid
+     * @return new validator
+     */
+    public static Validator<String> createRegexValidator(final String message, final Pattern regex, final Severity severity) {
+        return (c, value) -> {
+            boolean condition = value == null ? true : !regex.matcher(value).matches();
+            return ValidationResult.fromMessageIf(c, message, severity, condition);
+        };
+    }
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/validation/decoration/AbstractValidationDecoration.java b/controlsfx/src/main/java/org/controlsfx/validation/decoration/AbstractValidationDecoration.java
new file mode 100644
index 0000000..f717852
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/validation/decoration/AbstractValidationDecoration.java
@@ -0,0 +1,105 @@
+/**
+ * Copyright (c) 2014, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.validation.decoration;
+
+import java.util.Collection;
+import java.util.List;
+
+import javafx.scene.control.Control;
+
+import org.controlsfx.control.decoration.Decoration;
+import org.controlsfx.control.decoration.Decorator;
+import org.controlsfx.validation.ValidationMessage;
+import org.controlsfx.validation.ValidationSupport;
+
+/**
+ * Implements common functionality for validation decorators.
+ * This class intended as a base for custom validation decorators   
+ * Custom validation decorator should define only two things:
+ * how 'validation' and 'required' decorations should be created
+ * <br>
+ * See {@link GraphicValidationDecoration} or {@link StyleClassValidationDecoration} for examples of such implementations.
+ * 
+ */
+public abstract class AbstractValidationDecoration implements ValidationDecoration {
+	
+	private static final String VALIDATION_DECORATION = "$org.controlsfx.decoration.vaidation$"; //$NON-NLS-1$
+	
+	private static boolean isValidationDecoration( Decoration decoration) {
+        return decoration != null && decoration.getProperties().get(VALIDATION_DECORATION) == Boolean.TRUE;
+    }
+
+	private static void setValidationDecoration( Decoration decoration ) {
+        if ( decoration != null ) {
+            decoration.getProperties().put(VALIDATION_DECORATION, Boolean.TRUE);
+        }
+    }
+
+	protected abstract Collection<Decoration> createValidationDecorations(ValidationMessage message);
+	protected abstract Collection<Decoration> createRequiredDecorations(Control target);
+	
+	/**
+	 * Removes all validation related decorations from the target
+	 * @param target control
+	 */
+	@Override
+	public void removeDecorations(Control target) {
+		 List<Decoration> decorations = Decorator.getDecorations(target);
+        if ( decorations != null ) {
+        	// conversion to array is a trick to prevent concurrent modification exception 
+            for ( Decoration d: Decorator.getDecorations(target).toArray(new Decoration[0]) ) {
+            	if (isValidationDecoration(d)) Decorator.removeDecoration(target, d);
+            }
+        }
+	}
+	
+	/*
+	 * (non-Javadoc)
+	 * @see org.controlsfx.validation.decorator.ValidationDecorator#applyValidationDecoration(org.controlsfx.validation.ValidationMessage)
+	 */
+	@Override
+	public void applyValidationDecoration(ValidationMessage message) {
+		createValidationDecorations(message).stream().forEach( d -> decorate( message.getTarget(), d ));
+	}
+
+	/*
+	 * (non-Javadoc)
+	 * @see org.controlsfx.validation.decorator.ValidationDecorator#applyRequiredDecoration(javafx.scene.control.Control)
+	 */
+	@Override
+	public void applyRequiredDecoration(Control target) {
+		if ( ValidationSupport.isRequired(target)) { 
+			createRequiredDecorations(target).stream().forEach( d -> decorate( target, d ));
+		}
+	}
+	
+	private void decorate( Control target, Decoration d ) {
+		setValidationDecoration(d); // mark as validation specific decoration
+        Decorator.addDecoration(target, d);
+	}
+
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/validation/decoration/CompoundValidationDecoration.java b/controlsfx/src/main/java/org/controlsfx/validation/decoration/CompoundValidationDecoration.java
new file mode 100644
index 0000000..6ba3722
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/validation/decoration/CompoundValidationDecoration.java
@@ -0,0 +1,93 @@
+/**
+ * Copyright (c) 2014, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.validation.decoration;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import javafx.scene.control.Control;
+
+import org.controlsfx.control.decoration.Decoration;
+import org.controlsfx.validation.ValidationMessage;
+
+/**
+ * Validation decoration to combine several existing decorations into one.
+ * Here is example of combining {@link GraphicValidationDecoration} and  {@link StyleClassValidationDecoration} 
+ * <br> <br>
+ * <img src="CompoundValidationDecoration.png" alt="Screenshot of CompoundValidationDecoration">  
+ *   
+ * 
+ */
+public class CompoundValidationDecoration extends AbstractValidationDecoration {
+
+    private final Set<ValidationDecoration> decorators = new HashSet<>();
+
+    /**
+     * Creates an instance of validator using a collection of validators
+     * @param decorators collection of validators
+     */
+    public CompoundValidationDecoration(Collection<ValidationDecoration> decorators) {
+        this.decorators.addAll(decorators); 
+    }
+
+    /**
+     * Creates an instance of validator using a set of validators
+     * @param decorators set of validators
+     */
+    public CompoundValidationDecoration(ValidationDecoration... decorators) {
+        this(Arrays.asList(decorators));
+    }	
+
+    /**
+     * {@inheritDoc}
+     */
+	@Override
+	public void applyRequiredDecoration(Control target) {
+		this.decorators.stream().forEach( d -> d.applyRequiredDecoration(target)); 
+	}
+	
+    /**
+     * {@inheritDoc}
+     */
+	@Override
+	public void applyValidationDecoration(ValidationMessage message) {
+		this.decorators.stream().forEach( d -> d.applyValidationDecoration(message)); 
+	}
+
+	@Override
+	protected Collection<Decoration> createValidationDecorations(ValidationMessage message) {
+		return Collections.emptyList();
+	}
+
+	@Override
+	protected Collection<Decoration> createRequiredDecorations(Control target) {
+		return Collections.emptyList();
+	}
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/validation/decoration/GraphicValidationDecoration.java b/controlsfx/src/main/java/org/controlsfx/validation/decoration/GraphicValidationDecoration.java
new file mode 100644
index 0000000..c405cea
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/validation/decoration/GraphicValidationDecoration.java
@@ -0,0 +1,127 @@
+/**
+ * Copyright (c) 2014, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.validation.decoration;
+
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import javafx.geometry.Pos;
+import javafx.scene.Node;
+import javafx.scene.control.Control;
+import javafx.scene.control.Label;
+import javafx.scene.control.Tooltip;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+
+import org.controlsfx.control.decoration.Decoration;
+import org.controlsfx.control.decoration.GraphicDecoration;
+import org.controlsfx.validation.Severity;
+import org.controlsfx.validation.ValidationMessage;
+
+/**
+ * Validation decorator to decorate validation state using images.
+ * <br>
+ * Validation icons are shown in the bottom-left corner of the control as it is seems to be the most
+ * logical location for such information.
+ * Required components are marked at the top-left corner with small red triangle.
+ * Here is example of such decoration 
+ * <br> <br>
+ * <img src="GraphicValidationDecorationWithTooltip.png" alt="Screenshot of GraphicValidationDecoration">  
+ * 
+ */
+public class GraphicValidationDecoration extends AbstractValidationDecoration {
+
+    // TODO we shouldn't hardcode this - defer to CSS eventually
+	
+    private static final Image ERROR_IMAGE = new Image(GraphicValidationDecoration.class.getResource("/impl/org/controlsfx/control/validation/decoration-error.png").toExternalForm()); //$NON-NLS-1$
+    private static final Image WARNING_IMAGE = new Image(GraphicValidationDecoration.class.getResource("/impl/org/controlsfx/control/validation/decoration-warning.png").toExternalForm()); //$NON-NLS-1$
+    private static final Image REQUIRED_IMAGE = new Image(GraphicValidationDecoration.class.getResource("/impl/org/controlsfx/control/validation/required-indicator.png").toExternalForm()); //$NON-NLS-1$
+
+    private static final String SHADOW_EFFECT = "-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.8), 10, 0, 0, 0);"; //$NON-NLS-1$
+    private static final String POPUP_SHADOW_EFFECT = "-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.8), 5, 0, 0, 5);"; //$NON-NLS-1$
+    private static final String TOOLTIP_COMMON_EFFECTS = "-fx-font-weight: bold; -fx-padding: 5; -fx-border-width:1;"; //$NON-NLS-1$
+    
+    private static final String ERROR_TOOLTIP_EFFECT = POPUP_SHADOW_EFFECT + TOOLTIP_COMMON_EFFECTS
+            + "-fx-background-color: FBEFEF; -fx-text-fill: cc0033; -fx-border-color:cc0033;"; //$NON-NLS-1$
+
+    private static final String WARNING_TOOLTIP_EFFECT = POPUP_SHADOW_EFFECT + TOOLTIP_COMMON_EFFECTS
+            + "-fx-background-color: FFFFCC; -fx-text-fill: CC9900; -fx-border-color: CC9900;"; //$NON-NLS-1$
+
+    /**
+     * Creates default instance
+     */
+    public GraphicValidationDecoration() {
+
+    }
+
+    // TODO write javadoc that users should override these methods to customise
+    // the error / warning / success nodes to use 
+    protected Node createErrorNode() {
+        return new ImageView(ERROR_IMAGE);
+    }
+
+    protected Node createWarningNode() {
+        return new ImageView(WARNING_IMAGE);
+    }
+
+    private Node createDecorationNode(ValidationMessage message) {
+        Node graphic = Severity.ERROR == message.getSeverity() ? createErrorNode() : createWarningNode();
+        graphic.setStyle(SHADOW_EFFECT);
+        Label label = new Label();
+        label.setGraphic(graphic);
+        label.setTooltip(createTooltip(message));
+        label.setAlignment(Pos.CENTER);
+        return label;
+    }
+
+    protected Tooltip createTooltip(ValidationMessage message) {
+        Tooltip tooltip = new Tooltip(message.getText());
+        tooltip.setOpacity(.9);
+        tooltip.setAutoFix(true);
+        tooltip.setStyle( Severity.ERROR == message.getSeverity()? ERROR_TOOLTIP_EFFECT: WARNING_TOOLTIP_EFFECT);
+        return tooltip;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+	protected Collection<Decoration> createValidationDecorations(ValidationMessage message) {
+    	return Arrays.asList(new GraphicDecoration(createDecorationNode(message),Pos.BOTTOM_LEFT));
+	}
+    
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+	protected Collection<Decoration> createRequiredDecorations(Control target) {
+    	return Arrays.asList(new GraphicDecoration(new ImageView(REQUIRED_IMAGE),Pos.TOP_LEFT, REQUIRED_IMAGE.getWidth()/2, REQUIRED_IMAGE.getHeight()/2));
+	}
+    
+    
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/validation/decoration/StyleClassValidationDecoration.java b/controlsfx/src/main/java/org/controlsfx/validation/decoration/StyleClassValidationDecoration.java
new file mode 100644
index 0000000..01f0b81
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/validation/decoration/StyleClassValidationDecoration.java
@@ -0,0 +1,80 @@
+/**
+ * Copyright (c) 2014, 2015, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.validation.decoration;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+
+import javafx.scene.control.Control;
+
+import org.controlsfx.control.decoration.Decoration;
+import org.controlsfx.control.decoration.StyleClassDecoration;
+import org.controlsfx.validation.Severity;
+import org.controlsfx.validation.ValidationMessage;
+
+/**
+ * Validation decorator to decorate component validation state using two
+ * CSS classes for errors and warnings.
+ * Here is example of such decoration 
+ * <br> <br>
+ * <img src="StyleClassValidationDecoration.png" alt="Screenshot of StyleClassValidationDecoration">  
+ */
+public class StyleClassValidationDecoration extends AbstractValidationDecoration {
+
+    private final String errorClass;
+    private final String warningClass;
+
+    /**
+     * Creates a default instance of a decorator
+     */
+    public StyleClassValidationDecoration() {
+        this(null,null);
+    }
+
+    /**
+     * Creates an instance of validator using custom class names
+     * @param errorClass class name for error decoration
+     * @param warningClass class name for warning decoration
+     */
+    public StyleClassValidationDecoration(String errorClass, String warningClass) {
+        this.errorClass = errorClass != null? errorClass : "error"; //$NON-NLS-1$
+        this.warningClass = warningClass != null? warningClass : "warning";	 //$NON-NLS-1$
+    }
+
+
+	@Override
+	protected Collection<Decoration> createValidationDecorations(ValidationMessage message) {
+		return Arrays.asList(new StyleClassDecoration( Severity.ERROR == message.getSeverity()? errorClass:warningClass));
+	}
+
+	@Override
+	protected Collection<Decoration> createRequiredDecorations(Control target) {
+		return Collections.emptyList();
+	}
+    
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/validation/decoration/ValidationDecoration.java b/controlsfx/src/main/java/org/controlsfx/validation/decoration/ValidationDecoration.java
new file mode 100644
index 0000000..a6fab8c
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/validation/decoration/ValidationDecoration.java
@@ -0,0 +1,59 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.validation.decoration;
+
+import javafx.scene.control.Control;
+
+import org.controlsfx.validation.ValidationMessage;
+
+/**
+ * Contract for validation decorators.
+ * Classes implementing this interface are used for decorating components with error/warning conditions, if such exists.
+ * They also used for marking 'required' components.
+ */
+public interface ValidationDecoration {
+
+    /**
+     * Removes all validation specific decorations from the target control.
+     * Non-validation specific decorations are left untouched.
+     * @param target
+     */
+    void removeDecorations(Control target);
+	
+    /**
+     * Applies validation decoration based on a given validation message
+     * @param message validation message
+     */
+    void applyValidationDecoration(ValidationMessage message);
+    
+    
+    /**
+     * Applies 'required' decoration to a given control
+     * @param target control
+     */
+    void applyRequiredDecoration(Control target);
+}
diff --git a/controlsfx/src/main/java/org/controlsfx/validation/decoration/package-info.java b/controlsfx/src/main/java/org/controlsfx/validation/decoration/package-info.java
new file mode 100644
index 0000000..de37a25
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/validation/decoration/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * A package containing decoration API specific to the validation framework.
+ */
+package org.controlsfx.validation.decoration;
\ No newline at end of file
diff --git a/controlsfx/src/main/java/org/controlsfx/validation/package-info.java b/controlsfx/src/main/java/org/controlsfx/validation/package-info.java
new file mode 100644
index 0000000..db054a1
--- /dev/null
+++ b/controlsfx/src/main/java/org/controlsfx/validation/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * A package containing validation-related API (that is, API to allow for developers
+ * to easily validate user input, and to provide visual feedback on the results).
+ */
+package org.controlsfx.validation;
\ No newline at end of file
diff --git a/controlsfx/src/main/resources/META-INF/services/org.controlsfx.glyphfont.GlyphFont b/controlsfx/src/main/resources/META-INF/services/org.controlsfx.glyphfont.GlyphFont
new file mode 100644
index 0000000..1d1eed7
--- /dev/null
+++ b/controlsfx/src/main/resources/META-INF/services/org.controlsfx.glyphfont.GlyphFont
@@ -0,0 +1 @@
+org.controlsfx.glyphfont.FontAwesome
\ No newline at end of file
diff --git a/controlsfx/src/main/resources/controlsfx.properties b/controlsfx/src/main/resources/controlsfx.properties
new file mode 100644
index 0000000..82927d3
--- /dev/null
+++ b/controlsfx/src/main/resources/controlsfx.properties
@@ -0,0 +1,73 @@
+### Dialogs ###
+
+dlg.ok.button = OK
+dlg.cancel.button = Cancel
+dlg.yes.button = Yes
+dlg.no.button = No
+dlg.close.button = Close
+dlg.detail.button.more = Show Details
+dlg.detail.button.less = Hide Details
+
+### Common Dialogs ###
+
+font.dlg.title=Select font
+font.dlg.header=Select font
+font.dlg.sample.text=Sample
+font.dlg.font.label=Font
+font.dlg.style.label=Style
+font.dlg.size.label=Size
+
+progress.dlg.title=Progress
+progress.dlg.header=Progress
+
+login.dlg.title=Login
+login.dlg.header=Enter user name and password
+login.dlg.user.caption=User Name
+login.dlg.pswd.caption=Password
+login.dlg.login.button=Login
+
+exception.dlg.title = Exception Details
+exception.dlg.header = Exception Details
+exception.dlg.label = The exception stacktrace was:
+exception.button.label = Open Exception
+
+### Wizard ###
+
+wizard.next.button = Next
+wizard.previous.button = Previous
+
+### Property Sheet ###
+
+bean.property.change.error.title = Property Change Error
+bean.property.change.error.header = Change is not allowed
+bean.property.category.basic=Basic
+bean.property.category.expert=Expert
+
+property.sheet.search.field.prompt = Search
+property.sheet.group.mode.byname = By Name
+property.sheet.group.mode.bycategory = By Category
+
+### Spreadsheet View ###
+
+spreadsheet.view.menu.copy = Copy
+spreadsheet.view.menu.paste = Paste
+spreadsheet.view.menu.comment = Comment cell
+spreadsheet.view.menu.comment.top-left = top left
+spreadsheet.view.menu.comment.top-right = top right
+spreadsheet.view.menu.comment.bottom-right = bottom right
+spreadsheet.view.menu.comment.bottom-left = bottom left
+spreadsheet.column.menu.fix = Fix column
+spreadsheet.column.menu.unfix = Unfix column
+spreadsheet.verticalheader.menu.fix = Fix row
+spreadsheet.verticalheader.menu.unfix = Unfix row
+
+### Status Bar ###
+statusbar.ok = OK
+
+### List Selection View ###
+listSelectionView.header.source = Available
+listSelectionView.header.target = Selected
+
+### PopOver ###
+popOver.default.content = <No Content>
+popOver.default.title = Info
\ No newline at end of file
diff --git a/controlsfx/src/main/resources/impl/org/controlsfx/control/validation/decoration-error.png b/controlsfx/src/main/resources/impl/org/controlsfx/control/validation/decoration-error.png
new file mode 100644
index 0000000..237b39f
Binary files /dev/null and b/controlsfx/src/main/resources/impl/org/controlsfx/control/validation/decoration-error.png differ
diff --git a/controlsfx/src/main/resources/impl/org/controlsfx/control/validation/decoration-warning.png b/controlsfx/src/main/resources/impl/org/controlsfx/control/validation/decoration-warning.png
new file mode 100644
index 0000000..0d351c5
Binary files /dev/null and b/controlsfx/src/main/resources/impl/org/controlsfx/control/validation/decoration-warning.png differ
diff --git a/controlsfx/src/main/resources/impl/org/controlsfx/control/validation/required-indicator.png b/controlsfx/src/main/resources/impl/org/controlsfx/control/validation/required-indicator.png
new file mode 100644
index 0000000..4410d65
Binary files /dev/null and b/controlsfx/src/main/resources/impl/org/controlsfx/control/validation/required-indicator.png differ
diff --git a/controlsfx/src/main/resources/impl/org/controlsfx/table/filter.png b/controlsfx/src/main/resources/impl/org/controlsfx/table/filter.png
new file mode 100644
index 0000000..1098d3a
Binary files /dev/null and b/controlsfx/src/main/resources/impl/org/controlsfx/table/filter.png differ
diff --git a/controlsfx/src/main/resources/impl/org/controlsfx/table/no_filter.png b/controlsfx/src/main/resources/impl/org/controlsfx/table/no_filter.png
new file mode 100644
index 0000000..8861914
Binary files /dev/null and b/controlsfx/src/main/resources/impl/org/controlsfx/table/no_filter.png differ
diff --git a/controlsfx/src/main/resources/impl/org/controlsfx/table/tablefilter.css b/controlsfx/src/main/resources/impl/org/controlsfx/table/tablefilter.css
new file mode 100644
index 0000000..96e4c32
--- /dev/null
+++ b/controlsfx/src/main/resources/impl/org/controlsfx/table/tablefilter.css
@@ -0,0 +1,7 @@
+.column-filter .custom-menu-item:focused {
+    -fx-background-color: transparent;
+}
+
+.filter-panel {
+    -fx-spacing: 10px
+}
\ No newline at end of file
diff --git a/controlsfx/src/main/resources/org/controlsfx/control/breadcrumbbar.css b/controlsfx/src/main/resources/org/controlsfx/control/breadcrumbbar.css
new file mode 100644
index 0000000..18ba307
--- /dev/null
+++ b/controlsfx/src/main/resources/org/controlsfx/control/breadcrumbbar.css
@@ -0,0 +1,3 @@
+.bread-crumb-bar {
+	
+}
diff --git a/controlsfx/src/main/resources/org/controlsfx/control/collapse.png b/controlsfx/src/main/resources/org/controlsfx/control/collapse.png
new file mode 100644
index 0000000..b8896c7
Binary files /dev/null and b/controlsfx/src/main/resources/org/controlsfx/control/collapse.png differ
diff --git a/controlsfx/src/main/resources/org/controlsfx/control/expand.png b/controlsfx/src/main/resources/org/controlsfx/control/expand.png
new file mode 100644
index 0000000..c45d4ac
Binary files /dev/null and b/controlsfx/src/main/resources/org/controlsfx/control/expand.png differ
diff --git a/controlsfx/src/main/resources/org/controlsfx/control/format-indent-more.png b/controlsfx/src/main/resources/org/controlsfx/control/format-indent-more.png
new file mode 100644
index 0000000..d57376f
Binary files /dev/null and b/controlsfx/src/main/resources/org/controlsfx/control/format-indent-more.png differ
diff --git a/controlsfx/src/main/resources/org/controlsfx/control/format-line-spacing-triple.png b/controlsfx/src/main/resources/org/controlsfx/control/format-line-spacing-triple.png
new file mode 100644
index 0000000..8e80972
Binary files /dev/null and b/controlsfx/src/main/resources/org/controlsfx/control/format-line-spacing-triple.png differ
diff --git a/controlsfx/src/main/resources/org/controlsfx/control/gridview.css b/controlsfx/src/main/resources/org/controlsfx/control/gridview.css
new file mode 100644
index 0000000..ea2c3ca
--- /dev/null
+++ b/controlsfx/src/main/resources/org/controlsfx/control/gridview.css
@@ -0,0 +1,7 @@
+.grid-view {
+	-fx-vertical-cell-spacing: 12;
+	-fx-horizontal-cell-spacing: 12;
+	-fx-cell-height: 64;
+	-fx-cell-width: 64;
+	-fx-horizontal-alignment: CENTER;
+}
diff --git a/controlsfx/src/main/resources/org/controlsfx/control/info-overlay.css b/controlsfx/src/main/resources/org/controlsfx/control/info-overlay.css
new file mode 100644
index 0000000..a18ded7
--- /dev/null
+++ b/controlsfx/src/main/resources/org/controlsfx/control/info-overlay.css
@@ -0,0 +1,15 @@
+.info-overlay > .info-panel {
+    -fx-background-color: lightgray;
+    -fx-opacity: .75;
+    -fx-padding: 5 5 5 5;
+}
+
+.info-panel > .info-panel > .expand-button {
+    -fx-padding: 0 0 0 0 ;
+    /*-fx-graphic: url("/helloworld/david/collapse.png");*/
+}
+
+.info-panel > .info-panel > .collapse-button {
+    -fx-padding: 0 0 0 0 ;
+    /*-fx-graphic: url("expand.png");*/
+}
diff --git a/controlsfx/src/main/resources/org/controlsfx/control/listselectionview.css b/controlsfx/src/main/resources/org/controlsfx/control/listselectionview.css
new file mode 100644
index 0000000..c13cd77
--- /dev/null
+++ b/controlsfx/src/main/resources/org/controlsfx/control/listselectionview.css
@@ -0,0 +1,13 @@
+.list-selection-view {
+	-fx-padding: 10px;	
+}
+
+.list-selection-view > .grid-pane {
+	-fx-vgap: 10px;
+	-fx-hgap: 10px;
+}
+
+.list-selection-view > .grid-pane > .list-header-label {
+	-fx-font-size: 1.0em;
+	-fx-font-weight: bold;
+}
\ No newline at end of file
diff --git a/controlsfx/src/main/resources/org/controlsfx/control/maskerpane.css b/controlsfx/src/main/resources/org/controlsfx/control/maskerpane.css
new file mode 100644
index 0000000..cf0c78d
--- /dev/null
+++ b/controlsfx/src/main/resources/org/controlsfx/control/maskerpane.css
@@ -0,0 +1,17 @@
+.masker-pane {}
+
+.masker-pane .masker-glass {
+    -fx-background-color: rgba(0, 0, 0, .3);
+}
+
+.masker-pane .masker-text {
+    -fx-text-fill: white;
+    -fx-font-weight: bold;
+}
+
+.masker-pane .masker-center {
+    -fx-background-color: rgba(0, 0, 0, .6);
+    -fx-max-height: 1.50in;
+    -fx-padding: .25in;
+    -fx-background-radius: .1in;
+}
\ No newline at end of file
diff --git a/controlsfx/src/main/resources/org/controlsfx/control/masterdetailpane.css b/controlsfx/src/main/resources/org/controlsfx/control/masterdetailpane.css
new file mode 100644
index 0000000..82d11f1
--- /dev/null
+++ b/controlsfx/src/main/resources/org/controlsfx/control/masterdetailpane.css
@@ -0,0 +1,14 @@
+.tab-pane > * > .master-detail-pane > .split-pane,
+.split-pane > * > .master-detail-pane > .split-pane {
+    -fx-background-insets: 0, 0;
+    -fx-padding: 0;
+ }
+.tab-pane.floating > * > .master-detail-pane > .split-pane {
+    -fx-background-insets: 0, 0;
+    -fx-padding: -1;
+}
+.titled-pane > * > * > .master-detail-pane > .split-pane {
+    -fx-background-color: null;
+    -fx-background-insets: 0, 0;
+    -fx-padding: 0;
+}
diff --git a/controlsfx/src/main/resources/org/controlsfx/control/notificationpane.css b/controlsfx/src/main/resources/org/controlsfx/control/notificationpane.css
new file mode 100644
index 0000000..765ec1b
--- /dev/null
+++ b/controlsfx/src/main/resources/org/controlsfx/control/notificationpane.css
@@ -0,0 +1,103 @@
+/******************************************************************************
+ * 
+ * Light theme
+ * 
+ *****************************************************************************/
+
+.notification-pane .notification-bar > .pane {
+    -fx-background-color: -fx-outer-border, -fx-inner-border, -fx-body-color;
+    -fx-padding: 0 7 0 7;
+}
+
+.notification-pane.top .notification-bar > .pane {
+    -fx-background-insets: 0 0 0 0, 0 0 1 0, 0 0 2 0;
+}
+
+.notification-pane.bottom .notification-bar > .pane {
+    -fx-background-insets: 0 0 0 0, 1 0 0 0, 2 0 0 0;
+}
+
+.notification-pane .notification-bar > .pane .label {
+    -fx-font-size: 1.166667em; /*15px;*/
+    -fx-text-fill: #292929;
+}
+
+
+/******************************************************************************
+ * 
+ * Dark theme
+ * 
+ *****************************************************************************/
+
+.notification-pane.dark .notification-bar > .pane {
+    -fx-background-color: linear-gradient(#595959, #474747 37%, #343434);
+}
+
+.notification-pane.dark .notification-bar > .pane .label {
+    -fx-text-fill: #ebebeb;
+}
+
+
+/******************************************************************************
+ * 
+ * Drop shadow support
+ * 
+ *****************************************************************************/
+
+/* NotificationPane shows from the top, so put shadow at bottom of bar */
+.notification-pane.top .notification-bar > .pane {
+    -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.4), 11, 0.0, 0, 3);
+}
+
+/* 
+ * We could have a drop shadow at the top of the bar when it appears from the
+ * bottom, but it doesn't look right as the gradient is running in the opposite
+ * direction of the drop shadow. Therefore, the following is commented out,
+ * but it can always be re-enabled in the future if desired.
+ */
+ /*
+.notification-pane:bottom .notification-bar > .pane {
+    -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.4), 11, 0.0, 0, -3);
+}
+*/
+
+
+
+/******************************************************************************
+ * 
+ * Close button
+ * 
+ *****************************************************************************/
+.notification-pane .notification-bar > .pane .close-button {
+    -fx-background-color: transparent;
+    -fx-background-insets: 0;
+    -fx-background-radius: 2;
+    -fx-padding: 0 0 0 0;
+    -fx-alignment: center;
+}
+
+.notification-pane .notification-bar > .pane .close-button:hover {
+    -fx-background-color: linear-gradient(#a3a3a3, #8b8b8b 34%, #777777 36%, #777777 63%, #8b8b8b 65%, #adadad);
+}
+
+.notification-pane .notification-bar > .pane .close-button:pressed {
+    -fx-background-color: linear-gradient(#a3a3a3, #8b8b8b 34%, #777777 36%, #777777 63%, #8b8b8b 65%, #adadad);
+}
+
+.notification-pane .notification-bar > .pane .close-button > .graphic {
+    -fx-background-color: #949494;
+    -fx-scale-shape: false;
+    -fx-padding: 4.5 4.5 4.5 4.5; /* Graphic is 9x9 px */
+}
+
+.notification-pane .notification-bar > .pane .close-button:hover > .graphic {
+    -fx-background-color: #fefeff;
+}
+
+.notification-pane .notification-bar > .pane .close-button:pressed > .graphic {
+    -fx-background-color: #cfcfcf;
+}
+
+.notification-pane .notification-bar > .pane .close-button > .graphic {
+    -fx-shape: "M395.992,296.758l1.794-1.794l7.292,7.292l-1.795,1.794 L395.992,296.758z M403.256,294.992l1.794,1.794l-7.292,7.292l-1.794-1.795 L403.256,294.992z";
+}
\ No newline at end of file
diff --git a/controlsfx/src/main/resources/org/controlsfx/control/notificationpopup.css b/controlsfx/src/main/resources/org/controlsfx/control/notificationpopup.css
new file mode 100644
index 0000000..a35fa15
--- /dev/null
+++ b/controlsfx/src/main/resources/org/controlsfx/control/notificationpopup.css
@@ -0,0 +1,79 @@
+/******************************************************************************
+ * 
+ * Light theme
+ * 
+ *****************************************************************************/
+
+.notification-bar > .pane {
+    -fx-background-color: -fx-outer-border, -fx-inner-border, -fx-body-color;
+    -fx-padding: 7 7 7 7;
+    -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.4), 11, 0.0, 0, 3);
+    -fx-background-insets: 0, 1, 2;
+}
+
+.notification-bar > .pane .title {
+    -fx-font-size: 1.166667em; /*15px;*/
+    -fx-text-fill: #292929;
+    -fx-font-weight: bold;
+}
+
+.notification-bar > .pane .label {
+    -fx-font-size: 1.166667em; /*15px;*/
+    -fx-text-fill: #292929;
+    -fx-alignment: top-left;
+}
+
+/******************************************************************************
+ * 
+ * Dark theme
+ * 
+ *****************************************************************************/
+
+.notification-bar.dark > .pane {
+    -fx-background-color: -fx-outer-border, -fx-inner-border, linear-gradient(#595959, #474747 37%, #343434);
+}
+
+.notification-bar.dark > .pane .title,
+.notification-bar.dark > .pane .label {
+    -fx-text-fill: #ebebeb;
+}
+
+
+/******************************************************************************
+ * 
+ * Close button
+ * 
+ *****************************************************************************/
+ .notification-bar > .pane .close-button {
+    -fx-background-color: transparent;
+    -fx-background-insets: 0;
+    -fx-background-radius: 2;
+    -fx-padding: 0 0 0 0;
+    -fx-alignment: center;
+}
+
+.notification-bar > .pane .close-button:hover {
+    -fx-background-color: linear-gradient(#a3a3a3, #8b8b8b 34%, #777777 36%, #777777 63%, #8b8b8b 65%, #adadad);
+}
+
+.notification-bar > .pane .close-button:pressed {
+    -fx-background-color: linear-gradient(#a3a3a3, #8b8b8b 34%, #777777 36%, #777777 63%, #8b8b8b 65%, #adadad);
+}
+
+.notification-bar > .pane .close-button > .graphic {
+    -fx-background-color: #949494;
+    -fx-scale-shape: false;
+    -fx-padding: 4.5 4.5 4.5 4.5; /* Graphic is 9x9 px */
+}
+
+.notification-bar > .pane .close-button:hover > .graphic {
+    -fx-background-color: #fefeff;
+}
+
+.notification-bar > .pane .close-button:pressed > .graphic {
+    -fx-background-color: #cfcfcf;
+}
+
+.notification-bar > .pane .close-button > .graphic {
+    -fx-shape: "M395.992,296.758l1.794-1.794l7.292,7.292l-1.795,1.794 L395.992,296.758z M403.256,294.992l1.794,1.794l-7.292,7.292l-1.794-1.795 L403.256,294.992z";
+}
\ No newline at end of file
diff --git a/controlsfx/src/main/resources/org/controlsfx/control/open-editor.png b/controlsfx/src/main/resources/org/controlsfx/control/open-editor.png
new file mode 100644
index 0000000..87d22c3
Binary files /dev/null and b/controlsfx/src/main/resources/org/controlsfx/control/open-editor.png differ
diff --git a/controlsfx/src/main/resources/org/controlsfx/control/plusminusslider.css b/controlsfx/src/main/resources/org/controlsfx/control/plusminusslider.css
new file mode 100644
index 0000000..86cb6f1
--- /dev/null
+++ b/controlsfx/src/main/resources/org/controlsfx/control/plusminusslider.css
@@ -0,0 +1,57 @@
+.plus-minus-slider {
+    -fx-background-radius: 2.0;
+    -fx-border-color: -fx-box-border;
+    -fx-border-radius: 2.0;
+}
+
+.plus-minus-slider:horizontal {
+    -fx-background-color: derive(-fx-box-border,30.0%), linear-gradient(to bottom, derive(-fx-base,-3.0%), derive(-fx-base,5.0%) 50.0%, derive(-fx-base,-3.0%));
+    -fx-background-insets: 0.0, 1.0 0.0 1.0 0.0;
+    -fx-pref-height: 16.0;
+    -fx-max-height: 16.0;
+}
+
+.plus-minus-slider:vertical {
+    -fx-background-color: derive(-fx-box-border,30.0%), linear-gradient(to right, derive(-fx-base,-3.0%), derive(-fx-base,5.0%) 50.0%, derive(-fx-base,-3.0%));
+    -fx-background-insets: 0.0, 0.0 1.0 0.0 1.0;
+    -fx-pref-width: 16.0;
+    -fx-max-width: 16.0;
+}
+
+.plus-minus-slider > * > .slider {
+    -fx-show-tick-marks: false;
+}
+
+.plus-minus-slider > * > .slider > .track {
+    -fx-background-color: transparent;
+}
+
+.plus-minus-slider > * > .slider > .thumb {
+    -fx-background-color: -fx-outer-border, -fx-inner-border, -fx-body-color; 
+    -fx-background-insets: 2.0, 3.0, 4.0;
+    -fx-background-radius: 3.0, 2.0, 1.0;
+}
+
+.plus-minus-slider > * > .slider:focused > .thumb {
+    -fx-background-color: -fx-focus-color, -fx-inner-border, -fx-body-color, -fx-faint-focus-color, -fx-body-color;
+    -fx-background-insets: 1.8, 3.0, 4.0, 0.6, 4.6;
+    -fx-background-radius: 3.0, 2.0, 1.0, 4.0, 1.0;
+}
+
+.plus-minus-slider > * > .adjust-plus {
+    -fx-pref-width: 16.0;
+    -fx-pref-height: 16.0;
+    -fx-shape: "M0,3 H3 V0 H5 V3 H8 V5 H5 V8 H3 V5 H0 V3 Z";
+    -fx-scale-shape: false;
+    -fx-effect: dropshadow(two-pass-box , -fx-shadow-highlight-color, 1.0, 0.0 , 0.0, 1.4);
+    -fx-background-color: -fx-mark-highlight-color,derive(-fx-base,-45.0%);
+}
+
+.plus-minus-slider > * > .adjust-minus {
+    -fx-pref-width: 16.0;
+    -fx-pref-height: 16.0;
+    -fx-shape: "M0,0H8V2H0Z";
+    -fx-scale-shape: false;
+    -fx-effect: dropshadow(two-pass-box , -fx-shadow-highlight-color, 1.0, 0.0 , 0.0, 1.4);
+    -fx-background-color: -fx-mark-highlight-color,derive(-fx-base,-45.0%);
+}
diff --git a/controlsfx/src/main/resources/org/controlsfx/control/popover.css b/controlsfx/src/main/resources/org/controlsfx/control/popover.css
new file mode 100644
index 0000000..c9dd527
--- /dev/null
+++ b/controlsfx/src/main/resources/org/controlsfx/control/popover.css
@@ -0,0 +1,36 @@
+.popover  {
+	-fx-background-color: transparent;
+}
+
+.popover > .border {
+	-fx-stroke: linear-gradient(to bottom, rgba(0,0,0, .3), rgba(0, 0, 0, .7)) ;
+	-fx-stroke-width: 0.5;
+	-fx-fill: rgba(255.0,255.0,255.0, .95);
+	-fx-effect: dropshadow(gaussian, rgba(0,0,0,.2), 10.0, 0.5, 2.0, 2.0);
+}
+
+.popover > .content {
+}
+
+.popover > .detached {
+}
+
+.popover > .content > .title > .text  {
+	-fx-padding: 6.0 6.0 0.0 6.0;
+	-fx-text-fill: rgba(120, 120, 120, .8);
+	-fx-font-weight: bold;
+}
+
+.popover > .content > .title > .icon {
+	-fx-padding: 6.0 0.0 0.0 10.0;
+}
+
+.popover > .content > .title > .icon > .graphics > .circle {
+	-fx-fill: gray ;
+	-fx-effect: innershadow(gaussian, rgba(0,0,0,.2), 3, 0.5, 1.0, 1.0);
+}
+
+.popover > .content > .title > .icon > .graphics > .line {
+	-fx-stroke: white ;
+	-fx-stroke-width: 2;
+}
\ No newline at end of file
diff --git a/controlsfx/src/main/resources/org/controlsfx/control/propertysheet.css b/controlsfx/src/main/resources/org/controlsfx/control/propertysheet.css
new file mode 100644
index 0000000..3846aca
--- /dev/null
+++ b/controlsfx/src/main/resources/org/controlsfx/control/propertysheet.css
@@ -0,0 +1,15 @@
+/* Remove extraneous borders from PropertySheet */
+
+.property-sheet .scroll-pane .property-pane {
+    -fx-background-color: -fx-background;
+}
+
+.property-sheet .scroll-pane {
+    -fx-background-color: -fx-background;
+    -fx-background-insets: 0;
+    -fx-padding: 0;
+}
+
+.property-sheet .scroll-pane .accordion {
+    -fx-padding: -1;
+}
diff --git a/controlsfx/src/main/resources/org/controlsfx/control/rangeslider.css b/controlsfx/src/main/resources/org/controlsfx/control/rangeslider.css
new file mode 100644
index 0000000..373e9d5
--- /dev/null
+++ b/controlsfx/src/main/resources/org/controlsfx/control/rangeslider.css
@@ -0,0 +1,86 @@
+
+/*******************************************************************************
+ *                                                                             *
+ * RangeSlider                                                                 *
+ * (Largely derived from Modena styles)                                        *
+ *                                                                             *
+ ******************************************************************************/
+
+.range-slider .low-thumb, 
+.range-slider .high-thumb {
+    -fx-background-color: 
+        linear-gradient(to bottom, derive(-fx-text-box-border, -20%), derive(-fx-text-box-border, -30%)),
+        -fx-inner-border,
+        -fx-body-color;
+    -fx-background-insets: 0, 1, 2;
+    -fx-background-radius: 1.0em; /* makes sure this remains circular */
+    -fx-padding: 0.583333em;  /* 7 */
+    -fx-effect: dropshadow(two-pass-box , rgba(0, 0, 0, 0.1), 5, 0.0 , 0, 2);
+}
+
+.range-slider:focused .low-thumb,
+.range-slider:focused .high-thumb {
+    -fx-background-radius: 1.0em; /* makes sure this remains circular */
+}
+
+.range-slider .low-thumb:focused, 
+.range-slider .high-thumb:focused {
+    -fx-background-color:
+        -fx-focus-color,
+        derive(-fx-color,-36%),
+        derive(-fx-color,73%),
+        linear-gradient(to bottom, derive(-fx-color,-19%),derive(-fx-color,61%));
+    -fx-background-insets: -1.4, 0, 1, 2;
+    -fx-background-radius: 1.0em; /* makes sure this remains circular */
+}
+
+.range-slider .low-thumb:hover, 
+.range-slider .high-thumb:hover {
+    -fx-color: -fx-hover-base;
+}
+
+.range-slider .range-bar {
+    -fx-background-color: -fx-focus-color;
+}
+
+.range-slider .low-thumb:pressed,
+.range-slider .high-thumb:pressed {
+    -fx-color: -fx-pressed-base;
+}
+
+.range-slider .track {
+    -fx-background-color: 
+          -fx-shadow-highlight-color,
+          linear-gradient(to bottom, derive(-fx-text-box-border, -10%), -fx-text-box-border),
+          linear-gradient(to bottom, 
+            derive(-fx-control-inner-background, -9%),
+            derive(-fx-control-inner-background, 0%),
+            derive(-fx-control-inner-background, -5%),
+            derive(-fx-control-inner-background, -12%)
+          );
+    -fx-background-insets: 0 0 -1 0, 0, 1;
+    -fx-background-radius: 0.25em, 0.25em, 0.166667em; /* 3 3 2 */
+    -fx-padding: 0.25em; /* 3 */
+}
+
+.range-slider:vertical .track {
+    -fx-background-color: 
+          -fx-shadow-highlight-color,
+          -fx-text-box-border,
+          linear-gradient(to right, 
+            derive(-fx-control-inner-background, -9%),
+            -fx-control-inner-background,
+            derive(-fx-control-inner-background, -9%)
+          );
+}
+
+.range-slider .axis {
+    -fx-tick-label-fill: derive(-fx-text-background-color, 30%);
+    -fx-tick-length: 5px;
+    -fx-minor-tick-length: 3px;
+    -fx-border-color: null;
+}
+
+.range-slider:disabled {
+    -fx-opacity: 0.4;
+}
diff --git a/controlsfx/src/main/resources/org/controlsfx/control/rating.css b/controlsfx/src/main/resources/org/controlsfx/control/rating.css
new file mode 100644
index 0000000..393379d
--- /dev/null
+++ b/controlsfx/src/main/resources/org/controlsfx/control/rating.css
@@ -0,0 +1,15 @@
+.rating > .container {
+    -fx-spacing: 4;
+}
+.rating > .container > .button {
+    -fx-background-color: transparent;
+    -fx-background-image: url("unselected-star.png");
+    -fx-padding: 16 16; 
+    -fx-background-image-repeat: no-repeat;
+}
+.rating > .container > .button.strong {
+    -fx-background-image: url("selected-star.png");
+}
+.rating > .container > .button:hover {
+    -fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.6) , 8, 0.0 , 0 , 0 );
+}
\ No newline at end of file
diff --git a/controlsfx/src/main/resources/org/controlsfx/control/segmentedbutton.css b/controlsfx/src/main/resources/org/controlsfx/control/segmentedbutton.css
new file mode 100644
index 0000000..0e90a1d
--- /dev/null
+++ b/controlsfx/src/main/resources/org/controlsfx/control/segmentedbutton.css
@@ -0,0 +1,79 @@
+/* -------- Segmented Button ---------------- */
+.segmented-button.dark .toggle-button {
+    -fx-padding: 3 15 3 15;
+    -fx-border-color: transparent -fx-outer-border transparent transparent;
+}
+
+.segmented-button.dark .toggle-button:focused {
+    -fx-background-color:
+        rgba(23,134,248,0.2),
+        -fx-focus-color,
+        -fx-inner-border,
+        -fx-body-color;
+}
+
+.segmented-button.dark .toggle-button:selected Text {
+    -fx-effect: dropshadow( one-pass-box , rgba(0,0,0,0.9) , 2, 0.0 , 0 , 1 );
+}
+
+.segmented-button.dark .toggle-button:selected {
+    -fx-background-color:
+        -fx-shadow-highlight-color,
+        linear-gradient( to bottom, derive(-fx-color,-90%) 0%, derive(-fx-color,-60%) 100% ),        
+        linear-gradient( to bottom, derive(-fx-color,-60%) 0%, derive(-fx-color,-35%) 50%, derive(-fx-color,-30%) 98%, derive(-fx-color,-50%) 100% ),    
+        linear-gradient( to right, rgba(0,0,0,0.3) 0%, rgba(0,0,0,0) 10%, rgba(0,0,0,0) 90%, rgba(0,0,0,0.3) 100% );
+    -fx-background-insets: 0 0 -1 0, 0, 1, 1;
+    /* TODO: -fx-text-fill should be derived */
+    -fx-text-fill: -fx-light-text-color;
+}
+
+/* *************************** LEFT BUTTON ************************** */
+.segmented-button.dark .toggle-button.left-pill {
+    -fx-background-radius: 3 0 0 3;
+    -fx-background-insets: 0 0 -1 0, 0, 1 0 1 1, 2 0 2 2;
+}
+
+.segmented-button.dark .toggle-button.left-pill:focused {
+     -fx-background-insets: -2 0 -2 -2, 0 0 0 0, 1, 2;
+	-fx-border-color: transparent;
+}
+
+.segmented-button.dark .toggle-button.left-pill:selected:focused {
+    -fx-background-insets: 0 0 -1 0, 0, 1 0 1 1, 1 0 1 1;
+	-fx-border-color: transparent;
+}
+
+/* *************************** RIGHT BUTTON ************************** */
+.segmented-button.dark .toggle-button.right-pill {
+    -fx-background-radius: 0 3 3 0;
+    -fx-background-insets: 0 0 -1 0, 0, 1 1 1 0, 2 2 2 0;
+    -fx-border-color: transparent;
+}
+
+.segmented-button.dark .toggle-button.right-pill:focused {
+	-fx-background-insets: -2 -2 -2 0, 0, 1, 2;
+     -fx-border-color: transparent;
+}
+
+.segmented-button.dark .toggle-button.right-pill:selected:focused {
+	-fx-background-insets: -1 -1 -1 -1, 0 0 0 -1, 1 1 1 0, 1 1 1 0;
+    -fx-border-color: transparent;
+}
+
+/* *************************** CENTER BUTTON ************************** */
+.segmented-button.dark .toggle-button.center-pill {
+	-fx-background-radius: 0;
+	-fx-background-insets: 0 0 -1 0, 0, 1 0 1 0, 2 0 2 0;
+    
+}
+
+.segmented-button.dark .toggle-button.center-pill:focused {
+	-fx-background-radius: 0;
+	-fx-background-insets: -2 0 -2 -2, 0 0 0 -1, 1 1 1 0, 2 2 2 1;
+	-fx-border-color: transparent;
+}
+
+.segmented-button.dark .toggle-button.center-pill:selected:focused {
+	-fx-background-insets: -1.4 0 -1.4 -1, 0 0 0 -1, 1 1 1 0, 1 1 1 0;
+	-fx-border-color: transparent;
+}
\ No newline at end of file
diff --git a/controlsfx/src/main/resources/org/controlsfx/control/selected-star.png b/controlsfx/src/main/resources/org/controlsfx/control/selected-star.png
new file mode 100644
index 0000000..01b0fc0
Binary files /dev/null and b/controlsfx/src/main/resources/org/controlsfx/control/selected-star.png differ
diff --git a/controlsfx/src/main/resources/org/controlsfx/control/snapshot-view.css b/controlsfx/src/main/resources/org/controlsfx/control/snapshot-view.css
new file mode 100644
index 0000000..1aeaeee
--- /dev/null
+++ b/controlsfx/src/main/resources/org/controlsfx/control/snapshot-view.css
@@ -0,0 +1,4 @@
+
+.snapshot-view {
+
+}
\ No newline at end of file
diff --git a/controlsfx/src/main/resources/org/controlsfx/control/spreadsheet/comment.png b/controlsfx/src/main/resources/org/controlsfx/control/spreadsheet/comment.png
new file mode 100644
index 0000000..974c85b
Binary files /dev/null and b/controlsfx/src/main/resources/org/controlsfx/control/spreadsheet/comment.png differ
diff --git a/controlsfx/src/main/resources/org/controlsfx/control/spreadsheet/copySpreadsheetView.png b/controlsfx/src/main/resources/org/controlsfx/control/spreadsheet/copySpreadsheetView.png
new file mode 100644
index 0000000..de6c54d
Binary files /dev/null and b/controlsfx/src/main/resources/org/controlsfx/control/spreadsheet/copySpreadsheetView.png differ
diff --git a/controlsfx/src/main/resources/org/controlsfx/control/spreadsheet/pasteSpreadsheetView.png b/controlsfx/src/main/resources/org/controlsfx/control/spreadsheet/pasteSpreadsheetView.png
new file mode 100644
index 0000000..6228641
Binary files /dev/null and b/controlsfx/src/main/resources/org/controlsfx/control/spreadsheet/pasteSpreadsheetView.png differ
diff --git a/controlsfx/src/main/resources/org/controlsfx/control/spreadsheet/picker.png b/controlsfx/src/main/resources/org/controlsfx/control/spreadsheet/picker.png
new file mode 100644
index 0000000..69a1701
Binary files /dev/null and b/controlsfx/src/main/resources/org/controlsfx/control/spreadsheet/picker.png differ
diff --git a/controlsfx/src/main/resources/org/controlsfx/control/spreadsheet/pinSpreadsheetView.png b/controlsfx/src/main/resources/org/controlsfx/control/spreadsheet/pinSpreadsheetView.png
new file mode 100644
index 0000000..6304b62
Binary files /dev/null and b/controlsfx/src/main/resources/org/controlsfx/control/spreadsheet/pinSpreadsheetView.png differ
diff --git a/controlsfx/src/main/resources/org/controlsfx/control/spreadsheet/spreadsheet.css b/controlsfx/src/main/resources/org/controlsfx/control/spreadsheet/spreadsheet.css
new file mode 100644
index 0000000..516d395
--- /dev/null
+++ b/controlsfx/src/main/resources/org/controlsfx/control/spreadsheet/spreadsheet.css
@@ -0,0 +1,150 @@
+.cell-spreadsheet .table-row-cell {
+    -fx-background-color: transparent;
+}
+
+/* NORMAL CELL */
+.spreadsheet-cell:filled:selected,
+.spreadsheet-cell:filled:focused:selected,
+.spreadsheet-cell:filled:focused:selected:hover {
+    -fx-background-color: #8cb1ff;
+    -fx-border-color: #a9a9a9;
+    -fx-border-width : 0.5px;
+    -fx-text-fill: -fx-selection-bar-text;
+
+}
+.spreadsheet-cell:hover,
+.spreadsheet-cell:filled:focused {
+    -fx-background-color: #988490;
+    -fx-text-fill: -fx-text-inner-color;
+    -fx-background-insets: 0, 0 0 1 0;
+}
+
+.spreadsheet-cell{
+    -fx-padding: 0 0 0 0.2em;
+    -fx-border-color: black;
+    -fx-border-width : 0.3px;
+    -fx-background-color: -fx-table-cell-border-color,white;
+}
+
+.tooltip {
+    -fx-background-radius: 0px;
+    -fx-background-color:
+        linear-gradient(#cec340, #a59c31),
+        linear-gradient(#fefefc, #e6dd71),
+        linear-gradient(#fef592, #e5d848);
+    -fx-background-insets: 0,1,2;
+    -fx-padding: 0.333333em 0.666667em 0.333333em 0.666667em; /* 4 8 4 8 */
+    -fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.6) , 8, 0.0 , 0 , 0 );
+    -fx-text-fill:black;
+}
+
+/* FIXED HEADERS  */
+VerticalHeader > Label.fixed{
+    -fx-background-color: -fx-box-border, lightgray;
+    -fx-font-style : italic;
+}
+
+HorizontalHeaderColumn > TableColumnHeader.column-header.table-column.fixed{
+    -fx-background-color: -fx-box-border, lightgray;
+    -fx-font-style : italic;
+}
+
+/* HORIZONTAL AND VERTICAL HEADER SELECTION */
+VerticalHeader > Label ,
+HorizontalHeaderColumn > TableColumnHeader.column-header.table-column{
+    -fx-background-color: -fx-box-border, #F3F3F3;
+    -fx-background-insets: 0, 0 1 1 0, 1 2 2 1;
+    -fx-font-weight: bold;
+    -fx-size: 2em;
+    -fx-text-fill: -fx-selection-bar-text;
+    -fx-alignment: center;
+    -fx-font-style : normal;
+}  
+
+VerticalHeader > Label.selected{
+    -fx-background-color: #8FB1E8;
+    -fx-text-fill :white;
+}
+
+HorizontalHeaderColumn > TableColumnHeader.column-header.table-column.selected,
+HorizontalHeaderColumn > TableColumnHeader.column-header.table-column.selected > Label
+{
+    -fx-background-color:#8FB1E8;
+     -fx-text-fill :white;
+}  
+
+/* HORIZONTAL HEADER VISIBILITY */
+.column-header-background.invisible { visibility: hidden; -fx-padding: -1em; }
+
+.cell-corner{
+    -fx-background-color: red;
+}
+
+.cell-corner.top-left{
+    -fx-shape : "M 0 0 L 1 0 L 0 1 z";
+}
+
+.cell-corner.top-right{
+    -fx-shape : "M 0 0 L -1 0 L 0 1 z";
+}
+
+.cell-corner.bottom-right{
+    -fx-shape : "M 0 0 L -1 0 L 0 -1 z";
+}
+
+.cell-corner.bottom-left{
+    -fx-shape : "M 0 0 L 1 0 L 0 -1 z";
+}
+
+.indicationLabel{
+    -fx-font-style : italic;
+}
+
+/* PICKERS */
+.picker-label{
+    -fx-graphic: url("picker.png"); 
+    -fx-background-color: white;
+    -fx-padding: 0 0 0 0;
+    -fx-alignment: center;
+}
+
+.picker-label:hover{
+    /*-fx-effect:dropshadow(gaussian, black, 10, 0.1, 0, 0);*/
+    -fx-cursor:hand;
+}
+
+/* We don't want to show the white background both for TextField
+and textArea. We want it to be transparent just like Excel.
+
+Also we need to shift to the left the editor a bit*/
+CellView > .text-input.text-field{
+    -fx-padding : 0 0 0 -0.2em;
+    -fx-background-color: transparent;
+}
+CellView > .text-input.text-area,
+CellView > TextArea .scroll-pane > .viewport{
+    -fx-background-color: transparent;
+}
+
+/* I shift by 3px, it's not clean but it works for normal row (24px) as it 
+centers the textArea.*/
+CellView > TextArea .scroll-pane{
+    -fx-padding : 3px 0 0 -0.15em;
+}
+
+CellView > TextArea .scroll-pane > .viewport .content{
+    -fx-padding : 0 0 0 0;
+    -fx-background-color: transparent;
+}
+/* The scrollBars must always have the same size because we may have
+really big font in the editor (48px) and the scrollBars become obese otherwise.*/
+CellView >TextArea .scroll-bar:vertical ,
+CellView >TextArea .scroll-bar:horizontal {
+    -fx-font-size : 1em;
+}
+
+.selection-rectangle{
+    -fx-fill : transparent;
+    -fx-stroke : black;
+    -fx-stroke-width : 2;
+}
diff --git a/controlsfx/src/main/resources/org/controlsfx/control/statusbar.css b/controlsfx/src/main/resources/org/controlsfx/control/statusbar.css
new file mode 100644
index 0000000..09f669d
--- /dev/null
+++ b/controlsfx/src/main/resources/org/controlsfx/control/statusbar.css
@@ -0,0 +1,6 @@
+.status-bar {
+	-fx-padding: 4px;
+	-fx-pref-height: 30px;
+	-fx-background-color: lightgray, -fx-body-color;
+	-fx-background-insets: 0, 1
+}
diff --git a/controlsfx/src/main/resources/org/controlsfx/control/taskprogressview.css b/controlsfx/src/main/resources/org/controlsfx/control/taskprogressview.css
new file mode 100644
index 0000000..2c88aba
--- /dev/null
+++ b/controlsfx/src/main/resources/org/controlsfx/control/taskprogressview.css
@@ -0,0 +1,53 @@
+
+.task-progress-view  {
+ 	-fx-background-color: white;
+}
+
+.task-progress-view > * > .label {
+ 	-fx-text-fill: gray;
+ 	-fx-font-size: 18.0;
+ 	-fx-alignment: center;
+ 	-fx-padding: 10.0 0.0 5.0 0.0;
+}
+
+.task-progress-view > * > .list-view  {
+ 	-fx-border-color: transparent;
+ 	-fx-background-color: transparent;
+}
+
+.task-title {
+	-fx-font-weight: bold;
+}
+
+.task-progress-bar .bar {
+	-fx-padding: 6px;
+	-fx-background-radius: 0;
+	-fx-border-radius: 0;
+}
+
+.task-progress-bar .track {
+	-fx-background-radius: 0;
+}
+
+.task-message {
+}
+
+.task-list-cell {
+    -fx-background-color: transparent;
+    -fx-padding: 4 10 8 10;
+    -fx-border-color: transparent transparent linear-gradient(from 0.0% 0.0% to 100.0% 100.0%, transparent, rgba(0.0,0.0,0.0,0.2), transparent) transparent;
+}
+
+.task-list-cell-empty {
+	-fx-background-color: transparent;
+    -fx-border-color: transparent;
+}
+
+.task-cancel-button {
+	-fx-base-color: red;
+	-fx-font-size: .75em;
+	-fx-font-weight: bold;
+	-fx-padding: 4px;
+	-fx-border-radius: 0;
+	-fx-background-radius: 0;
+}
\ No newline at end of file
diff --git a/controlsfx/src/main/resources/org/controlsfx/control/textfield/autocompletion.css b/controlsfx/src/main/resources/org/controlsfx/control/textfield/autocompletion.css
new file mode 100644
index 0000000..2e22806
--- /dev/null
+++ b/controlsfx/src/main/resources/org/controlsfx/control/textfield/autocompletion.css
@@ -0,0 +1,31 @@
+/**
+ * Style based on Modena.css combo-box-popup style
+ */
+
+.auto-complete-popup > .list-view {
+    -fx-background-color:
+        linear-gradient(to bottom,
+            derive(-fx-color,-17%),
+            derive(-fx-color,-30%)
+        ),
+        -fx-control-inner-background;
+    -fx-background-insets: -1 -2 -1 -1, 0 -1 0 0;
+    -fx-effect: dropshadow( gaussian , rgba(0,0,0,0.2) , 12, 0.0 , 0 , 8 );
+}
+.auto-complete-popup > .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell {
+    -fx-padding: 4 0 4 5;
+    /* No alternate highlighting */
+    -fx-background: -fx-control-inner-background;
+}
+.auto-complete-popup > .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected {
+    -fx-background:  -fx-selection-bar-non-focused;
+    -fx-background-color:  -fx-background;
+}
+.auto-complete-popup  > .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:hover,
+.auto-complete-popup  > .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected:hover {
+    -fx-background: -fx-accent;
+    -fx-background-color: -fx-selection-bar;
+}
+.auto-complete-popup > .list-view > .placeholder > .label {
+    -fx-text-fill: derive(-fx-control-inner-background,-30%);
+}
\ No newline at end of file
diff --git a/controlsfx/src/main/resources/org/controlsfx/control/textfield/customtextfield.css b/controlsfx/src/main/resources/org/controlsfx/control/textfield/customtextfield.css
new file mode 100644
index 0000000..f8e08cf
--- /dev/null
+++ b/controlsfx/src/main/resources/org/controlsfx/control/textfield/customtextfield.css
@@ -0,0 +1,89 @@
+/**************************************************************************
+ * 
+ * CustomTextField
+ * 
+ **************************************************************************/
+
+.custom-text-field {
+    -fx-text-fill: -fx-text-inner-color;
+    -fx-highlight-fill: derive(-fx-control-inner-background,-20%);
+    -fx-highlight-text-fill: -fx-text-inner-color;
+    -fx-prompt-text-fill: derive(-fx-control-inner-background,-30%);
+    -fx-background-color: linear-gradient(to bottom, derive(-fx-text-box-border, -10%), -fx-text-box-border),
+        linear-gradient(from 0px 0px to 0px 5px, derive(-fx-control-inner-background, -9%), -fx-control-inner-background);
+    -fx-background-insets: 0, 1;
+    -fx-background-radius: 3, 2;
+    
+}
+
+/*
+.custom-text-field {
+    -fx-background-color: null;
+    -fx-background-insets: 0;
+}
+*/
+.custom-text-field:no-side-nodes {
+    -fx-padding: 0.333333em 0.583em 0.333333em 0.583em; 
+}
+
+.custom-text-field:left-node-visible {
+    -fx-padding: 0.333333em 0.583em 0.333333em 0; 
+}
+
+.custom-text-field:right-node-visible {
+    -fx-padding: 0.333333em 0 0.333333em 0.583em;
+}
+
+.custom-text-field:left-node-visible:right-node-visible {
+    -fx-padding: 0.333333em 0 0.333333em 0; 
+}
+
+.custom-text-field:left-node-visible .left-pane {
+    -fx-padding: 0 3 0 3;
+}
+
+.custom-text-field:right-node-visible .right-pane {
+    -fx-padding: 0 3 0 3; 
+}
+
+.custom-text-field:focused, 
+.custom-text-field:text-field-has-focus {
+    -fx-highlight-fill: -fx-accent;
+    -fx-highlight-text-fill: white;
+    -fx-background-color: 
+        -fx-focus-color,
+        -fx-control-inner-background,
+        -fx-faint-focus-color,
+        linear-gradient(from 0px 0px to 0px 5px, derive(-fx-control-inner-background, -9%), -fx-control-inner-background);
+    -fx-background-insets: -0.2, 1, -1.4, 3;
+    -fx-background-radius: 3, 2, 4, 0;
+    -fx-prompt-text-fill: transparent;
+}
+
+
+
+
+/**************************************************************************
+ * 
+ * Clearable Text / Password Field
+ * 
+ **************************************************************************/
+
+.clearable-field .clear-button {
+    -fx-padding: 0 3 0 0;
+}
+
+.clearable-field .clear-button > .graphic {
+    -fx-background-color: #949494;
+    -fx-scale-shape: false;
+    -fx-padding: 4.5 4.5 4.5 4.5; /* Graphic is 9x9 px */
+    -fx-shape: "M395.992,296.758l1.794-1.794l7.292,7.292l-1.795,1.794 L395.992,296.758z M403.256,294.992l1.794,1.794l-7.292,7.292l-1.794-1.795 L403.256,294.992z";
+}
+
+.clearable-field .clear-button:hover > .graphic {
+    -fx-background-color: #ee4444;
+}
+
+.clearable-field .clear-button:pressed > .graphic {
+    -fx-background-color: #ff1111;
+}
\ No newline at end of file
diff --git a/controlsfx/src/main/resources/org/controlsfx/control/toggleswitch.css b/controlsfx/src/main/resources/org/controlsfx/control/toggleswitch.css
new file mode 100644
index 0000000..022de66
--- /dev/null
+++ b/controlsfx/src/main/resources/org/controlsfx/control/toggleswitch.css
@@ -0,0 +1,51 @@
+/*******************************************************************************
+ *                                                                             *
+ * ToggleSwitch                                                                *
+ *                                                                             *
+ ******************************************************************************/
+.toggle-switch{
+    -thumb-move-animation-time: 200;
+}
+
+.toggle-switch .text {
+    -fx-font-size: 1em;
+    -fx-text-fill: -fx-text-base-color;
+}
+
+.toggle-switch .thumb {
+    -fx-background-color: linear-gradient(to bottom, derive(-fx-text-box-border, -20%), derive(-fx-text-box-border, -30%)),
+                            -fx-inner-border,
+                            -fx-body-color;
+    -fx-background-insets: 0, 1, 2;
+    -fx-background-radius: 1.0em; /* large value to make sure this remains circular */
+    -fx-padding: 0.75em;
+    -fx-alignment: CENTER;
+    -fx-content-display: LEFT;
+}
+
+.toggle-switch .thumb-area{
+    -fx-background-radius: 1em;
+    -fx-background-color: linear-gradient(to bottom, derive(-fx-text-box-border, -20%), derive(-fx-text-box-border, -30%)), #f5f5f5;
+    -fx-background-insets: 0, 1;
+}
+
+.toggle-switch:hover .thumb{
+    -fx-color: -fx-hover-base
+}
+
+.toggle-switch:selected .thumb-area{
+    -fx-background-color: linear-gradient(to bottom, derive(-fx-text-box-border, -20%), derive(-fx-text-box-border, -30%)),
+                            linear-gradient(to bottom, derive(#0b99c9, 30%), #0b99c9);
+    -fx-background-insets: 0, 1;
+
+}
+
+.toggle-switch .thumb-area
+{
+    -fx-padding: 0.75em 1.333333em 0.75em 1.333333em; /* 7 16 7 16 */
+}
+
+.toggle-switch:disabled
+{
+    -fx-opacity: 0.4;
+}
diff --git a/controlsfx/src/main/resources/org/controlsfx/control/unselected-star.png b/controlsfx/src/main/resources/org/controlsfx/control/unselected-star.png
new file mode 100644
index 0000000..774c240
Binary files /dev/null and b/controlsfx/src/main/resources/org/controlsfx/control/unselected-star.png differ
diff --git a/controlsfx/src/main/resources/org/controlsfx/dialog/arrow-green-right.png b/controlsfx/src/main/resources/org/controlsfx/dialog/arrow-green-right.png
new file mode 100644
index 0000000..63b675c
Binary files /dev/null and b/controlsfx/src/main/resources/org/controlsfx/dialog/arrow-green-right.png differ
diff --git a/controlsfx/src/main/resources/org/controlsfx/dialog/commandlink.css b/controlsfx/src/main/resources/org/controlsfx/dialog/commandlink.css
new file mode 100644
index 0000000..af693b3
--- /dev/null
+++ b/controlsfx/src/main/resources/org/controlsfx/dialog/commandlink.css
@@ -0,0 +1,71 @@
+/*******************************************************************************
+ *                                                                             *
+ * Command Link                                                                *
+ *                                                                             *
+ ******************************************************************************/
+ /* For the text displayed above command-link buttons (but below the header) */
+.command-links-dialog.dialog-pane > .container > .command-link-message {
+    -fx-font-size: 1.25em; 
+}
+
+.command-links-dialog.dialog-pane > .container > .command-link-button {
+    -fx-padding: 10 10 10 10;
+    -fx-background-color: transparent;
+    -fx-background-insets: 0;
+    -fx-border-color: transparent;
+    -fx-border-width: 1;
+    -fx-border-radius: 3px;
+}
+
+.command-links-dialog.dialog-pane > .container > .command-link-button:hover {
+    -fx-border-color: -fx-box-border;
+    -fx-background-color: linear-gradient(to bottom, 
+        white, 
+        derive(-fx-box-border, 60%)
+    );
+}
+
+.command-links-dialog.dialog-pane > .container > .command-link-button:armed  {
+    -fx-background-color: linear-gradient(to bottom, 
+        white, 
+        derive(-fx-box-border, 40%)
+    );
+}
+
+.command-links-dialog.dialog-pane > .container > .command-link-button:default {
+    -fx-border-color: -fx-default-button;
+    -fx-background-color: linear-gradient(to bottom, 
+        white, 
+        derive(-fx-default-button, 80%)
+    );
+}
+
+.command-links-dialog.dialog-pane > .container > .command-link-button:default:hover {
+    -fx-border-color: -fx-default-button;
+    -fx-background-color: linear-gradient(to bottom, 
+        white, 
+        derive(-fx-default-button, 60%)
+    );
+}
+
+.command-links-dialog.dialog-pane > .container > .command-link-button:default:armed {
+    -fx-border-color: -fx-default-button;
+    -fx-background-color: linear-gradient(to bottom, 
+        white, 
+        derive(-fx-default-button, 40%)
+    );
+}
+
+.command-links-dialog.dialog-pane > .container > .command-link-button > .container > .line-1 {
+    -fx-font-size: 1.25em;
+    -fx-padding: -5 0 0 5;
+}
+
+.command-links-dialog.dialog-pane > .container > .command-link-button > .container > .line-2 {
+    -fx-font-size: 1em; 
+    -fx-padding: 0 0 0 5;
+}
+
+.command-links-dialog.dialog-pane > .container > .command-link-button > .container > .graphic-container {
+    -fx-padding: 10 10 0 0
+}
\ No newline at end of file
diff --git a/controlsfx/src/main/resources/org/controlsfx/dialog/dialog-confirm.png b/controlsfx/src/main/resources/org/controlsfx/dialog/dialog-confirm.png
new file mode 100644
index 0000000..adb569b
Binary files /dev/null and b/controlsfx/src/main/resources/org/controlsfx/dialog/dialog-confirm.png differ
diff --git a/controlsfx/src/main/resources/org/controlsfx/dialog/dialog-error.png b/controlsfx/src/main/resources/org/controlsfx/dialog/dialog-error.png
new file mode 100644
index 0000000..769d7df
Binary files /dev/null and b/controlsfx/src/main/resources/org/controlsfx/dialog/dialog-error.png differ
diff --git a/controlsfx/src/main/resources/org/controlsfx/dialog/dialog-information.png b/controlsfx/src/main/resources/org/controlsfx/dialog/dialog-information.png
new file mode 100644
index 0000000..a220108
Binary files /dev/null and b/controlsfx/src/main/resources/org/controlsfx/dialog/dialog-information.png differ
diff --git a/controlsfx/src/main/resources/org/controlsfx/dialog/dialog-warning.png b/controlsfx/src/main/resources/org/controlsfx/dialog/dialog-warning.png
new file mode 100644
index 0000000..a374f4f
Binary files /dev/null and b/controlsfx/src/main/resources/org/controlsfx/dialog/dialog-warning.png differ
diff --git a/controlsfx/src/main/resources/org/controlsfx/dialog/dialogs.css b/controlsfx/src/main/resources/org/controlsfx/dialog/dialogs.css
new file mode 100644
index 0000000..2e93ee5
--- /dev/null
+++ b/controlsfx/src/main/resources/org/controlsfx/dialog/dialogs.css
@@ -0,0 +1,13 @@
+.progress-dialog.dialog-pane {
+    -fx-graphic: url("dialog-information.png");
+}
+
+.font-selector-dialog.dialog-pane,
+.login-dialog.dialog-pane,
+.command-links-dialog.dialog-pane {
+    -fx-graphic: url("dialog-confirm.png");
+}
+
+.exception-dialog.dialog-pane {
+    -fx-graphic: url("dialog-error.png");
+}
\ No newline at end of file
diff --git a/controlsfx/src/main/resources/org/controlsfx/dialog/fewer-details.png b/controlsfx/src/main/resources/org/controlsfx/dialog/fewer-details.png
new file mode 100644
index 0000000..c45d4ac
Binary files /dev/null and b/controlsfx/src/main/resources/org/controlsfx/dialog/fewer-details.png differ
diff --git a/controlsfx/src/main/resources/org/controlsfx/dialog/license.txt b/controlsfx/src/main/resources/org/controlsfx/dialog/license.txt
new file mode 100644
index 0000000..07c519b
--- /dev/null
+++ b/controlsfx/src/main/resources/org/controlsfx/dialog/license.txt
@@ -0,0 +1,7 @@
+Some of these images are from the Oxygen icon set.
+Oxygen icons are licensed under the 
+Creative Common Attribution-ShareAlike 3.0 License
+http://creativecommons.org/licenses/by-sa/3.0/
+
+For reference, these dialog icons were taken from
+http://websvn.kde.org/trunk/KDE/kdebase/runtime/pics/oxygen/?pathrev=706996
\ No newline at end of file
diff --git a/controlsfx/src/main/resources/org/controlsfx/dialog/lock.png b/controlsfx/src/main/resources/org/controlsfx/dialog/lock.png
new file mode 100644
index 0000000..9a7a9f6
Binary files /dev/null and b/controlsfx/src/main/resources/org/controlsfx/dialog/lock.png differ
diff --git a/controlsfx/src/main/resources/org/controlsfx/dialog/more-details.png b/controlsfx/src/main/resources/org/controlsfx/dialog/more-details.png
new file mode 100644
index 0000000..b8896c7
Binary files /dev/null and b/controlsfx/src/main/resources/org/controlsfx/dialog/more-details.png differ
diff --git a/controlsfx/src/main/resources/org/controlsfx/dialog/user.png b/controlsfx/src/main/resources/org/controlsfx/dialog/user.png
new file mode 100644
index 0000000..2bf6711
Binary files /dev/null and b/controlsfx/src/main/resources/org/controlsfx/dialog/user.png differ
diff --git a/controlsfx/src/main/resources/org/controlsfx/dialog/wizard-page.png b/controlsfx/src/main/resources/org/controlsfx/dialog/wizard-page.png
new file mode 100644
index 0000000..3c86ba7
Binary files /dev/null and b/controlsfx/src/main/resources/org/controlsfx/dialog/wizard-page.png differ
diff --git a/controlsfx/src/main/resources/org/controlsfx/dialog/wizard.css b/controlsfx/src/main/resources/org/controlsfx/dialog/wizard.css
new file mode 100644
index 0000000..d8e2689
--- /dev/null
+++ b/controlsfx/src/main/resources/org/controlsfx/dialog/wizard.css
@@ -0,0 +1,3 @@
+.wizard-pane { 
+	 -fx-graphic: url("wizard-page.png"); 
+}
\ No newline at end of file
diff --git a/controlsfx/src/main/resources/org/controlsfx/glyphfont/glyphfont.css b/controlsfx/src/main/resources/org/controlsfx/glyphfont/glyphfont.css
new file mode 100644
index 0000000..2803ef3
--- /dev/null
+++ b/controlsfx/src/main/resources/org/controlsfx/glyphfont/glyphfont.css
@@ -0,0 +1,16 @@
+
+.glyph-font {
+
+}
+
+.glyph-font.gradient{
+    -fx-effect: innershadow( three-pass-box , derive(-fx-text-fill,-70%) , 0.1em, 0.0 , 0.07em, 0.07em );
+}
+
+.glyph-font.hover-effect:hover{
+    -fx-effect: dropshadow( three-pass-box , derive(-fx-text-fill,0%) , 0.01em, 0.0 , 0, 0);
+}
+
+.glyph-font.hover-effect:selected{
+    -fx-effect: dropshadow( three-pass-box , derive(-fx-text-fill,0%) , 0.01em, 0.0 , 0, 0);
+}
\ No newline at end of file
diff --git a/controlsfx/src/test/java/org/controlsfx/control/CheckTreeViewTest.java b/controlsfx/src/test/java/org/controlsfx/control/CheckTreeViewTest.java
new file mode 100644
index 0000000..1624e7d
--- /dev/null
+++ b/controlsfx/src/test/java/org/controlsfx/control/CheckTreeViewTest.java
@@ -0,0 +1,82 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.controlsfx.control;
+
+import javafx.scene.control.CheckBoxTreeItem;
+import javafx.scene.control.SelectionMode;
+import org.controlsfx.control.spreadsheet.JavaFXThreadingRule;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+public class CheckTreeViewTest {
+    @Rule public JavaFXThreadingRule javafxRule = new JavaFXThreadingRule();
+    
+    private CheckTreeView<String> checkTreeView;
+    
+    private final CheckBoxTreeItem<String> treeItem_Jonathan = new CheckBoxTreeItem<>("Jonathan");
+    private final CheckBoxTreeItem<String> treeItem_Eugene = new CheckBoxTreeItem<>("Eugene");
+    private final CheckBoxTreeItem<String> treeItem_Henry = new CheckBoxTreeItem<>("Henry");
+    private final CheckBoxTreeItem<String> treeItem_Samir = new CheckBoxTreeItem<>("Samir");
+    
+    public CheckTreeViewTest() {
+    }
+    
+    @Before
+    public void setUp() {
+        CheckBoxTreeItem<String> root = new CheckBoxTreeItem<String>("Root");
+        root.setExpanded(true);
+        root.getChildren().addAll(
+                treeItem_Jonathan,
+                treeItem_Eugene,
+                treeItem_Henry,
+                treeItem_Samir);
+        
+        // lets check Eugene to make sure that it shows up in the tree
+        treeItem_Eugene.setSelected(true);
+        
+        // CheckListView
+        checkTreeView = new CheckTreeView<>(root);
+        checkTreeView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
+    }
+    
+    @After
+    public void tearDown() {
+    }
+
+    /**
+     * This is related to https://bitbucket.org/controlsfx/controlsfx/issue/447
+     * We test if the clearChecks raise ConcurrentModificationException.
+     */
+    @Test
+    public void testConcurrentModification() {
+       checkTreeView.getCheckModel().checkAll();
+       checkTreeView.getCheckModel().clearChecks();
+    }
+}
diff --git a/controlsfx/src/test/java/org/controlsfx/control/spreadsheet/GridBaseTest.java b/controlsfx/src/test/java/org/controlsfx/control/spreadsheet/GridBaseTest.java
new file mode 100644
index 0000000..f383ebd
--- /dev/null
+++ b/controlsfx/src/test/java/org/controlsfx/control/spreadsheet/GridBaseTest.java
@@ -0,0 +1,232 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.spreadsheet;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import org.controlsfx.control.spreadsheet.SpreadsheetView.SpanType;
+import org.junit.Before;
+import org.junit.Test;
+import static org.junit.Assert.*;
+import org.junit.Rule;
+
+public class GridBaseTest {
+    @Rule public JavaFXThreadingRule javafxRule = new JavaFXThreadingRule();
+    
+    private GridBase grid;
+
+    public GridBaseTest() {
+    }
+
+    private GridBase buildGrid() {
+        GridBase tempGrid;
+        tempGrid = new GridBase(15, 15);
+        List<ObservableList<SpreadsheetCell>> rows = FXCollections.observableArrayList();
+
+        for (int row = 0; row < tempGrid.getRowCount(); ++row) {
+            ObservableList<SpreadsheetCell> currentRow = FXCollections.observableArrayList();
+            for (int column = 0; column < tempGrid.getColumnCount(); ++column) {
+                currentRow.add(SpreadsheetCellType.STRING.createCell(row, column, 1, 1, ""));
+            }
+            rows.add(currentRow);
+        }
+        tempGrid.setRows(rows);
+        return tempGrid;
+    }
+
+    @Before
+    public void setUp() {
+        grid = buildGrid();
+    }
+
+    /**
+     * Test of setCellValue method, of class GridBase.
+     */
+    @Test public void testSetCellValue() {
+        String value = "The cake is a lie";
+        grid.setCellValue(0, 0, value);
+        assertEquals(value, grid.getRows().get(0).get(0).getItem());
+    }
+
+    /**
+     * Test of getRowCount method, of class GridBase.
+     */
+    @Test public void testGetRowCount() {
+        ObservableList<SpreadsheetCell> list = FXCollections.observableArrayList();
+        grid.getRows().add(list);
+
+        assertEquals(16, grid.getRowCount());
+    }
+
+    /**
+     * Test of getSpanType method, of class GridBase.
+     */
+    @Test public void testGetSpanType() {
+        SpreadsheetView spv = new SpreadsheetView(grid);
+        SpreadsheetView.SpanType type = SpanType.NORMAL_CELL;
+
+        assertEquals(type, grid.getSpanType(spv, -1, -1));
+        assertEquals(type, grid.getSpanType(spv, Integer.MAX_VALUE, Integer.MAX_VALUE));
+        assertEquals(type, grid.getSpanType(spv, Integer.MAX_VALUE, -1));
+        assertEquals(type, grid.getSpanType(spv, -1, Integer.MAX_VALUE));
+        assertEquals(type, grid.getSpanType(spv, grid.getRowCount(), grid.getColumnCount()));
+        
+        grid.spanColumn(5, 0, 0);
+        assertEquals(SpanType.NORMAL_CELL, grid.getSpanType(spv, 0, 0));
+        assertEquals(SpanType.COLUMN_SPAN_INVISIBLE, grid.getSpanType(spv, 0, 1));
+        assertEquals(SpanType.COLUMN_SPAN_INVISIBLE, grid.getSpanType(spv, 0, 2));
+        assertEquals(SpanType.COLUMN_SPAN_INVISIBLE, grid.getSpanType(spv, 0, 3));
+        assertEquals(SpanType.COLUMN_SPAN_INVISIBLE, grid.getSpanType(spv, 0, 4));
+        
+        grid.spanRow(5, 0, 0);
+        assertEquals(SpanType.ROW_VISIBLE, grid.getSpanType(spv, 0, 0));
+        assertEquals(SpanType.ROW_SPAN_INVISIBLE, grid.getSpanType(spv, 1, 0));
+        assertEquals(SpanType.ROW_SPAN_INVISIBLE, grid.getSpanType(spv, 2, 0));
+        assertEquals(SpanType.ROW_SPAN_INVISIBLE, grid.getSpanType(spv, 3, 0));
+        assertEquals(SpanType.ROW_SPAN_INVISIBLE, grid.getSpanType(spv, 4, 0));
+        
+        assertEquals(SpanType.BOTH_INVISIBLE, grid.getSpanType(spv, 1, 1));
+        assertEquals(SpanType.BOTH_INVISIBLE, grid.getSpanType(spv, 3, 4));
+        assertEquals(SpanType.BOTH_INVISIBLE, grid.getSpanType(spv, 2, 1));
+    }
+
+    /**
+     * Test of getRowHeight method, of class GridBase.
+     */
+    @Test public void testGetRowHeight() {
+        Map<Integer, Double> rowHeight = new HashMap<>();
+        rowHeight.put(1, 100.0);
+        rowHeight.put(5, 12.0);
+
+        grid.setRowHeightCallback(new GridBase.MapBasedRowHeightFactory(rowHeight));
+
+        double result = grid.getRowHeight(1);
+        assertEquals(100.0, result, 0.0);
+
+        result = grid.getRowHeight(5);
+        assertEquals(12.0, result, 0.0);
+    }
+
+    /**
+     * Test of setLocked method, of class GridBase.
+     */
+    @Test public void testSetLocked() {
+        assertFalse(grid.isLocked());
+
+        grid.setLocked(true);
+        assertTrue(grid.isLocked());
+
+        String value = "The cake is a lie";
+        grid.setCellValue(0, 0, value);
+        assertEquals("", grid.getRows().get(0).get(0).getItem());
+    }
+
+    /**
+     * Test of spanRow method, of class GridBase.
+     */
+    @Test public void testSpanRow() {
+        grid.spanRow(0, 0, 0);
+        assertEquals(1, grid.getRows().get(0).get(0).getRowSpan());
+
+        grid.spanRow(-1, -1, -1);
+        grid.spanRow(0, -1, 0);
+        grid.spanRow(0, 0, -1);
+        grid.spanRow(-1, 0, 0);
+        grid.spanRow(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE);
+        grid.spanRow(2, grid.getRowCount(), grid.getColumnCount());
+        
+        grid.spanRow(1, 0, 0);
+        assertEquals(1, grid.getRows().get(0).get(0).getRowSpan());
+
+        grid.spanRow(2, 0, 0);
+        assertEquals(2, grid.getRows().get(0).get(0).getRowSpan());
+        assertEquals(2, grid.getRows().get(1).get(0).getRowSpan());
+
+        grid.spanRow(3, 0, 0);
+        SpreadsheetCell cell = grid.getRows().get(0).get(0);
+        assertEquals(cell, grid.getRows().get(1).get(0));
+        assertEquals(cell, grid.getRows().get(2).get(0));
+    }
+
+    /**
+     * Test of mixed Span.
+     */
+    @Test public void testSpanBoth() {
+        grid.spanRow(4, 0, 0);
+        grid.spanColumn(5, 0, 0);
+        SpreadsheetCell cell = grid.getRows().get(0).get(0);
+        for (int i = 0; i < 4; ++i) {
+            for (int j = 0; j < 5; j++) {
+                assertEquals(cell, grid.getRows().get(i).get(j));
+            }
+        }
+    }
+
+    /**
+     * Test of mixed Span.
+     */
+    @Test  public void testSpanBoth2() {
+        grid.spanColumn(5, 0, 0);
+        grid.spanRow(4, 0, 0);
+        SpreadsheetCell cell = grid.getRows().get(0).get(0);
+        for (int i = 0; i < 4; ++i) {
+            for (int j = 0; j < 5; j++) {
+                assertEquals(cell, grid.getRows().get(i).get(j));
+            }
+        }
+    }
+
+    /**
+     * Test of spanColumn method, of class GridBase.
+     */
+    @Test public void testSpanColumn() {
+        grid.spanColumn(0, 0, 0);
+        assertEquals(1, grid.getRows().get(0).get(0).getColumnSpan());
+
+        grid.spanColumn(-1, -1, -1);
+        grid.spanColumn(0, -1, 0);
+        grid.spanColumn(0, 0, -1);
+        grid.spanColumn(-1, 0, 0);
+        grid.spanColumn(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE);
+        grid.spanColumn(2, grid.getRowCount(), grid.getColumnCount());
+        
+        grid.spanColumn(1, 0, 0);
+        assertEquals(1, grid.getRows().get(0).get(0).getColumnSpan());
+
+        grid.spanColumn(2, 0, 0);
+        assertEquals(2, grid.getRows().get(0).get(0).getColumnSpan());
+        assertEquals(2, grid.getRows().get(0).get(1).getColumnSpan());
+
+        grid.spanColumn(3, 0, 0);
+        SpreadsheetCell cell = grid.getRows().get(0).get(0);
+        assertEquals(cell, grid.getRows().get(0).get(1));
+        assertEquals(cell, grid.getRows().get(0).get(2));
+    }
+}
diff --git a/controlsfx/src/test/java/org/controlsfx/control/spreadsheet/JavaFXThreadingRule.java b/controlsfx/src/test/java/org/controlsfx/control/spreadsheet/JavaFXThreadingRule.java
new file mode 100644
index 0000000..c0db14f
--- /dev/null
+++ b/controlsfx/src/test/java/org/controlsfx/control/spreadsheet/JavaFXThreadingRule.java
@@ -0,0 +1,116 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.controlsfx.control.spreadsheet;
+
+import java.util.concurrent.CountDownLatch;
+
+import javax.swing.SwingUtilities;
+
+import javafx.application.Platform;
+import javafx.embed.swing.JFXPanel;
+
+import org.junit.Rule;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+/**
+ * A JUnit {@link Rule} for running tests on the JavaFX thread and performing
+ * JavaFX initialisation. To include in your test case, add the following code:
+ * 
+* <pre>
+ * {@literal @}Rule
+ * public JavaFXThreadingRule jfxRule = new JavaFXThreadingRule();
+ * </pre>
+ * 
+ * 
+*/
+public class JavaFXThreadingRule implements TestRule {
+
+    /**
+     * Flag for setting up the JavaFX, we only need to do this once for all
+     * tests.
+     */
+    private static boolean jfxIsSetup;
+
+    @Override
+    public Statement apply(Statement statement, Description description) {
+        return new OnJFXThreadStatement(statement);
+    }
+
+    private static class OnJFXThreadStatement extends Statement {
+
+        private final Statement statement;
+
+        public OnJFXThreadStatement(Statement aStatement) {
+            statement = aStatement;
+        }
+
+        private Throwable rethrownException = null;
+
+        @Override
+        public void evaluate() throws Throwable {
+            if (!jfxIsSetup) {
+                setupJavaFX();
+                jfxIsSetup = true;
+            }
+            final CountDownLatch countDownLatch = new CountDownLatch(1);
+            Platform.runLater(new Runnable() {
+                @Override
+                public void run() {
+                    try {
+                        statement.evaluate();
+                    } catch (Throwable e) {
+                        rethrownException = e;
+                    }
+                    countDownLatch.countDown();
+                }
+            });
+            countDownLatch.await();
+// if an exception was thrown by the statement during evaluation,
+// then re-throw it to fail the test
+            if (rethrownException != null) {
+                throw rethrownException;
+            }
+        }
+
+        protected void setupJavaFX() throws InterruptedException {
+            long timeMillis = System.currentTimeMillis();
+            final CountDownLatch latch = new CountDownLatch(1);
+            SwingUtilities.invokeLater(new Runnable() {
+                public void run() {
+// initializes JavaFX environment
+                    new JFXPanel();
+                    latch.countDown();
+                }
+            });
+            System.out.println("javafx initialising...");
+            latch.await();
+            System.out.println("javafx is initialised in " + (System.currentTimeMillis() - timeMillis) + "ms");
+        }
+    }
+}
diff --git a/controlsfx/src/test/java/org/controlsfx/control/spreadsheet/SpreadsheetViewTest.java b/controlsfx/src/test/java/org/controlsfx/control/spreadsheet/SpreadsheetViewTest.java
new file mode 100644
index 0000000..f5f4e45
--- /dev/null
+++ b/controlsfx/src/test/java/org/controlsfx/control/spreadsheet/SpreadsheetViewTest.java
@@ -0,0 +1,401 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.controlsfx.control.spreadsheet;
+
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.scene.control.TablePosition;
+import org.controlsfx.control.spreadsheet.SpreadsheetView.SpanType;
+import org.junit.*;
+
+import java.util.*;
+
+import static org.junit.Assert.*;
+
+public class SpreadsheetViewTest {
+    @Rule public JavaFXThreadingRule javafxRule = new JavaFXThreadingRule();
+    
+    private SpreadsheetView spv;
+
+    public SpreadsheetViewTest() {
+    }
+
+    @Before
+    public void setUp() {
+        //100 rows and 15 columns
+        spv = new SpreadsheetView();
+    }
+
+    private GridBase buildGrid() {
+        GridBase tempGrid;
+        tempGrid = new GridBase(15, 15);
+        List<ObservableList<SpreadsheetCell>> rows = FXCollections.observableArrayList();
+
+        for (int row = 0; row < tempGrid.getRowCount(); ++row) {
+            ObservableList<SpreadsheetCell> currentRow = FXCollections.observableArrayList();
+            for (int column = 0; column < tempGrid.getColumnCount(); ++column) {
+                currentRow.add(SpreadsheetCellType.STRING.createCell(row, column, 1, 1, ""));
+            }
+            rows.add(currentRow);
+        }
+        tempGrid.setRows(rows);
+        return tempGrid;
+    }
+    
+    private static class NonSerializableClass{
+        
+    }
+    
+    
+    /**
+     * We test that a non-serializable item can be put into the Grid, and then
+     * try to copy without throwing an exception.
+     */
+    @Test public void testCopyClipBoard() {
+        spv.getSelectionModel().select(0, spv.getColumns().get(0));
+
+        Grid grid = spv.getGrid();
+        SpreadsheetCell cell = new SpreadsheetCellBase(0, 0, 1, 1, SpreadsheetCellType.OBJECT);
+        cell.setItem(new NonSerializableClass());
+        grid.getRows().get(0).set(0, cell);
+
+        spv.copyClipboard();
+    }
+    
+    /**
+     * We test that a null item does not throw exception in copyClipboard.
+     */
+    @Test public void testCopyClipBoardNull() {
+        spv.getSelectionModel().select(0, spv.getColumns().get(0));
+
+        Grid grid = spv.getGrid();
+        SpreadsheetCell cell = new SpreadsheetCellBase(0, 0, 1, 1, SpreadsheetCellType.OBJECT);
+        cell.setItem(null);
+        grid.getRows().get(0).set(0, cell);
+
+        spv.copyClipboard();
+    }
+
+    /**
+     * Try to select a cell, then set a new grid, and verify that the
+     * selectedCells are well updated because we have modified the TableColumn
+     * so the TablePosition are normally wrong.
+     *
+     */
+    @Test public void testSelectionModel(){
+        spv.getSelectionModel().select(10, spv.getColumns().get(10));
+        spv.setGrid(buildGrid());
+
+        if (spv.getSelectionModel().getSelectedCells().size() != 1) {
+            fail();
+        }
+
+        TablePosition position = spv.getSelectionModel().getSelectedCells().get(0);
+        assertEquals(10, position.getRow());
+        assertEquals(10, position.getColumn());
+    }
+    /**
+     * Test of isRowFixable method, of class SpreadsheetView.
+     */
+    @Test public void testIsRowFixable() {
+        Grid grid = spv.getGrid();
+        //Normal
+        int row = 0;
+        Assert.assertTrue(spv.isRowFixable(row));
+
+        row = -1;
+        Assert.assertFalse(spv.isRowFixable(row));
+
+        row = Integer.MAX_VALUE;
+        Assert.assertFalse(spv.isRowFixable(row));
+
+        grid.spanColumn(5, 0, 0);
+        spv.setGrid(grid);
+
+        row = 0;
+        Assert.assertTrue(spv.isRowFixable(row));
+
+        grid.spanRow(3, 0, 0);
+        spv.setGrid(grid);
+        Assert.assertFalse(spv.isRowFixable(0));
+        Assert.assertFalse(spv.isRowFixable(1));
+        Assert.assertFalse(spv.isRowFixable(2));
+        Assert.assertTrue(spv.isRowFixable(3));
+
+    }
+
+    /**
+     * Test of areRowsFixable method, of class SpreadsheetView.
+     */
+    @Test public void testAreRowsFixable() {
+        Grid grid = spv.getGrid();
+        List<Integer> list = new ArrayList<>();
+
+        Assert.assertFalse(spv.areRowsFixable(null));
+
+        Assert.assertFalse(spv.areRowsFixable(Collections.emptyList()));
+
+        list.clear();
+        list.add(-1);
+        Assert.assertFalse(spv.areRowsFixable(list));
+
+        list.clear();
+        list.add(Integer.MAX_VALUE);
+        Assert.assertFalse(spv.areRowsFixable(list));
+
+        list.clear();
+        list.add(0);
+        list.add(2);
+        list.add(4);
+        Assert.assertTrue(spv.areRowsFixable(list));
+
+        grid.spanColumn(3, 0, 0);
+        grid.spanColumn(3, 2, 0);
+        grid.spanColumn(3, 4, 0);
+        list.clear();
+        list.add(0);
+        list.add(2);
+        list.add(4);
+        Assert.assertTrue(spv.areRowsFixable(list));
+
+        grid.spanRow(3, 0, 0);
+        list.clear();
+        list.add(0);
+        list.add(1);
+        list.add(2);
+        Assert.assertTrue(spv.areRowsFixable(list));
+    }
+
+    /**
+     * Test of isFixingRowsAllowed method, of class SpreadsheetView.
+     */
+    @Test public void testIsFixingRowsAllowed() {
+        spv.setFixingRowsAllowed(true);
+        Assert.assertTrue(spv.isFixingRowsAllowed());
+
+        spv.setFixingRowsAllowed(false);
+        Assert.assertFalse(spv.isFixingRowsAllowed());
+    }
+
+    /**
+     * Test of setFixingRowsAllowed method, of class SpreadsheetView.
+     */
+    @Test public void testSetFixingRowsAllowed() {
+        spv.setFixingRowsAllowed(true);
+        Assert.assertTrue(spv.isFixingRowsAllowed());
+
+        spv.setFixingRowsAllowed(false);
+        Assert.assertFalse(spv.isFixingRowsAllowed());
+
+        Assert.assertFalse(spv.isRowFixable(0));
+
+        List<Integer> list = new ArrayList<>();
+        list.add(1);
+        Assert.assertFalse(spv.areRowsFixable(list));
+    }
+
+    /**
+     * Test of isColumnFixable method, of class SpreadsheetView.
+     */
+    @Test
+    @Ignore
+    public void testIsColumnFixable() {
+//        System.out.println("isColumnFixable");
+//        int columnIndex = 0;
+//        SpreadsheetView instance = new SpreadsheetView();
+//        boolean expResult = false;
+//        boolean result = instance.isColumnFixable(columnIndex);
+//        assertEquals(expResult, result);
+    }
+
+    /**
+     * Test of isFixingColumnsAllowed method, of class SpreadsheetView.
+     */
+    @Test public void testIsFixingColumnsAllowed() {
+        spv.setFixingColumnsAllowed(true);
+        Assert.assertTrue(spv.isFixingColumnsAllowed());
+
+        spv.setFixingColumnsAllowed(false);
+        Assert.assertFalse(spv.isFixingColumnsAllowed());
+    }
+
+    /**
+     * Test of setFixingColumnsAllowed method, of class SpreadsheetView.
+     */
+    @Test public void testSetFixingColumnsAllowed() {
+
+        spv.setFixingColumnsAllowed(true);
+        Assert.assertTrue(spv.isFixingColumnsAllowed());
+
+        spv.setFixingColumnsAllowed(false);
+        Assert.assertFalse(spv.isFixingColumnsAllowed());
+
+        Assert.assertFalse(spv.getColumns().get(0).isColumnFixable());
+    }
+
+    /**
+     * Test of setShowColumnHeader method, of class SpreadsheetView.
+     */
+    @Test public void testSetShowColumnHeader() {
+        spv.setShowColumnHeader(false);
+        Assert.assertFalse(spv.isShowColumnHeader());
+
+        spv.setShowColumnHeader(true);
+        Assert.assertTrue(spv.isShowColumnHeader());
+    }
+
+    /**
+     * Test of isShowColumnHeader method, of class SpreadsheetView.
+     */
+    @Test public void testIsShowColumnHeader() {
+        spv.setShowColumnHeader(false);
+        Assert.assertFalse(spv.isShowColumnHeader());
+
+        spv.setShowColumnHeader(true);
+        Assert.assertTrue(spv.isShowColumnHeader());
+    }
+
+    /**
+     * Test of setShowRowHeader method, of class SpreadsheetView.
+     */
+    @Test public void testSetShowRowHeader() {
+        spv.setShowRowHeader(false);
+        Assert.assertFalse(spv.isShowRowHeader());
+
+        spv.setShowRowHeader(true);
+        Assert.assertTrue(spv.isShowRowHeader());
+    }
+
+    /**
+     * Test of isShowRowHeader method, of class SpreadsheetView.
+     */
+    @Test public void testIsShowRowHeader() {
+        spv.setShowRowHeader(false);
+        Assert.assertFalse(spv.isShowRowHeader());
+
+        spv.setShowRowHeader(true);
+        Assert.assertTrue(spv.isShowRowHeader());
+    }
+
+    /**
+     * Test of getRowHeight method, of class SpreadsheetView.
+     */
+    @Test public void testGetRowHeight() {
+        System.out.println("getRowHeight");
+
+        Map<Integer, Double> rowHeight = new HashMap<>();
+        rowHeight.put(1, 100.0);
+        rowHeight.put(5, 12.0);
+
+        GridBase grid = (GridBase) spv.getGrid();
+        grid.setRowHeightCallback(new GridBase.MapBasedRowHeightFactory(rowHeight));
+
+        double result = spv.getRowHeight(1);
+        assertEquals(100.0, result, 0.0);
+        
+        result = spv.getRowHeight(5);
+        assertEquals(12.0, result, 0.0);
+    }
+
+    /**
+     * Test of getEditor method, of class SpreadsheetView.
+     */
+    @Test public void testGetEditor() {
+        System.out.println("getEditor");
+        
+        SpreadsheetCellType cellType = null;
+        Optional<SpreadsheetCellEditor> result = spv.getEditor(cellType);
+        assertEquals(Optional.empty(), result);
+        
+        cellType = SpreadsheetCellType.DATE;
+        result = spv.getEditor(cellType);
+        assertNotNull(result);
+        if(!result.isPresent()){
+            fail();
+        }
+    }
+
+    /**
+     * Test of setEditable method, of class SpreadsheetView.
+     */
+    @Test public void testSetEditable() {
+        System.out.println("setEditable");
+      
+        spv.setEditable(false);
+        assertFalse(spv.isEditable());
+        
+        //FIXME To put in GridBase test
+//        String value = "The cake is a lie";
+//        spv.getGrid().setCellValue(0, 0, value);
+//        
+//        assertEquals("", spv.getGrid().getRows().get(0).get(0).getItem());
+        
+        spv.setEditable(true);
+        assertTrue(spv.isEditable());
+        
+//        spv.getGrid().setCellValue(0, 0, value);
+//        
+//        assertEquals(value, spv.getGrid().getRows().get(0).get(0).getItem());
+    }
+
+    /**
+     * Test of deleteSelectedCells method, of class SpreadsheetView.
+     * @throws java.lang.InterruptedException
+     */
+    @Test public void testDeleteSelectedCells() throws InterruptedException {
+        System.out.println("deleteSelectedCells");
+        
+        spv.setEditable(true);
+        String value = "The cake is a lie";
+        spv.getGrid().setCellValue(0, 0, value);
+        
+        assertEquals(value, spv.getGrid().getRows().get(0).get(0).getItem());
+
+        spv.getSelectionModel().select(0, spv.getColumns().get(0));
+        spv.deleteSelectedCells();
+        
+        assertNull(spv.getGrid().getRows().get(0).get(0).getItem());
+    }
+
+    /**
+     * Test of getSpanType method, of class SpreadsheetView.
+     */
+    @Test public void testGetSpanType() {
+        int row = 0;
+        int column = 0;
+        Grid grid = spv.getGrid();
+        SpanType type = SpreadsheetView.SpanType.NORMAL_CELL;
+
+        assertEquals(type, spv.getSpanType(-1, -1));
+        assertEquals(type, spv.getSpanType(Integer.MAX_VALUE, Integer.MAX_VALUE));
+        assertEquals(type, spv.getSpanType(Integer.MAX_VALUE, -1));
+        assertEquals(type, spv.getSpanType(-1, Integer.MAX_VALUE));
+        assertEquals(type, spv.getSpanType(grid.getRowCount(), grid.getColumnCount()));
+        
+    }
+}
diff --git a/doRelease.bat b/doRelease.bat
new file mode 100644
index 0000000..fb6de07
--- /dev/null
+++ b/doRelease.bat
@@ -0,0 +1,76 @@
+ at echo off
+echo ControlsFX Release Tool
+echo =======================
+echo.
+
+echo Step 1: In the root build file edit artifact_suffix to remove the -SNAPSHOT text.
+echo.
+pause
+
+echo.
+echo Step 2: Building projects...
+echo.
+call gradle -b clean assemble install
+
+echo.
+echo Success - all projects built!
+pause
+
+echo.
+echo Step 3.1: Copy new javadocs from controlsfx/build/docs/javadoc to ../controlsfx-javadoc directory
+echo.
+pause
+
+echo.
+echo Step 3.2: Copying samples source code from controlsfx-samples/src/main/java to ../controlsfx-javadoc/samples-src directory
+rmdir /S /Q ..\controlsfx-javadoc\samples-src
+xcopy controlsfx-samples\src\main\java ..\controlsfx-javadoc\samples-src /E /I
+echo.
+pause
+
+echo.
+echo Step 4: Commit, tag and push the javadocs to the repo
+echo.
+pause
+
+echo.
+echo Step 5: Test that ControlsFX-samples can load the javadoc and source tab for all samples. If not, update the URLs in the samples and rebuild the jar files. 
+echo.
+pause
+
+echo.
+echo Step 6: Maven time!
+echo Step 6.1: Pushing to Maven Central
+echo.
+call gradle -b controlsfx/build.gradle uploadPublished
+echo.
+call gradle -b fxsampler/build.gradle uploadPublished
+echo.
+echo Step 6.2: Go to Maven Central to publish the jars (https://oss.sonatype.org, then Staging Repositories, find release, select and 'close', then 'release')
+echo.
+
+echo Step 7: Edit the root build file to add back in the -SNAPSHOT text.
+echo.
+pause
+
+echo Step 8: Tag the repo with the version number.
+echo.
+pause
+
+echo Step 9: Create a zip file containing the controlsfx jar, the controlsfx-samples jar, the fxsample jar, and the license.txt file.
+echo.
+pause
+
+echo Step 10: Push zip file to download location
+echo.
+pause
+
+echo Step 11: Update root build file with version numbers to be the next version with -SNAPSHOT.
+echo.
+pause
+
+echo Step 12: Update bitbucket readme.md and controlsfx.org to refer to new version number
+echo.
+
+echo Step 11: Post blog post to controlsfx.org
+echo.
\ No newline at end of file
diff --git a/fxsampler/.classpath b/fxsampler/.classpath
new file mode 100644
index 0000000..02d3c70
--- /dev/null
+++ b/fxsampler/.classpath
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="src" path="src/main/java"/>
+	<classpathentry kind="src" path="src/main/resources"/>
+	<classpathentry exported="true" kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+	<classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/fxsampler/.hgignore b/fxsampler/.hgignore
new file mode 100644
index 0000000..0968078
--- /dev/null
+++ b/fxsampler/.hgignore
@@ -0,0 +1,11 @@
+syntax: glob
+*.class
+*.iml
+.DS_Store
+build/*
+.idea/*
+.gradle/*
+bin/*
+doc/*
+.settings/*
+
diff --git a/fxsampler/.project b/fxsampler/.project
new file mode 100644
index 0000000..3aaf87c
--- /dev/null
+++ b/fxsampler/.project
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>fxsampler</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.springsource.ide.eclipse.gradle.core.nature</nature>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+		<nature>org.eclipse.jdt.groovy.core.groovyNature</nature>
+	</natures>
+</projectDescription>
diff --git a/fxsampler/build.gradle b/fxsampler/build.gradle
new file mode 100644
index 0000000..b9a244e
--- /dev/null
+++ b/fxsampler/build.gradle
@@ -0,0 +1,46 @@
+version = fxsampler_version
+
+configurations {
+    //samples.extendsFrom mainRuntime
+    jdk
+}
+
+sourceSets {
+    main {
+        compileClasspath += configurations.jdk
+    }
+}
+
+dependencies {
+    try {
+        jdk files(jfxrtJar)
+    } catch (MissingPropertyException pne) {
+        jdk files("${System.properties['java.home']}/lib/jfxrt.jar")
+        jdk files("${System.properties['java.home']}/lib/ext/jfxrt.jar")
+    }
+}
+
+
+javadoc {
+    exclude 'impl/*'
+    failOnError = false
+    classpath = project.sourceSets.main.runtimeClasspath + configurations.jdk
+    
+    options.windowTitle("FXSampler Project ${version}")
+    options.links("http://docs.oracle.com/javase/8/docs/api/");
+    options.links("http://docs.oracle.com/javase/8/javafx/api/");
+    options.addBooleanOption("Xdoclint:none").setValue(true);
+    options.addBooleanOption("javafx").setValue(true);
+    
+    // All doc-files are located in src/main/docs because Gradle's javadoc doesn't copy
+    // over the doc-files if they are embedded with the sources. I find this arrangement
+    // somewhat cleaner anyway (never was a fan of mixing javadoc files with the sources)
+    doLast {
+        copy {
+            from "src/main/docs"
+            into "$buildDir/docs/javadoc"
+        }
+    }
+}
+
+
diff --git a/fxsampler/src/main/java/fxsampler/FXSampler.java b/fxsampler/src/main/java/fxsampler/FXSampler.java
new file mode 100644
index 0000000..8b98778
--- /dev/null
+++ b/fxsampler/src/main/java/fxsampler/FXSampler.java
@@ -0,0 +1,466 @@
+/**
+ * Copyright (c) 2013, 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package fxsampler;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.ServiceLoader;
+
+import javafx.application.Application;
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.geometry.Insets;
+import javafx.geometry.Rectangle2D;
+import javafx.scene.Node;
+import javafx.scene.Scene;
+import javafx.scene.control.Label;
+import javafx.scene.control.Tab;
+import javafx.scene.control.TabPane;
+import javafx.scene.control.TabPane.TabClosingPolicy;
+import javafx.scene.control.TextField;
+import javafx.scene.control.TreeCell;
+import javafx.scene.control.TreeItem;
+import javafx.scene.control.TreeView;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.VBox;
+import javafx.scene.web.WebView;
+import javafx.stage.Screen;
+import javafx.stage.Stage;
+import javafx.util.Callback;
+import fxsampler.model.EmptySample;
+import fxsampler.model.Project;
+import fxsampler.model.SampleTree.TreeNode;
+import fxsampler.model.WelcomePage;
+import fxsampler.util.SampleScanner;
+
+public class FXSampler extends Application {
+    
+    private Map<String, Project> projectsMap;
+
+    private Stage stage;
+    private GridPane grid;
+    
+    private Sample selectedSample;
+    
+    private TreeView<Sample> samplesTreeView;
+    private TreeItem<Sample> root;
+
+    private TabPane tabPane;
+    private Tab welcomeTab;
+    private Tab sampleTab;
+    private Tab javaDocTab;
+    private Tab sourceTab;
+    private Tab cssTab;
+
+    private WebView javaDocWebView;
+    private WebView sourceWebView;
+    private WebView cssWebView;
+
+
+    public static void main(String[] args) {
+        launch(args);
+    }
+
+    @Override public void start(final Stage primaryStage) throws Exception {
+        this.stage = primaryStage;
+//        primaryStage.getIcons().add(new Image("/org/controlsfx/samples/controlsfx-logo.png"));
+        
+        ServiceLoader<FXSamplerConfiguration> configurationServiceLoader = ServiceLoader.load(FXSamplerConfiguration.class);
+
+        projectsMap = new SampleScanner().discoverSamples();
+        buildSampleTree(null);
+
+        // simple layout: TreeView on left, sample area on right
+        grid = new GridPane();
+        grid.setPadding(new Insets(5, 10, 10, 10));
+        grid.setHgap(10);
+        grid.setVgap(10);
+
+        // --- left hand side
+        // search box
+        final TextField searchBox = new TextField();
+        searchBox.setPromptText("Search");
+        searchBox.getStyleClass().add("search-box");
+        searchBox.textProperty().addListener(new InvalidationListener() {
+            @Override public void invalidated(Observable o) {
+                buildSampleTree(searchBox.getText());
+            }
+        });
+        GridPane.setMargin(searchBox, new Insets(5, 0, 0, 0));
+        grid.add(searchBox, 0, 0);
+        
+        // treeview
+        samplesTreeView = new TreeView<>(root);
+        samplesTreeView.setShowRoot(false);
+        samplesTreeView.getStyleClass().add("samples-tree");
+        samplesTreeView.setMinWidth(200);
+        samplesTreeView.setMaxWidth(200);
+        samplesTreeView.setCellFactory(new Callback<TreeView<Sample>, TreeCell<Sample>>() {
+            @Override public TreeCell<Sample> call(TreeView<Sample> param) {
+                return new TreeCell<Sample>() {
+                    @Override protected void updateItem(Sample item, boolean empty) {
+                        super.updateItem(item, empty);
+
+                        if (empty) {
+                            setText("");
+                        } else {
+                            setText(item.getSampleName());
+                        }
+                    }
+                };
+            }
+        });
+        samplesTreeView.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<TreeItem<Sample>>() {
+            @Override public void changed(ObservableValue<? extends TreeItem<Sample>> observable, TreeItem<Sample> oldValue, TreeItem<Sample> newSample) {
+                if (newSample == null) {
+                    return;
+                } else if (newSample.getValue() instanceof EmptySample) {
+                    Sample selectedSample = newSample.getValue();
+                    Project selectedProject = projectsMap.get(selectedSample.getSampleName());
+                    if(selectedProject != null) {
+                        changeToWelcomeTab(selectedProject.getWelcomePage());
+                    }
+                    return;
+                }
+                selectedSample = newSample.getValue();
+                changeSample();
+            }
+        });
+        GridPane.setVgrow(samplesTreeView, Priority.ALWAYS);
+//        GridPane.setMargin(samplesTreeView, new Insets(5, 0, 0, 0));
+        grid.add(samplesTreeView, 0, 1);
+
+        // right hand side
+        tabPane = new TabPane();
+        tabPane.setTabClosingPolicy(TabClosingPolicy.UNAVAILABLE);
+        tabPane.getStyleClass().add(TabPane.STYLE_CLASS_FLOATING);
+        tabPane.getSelectionModel().selectedItemProperty().addListener(new InvalidationListener() {
+            @Override public void invalidated(Observable arg0) {
+                updateTab();
+            }
+        });
+        GridPane.setHgrow(tabPane, Priority.ALWAYS);
+        GridPane.setVgrow(tabPane, Priority.ALWAYS);
+        grid.add(tabPane, 1, 0, 1, 2);
+
+        sampleTab = new Tab("Sample");
+        javaDocTab = new Tab("JavaDoc");
+        javaDocWebView = new WebView();
+        javaDocTab.setContent(javaDocWebView);
+
+        sourceTab = new Tab("Source");
+        sourceWebView = new WebView();
+        sourceTab.setContent(sourceWebView);
+
+        cssTab = new Tab("Css");
+        cssWebView = new WebView();
+        cssTab.setContent(cssWebView);
+
+        // by default we'll show the welcome message of first project in the tree
+        // if no projects are available, we'll show the default page
+        List<TreeItem<Sample>> projects = samplesTreeView.getRoot().getChildren();
+        if(!projects.isEmpty()) {
+            TreeItem<Sample> firstProject = projects.get(0);
+            samplesTreeView.getSelectionModel().select(firstProject);
+        } else {
+            changeToWelcomeTab(null);
+        }
+
+        // put it all together
+        Scene scene = new Scene(grid);
+        scene.getStylesheets().add(getClass().getResource("fxsampler.css").toExternalForm());
+        for (FXSamplerConfiguration fxsamplerConfiguration : configurationServiceLoader) {
+        	String stylesheet = fxsamplerConfiguration.getSceneStylesheet();
+        	if (stylesheet != null) {
+            	scene.getStylesheets().add(stylesheet);
+        	}
+        }
+        primaryStage.setScene(scene);
+        primaryStage.setMinWidth(1000);
+        primaryStage.setMinHeight(600);
+        
+        // set width / height values to be 75% of users screen resolution
+        Rectangle2D screenBounds = Screen.getPrimary().getVisualBounds();
+        primaryStage.setWidth(screenBounds.getWidth() * 0.75);
+        primaryStage.setHeight(screenBounds.getHeight() * .75);
+        
+        primaryStage.setTitle("FXSampler!");
+        primaryStage.show();
+
+        samplesTreeView.requestFocus();
+    }
+
+    protected void buildSampleTree(String searchText) {
+        // rebuild the whole tree (it isn't memory intensive - we only scan
+        // classes once at startup)
+        root = new TreeItem<Sample>(new EmptySample("FXSampler"));
+        root.setExpanded(true);
+        
+        for (String projectName : projectsMap.keySet()) {
+            final Project project = projectsMap.get(projectName);
+            if (project == null) continue;
+            
+            // now work through the project sample tree building the rest
+            TreeNode n = project.getSampleTree().getRoot();
+            root.getChildren().add(n.createTreeItem());
+        }
+        
+        // with this newly built and full tree, we filter based on the search text
+        if (searchText != null) {
+           pruneSampleTree(root, searchText); 
+           
+           // FIXME weird bug in TreeView I think
+           samplesTreeView.setRoot(null);
+           samplesTreeView.setRoot(root);
+        }
+        
+        // and finally we sort the display a little
+        sort(root, (o1, o2) -> o1.getValue().getSampleName().compareTo(o2.getValue().getSampleName()));
+    }
+    
+    private void sort(TreeItem<Sample> node, Comparator<TreeItem<Sample>> comparator) {
+        node.getChildren().sort(comparator);
+        for (TreeItem<Sample> child : node.getChildren()) {
+            sort(child, comparator);
+        }
+    }
+    
+    // true == keep, false == delete
+    private boolean pruneSampleTree(TreeItem<Sample> treeItem, String searchText) {
+        // we go all the way down to the leaf nodes, and check if they match
+        // the search text. If they do, they stay. If they don't, we remove them.
+        // As we pop back up we check if the branch nodes still have children,
+        // and if not we remove them too
+        if (searchText == null) return true;
+        
+        if (treeItem.isLeaf()) {
+            // check for match. Return true if we match (to keep), and false
+            // to delete
+            return treeItem.getValue().getSampleName().toUpperCase().contains(searchText.toUpperCase());
+        } else {
+            // go down the tree...
+            List<TreeItem<Sample>> toRemove = new ArrayList<>();
+            
+            for (TreeItem<Sample> child : treeItem.getChildren()) {
+                boolean keep = pruneSampleTree(child, searchText);
+                if (! keep) {
+                    toRemove.add(child);
+                }
+            }
+            
+            // remove the unrelated tree items
+            treeItem.getChildren().removeAll(toRemove);
+            
+            // return true if there are children to this branch, false otherwise
+            // (by returning false we say that we should delete this now-empty branch)
+            return ! treeItem.getChildren().isEmpty();
+        }
+    }
+    
+    protected void changeSample() {
+        if (selectedSample == null) {
+            return;
+        }
+
+        if (tabPane.getTabs().contains(welcomeTab)) {
+            tabPane.getTabs().setAll(sampleTab, javaDocTab, sourceTab,cssTab);
+        }
+        
+        updateTab();
+    }
+    
+    private void updateTab() {
+        Tab selectedTab = tabPane.getSelectionModel().getSelectedItem();
+        
+        // we only update the selected tab - leaving the other tabs in their
+        // previous state until they are selected
+        if (selectedTab == sampleTab) {
+            sampleTab.setContent(buildSampleTabContent(selectedSample));
+        } else if (selectedTab == javaDocTab) {
+            javaDocWebView.getEngine().load(selectedSample.getJavaDocURL());
+        } else if (selectedTab == sourceTab) {
+            sourceWebView.getEngine().loadContent(formatSourceCode(selectedSample));
+        } else if (selectedTab == cssTab) {
+            cssWebView.getEngine().loadContent(formatCss(selectedSample));
+        }  
+    }
+    
+    private String getResource(String resourceName, Class<?> baseClass) {
+        Class<?> clz = baseClass == null? getClass(): baseClass;
+        return getResource(clz.getResourceAsStream(resourceName));
+    }
+
+    private String getResource(InputStream is) {
+        try (BufferedReader br = new BufferedReader(new InputStreamReader(is))) {
+        	String line;
+            StringBuilder sb = new StringBuilder();
+            while ((line = br.readLine()) != null) {
+                sb.append(line);
+                sb.append("\n");
+            } 
+            return sb.toString();
+        } catch (IOException e) {
+			e.printStackTrace();
+			return "";
+		}
+    }
+    
+    private String getSourceCode( Sample sample ) {
+        String sourceURL = sample.getSampleSourceURL();
+        
+        try {
+            // try loading via the web or local file system
+            URL url = new URL(sourceURL);
+            InputStream is = url.openStream();
+            return getResource(is);
+        } catch (IOException e) {
+            // no-op - the URL may not be valid, no biggy
+        }
+        
+        return getResource(sourceURL, sample.getClass());
+    }
+    
+    private String formatSourceCode(Sample sample) {
+        String sourceURL = sample.getSampleSourceURL();
+        String src;
+        if (sourceURL == null) {
+            src = "No sample source available";
+        } else {
+	        src = "Sample Source not found";
+	        try {
+	           src = getSourceCode(sample);
+	        } catch(Throwable ex){
+	            ex.printStackTrace();
+	        }
+        }
+        
+        // Escape '<' by "<" to ensure correct rendering by SyntaxHighlighter
+        src = src.replace("<", "<");
+        
+        String template = getResource("/fxsampler/util/SourceCodeTemplate.html", null);
+        return template.replace("<source/>", src);
+    }
+    
+    
+    private String formatCss(Sample sample) {
+        String cssUrl = sample.getControlStylesheetURL();
+        String src;
+        if (cssUrl == null) {
+            src = "No CSS source available";
+        } else {
+	        src = "Css not found";
+	        try {
+	        	src = new String(
+					Files.readAllBytes( Paths.get(getClass().getResource(cssUrl).toURI()) )
+				);
+	        } catch(Throwable ex){
+	            ex.printStackTrace();
+	        }
+        }
+        
+        // Escape '<' by "<" to ensure correct rendering by SyntaxHighlighter
+        src = src.replace("<", "<");
+        
+        String template = getResource("/fxsampler/util/CssTemplate.html", null);
+        return template.replace("<source/>", src);
+    }    
+
+
+    private Node buildSampleTabContent(Sample sample) {
+        return SampleBase.buildSample(sample, stage);
+    }
+
+    private void changeToWelcomeTab(WelcomePage wPage) {
+        if(null == wPage) {
+            wPage = getDefaultWelcomePage();
+        }
+        welcomeTab = new Tab(wPage.getTitle());
+        welcomeTab.setContent(wPage.getContent());
+        tabPane.getTabs().setAll(welcomeTab);
+    }
+    
+    private WelcomePage getDefaultWelcomePage() {
+        // line 1
+        Label welcomeLabel1 = new Label("Welcome to FXSampler!");
+        welcomeLabel1.setStyle("-fx-font-size: 2em; -fx-padding: 0 0 0 5;");
+
+        // line 2
+        Label welcomeLabel2 = new Label(
+                "Explore the available UI controls and other interesting projects "
+                + "by clicking on the options to the left.");
+        welcomeLabel2.setStyle("-fx-font-size: 1.25em; -fx-padding: 0 0 0 5;");
+
+        WelcomePage wPage = new WelcomePage("Welcome!", new VBox(5, welcomeLabel1, welcomeLabel2));
+        return wPage;
+    }
+
+    
+    
+    public final GridPane getGrid() {
+        return grid;
+    }
+
+    public final TabPane getTabPane() {
+        return tabPane;
+    }
+    // should never be null
+    public final Tab getWelcomeTab() {
+        return welcomeTab;
+    }
+
+    public final Tab getSampleTab() {
+        return sampleTab;
+    }
+
+    public final Tab getJavaDocTab() {
+        return javaDocTab;
+    }
+
+    public final Tab getSourceTab() {
+        return sourceTab;
+    }
+
+    public final Tab getCssTab() {
+        return cssTab;
+    }
+    
+    
+}
+
+
diff --git a/fxsampler/src/main/java/fxsampler/FXSamplerConfiguration.java b/fxsampler/src/main/java/fxsampler/FXSamplerConfiguration.java
new file mode 100644
index 0000000..806b3b0
--- /dev/null
+++ b/fxsampler/src/main/java/fxsampler/FXSamplerConfiguration.java
@@ -0,0 +1,5 @@
+package fxsampler;
+
+public interface FXSamplerConfiguration {
+	String getSceneStylesheet();
+}
diff --git a/fxsampler/src/main/java/fxsampler/FXSamplerProject.java b/fxsampler/src/main/java/fxsampler/FXSamplerProject.java
new file mode 100644
index 0000000..8a71a82
--- /dev/null
+++ b/fxsampler/src/main/java/fxsampler/FXSamplerProject.java
@@ -0,0 +1,24 @@
+package fxsampler;
+
+import fxsampler.model.WelcomePage;
+
+public interface FXSamplerProject {
+
+    /**
+     * Returns the pretty name of the project, e.g. 'JFXtras' or 'ControlsFX'
+     */
+    public String getProjectName();
+    
+    /**
+     * All samples should be beneath this base package. For example, in ControlsFX,
+     * this may be 'org.controlsfx.samples'.
+     */
+    public String getSampleBasePackage();
+    
+    /**
+     * Node that will be displayed in welcome tab, when project's root is
+     * selected in the tree. If this method returns null, default page will 
+     * be used
+     */
+    public WelcomePage getWelcomePage();
+}
diff --git a/fxsampler/src/main/java/fxsampler/Sample.java b/fxsampler/src/main/java/fxsampler/Sample.java
new file mode 100644
index 0000000..f77bfe3
--- /dev/null
+++ b/fxsampler/src/main/java/fxsampler/Sample.java
@@ -0,0 +1,93 @@
+/**
+ * Copyright (c) 2013, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package fxsampler;
+
+import javafx.scene.Node;
+import javafx.stage.Stage;
+
+/**
+ */
+public interface Sample {
+
+    /**
+     * A short, most likely single-word, name to show to the user - e.g. "CheckBox"
+     */
+    public String getSampleName();
+
+    /**
+     * A short, multiple sentence description of the sample. 
+     */
+    public String getSampleDescription();
+    
+    /**
+     * Returns the name of the project that this sample belongs to (e.g. 'JFXtras'
+     * or 'ControlsFX').
+     */
+    public String getProjectName();
+    
+    /**
+     * Returns the version of the project that this sample belongs to (e.g. '1.0.0')
+     */
+    public String getProjectVersion();
+    
+    /**
+     * Returns the main sample panel.
+     */
+    public Node getPanel(final Stage stage);
+
+    /**
+     * Returns the panel to display to the user that allows for manipulating
+     * the sample.
+     */
+    public Node getControlPanel();
+    
+    /**
+     * Returns divider position to use for split between main panel and control panel 
+     */
+    public double getControlPanelDividerPosition();
+
+    /**
+     * A full URL to the javadoc for the API being demonstrated in this sample.
+     */
+    public String getJavaDocURL();
+    
+    /**
+     * Returns URL for control's stylsheet 
+     */
+    public String getControlStylesheetURL();
+    
+    /**
+     * A full URL to a sample source code, which is assumed to be in java.
+     */
+    public String getSampleSourceURL();
+    
+    /**
+     * If true this sample is shown to users, if false it is not.
+     */
+    public boolean isVisible();
+
+}
\ No newline at end of file
diff --git a/fxsampler/src/main/java/fxsampler/SampleBase.java b/fxsampler/src/main/java/fxsampler/SampleBase.java
new file mode 100644
index 0000000..7ea2665
--- /dev/null
+++ b/fxsampler/src/main/java/fxsampler/SampleBase.java
@@ -0,0 +1,145 @@
+package fxsampler;
+
+import java.util.ServiceLoader;
+
+import javafx.application.Application;
+import javafx.scene.Node;
+import javafx.scene.Parent;
+import javafx.scene.Scene;
+import javafx.scene.control.Label;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.control.Separator;
+import javafx.scene.control.SplitPane;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.VBox;
+import javafx.scene.text.TextFlow;
+import javafx.stage.Stage;
+
+/**
+ * A base class for samples - it is recommended that they extend this class
+ * rather than Application, as then the samples can be run either standalone
+ * or within FXSampler. 
+ */
+public abstract class SampleBase extends Application implements Sample {
+    
+    /** {@inheritDoc} */
+    @Override public void start(Stage primaryStage) throws Exception {
+        ServiceLoader<FXSamplerConfiguration> configurationServiceLoader = ServiceLoader.load(FXSamplerConfiguration.class);
+
+        primaryStage.setTitle(getSampleName());
+        
+        Scene scene = new Scene((Parent)buildSample(this, primaryStage), 800, 800);
+        scene.getStylesheets().add(SampleBase.class.getResource("fxsampler.css").toExternalForm());
+        for (FXSamplerConfiguration fxsamplerConfiguration : configurationServiceLoader) {
+        	String stylesheet = fxsamplerConfiguration.getSceneStylesheet();
+        	if (stylesheet != null) {
+            	scene.getStylesheets().add(stylesheet);
+        	}
+        }
+        primaryStage.setScene(scene);
+        primaryStage.show();
+    }
+    
+    /** {@inheritDoc} */
+    @Override public boolean isVisible() {
+        return true;
+    }
+    
+    /** {@inheritDoc} */
+    @Override public Node getControlPanel() {
+        return null;
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    public double getControlPanelDividerPosition() {
+    	return 0.6;
+    }
+    
+    /** {@inheritDoc} */
+    @Override public String getSampleDescription() {
+        return "";
+    }
+    
+    /** {@inheritDoc} */
+    @Override public String getProjectName() {
+        return "ControlsFX";
+    }
+    
+    /**
+     * Utility method to create the default look for samples.
+     */
+    public static Node buildSample(Sample sample, Stage stage) {
+        SplitPane splitPane = new SplitPane();
+        
+        
+        // we guarantee that the build order is panel then control panel.
+        final Node samplePanel = sample.getPanel(stage);
+        final Node controlPanel = sample.getControlPanel();
+        splitPane.setDividerPosition(0, sample.getControlPanelDividerPosition());
+        
+        if (samplePanel != null) {
+            splitPane.getItems().add(samplePanel);
+        }
+        
+        final VBox rightPanel = new VBox();
+        rightPanel.getStyleClass().add("right-panel");
+        rightPanel.setMaxHeight(Double.MAX_VALUE);
+
+        boolean addRightPanel = false;
+        
+        Label sampleName = new Label(sample.getSampleName());
+        sampleName.getStyleClass().add("sample-name");
+        rightPanel.getChildren().add(sampleName);
+        
+        // --- project name & version
+        String version = sample.getProjectVersion();
+        version = version == null ? "" : 
+                  version.equals("@version@") ? "" :
+                  " " + version.trim();
+        
+        final String projectName = sample.getProjectName() + version;
+        if (projectName != null && ! projectName.isEmpty()) {
+            Label projectNameTitleLabel = new Label("Project: ");
+            projectNameTitleLabel.getStyleClass().add("project-name-title");
+            
+            Label projectNameLabel = new Label(projectName);
+            projectNameLabel.getStyleClass().add("project-name");
+            projectNameLabel.setWrapText(true);
+            
+            TextFlow textFlow = new TextFlow(projectNameTitleLabel, projectNameLabel);
+            rightPanel.getChildren().add(textFlow);
+        }
+        
+        // --- description
+        final String description = sample.getSampleDescription();
+        if (description != null && ! description.isEmpty()) {
+            Label descriptionLabel = new Label(description);
+            descriptionLabel.getStyleClass().add("description");
+            descriptionLabel.setWrapText(true);
+            rightPanel.getChildren().add(descriptionLabel);
+            
+            addRightPanel = true;
+        }
+        
+        if (controlPanel != null) {
+            rightPanel.getChildren().add(new Separator());
+            
+            controlPanel.getStyleClass().add("control-panel");
+            rightPanel.getChildren().add(controlPanel);
+            VBox.setVgrow(controlPanel, Priority.ALWAYS);
+            addRightPanel = true;
+        }
+        
+        if (addRightPanel) {
+            ScrollPane scrollPane = new ScrollPane(rightPanel);
+            scrollPane.setMaxHeight(Double.MAX_VALUE);
+            scrollPane.setFitToWidth(true);
+            scrollPane.setFitToHeight(true);
+            SplitPane.setResizableWithParent(scrollPane, false);
+            splitPane.getItems().add(scrollPane);
+        }
+        
+        return splitPane;
+    }
+}
diff --git a/fxsampler/src/main/java/fxsampler/model/EmptySample.java b/fxsampler/src/main/java/fxsampler/model/EmptySample.java
new file mode 100644
index 0000000..20db177
--- /dev/null
+++ b/fxsampler/src/main/java/fxsampler/model/EmptySample.java
@@ -0,0 +1,60 @@
+package fxsampler.model;
+
+import javafx.scene.Node;
+import javafx.stage.Stage;
+import fxsampler.Sample;
+
+public class EmptySample implements Sample {
+    private final String name;
+
+    public EmptySample(String name) {
+        this.name = name;
+    }
+
+    @Override public String getSampleName() {
+        return name;
+    }
+
+    @Override public String getSampleDescription() {
+        return null;
+    }
+    
+    @Override public String getProjectName() {
+        return null;
+    }
+
+	@Override
+	public String getProjectVersion() {
+		return null;
+	}
+	
+    @Override public Node getPanel(Stage stage) {
+        return null;
+    }
+
+    @Override public String getJavaDocURL() {
+        return null;
+    }
+    
+    @Override public String getSampleSourceURL() {
+        return null;
+    }
+
+    @Override public boolean isVisible() {
+        return true;
+    }
+
+    @Override public Node getControlPanel() {
+        return null;
+    }
+    
+    public double getControlPanelDividerPosition() {
+    	return 0.6;
+    }
+
+	@Override
+	public String getControlStylesheetURL() {
+		return null;
+	}
+
+}
\ No newline at end of file
diff --git a/fxsampler/src/main/java/fxsampler/model/Project.java b/fxsampler/src/main/java/fxsampler/model/Project.java
new file mode 100644
index 0000000..c70fcad
--- /dev/null
+++ b/fxsampler/src/main/java/fxsampler/model/Project.java
@@ -0,0 +1,84 @@
+package fxsampler.model;
+
+import fxsampler.Sample;
+
+/**
+ * Represents a project such as ControlsFX or JFXtras
+ */
+public class Project {
+    
+    private final String name;
+    
+    private final String basePackage;
+
+    // A Project has a Tree of samples
+    private final SampleTree sampleTree;
+    
+    // Pojo that holds the welcome tab content and title
+    private WelcomePage welcomePage;
+    
+    public Project(String name, String basePackage) {
+        this.name = name;
+        this.basePackage = basePackage;
+        this.sampleTree = new SampleTree(new EmptySample(name));
+    }
+
+    public void addSample(String packagePath, Sample sample) {
+        // convert something like 'org.controlsfx.samples.actions' to 'samples.actions'
+        String packagesWithoutBase = "";
+        try {
+            if (! basePackage.equals(packagePath)) {
+                packagesWithoutBase = packagePath.substring(basePackage.length() + 1);
+            }
+        } catch (StringIndexOutOfBoundsException e) {
+            System.out.println("packagePath: " + packagePath + ", basePackage: " + basePackage);
+            e.printStackTrace();
+            return;
+        }
+        
+        // then split up the packages into separate strings
+        String[] packages = packagesWithoutBase.isEmpty() ? new String[] { } : packagesWithoutBase.split("\\.");
+        
+        // then for each package convert to a prettier form
+        for (int i = 0; i < packages.length; i++) {
+            String packageName = packages[i];
+            if (packageName.isEmpty()) continue;
+            
+            packageName = packageName.substring(0, 1).toUpperCase() + packageName.substring(1);
+            packageName = packageName.replace("_", " ");
+            packages[i] = packageName;
+        }
+        
+        // now we have the pretty package names, we add this sample into the
+        // tree in the appropriate place
+        sampleTree.addSample(packages, sample);
+    }
+    
+    public SampleTree getSampleTree() {
+        return sampleTree;
+    }
+    
+    public void setWelcomePage(WelcomePage welcomePage) {
+        if(null != welcomePage) {
+            this.welcomePage = welcomePage;
+        }
+    }
+    
+    public WelcomePage getWelcomePage() {
+        return this.welcomePage;
+    }
+
+    @Override public String toString() {
+        StringBuilder sb = new StringBuilder();
+        
+        sb.append("Project [ name: ");
+        sb.append(name);
+        sb.append(", sample count: ");
+        sb.append(sampleTree.size());
+        sb.append(", tree: ");
+        sb.append(sampleTree);
+        sb.append(" ]");
+        
+        return sb.toString();
+    }
+}
diff --git a/fxsampler/src/main/java/fxsampler/model/SampleTree.java b/fxsampler/src/main/java/fxsampler/model/SampleTree.java
new file mode 100644
index 0000000..c658f8d
--- /dev/null
+++ b/fxsampler/src/main/java/fxsampler/model/SampleTree.java
@@ -0,0 +1,141 @@
+package fxsampler.model;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javafx.scene.control.TreeItem;
+import fxsampler.Sample;
+
+public class SampleTree {
+    private TreeNode root;
+    
+    private int count = 0;
+
+    public SampleTree(Sample rootSample) {
+        root = new TreeNode(null, null, rootSample);
+    }
+    
+    public TreeNode getRoot() {
+        return root;
+    }
+    
+    public Object size() {
+        return count;
+    }
+    
+    public void addSample(String[] packages, Sample sample) {
+        if (packages.length == 0) {
+            root.addSample(sample);
+            return;
+        }
+        
+        TreeNode n = root;
+        for (String packageName : packages) {
+            if (n.containsChild(packageName)) {
+                n = n.getChild(packageName);
+            } else {
+                TreeNode newNode = new TreeNode(packageName);
+                n.addNode(newNode);
+                n = newNode;
+            }
+        }
+        
+        if (n.packageName.equals(packages[packages.length - 1])) {
+            n.addSample(sample);
+            count++;
+        }
+    }
+    
+    @Override public String toString() {
+        return root.toString();
+    }
+    
+
+    public static class TreeNode {
+        private final Sample sample;
+        private final String packageName;
+        
+        private final TreeNode parent;
+        private List<TreeNode> children;
+        
+        public TreeNode() {
+            this(null, null, null);
+        }
+        
+        public TreeNode(String packageName) {
+            this(null, packageName, null);
+        }
+        
+        public TreeNode(TreeNode parent, String packageName, Sample sample) {
+            this.children = new ArrayList<>();
+            this.sample = sample;
+            this.parent = parent;
+            this.packageName = packageName;
+        }
+        
+        public boolean containsChild(String packageName) {
+            if (packageName == null) return false;
+            
+            for (TreeNode n : children) {
+                if (packageName.equals(n.packageName)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+        
+        public TreeNode getChild(String packageName) {
+            if (packageName == null) return null;
+            
+            for (TreeNode n : children) {
+                if (packageName.equals(n.packageName)) {
+                    return n;
+                }
+            }
+            return null;
+        }
+        
+        public void addSample(Sample sample) {
+            children.add(new TreeNode(this, null, sample));
+        }
+        
+        public void addNode(TreeNode n) {
+            children.add(n);
+        }
+        
+        public Sample getSample() {
+            return sample;
+        }
+        
+        public String getPackageName() {
+            return packageName;
+        }
+        
+        public TreeItem<Sample> createTreeItem() {
+            TreeItem<Sample> treeItem = null;
+            
+            if (sample != null) {
+                treeItem = new TreeItem<Sample>(sample);
+            } else if (packageName != null) {
+                treeItem = new TreeItem<Sample>(new EmptySample(packageName));
+            }
+            
+            treeItem.setExpanded(true);
+            
+            // recursively add in children
+            for (TreeNode n : children) {
+                treeItem.getChildren().add(n.createTreeItem());
+            }
+            
+            return treeItem;
+        }
+        
+        @Override public String toString() {
+            if (sample != null) {
+                return " Sample [ sampleName: " + sample.getSampleName() + ", children: " + children + " ]";
+            } else {
+                return " Sample [ packageName: " + packageName + ", children: " + children + " ]";
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/fxsampler/src/main/java/fxsampler/model/WelcomePage.java b/fxsampler/src/main/java/fxsampler/model/WelcomePage.java
new file mode 100644
index 0000000..1835473
--- /dev/null
+++ b/fxsampler/src/main/java/fxsampler/model/WelcomePage.java
@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package fxsampler.model;
+
+import javafx.scene.Node;
+
+public class WelcomePage {
+    private String title;
+    private Node content;
+    
+    public WelcomePage(String title, Node content) {
+        this.title = title;
+        this.content = content;
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    public Node getContent() {
+        return content;
+    }
+
+    public void setContent(Node content) {
+        this.content = content;
+    }
+}
diff --git a/fxsampler/src/main/java/fxsampler/util/SampleScanner.java b/fxsampler/src/main/java/fxsampler/util/SampleScanner.java
new file mode 100644
index 0000000..724dc0c
--- /dev/null
+++ b/fxsampler/src/main/java/fxsampler/util/SampleScanner.java
@@ -0,0 +1,280 @@
+package fxsampler.util;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Modifier;
+import java.net.URL;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.ServiceLoader;
+import java.util.Set;
+
+import fxsampler.FXSamplerProject;
+import fxsampler.Sample;
+import fxsampler.model.EmptySample;
+import fxsampler.model.Project;
+
+/**
+ * All the code related to classpath scanning, etc for samples.
+ */
+public class SampleScanner {
+    
+    private static List<String> ILLEGAL_CLASS_NAMES = new ArrayList<>();
+    static {
+        ILLEGAL_CLASS_NAMES.add("/com/javafx/main/Main.class");
+        ILLEGAL_CLASS_NAMES.add("/com/javafx/main/NoJavaFXFallback.class");
+    }
+    
+    private static Map<String, FXSamplerProject> packageToProjectMap = new HashMap<>();
+    static {
+        System.out.println("Initialising FXSampler sample scanner...");
+        System.out.println("\tDiscovering projects...");
+        // find all projects on the classpath that expose a FXSamplerProject
+        // service. These guys are our friends....
+        ServiceLoader<FXSamplerProject> loader = ServiceLoader.load(FXSamplerProject.class);
+        for (FXSamplerProject project : loader) {
+            final String projectName = project.getProjectName();
+            final String basePackage = project.getSampleBasePackage();
+            packageToProjectMap.put(basePackage, project);
+            System.out.println("\t\tFound project '" + projectName + 
+                    "', with sample base package '" + basePackage + "'");
+        }
+        
+        if (packageToProjectMap.isEmpty()) {
+            System.out.println("\tError: Did not find any projects!");
+        }
+    }
+    
+    private final Map<String, Project> projectsMap = new HashMap<>();
+    
+    /**
+     * Gets the list of sample classes to load
+     *
+     * @return The classes
+     * @throws ClassNotFoundException
+     * @throws IOException
+     */
+    public Map<String, Project> discoverSamples() {
+        Class<?>[] results = new Class[] { };
+        
+        try {
+        	  results = loadFromPathScanning();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        
+        for (Class<?> sampleClass : results) {
+            if (! Sample.class.isAssignableFrom(sampleClass)) continue;
+            if (sampleClass.isInterface()) continue;
+            if (Modifier.isAbstract(sampleClass.getModifiers())) continue;
+//            if (Sample.class.isAssignableFrom(EmptySample.class)) continue;
+            if (sampleClass == EmptySample.class) continue;
+            
+            Sample sample = null;
+            try {
+                sample = (Sample)sampleClass.newInstance();
+            } catch (InstantiationException | IllegalAccessException e) {
+                e.printStackTrace();
+            }
+            if (sample == null || ! sample.isVisible()) continue;
+            
+            
+
+            final String packageName = sampleClass.getPackage().getName();
+            
+            for (String key : packageToProjectMap.keySet()) {
+                if (packageName.contains(key)) {
+                    final String prettyProjectName = packageToProjectMap.get(key).getProjectName();
+                    
+                    Project project;
+                    if (! projectsMap.containsKey(prettyProjectName)) {
+                        project = new Project(prettyProjectName, key);
+                        project.setWelcomePage(packageToProjectMap.get(key).getWelcomePage());
+                        projectsMap.put(prettyProjectName, project);
+                    } else {
+                        project = projectsMap.get(prettyProjectName);
+                    }
+                    
+                    project.addSample(packageName, sample);
+                }
+            }
+        }
+        
+        return projectsMap;
+    } 
+
+    /**
+     * Scans all classes.
+     *
+     * @return The classes
+     * @throws ClassNotFoundException
+     * @throws IOException
+     */
+    private Class<?>[] loadFromPathScanning() throws ClassNotFoundException, IOException {
+        final List<File> dirs = new ArrayList<>();
+        final List<File> jars = new ArrayList<>();
+        
+        // scan the classpath
+        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
+        String path = "";
+        Enumeration<URL> resources = classLoader.getResources(path);
+        while (resources.hasMoreElements()) {
+            URL url = resources.nextElement();
+            
+            if (url.toExternalForm().contains("/jre/")) continue;
+            
+            // Only "file" and "jar" URLs are recognized, other schemes will be ignored.
+            String protocol = url.getProtocol().toLowerCase();
+            if ("file".equals(protocol)) {
+                dirs.add(new File(url.getFile()));
+            } else if ("jar".equals(protocol)) {
+                String fileName = new URL(url.getFile()).getFile();
+                
+                // JAR URL specs must contain the string "!/" which separates the name
+                // of the JAR file from the path of the resource contained in it, even
+                // if the path is empty.
+                int sep = fileName.indexOf("!/");
+                if (sep > 0) {
+                    jars.add(new File(fileName.substring(0, sep)));
+                }
+            }
+        }
+
+        // and also scan the current working directory
+        final Path workingDirectory = new File("").toPath();
+        scanPath(workingDirectory, dirs, jars);
+        
+        // process directories first, then jars, so that classes take precedence
+        // over built jars (it makes rapid development easier in the IDE)
+        final Set<Class<?>> classes = new LinkedHashSet<>();
+        for (File directory : dirs) {
+            classes.addAll(findClassesInDirectory(directory));
+        }
+        for (File jar : jars) {
+            String fullPath = jar.getAbsolutePath();
+            if (fullPath.endsWith("jfxrt.jar")) continue;
+            classes.addAll(findClassesInJar(new File(fullPath)));
+        }
+        
+        return classes.toArray(new Class[classes.size()]);
+    }
+
+    private void scanPath(Path workingDirectory, final List<File> dirs, final List<File> jars) throws IOException {
+        Files.walkFileTree(workingDirectory, new SimpleFileVisitor<Path>() {
+            @Override public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException {
+                final File file = path.toFile();
+                final String fullPath = file.getAbsolutePath();
+                final String name = file.toString();
+                
+                if (fullPath.endsWith("jfxrt.jar") || name.contains("jre")) {
+                    return FileVisitResult.CONTINUE;
+                }
+                
+                if (file.isDirectory()) {
+                    dirs.add(file);
+                } else if (name.toLowerCase().endsWith(".jar")) {
+                    jars.add(file);
+                }
+                return FileVisitResult.CONTINUE;
+            }
+
+            @Override public FileVisitResult visitFileFailed(Path file, IOException ex) {
+                System.err.println(ex + " Skipping...");
+                return FileVisitResult.CONTINUE;
+            }
+        });
+    }
+
+    private List<Class<?>> findClassesInDirectory(File directory) throws IOException {
+        List<Class<?>> classes = new ArrayList<>();
+        if (!directory.exists()) {
+            System.out.println("Directory does not exist: " + directory.getAbsolutePath());
+            return classes;
+        }
+        
+        processPath(directory.toPath(), classes);
+        return classes;
+    }
+
+    private List<Class<?>> findClassesInJar(File jarFile) throws IOException, ClassNotFoundException {
+        List<Class<?>> classes = new ArrayList<>();
+        if (!jarFile.exists()) {
+            System.out.println("Jar file does not exist here: " + jarFile.getAbsolutePath());
+            return classes;
+        }
+        
+        FileSystem jarFileSystem = FileSystems.newFileSystem(jarFile.toPath(), null);
+        processPath(jarFileSystem.getPath("/"), classes);
+        return classes;
+    }
+    
+    private void processPath(Path path, final List<Class<?>> classes) throws IOException {
+        final String root = path.toString();
+        
+        Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
+            @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+                String name = file.toString();
+                if (name.endsWith(".class") && ! ILLEGAL_CLASS_NAMES.contains(name)) {
+                    
+                    // remove root path to make class name correct in all cases
+                    name = name.substring(root.length());
+                    
+                    Class<?> clazz = processClassName(name);
+                    if (clazz != null) {
+                        classes.add(clazz);
+                    }
+                }
+                return FileVisitResult.CONTINUE;
+            }
+
+            @Override public FileVisitResult visitFileFailed(Path file, IOException ex) {
+                System.err.println(ex + " Skipping...");
+                return FileVisitResult.CONTINUE;
+            }
+        });        
+    }
+
+    private Class<?> processClassName(final String name) {
+        String className = name.replace("\\", ".");
+        className = className.replace("/", ".");
+        
+        // some cleanup code
+        if (className.contains("$")) {
+            // we don't care about samples as inner classes, so 
+            // we jump out
+            return null;
+        }
+        if (className.contains(".bin")) {
+            className = className.substring(className.indexOf(".bin") + 4);
+            className = className.replace(".bin", "");
+        }
+        if (className.startsWith(".")) {
+            className = className.substring(1);
+        }
+        if (className.endsWith(".class")) {
+            className = className.substring(0, className.length() - 6);
+        }
+
+        Class<?> clazz = null;
+        try {
+            clazz = Class.forName(className);
+        } catch (Throwable e) {
+            // Throwable, could be all sorts of bad reasons the class won't instantiate
+            System.out.println("ERROR: Class name: " + className);
+            System.out.println("ERROR: Initial filename: " + name);
+//            e.printStackTrace();
+        }
+        return clazz;
+    } 
+}
diff --git a/fxsampler/src/main/resources/fxsampler/fxsampler.css b/fxsampler/src/main/resources/fxsampler/fxsampler.css
new file mode 100644
index 0000000..792b7e3
--- /dev/null
+++ b/fxsampler/src/main/resources/fxsampler/fxsampler.css
@@ -0,0 +1,39 @@
+.samples-tree {
+    -fx-background-color: -fx-outer-border, -fx-background;
+    -fx-background-insets: 0, 1;
+    -fx-background-radius: 2, 0;
+}
+
+.right-panel {
+    -fx-padding: 5;
+}
+
+.right-panel > .sample-name {
+    -fx-font-size: 18;
+    -fx-font-weight: bold;
+}
+
+.right-panel > .separator {
+    -fx-padding: 5 0 5 0;
+}
+
+.right-panel > .description {
+    -fx-text-alignment: justify;
+}
+
+.right-panel > * > .project-name-title, 
+.right-panel > * > .property {
+    -fx-font-weight: bold;
+}
+
+.right-panel > * > .project-name {
+    -fx-text-alignment: justify;
+}
+
+/* 
+ * The area beneath the description specifically to allow people to interact with
+ * the sample.
+ */
+.right-panel > .control-panel {
+    -fx-padding: 5 0 0 0;
+}
diff --git a/fxsampler/src/main/resources/fxsampler/util/CssTemplate.html b/fxsampler/src/main/resources/fxsampler/util/CssTemplate.html
new file mode 100644
index 0000000..bcea452
--- /dev/null
+++ b/fxsampler/src/main/resources/fxsampler/util/CssTemplate.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+
+    <link href="http://alexgorbatchev.com/pub/sh/current/styles/shThemeEclipse.css" rel="stylesheet" type="text/css" />
+    <script src="http://alexgorbatchev.com/pub/sh/current/scripts/shCore.js" type="text/javascript"></script>
+    <script src="http://alexgorbatchev.com/pub/sh/current/scripts/shAutoloader.js" type="text/javascript"></script>
+    <script src="http://alexgorbatchev.com/pub/sh/current/scripts/shBrushCSS.js" type="text/javascript"></script>
+
+
+    <!-- Finally, to actually run the highlighter, you need to include this JS on your page -->
+    <script type="text/javascript">
+        SyntaxHighlighter.defaults['gutter'] = false;
+        SyntaxHighlighter.defaults['smart-tabs'] = false;
+        SyntaxHighlighter.defaults['toolbar'] = false;
+        SyntaxHighlighter.all()
+    </script>
+
+</head>
+<body>
+<pre class="brush: css">
+<source/>
+</pre>
+</body>
+</html>
+
diff --git a/fxsampler/src/main/resources/fxsampler/util/SourceCodeTemplate.html b/fxsampler/src/main/resources/fxsampler/util/SourceCodeTemplate.html
new file mode 100644
index 0000000..ca546d9
--- /dev/null
+++ b/fxsampler/src/main/resources/fxsampler/util/SourceCodeTemplate.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+
+    <link href="http://alexgorbatchev.com/pub/sh/current/styles/shThemeEclipse.css" rel="stylesheet" type="text/css" />
+    <script src="http://alexgorbatchev.com/pub/sh/current/scripts/shCore.js" type="text/javascript"></script>
+    <script src="http://alexgorbatchev.com/pub/sh/current/scripts/shAutoloader.js" type="text/javascript"></script>
+    <script src="http://alexgorbatchev.com/pub/sh/current/scripts/shBrushJava.js" type="text/javascript"></script>
+
+
+    <!-- Finally, to actually run the highlighter, you need to include this JS on your page -->
+    <script type="text/javascript">
+        SyntaxHighlighter.defaults['gutter'] = false;
+        SyntaxHighlighter.defaults['smart-tabs'] = false;
+        SyntaxHighlighter.defaults['toolbar'] = false;
+        SyntaxHighlighter.all()
+    </script>
+
+</head>
+<body>
+<pre class="brush: java">
+<source/>
+</pre>
+</body>
+</html>
+
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..b6b646b
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..66aa5d5
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Wed Jun 19 21:19:07 EDT 2013
+zipStoreBase=GRADLE_USER_HOME
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStorePath=wrapper/dists
+distributionUrl=http\://services.gradle.org/distributions/gradle-2.0-bin.zip
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..91a7e26
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+    echo "$*"
+}
+
+die ( ) {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+    [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+    JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..aec9973
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,90 @@
+ at if "%DEBUG%" == "" @echo off
+ at rem ##########################################################################
+ at rem
+ at rem  Gradle startup script for Windows
+ at rem
+ at rem ##########################################################################
+
+ at rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+ at rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+ at rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+ at rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+ at rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+ at rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+ at rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+ at rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+ at rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/javafx.plugin b/javafx.plugin
new file mode 100644
index 0000000..8c92f4e
--- /dev/null
+++ b/javafx.plugin
@@ -0,0 +1,36 @@
+/*
+ * Bootstrap script for the Gradle JavaFX Plugin.
+ * (based on http://plugins.jasoft.fi/vaadin.plugin)
+ *
+ * The script will add the latest version of the plugin to the build script
+ * dependencies and apply the plugin to the project. If you do not want
+ * this behavior you can copy and paste the below configuration into your
+ * own build script and define your own repository and version for the plugin.
+ */
+
+buildscript {
+    repositories {
+        mavenLocal()
+        maven {
+            name = 'BinTray'
+            url = 'http://dl.bintray.com/content/shemnon/javafx-gradle/'
+        }
+        maven {
+            name = 'CloudBees Snapshot'
+            url = 'http://repository-javafx-gradle-plugin.forge.cloudbees.com/snapshot'
+        }
+        ivy {
+            url = 'http://repository-javafx-gradle-plugin.forge.cloudbees.com/snapshot'
+        }
+        mavenCentral()
+    }
+    dependencies {
+        classpath 'org.bitbucket.shemnon.javafxplugin:gradle-javafx-plugin:0.4.0'
+        classpath project.files("${System.properties['java.home']}/../lib/ant-javafx.jar")
+        classpath project.files("${System.properties['java.home']}/lib/jfxrt.jar")
+    }
+}
+
+if (!project.plugins.findPlugin(org.bitbucket.shemnon.javafxplugin.JavaFXPlugin)) {
+    project.apply(plugin: org.bitbucket.shemnon.javafxplugin.JavaFXPlugin)
+}
diff --git a/license.txt b/license.txt
new file mode 100644
index 0000000..3985baa
--- /dev/null
+++ b/license.txt
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2013, 2014, ControlsFX
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *     * Neither the name of ControlsFX, any associated website, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
\ No newline at end of file
diff --git a/mavenPublish.gradle b/mavenPublish.gradle
new file mode 100644
index 0000000..1577950
--- /dev/null
+++ b/mavenPublish.gradle
@@ -0,0 +1,118 @@
+apply plugin: 'signing'
+
+configurations {
+    samples { extendsFrom runtime}
+    jdk
+    maven { extendsFrom archives }
+    published { extendsFrom archives, signatures}
+}
+
+signing {
+    required = { gradle.taskGraph.hasTask(uploadPublished) && !version.endsWith("SNAPSHOT") }
+    sign configurations.archives
+}
+
+repositories {
+    mavenLocal()
+    maven { url 'https://oss.sonatype.org/content/groups/staging' }
+    mavenCentral()
+}
+
+uploadPublished {
+
+    doFirst {
+        // configure repositories in a doFirst so we can late bind the properties
+        checkPasswords()
+        repositories {
+            mavenDeployer {
+                configurePOM(pom)
+                beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
+                snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") {
+                    authentication userName: sonatypeUsername, password: sonatypePassword
+                }
+                repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") {
+                    authentication userName: sonatypeUsername, password: sonatypePassword
+                }
+            }
+        }
+    }
+    configuration = configurations.published
+}
+
+install {
+    doFirst {
+        configurePOM(repositories.mavenInstaller.pom)
+    }
+    configuration = configurations.published
+}
+
+//ext {
+//  pomExclusions = []
+//}
+
+private configurePOM(def pom) {
+    pom.project {
+        name 'ControlsFX'
+        description 'High quality UI controls and other tools to complement the core JavaFX distribution'
+        url 'http://www.controlsfx.org/'
+        modelVersion '4.0.0'
+        packaging 'jar'
+        scm {
+            connection 'scm:hg:https://bitbucket.org/controlsfx/controlsfx'
+            developerConnection 'scm:hg:ssh://hg@bitbucket.org/controlsfx/controlsfx'
+            url 'https://bitbucket.org/controlsfx/controlsfx'
+        }
+        developers {
+            developer {
+                name 'Jonathan Giles'
+                email 'jonathan at jonathangiles.net'
+                roles {
+                    role 'author'
+                    role 'developer'
+                }
+            }
+        }
+        licenses {
+            license {
+                name 'The 3-Clause BSD License'
+                url 'http://www.opensource.org/licenses/bsd-license.php'
+                distribution 'repo'
+            }
+        }
+        properties {
+            //project.build.sourceEncoding='UTF-8'
+            setProperty('project.build.sourceEncoding', 'UTF8')
+        }
+      //}
+      //whenConfigured{
+      //   dependencies = dependencies.findAll {dep -> !pomExclusions.contains(dep.artifactId) } 
+      //}
+
+    }
+}
+
+ext.checkPasswords =  {
+    try {
+        def check = [sonatypeUsername, sonatypePassword]
+        println "Using sonatype user $sonatypeUsername"
+    } catch (MissingPropertyException e) {
+        e.printStackTrace()
+        Console console = System.console()
+        console.printf "\n\nIn order to upload to Sonatype we need your username and password.\nEnter a blank username or password to skip upload\n\n"
+        ext.sonatypeUsername = console.readLine("Sonatype Username: ")
+        ext.sonatypePassword = new String(console.readPassword("Sonatype Password: "))
+        if (!sonatypePassword || !sonatypeUsername) {
+            console.printf("\n\nSonatype upload aborted")
+            subprojects {
+                signing {
+                    enabled = false
+                }
+                uploadPublished {
+                    enabled = false
+                }
+            }
+            throw new StopExecutionException()
+        }
+    }
+}
+
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..89f8cf3
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,29 @@
+## Overview
+
+ControlsFX is an [open source project][1] for JavaFX that aims to provide really high quality UI controls and other tools to complement the core JavaFX distribution. It has been developed for JavaFX 8.0 and beyond, and has a guiding principle of only accepting new controls / features when all existing code is at an acceptably high level, including thankless jobs like having high quality javadoc documentation. This ensure a high quality release is available at all times, with all experime [...]
+
+You can learn more about [ControlsFX][1] on its website, and be sure to check out the [features page][2] for an overview of everything ControlsFX offers.
+
+If you think you have a feature you can contribute, a bug you want to fix please review our [guide for contributors][3]
+
+   [1]: http://controlsfx.org
+   [2]: http://controlsfx.org/features
+   [3]: https://bitbucket.org/controlsfx/controlsfx/wiki/Contributing%20to%20ControlsFX
+
+## Build Status and Project Links
+
+|                                                                                                                                          |
+|----------------------------------------------|--------------------------------------------------------------------------------------------|
+| Jenkins (ControlsFX 8 build status)           | ![Build Status](http://img.shields.io/jenkins/s/http/jonathangiles.no-ip.biz%3a81/ControlsFX.svg?style=flat)        |
+| Jenkins (ControlsFX 9 build status)           | ![Build Status](http://img.shields.io/jenkins/s/http/jonathangiles.no-ip.biz%3a81/ControlsFX%209.svg?style=flat)        |
+| Jenkins (ControlsFX samples build status)     | ![Build Status](http://img.shields.io/jenkins/s/http/jonathangiles.no-ip.biz%3a81/ControlsFX%20Samples.svg?style=flat) |
+| Jenkins (FXSampler build status)              | ![Build Status](http://img.shields.io/jenkins/s/http/jonathangiles.no-ip.biz%3a81/FXSampler.svg?style=flat)          | 
+| Latest published version in Maven Central     | [![Maven Central](http://img.shields.io/maven-central/v/org.controlsfx/controlsfx.svg?style=flat)](https://maven-badges.herokuapp.com/maven-central/org.controlsfx/controlsfx) |
+| Open Hub (Repository stats)                   | [![Open Hub project report for ControlsFX](https://www.openhub.net/p/controlsfx/widgets/project_thin_badge.gif)](https://www.openhub.net/p/controlsfx?ref=sample) |
+| Version Eye (External project dependencies)   | [![Dependency Status](http://www.versioneye.com/java/org.controlsfx%3Acontrolsfx/badge.svg?style=flat)](http://www.versioneye.com/java/org.controlsfx%3Acontrolsfx) |
+| Version Eye (External project references)     | [![Reference Status](http://www.versioneye.com/java/org.controlsfx%3Acontrolsfx/reference_badge.svg?style=flat)](http://www.versioneye.com/java/org.controlsfx%3Acontrolsfx/references) |
+| License                                       | [![License](http://img.shields.io/badge/license-BSD--3--Clause-red.svg?style=flat)](https://bitbucket.org/controlsfx/controlsfx/src/default/license.txt) |
+
+Thanks to 
+
+[![IntelliJ Idea](http://resources.jetbrains.com/assets/media/open-graph/intellij-idea_250x250.png)](https://www.jetbrains.com/idea)
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..3b21e44
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+include "controlsfx", "fxsampler", "controlsfx-samples"

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-java/libcontrolsfx-java.git



More information about the pkg-java-commits mailing list