Browse Source

:boom: Huge update [Part 3]
:boom: New controls: MFXCircleToggleNode, MFXRectangleToggleNode, MFXPasswordField

Demo:
:recycle: Updated toggles demo to reflect changes made to MFXToggleNode
:recycle: Refactored the table views demo. Now the table are shown in a bigger resizable window
:sparkles: Added a new section to show all the icons included in this project, see FontResources

MaterialFX:
MFXCircleToggleNode and MFXRectangleToggleNode:
:boom: I decided to split MFXToggleNode into two specialized controls, each with its own skin and features
:fire: MFXToggleNode has been removed

MFXTextField:
:sparkles: Introduced the possibility to add an icon to the field and to adjust its position manually
:sparkles: Added a MFXContextMenu to the field by default

MFXTextFieldSkin:
:recycle: Made little adjustments to the layout
:recycle: Made little changes to add the new features listed above

MFXPasswordField:
:boom: The long awaited MFXPasswordField. Extends MFXTextField and introduces specific features such as: show/hide password, change mask character

MFXTableView:
:boom: Even if the codebase is almost the same as before the control has received a huge refactor, new features have been implemented, the layout is now more stable and reliable. It's now possible to change columns at runtime, sort columns manually by using the table's sort model, it's possible to change the observable items list, the table now has a header which is fully customizable even at runtime, the selection model has been completely reviewed from scratch, it's now easier to manually update the table

MFXTableRow:
:recycle: Now the rows keep a reference to the represented item. They also include a ripple generator

MFXTableRowCell:
:boom: Does not extend Label anymore. Extends Control and defines its own skin. This makes easier to design custom row cells. Now it also offers the possibility of adding up to two icons/nodes to the cell (read the documentation please)

MFXTableColumn (renamed from MFXTableColumnCell):
:boom: Does not extend Label anymore. Extends Control and defines its own skin. This makes easier to design custom table columns. The tooltip feature has been reworked, now the columns tooltip is specified by a Supplier and can be changed anytime. Introduced the possibility to disable the DragResizer making the column not resizable, this can be switched anytime as well, this feature also introduces a new pseudo class ":resizable"

MFXFilterDialog, MFXEvaluationBox:
:boom: The MFXTableView dialog used for filtering the table has been redesigned to be more appealing. It is also a bit bigger than before
:boom: Added two new functions, "Start With Ignore Case" and "Ends With Ignore Case"

MFXComboBoxSkin, MFXFilterComboBoxSkin, ComboSelectionModelMock:
:bug: Fixed selection inconsistencies

MFXContextMenu:
:recycle: Set a min width of 100
:recycle: Made dispose() method public to allow removing/changing context menus
:bug: Fixed install() method if the node scene is not null

MFXContextMenuItem:
:bug: Fixed constructor oversight

MFXDatePickerContent:
:bug: Fixed MFXDatePicker showing incorrect days (Issue #33)

MFXFontIcon:
:sparkles: Added a method to get a random icon
:memo: Added/Updated documentation

MFXStepperToggle:
:recycle: Renamed property

MFXStepperToggleSkin:
:recycle: Request layout if the labelTextGap property changes

BindingUtils:
:recycle: Added a new toProperty method for object properties

DragResizer:
:recycle: Added the possibility of enabling/disabling it anytime by adding /removing the needed mouse event filters

FontResources:
:recycle: Whoops, I lost the latest JSON of the icons and I had to redo the whole thing again 🙂

MFXCircleRippleGenerator:
:recycle: Throw NullPointerException if the clip supplier is null or returns null and animateBackground is set to true

RippleClipTypeFactory:
:sparkles: Added methods to specify both arcs if they are the same, and to adjust the clip width/height

StringUtils
:sparkles: Added two new methods

Documentation:
:recycle: Updated
:recycle: Fixed some compiler warnings

Signed-off-by: palexdev <alessandro.parisi406@gmail.com>

palexdev 4 years ago
parent
commit
24338022af
72 changed files with 4474 additions and 2237 deletions
  1. 1 0
      .gitignore
  2. 17 16
      demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/DemoController.java
  3. 96 0
      demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/FontResourcesDemoController.java
  4. 60 25
      demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/TableViewsDemoController.java
  5. 5 1
      demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/TextFieldsDemoController.java
  6. 21 1
      demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/TogglesController.java
  7. 7 9
      demo/src/main/resources/io/github/palexdev/materialfx/demo/css/demo.css
  8. 2 2
      demo/src/main/resources/io/github/palexdev/materialfx/demo/demo.fxml
  9. 14 0
      demo/src/main/resources/io/github/palexdev/materialfx/demo/fontresources_demo.fxml
  10. 10 22
      demo/src/main/resources/io/github/palexdev/materialfx/demo/tableviews_demo.fxml
  11. 6 4
      demo/src/main/resources/io/github/palexdev/materialfx/demo/textfields_demo.fxml
  12. 49 55
      demo/src/main/resources/io/github/palexdev/materialfx/demo/toggle_buttons_demo.fxml
  13. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/beans/MFXContextMenuItem.java
  14. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/collections/ObservableStack.java
  15. 194 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXCircleToggleNode.java
  16. 12 1
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXContextMenu.java
  17. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXFilterComboBox.java
  18. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXLabel.java
  19. 231 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXPasswordField.java
  20. 109 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXRectangleToggleNode.java
  21. 12 12
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXStepperToggle.java
  22. 42 64
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXTableRow.java
  23. 102 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXTableSortModel.java
  24. 244 58
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXTableView.java
  25. 123 1
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXTextField.java
  26. 0 309
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXToggleNode.java
  27. 3 3
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/base/AbstractMFXFlowlessListCell.java
  28. 272 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/base/AbstractMFXToggleNode.java
  29. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/base/AbstractMFXTreeCell.java
  30. 3 3
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/cell/MFXFlowlessCheckListCell.java
  31. 272 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/cell/MFXTableColumn.java
  32. 0 209
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/cell/MFXTableColumnCell.java
  33. 131 53
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/cell/MFXTableRowCell.java
  34. 10 19
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/enums/SortState.java
  35. 22 4
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/factories/RippleClipTypeFactory.java
  36. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/legacy/MFXLegacyComboBox.java
  37. 4 0
      materialfx/src/main/java/io/github/palexdev/materialfx/effects/ripple/MFXCircleRippleGenerator.java
  38. 3 9
      materialfx/src/main/java/io/github/palexdev/materialfx/filter/EvaluationMode.java
  39. 157 0
      materialfx/src/main/java/io/github/palexdev/materialfx/filter/MFXEvaluationBox.java
  40. 123 295
      materialfx/src/main/java/io/github/palexdev/materialfx/filter/MFXFilterDialog.java
  41. 70 65
      materialfx/src/main/java/io/github/palexdev/materialfx/font/FontResources.java
  42. 23 7
      materialfx/src/main/java/io/github/palexdev/materialfx/font/MFXFontIcon.java
  43. 35 13
      materialfx/src/main/java/io/github/palexdev/materialfx/selection/ComboSelectionModelMock.java
  44. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/selection/ListSelectionModel.java
  45. 140 79
      materialfx/src/main/java/io/github/palexdev/materialfx/selection/TableSelectionModel.java
  46. 20 24
      materialfx/src/main/java/io/github/palexdev/materialfx/selection/base/ITableSelectionModel.java
  47. 219 0
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXCircleToggleNodeSkin.java
  48. 46 0
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXComboBoxSkin.java
  49. 1 1
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXDateCellSkin.java
  50. 8 12
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXDatePickerContent.java
  51. 53 5
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXFilterComboBoxSkin.java
  52. 337 0
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXPasswordFieldSkin.java
  53. 180 0
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXRectangleToggleNodeSkin.java
  54. 4 2
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXStepperToggleSkin.java
  55. 0 115
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXTableColumnCellSkin.java
  56. 116 0
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXTableColumnSkin.java
  57. 79 0
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXTableRowCellSkin.java
  58. 496 569
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXTableViewSkin.java
  59. 66 3
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXTextFieldSkin.java
  60. 9 0
      materialfx/src/main/java/io/github/palexdev/materialfx/utils/BindingUtils.java
  61. 25 7
      materialfx/src/main/java/io/github/palexdev/materialfx/utils/DragResizer.java
  62. 15 0
      materialfx/src/main/java/io/github/palexdev/materialfx/utils/StringUtils.java
  63. 28 32
      materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-circle-togglenode.css
  64. 26 0
      materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-evaluationbox.css
  65. 23 0
      materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-filterdialog.css
  66. 9 14
      materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-passwordfield.css
  67. 58 0
      materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-rectangle-togglenode.css
  68. 0 53
      materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-table-column-cell.css
  69. 8 0
      materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-tablecolumn.css
  70. 9 0
      materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-tablerow.css
  71. 7 54
      materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-tableview.css
  72. BIN
      materialfx/src/main/resources/io/github/palexdev/materialfx/fonts/materialfx-resources.ttf

+ 1 - 0
.gitignore

@@ -11,5 +11,6 @@ out/
 
 # Others
 demo/src/main/java/io/github/palexdev/materialfx/demo/TestDemo.java
+demo/src/main/resources/io/github/palexdev/materialfx/demo/css/TestDemo.css
 materialfx/src/test
 demo/scenicView.properties

+ 17 - 16
demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/DemoController.java

@@ -118,21 +118,22 @@ public class DemoController implements Initializable {
 
         // VLoader
         vLoader.setContentPane(contentPane);
-        vLoader.addItem("BUTTONS", Builder.build(new MFXToggleNode("BUTTONS"), MFXResourcesLoader.load("buttons_demo.fxml")).setDefaultRoot(true));
-        vLoader.addItem("CHECKBOXES", Builder.build(new MFXToggleNode("CHECKBOXES"), MFXResourcesLoader.load("checkboxes_demo.fxml")));
-        vLoader.addItem("COMBOBOXES", Builder.build(new MFXToggleNode("COMBOBOXES"), MFXResourcesLoader.load("combo_boxes_demo.fxml")));
-        vLoader.addItem("DATEPICKERS", Builder.build(new MFXToggleNode("DATEPICKERS"), MFXResourcesLoader.load("datepickers_demo.fxml")));
-        vLoader.addItem("DIALOGS", Builder.build(new MFXToggleNode("DIALOGS"), MFXResourcesLoader.load("dialogs_demo.fxml")).setControllerFactory(controller -> new DialogsController(demoPane)));
-        vLoader.addItem("LABELS", Builder.build(new MFXToggleNode("LABELS"), MFXResourcesLoader.load("labels_demo.fxml")));
-        vLoader.addItem("LISTVIEWS", Builder.build(new MFXToggleNode("LISTVIEWS"), MFXResourcesLoader.load("listviews_demo.fxml")));
-        vLoader.addItem("NOTIFICATIONS", Builder.build(new MFXToggleNode("NOTIFICATIONS"), MFXResourcesLoader.load("notifications_demo.fxml")));
-        vLoader.addItem("PROGRESS_SPINNERS", Builder.build(new MFXToggleNode("PROGRESS_SPINNERS"), MFXResourcesLoader.load("progress_spinners_demo.fxml")));
-        vLoader.addItem("RADIOBUTTONS", Builder.build(new MFXToggleNode("RADIOBUTTONS"), MFXResourcesLoader.load("radio_buttons_demo.fxml")));
-        vLoader.addItem("SCROLLPANES", Builder.build(new MFXToggleNode("SCROLLPANES"), MFXResourcesLoader.load("scrollpanes_demo.fxml")));
-        vLoader.addItem("TABLEVIEWS", Builder.build(new MFXToggleNode("TABLEVIEWS"), MFXResourcesLoader.load("tableviews_demo.fxml")));
-        vLoader.addItem("TEXTFIELDS", Builder.build(new MFXToggleNode("TEXTFIELDS"), MFXResourcesLoader.load("textfields_demo.fxml")));
-        vLoader.addItem("TOGGLES", Builder.build(new MFXToggleNode("TOGGLES"), MFXResourcesLoader.load("toggle_buttons_demo.fxml")));
-        vLoader.addItem("TREEVIEWS", Builder.build(new MFXToggleNode("TREEVIEWS"), MFXResourcesLoader.load("treeviews_demo.fxml")));
+        vLoader.addItem("BUTTONS", Builder.build(new MFXRectangleToggleNode("BUTTONS"), MFXResourcesLoader.load("buttons_demo.fxml")).setDefaultRoot(true));
+        vLoader.addItem("CHECKBOXES", Builder.build(new MFXRectangleToggleNode("CHECKBOXES"), MFXResourcesLoader.load("checkboxes_demo.fxml")));
+        vLoader.addItem("COMBOBOXES", Builder.build(new MFXRectangleToggleNode("COMBOBOXES"), MFXResourcesLoader.load("combo_boxes_demo.fxml")));
+        vLoader.addItem("DATEPICKERS", Builder.build(new MFXRectangleToggleNode("DATEPICKERS"), MFXResourcesLoader.load("datepickers_demo.fxml")));
+        vLoader.addItem("DIALOGS", Builder.build(new MFXRectangleToggleNode("DIALOGS"), MFXResourcesLoader.load("dialogs_demo.fxml")).setControllerFactory(controller -> new DialogsController(demoPane)));
+        vLoader.addItem("LABELS", Builder.build(new MFXRectangleToggleNode("LABELS"), MFXResourcesLoader.load("labels_demo.fxml")));
+        vLoader.addItem("LISTVIEWS", Builder.build(new MFXRectangleToggleNode("LISTVIEWS"), MFXResourcesLoader.load("listviews_demo.fxml")));
+        vLoader.addItem("NOTIFICATIONS", Builder.build(new MFXRectangleToggleNode("NOTIFICATIONS"), MFXResourcesLoader.load("notifications_demo.fxml")));
+        vLoader.addItem("PROGRESS_SPINNERS", Builder.build(new MFXRectangleToggleNode("PROGRESS SPINNERS"), MFXResourcesLoader.load("progress_spinners_demo.fxml")));
+        vLoader.addItem("RADIOBUTTONS", Builder.build(new MFXRectangleToggleNode("RADIOBUTTONS"), MFXResourcesLoader.load("radio_buttons_demo.fxml")));
+        vLoader.addItem("SCROLLPANES", Builder.build(new MFXRectangleToggleNode("SCROLLPANES"), MFXResourcesLoader.load("scrollpanes_demo.fxml")));
+        vLoader.addItem("TABLEVIEWS", Builder.build(new MFXRectangleToggleNode("TABLEVIEWS"), MFXResourcesLoader.load("tableviews_demo.fxml")));
+        vLoader.addItem("TEXTFIELDS", Builder.build(new MFXRectangleToggleNode("TEXTFIELDS"), MFXResourcesLoader.load("textfields_demo.fxml")));
+        vLoader.addItem("TOGGLES", Builder.build(new MFXRectangleToggleNode("TOGGLES"), MFXResourcesLoader.load("toggle_buttons_demo.fxml")));
+        vLoader.addItem("TREEVIEWS", Builder.build(new MFXRectangleToggleNode("TREEVIEWS"), MFXResourcesLoader.load("treeviews_demo.fxml")));
+        vLoader.addItem("FONTRESOURCES", Builder.build(new MFXRectangleToggleNode("FONTRESOURCES"), MFXResourcesLoader.load("fontresources_demo.fxml")));
         vLoader.start();
 
         // Others
@@ -169,7 +170,7 @@ public class DemoController implements Initializable {
 
         Timeline fadeOut = MFXAnimationFactory.FADE_OUT.build(navBar, 50);
         Timeline close = new Timeline(
-                new KeyFrame(Duration.millis(300), new KeyValue(navBar.translateXProperty(), -200))
+                new KeyFrame(Duration.millis(300), new KeyValue(navBar.translateXProperty(), -240))
         );
         Timeline right = new Timeline(
                 new KeyFrame(Duration.millis(200), new KeyValue(opNavButton.rotateProperty(), 0))

+ 96 - 0
demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/FontResourcesDemoController.java

@@ -0,0 +1,96 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
+ *
+ *     MaterialFX is free software: you can redistribute it and/or modify
+ *     it under the terms of the GNU General Public License as published by
+ *     the Free Software Foundation, either version 3 of the License, or
+ *     (at your option) any later version.
+ *
+ *     MaterialFX is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *     GNU General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.demo.controllers;
+
+import io.github.palexdev.materialfx.controls.MFXFlowlessListView;
+import io.github.palexdev.materialfx.controls.MFXIconWrapper;
+import io.github.palexdev.materialfx.controls.MFXLabel;
+import io.github.palexdev.materialfx.controls.cell.MFXFlowlessListCell;
+import io.github.palexdev.materialfx.font.FontResources;
+import io.github.palexdev.materialfx.font.MFXFontIcon;
+import javafx.fxml.FXML;
+import javafx.fxml.Initializable;
+import javafx.geometry.Insets;
+import javafx.geometry.Orientation;
+import javafx.geometry.Pos;
+import javafx.scene.control.Separator;
+import javafx.scene.layout.HBox;
+import javafx.scene.paint.Color;
+
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ResourceBundle;
+import java.util.stream.Collectors;
+
+public class FontResourcesDemoController implements Initializable {
+
+    @FXML
+    private MFXFlowlessListView<HBox> list;
+
+
+    @Override
+    public void initialize(URL location, ResourceBundle resources) {
+        list.setCellFactory(hBox -> {
+            MFXFlowlessListCell<HBox> cell = new MFXFlowlessListCell<>(list, hBox);
+            cell.setFixedCellHeight(48);
+            return cell;
+        });
+        MFXFlowlessListView.setSmoothScrolling(list);
+        populateList();
+    }
+
+    private void populateList() {
+        List<FontResources> fontResources = Arrays.asList(FontResources.values());
+        fontResources.sort(Comparator.comparing(FontResources::name));
+
+
+        List<HBox> resBoxes = fontResources.stream().map(this::buildNode).collect(Collectors.toList());
+        list.setItems(resBoxes);
+    }
+
+    private HBox buildNode(FontResources fontResource) {
+        MFXFontIcon icon = new MFXFontIcon(fontResource.getDescription(), 20);
+        MFXLabel l1 = new MFXLabel();
+        l1.setLineColor(Color.TRANSPARENT);
+        l1.setUnfocusedLineColor(Color.TRANSPARENT);
+        l1.setStyle("-fx-background-color: transparent");
+        l1.setText("Description: " + fontResource.getDescription());
+        l1.setMinWidth(300);
+
+        MFXLabel l2 = new MFXLabel();
+        l2.setLineColor(Color.TRANSPARENT);
+        l2.setUnfocusedLineColor(Color.TRANSPARENT);
+        l2.setStyle("-fx-background-color: transparent");
+        l2.setText("Code: " + Integer.toHexString(fontResource.getCode() | 0x10000).substring(1).toUpperCase());
+        l2.setMinWidth(300);
+
+        Separator s1 = new Separator(Orientation.VERTICAL);
+        s1.setStyle("-fx-fill: white");
+        Separator s2 = new Separator(Orientation.VERTICAL);
+        s2.setStyle("-fx-fill: white");
+
+
+        HBox box = new HBox(10, new MFXIconWrapper(icon, 24), s1, l1, s2, l2);
+        box.setPadding(new Insets(5));
+        box.setAlignment(Pos.CENTER_LEFT);
+        return box;
+    }
+}

+ 60 - 25
demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/TableViewsDemoController.java

@@ -20,17 +20,24 @@ package io.github.palexdev.materialfx.demo.controllers;
 
 import io.github.palexdev.materialfx.controls.MFXButton;
 import io.github.palexdev.materialfx.controls.MFXTableView;
-import io.github.palexdev.materialfx.controls.cell.MFXTableColumnCell;
+import io.github.palexdev.materialfx.controls.cell.MFXTableColumn;
 import io.github.palexdev.materialfx.controls.cell.MFXTableRowCell;
 import io.github.palexdev.materialfx.controls.legacy.MFXLegacyTableView;
 import io.github.palexdev.materialfx.demo.model.FilterablePerson;
 import io.github.palexdev.materialfx.demo.model.Person;
+import javafx.application.Platform;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
 import javafx.collections.FXCollections;
 import javafx.collections.ObservableList;
 import javafx.fxml.FXML;
 import javafx.fxml.Initializable;
 import javafx.geometry.Pos;
+import javafx.scene.Scene;
 import javafx.scene.control.TableColumn;
+import javafx.scene.layout.StackPane;
+import javafx.stage.Modality;
+import javafx.stage.Stage;
 
 import java.net.URL;
 import java.util.Comparator;
@@ -38,26 +45,46 @@ import java.util.List;
 import java.util.ResourceBundle;
 
 public class TableViewsDemoController implements Initializable {
+    private final ObjectProperty<Stage> tableStage = new SimpleObjectProperty<>();
+    private final MFXLegacyTableView<Person> legacyTable;
+    private final MFXTableView<FilterablePerson> tableView;
+    private final StackPane stackPane = new StackPane();
+    private final Scene scene = new Scene(stackPane, 800, 600);
 
     @FXML
-    private MFXButton switchButton;
+    private MFXButton showLegacy;
 
     @FXML
-    private MFXLegacyTableView<Person> legacyTable;
+    private MFXButton showNew;
 
-    @FXML
-    private MFXTableView<FilterablePerson> table;
+    public TableViewsDemoController() {
+        tableStage.addListener((observable, oldValue, newValue) -> {
+            getTableStage().initOwner(showLegacy.getScene().getWindow());
+            getTableStage().initModality(Modality.APPLICATION_MODAL);
+        });
+
+        Platform.runLater(() -> tableStage.set(new Stage()));
+
+        legacyTable = new MFXLegacyTableView<>();
+        tableView = new MFXTableView<>();
+    }
 
     @Override
     public void initialize(URL location, ResourceBundle resources) {
-        switchButton.setOnAction(event -> {
-            if (legacyTable.isVisible()) {
-                legacyTable.setVisible(false);
-                table.setVisible(true);
-            } else {
-                legacyTable.setVisible(true);
-                table.setVisible(false);
-            }
+        showLegacy.setOnAction(event -> {
+            getTableStage().close();
+            stackPane.getChildren().setAll(legacyTable);
+            getTableStage().setScene(scene);
+            getTableStage().setTitle("Legacy TableView - Preview");
+            getTableStage().show();
+        });
+
+        showNew.setOnAction(event -> {
+            getTableStage().close();
+            stackPane.getChildren().setAll(tableView);
+            getTableStage().setScene(scene);
+            getTableStage().setTitle("New TableView - Preview");
+            getTableStage().show();
         });
 
         populateLegacy();
@@ -112,22 +139,30 @@ public class TableViewsDemoController implements Initializable {
                 )
         );
 
-        MFXTableColumnCell<FilterablePerson> firstNameColumn = new MFXTableColumnCell<>("First Name", Comparator.comparing(FilterablePerson::getFirstName));
-        firstNameColumn.setRowCellFactory(person -> new MFXTableRowCell(person.firstNameProperty()));
-        MFXTableColumnCell<FilterablePerson> lastNameColumn = new MFXTableColumnCell<>("Last Name", Comparator.comparing(FilterablePerson::getLastName));
-        lastNameColumn.setRowCellFactory(person -> new MFXTableRowCell(person.lastNameProperty()));
-        MFXTableColumnCell<FilterablePerson> addressColumn = new MFXTableColumnCell<>("Address", Comparator.comparing(FilterablePerson::getAddress));
-        addressColumn.setRowCellFactory(person -> new MFXTableRowCell(person.addressProperty()));
-        MFXTableColumnCell<FilterablePerson> ageColumn = new MFXTableColumnCell<>("Age", Comparator.comparing(FilterablePerson::getAge));
-        ageColumn.setRowCellFactory(person -> new MFXTableRowCell(person.ageProperty().asString()) {
+        MFXTableColumn<FilterablePerson> firstNameColumn = new MFXTableColumn<>("First Name", Comparator.comparing(FilterablePerson::getFirstName));
+        firstNameColumn.setRowCellFunction(person -> new MFXTableRowCell(person.firstNameProperty()));
+        MFXTableColumn<FilterablePerson> lastNameColumn = new MFXTableColumn<>("Last Name", Comparator.comparing(FilterablePerson::getLastName));
+        lastNameColumn.setRowCellFunction(person -> new MFXTableRowCell(person.lastNameProperty()));
+        MFXTableColumn<FilterablePerson> addressColumn = new MFXTableColumn<>("Address", Comparator.comparing(FilterablePerson::getAddress));
+        addressColumn.setRowCellFunction(person -> new MFXTableRowCell(person.addressProperty()));
+        MFXTableColumn<FilterablePerson> ageColumn = new MFXTableColumn<>("Age", Comparator.comparing(FilterablePerson::getAge));
+        ageColumn.setRowCellFunction(person -> new MFXTableRowCell(person.ageProperty().asString()) {
             {
-                setAlignment(Pos.CENTER_RIGHT);
+                setRowAlignment(Pos.CENTER_RIGHT);
             }
         });
-        ageColumn.setAlignment(Pos.CENTER_RIGHT);
+        ageColumn.setColumnAlignment(Pos.CENTER_RIGHT);
+
+        tableView.setItems(people);
+        tableView.getTableColumns().addAll(firstNameColumn, lastNameColumn, addressColumn, ageColumn);
+    }
+
+    public Stage getTableStage() {
+        return tableStage.get();
+    }
 
-        table.setItems(people);
-        table.getColumns().addAll(firstNameColumn, lastNameColumn, addressColumn, ageColumn);
+    public void setTableStage(Stage tableStage) {
+        this.tableStage.set(tableStage);
     }
 }
 

+ 5 - 1
demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/TextfieldsDemoController.java → demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/TextFieldsDemoController.java

@@ -3,19 +3,21 @@ package io.github.palexdev.materialfx.demo.controllers;
 import io.github.palexdev.materialfx.controls.MFXCheckbox;
 import io.github.palexdev.materialfx.controls.MFXDatePicker;
 import io.github.palexdev.materialfx.controls.MFXTextField;
+import io.github.palexdev.materialfx.font.MFXFontIcon;
 import io.github.palexdev.materialfx.utils.BindingUtils;
 import javafx.beans.binding.Bindings;
 import javafx.beans.property.BooleanProperty;
 import javafx.fxml.FXML;
 import javafx.fxml.Initializable;
 import javafx.scene.control.Label;
+import javafx.scene.paint.Color;
 
 import java.net.URL;
 import java.time.LocalDate;
 import java.time.Month;
 import java.util.ResourceBundle;
 
-public class TextfieldsDemoController implements Initializable {
+public class TextFieldsDemoController implements Initializable {
 
     @FXML
     private MFXTextField validated;
@@ -53,6 +55,8 @@ public class TextfieldsDemoController implements Initializable {
         validated.getValidator().add(checkboxValidation, "Checkbox must be selected");
         validated.getValidator().add(datePickerValidation, "Selected date must be 03/10/1911");
         validated.setValidated(true);
+        validated.setIcon(new MFXFontIcon("mfx-variant7-mark", 16, Color.web("#8FF7A7")));
+        validated.getIcon().visibleProperty().bind(validated.getValidator().validProperty());
 
         label.visibleProperty().bind(validated.getValidator().validProperty());
     }

+ 21 - 1
demo/src/main/java/io/github/palexdev/materialfx/demo/controllers/TogglesController.java

@@ -1,14 +1,34 @@
 package io.github.palexdev.materialfx.demo.controllers;
 
+import io.github.palexdev.materialfx.controls.MFXRectangleToggleNode;
 import io.github.palexdev.materialfx.controls.MFXToggleButton;
+import io.github.palexdev.materialfx.font.MFXFontIcon;
 import io.github.palexdev.materialfx.utils.ColorUtils;
 import javafx.fxml.FXML;
+import javafx.fxml.Initializable;
 
-public class TogglesController {
+import java.net.URL;
+import java.util.ResourceBundle;
+
+public class TogglesController implements Initializable {
 
     @FXML
     private MFXToggleButton toggleButton;
 
+    @FXML
+    private MFXRectangleToggleNode rec1;
+
+    @FXML
+    private MFXRectangleToggleNode rec2;
+
+    @Override
+    public void initialize(URL location, ResourceBundle resources) {
+        rec1.setLabelLeadingIcon(MFXFontIcon.getRandomIcon(16, ColorUtils.getRandomColor()));
+        rec1.setLabelTrailingIcon(MFXFontIcon.getRandomIcon(16, ColorUtils.getRandomColor()));
+        rec2.setLabelLeadingIcon(MFXFontIcon.getRandomIcon(16, ColorUtils.getRandomColor()));
+        rec2.setLabelTrailingIcon(MFXFontIcon.getRandomIcon(16, ColorUtils.getRandomColor()));
+    }
+
     @FXML
     private void handleButtonClick() {
         toggleButton.setToggleColor(ColorUtils.getRandomColor());

+ 7 - 9
demo/src/main/resources/io/github/palexdev/materialfx/demo/css/demo.css

@@ -76,25 +76,23 @@
     -fx-border-color: #7F0FFF;
     -fx-border-radius: 6;
     -fx-border-insets: -1;
-    -fx-border-width: 1.2;
-    -mfx-shape: rectangle;
-    -mfx-size: 35px;
+    -fx-pref-width: 160;
 }
 
-#vLoader .mfx-toggle-node .text {
-    -fx-font-family: 'Open Sans Bold';
-    -fx-fill: #7F0FFF;
-    -fx-font-size: 13;
+#vLoader .mfx-toggle-node .mfx-label {
+    -mfx-font-family: 'Open Sans Bold';
+    -mfx-text-fill: #7F0FFF;
+    -mfx-font-size: 13;
     -fx-opacity: 0.7;
 }
 
 #vLoader .mfx-toggle-node:hover,
-#vLoader .mfx-toggle-node:hover .text {
+#vLoader .mfx-toggle-node:hover .mfx-label {
     -fx-opacity: 1.0;
 }
 
 #vLoader .mfx-toggle-node:selected {
-    -fx-background-color: white;
+    -fx-background-color: derive(#7F0FFF, 150%);
     -fx-opacity: 1;
 }
 

+ 2 - 2
demo/src/main/resources/io/github/palexdev/materialfx/demo/demo.fxml

@@ -4,13 +4,13 @@
 <?import io.github.palexdev.materialfx.controls.MFXVLoader?>
 <?import javafx.geometry.*?>
 <?import javafx.scene.layout.*?>
-<StackPane id="demoPane" fx:id="demoPane" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="550.0" prefWidth="960.0" stylesheets="@css/demo.css" xmlns="http://javafx.com/javafx/15.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="io.github.palexdev.materialfx.demo.controllers.DemoController">
+<StackPane id="demoPane" fx:id="demoPane" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="550.0" prefWidth="960.0" stylesheets="@css/demo.css" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1" fx:controller="io.github.palexdev.materialfx.demo.controllers.DemoController">
    <StackPane id="contentPane" fx:id="contentPane" prefHeight="500.0" prefWidth="441.0">
       <StackPane.margin>
          <Insets bottom="15.0" left="20.0" right="20.0" top="15.0" />
       </StackPane.margin>
    </StackPane>
-   <StackPane id="navBar" fx:id="navBar" maxWidth="-Infinity" style="-fx-background-radius: 10; -fx-background-color: white;" translateX="-200.0" StackPane.alignment="CENTER_LEFT">
+   <StackPane id="navBar" fx:id="navBar" maxWidth="-Infinity" prefWidth="200.0" style="-fx-background-radius: 10; -fx-background-color: white;" translateX="-240.0" StackPane.alignment="CENTER_LEFT">
       <StackPane.margin>
          <Insets bottom="15.0" left="15.0" top="15.0" />
       </StackPane.margin>

+ 14 - 0
demo/src/main/resources/io/github/palexdev/materialfx/demo/fontresources_demo.fxml

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import io.github.palexdev.materialfx.controls.MFXFlowlessListView?>
+<?import javafx.geometry.Insets?>
+<?import javafx.scene.layout.StackPane?>
+<StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="500.0"
+           prefWidth="900.0" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1"
+           fx:controller="io.github.palexdev.materialfx.demo.controllers.FontResourcesDemoController">
+   <MFXFlowlessListView fx:id="list" depthLevel="LEVEL1">
+      <StackPane.margin>
+         <Insets bottom="20.0" left="20.0" right="20.0" top="20.0"/>
+      </StackPane.margin>
+   </MFXFlowlessListView>
+</StackPane>

+ 10 - 22
demo/src/main/resources/io/github/palexdev/materialfx/demo/tableviews_demo.fxml

@@ -1,37 +1,25 @@
 <?xml version="1.0" encoding="UTF-8"?>
 
-<?import io.github.palexdev.materialfx.controls.legacy.MFXLegacyTableView?>
 <?import io.github.palexdev.materialfx.controls.MFXButton?>
-<?import io.github.palexdev.materialfx.controls.MFXTableView?>
-<?import javafx.geometry.*?>
+<?import javafx.geometry.Insets?>
 <?import javafx.scene.control.Label?>
 <?import javafx.scene.layout.*?>
-<StackPane prefHeight="500.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/15.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="io.github.palexdev.materialfx.demo.controllers.TableViewsDemoController">
+<StackPane prefHeight="180.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1" fx:controller="io.github.palexdev.materialfx.demo.controllers.TableViewsDemoController">
     <Label id="customLabel" alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="Table Views" StackPane.alignment="TOP_CENTER">
         <StackPane.margin>
           <Insets top="20.0" />
         </StackPane.margin>
     </Label>
-    <StackPane maxHeight="-Infinity" maxWidth="1.7976931348623157E308" prefHeight="430.0" prefWidth="570.0" StackPane.alignment="BOTTOM_CENTER">
+    <HBox maxHeight="-Infinity" maxWidth="-Infinity" spacing="50.0" StackPane.alignment="TOP_CENTER">
         <StackPane.margin>
-            <Insets bottom="15.0" left="15.0" right="15.0" />
+            <Insets top="75.0"/>
         </StackPane.margin>
         <padding>
-            <Insets top="20.0" />
+            <Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
         </padding>
-        <MFXButton fx:id="switchButton" buttonType="RAISED" depthLevel="LEVEL1" text="Switch Table View" StackPane.alignment="TOP_CENTER">
-         <StackPane.margin>
-            <Insets />
-         </StackPane.margin></MFXButton>
-        <MFXLegacyTableView fx:id="legacyTable" maxHeight="-Infinity" prefHeight="400.0">
-            <StackPane.margin>
-                <Insets left="5.0" right="5.0" top="35.0" />
-            </StackPane.margin>
-        </MFXLegacyTableView>
-        <MFXTableView fx:id="table" visible="false" StackPane.alignment="BOTTOM_CENTER">
-            <StackPane.margin>
-                <Insets left="5.0" right="5.0" />
-            </StackPane.margin>
-        </MFXTableView>
-    </StackPane>
+        <MFXButton fx:id="showLegacy" buttonType="RAISED" depthLevel="LEVEL1" prefHeight="32.0" prefWidth="120.0"
+                   text="Show Legacy"/>
+        <MFXButton fx:id="showNew" buttonType="RAISED" depthLevel="LEVEL1" prefHeight="32.0" prefWidth="120.0"
+                   text="Show New"/>
+    </HBox>
 </StackPane>

+ 6 - 4
demo/src/main/resources/io/github/palexdev/materialfx/demo/textfields_demo.fxml

@@ -4,7 +4,7 @@
 <?import javafx.geometry.*?>
 <?import javafx.scene.control.Label?>
 <?import javafx.scene.layout.*?>
-<StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" stylesheets="@css/textfields_demo.css" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1" fx:controller="io.github.palexdev.materialfx.demo.controllers.TextfieldsDemoController">
+<StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" stylesheets="@css/textfields_demo.css" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1" fx:controller="io.github.palexdev.materialfx.demo.controllers.TextFieldsDemoController">
     <Label id="customLabel" alignment="CENTER" prefHeight="26.0" prefWidth="266.0" text="TextFields" StackPane.alignment="TOP_CENTER">
         <StackPane.margin>
           <Insets top="20.0" />
@@ -30,9 +30,11 @@
       </StackPane.margin>
    </Label>
    <HBox maxHeight="-Infinity" maxWidth="-Infinity" spacing="100.0">
-      <MFXTextField id="colors" alignment="CENTER" lineColor="#52db32" maxWidth="-Infinity" prefWidth="120.0"
-                    promptText="Colors"/>
-      <MFXTextField fx:id="validated" alignment="CENTER" maxWidth="-Infinity" prefWidth="120.0" text="Validation"/>
+      <MFXTextField id="colors" alignment="CENTER" lineColor="#52db32" maxWidth="-Infinity" prefWidth="120.0" promptText="Colors" />
+      <MFXTextField fx:id="validated" maxWidth="-Infinity" prefWidth="120.0" text="Validation">
+         <HBox.margin>
+            <Insets />
+         </HBox.margin></MFXTextField>
    </HBox>
    <MFXCheckbox fx:id="checkbox" checkedColor="#00e240" markSize="9.0" markType="mfx-variant9-mark" text="CheckBox Validation" StackPane.alignment="BOTTOM_LEFT">
       <StackPane.margin>

+ 49 - 55
demo/src/main/resources/io/github/palexdev/materialfx/demo/toggle_buttons_demo.fxml

@@ -5,74 +5,68 @@
 <?import javafx.scene.control.Label?>
 <?import javafx.scene.layout.*?>
 <?import org.kordamp.ikonli.javafx.FontIcon?>
-<StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" stylesheets="@css/toggle_buttons_demo.css" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="io.github.palexdev.materialfx.demo.controllers.TogglesController">
-   <MFXToggleButton StackPane.alignment="TOP_CENTER">
-      <StackPane.margin>
-         <Insets right="320.0" top="60.0" />
-      </StackPane.margin>
-   </MFXToggleButton>
-   <MFXToggleButton toggleColor="#008f1b" toggleLineColor="#aaff00" StackPane.alignment="TOP_CENTER">
-      <StackPane.margin>
-         <Insets top="60.0" />
-      </StackPane.margin>
-   </MFXToggleButton>
-   <MFXToggleButton toggleColor="#b200ff" toggleLineColor="#ff00f6" unToggleColor="#797979" unToggleLineColor="#bfbfbf" StackPane.alignment="TOP_CENTER">
-      <StackPane.margin>
-         <Insets left="320.0" top="60.0" />
-      </StackPane.margin>
-   </MFXToggleButton>
+<StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="450.0" prefWidth="600.0" stylesheets="@css/toggle_buttons_demo.css" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1" fx:controller="io.github.palexdev.materialfx.demo.controllers.TogglesController">
    <Label id="customLabel" alignment="CENTER" maxWidth="266.0" prefHeight="26.0" text="Toggle Buttons" StackPane.alignment="TOP_CENTER">
       <StackPane.margin>
          <Insets top="20.0" />
       </StackPane.margin>
    </Label>
-   <MFXButton buttonType="RAISED" depthLevel="LEVEL1" onAction="#handleButtonClick" rippleColor="#0096ed" rippleRadius="30.0" text="Change color">
-      <StackPane.margin>
-         <Insets bottom="135.0" right="370.0" />
-      </StackPane.margin>
-   </MFXButton>
-   <MFXToggleButton fx:id="toggleButton" automaticColorAdjustment="true" text="Automatic Colors" toggleColor="#006aff" StackPane.alignment="TOP_CENTER">
+   <HBox alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity" spacing="30.0" StackPane.alignment="TOP_CENTER">
       <StackPane.margin>
-         <Insets right="320.0" top="140.0" />
+         <Insets top="50.0"/>
       </StackPane.margin>
-   </MFXToggleButton>
-   <MFXToggleButton id="customRippleRadius" automaticColorAdjustment="true" text="RippleRadiusCss" toggleColor="#0095c2" StackPane.alignment="TOP_CENTER">
+      <MFXToggleButton/>
+      <MFXToggleButton toggleColor="#008f1b" toggleLineColor="#aaff00"/>
+      <MFXToggleButton toggleColor="#b200ff" toggleLineColor="#ff00f6" unToggleColor="#797979"
+                       unToggleLineColor="#bfbfbf"/>
+   </HBox>
+   <HBox alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity" spacing="15.0" StackPane.alignment="TOP_CENTER">
       <StackPane.margin>
-         <Insets top="140.0" />
+         <Insets top="120.0"/>
       </StackPane.margin>
-   </MFXToggleButton>
-   <MFXToggleButton id="customRippleRadius" automaticColorAdjustment="true" disable="true" text="Disabled" toggleColor="#0095c2" StackPane.alignment="TOP_CENTER">
-      <StackPane.margin>
-         <Insets left="320.0" top="140.0" />
-      </StackPane.margin>
-   </MFXToggleButton>
+      <MFXButton buttonType="RAISED" depthLevel="LEVEL1" onAction="#handleButtonClick" rippleColor="#0096ed"
+                 rippleRadius="30.0" text="Change color"/>
+      <MFXToggleButton fx:id="toggleButton" automaticColorAdjustment="true" text="Automatic Colors"
+                       toggleColor="#006aff"/>
+      <MFXToggleButton id="customRippleRadius" automaticColorAdjustment="true" text="RippleRadiusCss"
+                       toggleColor="#0095c2"/>
+      <MFXToggleButton id="customRippleRadius" automaticColorAdjustment="true" disable="true" text="Disabled"
+                       toggleColor="#0095c2"/>
+   </HBox>
    <Label id="customLabel" alignment="CENTER" maxWidth="266.0" prefHeight="26.0" text="Toggle Nodes">
       <StackPane.margin>
-         <Insets top="20.0" />
+         <Insets bottom="45.0" />
       </StackPane.margin>
    </Label>
-   <MFXToggleNode textFill="#006aff">
-      <StackPane.margin>
-         <Insets right="170.0" top="130.0" />
-      </StackPane.margin>
-      <graphic>
-         <FontIcon iconColor="#5f9ff8" iconLiteral="fas-home" iconSize="30" />
-      </graphic>
-   </MFXToggleNode>
-   <MFXToggleNode>
-      <StackPane.margin>
-         <Insets top="130.0" />
-      </StackPane.margin>
-      <graphic>
-         <FontIcon iconColor="#e84747" iconLiteral="fas-heart" iconSize="30" />
-      </graphic>
-   </MFXToggleNode>
-   <MFXToggleNode>
+   <FlowPane alignment="TOP_CENTER" columnHalignment="CENTER" hgap="50.0" maxHeight="-Infinity" maxWidth="-Infinity"
+             prefHeight="200.0" prefWidth="400.0" vgap="50.0" StackPane.alignment="BOTTOM_CENTER">
       <StackPane.margin>
-         <Insets left="170.0" top="130.0" />
+         <Insets bottom="20.0" left="10.0" right="10.0"/>
       </StackPane.margin>
-      <graphic>
-         <FontIcon iconColor="#e8a54d" iconLiteral="fas-key" iconSize="30" />
-      </graphic>
-   </MFXToggleNode>
+      <MFXCircleToggleNode text="Hearth">
+         <graphic>
+            <FontIcon iconColor="RED" iconLiteral="fas-heart" iconSize="40" translateY="2.0"/>
+         </graphic>
+      </MFXCircleToggleNode>
+      <MFXCircleToggleNode text="Key">
+         <graphic>
+            <FontIcon iconColor="#f1c40f" iconLiteral="fas-key" iconSize="40"/>
+         </graphic>
+      </MFXCircleToggleNode>
+      <MFXCircleToggleNode text="Home">
+         <graphic>
+            <FontIcon iconColor="#49a6d7" iconLiteral="fas-home" iconSize="40"/>
+         </graphic>
+      </MFXCircleToggleNode>
+      <MFXRectangleToggleNode fx:id="rec1" labelTextGap="20.0" text="Hello">
+         <FlowPane.margin>
+            <Insets/>
+         </FlowPane.margin>
+      </MFXRectangleToggleNode>
+      <MFXRectangleToggleNode fx:id="rec2" labelTextGap="20.0" text="There">
+         <FlowPane.margin>
+            <Insets/>
+         </FlowPane.margin>
+      </MFXRectangleToggleNode>
+   </FlowPane>
 </StackPane>

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/beans/MFXContextMenuItem.java

@@ -54,7 +54,7 @@ public class MFXContextMenuItem {
     public MFXContextMenuItem(String text) {
         Label label = new Label(text);
         label.setMaxWidth(Double.MAX_VALUE);
-        node = new Label(text);
+        node = label;
     }
 
     /**

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/collections/ObservableStack.java

@@ -188,7 +188,7 @@ public class ObservableStack<E> extends SimpleListProperty<E> {
     }
 
     /**
-     * Used to determine what change occured in the stack
+     * Used to determine what change occurred in the stack
      */
     private enum ChangeType {
         PUSH, POP;

+ 194 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXCircleToggleNode.java

@@ -0,0 +1,194 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
+ *
+ *     MaterialFX is free software: you can redistribute it and/or modify
+ *     it under the terms of the GNU General Public License as published by
+ *     the Free Software Foundation, either version 3 of the License, or
+ *     (at your option) any later version.
+ *
+ *     MaterialFX is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *     GNU General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.controls;
+
+import io.github.palexdev.materialfx.MFXResourcesLoader;
+import io.github.palexdev.materialfx.controls.base.AbstractMFXToggleNode;
+import io.github.palexdev.materialfx.controls.enums.TextPosition;
+import io.github.palexdev.materialfx.skins.MFXCircleToggleNodeSkin;
+import javafx.css.*;
+import javafx.scene.Node;
+import javafx.scene.control.Skin;
+import javafx.scene.control.ToggleButton;
+
+import java.util.List;
+
+/**
+ * This is the implementation of a {@link ToggleButton} with a completely different skin, {@link MFXCircleToggleNodeSkin}.
+ * <p></p>
+ * Extends {@link ToggleButton} and redefines the style class to "mfx-toggle-node" for usage in CSS.
+ * <p>
+ * Allows to specify up to three icons: one icon for the toggle, and tho other two for the toggle's label.
+ */
+public class MFXCircleToggleNode extends AbstractMFXToggleNode {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private static final StyleablePropertyFactory<MFXCircleToggleNode> FACTORY = new StyleablePropertyFactory<>(AbstractMFXToggleNode.getControlCssMetaDataList());
+    private final String STYLESHEET = MFXResourcesLoader.load("css/mfx-circle-togglenode.css");
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXCircleToggleNode() {
+        this("");
+    }
+
+    public MFXCircleToggleNode(String text) {
+        this(text, null);
+    }
+
+    public MFXCircleToggleNode(String text, Node icon) {
+        super(text, icon);
+    }
+
+    public MFXCircleToggleNode(String text, Node icon, Node leadingIcon, Node trailingIcon) {
+        super(text, icon);
+        setLabelLeadingIcon(leadingIcon);
+        setLabelTrailingIcon(trailingIcon);
+    }
+
+    //================================================================================
+    // Styleable Properties
+    //================================================================================
+    private final StyleableDoubleProperty size = new SimpleStyleableDoubleProperty(
+            StyleableProperties.SIZE,
+            this,
+            "size",
+            32.0
+    );
+
+    private final StyleableObjectProperty<TextPosition> textPosition = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.TEXT_POSITION,
+            this,
+            "textPosition",
+            TextPosition.BOTTOM
+    );
+
+    private final StyleableDoubleProperty strokeWidth = new SimpleStyleableDoubleProperty(
+            StyleableProperties.STROKE_WIDTH,
+            this,
+            "strokeWidth",
+            1.5
+    );
+
+    public double getSize() {
+        return size.get();
+    }
+
+    /**
+     * Specifies the toggle's radius.
+     */
+    public StyleableDoubleProperty sizeProperty() {
+        return size;
+    }
+
+    public void setSize(double size) {
+        this.size.set(size);
+    }
+
+    public TextPosition getTextPosition() {
+        return textPosition.get();
+    }
+
+    /**
+     * Specifies the position of the label, above or underneath the toggle's circle.
+     */
+    public StyleableObjectProperty<TextPosition> textPositionProperty() {
+        return textPosition;
+    }
+
+    public void setTextPosition(TextPosition textPosition) {
+        this.textPosition.set(textPosition);
+    }
+
+    public double getStrokeWidth() {
+        return strokeWidth.get();
+    }
+
+    /**
+     * Specifies the stroke width of the toggle.
+     */
+    public StyleableDoubleProperty strokeWidthProperty() {
+        return strokeWidth;
+    }
+
+    public void setStrokeWidth(double strokeWidth) {
+        this.strokeWidth.set(strokeWidth);
+    }
+
+    //================================================================================
+    // CssMetaData
+    //================================================================================
+    private static class StyleableProperties {
+        private static final List<CssMetaData<? extends Styleable, ?>> cssMetaDataList;
+
+        private static final CssMetaData<MFXCircleToggleNode, Number> SIZE =
+                FACTORY.createSizeCssMetaData(
+                        "-mfx-size",
+                        MFXCircleToggleNode::sizeProperty,
+                        32.0
+                );
+
+        private static final CssMetaData<MFXCircleToggleNode, TextPosition> TEXT_POSITION =
+                FACTORY.createEnumCssMetaData(
+                        TextPosition.class,
+                        "-mfx-text-position",
+                        MFXCircleToggleNode::textPositionProperty,
+                        TextPosition.BOTTOM
+                );
+
+        private static final CssMetaData<MFXCircleToggleNode, Number> STROKE_WIDTH =
+                FACTORY.createSizeCssMetaData(
+                        "-mfx-stroke-width",
+                        MFXCircleToggleNode::strokeWidthProperty,
+                        1.5
+                );
+
+        static {
+            cssMetaDataList = List.of(
+                    SIZE, TEXT_POSITION,
+                    STROKE_WIDTH
+            );
+        }
+                
+    }
+
+    public static List<CssMetaData<? extends Styleable, ?>> getControlCssMetaDataList() {
+        return MFXCircleToggleNode.StyleableProperties.cssMetaDataList;
+    }
+
+    //================================================================================
+    // Override Methods
+    //================================================================================
+    @Override
+    protected Skin<?> createDefaultSkin() {
+        return new MFXCircleToggleNodeSkin(this);
+    }
+
+    @Override
+    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
+        return getControlCssMetaDataList();
+    }
+
+    @Override
+    public String getUserAgentStylesheet() {
+        return STYLESHEET;
+    }
+}

+ 12 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXContextMenu.java

@@ -76,6 +76,7 @@ public class MFXContextMenu extends VBox {
 
     public MFXContextMenu(double spacing) {
         super(spacing);
+        setMinWidth(100);
         setAlignment(Pos.TOP_CENTER);
         getStyleClass().setAll(STYLE_CLASS);
         popupControl = new PopupControl();
@@ -124,6 +125,15 @@ public class MFXContextMenu extends VBox {
      * Installs the context menu to the given node.
      */
     public void install(Node node) {
+        if (node.getScene() != null) {
+            Scene scene = node.getScene();
+            Window window = scene.getWindow();
+            scene.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> hide());
+            if (window != null) {
+                window.focusedProperty().addListener(windowFocusListener);
+            }
+        }
+
         node.sceneProperty().addListener((observable, oldValue, newValue) -> {
             if (newValue != null) {
                 newValue.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> hide());
@@ -133,6 +143,7 @@ public class MFXContextMenu extends VBox {
                 }
             }
         });
+
         Node nR = nodeReference != null ? nodeReference.get() : null;
         if (nR != null && nR == node) {
             return;
@@ -162,7 +173,7 @@ public class MFXContextMenu extends VBox {
      * If the node reference is not null and {@link #install(Node)} is called again, this method is called to
      * remove the context menu from the previous node.
      */
-    private void dispose() {
+    public void dispose() {
         if (nodeReference != null) {
             Node node = nodeReference.get();
             if (node != null) {

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXFilterComboBox.java

@@ -68,7 +68,7 @@ public class MFXFilterComboBox<T> extends MFXComboBox<T> {
 
     /**
      * Bound to the editor focus property. This allows to keep the focused style specified
-     * by css when the focus is acquired by the editor. The pseudo class to use in css is ":editor"
+     * by css when the focus is acquired by the editor. The PseudoClass to use in css is ":editor"
      */
     public BooleanProperty editorFocusedProperty() {
         return editorFocused;

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXLabel.java

@@ -200,7 +200,7 @@ public class MFXLabel extends Control {
 
     /**
      * Bound to the editor focus property. This allows to keep the focused style specified
-     * by css when the focus is acquired by the editor. The pseudo class to use in css is ":editor"
+     * by css when the focus is acquired by the editor. The PseudoClass to use in css is ":editor"
      */
     public BooleanProperty editorFocusedProperty() {
         return editorFocused;

+ 231 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXPasswordField.java

@@ -0,0 +1,231 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
+ *
+ *     MaterialFX is free software: you can redistribute it and/or modify
+ *     it under the terms of the GNU General Public License as published by
+ *     the Free Software Foundation, either version 3 of the License, or
+ *     (at your option) any later version.
+ *
+ *     MaterialFX is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *     GNU General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.controls;
+
+import io.github.palexdev.materialfx.MFXResourcesLoader;
+import io.github.palexdev.materialfx.font.MFXFontIcon;
+import io.github.palexdev.materialfx.skins.MFXPasswordFieldSkin;
+import io.github.palexdev.materialfx.utils.NodeUtils;
+import javafx.beans.binding.Bindings;
+import javafx.beans.property.*;
+import javafx.scene.control.Skin;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.paint.Color;
+
+/**
+ * This is my implementation of a password field, a TextField which masks the given input text.
+ * <p></p>
+ * Extends {@link MFXTextField}, defines a default icon which allows to show/hide the password and it's
+ * defined by the {@link #defaultIcon()} method so it can be changed after instantiation or by overriding the method.
+ * <p></p>
+ * Specific features:
+ * <p>
+ * <p> - Allows to change the "mask" character, event at runtime
+ * <p> - Allows to show/hide the password. When the password is hidden the text field {@code getText()} method will return
+ * the masked string so to get the password you must use {@link #getPassword()}. When the password is shown you can use both
+ * <p> - Allows to quickly position the caret with all four arrows
+ * <p> - Allows to selected all the text (Ctrl + A)
+ * <p> - Allows to copy the selected text (Ctrl + C), note that if the password is hidden it will copy the masked text
+ * <p> - Allows to cut the selected text (Ctrl + X), note that if the password is hidden it will cut the masked text
+ * <p> - Allows to paste the text in the clipboard to the field (Ctrl + V)
+ * <p> - Allows to enable/disable copy, cut and paste at any time
+ * <p> - Allows to delete the selected text with the shortcut (Ctrl + D)
+ * <p></p>
+ * Note: the context menu is redefined in the skin since some methods are private in the skin.
+ */
+public class MFXPasswordField extends MFXTextField {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private final String STYLE_CLASS = "mfx-password-field";
+    private final String STYLESHEET = MFXResourcesLoader.load("css/mfx-passwordfield.css");
+
+    private final ReadOnlyStringWrapper password = new ReadOnlyStringWrapper();
+    private final BooleanProperty showPassword = new SimpleBooleanProperty(false);
+    private final StringProperty hideCharacter = new SimpleStringProperty("\u25cf") {
+        @Override
+        public void set(String newValue) {
+            if (newValue.trim().isEmpty()) {
+                return;
+            }
+            if (newValue.length() > 1) {
+                super.set(newValue.substring(0, 1));
+            } else {
+                super.set(newValue);
+            }
+        }
+    };
+    private final BooleanProperty allowCopy = new SimpleBooleanProperty(true);
+    private final BooleanProperty allowCut = new SimpleBooleanProperty(true);
+    private final BooleanProperty allowPaste = new SimpleBooleanProperty(true);
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXPasswordField() {
+        this("");
+    }
+
+    public MFXPasswordField(String text) {
+        setText(text);
+        initialize();
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+    private void initialize() {
+        getStyleClass().add(STYLE_CLASS);
+        defaultIcon();
+    }
+
+    /**
+     * Installs the default password field icon
+     */
+    protected void defaultIcon() {
+        MFXFontIcon icon = new MFXFontIcon("mfx-eye", 16, Color.web("#4D4D4D"));
+        icon.descriptionProperty().bind(Bindings.createStringBinding(
+                () -> isShowPassword() ? "mfx-eye-slash" : "mfx-eye",
+                showPasswordProperty()
+        ));
+        MFXIconWrapper showPasswordIcon = new MFXIconWrapper(icon, 24).defaultRippleGeneratorBehavior();
+        NodeUtils.makeRegionCircular(showPasswordIcon);
+        showPasswordIcon.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
+            setShowPassword(!isShowPassword());
+            positionCaret(getText().length());
+            event.consume();
+        });
+
+        setIcon(showPasswordIcon);
+    }
+
+    /**
+     * Overridden, does nothing. The context menu is redefined in the skin
+     * as some methods are declared in the skin, are private and are needed to
+     * make the password field work correctly.
+     * <p></p>
+     * You can still change it or remove it anyway but keep in mind
+     * that functionalities like cut, paste and delete won't work by
+     * simply calling JavaFX methods.
+     */
+    @Override
+    protected void defaultContextMenu() {}
+
+    /**
+     * @return the un-masked text
+     */
+    public String getPassword() {
+        return password.get();
+    }
+
+    /**
+     * Specifies the un-masked text property.
+     */
+    public ReadOnlyStringWrapper passwordProperty() {
+        return password;
+    }
+
+    public String getHideCharacter() {
+        return hideCharacter.get();
+    }
+
+    /**
+     * Specifies the character used to mask the text.
+     */
+    public StringProperty hideCharacterProperty() {
+        return hideCharacter;
+    }
+
+    public void setHideCharacter(char hideCharacter) {
+        this.hideCharacter.set(String.valueOf(hideCharacter));
+    }
+
+    public boolean isShowPassword() {
+        return showPassword.get();
+    }
+
+    /**
+     * Specifies if the text should be un-masked to show the password.
+     */
+    public BooleanProperty showPasswordProperty() {
+        return showPassword;
+    }
+
+    public void setShowPassword(boolean showPassword) {
+        this.showPassword.set(showPassword);
+    }
+
+    public boolean isAllowCopy() {
+        return allowCopy.get();
+    }
+
+    /**
+     * Specifies if copying the password field text is allowed.
+     */
+    public BooleanProperty allowCopyProperty() {
+        return allowCopy;
+    }
+
+    public void setAllowCopy(boolean allowCopy) {
+        this.allowCopy.set(allowCopy);
+    }
+
+    public boolean isAllowCut() {
+        return allowCut.get();
+    }
+
+    /**
+     * Specifies if it's allowed to cut text from the password field.
+     */
+    public BooleanProperty allowCutProperty() {
+        return allowCut;
+    }
+
+    public void setAllowCut(boolean allowCut) {
+        this.allowCut.set(allowCut);
+    }
+
+    public boolean isAllowPaste() {
+        return allowPaste.get();
+    }
+
+    /**
+     * Specifies if it's allowed to paste text from the clipboard to the field.
+     */
+    public BooleanProperty allowPasteProperty() {
+        return allowPaste;
+    }
+
+    public void setAllowPaste(boolean allowPaste) {
+        this.allowPaste.set(allowPaste);
+    }
+
+    //================================================================================
+    // Override Methods
+    //================================================================================
+    @Override
+    protected Skin<?> createDefaultSkin() {
+        return new MFXPasswordFieldSkin(this, passwordProperty());
+    }
+
+    @Override
+    public String getUserAgentStylesheet() {
+        return STYLESHEET;
+    }
+}

+ 109 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXRectangleToggleNode.java

@@ -0,0 +1,109 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
+ *
+ *     MaterialFX is free software: you can redistribute it and/or modify
+ *     it under the terms of the GNU General Public License as published by
+ *     the Free Software Foundation, either version 3 of the License, or
+ *     (at your option) any later version.
+ *
+ *     MaterialFX is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *     GNU General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.controls;
+
+import io.github.palexdev.materialfx.MFXResourcesLoader;
+import io.github.palexdev.materialfx.controls.base.AbstractMFXToggleNode;
+import io.github.palexdev.materialfx.controls.factories.RippleClipTypeFactory;
+import io.github.palexdev.materialfx.effects.ripple.RippleClipType;
+import io.github.palexdev.materialfx.skins.MFXRectangleToggleNodeSkin;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.scene.Node;
+import javafx.scene.control.Skin;
+import javafx.scene.control.ToggleButton;
+
+/**
+ * This is the implementation of a {@link ToggleButton} with a completely different skin, {@link MFXRectangleToggleNodeSkin}.
+ * <p></p>
+ * Extends {@link ToggleButton} and redefines the style class to "mfx-toggle-node" for usage in CSS.
+ * <p>
+ * Allows to specify up to two icons for toggle's label.
+ */
+public class MFXRectangleToggleNode extends AbstractMFXToggleNode {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private final String STYLESHEET = MFXResourcesLoader.load("css/mfx-rectangle-togglenode.css");
+    private final ObjectProperty<RippleClipTypeFactory> rippleClipTypeFactory = new SimpleObjectProperty<>();
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXRectangleToggleNode() {
+        this("");
+    }
+
+    public MFXRectangleToggleNode(String text) {
+        this(text, null);
+    }
+
+    public MFXRectangleToggleNode(String text, Node leadingIcon) {
+        this(text, leadingIcon, null);
+    }
+
+    public MFXRectangleToggleNode(String text, Node leadingIcon, Node trailingIcon) {
+        super(text, null);
+        setLabelLeadingIcon(leadingIcon);
+        setLabelTrailingIcon(trailingIcon);
+        initialize();
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+    private void initialize() {
+        setMinSize(USE_PREF_SIZE, USE_PREF_SIZE);
+        setPrefSize(145, 45);
+        setRippleClipTypeFactory(
+                new RippleClipTypeFactory(RippleClipType.ROUNDED_RECTANGLE)
+                .setArcs(15)
+        );
+    }
+
+    public RippleClipTypeFactory getRippleClipTypeFactory() {
+        return rippleClipTypeFactory.get();
+    }
+
+    /**
+     * Specifies the ripple generator's clip factory.
+     * <p></p>
+     * If you change the borders' radius this property will most likely need to be changed.
+     */
+    public ObjectProperty<RippleClipTypeFactory> rippleClipTypeFactoryProperty() {
+        return rippleClipTypeFactory;
+    }
+
+    public void setRippleClipTypeFactory(RippleClipTypeFactory rippleClipTypeFactory) {
+        this.rippleClipTypeFactory.set(rippleClipTypeFactory);
+    }
+
+    //================================================================================
+    // Override Methods
+    //================================================================================
+    @Override
+    protected Skin<?> createDefaultSkin() {
+        return new MFXRectangleToggleNodeSkin(this);
+    }
+
+    @Override
+    public String getUserAgentStylesheet() {
+        return STYLESHEET;
+    }
+}

+ 12 - 12
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXStepperToggle.java

@@ -293,8 +293,8 @@ public class MFXStepperToggle extends Control implements Validated<MFXDialogVali
     //================================================================================
     // Styleable Properties
     //================================================================================
-    private final StyleableDoubleProperty textGap = new SimpleStyleableDoubleProperty(
-            StyleableProperties.TEXT_GAP,
+    private final StyleableDoubleProperty labelTextGap = new SimpleStyleableDoubleProperty(
+            StyleableProperties.LABEL_TEXT_GAP,
             this,
             "textGap",
             10.0
@@ -321,19 +321,19 @@ public class MFXStepperToggle extends Control implements Validated<MFXDialogVali
             2.5
     );
 
-    public double getTextGap() {
-        return textGap.get();
+    public double getLabelTextGap() {
+        return labelTextGap.get();
     }
 
     /**
      * Specifies the gap between the toggle's circle and the label.
      */
-    public StyleableDoubleProperty textGapProperty() {
-        return textGap;
+    public StyleableDoubleProperty labelTextGapProperty() {
+        return labelTextGap;
     }
 
-    public void setTextGap(double textGap) {
-        this.textGap.set(textGap);
+    public void setLabelTextGap(double labelTextGap) {
+        this.labelTextGap.set(labelTextGap);
     }
 
     public TextPosition getTextPosition() {
@@ -388,10 +388,10 @@ public class MFXStepperToggle extends Control implements Validated<MFXDialogVali
     private static class StyleableProperties {
         private static final List<CssMetaData<? extends Styleable, ?>> cssMetaDataList;
 
-        private static final CssMetaData<MFXStepperToggle, Number> TEXT_GAP =
+        private static final CssMetaData<MFXStepperToggle, Number> LABEL_TEXT_GAP =
                 FACTORY.createSizeCssMetaData(
-                        "-mfx-text-gap",
-                        MFXStepperToggle::textGapProperty,
+                        "-mfx-label-text-gap",
+                        MFXStepperToggle::labelTextGapProperty,
                         10.0
                 );
 
@@ -418,7 +418,7 @@ public class MFXStepperToggle extends Control implements Validated<MFXDialogVali
                 );
 
         static {
-            cssMetaDataList = List.of(TEXT_GAP, TEXT_POSITION, SIZE, STROKE_WIDTH);
+            cssMetaDataList = List.of(LABEL_TEXT_GAP, TEXT_POSITION, SIZE, STROKE_WIDTH);
         }
     }
 

+ 42 - 64
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXTableRow.java

@@ -1,78 +1,58 @@
-/*
- *     Copyright (C) 2021 Parisi Alessandro
- *     This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
- *
- *     MaterialFX is free software: you can redistribute it and/or modify
- *     it under the terms of the GNU General Public License as published by
- *     the Free Software Foundation, either version 3 of the License, or
- *     (at your option) any later version.
- *
- *     MaterialFX is distributed in the hope that it will be useful,
- *     but WITHOUT ANY WARRANTY; without even the implied warranty of
- *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *     GNU General Public License for more details.
- *
- *     You should have received a copy of the GNU General Public License
- *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
- */
-
 package io.github.palexdev.materialfx.controls;
 
 import io.github.palexdev.materialfx.MFXResourcesLoader;
+import io.github.palexdev.materialfx.controls.factories.RippleClipTypeFactory;
+import io.github.palexdev.materialfx.effects.ripple.MFXCircleRippleGenerator;
+import io.github.palexdev.materialfx.effects.ripple.RippleClipType;
+import io.github.palexdev.materialfx.effects.ripple.RipplePosition;
 import javafx.beans.property.BooleanProperty;
 import javafx.beans.property.SimpleBooleanProperty;
 import javafx.css.PseudoClass;
-import javafx.geometry.Pos;
 import javafx.scene.Node;
+import javafx.scene.input.MouseEvent;
 import javafx.scene.layout.HBox;
 
 /**
- * This is the implementation of the rows used in every {@link MFXTableView}.
- * <p>
- * This class contains the reference to the represented item and allows the selection of
- * the row and the item.
+ * This is the HBox that contains the table row cells built by each column.
+ * <p></p>
+ * This little class is needed to select rows.
  */
 public class MFXTableRow<T> extends HBox {
     //================================================================================
     // Properties
     //================================================================================
     private final String STYLE_CLASS = "mfx-table-row";
-    private final String STYLESHEET = MFXResourcesLoader.load("css/mfx-table-row.css");
-    private final T item;
+    private final String STYLESHEET = MFXResourcesLoader.load("css/mfx-tablerow.css");
 
-    private static final PseudoClass SELECTED_PSEUDO_CLASS = PseudoClass.getPseudoClass("selected");
+    protected static final PseudoClass SELECTED_PSEUDO_CLASS = PseudoClass.getPseudoClass("selected");
     private final BooleanProperty selected = new SimpleBooleanProperty(false);
 
+    private final T data;
+    private final MFXCircleRippleGenerator rippleGenerator = new MFXCircleRippleGenerator(this);
+
     //================================================================================
     // Constructors
     //================================================================================
-    public MFXTableRow(T item) {
-        this.item = item;
+    public MFXTableRow(T data) {
+        this.data = data;
         initialize();
     }
 
-    public MFXTableRow(double spacing, T item) {
+    public MFXTableRow(T data, double spacing) {
         super(spacing);
-        this.item = item;
+        this.data = data;
         initialize();
     }
 
-    public MFXTableRow(double spacing, Pos alignment, T item) {
-        super(spacing);
-        this.item = item;
-        setAlignment(alignment);
-        initialize();
-    }
-
-    public MFXTableRow(T item, Node... children) {
+    public MFXTableRow(T data, Node... children) {
         super(children);
-        this.item = item;
+        this.data = data;
         initialize();
     }
 
-    public MFXTableRow(double spacing, T item, Node... children) {
+    public MFXTableRow(T data, double spacing, Node... children) {
         super(spacing, children);
-        this.item = item;
+        this.data = data;
         initialize();
     }
 
@@ -80,22 +60,37 @@ public class MFXTableRow<T> extends HBox {
     // Methods
     //================================================================================
     private void initialize() {
-        getStyleClass().add(STYLE_CLASS);
-        addListeners();
+        getStyleClass().setAll(STYLE_CLASS);
+        selected.addListener(invalidated -> pseudoClassStateChanged(SELECTED_PSEUDO_CLASS, isSelected()));
+        setupRippleGenerator();
     }
 
-    private void addListeners() {
-        selected.addListener(invalidate -> pseudoClassStateChanged(SELECTED_PSEUDO_CLASS, selected.get()));
+    protected void setupRippleGenerator() {
+        getChildren().add(0, rippleGenerator);
+        rippleGenerator.setAnimateBackground(false);
+        rippleGenerator.setClipSupplier(() -> new RippleClipTypeFactory(RippleClipType.RECTANGLE).setOffsetW(10).build(this));
+        rippleGenerator.setComputeRadiusMultiplier(true);
+        rippleGenerator.setManaged(false);
+        rippleGenerator.setRipplePositionFunction(event -> new RipplePosition(event.getX(), event.getY()));
+        rippleGenerator.setTranslateX(-5);
+        rippleGenerator.rippleRadiusProperty().bind(widthProperty().divide(2.0));
+        addEventFilter(MouseEvent.MOUSE_PRESSED, rippleGenerator::generateRipple);
     }
 
-    public T getItem() {
-        return item;
+    /**
+     * @return the data represented by this row (by its cells to be more precise)
+     */
+    public T getData() {
+        return data;
     }
 
     public boolean isSelected() {
         return selected.get();
     }
 
+    /**
+     * Specifies the selection state of the row.
+     */
     public BooleanProperty selectedProperty() {
         return selected;
     }
@@ -111,21 +106,4 @@ public class MFXTableRow<T> extends HBox {
     public String getUserAgentStylesheet() {
         return STYLESHEET;
     }
-
-    @Override
-    public String toString() {
-        String className = getClass().getName();
-        String simpleName = className.substring(className.lastIndexOf('.') + 1);
-        StringBuilder sb = new StringBuilder();
-        sb.append("[").append(simpleName);
-        sb.append('@');
-        sb.append(Integer.toHexString(hashCode()));
-        sb.append("]");
-        sb.append("[Data:").append(getItem()).append("]");
-        if (getId() != null) {
-            sb.append("[id:").append(getId()).append("]");
-        }
-
-        return sb.toString();
-    }
 }

+ 102 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXTableSortModel.java

@@ -0,0 +1,102 @@
+package io.github.palexdev.materialfx.controls;
+
+import io.github.palexdev.materialfx.controls.cell.MFXTableColumn;
+import io.github.palexdev.materialfx.controls.enums.SortState;
+import javafx.beans.property.ReadOnlyObjectProperty;
+import javafx.beans.property.ReadOnlyObjectWrapper;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import javafx.util.Pair;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * This helper class makes managing {@link MFXTableView} sort state easier.
+ * <p>
+ * Keeps track of the current sorted column and its sort state using a read only object
+ * property, {@link #sortedColumnProperty()}, and JavaFX {@link Pair} class.
+ * <p></p>
+ * Why Pair? Because this class needs to inform the table view of changes even when the sorted column is the same.
+ * The JavaFX change listeners fire the change event only when the value is different.
+ * So when I tried to use this {@code ReadOnlyObjectWrapper<TableColumn<T>>}, the table would not update if the
+ * sort column was the same (but with a different sort state of course). There are different solutions to this issue,
+ * for example I could have modified/expanded the JavaFX property class but having this helper class is better as it adds
+ * other features as well.
+ * <p>
+ * So to have the needed behavior every time a column changes its state {@link #sortBy(MFXTableColumn, SortState)} is called.
+ * This method simply sets the {@link #sortedColumnProperty()} to a new {@link Pair} with the specified arguments, and since it is
+ * a new instance JavaFX will fire the change event even if the column is the same.
+ * <p></p>
+ * This mechanism also allows the user to sort the table manually, since you can retrieve the columns with {@link MFXTableView#getTableColumns()}.
+ * <p></p>
+ * It also does two checks: one is done by the {@link #init()} method, the other one is done everytime a change in the columns list occurs.
+ * Both those checks call {@link #useLast()}
+ */
+public class MFXTableSortModel<T> {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private boolean init = false;
+    private final ObservableList<MFXTableColumn<T>> tableColumns;
+    private final ReadOnlyObjectWrapper<Pair<MFXTableColumn<T>, SortState>> sortedColumn = new ReadOnlyObjectWrapper<>(new Pair<>(null, null));
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXTableSortModel(ObservableList<MFXTableColumn<T>> tableColumns) {
+        this.tableColumns = tableColumns;
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+    public void init() {
+        if (init) {
+            return;
+        }
+
+        tableColumns.addListener((ListChangeListener<? super MFXTableColumn<T>>) change -> useLast());
+        useLast();
+        init = true;
+    }
+
+    /**
+     * Sets the {@link #sortedColumnProperty()} with a new {@link Pair} with the specified
+     * column and sort state.
+     */
+    public void sortBy(MFXTableColumn<T> column, SortState sortState) {
+        if (!tableColumns.contains(column)) {
+            throw new IllegalArgumentException("The specified column is not present in the TableView's columns list!!");
+        }
+
+        sortedColumn.set(new Pair<>(column, sortState));
+    }
+
+    /**
+     * Collects all the table columns whose sort state is not UNSORTED in a temporary list.
+     * The last column is the column chosen for {@link #sortBy(MFXTableColumn, SortState)}, the other ones
+     * are reset to UNSORTED.
+     */
+    private void useLast() {
+        List<MFXTableColumn<T>> sorted = tableColumns.stream()
+                .filter(column -> column.getSortState() != SortState.UNSORTED)
+                .collect(Collectors.toList());
+        if (!sorted.isEmpty()) {
+            MFXTableColumn<T> last = sorted.remove(sorted.size() - 1);
+            sorted.forEach(column -> column.setSortState(SortState.UNSORTED));
+            sortBy(last, last.getSortState());
+        }
+    }
+
+    public Pair<MFXTableColumn<T>, SortState> getSortedColumn() {
+        return sortedColumn.get();
+    }
+
+    /**
+     * Specifies the current sorted column and its sort state.
+     */
+    public ReadOnlyObjectProperty<Pair<MFXTableColumn<T>, SortState>> sortedColumnProperty() {
+        return sortedColumn.getReadOnlyProperty();
+    }
+}

+ 244 - 58
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXTableView.java

@@ -1,35 +1,31 @@
-/*
- *     Copyright (C) 2021 Parisi Alessandro
- *     This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
- *
- *     MaterialFX is free software: you can redistribute it and/or modify
- *     it under the terms of the GNU General Public License as published by
- *     the Free Software Foundation, either version 3 of the License, or
- *     (at your option) any later version.
- *
- *     MaterialFX is distributed in the hope that it will be useful,
- *     but WITHOUT ANY WARRANTY; without even the implied warranty of
- *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *     GNU General Public License for more details.
- *
- *     You should have received a copy of the GNU General Public License
- *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
- */
-
 package io.github.palexdev.materialfx.controls;
 
 import io.github.palexdev.materialfx.MFXResourcesLoader;
-import io.github.palexdev.materialfx.controls.cell.MFXTableColumnCell;
+import io.github.palexdev.materialfx.controls.cell.MFXTableColumn;
 import io.github.palexdev.materialfx.selection.TableSelectionModel;
 import io.github.palexdev.materialfx.selection.base.ITableSelectionModel;
 import io.github.palexdev.materialfx.skins.MFXTableViewSkin;
+import javafx.beans.binding.Bindings;
 import javafx.beans.property.*;
 import javafx.collections.FXCollections;
+import javafx.collections.ListChangeListener;
 import javafx.collections.ObservableList;
+import javafx.collections.ObservableMap;
 import javafx.event.Event;
 import javafx.event.EventType;
+import javafx.geometry.Insets;
+import javafx.scene.Node;
 import javafx.scene.control.Control;
+import javafx.scene.control.Label;
 import javafx.scene.control.Skin;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.VBox;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+import java.util.stream.IntStream;
 
 /**
  * This is the implementation of a table view following Google's material design guidelines in JavaFX.
@@ -37,6 +33,7 @@ import javafx.scene.control.Skin;
  * Extends {@code Control} and provides a new skin since it is built from scratch.
  *
  * @param <T> The type of the data within the table.
+ * @see MFXTableViewSkin
  */
 public class MFXTableView<T> extends Control {
     //================================================================================
@@ -45,26 +42,66 @@ public class MFXTableView<T> extends Control {
     private final String STYLE_CLASS = "mfx-table-view";
     private final String STYLESHEET = MFXResourcesLoader.load("css/mfx-tableview.css");
 
-    private final ObservableList<T> items = FXCollections.observableArrayList();
-    private final ObjectProperty<ITableSelectionModel<T>> selectionModel = new SimpleObjectProperty<>(null);
+    private final ObjectProperty<ObservableList<T>> items = new SimpleObjectProperty<>(FXCollections.observableArrayList());
+    private final ObjectProperty<ITableSelectionModel<T>> selectionModel = new SimpleObjectProperty<>();
+    private final ObservableList<MFXTableColumn<T>> tableColumns = FXCollections.observableArrayList();
+    private final MFXTableSortModel<T> sortModel;
+
+    private final ObjectProperty<Supplier<Region>> headerSupplier = new SimpleObjectProperty<>();
+    private final DoubleProperty headerHeight = new SimpleDoubleProperty(48);
+    private final StringProperty headerText = new SimpleStringProperty("");
+    private final ObjectProperty<Node> headerIcon = new SimpleObjectProperty<>();
 
-    private final ObservableList<MFXTableColumnCell<T>> columns = FXCollections.observableArrayList();
-    private final IntegerProperty maxRows = new SimpleIntegerProperty(10);
-    private final IntegerProperty maxRowsCombo = new SimpleIntegerProperty(20);
-    private final DoubleProperty fixedRowsHeight = new SimpleDoubleProperty(27);
+    private final DoubleProperty fixedRowsHeight = new SimpleDoubleProperty(30);
+    private final IntegerProperty maxRowsPerPage = new SimpleIntegerProperty(20);
+
+    private final ListChangeListener<? super T> changeListener;
 
     //================================================================================
     // Constructors
     //================================================================================
     public MFXTableView() {
-        installSelectionModel();
-        initialize();
+        this(FXCollections.observableArrayList());
     }
 
-    public MFXTableView(double fixedRowsHeight) {
-        installSelectionModel();
+    public MFXTableView(ObservableList<T> items) {
+        setItems(items);
 
-        setFixedRowsHeight(fixedRowsHeight);
+        sortModel = new MFXTableSortModel<>(tableColumns);
+
+        changeListener = change -> {
+            if (getSelectionModel().getSelectedItems().isEmpty()) {
+                return;
+            }
+            if (change.getList().isEmpty()) {
+                getSelectionModel().clearSelection();
+                return;
+            }
+
+            getSelectionModel().setUpdating(true);
+            Map<Integer, Integer> addedOffsets = new HashMap<>();
+            Map<Integer, Integer> removedOffsets = new HashMap<>();
+
+            while (change.next()) {
+                if (change.wasAdded()) {
+                    int from = change.getFrom();
+                    int to = change.getTo();
+                    int offset = to - from;
+                    addedOffsets.put(from, offset);
+                }
+                if (change.wasRemoved()) {
+                    int from = change.getFrom();
+                    int offset = change.getRemovedSize();
+                    IntStream.range(from, from + offset)
+                            .filter(getSelectionModel()::containSelected)
+                            .forEach(getSelectionModel()::clearSelectedItem);
+                    removedOffsets.put(from, offset);
+                }
+            }
+            updateSelection(addedOffsets, removedOffsets);
+        };
+
+        getItems().addListener(changeListener);
         initialize();
     }
 
@@ -72,24 +109,89 @@ public class MFXTableView<T> extends Control {
     // Methods
     //================================================================================
     private void initialize() {
-        getStyleClass().add(STYLE_CLASS);
+        getStyleClass().setAll(STYLE_CLASS);
+        defaultHeaderSupplier();
+        defaultSelectionModel();
+
+        items.addListener((observable, oldValue, newValue) -> {
+            getSelectionModel().clearSelection();
+            if (oldValue != null) {
+                oldValue.removeListener(changeListener);
+            }
+            if (newValue != null) {
+                newValue.addListener(changeListener);
+            }
+        });
     }
 
     /**
      * Installs the default selection model in this table view.
+     *
+     * @see #selectionModelProperty()
      */
-    protected void installSelectionModel() {
-        ITableSelectionModel<T> selectionModel = new TableSelectionModel<>();
+    protected void defaultSelectionModel() {
+        TableSelectionModel<T> selectionModel = new TableSelectionModel<>();
         selectionModel.setAllowsMultipleSelection(true);
         setSelectionModel(selectionModel);
     }
 
+    /**
+     * Installs the default header supplier in this table view.
+     *
+     * @see #headerSupplierProperty()
+     */
+    protected void defaultHeaderSupplier() {
+        setHeaderSupplier(() -> {
+            Label header = new Label();
+            header.getStyleClass().add("header");
+            header.setMinHeight(Region.USE_PREF_SIZE);
+            header.prefHeightProperty().bind(Bindings.createDoubleBinding(
+                    () -> getHeaderText().isEmpty() ? 0 : getHeaderHeight(),
+                    headerText, headerHeight
+            ));
+            header.setMaxWidth(Double.MAX_VALUE);
+            header.textProperty().bind(headerText);
+            header.graphicProperty().bind(headerIcon);
+            VBox.setMargin(header, new Insets(5, 10, 0, 10));
+
+            return header;
+        });
+    }
+
+    protected void updateSelection(Map<Integer, Integer> addedOffsets, Map<Integer, Integer> removedOffsets) {
+        MapProperty<Integer, T> selectedItems = getSelectionModel().selectedItemsProperty();
+        ObservableMap<Integer, T> updatedMap = FXCollections.observableHashMap();
+        selectedItems.forEach((key, value) -> {
+            int sum = addedOffsets.entrySet().stream()
+                    .filter(entry -> entry.getKey() <= key)
+                    .mapToInt(Map.Entry::getValue)
+                    .sum();
+            int diff = removedOffsets.entrySet().stream()
+                    .filter(entry -> entry.getKey() < key)
+                    .mapToInt(Map.Entry::getValue)
+                    .sum();
+            int shift = sum - diff;
+            updatedMap.put(key + shift, value);
+        });
+        if (!selectedItems.equals(updatedMap)) {
+            selectedItems.set(updatedMap);
+        }
+        getSelectionModel().setUpdating(false);
+    }
+
     public ObservableList<T> getItems() {
+        return items.get();
+    }
+
+    /**
+     * Specifies the items observable list for the table.
+     */
+    public ObjectProperty<ObservableList<T>> itemsProperty() {
         return items;
     }
 
     public void setItems(ObservableList<T> items) {
-        this.items.setAll(items);
+        this.items.set(items);
     }
 
     public ITableSelectionModel<T> getSelectionModel() {
@@ -97,7 +199,7 @@ public class MFXTableView<T> extends Control {
     }
 
     /**
-     * Specifies the selection model used by the control.
+     * Specifies the selection model to be used.
      */
     public ObjectProperty<ITableSelectionModel<T>> selectionModelProperty() {
         return selectionModel;
@@ -107,34 +209,90 @@ public class MFXTableView<T> extends Control {
         this.selectionModel.set(selectionModel);
     }
 
-    public ObservableList<MFXTableColumnCell<T>> getColumns() {
-        return columns;
+    /**
+     * @return the table columns observable list
+     */
+    public ObservableList<MFXTableColumn<T>> getTableColumns() {
+        return tableColumns;
+    }
+
+    /**
+     * Replaces the table columns with the given list.
+     */
+    public void setTableColumns(List<MFXTableColumn<T>> columns) {
+        tableColumns.setAll(columns);
+    }
+
+    /**
+     * @return this table sort model instance.
+     */
+    public MFXTableSortModel<T> getSortModel() {
+        return sortModel;
+    }
+
+    public Supplier<Region> getHeaderSupplier() {
+        return headerSupplier.get();
+    }
+
+    /**
+     * Specifies the supplier used in the table skin to build the column header region.
+     * <p>
+     * The default supplier makes use of the following properties as well:
+     * <p> - {@link #headerHeightProperty()}
+     * <p> - {@link #headerTextProperty()}
+     * <p> - {@link #headerIconProperty()}
+     */
+    public ObjectProperty<Supplier<Region>> headerSupplierProperty() {
+        return headerSupplier;
+    }
+
+    public void setHeaderSupplier(Supplier<Region> headerSupplier) {
+        this.headerSupplier.set(headerSupplier);
     }
 
-    public int getMaxRows() {
-        return maxRows.get();
+    public double getHeaderHeight() {
+        return headerHeight.get();
     }
 
     /**
-     * Specifies the max rows per page.
+     * Specifies the header height.
      */
-    public IntegerProperty maxRowsProperty() {
-        return maxRows;
+    public DoubleProperty headerHeightProperty() {
+        return headerHeight;
+    }
+
+    public void setHeaderHeight(double headerHeight) {
+        this.headerHeight.set(headerHeight);
     }
 
-    public int getMaxRowsCombo() {
-        return maxRowsCombo.get();
+    public String getHeaderText() {
+        return headerText.get();
     }
 
     /**
-     * Specifies the max value in the combo box.
+     * Specifies the header text.
      */
-    public IntegerProperty maxRowsComboProperty() {
-        return maxRowsCombo;
+    public StringProperty headerTextProperty() {
+        return headerText;
+    }
+
+    public void setHeaderText(String headerText) {
+        this.headerText.set(headerText);
     }
 
-    public void setMaxRowsCombo(int maxRowsCombo) {
-        this.maxRowsCombo.set(maxRowsCombo);
+    public Node getHeaderIcon() {
+        return headerIcon.get();
+    }
+
+    /**
+     * Specifies the header icon.
+     */
+    public ObjectProperty<Node> headerIconProperty() {
+        return headerIcon;
+    }
+
+    public void setHeaderIcon(Node headerIcon) {
+        this.headerIcon.set(headerIcon);
     }
 
     public double getFixedRowsHeight() {
@@ -142,7 +300,7 @@ public class MFXTableView<T> extends Control {
     }
 
     /**
-     * Specifies the max height of all rows in the table.
+     * Specifies the height of the rows.
      */
     public DoubleProperty fixedRowsHeightProperty() {
         return fixedRowsHeight;
@@ -152,6 +310,26 @@ public class MFXTableView<T> extends Control {
         this.fixedRowsHeight.set(fixedRowsHeight);
     }
 
+    public int getMaxRowsPerPage() {
+        return maxRowsPerPage.get();
+    }
+
+    /**
+     * Specifies the max number of rows that can be shown in a single page.
+     * This value is used by the table combo box.
+     * <p></p>
+     * <b>
+     * N.B: Values must be multiples of 5/10 otherwise the navigation system will break.
+     * </b>
+     */
+    public IntegerProperty maxRowsPerPageProperty() {
+        return maxRowsPerPage;
+    }
+
+    public void setMaxRowsPerPage(int maxRowsPerPage) {
+        this.maxRowsPerPage.set(maxRowsPerPage);
+    }
+
     //================================================================================
     // Override Methods
     //================================================================================
@@ -172,18 +350,26 @@ public class MFXTableView<T> extends Control {
     /**
      * Events class for the table view.
      * <p>
-     * Defines a new EventType:
-     * <p>
-     * - FORCE_UPDATE_EVENT: this event is captures by the table view's skin to force an update
-     * of the rows. This is useful when the model is not based on JavaFX's properties because when
-     * some item changes the data is not updated automatically, so it must be done manually.
+     * Defines a new EventTypes:
      * <p>
+     * - FORCE_UPDATE_EVENT: used to manually update the table <p></p>
      */
-    public static class TableViewEvent extends Event {
-        public static final EventType<TableViewEvent> FORCE_UPDATE_EVENT = new EventType<>(ANY, "FORCE_UPDATE_EVENT");
+    public static class MFXTableViewEvent extends Event {
 
-        public TableViewEvent(EventType<? extends Event> eventType) {
+        public static EventType<MFXTableViewEvent> FORCE_UPDATE_EVENT = new EventType<>(ANY, "FORCE_UPDATE_EVENT");
+
+        public MFXTableViewEvent(EventType<? extends Event> eventType) {
             super(eventType);
         }
     }
+
+    /**
+     * Forces the table to update
+     * <p>
+     * This is especially useful when the model is not built with
+     * JavaFX properties so when it changes the table must be updated manually
+     */
+    public void updateTable() {
+        Event.fireEvent(this, new MFXTableViewEvent(MFXTableViewEvent.FORCE_UPDATE_EVENT));
+    }
 }

+ 123 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXTextField.java

@@ -19,15 +19,22 @@
 package io.github.palexdev.materialfx.controls;
 
 import io.github.palexdev.materialfx.MFXResourcesLoader;
+import io.github.palexdev.materialfx.beans.MFXContextMenuItem;
 import io.github.palexdev.materialfx.controls.enums.DialogType;
 import io.github.palexdev.materialfx.skins.MFXTextFieldSkin;
 import io.github.palexdev.materialfx.validation.MFXDialogValidator;
 import io.github.palexdev.materialfx.validation.base.AbstractMFXValidator;
 import io.github.palexdev.materialfx.validation.base.Validated;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
 import javafx.beans.property.StringProperty;
 import javafx.css.*;
+import javafx.event.Event;
+import javafx.geometry.Insets;
+import javafx.scene.Node;
 import javafx.scene.control.Skin;
 import javafx.scene.control.TextField;
+import javafx.scene.input.ContextMenuEvent;
 import javafx.scene.paint.Color;
 import javafx.scene.paint.Paint;
 import javafx.scene.shape.StrokeLineCap;
@@ -40,6 +47,9 @@ import java.util.function.Supplier;
  * <p></p>
  * Extends {@code TextField}, redefines the style class to "mfx-text-field" for usage in CSS and
  * includes a {@code MFXDialogValidator} for input validation.
+ * <p>
+ * Also includes new features: you can now add an icon to the text field and adjust its position,
+ * replaces the default JavaFX context menu in favor of {@link MFXContextMenu}.
  * <p></p>
  * Defines a new PseudoClass: ":invalid" to specify the control's look when the validator's state is invalid.
  */
@@ -51,6 +61,11 @@ public class MFXTextField extends TextField implements Validated<MFXDialogValida
     private final String STYLE_CLASS = "mfx-text-field";
     private final String STYLESHEET = MFXResourcesLoader.load("css/mfx-textfield.css");
 
+    private final ObjectProperty<Node> icon = new SimpleObjectProperty<>();
+    private final ObjectProperty<Insets> iconInsets = new SimpleObjectProperty<>(new Insets(0, 0, 0, 9));
+
+    private final ObjectProperty<MFXContextMenu> mfxContextMenu = new SimpleObjectProperty<>();
+
     private MFXDialogValidator validator;
     protected static final PseudoClass INVALID_PSEUDO_CLASS = PseudoClass.getPseudoClass("invalid");
 
@@ -80,7 +95,7 @@ public class MFXTextField extends TextField implements Validated<MFXDialogValida
      * <p></p>
      * Then the label visible property is automatically updated when the validator state changes.
      * <p></p>
-     * The validator is also responsible for updating the ":invalid" pseudo class.
+     * The validator is also responsible for updating the ":invalid" PseudoClass.
      */
     private void setupValidator() {
         validator = new MFXDialogValidator("Error");
@@ -145,6 +160,20 @@ public class MFXTextField extends TextField implements Validated<MFXDialogValida
         getStyleClass().add(STYLE_CLASS);
         setupValidator();
 
+        addListeners();
+        defaultContextMenu();
+    }
+
+    private void addListeners() {
+        mfxContextMenu.addListener((observable, oldValue, newValue) -> {
+            if (oldValue != null) {
+                oldValue.dispose();
+            }
+            if (newValue != null) {
+                newValue.install(this);
+            }
+        });
+
         textProperty().addListener((observable, oldValue, newValue) -> {
             int limit = getTextLimit();
             if (limit == -1) {
@@ -156,6 +185,99 @@ public class MFXTextField extends TextField implements Validated<MFXDialogValida
                 setText(s);
             }
         });
+        addEventFilter(ContextMenuEvent.CONTEXT_MENU_REQUESTED, Event::consume);
+    }
+
+    /**
+     * Installs the default {@link MFXContextMenu}.
+     */
+    protected void defaultContextMenu() {
+        MFXContextMenuItem copy = new MFXContextMenuItem(
+                "Copy",
+                event -> copy()
+        );
+
+        MFXContextMenuItem cut = new MFXContextMenuItem(
+                "Cut",
+                event -> cut()
+        );
+
+        MFXContextMenuItem paste = new MFXContextMenuItem(
+                "Paste",
+                event -> paste()
+        );
+
+        MFXContextMenuItem delete = new MFXContextMenuItem(
+                "Delete",
+                event -> deleteText(getSelection())
+        );
+
+        MFXContextMenuItem selectAll = new MFXContextMenuItem(
+                "Select All",
+                event -> selectAll()
+        );
+
+        setMFXContextMenu(
+                new MFXContextMenu.Builder()
+                .addMenuItem(copy)
+                .addMenuItem(cut)
+                .addMenuItem(paste)
+                .addMenuItem(delete)
+                .addSeparator()
+                .addMenuItem(selectAll)
+                .get()
+        );
+    }
+
+    public Node getIcon() {
+        return icon.get();
+    }
+
+    /**
+     * Specifies the field's icon.
+     */
+    public ObjectProperty<Node> iconProperty() {
+        return icon;
+    }
+
+    public void setIcon(Node icon) {
+        this.icon.set(icon);
+    }
+
+    public Insets getIconInsets() {
+        return iconInsets.get();
+    }
+
+    /**
+     * Allows to adjust the icon's position without changing the skin.
+     * <p></p>
+     * Positive Bottom and Top insets adjust the Y position (up/down respectively).
+     * <p>
+     * Positive Right and Left insets adjust the X position (left/right respectively).
+     *
+     * @see #iconProperty()
+     */
+    public ObjectProperty<Insets> iconInsetsProperty() {
+        return iconInsets;
+    }
+
+    public void setIconInsets(Insets iconInsets) {
+        this.iconInsets.set(iconInsets);
+    }
+
+    public MFXContextMenu getMFXContextMenu() {
+        return mfxContextMenu.get();
+    }
+
+    /**
+     * Specifies the field's {@link MFXContextMenu}.
+     */
+    public ObjectProperty<MFXContextMenu> mfxContextMenuProperty() {
+        return mfxContextMenu;
+    }
+
+    public void setMFXContextMenu(MFXContextMenu mfxContextMenu) {
+        this.mfxContextMenu.set(mfxContextMenu);
     }
 
     //================================================================================

+ 0 - 309
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXToggleNode.java

@@ -1,309 +0,0 @@
-/*
- *     Copyright (C) 2021 Parisi Alessandro
- *     This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
- *
- *     MaterialFX is free software: you can redistribute it and/or modify
- *     it under the terms of the GNU General Public License as published by
- *     the Free Software Foundation, either version 3 of the License, or
- *     (at your option) any later version.
- *
- *     MaterialFX is distributed in the hope that it will be useful,
- *     but WITHOUT ANY WARRANTY; without even the implied warranty of
- *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *     GNU General Public License for more details.
- *
- *     You should have received a copy of the GNU General Public License
- *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package io.github.palexdev.materialfx.controls;
-
-import io.github.palexdev.materialfx.MFXResourcesLoader;
-import io.github.palexdev.materialfx.controls.enums.ToggleNodeShape;
-import io.github.palexdev.materialfx.effects.ripple.RippleGenerator;
-import io.github.palexdev.materialfx.utils.NodeUtils;
-import javafx.css.*;
-import javafx.geometry.Insets;
-import javafx.scene.Node;
-import javafx.scene.control.Skin;
-import javafx.scene.control.ToggleButton;
-import javafx.scene.control.skin.ToggleButtonSkin;
-import javafx.scene.input.MouseEvent;
-import javafx.scene.layout.Background;
-import javafx.scene.layout.BackgroundFill;
-import javafx.scene.layout.CornerRadii;
-import javafx.scene.layout.Region;
-import javafx.scene.paint.Color;
-import javafx.scene.paint.Paint;
-import javafx.util.Duration;
-
-import java.util.List;
-
-/**
- * This control is basically a {@code ToggleButton} but it is mostly used to contain graphic rather than text.
- * It's also possible to make it appear circular for a modern like design.
- * <p>
- * Extends {@code ToggleButton}, redefines the style class to "mfx-toggle-node" for usage in CSS and
- * includes a {@code RippleGenerator} to generate ripple effects on click.
- */
-public class MFXToggleNode extends ToggleButton {
-    //================================================================================
-    // Properties
-    //================================================================================
-    private static final StyleablePropertyFactory<MFXToggleNode> FACTORY = new StyleablePropertyFactory<>(ToggleButton.getClassCssMetaData());
-    private final String STYLE_CLASS = "mfx-toggle-node";
-    private final String STYLESHEET = MFXResourcesLoader.load("css/mfx-togglenode.css");
-    protected final RippleGenerator rippleGenerator = new RippleGenerator(this);
-
-    //================================================================================
-    // Constructors
-    //================================================================================
-    public MFXToggleNode() {
-        setText("");
-        initialize();
-    }
-
-    public MFXToggleNode(String text) {
-        super(text);
-        initialize();
-    }
-
-    public MFXToggleNode(Node graphic) {
-        super("", graphic);
-        initialize();
-    }
-
-    public MFXToggleNode(String text, Node graphic) {
-        super(text, graphic);
-        initialize();
-    }
-
-    //================================================================================
-    // Methods
-    //================================================================================
-    private void initialize() {
-        getStyleClass().add(STYLE_CLASS);
-        setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
-        setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
-
-        setupRippleGenerator();
-
-        prefWidthProperty().bind(size);
-        prefHeightProperty().bind(size);
-        clip();
-
-        addListeners();
-        setSize(40);
-    }
-
-    /**
-     * Adds listener for toggleShapeProperty, sizeProperty and selectedProperty
-     */
-    private void addListeners() {
-        toggleShapeProperty().addListener((observable, oldValue, newValue) -> {
-            if (newValue.equals(ToggleNodeShape.CIRCLE)) {
-                prefWidthProperty().bind(size);
-                prefHeightProperty().bind(size);
-                clip();
-            } else {
-                setClip(null);
-                prefWidthProperty().bind(size.multiply(3.5));
-                prefHeightProperty().bind(size);
-            }
-        });
-
-        sizeProperty().addListener((sObservable, sOldValue, sNewValue) -> {
-            if (sNewValue.doubleValue() != sOldValue.doubleValue()) {
-                setSize(sNewValue.doubleValue());
-            }
-        });
-
-        selectedProperty().addListener((observable, oldValue, newValue) -> {
-            if (newValue) {
-                setBackground(new Background(new BackgroundFill(selectedColor.get(), CornerRadii.EMPTY, Insets.EMPTY)));
-            } else {
-                setBackground(new Background(new BackgroundFill(unSelectedColor.get(), CornerRadii.EMPTY, Insets.EMPTY)));
-            }
-        });
-    }
-
-    /**
-     * Resets the clip
-     */
-    private void clip() {
-        setClip(null);
-        NodeUtils.makeRegionCircular(this);
-    }
-
-    /**
-     * Initializes the ripple generator's properties and
-     * adds the handler for ripple generation to the control.
-     */
-    protected void setupRippleGenerator() {
-        rippleGenerator.setAnimateBackground(false);
-        rippleGenerator.setRippleColor(Color.GRAY);
-        rippleGenerator.setInDuration(Duration.millis(350));
-
-        addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
-            rippleGenerator.setGeneratorCenterX(event.getX());
-            rippleGenerator.setGeneratorCenterY(event.getY());
-            rippleGenerator.createRipple();
-        });
-    }
-
-    //================================================================================
-    // Styleable Properties
-    //================================================================================
-
-    private final StyleableDoubleProperty size = new SimpleStyleableDoubleProperty(
-            StyleableProperties.SIZE,
-            this,
-            "size",
-            40.0
-    );
-
-    private final StyleableObjectProperty<ToggleNodeShape> toggleShape = new SimpleStyleableObjectProperty<>(
-            StyleableProperties.SHAPE,
-            this,
-            "toggleShape",
-            ToggleNodeShape.CIRCLE
-    );
-
-    private final StyleableObjectProperty<Paint> selectedColor = new SimpleStyleableObjectProperty<>(
-            StyleableProperties.SELECTED_COLOR,
-            this,
-            "selectedColor",
-            Color.rgb(190, 190, 190, 0.5)
-    );
-
-    private final StyleableObjectProperty<Paint> unSelectedColor = new SimpleStyleableObjectProperty<>(
-            StyleableProperties.UNSELECTED_COLOR,
-            this,
-            "unSelectedColor",
-            Color.TRANSPARENT
-    );
-
-    public double getSize() {
-        return size.get();
-    }
-
-    /**
-     * Specifies the size (both width and height) of the control.
-     */
-    public StyleableDoubleProperty sizeProperty() {
-        return size;
-    }
-
-    public void setSize(double size) {
-        this.size.set(size);
-    }
-
-    public ToggleNodeShape getToggleShape() {
-        return toggleShape.get();
-    }
-
-    /**
-     * Specifies the shape of the control
-     */
-    public StyleableObjectProperty<ToggleNodeShape> toggleShapeProperty() {
-        return toggleShape;
-    }
-
-    public void setToggleShape(ToggleNodeShape toggleShape) {
-        this.toggleShape.set(toggleShape);
-    }
-
-    public Paint getSelectedColor() {
-        return selectedColor.get();
-    }
-
-    /**
-     * Specifies the background color when selected.
-     */
-    public StyleableObjectProperty<Paint> selectedColorProperty() {
-        return selectedColor;
-    }
-
-    public void setSelectedColor(Paint selectedColor) {
-        this.selectedColor.set(selectedColor);
-    }
-
-    public Paint getUnSelectedColor() {
-        return unSelectedColor.get();
-    }
-
-    /**
-     * Specifies the background color when unselected.
-     */
-    public StyleableObjectProperty<Paint> unSelectedColorProperty() {
-        return unSelectedColor;
-    }
-
-    public void setUnSelectedColor(Paint unSelectedColor) {
-        this.unSelectedColor.set(unSelectedColor);
-    }
-
-    //================================================================================
-    // CssMetaData
-    //================================================================================
-    private static class StyleableProperties {
-        private static final List<CssMetaData<? extends Styleable, ?>> cssMetaDataList;
-
-        private static final CssMetaData<MFXToggleNode, Number> SIZE =
-                FACTORY.createSizeCssMetaData(
-                        "-mfx-size",
-                        MFXToggleNode::sizeProperty,
-                        40
-                );
-
-        private static final CssMetaData<MFXToggleNode, ToggleNodeShape> SHAPE =
-                FACTORY.createEnumCssMetaData(
-                        ToggleNodeShape.class,
-                        "-mfx-shape",
-                        MFXToggleNode::toggleShapeProperty,
-                        ToggleNodeShape.CIRCLE
-                );
-
-        private static final CssMetaData<MFXToggleNode, Paint> SELECTED_COLOR =
-                FACTORY.createPaintCssMetaData(
-                        "-mfx-selected-color",
-                        MFXToggleNode::selectedColorProperty,
-                        Color.rgb(0, 0, 0, 0.2)
-                );
-
-        private static final CssMetaData<MFXToggleNode, Paint> UNSELECTED_COLOR =
-                FACTORY.createPaintCssMetaData(
-                        "-mfx-unselected-color",
-                        MFXToggleNode::unSelectedColorProperty,
-                        Color.TRANSPARENT
-                );
-
-        static {
-            cssMetaDataList = List.of(SIZE, SHAPE, SELECTED_COLOR, UNSELECTED_COLOR);
-        }
-    }
-
-    public static List<CssMetaData<? extends Styleable, ?>> getControlCssMetaDataList() {
-        return StyleableProperties.cssMetaDataList;
-    }
-
-    //================================================================================
-    // Override Methods
-    //================================================================================
-    @Override
-    protected Skin<?> createDefaultSkin() {
-        ToggleButtonSkin skin = new ToggleButtonSkin(this);
-        getChildren().add(0, rippleGenerator);
-        return skin;
-    }
-
-    @Override
-    public String getUserAgentStylesheet() {
-        return STYLESHEET;
-    }
-
-    @Override
-    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
-        return MFXToggleNode.getControlCssMetaDataList();
-    }
-}

+ 3 - 3
materialfx/src/main/java/io/github/palexdev/materialfx/controls/base/AbstractMFXFlowlessListCell.java

@@ -31,7 +31,7 @@ import javafx.scene.layout.HBox;
 /**
  * Base class for all cells used in list views based on Flowless,
  * defines common properties and behavior (e.g selection), has the selected property
- * and pseudo class ":selected" for usage in CSS.
+ * and PseudoClass ":selected" for usage in CSS.
  * <p>
  * Extends {@link HBox} and implements {@link Cell}.
  *
@@ -88,7 +88,7 @@ public abstract class AbstractMFXFlowlessListCell<T> extends HBox implements Cel
      * Sets the following behaviors:
      * <p>
      * - Calls {@link #updateSelection(MouseEvent)} on mouse pressed.<p>
-     * - Updates the selected pseudo class state when selected property changes.<p>
+     * - Updates the selected PseudoClass state when selected property changes.<p>
      * - Calls {@link #afterUpdateIndex()} when the index property changes.<p>
      * - Updates the selected property according to the list view' selection model changes.
      */
@@ -108,7 +108,7 @@ public abstract class AbstractMFXFlowlessListCell<T> extends HBox implements Cel
      * then according to the new state updates the selection model.
      * <p></p>
      * If true and the selection model doesn't already contain the cell index then calls
-     * {@link ListSelectionModel#select(int, T, MouseEvent)} with the cell's index and data.
+     * {@link ListSelectionModel#select(int, Object, MouseEvent)} with the cell's index and data.
      * <p></p>
      * If false calls {@link ListSelectionModel#clearSelectedItem(int)} with the cell's index.
      */

+ 272 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/base/AbstractMFXToggleNode.java

@@ -0,0 +1,272 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
+ *
+ *     MaterialFX is free software: you can redistribute it and/or modify
+ *     it under the terms of the GNU General Public License as published by
+ *     the Free Software Foundation, either version 3 of the License, or
+ *     (at your option) any later version.
+ *
+ *     MaterialFX is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *     GNU General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.controls.base;
+
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.css.*;
+import javafx.scene.Node;
+import javafx.scene.control.ToggleButton;
+import javafx.scene.paint.Color;
+import javafx.scene.paint.Paint;
+
+import java.util.List;
+
+public abstract class AbstractMFXToggleNode extends ToggleButton {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private static final StyleablePropertyFactory<AbstractMFXToggleNode> FACTORY = new StyleablePropertyFactory<>(ToggleButton.getClassCssMetaData());
+    private final String STYLE_CLASS = "mfx-toggle-node";
+
+    private final ObjectProperty<Node> labelLeadingIcon = new SimpleObjectProperty<>();
+    private final ObjectProperty<Node> labelTrailingIcon = new SimpleObjectProperty<>();
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public AbstractMFXToggleNode() {
+        initialize();
+    }
+
+    public AbstractMFXToggleNode(String text) {
+        super(text);
+        initialize();
+    }
+
+    public AbstractMFXToggleNode(String text, Node graphic) {
+        super(text, graphic);
+        initialize();
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+    private void initialize() {
+        getStyleClass().add(STYLE_CLASS);
+    }
+
+    public Node getLabelLeadingIcon() {
+        return labelLeadingIcon.get();
+    }
+
+    /**
+     * Specifies the label's leading icon.
+     */
+    public ObjectProperty<Node> labelLeadingIconProperty() {
+        return labelLeadingIcon;
+    }
+
+    public void setLabelLeadingIcon(Node labelLeadingIcon) {
+        this.labelLeadingIcon.set(labelLeadingIcon);
+    }
+
+    public Node getLabelTrailingIcon() {
+        return labelTrailingIcon.get();
+    }
+
+    /**
+     * Specifies the label's trailing icon.
+     */
+    public ObjectProperty<Node> labelTrailingIconProperty() {
+        return labelTrailingIcon;
+    }
+
+    public void setLabelTrailingIcon(Node labelTrailingIcon) {
+        this.labelTrailingIcon.set(labelTrailingIcon);
+    }
+
+
+    //================================================================================
+    // Styleable Properties
+    //================================================================================
+    private final StyleableObjectProperty<Paint> unselectedColor = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.UNSELECTED_COLOR,
+            this,
+            "unselectedColor",
+            Color.web("#F5F5F5")
+    );
+
+    private final StyleableObjectProperty<Paint> selectedColor = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.SELECTED_COLOR,
+            this,
+            "selectedColor",
+            Color.web("#EDEDED")
+    );
+
+    private final StyleableObjectProperty<Paint> unselectedBorderColor = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.UNSELECTED_BORDER_COLOR,
+            this,
+            "unselectedBorderColor",
+            Color.web("#E1E1E1")
+    );
+
+    private final StyleableObjectProperty<Paint> selectedBorderColor = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.SELECTED_BORDER_COLOR,
+            this,
+            "selectedBorderColor",
+            Color.web("#E1E1E1")
+    );
+
+    private final StyleableDoubleProperty labelTextGap = new SimpleStyleableDoubleProperty(
+            StyleableProperties.LABEL_TEXT_GAP,
+            this,
+            "labelTextGap",
+            10.0
+    );
+
+
+    public Paint getUnselectedColor() {
+        return unselectedColor.get();
+    }
+
+    /**
+     * Specifies the toggle's color when it's not selected.
+     */
+    public StyleableObjectProperty<Paint> unselectedColorProperty() {
+        return unselectedColor;
+    }
+
+    public void setUnselectedColor(Paint unselectedColor) {
+        this.unselectedColor.set(unselectedColor);
+    }
+
+    public Paint getSelectedColor() {
+        return selectedColor.get();
+    }
+
+    /**
+     * Specifies the toggle's color when it's selected.
+     */
+    public StyleableObjectProperty<Paint> selectedColorProperty() {
+        return selectedColor;
+    }
+
+    public void setSelectedColor(Paint selectedColor) {
+        this.selectedColor.set(selectedColor);
+    }
+
+    public Paint getUnselectedBorderColor() {
+        return unselectedBorderColor.get();
+    }
+
+    /**
+     * Specifies the toggle borders color when not selected.
+     */
+    public StyleableObjectProperty<Paint> unselectedBorderColorProperty() {
+        return unselectedBorderColor;
+    }
+
+    public void setUnselectedBorderColor(Paint unselectedBorderColor) {
+        this.unselectedBorderColor.set(unselectedBorderColor);
+    }
+
+    public Paint getSelectedBorderColor() {
+        return selectedBorderColor.get();
+    }
+
+    /**
+     * Specifies the toggle borders color when selected.
+     */
+    public StyleableObjectProperty<Paint> selectedBorderColorProperty() {
+        return selectedBorderColor;
+    }
+
+    public void setSelectedBorderColor(Paint selectedBorderColor) {
+        this.selectedBorderColor.set(selectedBorderColor);
+    }
+
+    public double getLabelTextGap() {
+        return labelTextGap.get();
+    }
+
+    /**
+     * Specifies the gap between the toggle's circle and the label.
+     */
+    public StyleableDoubleProperty labelTextGapProperty() {
+        return labelTextGap;
+    }
+
+    public void setLabelTextGap(double labelTextGap) {
+        this.labelTextGap.set(labelTextGap);
+    }
+
+    //================================================================================
+    // CssMetaData
+    //================================================================================
+    private static class StyleableProperties {
+        private static final List<CssMetaData<? extends Styleable, ?>> cssMetaDataList;
+
+        private static final CssMetaData<AbstractMFXToggleNode, Paint> UNSELECTED_COLOR =
+                FACTORY.createPaintCssMetaData(
+                        "-mfx-unselected-color",
+                        AbstractMFXToggleNode::unselectedColorProperty,
+                        Color.web("#F5F5F5")
+                );
+
+        private static final CssMetaData<AbstractMFXToggleNode, Paint> SELECTED_COLOR =
+                FACTORY.createPaintCssMetaData(
+                        "-mfx-selected-color",
+                        AbstractMFXToggleNode::selectedColorProperty,
+                        Color.web("#EDEDED")
+                );
+
+        private static final CssMetaData<AbstractMFXToggleNode, Paint> UNSELECTED_BORDER_COLOR =
+                FACTORY.createPaintCssMetaData(
+                        "-mfx-unselected-border-color",
+                        AbstractMFXToggleNode::unselectedBorderColorProperty,
+                        Color.web("#E1E1E1")
+                );
+
+        private static final CssMetaData<AbstractMFXToggleNode, Paint> SELECTED_BORDER_COLOR =
+                FACTORY.createPaintCssMetaData(
+                        "-mfx-selected-border-color",
+                        AbstractMFXToggleNode::selectedBorderColorProperty,
+                        Color.web("#E1E1E1")
+                );
+
+        private static final CssMetaData<AbstractMFXToggleNode, Number> LABEL_TEXT_GAP =
+                FACTORY.createSizeCssMetaData(
+                        "-mfx-label-text-gap",
+                        AbstractMFXToggleNode::labelTextGapProperty,
+                        10.0
+                );
+
+        static {
+            cssMetaDataList = List.of(
+                    UNSELECTED_COLOR, SELECTED_COLOR,
+                    UNSELECTED_BORDER_COLOR, SELECTED_BORDER_COLOR,
+                    LABEL_TEXT_GAP
+            );
+        }
+
+    }
+
+    public static List<CssMetaData<? extends Styleable, ?>> getControlCssMetaDataList() {
+        return AbstractMFXToggleNode.StyleableProperties.cssMetaDataList;
+    }
+
+    //================================================================================
+    // Override Methods
+    //================================================================================
+    @Override
+    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
+        return getControlCssMetaDataList();
+    }
+}

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/controls/base/AbstractMFXTreeCell.java

@@ -93,7 +93,7 @@ public abstract class AbstractMFXTreeCell<T> extends HBox {
     }
 
     /**
-     * Adds a listener to the selected property to change the pseudo class state.
+     * Adds a listener to the selected property to change the PseudoClass state.
      */
     private void addListeners() {
         selected.addListener(invalidate -> pseudoClassStateChanged(SELECTED_PSEUDO_CLASS, selected.get()));

+ 3 - 3
materialfx/src/main/java/io/github/palexdev/materialfx/controls/cell/MFXFlowlessCheckListCell.java

@@ -36,7 +36,7 @@ import javafx.scene.layout.HBox;
 
 /**
  * Implementation of an {@link AbstractMFXFlowlessListCell} which has a combo box
- * for usage in {@link MFXFlowlessCheckListView}, has the checked property and pseudo class
+ * for usage in {@link MFXFlowlessCheckListView}, has the checked property and PseudoClass
  * ":checked" for usage in CSS.
  */
 public class MFXFlowlessCheckListCell<T> extends AbstractMFXFlowlessListCell<T> {
@@ -100,7 +100,7 @@ public class MFXFlowlessCheckListCell<T> extends AbstractMFXFlowlessListCell<T>
      * <p>
      * - Binds the checked property to the selected property of the combo box.<p>
      * - Clears the selection (if {@link #clearSelectionOnCheck} is true), updates the
-     * checked pseudo class state and calls {@link #updateCheck()} when the checked property changes.
+     * checked PseudoClass state and calls {@link #updateCheck()} when the checked property changes.
      */
     @Override
     protected void setBehavior() {
@@ -119,7 +119,7 @@ public class MFXFlowlessCheckListCell<T> extends AbstractMFXFlowlessListCell<T>
      * Updates the check model accordingly to the new state of the checked property.
      * <p></p>
      * If true and the check model doesn't already contain the cell index then calls
-     * {@link ListCheckModel#check(int, T)} with the cell's index and data.
+     * {@link ListCheckModel#check(int, Object)} with the cell's index and data.
      * <p></p>
      * If false calls {@link ListCheckModel#clearCheckedItem(int)} with the cell's index.
      */

+ 272 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/cell/MFXTableColumn.java

@@ -0,0 +1,272 @@
+package io.github.palexdev.materialfx.controls.cell;
+
+import io.github.palexdev.materialfx.MFXResourcesLoader;
+import io.github.palexdev.materialfx.controls.MFXIconWrapper;
+import io.github.palexdev.materialfx.controls.MFXTableView;
+import io.github.palexdev.materialfx.controls.enums.SortState;
+import io.github.palexdev.materialfx.font.MFXFontIcon;
+import io.github.palexdev.materialfx.skins.MFXTableColumnSkin;
+import io.github.palexdev.materialfx.skins.MFXTableViewSkin;
+import io.github.palexdev.materialfx.utils.NodeUtils;
+import javafx.beans.binding.StringExpression;
+import javafx.beans.property.*;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.css.PseudoClass;
+import javafx.geometry.Pos;
+import javafx.scene.control.Control;
+import javafx.scene.control.Skin;
+import javafx.scene.control.Tooltip;
+import javafx.scene.input.MouseEvent;
+
+import java.util.Comparator;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * This is the implementation of the column cells used in the {@link MFXTableView} control.
+ * <p></p>
+ * Extends {@code Control} so a new skin is also provided and can be also easily changed.
+ * <p>
+ * Defines the following new PseudoClasses for usage in CSS:
+ * <p> - ":dragged", to customize the column when it is dragged
+ * <p> - ":resizable", to customize the column depending on {@link #resizableProperty()}
+ * <p></p>
+ * Each column cell has the following responsibilities:
+ * - Has a row cell factory because each column knows how to build the corresponding row cell in each table row<p>
+ * - Has a sort state and a comparator because each column knows how to sort the rows based on the given comparator, also
+ * retains its sort state thus allowing switching between ASCENDING, DESCENDING, UNSORTED<p>
+ */
+public class MFXTableColumn<T> extends Control {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private final String STYLE_CLASS = "mfx-table-column";
+    private final String STYLESHEET = MFXResourcesLoader.load("css/mfx-tablecolumn.css");
+
+    private final ReadOnlyDoubleWrapper initialWidth = new ReadOnlyDoubleWrapper();
+
+    private final StringProperty text = new SimpleStringProperty();
+    private final ObjectProperty<Pos> columnAlignment = new SimpleObjectProperty<>(Pos.CENTER_LEFT);
+    private final ObjectProperty<Function<T, MFXTableRowCell>> rowCellFunction = new SimpleObjectProperty<>();
+
+    private final ObjectProperty<SortState> sortState = new SimpleObjectProperty<>(SortState.UNSORTED);
+    private MFXIconWrapper sortIcon;
+    private Comparator<T> comparator;
+
+    private final ObjectProperty<Supplier<Tooltip>> tooltipSupplier = new SimpleObjectProperty<>();
+
+    protected static final PseudoClass RESIZABLE_PSEUDO_CLASS = PseudoClass.getPseudoClass("resizable");
+    protected static final PseudoClass DRAG_PSEUDO_CLASS = PseudoClass.getPseudoClass("dragged");
+    private final BooleanProperty dragged = new SimpleBooleanProperty(false);
+    private final BooleanProperty resizable = new SimpleBooleanProperty(true);
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXTableColumn(String text) {
+        this(text, null);
+    }
+
+    public MFXTableColumn(StringExpression text) {
+        this(text, null);
+    }
+
+    public MFXTableColumn(String text, Comparator<T> comparator) {
+        setText(text);
+        this.comparator = comparator;
+        initialize();
+    }
+
+    public MFXTableColumn(StringExpression text, Comparator<T> comparator) {
+        this.text.bind(text);
+        this.comparator = comparator;
+        initialize();
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+    private void initialize() {
+        getStyleClass().setAll(STYLE_CLASS);
+        setMinSize(180, 30);
+
+        resizable.addListener(invalidated -> pseudoClassStateChanged(RESIZABLE_PSEUDO_CLASS, resizable.get()));
+        pseudoClassStateChanged(RESIZABLE_PSEUDO_CLASS, resizable.get());
+        dragged.addListener(invalidate -> pseudoClassStateChanged(DRAG_PSEUDO_CLASS, dragged.get()));
+        addEventFilter(MouseEvent.MOUSE_DRAGGED, event -> dragged.set(true));
+        addEventFilter(MouseEvent.MOUSE_RELEASED, event -> dragged.set(false));
+
+        sortIcon = new MFXIconWrapper(new MFXFontIcon("mfx-caret-up", 12), 18);
+        sortIcon.setManaged(false);
+        sortIcon.setVisible(false);
+        NodeUtils.makeRegionCircular(sortIcon);
+
+        needsLayoutProperty().addListener(new ChangeListener<>() {
+            @Override
+            public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
+                if (!newValue && getWidth() > 0) {
+                    setInitialWidth(getWidth());
+                    needsLayoutProperty().removeListener(this);
+                }
+            }
+        });
+
+        tooltipSupplier.addListener((observable, oldValue, newValue) -> setTooltip(newValue.get()));
+        defaultTooltipSupplier();
+    }
+
+    /**
+     * Defines the default column's tooltip.
+     * <p></p>
+     * The default tooltip has its text property bound to the column's text property.
+     */
+    protected void defaultTooltipSupplier() {
+        setTooltipSupplier(() -> {
+            Tooltip tooltip = new Tooltip();
+            tooltip.textProperty().bind(textProperty());
+            return tooltip;
+        });
+    }
+
+    public double getInitialWidth() {
+        return initialWidth.get();
+    }
+
+    /**
+     * Specifies what was the initial width assigned to the control by JavaFX.
+     * We keep this value to use it the the context menu of the column,
+     * see {@link MFXTableViewSkin}
+     */
+    public ReadOnlyDoubleProperty initialWidthProperty() {
+        return initialWidth.getReadOnlyProperty();
+    }
+
+    protected void setInitialWidth(double initialWidth) {
+        this.initialWidth.set(initialWidth);
+    }
+
+    public String getText() {
+        return text.get();
+    }
+
+    /**
+     * Specifies the column text/name.
+     */
+    public StringProperty textProperty() {
+        return text;
+    }
+
+    public void setText(String text) {
+        this.text.set(text);
+    }
+
+    public Pos getColumnAlignment() {
+        return columnAlignment.get();
+    }
+
+    /**
+     * Specifies the column's alignment.
+     */
+    public ObjectProperty<Pos> columnAlignmentProperty() {
+        return columnAlignment;
+    }
+
+    public void setColumnAlignment(Pos columnAlignment) {
+        this.columnAlignment.set(columnAlignment);
+    }
+
+    public Function<T, MFXTableRowCell> getRowCellFunction() {
+        return rowCellFunction.get();
+    }
+
+    /**
+     * Specifies the function responsible for building the table row cells.
+     */
+    public ObjectProperty<Function<T, MFXTableRowCell>> rowCellFunctionProperty() {
+        return rowCellFunction;
+    }
+
+    public void setRowCellFunction(Function<T, MFXTableRowCell> rowCellFunction) {
+        this.rowCellFunction.set(rowCellFunction);
+    }
+
+    public SortState getSortState() {
+        return sortState.get();
+    }
+
+    /**
+     * Specifies the sort state of the column: UNSORTED, ASCENDING, DESCENDING.
+     */
+    public ObjectProperty<SortState> sortStateProperty() {
+        return sortState;
+    }
+
+    public void setSortState(SortState sortState) {
+        this.sortState.set(sortState);
+    }
+
+    /**
+     * @return the instance of the sort icon
+     */
+    public MFXIconWrapper getSortIcon() {
+        return sortIcon;
+    }
+
+    /**
+     * @return the user specifies comparator for this column
+     */
+    public Comparator<T> getComparator() {
+        return comparator;
+    }
+
+    /**
+     * Sets this column's comparator
+     */
+    public void setComparator(Comparator<T> comparator) {
+        this.comparator = comparator;
+    }
+
+    public Supplier<Tooltip> getTooltipSupplier() {
+        return tooltipSupplier.get();
+    }
+
+    /**
+     * Specifies the supplier used to build the column's tooltip.
+     */
+    public ObjectProperty<Supplier<Tooltip>> tooltipSupplierProperty() {
+        return tooltipSupplier;
+    }
+
+    public void setTooltipSupplier(Supplier<Tooltip> tooltipSupplier) {
+        this.tooltipSupplier.set(tooltipSupplier);
+    }
+
+    public boolean isResizable() {
+        return resizable.get();
+    }
+
+    /**
+     * Specifies is this column should be resizable.
+     */
+    public BooleanProperty resizableProperty() {
+        return resizable;
+    }
+
+    public void setResizable(boolean resizable) {
+        this.resizable.set(resizable);
+    }
+
+    //================================================================================
+    // Override Methods
+    //================================================================================
+    @Override
+    protected Skin<?> createDefaultSkin() {
+        return new MFXTableColumnSkin<>(this);
+    }
+
+    @Override
+    public String getUserAgentStylesheet() {
+        return STYLESHEET;
+    }
+}

+ 0 - 209
materialfx/src/main/java/io/github/palexdev/materialfx/controls/cell/MFXTableColumnCell.java

@@ -1,209 +0,0 @@
-/*
- *     Copyright (C) 2021 Parisi Alessandro
- *     This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
- *
- *     MaterialFX is free software: you can redistribute it and/or modify
- *     it under the terms of the GNU General Public License as published by
- *     the Free Software Foundation, either version 3 of the License, or
- *     (at your option) any later version.
- *
- *     MaterialFX is distributed in the hope that it will be useful,
- *     but WITHOUT ANY WARRANTY; without even the implied warranty of
- *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *     GNU General Public License for more details.
- *
- *     You should have received a copy of the GNU General Public License
- *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package io.github.palexdev.materialfx.controls.cell;
-
-import io.github.palexdev.materialfx.MFXResourcesLoader;
-import io.github.palexdev.materialfx.controls.enums.SortState;
-import io.github.palexdev.materialfx.skins.MFXTableColumnCellSkin;
-import javafx.beans.property.*;
-import javafx.beans.value.ChangeListener;
-import javafx.beans.value.ObservableValue;
-import javafx.css.PseudoClass;
-import javafx.scene.control.Label;
-import javafx.scene.control.Skin;
-import javafx.scene.input.MouseEvent;
-import javafx.util.Callback;
-
-import java.util.Comparator;
-
-/**
- * This is the implementation of the column cells used in the {@link io.github.palexdev.materialfx.controls.MFXTableView} columns header.
- * <p>
- * Each column cell is a {@code Label}, has a name and has the following responsibilities:
- * - Has a row cell factory because each column knows how to build the corresponding row cell in each table row<p>
- * - Has a sort state and a comparator because each column knows how to sort the rows based on the given comparator, also
- * retains its sort state thus allowing switching between ASCENDING, DESCENDING, UNSORTED<p>
- */
-public class MFXTableColumnCell<T> extends Label {
-    //================================================================================
-    // Properties
-    //================================================================================
-    private final String STYLE_CLASS = "mfx-table-column-cell";
-    private final String STYLESHEET = MFXResourcesLoader.load("css/mfx-table-column-cell.css");
-
-    private final ReadOnlyDoubleWrapper initialWidth = new ReadOnlyDoubleWrapper();
-
-    private final ObjectProperty<Callback<T, ? extends MFXTableRowCell>> rowCellFactory = new SimpleObjectProperty<>();
-    private final StringProperty columnName = new SimpleStringProperty("");
-    private final BooleanProperty hasTooltip = new SimpleBooleanProperty(true);
-    private final StringProperty tooltipText = new SimpleStringProperty();
-
-    private SortState sortState = SortState.UNSORTED;
-    private Comparator<T> comparator;
-
-    private static final PseudoClass DRAG_PSEUDO_CLASS = PseudoClass.getPseudoClass("dragged");
-    private final BooleanProperty dragged = new SimpleBooleanProperty(false);
-
-    //================================================================================
-    // Constructors
-    //================================================================================
-    public MFXTableColumnCell(String columnName) {
-        textProperty().bind(columnNameProperty());
-        setColumnName(columnName);
-        initialize();
-    }
-
-    public MFXTableColumnCell(String columnName, Comparator<T> comparator) {
-        textProperty().bind(columnNameProperty());
-        setColumnName(columnName);
-        this.comparator = comparator;
-        initialize();
-    }
-
-    //================================================================================
-    // Methods
-    //================================================================================
-    private void initialize() {
-        getStyleClass().add(STYLE_CLASS);
-        setTooltipText(getColumnName());
-
-        dragged.addListener(invalidate -> pseudoClassStateChanged(DRAG_PSEUDO_CLASS, dragged.get()));
-        addEventFilter(MouseEvent.MOUSE_DRAGGED, event -> dragged.set(true));
-        addEventFilter(MouseEvent.MOUSE_RELEASED, event -> dragged.set(false));
-
-        widthProperty().addListener(new ChangeListener<>() {
-            @Override
-            public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
-                if (newValue != null && newValue.doubleValue() > 0) {
-                    setInitialWidth(newValue.doubleValue());
-                    widthProperty().removeListener(this);
-                }
-            }
-        });
-    }
-
-    public double getInitialWidth() {
-        return initialWidth.get();
-    }
-
-    /**
-     * Specifies what was the initial width assigned to the control by JavaFX.
-     * We keep this value to use it the the context menu of the column,
-     * see {@link io.github.palexdev.materialfx.skins.MFXTableViewSkin}
-     */
-    public ReadOnlyDoubleProperty initialWidthProperty() {
-        return initialWidth.getReadOnlyProperty();
-    }
-
-    protected void setInitialWidth(double initialWidth) {
-        this.initialWidth.set(initialWidth);
-    }
-
-    public Callback<T, ? extends MFXTableRowCell> getRowCellFactory() {
-        return rowCellFactory.get();
-    }
-
-    /**
-     * Specifies the callback which is used to build the row cell of the column.
-     */
-    public ObjectProperty<Callback<T, ? extends MFXTableRowCell>> rowCellFactoryProperty() {
-        return rowCellFactory;
-    }
-
-    public void setRowCellFactory(Callback<T, ? extends MFXTableRowCell> rowCellFactory) {
-        this.rowCellFactory.set(rowCellFactory);
-    }
-
-    public String getColumnName() {
-        return columnName.get();
-    }
-
-    /**
-     * Specifies the name of the column.
-     */
-    public StringProperty columnNameProperty() {
-        return columnName;
-    }
-
-    public void setColumnName(String columnName) {
-        this.columnName.set(columnName);
-    }
-
-    public boolean hasTooltip() {
-        return hasTooltip.get();
-    }
-
-    /**
-     * Specifies if the column cell should show a tooltip or not.
-     * <p>
-     * By default the tooltip is initialized with the column's name.
-     */
-    public BooleanProperty hasTooltipProperty() {
-        return hasTooltip;
-    }
-
-    public void setHasTooltip(boolean hasTooltip) {
-        this.hasTooltip.set(hasTooltip);
-    }
-
-    public String getTooltipText() {
-        return tooltipText.get();
-    }
-
-    /**
-     * Specifies the text shown by the tooltip.
-     */
-    public StringProperty tooltipTextProperty() {
-        return tooltipText;
-    }
-
-    public void setTooltipText(String tooltipText) {
-        this.tooltipText.set(tooltipText);
-    }
-
-    public SortState getSortState() {
-        return sortState;
-    }
-
-    public void setSortState(SortState sortState) {
-        this.sortState = sortState;
-    }
-
-    public Comparator<T> getComparator() {
-        return comparator;
-    }
-
-    public void setComparator(Comparator<T> comparator) {
-        this.comparator = comparator;
-    }
-
-    //================================================================================
-    // Override Methods
-    //================================================================================
-    @Override
-    protected Skin<?> createDefaultSkin() {
-        return new MFXTableColumnCellSkin<>(this);
-    }
-
-    @Override
-    public String getUserAgentStylesheet() {
-        return STYLESHEET;
-    }
-
-}

+ 131 - 53
materialfx/src/main/java/io/github/palexdev/materialfx/controls/cell/MFXTableRowCell.java

@@ -1,72 +1,60 @@
-/*
- *     Copyright (C) 2021 Parisi Alessandro
- *     This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
- *
- *     MaterialFX is free software: you can redistribute it and/or modify
- *     it under the terms of the GNU General Public License as published by
- *     the Free Software Foundation, either version 3 of the License, or
- *     (at your option) any later version.
- *
- *     MaterialFX is distributed in the hope that it will be useful,
- *     but WITHOUT ANY WARRANTY; without even the implied warranty of
- *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *     GNU General Public License for more details.
- *
- *     You should have received a copy of the GNU General Public License
- *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
- */
-
 package io.github.palexdev.materialfx.controls.cell;
 
-import io.github.palexdev.materialfx.MFXResourcesLoader;
-import javafx.beans.binding.StringBinding;
-import javafx.beans.property.StringProperty;
-import javafx.scene.control.Label;
+import io.github.palexdev.materialfx.controls.MFXTableRow;
+import io.github.palexdev.materialfx.controls.MFXTableView;
+import io.github.palexdev.materialfx.skins.MFXTableRowCellSkin;
+import javafx.beans.binding.StringExpression;
+import javafx.beans.property.*;
+import javafx.geometry.Pos;
+import javafx.scene.Node;
+import javafx.scene.control.Control;
+import javafx.scene.control.Skin;
 
 /**
- * This is the implementation of the row cells used in every {@link io.github.palexdev.materialfx.controls.MFXTableView} row.
- * The row cell is built by the corresponding {@link MFXTableColumnCell}.
+ * This is the implementation of the row cells used by {@link MFXTableView} to fill a {@link MFXTableRow}.
+ * <p>
+ * The cell is built by the corresponding column that defines the function, {@link MFXTableColumn#rowCellFunctionProperty()}
+ * <p>
+ * Extends {@code Control} so that anyone can implement their own skin if needed.
+ * <p>
+ * The default skin, {@link MFXTableRowCellSkin}, also allows to place up to two nodes in the cell. These nodes are specified by
+ * the following properties, {@link #leadingGraphicProperty()}, {@link #trailingGraphicProperty()}.
+ * <p>
+ * A little side note, also to respond to some Github issues. It is not recommended to use big nodes. It is not recommended to
+ * use too many nodes, that's why it's limited to two. If you need a lot of controls then consider having specific columns which build cells only with graphic
+ * like here <a href="https://bit.ly/2SzjrVu">Example</a>.
  * <p>
- * Each row cell is a {@code Label} and provides a series of constructors offering support from simple {@code Strings} to
- * JavaFX's {@code StringProperties} and {@code StringBindings}. This allows to use {@code MFXTableView} with models which
- * don't use JavaFX's properties.
- * <p></p>
- * Beware though, the data won't be automatically updated if it changes.
+ * Since it now extends {@code Control} you can easily define your own skin and do whatever you like with the
+ * control, just keep in mind that tables are designed to mostly show text.
  * <p>
- * In that case you must manually update the table firing a TableViewEvent.FORCE_UPDATE event on it.
+ * Has two constructors, one with a String and one with a {@link StringExpression}. The first one simply sets the cell's text to the given string,
+ * the other one binds the cell's text property to the given string expression.
  * <p>
- * After the data changed like this:
- * <pre>
- * {@code
- * ObservableList<Items> list = ...;
- * MFXTableView tableView = new MFXTableView(list);
- *
- * tableView.fireEvent(new MFXTableView.TableViewEvent(MFXTableView.TableViewEvent.FORCE_UPDATE_EVENT));
- * }
- * </pre>
+ * That allows to use {@link MFXTableView} with models which don't use JavaFX's properties. Of course the data won't change automatically in that case,
+ * so the table must be updated manually after the data has changed, {@link MFXTableView#updateTable()}.
  */
-public class MFXTableRowCell extends Label {
+public class MFXTableRowCell extends Control {
     //================================================================================
     // Properties
     //================================================================================
-    private final String STYLE_CLASS = "mfx-table-row-cell";
-    private final String STYLESHEET = MFXResourcesLoader.load("css/mfx-table-row-cell.css");
+    private final String STYLE_CLASS = "custom-row-cell";
+
+    private final StringProperty text = new SimpleStringProperty();
+    private final ObjectProperty<Node> leadingGraphic = new SimpleObjectProperty<>();
+    private final ObjectProperty<Node> trailingGraphic = new SimpleObjectProperty<>();
+    private final DoubleProperty graphicTextGap = new SimpleDoubleProperty(10);
+    private final ObjectProperty<Pos> rowAlignment = new SimpleObjectProperty<>(Pos.CENTER_LEFT);
 
     //================================================================================
     // Constructors
     //================================================================================
     public MFXTableRowCell(String text) {
-        super(text);
+        setText(text);
         initialize();
     }
 
-    public MFXTableRowCell(StringProperty stringProperty) {
-        textProperty().bind(stringProperty);
-        initialize();
-    }
-
-    public MFXTableRowCell(StringBinding stringBinding) {
-        textProperty().bind(stringBinding);
+    public MFXTableRowCell(StringExpression text) {
+        this.text.bind(text);
         initialize();
     }
 
@@ -74,14 +62,104 @@ public class MFXTableRowCell extends Label {
     // Methods
     //================================================================================
     private void initialize() {
-        getStyleClass().add(STYLE_CLASS);
+        getStyleClass().setAll(STYLE_CLASS);
+    }
+
+    /**
+     * Computes the minimum needed width so that the text is not truncated.
+     * By default it calls {@link #computePrefWidth(double)}
+     */
+    public double computeWidth() {
+        return computePrefWidth(-1);
+    }
+
+    /**
+     * By default checks if the current width is less than {@link #computeWidth()}.
+     */
+    public boolean isTruncated() {
+        return getWidth() < computeWidth();
+    }
+
+    public String getText() {
+        return text.get();
+    }
+
+    /**
+     * Specifies the cell's text. Can also be empty to show only the graphic.
+     */
+    public StringProperty textProperty() {
+        return text;
+    }
+
+    public void setText(String text) {
+        this.text.set(text);
+    }
+
+    public Node getLeadingGraphic() {
+        return leadingGraphic.get();
+    }
+
+    /**
+     * Specifies leading graphic of the cell.
+     */
+    public ObjectProperty<Node> leadingGraphicProperty() {
+        return leadingGraphic;
+    }
+
+    public void setLeadingGraphic(Node leadingGraphic) {
+        this.leadingGraphic.set(leadingGraphic);
+    }
+
+    public Node getTrailingGraphic() {
+        return trailingGraphic.get();
+    }
+
+    /**
+     * Specifies the trailing graphic of the cell.
+     */
+    public ObjectProperty<Node> trailingGraphicProperty() {
+        return trailingGraphic;
+    }
+
+    public void setTrailingGraphic(Node trailingGraphic) {
+        this.trailingGraphic.set(trailingGraphic);
+    }
+
+    public double getGraphicTextGap() {
+        return graphicTextGap.get();
+    }
+
+    /**
+     * Specifies the gap between the graphic nodes and the text.
+     */
+    public DoubleProperty graphicTextGapProperty() {
+        return graphicTextGap;
+    }
+
+    public void setGraphicTextGap(double graphicTextGap) {
+        this.graphicTextGap.set(graphicTextGap);
+    }
+
+    public Pos getRowAlignment() {
+        return rowAlignment.get();
+    }
+
+    /**
+     * Specifies the cell alignment.
+     */
+    public ObjectProperty<Pos> rowAlignmentProperty() {
+        return rowAlignment;
+    }
+
+    public void setRowAlignment(Pos rowAlignment) {
+        this.rowAlignment.set(rowAlignment);
     }
 
     //================================================================================
     // Override Methods
     //================================================================================
     @Override
-    public String getUserAgentStylesheet() {
-        return STYLESHEET;
+    protected Skin<?> createDefaultSkin() {
+        return new MFXTableRowCellSkin(this);
     }
 }

+ 10 - 19
materialfx/src/main/java/io/github/palexdev/materialfx/controls/enums/SortState.java

@@ -1,26 +1,17 @@
-/*
- *     Copyright (C) 2021 Parisi Alessandro
- *     This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
- *
- *     MaterialFX is free software: you can redistribute it and/or modify
- *     it under the terms of the GNU General Public License as published by
- *     the Free Software Foundation, either version 3 of the License, or
- *     (at your option) any later version.
- *
- *     MaterialFX is distributed in the hope that it will be useful,
- *     but WITHOUT ANY WARRANTY; without even the implied warranty of
- *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *     GNU General Public License for more details.
- *
- *     You should have received a copy of the GNU General Public License
- *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
- */
-
 package io.github.palexdev.materialfx.controls.enums;
 
 public enum SortState {
     ASCENDING,
     DESCENDING,
-    UNSORTED
+    UNSORTED;
+
+    private static final SortState[] valuesArr = values();
+
+    /**
+     * @return the next sort state
+     */
+    public SortState next() {
+        return valuesArr[(this.ordinal() + 1) % valuesArr.length];
+    }
 }
 

+ 22 - 4
materialfx/src/main/java/io/github/palexdev/materialfx/controls/factories/RippleClipTypeFactory.java

@@ -32,6 +32,8 @@ public class RippleClipTypeFactory {
     private double radius = 0;
     private double arcW = 0;
     private double arcH = 0;
+    private double offsetW = 0;
+    private double offsetH = 0;
 
     public RippleClipTypeFactory() {
     }
@@ -47,8 +49,8 @@ public class RippleClipTypeFactory {
     }
 
     public Shape build(Region region) {
-        double w = region.getWidth();
-        double h = region.getHeight();
+        double w = region.getWidth() + offsetW;
+        double h = region.getHeight() + offsetH;
 
         switch (rippleClipType) {
             case CIRCLE:
@@ -69,14 +71,30 @@ public class RippleClipTypeFactory {
         }
     }
 
+    public RippleClipTypeFactory setRadius(double radius) {
+        this.radius = radius;
+        return this;
+    }
+
+    public RippleClipTypeFactory setArcs(double arcs) {
+        this.arcW = arcs;
+        this.arcH = arcs;
+        return this;
+    }
+
     public RippleClipTypeFactory setArcs(double arcW, double arcH) {
         this.arcW = arcW;
         this.arcH = arcH;
         return this;
     }
 
-    public RippleClipTypeFactory setRadius(double radius) {
-        this.radius = radius;
+    public RippleClipTypeFactory setOffsetW(double offsetW) {
+        this.offsetW = offsetW;
+        return this;
+    }
+
+    public RippleClipTypeFactory setOffsetH(double offsetH) {
+        this.offsetH = offsetH;
         return this;
     }
 

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/controls/legacy/MFXLegacyComboBox.java

@@ -104,7 +104,7 @@ public class MFXLegacyComboBox<T> extends ComboBox<T> implements Validated<MFXDi
      * <p></p>
      * Then the label visible property is automatically updated when the validator state changes.
      * <p></p>
-     * The validator is also responsible for updating the ":invalid" pseudo class.
+     * The validator is also responsible for updating the ":invalid" PseudoClass.
      */
     private void setupValidator() {
         validator = new MFXDialogValidator("Error");

+ 4 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/effects/ripple/MFXCircleRippleGenerator.java

@@ -144,6 +144,10 @@ public class MFXCircleRippleGenerator extends AbstractMFXRippleGenerator<CircleR
      * and then dropped to 0. When the opacity is 0 it is removed from the children list.
      */
     protected Animation getBackgroundAnimation() {
+        if (getClipSupplier() == null || getClipSupplier().get() == null) {
+            throw new NullPointerException("RippleGenerator cannot animate background because clip supplier is null!!");
+        }
+
         Shape shape = getClipSupplier().get();
         shape.setFill(getRippleColor());
         shape.setOpacity(0);

+ 3 - 9
materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-table-row.css → materialfx/src/main/java/io/github/palexdev/materialfx/filter/EvaluationMode.java

@@ -16,14 +16,8 @@
  *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-.mfx-table-row {
-    -fx-background-insets: 0 -5 0 -5;
-}
+package io.github.palexdev.materialfx.filter;
 
-.mfx-table-row:hover {
-    -fx-background-color: rgb(245, 245, 245);
+public enum EvaluationMode {
+    AND, OR
 }
-
-.mfx-table-row:selected {
-    -fx-background-color: lightgreen;
-}

+ 157 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/filter/MFXEvaluationBox.java

@@ -0,0 +1,157 @@
+package io.github.palexdev.materialfx.filter;
+
+import io.github.palexdev.materialfx.MFXResourcesLoader;
+import io.github.palexdev.materialfx.controls.MFXComboBox;
+import io.github.palexdev.materialfx.controls.MFXTextField;
+import io.github.palexdev.materialfx.controls.enums.Styles;
+import io.github.palexdev.materialfx.font.MFXFontIcon;
+import io.github.palexdev.materialfx.utils.StringUtils;
+import javafx.collections.FXCollections;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.control.Label;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.paint.Color;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.function.BiPredicate;
+
+/**
+ * This little control provides a graphical way of evaluating a condition on
+ * a given string with a specific predicate. The computed boolean can also be chained with
+ * other conditions as an AND or an OR, specified by {@link EvaluationMode}.
+ * <p></p>
+ * The control is made of:
+ * <p> - An icon that should be used by the control's parent to remove it from the children list
+ * <p> - A text field that provides one of the strings to test
+ * <p> - A combo box that contains the predicate to apply
+ * <p></p>
+ * An usage example would be:
+ * <p></p>
+ * Let's say I have a string {@code s1 = AbcdE} and a string {@code s2 = "cde"} provided by the text field.
+ * <p>
+ * If the selected predicate is "Contains" then {@link #test(String)} will check if s1 contains s2 and will return false.
+ * <p>
+ * If the selected predicate is "Contains Ignore Case" then {@link #test(String)} will check if s1 contains s2 ignoring case and will return true.
+ *
+ * @see BiPredicate
+ */
+public class MFXEvaluationBox extends HBox {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private final String STYLE_CLASS = "mfx/evaluation-box";
+    private final String STYLESHEET = MFXResourcesLoader.load("css/mfx-evaluationbox.css");
+
+    private final EvaluationMode mode;
+    private final Map<String, BiPredicate<String, String>> biPredicates = new LinkedHashMap<>();
+    private final MFXFontIcon removeIcon;
+    private final MFXTextField inputField;
+    private final MFXComboBox<String> predicatesCombo;
+
+    //================================================================================
+    // Constructor
+    //================================================================================
+    public MFXEvaluationBox(EvaluationMode mode) {
+        setPrefWidth(600);
+        setAlignment(Pos.CENTER_LEFT);
+        setSpacing(20);
+        setPadding(new Insets(15, 10, 15, 10));
+
+        this.mode = mode;
+
+        Label modeLabel = new Label(mode.name());
+        modeLabel.setId("modeLabel");
+        modeLabel.setPrefSize(40, 40);
+        modeLabel.setMaxHeight(Double.MAX_VALUE);
+        modeLabel.setPadding(new Insets(3));
+        modeLabel.setAlignment(Pos.CENTER);
+
+        HBox box = new HBox(5);
+        box.setAlignment(Pos.CENTER);
+
+        Label predicateLabel = new Label("Evaluation Predicate:");
+        predicatesCombo = new MFXComboBox<>();
+        predicatesCombo.setComboStyle(Styles.ComboBoxStyles.STYLE2);
+        predicatesCombo.setPrefSize(180, 27);
+        predicatesCombo.setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE);
+
+        inputField = new MFXTextField();
+        inputField.setPromptText("Input String...");
+        inputField.setAnimateLines(false);
+        inputField.setLineColor(Color.web("#4d4d4d"));
+        inputField.setLineStrokeWidth(1);
+        inputField.setMaxWidth(Double.MAX_VALUE);
+        HBox.setHgrow(inputField, Priority.ALWAYS);
+        HBox.setMargin(inputField, new Insets(0, 10, 2, 0));
+        inputField.getStylesheets().add(STYLESHEET);
+
+        removeIcon = new MFXFontIcon("mfx-x-circle", 16, Color.web("#4D4D4D"));
+
+        box.getChildren().addAll(predicateLabel, predicatesCombo);
+        getChildren().addAll(removeIcon, modeLabel, box, inputField);
+
+        initialize();
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+    private void initialize() {
+        getStyleClass().add(STYLE_CLASS);
+
+        biPredicates.put("Contains", String::contains);
+        biPredicates.put("Contains Ignore Case", StringUtils::containsIgnoreCase);
+        biPredicates.put("Starts With", String::startsWith);
+        biPredicates.put("Start With Ignore Case", StringUtils::startsWithIgnoreCase);
+        biPredicates.put("Ends With", String::endsWith);
+        biPredicates.put("Ends With Ignore Case", StringUtils::endsWithIgnoreCase);
+        biPredicates.put("Equals", String::equals);
+        biPredicates.put("Equals Ignore Case", String::equalsIgnoreCase);
+
+        predicatesCombo.setItems(FXCollections.observableArrayList(biPredicates.keySet()));
+        predicatesCombo.getSelectionModel().selectFirst();
+    }
+
+    /**
+     * Applies the selected predicate (provided by the combo box) to the given
+     * string and the text provided by the text field.
+     */
+    public Boolean test(String testString) {
+        if (getPredicate() != null) {
+            return biPredicates.get(getPredicate()).test(testString, inputField.getText());
+        }
+        return false;
+    }
+
+    /**
+     * @return the evaluation mode of this control
+     */
+    public EvaluationMode getMode() {
+        return mode;
+    }
+
+    /**
+     * @return the currently selected predicate
+     */
+    public String getPredicate() {
+        return predicatesCombo.getSelectedValue();
+    }
+
+    /**
+     * @return the remove icon instance
+     */
+    public MFXFontIcon getRemoveIcon() {
+        return removeIcon;
+    }
+
+    //================================================================================
+    // Override Methods
+    //================================================================================
+    @Override
+    public String getUserAgentStylesheet() {
+        return STYLESHEET;
+    }
+}

+ 123 - 295
materialfx/src/main/java/io/github/palexdev/materialfx/filter/MFXFilterDialog.java

@@ -1,150 +1,124 @@
-/*
- *     Copyright (C) 2021 Parisi Alessandro
- *     This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
- *
- *     MaterialFX is free software: you can redistribute it and/or modify
- *     it under the terms of the GNU General Public License as published by
- *     the Free Software Foundation, either version 3 of the License, or
- *     (at your option) any later version.
- *
- *     MaterialFX is distributed in the hope that it will be useful,
- *     but WITHOUT ANY WARRANTY; without even the implied warranty of
- *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *     GNU General Public License for more details.
- *
- *     You should have received a copy of the GNU General Public License
- *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
- */
-
 package io.github.palexdev.materialfx.filter;
 
 import io.github.palexdev.materialfx.MFXResourcesLoader;
 import io.github.palexdev.materialfx.controls.*;
-import io.github.palexdev.materialfx.controls.enums.ButtonType;
+import io.github.palexdev.materialfx.controls.cell.MFXListCell;
 import io.github.palexdev.materialfx.controls.enums.Styles;
-import io.github.palexdev.materialfx.controls.factories.MFXAnimationFactory;
+import io.github.palexdev.materialfx.controls.factories.RippleClipTypeFactory;
+import io.github.palexdev.materialfx.effects.DepthLevel;
+import io.github.palexdev.materialfx.effects.ripple.RippleClipType;
 import io.github.palexdev.materialfx.font.MFXFontIcon;
-import io.github.palexdev.materialfx.utils.NodeUtils;
-import io.github.palexdev.materialfx.utils.StringUtils;
-import io.github.palexdev.materialfx.utils.ToggleButtonsUtil;
-import javafx.beans.property.BooleanProperty;
-import javafx.beans.property.SimpleBooleanProperty;
-import javafx.beans.value.ChangeListener;
-import javafx.beans.value.ObservableValue;
+import javafx.beans.binding.Bindings;
 import javafx.collections.FXCollections;
 import javafx.collections.ObservableList;
+import javafx.event.Event;
 import javafx.geometry.Insets;
-import javafx.geometry.Orientation;
 import javafx.geometry.Pos;
-import javafx.scene.Node;
-import javafx.scene.control.Separator;
-import javafx.scene.control.Skin;
-import javafx.scene.control.ToggleGroup;
 import javafx.scene.input.MouseEvent;
 import javafx.scene.layout.HBox;
-import javafx.scene.layout.VBox;
+import javafx.scene.layout.StackPane;
 import javafx.scene.paint.Color;
 
-import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.Map;
-import java.util.function.BiPredicate;
 import java.util.stream.Collectors;
 
 /**
- * This dialog allows implements a filtering mechanism based on boolean
- * expressions on strings.
- * <p>
- * Since {@link MFXStageDialog} can't be extended in this case because the constructors need
- * a {@code MFXDialog}, it extends {@link MFXDialog}, builds the stage dialog passing itself as reference
- * and overrides the open() and close() methods to use the {@link MFXStageDialog} ones.
- * <p></p>
- * <b>Structure</b>
- * <p>
- * A label allows to add a new HBox which contains: an icon to remove itself if not needed anymore,
- * two toggles to specify if the expression should an "AND" or an "OR" condition, a combo box to choose
- * the function to apply on the passed string and the given string, one or more text fields
- * which contain the text used in the evaluations, and a button, {@link #getFilterButton()}.
- * <p>
- * <b>Functioning</b>
- * <p>
- * The main method is {@link #filter(String)}. It takes a string and evaluates all the given conditions
- * on that string, returning the computed boolean result.
+ * This dialog provides a graphical way of filtering a given list of T items based
+ * on the conditions specified by the added evaluation boxes.
+ *
+ * @see MFXEvaluationBox
  */
-public class MFXFilterDialog extends MFXDialog {
+public class MFXFilterDialog<T> extends MFXDialog {
     //================================================================================
     // Properties
     //================================================================================
     private final String STYLE_CLASS = "mfx-filter-dialog";
-    private final String STYLESHEET = MFXResourcesLoader.load("css/mfx-filter-dialog.css");
-
-    private final VBox container;
-    private final VBox textFieldsContainer;
-    private final MFXButton filterButton;
+    private final String STYLESHEET = MFXResourcesLoader.load("css/mfx-filterdialog.css");
 
-    private final MFXStageDialog stage;
     private final MFXIconWrapper closeIcon;
+    private final MFXLabel label;
+    private final MFXButton filterButton;
+    private final MFXButton addAnd;
+    private final MFXButton addOr;
+    private final MFXButton clear;
+    private final ObservableList<MFXEvaluationBox> evaluationBoxes = FXCollections.observableArrayList();
 
     //================================================================================
     // Constructors
     //================================================================================
     public MFXFilterDialog() {
+        setPrefSize(720, 400);
         setTitle("Filter Dialog");
-        setPrefWidth(550);
 
-        Separator s1 = new Separator(Orientation.HORIZONTAL);
-        Separator s2 = new Separator(Orientation.HORIZONTAL);
-        container = new VBox();
-        container.setAlignment(Pos.TOP_CENTER);
-        container.getStylesheets().addAll(STYLESHEET);
+        MFXFontIcon closeFontIcon = new MFXFontIcon("mfx-x-circle", 18, Color.web("#4D4D4D"));
+        closeFontIcon.colorProperty().bind(Bindings.createObjectBinding(
+                () -> closeFontIcon.isHover() ? Color.web("#EF6E6B") : Color.web("#4D4D4D"),
+                closeFontIcon.hoverProperty()
+        ));
 
-        MFXLabel label = new MFXLabel();
+        closeIcon = new MFXIconWrapper(closeFontIcon, 20);
+        closeIcon.setManaged(false);
+
+        label = new MFXLabel();
+        label.setId("headerLabel");
         label.setLabelStyle(Styles.LabelStyles.STYLE2);
-        label.textProperty().bind(title);
-        label.setAlignment(Pos.CENTER);
-        label.setLabelAlignment(Pos.CENTER);
+        label.setPrefSize(USE_COMPUTED_SIZE, 32);
+        label.setMaxSize(Double.MAX_VALUE, USE_PREF_SIZE);
+        label.setPadding(new Insets(10, 0, 0, 0));
+        label.textProperty().bind(titleProperty());
+        label.setLeadingIcon(new MFXFontIcon("mfx-filter-alt", 16));
+        label.setMouseTransparent(true);
         label.getStylesheets().setAll(STYLESHEET);
-        VBox.setMargin(label, new Insets(7, 0, 7, 0));
 
-        MFXIconWrapper add = new MFXIconWrapper(new MFXFontIcon("mfx-search-plus"), 20).defaultRippleGeneratorBehavior();
-        NodeUtils.makeRegionCircular(add);
-        add.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> addTextField());
-        label.setTrailingIcon(add);
-        label.skinProperty().addListener(new ChangeListener<>() {
-            @Override
-            public void changed(ObservableValue<? extends Skin<?>> observable, Skin<?> oldValue, Skin<?> newValue) {
-                if (newValue != null) {
-                    addTextField();
-                    label.skinProperty().removeListener(this);
+        StackPane stackPane = new StackPane();
+        stackPane.setPadding(new Insets(10.0));
+
+        MFXListView<MFXEvaluationBox> listView = new MFXListView<>();
+        listView.setDepthLevel(DepthLevel.LEVEL0);
+        listView.setHideScrollBars(true);
+        listView.setItems(evaluationBoxes);
+        listView.setCellFactory(box -> {
+            MFXListCell<MFXEvaluationBox> cell = new MFXListCell<>() {
+                @Override
+                protected void setupRippleGenerator() {
                 }
-            }
+            };
+            cell.addEventHandler(MouseEvent.MOUSE_PRESSED, Event::consume);
+            cell.setHoverColor(Color.WHITE);
+            cell.setSelectedColor(Color.WHITE);
+            return cell;
         });
 
-        textFieldsContainer = new VBox();
-        MFXScrollPane scrollPane = new MFXScrollPane(textFieldsContainer);
-        scrollPane.setPrefHeight(400);
-        scrollPane.setFitToWidth(true);
-        scrollPane.setStyle("-fx-background-insets: 3");
+        HBox buttonsBox = new HBox(30);
+        buttonsBox.setPrefHeight(60);
+        buttonsBox.setAlignment(Pos.CENTER);
+        buttonsBox.getStylesheets().add(STYLESHEET);
 
         filterButton = new MFXButton("Filter");
-        filterButton.setButtonType(ButtonType.FLAT);
-        filterButton.setMinWidth(80);
-        VBox.setMargin(filterButton, new Insets(5, 0, 5, 0));
+        addAnd = new MFXButton("Add \"AND\"");
+        addOr = new MFXButton("Add \"OR\"");
+        clear = new MFXButton("Clear");
 
-        container.getChildren().addAll(label, s1, scrollPane, s2, filterButton);
+        filterButton.setPrefSize(110, 32);
+        addAnd.setPrefSize(110, 32);
+        addOr.setPrefSize(110, 32);
+        clear.setPrefSize(110, 32);
 
-        setScrimBackground(false);
-        setCenter(container);
+        filterButton.getRippleGenerator().setClipSupplier(() -> new RippleClipTypeFactory(RippleClipType.ROUNDED_RECTANGLE).setArcs(10).build(filterButton));
+        addAnd.getRippleGenerator().setClipSupplier(() -> new RippleClipTypeFactory(RippleClipType.ROUNDED_RECTANGLE).setArcs(10).build(addAnd));
+        addOr.getRippleGenerator().setClipSupplier(() -> new RippleClipTypeFactory(RippleClipType.ROUNDED_RECTANGLE).setArcs(10).build(addOr));
+        clear.getRippleGenerator().setClipSupplier(() -> new RippleClipTypeFactory(RippleClipType.ROUNDED_RECTANGLE).setArcs(10).build(clear));
 
-        initialize();
+        stackPane.getChildren().add(listView);
+        buttonsBox.getChildren().addAll(filterButton, addAnd, addOr, clear);
 
-        stage = new MFXStageDialog(this);
-        stage.setAllowDrag(false);
+        setTop(label);
+        setCenter(stackPane);
+        setBottom(buttonsBox);
 
-        closeIcon = new MFXIconWrapper(new MFXFontIcon("mfx-x"), 40);
-        closeIcon.setManaged(false);
-        closeIcon.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> close());
+        setCloseButtons(closeFontIcon);
         getChildren().add(closeIcon);
+        initialize();
     }
 
     //================================================================================
@@ -152,68 +126,78 @@ public class MFXFilterDialog extends MFXDialog {
     //================================================================================
     private void initialize() {
         getStyleClass().add(STYLE_CLASS);
+        addFilterBox(EvaluationMode.AND);
+        setBehavior();
     }
 
     /**
-     * Adds a new {@code FilterField} to the dialog.
+     * Sets the buttons behavior
      */
-    protected void addTextField() {
-        FilterField filterField = new FilterField();
-        filterField.getRemoveIcon().addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
-            if (textFieldsContainer.getChildren().size() == 1) {
-                return;
-            }
-            textFieldsContainer.getChildren().remove(filterField);
-        });
-        textFieldsContainer.getChildren().add(filterField);
+    private void setBehavior() {
+        addAnd.setOnAction(event -> addFilterBox(EvaluationMode.AND));
+        addOr.setOnAction(event -> addFilterBox(EvaluationMode.OR));
+        clear.setOnAction(event -> evaluationBoxes.clear());
     }
 
     /**
-     * Evaluates all the conditions specified by the dialog's {@code FilterFields} on
-     * the given item and returns if it meets the specified conditions or not.
-     *
-     * @param item the string on which evaluate the conditions
+     * Filters the given list and returns an observable filtered list.
+     * <p></p>
+     * Calls {@link #filter(String)} on each item for filtering.
+     * <p></p>
+     * <b>N.B:</b> The evaluation is done by calling the item's toString method or, if the item implements {@link IFilterable},
+     * by calling {@link IFilterable#toFilterString()}. If the toString method is not overridden
+     * or does not contain any useful information for filtering it won't work.
      */
-    public boolean filter(String item) {
-        List<FilterField> filterFields = textFieldsContainer.getChildren().stream()
-                .filter(node -> node instanceof FilterField)
-                .map(node -> (FilterField) node)
-                .collect(Collectors.toList());
+    public ObservableList<T> filter(List<T> list) {
+        return list.stream()
+                .filter(item -> {
+                    if (item instanceof IFilterable) {
+                        IFilterable fData = (IFilterable) item;
+                        return filter(fData.toFilterString());
+                    } else {
+                        return filter(item.toString());
+                    }
+                })
+                .collect(Collectors.toCollection(FXCollections::observableArrayList));
+    }
 
+    /**
+     * Tests all the evaluation boxes conditions on the given string.
+     */
+    private boolean filter(String filterString) {
         Boolean expression = null;
-        for (FilterField field : filterFields) {
+        for (MFXEvaluationBox box : evaluationBoxes) {
             if (expression == null) {
-                expression = field.callEvaluation(item);
+                expression = box.test(filterString);
                 continue;
             }
 
-            boolean newExpr;
-            if (field.isAnd()) {
-                newExpr = expression && field.callEvaluation(item);
+            boolean tmp;
+            if (box.getMode() == EvaluationMode.AND) {
+                tmp = expression && box.test(filterString);
             } else {
-                newExpr = expression || field.callEvaluation(item);
+                tmp = expression || box.test(filterString);
             }
-            expression = newExpr;
+            expression = tmp;
         }
 
         return expression != null ? expression : false;
     }
 
     /**
-     * Returns the dialog's button reference.
-     * <p>
-     * In {@link io.github.palexdev.materialfx.skins.MFXTableViewSkin} for example, it is used
-     * to add an event handler to the button which starts the filtering and closes the dialog.
+     * Adds a new {@link MFXEvaluationBox} with the specified {@link EvaluationMode} to the dialog.
      */
-    public MFXButton getFilterButton() {
-        return filterButton;
+    private void addFilterBox(EvaluationMode mode) {
+        MFXEvaluationBox evaluationBox = new MFXEvaluationBox(mode);
+        evaluationBox.getRemoveIcon().addEventFilter(MouseEvent.MOUSE_PRESSED, event -> evaluationBoxes.remove(evaluationBox));
+        evaluationBoxes.add(evaluationBox);
     }
 
     /**
-     * @return the stage dialog reference.
+     * @return the filter button instance
      */
-    public MFXStageDialog getStage() {
-        return stage;
+    public MFXButton getFilterButton() {
+        return filterButton;
     }
 
     //================================================================================
@@ -228,165 +212,9 @@ public class MFXFilterDialog extends MFXDialog {
     protected void layoutChildren() {
         super.layoutChildren();
 
-        closeIcon.relocate(getWidth() - 17, 17);
-    }
-
-    /**
-     * Shows the stage dialog.
-     */
-    @Override
-    public void show() {
-        stage.show();
-    }
-
-    /**
-     * Closes the stage dialog.
-     */
-    @Override
-    public void close() {
-        stage.close();
-    }
-
-    /**
-     * This is the class used in the filter dialog for the filter boxes.
-     * <p>
-     * It's this node which contains the remove icon, the toggles, the combo box and the text field.
-     * <p>
-     * It uses a {@code Map<String, BiPredicate<String, String>>} to map the combo box choice to the function
-     * which will we applied on the given strings, {@link MFXFilterDialog#filter(String)}.
-     */
-    private class FilterField extends HBox {
-        //================================================================================
-        // Properties
-        //================================================================================
-        private final Map<String, BiPredicate<String, String>> evaluators = new LinkedHashMap<>();
-
-        private final MFXIconWrapper icon;
-        private final MFXTextField textField;
-        private final MFXComboBox<String> evaluationCombo;
-
-        private final BooleanProperty isAnd = new SimpleBooleanProperty(true);
-
-        //================================================================================
-        // Constructors
-        //================================================================================
-        public FilterField() {
-            populateMap();
-            getStylesheets().addAll(STYLESHEET);
-
-            setAlignment(Pos.CENTER);
-            setSpacing(5);
-            setPadding(new Insets(5));
-
-            MFXFontIcon minus = new MFXFontIcon("mfx-minus");
-            icon = new MFXIconWrapper(minus, 12);
-            icon.setOpacity(0.0);
-
-            textField = new MFXTextField();
-            textField.getStyleClass().add("text-filter");
-            textField.setPromptText("String filter...");
-            textField.setAlignment(Pos.CENTER);
-            textField.setLineColor(Color.rgb(82, 0, 237));
-
-            evaluationCombo = new MFXComboBox<>();
-
-            //box.setMinWidth(width);
-            hoverProperty().addListener((observable, oldValue, newValue) -> {
-                if (newValue) {
-                    MFXAnimationFactory.FADE_IN.build(icon, 300).play();
-                } else {
-                    MFXAnimationFactory.FADE_OUT.build(icon, 300).play();
-                }
-            });
-
-            getChildren().addAll(icon, buildOptions(), textField);
-        }
-
-        //================================================================================
-        // Methods
-        //================================================================================
-        private Node buildOptions() {
-            HBox box = new HBox(5);
-
-            MFXToggleButton and = new MFXToggleButton("And");
-            and.setSelected(true);
-            and.setAutomaticColorAdjustment(true);
-            and.setToggleColor(Color.rgb(82, 0, 237));
-
-            MFXToggleButton or = new MFXToggleButton("Or");
-            or.setAutomaticColorAdjustment(true);
-            or.setToggleColor(Color.rgb(82, 0, 237));
-
-            ToggleGroup group = new ToggleGroup();
-            and.setToggleGroup(group);
-            or.setToggleGroup(group);
-            ToggleButtonsUtil.addAlwaysOneSelectedSupport(group);
-
-            isAnd.bind(and.selectedProperty());
-
-            ObservableList<String> evaluatorsKeys = FXCollections.observableArrayList(evaluators.keySet());
-            evaluationCombo.setItems(evaluatorsKeys);
-            evaluationCombo.setMinWidth(150);
-            evaluationCombo.setComboStyle(Styles.ComboBoxStyles.STYLE2);
-            evaluationCombo.setMaxPopupHeight(-1);
-            evaluationCombo.skinProperty().addListener(new ChangeListener<>() {
-                @Override
-                public void changed(ObservableValue<? extends Skin<?>> observable, Skin<?> oldValue, Skin<?> newValue) {
-                    if (newValue != null) {
-                        evaluationCombo.getSelectionModel().selectFirst();
-                        evaluationCombo.skinProperty().removeListener(this);
-                    }
-                }
-            });
-            HBox.setMargin(evaluationCombo, new Insets(10, 10, 0, 0));
-
-            box.getChildren().addAll(and, or, evaluationCombo);
-            return box;
-        }
-
-        /**
-         * Populates the map with:
-         * <p>
-         * - Contains -> String::contains <p>
-         * - Contains Ignore Case -> StringUtils::containsIgnoreCase <p>
-         * - Starts With -> String::startsWith <p>
-         * - Ends With -> String::endsWith <p>
-         * - Equals -> String::equals <p>
-         * - Equals Ignore Case -> String::equalsIgnoreCase <p>
-         *
-         * @see StringUtils
-         */
-        private void populateMap() {
-            evaluators.put("Contains", String::contains);
-            evaluators.put("Contains Ignore Case", StringUtils::containsIgnoreCase);
-            evaluators.put("Starts With", String::startsWith);
-            evaluators.put("Ends With", String::endsWith);
-            evaluators.put("Equals", String::equals);
-            evaluators.put("Equals Ignore Case", String::equalsIgnoreCase);
-        }
-
-        public MFXIconWrapper getRemoveIcon() {
-            return icon;
-        }
-
-        /**
-         * Retrieves the BiPredicate from the map with the combo box selected value as key and
-         * applies the function to the given string.
-         */
-        public Boolean callEvaluation(String item) {
-            if (evaluationCombo.getSelectedValue() != null) {
-                return evaluators.get(evaluationCombo.getSelectedValue()).test(item, textField.getText());
-            }
-            return false;
-        }
-
-        /**
-         * Returns whether the selected toggle is the "And" toggle,
-         * if it is false then the selected toggle is the "Or" toggle because
-         * the toggles are in a group which uses {@link ToggleButtonsUtil#addAlwaysOneSelectedSupport(ToggleGroup)}
-         */
-        public boolean isAnd() {
-            return isAnd.get();
-        }
+        double ciSize = closeIcon.getSize();
+        double ciX = snapPositionX(getWidth() - 27);
+        double ciY = snapPositionY(ciSize / 2.0);
+        closeIcon.resizeRelocate(ciX, ciY, ciSize, ciSize);
     }
 }

+ 70 - 65
materialfx/src/main/java/io/github/palexdev/materialfx/font/FontResources.java

@@ -19,73 +19,78 @@
 package io.github.palexdev.materialfx.font;
 
 /**
- * Enumerator class for MaterialFX font resources.
+ * Enumerator class for MaterialFX font resources. (Count: 69)
  */
 public enum FontResources {
-    ANGLE_DOWN("mfx-angle-down", '\uE91b'),
-    ANGLE_LEFT("mfx-angle-left", '\uE91c'),
-    ANGLE_RIGHT("mfx-angle-right", '\uE91d'),
-    ANGLE_UP("mfx-angle-up", '\uE91e'),
-    ARROW_BACK("mfx-arrow-back", '\uE925'),
-    ARROW_FORWARD("mfx-arrow-forward", '\uE926'),
-    CALENDAR_BLACK("mfx-calendar-black", '\uE904'),
-    CALENDAR_SEMI_BLACK("mfx-calendar-semi-black", '\uE905'),
-    CALENDAR_WHITE("mfx-calendar-white", '\uE906'),
-    CARET_DOWN("mfx-caret-down", '\uE91f'),
-    CARET_LEFT("mfx-caret-left", '\uE920'),
-    CARET_RIGHT("mfx-caret-right", '\uE921'),
-    CARET_UP("mfx-caret-up", '\uE922'),
-    CASPIAN_MARK("mfx-caspian-mark", '\uE90b'),
-    CHART_PIE("mfx-chart-pie", '\uE934'),
-    CHECK_CIRCLE("mfx-check-circle", '\uE92f'),
-    CHEVRON_DOWN("mfx-chevron-down", '\uE902'),
-    CHEVRON_LEFT("mfx-chevron-left", '\uE903'),
-    CHEVRON_RIGHT("mfx-chevron-right", '\uE907'),
-    CHEVRON_UP("mfx-chevron-up", '\uE908'),
-    CIRCLE("mfx-circle", '\uE909'),
-    CONTENT_COPY("mfx-content-copy", '\uE935'),
-    DASHBOARD("mfx-dashboard", '\uE930'),
-    DEBUG("mfx-debug", '\uE93e'),
-    EXCLAMATION_CIRCLE("mfx-exclamation-circle", '\uE917'),
-    EXCLAMATION_TRIANGLE("mfx-exclamation-triangle", '\uE918'),
-    EXPAND("mfx-expand", '\uE919'),
-    FILTER("mfx-filter", '\uE929'),
-    FILTER_CLEAR("mfx-filter-clear", '\uE92b'),
-    FIRST_PAGE("mfx-first-page", '\uE927'),
-    GEAR("mfx-gear", '\uE900'),
-    GOOGLE("mfx-google", '\uE90a'),
-    GOOGLE_DRIVE("mfx-google-drive", '\uE931'),
-    HOME("mfx-home", '\uE932'),
-    INFO("mfx-info", '\uE933'),
-    INFO_CIRCLE("mfx-info-circle", '\uE91a'),
-    LAST_PAGE("mfx-last-page", '\uE928'),
-    LEVEL_UP("mfx-level-up", '\uE936'),
-    MINUS("mfx-minus", '\uE901'),
-    MINUS_CIRCLE("mfx-minus-circle", '\uE90c'),
-    MODENA_MARK("mfx-modena-mark", '\uE90d'),
-    SEARCH("mfx-search", '\uE92e'),
-    SEARCH_PLUS("mfx-search-plus", '\uE92a'),
-    SLIDERS("mfx-sliders", '\uE93f'),
-    STEP_BACKWARD("mfx-step-backward", '\uE923'),
-    STEP_FORWARD("mfx-step-forward", '\uE924'),
-    SYNC("mfx-sync", '\uE937'),
-    SYNC_LIGHT("mfx-sync-light", '\uE938'),
-    USER("mfx-user", '\uE92c'),
-    USERS("mfx-users", '\uE92d'),
-    VARIANT3_MARK("mfx-variant3-mark", '\uE90f'),
-    VARIANT4_MARK("mfx-variant4-mark", '\uE90e'),
-    VARIANT5_MARK("mfx-variant5-mark", '\uE911'),
-    VARIANT6_MARK("mfx-variant6-mark", '\uE910'),
-    VARIANT7_MARK("mfx-variant7-mark", '\uE912'),
-    VARIANT8_MARK("mfx-variant8-mark", '\uE93a'),
-    VARIANT9_MARK("mfx-variant9-mark", '\uE913'),
-    VARIANT10_MARK("mfx-variant10-mark", '\uE93b'),
-    VARIANT11_MARK("mfx-variant11-mark", '\uE93c'),
-    VARIANT12_MARK("mfx-variant12-mark", '\uE914'),
-    X("mfx-x", '\uE916'),
-    X_ALT("mfx-x-alt", '\uE93d'),
-    X_CIRCLE("mfx-x-circle", '\uE915'),
-    X_CIRCLE_LIGHT("mfx-x-circle-light", '\uE939'),
+    ANGLE_DOWN("mfx-angle-down", '\uE900'),
+    ANGLE_LEFT("mfx-angle-left", '\uE901'),
+    ANGLE_RIGHT("mfx-angle-right", '\uE902'),
+    ANGLE_UP("mfx-angle-up", '\uE903'),
+    ARROW_BACK("mfx-arrow-back", '\uE904'),
+    ARROW_FORWARD("mfx-arrow-forward", '\uE905'),
+    CALENDAR_BLACK("mfx-calendar-black", '\uE906'),
+    CALENDAR_SEMI_BLACK("mfx-calendar-semi-black", '\uE907'),
+    CALENDAR_WHITE("mfx-calendar-white", '\uE908'),
+    CARET_DOWN("mfx-caret-down", '\uE909'),
+    CARET_LEFT("mfx-caret-left", '\uE90A'),
+    CARET_RIGHT("mfx-caret-right", '\uE90B'),
+    CARET_UP("mfx-caret-up", '\uE90C'),
+    CASPIAN_MARK("mfx-caspian-mark", '\uE90D'),
+    CHART_PIE("mfx-chart-pie", '\uE90E'),
+    CHECK_CIRCLE("mfx-check-circle", '\uE90F'),
+    CHEVRON_DOWN("mfx-chevron-down", '\uE910'),
+    CHEVRON_LEFT("mfx-chevron-left", '\uE911'),
+    CHEVRON_RIGHT("mfx-chevron-right", '\uE912'),
+    CHEVRON_UP("mfx-chevron-up", '\uE913'),
+    CIRCLE("mfx-circle", '\uE914'),
+    CONTENT_COPY("mfx-content-copy", '\uE915'),
+    DASHBOARD("mfx-dashboard", '\uE916'),
+    DEBUG("mfx-debug", '\uE917'),
+    EXCLAMATION_CIRCLE("mfx-exclamation-circle", '\uE918'),
+    EXCLAMATION_TRIANGLE("mfx-exclamation-triangle", '\uE919'),
+    EXPAND("mfx-expand", '\uE91A'),
+    EYE("mfx-eye", '\uE91B'),
+    EYE_SLASH("mfx-eye-slash", '\uE91C'),
+    FILTER("mfx-filter", '\uE91D'),
+    FILTER_ALT("mfx-filter-alt", '\uE91E'),
+    FILTER_ALT_CLEAR("mfx-filter-alt-clear", '\uE91F'),
+    FIRST_PAGE("mfx-first-page", '\uE920'),
+    GEAR("mfx-gear", '\uE921'),
+    GOOGLE("mfx-google", '\uE922'),
+    GOOGLE_DRIVE("mfx-google-drive", '\uE923'),
+    HOME("mfx-home", '\uE924'),
+    INFO("mfx-info", '\uE925'),
+    INFO_CIRCLE("mfx-info-circle", '\uE926'),
+    LAST_PAGE("mfx-last-page", '\uE927'),
+    LEVEL_UP("mfx-level-up", '\uE928'),
+    LOCK("mfx-lock", '\uE929'),
+    LOCK_OPEN("mfx-lock-open", '\uE92A'),
+    MINUS("mfx-minus", '\uE92B'),
+    MINUS_CIRCLE("mfx-minus-circle", '\uE92C'),
+    MODENA_MARK("mfx-modena-mark", '\uE92D'),
+    SEARCH("mfx-search", '\uE92E'),
+    SEARCH_PLUS("mfx-search-plus", '\uE92F'),
+    SLIDERS("mfx-sliders", '\uE930'),
+    STEP_BACKWARD("mfx-step-backward", '\uE931'),
+    STEP_FORWARD("mfx-step-forward", '\uE932'),
+    SYNC("mfx-sync", '\uE933'),
+    SYNC_LIGHT("mfx-sync-light", '\uE934'),
+    USER("mfx-user", '\uE935'),
+    USERS("mfx-users", '\uE936'),
+    VARIANT10_MARK("mfx-variant10-mark", '\uE937'),
+    VARIANT11_MARK("mfx-variant11-mark", '\uE938'),
+    VARIANT12_MARK("mfx-variant12-mark", '\uE939'),
+    VARIANT3_MARK("mfx-variant3-mark", '\uE93A'),
+    VARIANT4_MARK("mfx-variant4-mark", '\uE93B'),
+    VARIANT5_MARK("mfx-variant5-mark", '\uE93C'),
+    VARIANT6_MARK("mfx-variant6-mark", '\uE93D'),
+    VARIANT7_MARK("mfx-variant7-mark", '\uE93E'),
+    VARIANT8_MARK("mfx-variant8-mark", '\uE93F'),
+    VARIANT9_MARK("mfx-variant9-mark", '\uE940'),
+    X("mfx-x", '\uE941'),
+    X_ALT("mfx-x-alt", '\uE942'),
+    X_CIRCLE("mfx-x-circle", '\uE943'),
+    X_CIRCLE_LIGHT("mfx-x-circle-light", '\uE944'),
     ;
 
     public static FontResources findByDescription(String description) {

+ 23 - 7
materialfx/src/main/java/io/github/palexdev/materialfx/font/MFXFontIcon.java

@@ -90,17 +90,24 @@ public class MFXFontIcon extends Text {
     }
 
     /**
-     * Specifies the icon code of the icon.
+     * @return a new MFXFontIcon with a random icon, the specified size and color.
      */
+    public static MFXFontIcon getRandomIcon(double size, Color color) {
+        FontResources[] resources = FontResources.values();
+        int random = (int) (Math.random() * resources.length);
+        String desc = resources[random].getDescription();
+        return new MFXFontIcon(desc, size, color);
+    }
+
+    //================================================================================
+    // Styleable Properties
+    //================================================================================
     private final StyleableStringProperty description = new SimpleStyleableStringProperty(
             StyleableProperties.DESCRIPTION,
             this,
             "description"
     );
 
-    /**
-     * Specifies the size of the icon.
-     */
     private final StyleableDoubleProperty size = new SimpleStyleableDoubleProperty(
             StyleableProperties.SIZE,
             this,
@@ -108,9 +115,6 @@ public class MFXFontIcon extends Text {
             10.0
     );
 
-    /**
-     * Specifies the color of the icon.
-     */
     private final StyleableObjectProperty<Paint> color = new SimpleStyleableObjectProperty<>(
             StyleableProperties.COLOR,
             this,
@@ -122,6 +126,9 @@ public class MFXFontIcon extends Text {
         return description.get();
     }
 
+    /**
+     * Specifies the icon's code.
+     */
     public StyleableStringProperty descriptionProperty() {
         return description;
     }
@@ -134,6 +141,9 @@ public class MFXFontIcon extends Text {
         return size.get();
     }
 
+    /**
+     * Specifies the size of the icon.
+     */
     public StyleableDoubleProperty sizeProperty() {
         return size;
     }
@@ -146,6 +156,9 @@ public class MFXFontIcon extends Text {
         return color.get();
     }
 
+    /**
+     * Specifies the color of the icon.
+     */
     public StyleableObjectProperty<Paint> colorProperty() {
         return color;
     }
@@ -189,6 +202,9 @@ public class MFXFontIcon extends Text {
         return StyleableProperties.cssMetaDataList;
     }
 
+    //================================================================================
+    // Override Methods
+    //================================================================================
     @Override
     public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
         return MFXFontIcon.getClassCssMetaDataList();

+ 35 - 13
materialfx/src/main/java/io/github/palexdev/materialfx/selection/ComboSelectionModelMock.java

@@ -55,8 +55,8 @@ public class ComboSelectionModelMock<T> {
      * Clears the selection.
      */
     public void clearSelection() {
-        selectedItem.set(null);
-        selectedIndex.set(-1);
+        setSelectItem(null);
+        setSelectIndex(-1);
     }
 
     /**
@@ -66,15 +66,20 @@ public class ComboSelectionModelMock<T> {
         if (!comboBox.getItems().contains(item)) {
             return;
         }
-        selectedIndex.set(comboBox.getItems().indexOf(item));
-        selectedItem.set(item);
+        setSelectIndex(comboBox.getItems().indexOf(item));
+        setSelectItem(item);
     }
 
     /**
      * Selects the first item in the combo items list.
      */
     public void selectFirst() {
-        selectedIndex.set(0);
+        if (comboBox.getItems().isEmpty()) {
+            return;
+        }
+
+        setSelectIndex(0);
+        setSelectItem(comboBox.getItems().get(0));
     }
 
     /**
@@ -84,24 +89,33 @@ public class ComboSelectionModelMock<T> {
         if (getSelectedIndex() == (comboBox.getItems().size() - 1)) {
             return;
         }
-        selectedIndex.add(1);
+
+        setSelectIndex(getSelectedIndex() + 1);
+        setSelectItem(comboBox.getItems().get(getSelectedIndex()));
     }
 
     /**
-     * Selects the last item in the combo items list.
+     * Selects the previous item in the combo items list.
      */
-    public void selectLast() {
-        selectedIndex.set(comboBox.getItems().size());
+    public void selectPrevious() {
+        if (getSelectedIndex() <= 0) {
+            return;
+        }
+
+        setSelectIndex(getSelectedIndex() - 1);
+        setSelectItem(comboBox.getItems().get(getSelectedIndex()));
     }
 
     /**
-     * Selects the previous item in the combo items list.
+     * Selects the last item in the combo items list.
      */
-    public void selectPrevious() {
-        if (getSelectedIndex() == -1) {
+    public void selectLast() {
+        if (comboBox.getItems().isEmpty()) {
             return;
         }
-        selectedIndex.subtract(1);
+
+        setSelectIndex(comboBox.getItems().size() - 1);
+        setSelectItem(comboBox.getItems().get(comboBox.getItems().size() - 1));
     }
 
     /**
@@ -118,6 +132,10 @@ public class ComboSelectionModelMock<T> {
         return selectedIndex.getReadOnlyProperty();
     }
 
+    private void setSelectIndex(int index) {
+        selectedIndex.set(index);
+    }
+
     /**
      * Returns the current selected item.
      */
@@ -131,4 +149,8 @@ public class ComboSelectionModelMock<T> {
     public ReadOnlyObjectProperty<T> selectedItemProperty() {
         return selectedItem.getReadOnlyProperty();
     }
+
+    public void setSelectItem(T item) {
+        selectedItem.set(item);
+    }
 }

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/selection/ListSelectionModel.java

@@ -46,7 +46,7 @@ public class ListSelectionModel<T> implements IListSelectionModel<T> {
     //================================================================================
 
     /**
-     * This method is called when the mouse event passed to {@link #select(int, Object, MouseEvent)}
+     * This method is called when the mouse event passed to {@link #select(int, T, MouseEvent)}
      * is null. Since it's null there's no check for isShiftDown() or isControlDown(), so in case
      * of multiple selection enabled the passed index and data will always be added to the map.
      */

+ 140 - 79
materialfx/src/main/java/io/github/palexdev/materialfx/selection/TableSelectionModel.java

@@ -2,10 +2,12 @@ package io.github.palexdev.materialfx.selection;
 
 import io.github.palexdev.materialfx.controls.MFXTableRow;
 import io.github.palexdev.materialfx.selection.base.ITableSelectionModel;
-import javafx.beans.property.ListProperty;
-import javafx.beans.property.SimpleListProperty;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.MapProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleMapProperty;
 import javafx.collections.FXCollections;
-import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableMap;
 import javafx.scene.input.MouseEvent;
 
 import java.util.ArrayList;
@@ -14,133 +16,173 @@ import java.util.List;
 /**
  * Concrete implementation of the {@code ITableSelectionModel} interface.
  * <p>
- * Basic selection model, allows to: clear the selection, single and multiple selection of {@link MFXTableRow}s.
+ * Basic selection model, allows to: clear the selection, single and multiple selection for {@link MFXTableRow}s data.
  */
 public class TableSelectionModel<T> implements ITableSelectionModel<T> {
     //================================================================================
     // Properties
     //================================================================================
-    private final ListProperty<MFXTableRow<T>> selectedItems = new SimpleListProperty<>(FXCollections.observableArrayList());
+    private final MapProperty<Integer, T> selectedItems = new SimpleMapProperty<>(getMap());
+    private final BooleanProperty updating = new SimpleBooleanProperty();
     private boolean allowsMultipleSelection = false;
 
     //================================================================================
-    // Constructors
+    // Methods
     //================================================================================
-    public TableSelectionModel() {
-        selectedItems.addListener((ListChangeListener<MFXTableRow<T>>) change -> {
-            List<MFXTableRow<T>> tmpRemoved = new ArrayList<>();
-            List<MFXTableRow<T>> tmpAdded = new ArrayList<>();
 
-            while (change.next()) {
-                tmpRemoved.addAll(change.getRemoved());
-                tmpAdded.addAll(change.getAddedSubList());
-            }
-            tmpRemoved.forEach(item -> item.setSelected(false));
-            tmpAdded.forEach(item -> item.setSelected(true));
-        });
+    /**
+     * Builds a new observable hash map.
+     */
+    protected ObservableMap<Integer, T> getMap() {
+        return FXCollections.observableHashMap();
     }
 
-    //================================================================================
-    // Methods
-    //================================================================================
-
     /**
-     * This method is called when the mouseEvent argument passed to
-     * {@link #select(MFXTableRow, MouseEvent)} is null.
-     * <p>
-     * If the model is set to not allow multiple selection then we clear the list
-     * and then add the item to it.
-     *
-     * @param row the row to select
-     */
-    @SuppressWarnings("unchecked")
-    protected void select(MFXTableRow<T> row) {
-        if (!allowsMultipleSelection) {
-            selectedItems.setAll(row);
+     * This method is called when the mouse event passed to {@link #select(int, T, MouseEvent)}
+     * is null. Since it's null there's no check for isShiftDown() or isControlDown(), so in case
+     * of multiple selection enabled the passed index and data will always be added to the map.
+     */
+    private void select(int index, T data) {
+        if (allowsMultipleSelection) {
+            selectedItems.put(index, data);
         } else {
-            selectedItems.add(row);
+            ObservableMap<Integer, T> tmpMap = getMap();
+            tmpMap.put(index, data);
+            selectedItems.set(tmpMap);
         }
     }
 
     //================================================================================
-    // Methods Implementation
+    // Override Methods
     //================================================================================
 
     /**
-     * This method is called by {@link io.github.palexdev.materialfx.skins.MFXTableViewSkin} when
-     * the mouse is pressed on a row. We need the mouse event as a parameter in case multiple selection is
-     * allowed because we need to check if the Shift key or Ctrl key were pressed.
+     * Checks if the map contains the given item.
+     */
+    @Override
+    public boolean containsSelected(T data) {
+        return selectedItems.containsValue(data);
+    }
+
+    /**
+     * Checks if the map contains the given index key.
+     */
+    @Override
+    public boolean containSelected(int index) {
+        return selectedItems.containsKey(index);
+    }
+
+    /**
+     * Called by the rows when the mouse is pressed.
+     * The mouse event is needed in case of multiple selection allowed because
+     * we check if the Shift key or Ctrl key were pressed.
      * <p>
-     * If the mouseEvent is null we call the other {@link #select(MFXTableRow)} method.
+     * If the mouseEvent is null we call the other {@link #select(int, T)} method.
      * <p>
-     * If the selection is single {@link #clearSelection()} we clear the selection
-     * and add the new selected item to the list.
+     * If the selection is multiple and Shift or Ctrl are pressed the new entry
+     * is put in the map.
      * <p>
-     * If the selection is multiple we check if the item was already selected,
-     * if that is the case by default the item is deselected.
+     * If the selection is single the map is replaced by a new one that contains only the
+     * passed entry.
      * <p>
-     * In case neither Shift nor Ctrl are pressed we clear the selection.
+     * Note that if the item is already selected it is removed from the map, this behavior though is
+     * managed by the rows.
      */
-    @SuppressWarnings("unchecked")
     @Override
-    public void select(MFXTableRow<T> row, MouseEvent mouseEvent) {
+    public void select(int index, T data, MouseEvent mouseEvent) {
         if (mouseEvent == null) {
-            select(row);
+            select(index, data);
             return;
         }
 
-        if (!allowsMultipleSelection) {
-            clearSelection();
-            selectedItems.setAll(row);
-            return;
+        if (allowsMultipleSelection && (mouseEvent.isShiftDown() || mouseEvent.isControlDown())) {
+            selectedItems.put(index, data);
+        } else {
+            ObservableMap<Integer, T> tmpMap = getMap();
+            tmpMap.put(index, data);
+            selectedItems.set(tmpMap);
         }
+    }
+
+    /**
+     * Removes the mapping for the given index.
+     */
+    @Override
+    public void clearSelectedItem(int index) {
+        selectedItems.remove(index);
+    }
 
+    /**
+     * Retrieves the index for the given data, if preset
+     * removes the mapping for that index.
+     */
+    @Override
+    public void clearSelectedItem(T item) {
+        selectedItems.entrySet().stream()
+                .filter(entry -> entry.getValue().equals(item))
+                .findFirst()
+                .ifPresent(entry -> selectedItems.remove(entry.getKey()));
 
-        if (mouseEvent.isShiftDown() || mouseEvent.isControlDown()) {
-            if (row.isSelected()) {
-                selectedItems.remove(row);
-            } else {
-                selectedItems.add(row);
-            }
-        } else {
-            clearSelection();
-            selectedItems.setAll(row);
-        }
     }
 
     /**
-     * Resets every item in the list to selected false and then clears the list.
+     * Removes all the entries from the map.
      */
     @Override
     public void clearSelection() {
-        if (selectedItems.isEmpty()) {
-            return;
-        }
+        selectedItems.set(getMap());
+    }
 
-        selectedItems.forEach(item -> item.setSelected(false));
-        selectedItems.clear();
+    /**
+     * @return the currently selected index, 0 if more than one item is selected,
+     * -1 if no item is selected
+     */
+    @Override
+    public int getSelectedIndex() {
+        List<Integer> keys = new ArrayList<>(selectedItems.keySet());
+        return !keys.isEmpty() ? keys.get(0) : -1;
     }
 
     /**
-     * Gets the selected row. If the selection is multiple {@link #getSelectedRows()} ()} should be
-     * called instead, as this method will only return the first item of the list.
-     *
-     * @return the first selected item of the list
+     * @return an unmodifiable list containing the currently selected indexes
      */
     @Override
-    public MFXTableRow<T> getSelectedRow() {
-        if (selectedItems.isEmpty()) {
-            return null;
-        }
-        return selectedItems.get(0);
+    public List<Integer> getSelectedIndexes() {
+        return List.copyOf(selectedItems.keySet());
+    }
+
+    /**
+     * @return the first selected item in the map
+     */
+    @Override
+    public T getSelectedItem() {
+        return getSelectedItem(0);
+    }
+
+    /**
+     * @return the selected item in the map with the given index or null
+     * if not found
+     */
+    @Override
+    public T getSelectedItem(int index) {
+        List<T> items = new ArrayList<>(selectedItems.values());
+        return items.size() > index ? items.get(index) : null;
     }
 
     /**
-     * @return the ListProperty which contains all the selected items.
+     * @return an unmodifiable list of all the selected items
      */
     @Override
-    public ListProperty<MFXTableRow<T>> getSelectedRows() {
-        return this.selectedItems;
+    public List<T> getSelectedItems() {
+        return List.copyOf(selectedItems.values());
+    }
+
+    /**
+     * @return the map property used for the selection
+     */
+    @Override
+    public MapProperty<Integer, T> selectedItemsProperty() {
+        return selectedItems;
     }
 
     /**
@@ -158,4 +200,23 @@ public class TableSelectionModel<T> implements ITableSelectionModel<T> {
     public void setAllowsMultipleSelection(boolean multipleSelection) {
         this.allowsMultipleSelection = multipleSelection;
     }
+
+    @Override
+    public boolean isUpdating() {
+        return updating.get();
+    }
+
+    /**
+     * Specifies if the model is being updated by the table view after a change
+     * in the items observable list.
+     */
+    @Override
+    public BooleanProperty updatingProperty() {
+        return updating;
+    }
+
+    @Override
+    public void setUpdating(boolean updating) {
+        this.updating.set(updating);
+    }
 }

+ 20 - 24
materialfx/src/main/java/io/github/palexdev/materialfx/selection/base/ITableSelectionModel.java

@@ -1,35 +1,31 @@
-/*
- *     Copyright (C) 2021 Parisi Alessandro
- *     This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
- *
- *     MaterialFX is free software: you can redistribute it and/or modify
- *     it under the terms of the GNU General Public License as published by
- *     the Free Software Foundation, either version 3 of the License, or
- *     (at your option) any later version.
- *
- *     MaterialFX is distributed in the hope that it will be useful,
- *     but WITHOUT ANY WARRANTY; without even the implied warranty of
- *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *     GNU General Public License for more details.
- *
- *     You should have received a copy of the GNU General Public License
- *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
- */
-
 package io.github.palexdev.materialfx.selection.base;
 
-import io.github.palexdev.materialfx.controls.MFXTableRow;
-import javafx.beans.property.ListProperty;
+import io.github.palexdev.materialfx.controls.MFXTableView;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.MapProperty;
 import javafx.scene.input.MouseEvent;
 
+import java.util.List;
+
 /**
- * Public API used by any {@code MFXTableView}.
+ * Public API for selection used by any {@link MFXTableView}.
  */
 public interface ITableSelectionModel<T> {
-    void select(MFXTableRow<T> row, MouseEvent mouseEvent);
+    boolean containsSelected(T data);
+    boolean containSelected(int index);
+    void select(int index, T data, MouseEvent mouseEvent);
+    void clearSelectedItem(int index);
+    void clearSelectedItem(T item);
     void clearSelection();
-    MFXTableRow<T> getSelectedRow();
-    ListProperty<MFXTableRow<T>> getSelectedRows();
+    int getSelectedIndex();
+    List<Integer> getSelectedIndexes();
+    T getSelectedItem();
+    T getSelectedItem(int index);
+    List<T> getSelectedItems();
+    MapProperty<Integer, T> selectedItemsProperty();
     boolean allowsMultipleSelection();
     void setAllowsMultipleSelection(boolean multipleSelection);
+    boolean isUpdating();
+    BooleanProperty updatingProperty();
+    void setUpdating(boolean updating);
 }

+ 219 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXCircleToggleNodeSkin.java

@@ -0,0 +1,219 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
+ *
+ *     MaterialFX is free software: you can redistribute it and/or modify
+ *     it under the terms of the GNU General Public License as published by
+ *     the Free Software Foundation, either version 3 of the License, or
+ *     (at your option) any later version.
+ *
+ *     MaterialFX is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *     GNU General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.skins;
+
+import io.github.palexdev.materialfx.controls.MFXCircleToggleNode;
+import io.github.palexdev.materialfx.controls.MFXLabel;
+import io.github.palexdev.materialfx.controls.enums.TextPosition;
+import io.github.palexdev.materialfx.effects.ripple.MFXCircleRippleGenerator;
+import io.github.palexdev.materialfx.effects.ripple.RipplePosition;
+import io.github.palexdev.materialfx.utils.LabelUtils;
+import io.github.palexdev.materialfx.utils.NodeUtils;
+import javafx.beans.binding.Bindings;
+import javafx.scene.Node;
+import javafx.scene.control.SkinBase;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.StackPane;
+import javafx.scene.shape.Circle;
+
+/**
+ * This is the default skin for every {@link MFXCircleToggleNode}.
+ * <p></p>
+ * The base container is a {@link StackPane} which contains: a {@link Circle} that represents
+ * the toggle, a {@link MFXLabel} to show the toggle's text.
+ * <p></p>
+ * Includes a {@link MFXCircleRippleGenerator} to generate ripple effects on mouse pressed.
+ */
+public class MFXCircleToggleNodeSkin extends SkinBase<MFXCircleToggleNode> {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private final StackPane container;
+    private final Circle circle;
+    private final MFXLabel label;
+    private final MFXCircleRippleGenerator rippleGenerator;
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXCircleToggleNodeSkin(MFXCircleToggleNode toggleNode) {
+        super(toggleNode);
+
+        circle = new Circle();
+        circle.setId("circle");
+        circle.radiusProperty().bind(toggleNode.sizeProperty());
+        circle.strokeWidthProperty().bind(toggleNode.strokeWidthProperty());
+
+        label = new MFXLabel();
+        label.setPromptText("");
+        label.setId("textNode");
+        label.setManaged(false);
+        label.textProperty().bind(toggleNode.textProperty());
+        label.setLeadingIcon(toggleNode.getLabelLeadingIcon());
+        label.setTrailingIcon(toggleNode.getLabelTrailingIcon());
+        label.getStylesheets().setAll(toggleNode.getUserAgentStylesheet());
+
+        container = new StackPane();
+        container.getStyleClass().setAll("container");
+        container.setPrefSize(Region.USE_COMPUTED_SIZE, Region.USE_COMPUTED_SIZE);
+
+        rippleGenerator = new MFXCircleRippleGenerator(container);
+        rippleGenerator.setMouseTransparent(true);
+
+        if (toggleNode.getGraphic() != null) {
+            toggleNode.getGraphic().setMouseTransparent(true);
+            container.getChildren().setAll(circle, rippleGenerator, label, toggleNode.getGraphic());
+        } else {
+            container.getChildren().setAll(circle, rippleGenerator, label);
+        }
+
+        setupRippleGenerator();
+        setListeners();
+        getChildren().setAll(container);
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+
+    /**
+     * Sets up the ripple generator.
+     */
+    protected void setupRippleGenerator() {
+        rippleGenerator.setAnimateBackground(false);
+        rippleGenerator.setAnimationSpeed(1.3);
+        rippleGenerator.setClipSupplier(() -> {
+            Circle clip = new Circle();
+            clip.radiusProperty().bind(circle.radiusProperty());
+            clip.centerXProperty().bind(Bindings.createDoubleBinding(
+                    () -> circle.getBoundsInParent().getCenterX(),
+                    circle.boundsInParentProperty()
+            ));
+            clip.centerYProperty().bind(Bindings.createDoubleBinding(
+                    () -> circle.getBoundsInParent().getCenterY(),
+                    circle.boundsInParentProperty()
+            ));
+            return clip;
+        });
+        rippleGenerator.setRipplePositionFunction(event -> new RipplePosition(event.getX(), event.getY()));
+        rippleGenerator.rippleRadiusProperty().bind(circle.radiusProperty().add(5));
+    }
+
+    /**
+     * Adds listeners for:
+     * <p>
+     * <p> - {@link MFXCircleToggleNode#graphicProperty()} ()}: to update the toggle's icon when changes.
+     * <p> - {@link MFXCircleToggleNode#labelTextGapProperty()} and {@link MFXCircleToggleNode#textPositionProperty()}: to update the layout when they change.
+     * <p></p>
+     * Adds bindings for:
+     * <p>
+     * <p> - Binds the {@link MFXLabel} icons properties to the corresponding toggle's properties.
+     * <p></p>
+     * Adds event filters/handlers for:
+     * <p>
+     * <p> - MOUSE_PRESSED: to consume the check if the mouse was pressed inside tje circle,
+     * to ignore mouse pressed on label and its icons, to update the selection state and create riffle effects.
+     * (Unfortunately, JavaFX is not very accurate for circles).
+     */
+    private void setListeners() {
+        MFXCircleToggleNode toggleNode = getSkinnable();
+
+        toggleNode.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
+            if (!NodeUtils.inHierarchy(event.getPickResult().getIntersectedNode(), circle)) {
+                return;
+            }
+
+            Node leadingIcon = label.getLeadingIcon();
+            Node trailingIcon = label.getTrailingIcon();
+
+            if (leadingIcon != null && NodeUtils.inHierarchy(event.getPickResult().getIntersectedNode(), leadingIcon)) {
+                return;
+            }
+            if (trailingIcon != null && NodeUtils.inHierarchy(event.getPickResult().getIntersectedNode(), trailingIcon)) {
+                return;
+            }
+            if (NodeUtils.inHierarchy(event.getPickResult().getIntersectedNode(), label)) {
+                return;
+            }
+
+            toggleNode.setSelected(!toggleNode.isSelected());
+            rippleGenerator.generateRipple(event);
+            event.consume();
+        });
+
+        toggleNode.graphicProperty().addListener((observable, oldValue, newValue) -> {
+            if (oldValue != null) {
+                container.getChildren().remove(oldValue);
+            }
+            if (newValue != null) {
+                newValue.setMouseTransparent(true);
+                container.getChildren().add(newValue);
+            }
+        });
+
+        toggleNode.labelTextGapProperty().addListener(invalidated -> toggleNode.requestLayout());
+        toggleNode.textPositionProperty().addListener(invalidated -> toggleNode.requestLayout());
+        label.leadingIconProperty().bind(toggleNode.labelLeadingIconProperty());
+        label.trailingIconProperty().bind(toggleNode.labelTrailingIconProperty());
+    }
+
+    //================================================================================
+    // Override Methods
+    //================================================================================
+    @Override
+    protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return super.computePrefWidth(height, topInset, rightInset, bottomInset, leftInset);
+    }
+
+    @Override
+    protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
+    }
+
+    @Override
+    protected void layoutChildren(double x, double y, double w, double h) {
+        super.layoutChildren(x, y, w, h);
+
+        MFXCircleToggleNode toggleNode = getSkinnable();
+
+        Node leading = toggleNode.getLabelLeadingIcon();
+        Node trailing = toggleNode.getLabelTrailingIcon();
+        double textWidth = snapSizeX(LabelUtils.computeTextWidth(label.getFont(), label.getText()));
+
+        double lw = snapSizeX((leading != null ? leading.getBoundsInParent().getWidth() : 0.0) +
+                textWidth +
+                (trailing != null ? trailing.getBoundsInParent().getWidth() : 0.0) +
+                (toggleNode.getGraphicTextGap() * 2) +
+                60
+        );
+        double lh = snapSizeY(LabelUtils.computeTextHeight(label.getFont(), label.getText()));
+        double lx = snapPositionX(circle.getBoundsInParent().getCenterX() - (lw / 2.0));
+        double ly = 0;
+
+        if (toggleNode.getTextPosition() == TextPosition.BOTTOM) {
+            label.setTranslateY(0);
+            ly = snapPositionY(circle.getBoundsInParent().getMaxY() + toggleNode.getLabelTextGap());
+            label.resizeRelocate(lx, ly, lw, lh);
+        } else {
+            label.resizeRelocate(lx, ly, lw, lh);
+            label.setTranslateY(-toggleNode.getLabelTextGap() - lh);
+        }
+    }
+}

+ 46 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXComboBoxSkin.java

@@ -33,6 +33,8 @@ import javafx.animation.KeyFrame;
 import javafx.animation.KeyValue;
 import javafx.animation.ScaleTransition;
 import javafx.animation.Timeline;
+import javafx.beans.InvalidationListener;
+import javafx.collections.FXCollections;
 import javafx.collections.MapChangeListener;
 import javafx.event.EventHandler;
 import javafx.geometry.HPos;
@@ -128,6 +130,34 @@ public class MFXComboBoxSkin<T> extends SkinBase<MFXComboBox<T>> {
         }
 
         setBehavior();
+        initSelection();
+    }
+
+    /**
+     * Initialized the combo box value if the selected index specified by the selection model is not -1.
+     *
+     * @see ComboSelectionModelMock
+     */
+    private void initSelection() {
+        MFXComboBox<T> comboBox = getSkinnable();
+        ComboSelectionModelMock<T> selectionModel = comboBox.getSelectionModel();
+
+        if (selectionModel.getSelectedIndex() != -1 && comboBox.getItems().isEmpty()) {
+            selectionModel.clearSelection();
+            return;
+        }
+
+        if (selectionModel.getSelectedIndex() != -1) {
+            int index = selectionModel.getSelectedIndex();
+            if (index < comboBox.getItems().size()) {
+                T item = comboBox.getItems().get(index);
+                selectionModel.selectItem(item);
+                listView.getSelectionModel().select(index, item, null);
+                comboBox.setSelectedValue(item);
+            } else {
+                comboBox.getSelectionModel().clearSelection();
+            }
+        }
     }
 
     //================================================================================
@@ -270,6 +300,22 @@ public class MFXComboBoxSkin<T> extends SkinBase<MFXComboBox<T>> {
                 popup.hide();
             }
         });
+
+        if (comboBox.getItems() != null) {
+            comboBox.getItems().addListener((InvalidationListener) invalidated -> {
+                comboBox.getSelectionModel().clearSelection();
+                listView.setItems(comboBox.getItems());
+            });
+        }
+        comboBox.itemsProperty().addListener((observable, oldValue, newValue) -> {
+            comboBox.getSelectionModel().clearSelection();
+            if (newValue != null) {
+                newValue.addListener((InvalidationListener) invalidated -> listView.setItems(newValue));
+                listView.setItems(newValue);
+            } else {
+                listView.setItems(FXCollections.observableArrayList());
+            }
+        });
     }
 
     /**

+ 1 - 1
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXDateCellSkin.java

@@ -45,7 +45,7 @@ public class MFXDateCellSkin extends DateCellSkin {
         super(dateCell);
 
         rippleGenerator = new MFXCircleRippleGenerator(dateCell);
-        rippleGenerator.setClipSupplier(() -> new RippleClipTypeFactory(RippleClipType.ROUNDED_RECTANGLE).setArcs(15, 15).build(dateCell));
+        rippleGenerator.setClipSupplier(() -> new RippleClipTypeFactory(RippleClipType.ROUNDED_RECTANGLE).setArcs(15).build(dateCell));
         rippleGenerator.setRippleColor(Color.rgb(220, 220, 220, 0.6));
         rippleGenerator.setRipplePositionFunction(event -> new RipplePosition(event.getX(), event.getY()));
         dateCell.addEventFilter(MouseEvent.MOUSE_PRESSED, rippleGenerator::generateRipple);

+ 8 - 12
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXDatePickerContent.java

@@ -62,9 +62,7 @@ import java.time.format.DateTimeFormatter;
 import java.time.format.DateTimeParseException;
 import java.time.format.TextStyle;
 import java.time.temporal.WeekFields;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Locale;
+import java.util.*;
 
 import static java.time.temporal.ChronoUnit.DAYS;
 import static java.time.temporal.ChronoUnit.MONTHS;
@@ -95,6 +93,7 @@ public class MFXDatePickerContent extends VBox {
 
     private final int daysPerWeek = 7;
     private final List<MFXDateCell> days = new ArrayList<>();
+    private final Map<String, Integer> dayNameMap = new LinkedHashMap<>();
     private final List<MFXDateCell> dayNameCells = new ArrayList<>();
     private final List<MFXDateCell> yearsList = new ArrayList<>();
 
@@ -276,6 +275,7 @@ public class MFXDatePickerContent extends VBox {
             cell.setAlignment(Pos.CENTER);
 
             String name = weekDayNameFormatter.withLocale(getLocale()).format(date.plus(i, DAYS));
+            dayNameMap.put(name, i);
             if (weekDayNameFormatter.getLocale() == java.util.Locale.CHINA) {
                 name = name.substring(name.length() - 1).toUpperCase();
             } else {
@@ -322,20 +322,14 @@ public class MFXDatePickerContent extends VBox {
             days.add(cell);
         }
 
-        int offset = 0;
-        int firstDayIndex = firstDayIndex();
-        if (firstDayIndex == 7) {
-            offset = 1;
-        }
-
-        int index = firstDayIndex - 1 + offset;
+        int index = firstDayIndex();
         int monthLength = getYearMonth().getMonth().length(getYearMonth().isLeapYear());
         int cnt = 1;
         for (i = index; i < monthLength + index; i++) {
             MFXDateCell cell = days.get(i);
             cell.setText(Integer.toString(cnt));
 
-            if (day == i &&
+            if (day == cnt &&
                     LocalDate.now().getMonth().equals(getYearMonth().getMonth()) &&
                     LocalDate.now().getYear() == getYearMonth().getYear()) {
                 cell.setCurrent(true);
@@ -754,7 +748,9 @@ public class MFXDatePickerContent extends VBox {
      */
     private int firstDayIndex() {
         DayOfWeek fd = getYearMonth().atDay(1).getDayOfWeek();
-        return fd.getValue();
+        LocalDate date = LocalDate.of(2009, 7, 12 + fd.getValue());
+        String name = weekDayNameFormatter.withLocale(getLocale()).format(date.plus(0, DAYS));
+        return dayNameMap.get(name);
     }
 
     /**

+ 53 - 5
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXFilterComboBoxSkin.java

@@ -1,10 +1,7 @@
 package io.github.palexdev.materialfx.skins;
 
 import io.github.palexdev.materialfx.beans.MFXSnapshotWrapper;
-import io.github.palexdev.materialfx.controls.MFXFilterComboBox;
-import io.github.palexdev.materialfx.controls.MFXFlowlessListView;
-import io.github.palexdev.materialfx.controls.MFXIconWrapper;
-import io.github.palexdev.materialfx.controls.MFXTextField;
+import io.github.palexdev.materialfx.controls.*;
 import io.github.palexdev.materialfx.controls.cell.MFXFlowlessListCell;
 import io.github.palexdev.materialfx.controls.enums.Styles;
 import io.github.palexdev.materialfx.controls.factories.MFXAnimationFactory;
@@ -16,6 +13,8 @@ import io.github.palexdev.materialfx.selection.base.IListSelectionModel;
 import io.github.palexdev.materialfx.utils.NodeUtils;
 import io.github.palexdev.materialfx.utils.StringUtils;
 import javafx.animation.*;
+import javafx.beans.InvalidationListener;
+import javafx.collections.FXCollections;
 import javafx.collections.transformation.FilteredList;
 import javafx.event.EventHandler;
 import javafx.geometry.*;
@@ -56,7 +55,7 @@ public class MFXFilterComboBoxSkin<T> extends SkinBase<MFXFilterComboBox<T>> {
     private final Line focusedLine;
 
     private final HBox searchContainer;
-    private final FilteredList<T> filteredList;
+    private FilteredList<T> filteredList;
     private MFXTextField searchField;
 
     private Timeline arrowAnimation;
@@ -124,12 +123,40 @@ public class MFXFilterComboBoxSkin<T> extends SkinBase<MFXFilterComboBox<T>> {
         }
 
         setBehavior();
+        initSelection();
     }
 
     //================================================================================
     // Methods
     //================================================================================
 
+    /**
+     * Initialized the combo box value if the selected index specified by the selection model is not -1.
+     *
+     * @see ComboSelectionModelMock
+     */
+    private void initSelection() {
+        MFXComboBox<T> comboBox = getSkinnable();
+        ComboSelectionModelMock<T> selectionModel = comboBox.getSelectionModel();
+
+        if (selectionModel.getSelectedIndex() != -1 && comboBox.getItems().isEmpty()) {
+            selectionModel.clearSelection();
+            return;
+        }
+
+        if (selectionModel.getSelectedIndex() != -1) {
+            int index = selectionModel.getSelectedIndex();
+            if (index < comboBox.getItems().size()) {
+                T item = comboBox.getItems().get(index);
+                selectionModel.selectItem(item);
+                listView.getSelectionModel().select(index, item, null);
+                comboBox.setSelectedValue(item);
+            } else {
+                comboBox.getSelectionModel().clearSelection();
+            }
+        }
+    }
+
     /**
      * Calls the methods which define the control behavior.
      * <p>
@@ -274,6 +301,27 @@ public class MFXFilterComboBoxSkin<T> extends SkinBase<MFXFilterComboBox<T>> {
                 reset();
             }
         });
+
+        if (comboBox.getItems() != null) {
+            comboBox.getItems().addListener((InvalidationListener) invalidated -> {
+                comboBox.getSelectionModel().clearSelection();
+                filteredList = new FilteredList<>(comboBox.getItems());
+                listView.setItems(filteredList);
+            });
+        }
+        comboBox.itemsProperty().addListener((observable, oldValue, newValue) -> {
+            comboBox.getSelectionModel().clearSelection();
+            if (newValue != null) {
+                newValue.addListener((InvalidationListener) invalidated -> {
+                    filteredList = new FilteredList<>(comboBox.getItems());
+                    listView.setItems(filteredList);
+                });
+                filteredList = new FilteredList<>(comboBox.getItems());
+            } else {
+                filteredList = new FilteredList<>(FXCollections.observableArrayList());
+            }
+            listView.setItems(filteredList);
+        });
     }
 
     /**

+ 337 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXPasswordFieldSkin.java

@@ -0,0 +1,337 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
+ *
+ *     MaterialFX is free software: you can redistribute it and/or modify
+ *     it under the terms of the GNU General Public License as published by
+ *     the Free Software Foundation, either version 3 of the License, or
+ *     (at your option) any later version.
+ *
+ *     MaterialFX is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *     GNU General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.skins;
+
+import io.github.palexdev.materialfx.beans.MFXContextMenuItem;
+import io.github.palexdev.materialfx.controls.MFXContextMenu;
+import io.github.palexdev.materialfx.controls.MFXPasswordField;
+import javafx.beans.binding.Bindings;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.scene.control.IndexRange;
+import javafx.scene.input.Clipboard;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.input.MouseEvent;
+
+public class MFXPasswordFieldSkin extends MFXTextFieldSkin {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private final StringBuilder sb = new StringBuilder();
+    private final StringProperty fakeText = new SimpleStringProperty("");
+    private final StringProperty password;
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXPasswordFieldSkin(MFXPasswordField passwordField, StringProperty password) {
+        super(passwordField);
+        this.password = password;
+
+        if (!passwordField.getText().isEmpty()) {
+            sb.append(passwordField.getText());
+            for (int i = 0; i < passwordField.getText().length(); i++) {
+                setFakeText(getFakeText().concat(passwordField.getHideCharacter()));
+                setPassword(sb.toString());
+            }
+            passwordField.selectedTextProperty().addListener(new ChangeListener<>() {
+                @Override
+                public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
+                    if (!newValue.isEmpty()) {
+                        passwordField.deselect();
+                        passwordField.positionCaret(passwordField.getText().length());
+                        passwordField.selectedTextProperty().removeListener(this);
+                    }
+                }
+            });
+        }
+
+        setContextMenu();
+        setListeners();
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+
+    /**
+     * Adds listeners for:
+     * <p>
+     * <p> - {@link MFXPasswordField#hideCharacterProperty()}: to change the mask character when changes.
+     * <p></p>
+     * Adds bindings for:
+     * <p>
+     * <p> - text property: to update the field text when {@link MFXPasswordField#showPasswordProperty()} changes
+     * and when the user inputs new characters.
+     * <p></p>
+     * Adds event filters/handlers for:
+     * <p>
+     * <p> - MOUSE_PRESSED: to select all the text when double click occurs, consumes the event.
+     * <p> - KEY_TYPED: to listen to the input characters and update the password and the shown text.
+     * <p> - KEY_PRESSED: to allow "navigation" with arrows, and make all the shortcuts work.
+     */
+    private void setListeners() {
+        MFXPasswordField passwordField = (MFXPasswordField) getSkinnable();
+
+        passwordField.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
+            if (event.getClickCount() >= 2 && event.getClickCount() % 2 == 0) {
+                passwordField.selectAll();
+                event.consume();
+            }
+        });
+
+        passwordField.addEventFilter(KeyEvent.KEY_TYPED, keyEvent -> {
+            if (!isInvalidCharacter(keyEvent.getCharacter().charAt(0))) {
+                if (passwordField.getSelection().getLength() > 0) {
+                    handleDeletion(passwordField.getText().length());
+                }
+
+                sb.append(keyEvent.getCharacter());
+                setFakeText(getFakeText().concat(passwordField.getHideCharacter()));
+                setPassword(sb.toString());
+                passwordField.positionCaret(passwordField.getText().length());
+            }
+            keyEvent.consume();
+        });
+
+        passwordField.addEventFilter(KeyEvent.KEY_PRESSED, keyEvent -> {
+            KeyCode code = keyEvent.getCode();
+
+            if (keyEvent.getCode() == KeyCode.BACK_SPACE) {
+                int pos = passwordField.getCaretPosition();
+                int removeIndex = pos - 1;
+                handleDeletion(removeIndex);
+            }
+            if (keyEvent.getCode() == KeyCode.DELETE) {
+                int pos = passwordField.getCaretPosition();
+                handleDeletion(pos);
+            }
+
+            switch (code) {
+                case UP:
+                case END:
+                    passwordField.positionCaret(getPassword().length());
+                    break;
+                case DOWN:
+                case HOME:
+                    passwordField.positionCaret(0);
+                    break;
+                case LEFT:
+                    if (keyEvent.isShiftDown()) {
+                        passwordField.selectBackward();
+                    } else {
+                        passwordField.positionCaret(passwordField.getCaretPosition() - 1);
+                    }
+                    break;
+                case RIGHT:
+                    if (keyEvent.isShiftDown()) {
+                        passwordField.selectForward();
+                    } else {
+                        passwordField.positionCaret(passwordField.getCaretPosition() + 1);
+                    }
+                    break;
+                case A: {
+                    if (keyEvent.isControlDown()) {
+                        passwordField.selectAll();
+                    }
+                    break;
+                }
+                case C:
+                    if (keyEvent.isControlDown() && passwordField.isAllowCopy()) {
+                        passwordField.copy();
+                    }
+                    break;
+                case D:
+                    if (keyEvent.isControlDown()) {
+                        handleDeletion(passwordField.getText().length());
+                    }
+                    break;
+                case V:
+                    if (keyEvent.isControlDown() && passwordField.isAllowPaste()) {
+                        handlePaste();
+                    }
+                    break;
+                case X:
+                    if (keyEvent.isControlDown() && passwordField.isAllowCut()) {
+                        passwordField.cut();
+                        handleDeletion(passwordField.getText().length());
+                    }
+                default:
+                    break;
+            }
+
+            keyEvent.consume();
+        });
+
+        passwordField.textProperty().bind(Bindings.createStringBinding(
+                () -> passwordField.isShowPassword() ? getPassword() : getFakeText(),
+                passwordField.showPasswordProperty(), fakeText, password
+        ));
+        passwordField.hideCharacterProperty().addListener((observable, oldValue, newValue) -> fakeText.set(getFakeText().replace(oldValue, newValue)));
+    }
+
+    /**
+     * Handles the past action by checking that the clipboard is not empty,
+     * by removing the selected text (if there is), ensures that the pasted text
+     * is inserted at the right position and ensures that the caret is at the right index.
+     */
+    private void handlePaste() {
+        MFXPasswordField passwordField = (MFXPasswordField) getSkinnable();
+
+        Clipboard clipboard = Clipboard.getSystemClipboard();
+        String content = clipboard.getString();
+        if (!content.trim().isEmpty()) {
+            if (passwordField.getSelection().getLength() > 0) {
+                handleDeletion(passwordField.getText().length());
+            }
+
+            int caretPos = passwordField.getCaretPosition();
+            int end = caretPos + content.length();
+            sb.insert(caretPos, content);
+            for (int i = 0; i < content.length(); i++) {
+                setFakeText(getFakeText().concat(passwordField.getHideCharacter()));
+                setPassword(sb.toString());
+            }
+            passwordField.positionCaret(end);
+        }
+    }
+
+    /**
+     * Handles the deletion of text.
+     */
+    private void handleDeletion(int pos) {
+        MFXPasswordField passwordField = (MFXPasswordField) getSkinnable();
+
+        if (!passwordField.getSelectedText().isEmpty()) {
+            IndexRange range = passwordField.getSelection();
+            int start = range.getStart();
+            int end = range.getEnd();
+
+            setPassword(sb.delete(start, end).toString());
+            StringBuilder tmp = new StringBuilder(getFakeText());
+            setFakeText(tmp.delete(start, end).toString());
+            passwordField.positionCaret(pos);
+            return;
+        }
+
+        if (pos >= 0 && pos < getPassword().length()) {
+            setPassword(sb.deleteCharAt(pos).toString());
+            StringBuilder tmp = new StringBuilder(getFakeText());
+            setFakeText(tmp.deleteCharAt(pos).toString());
+            passwordField.positionCaret(pos);
+        }
+    }
+
+    /**
+     * Checks if the typed character is valid.
+     */
+    private static boolean isInvalidCharacter(char c) {
+        if (c == 0x7F) return true;
+        if (c == 0xA) return true;
+        if (c == 0x9) return true;
+        return c < 0x20;
+    }
+
+    /**
+     * Sets the default {@link MFXContextMenu} for the password field.
+     */
+    protected void setContextMenu() {
+        MFXPasswordField passwordField = (MFXPasswordField) getSkinnable();
+
+        MFXContextMenuItem copy = new MFXContextMenuItem(
+                "Copy",
+                event -> {
+                    if (passwordField.isAllowCopy()) {
+                        passwordField.copy();
+                    }
+                }
+        );
+
+        MFXContextMenuItem cut = new MFXContextMenuItem(
+                "Cut",
+                event -> {
+                    if (passwordField.isAllowCut()) {
+                        passwordField.cut();
+                        handleDeletion(passwordField.getText().length());
+                    }
+                }
+        );
+
+        MFXContextMenuItem paste = new MFXContextMenuItem(
+                "Paste",
+                event -> {
+                    if (passwordField.isAllowPaste()) {
+                        handlePaste();
+                    }
+                }
+        );
+
+        MFXContextMenuItem delete = new MFXContextMenuItem(
+                "Delete",
+                event -> handleDeletion(passwordField.getText().length())
+        );
+
+        MFXContextMenuItem selectAll = new MFXContextMenuItem(
+                "Select All",
+                event -> passwordField.selectAll()
+        );
+
+        passwordField.setMFXContextMenu(
+                new MFXContextMenu.Builder()
+                        .addMenuItem(copy)
+                        .addMenuItem(cut)
+                        .addMenuItem(paste)
+                        .addMenuItem(delete)
+                        .addSeparator()
+                        .addMenuItem(selectAll)
+                        .get()
+        );
+    }
+
+    /**
+     * @return the masked text
+     */
+    public String getFakeText() {
+        return fakeText.get();
+    }
+
+    /**
+     * Sets the masked text
+     */
+    public void setFakeText(String fakeText) {
+        this.fakeText.set(fakeText);
+    }
+
+    /**
+     * @return the password/un-masked text
+     */
+    public String getPassword() {
+        return password.get();
+    }
+
+    /**
+     * Sets the password
+     */
+    public void setPassword(String password) {
+        this.password.set(password);
+    }
+}

+ 180 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXRectangleToggleNodeSkin.java

@@ -0,0 +1,180 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
+ *
+ *     MaterialFX is free software: you can redistribute it and/or modify
+ *     it under the terms of the GNU General Public License as published by
+ *     the Free Software Foundation, either version 3 of the License, or
+ *     (at your option) any later version.
+ *
+ *     MaterialFX is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *     GNU General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.github.palexdev.materialfx.skins;
+
+import io.github.palexdev.materialfx.controls.MFXLabel;
+import io.github.palexdev.materialfx.controls.MFXRectangleToggleNode;
+import io.github.palexdev.materialfx.effects.ripple.MFXCircleRippleGenerator;
+import io.github.palexdev.materialfx.effects.ripple.RipplePosition;
+import io.github.palexdev.materialfx.utils.LabelUtils;
+import io.github.palexdev.materialfx.utils.NodeUtils;
+import javafx.event.Event;
+import javafx.scene.Node;
+import javafx.scene.control.SkinBase;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.StackPane;
+
+/**
+ * This is the default skin for every {@link MFXRectangleToggleNode}.
+ * <p></p>
+ * The base container is a {@link StackPane} which contains: a {@link MFXLabel} to show the toggle's text.
+ * <p></p>
+ * Includes a {@link MFXCircleRippleGenerator} to generate ripple effects on mouse pressed.
+ */
+public class MFXRectangleToggleNodeSkin extends SkinBase<MFXRectangleToggleNode> {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private final StackPane container;
+    private final MFXLabel label;
+    private final MFXCircleRippleGenerator rippleGenerator;
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXRectangleToggleNodeSkin(MFXRectangleToggleNode toggleNode) {
+        super(toggleNode);
+
+        label = new MFXLabel();
+        label.setPromptText("");
+        label.setId("textNode");
+        label.textProperty().bind(toggleNode.textProperty());
+        label.graphicTextGapProperty().bind(toggleNode.labelTextGapProperty());
+        label.getStylesheets().setAll(toggleNode.getUserAgentStylesheet());
+        label.setLeadingIcon(toggleNode.getLabelLeadingIcon());
+        label.setTrailingIcon(toggleNode.getLabelTrailingIcon());
+
+        container = new StackPane();
+        container.getStyleClass().setAll("container");
+        container.setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
+        container.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
+        container.prefWidthProperty().bind(toggleNode.widthProperty());
+        container.prefHeightProperty().bind(toggleNode.heightProperty());
+
+        rippleGenerator = new MFXCircleRippleGenerator(toggleNode);
+
+        container.getChildren().setAll(rippleGenerator, label);
+
+        setupRippleGenerator();
+        setListeners();
+        getChildren().setAll(container);
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+
+    /**
+     * Sets up the ripple generator.
+     */
+    protected void setupRippleGenerator() {
+        MFXRectangleToggleNode toggleNode = getSkinnable();
+
+        rippleGenerator.setAnimateBackground(false);
+        rippleGenerator.setClipSupplier(() -> toggleNode.getRippleClipTypeFactory().build(container));
+        rippleGenerator.setRipplePositionFunction(event -> new RipplePosition(event.getX(), event.getY()));
+        rippleGenerator.rippleRadiusProperty().bind(toggleNode.widthProperty().divide(2.0));
+    }
+
+    /**
+     * Adds listeners for:
+     * <p>
+     * <p> - {@link MFXRectangleToggleNode}: to update the ripple generator's clip supplier.
+     * <p></p>
+     * Adds bindings for:
+     * <p>
+     * <p> - Binds the {@link MFXLabel} icons properties to the corresponding toggle's properties.
+     * <p></p>
+     * Adds event filters/handlers for:
+     * <p>
+     * <p> - MOUSE_PRESSED: to not change the selection state if the mouse is pressed on the label's icons, to update the selection state, and to generate ripple effects.
+     * <p> - MOUSE_PRESSED on the label: to not change the selection state if the mouse is pressed on the label's icons and pass the mouse event to the toggle.
+     */
+    private void setListeners() {
+        MFXRectangleToggleNode toggleNode = getSkinnable();
+
+        toggleNode.rippleClipTypeFactoryProperty().addListener((observable, oldValue, newValue) -> rippleGenerator.setClipSupplier(() -> newValue.build(container)));
+        label.leadingIconProperty().bind(toggleNode.labelLeadingIconProperty());
+        label.trailingIconProperty().bind(toggleNode.labelTrailingIconProperty());
+
+        container.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
+            Node leadingIcon = label.getLeadingIcon();
+            Node trailingIcon = label.getTrailingIcon();
+
+            if (leadingIcon != null && NodeUtils.inHierarchy(event.getPickResult().getIntersectedNode(), leadingIcon)) {
+                return;
+            }
+            if (trailingIcon != null && NodeUtils.inHierarchy(event.getPickResult().getIntersectedNode(), trailingIcon)) {
+                return;
+            }
+
+            toggleNode.setSelected(!toggleNode.isSelected());
+            rippleGenerator.generateRipple(event);
+        });
+
+        label.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
+            Node leadingIcon = label.getLeadingIcon();
+            Node trailingIcon = label.getTrailingIcon();
+
+
+            if (leadingIcon != null && NodeUtils.inHierarchy(event.getPickResult().getIntersectedNode(), leadingIcon)) {
+                return;
+            }
+            if (trailingIcon != null && NodeUtils.inHierarchy(event.getPickResult().getIntersectedNode(), trailingIcon)) {
+                return;
+            }
+
+            Event.fireEvent(toggleNode, event);
+        });
+    }
+
+    //================================================================================
+    // Override Methods
+    //================================================================================
+    @Override
+    protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return computePrefWidth(height, topInset, rightInset, bottomInset, leftInset);
+    }
+
+    @Override
+    protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+        MFXRectangleToggleNode toggleNode = getSkinnable();
+        Node leading = toggleNode.getLabelLeadingIcon();
+        Node trailing = toggleNode.getLabelTrailingIcon();
+        double lw = snapSizeX(LabelUtils.computeTextWidth(label.getFont(), label.getText()));
+        return leftInset +
+                (leading != null ? leading.getBoundsInParent().getWidth() : 0.0) +
+                lw +
+                (trailing != null ? trailing.getBoundsInParent().getWidth() : 0.0) +
+                (toggleNode.getGraphicTextGap() * 2) +
+                40 +
+                rightInset;
+    }
+
+    @Override
+    protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return computePrefWidth(height, topInset, rightInset, bottomInset, leftInset);
+    }
+
+    @Override
+    protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
+    }
+}

+ 4 - 2
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXStepperToggleSkin.java

@@ -108,6 +108,8 @@ public class MFXStepperToggleSkin extends SkinBase<MFXStepperToggle> {
                 errorIcon.setVisible(false);
             }
         });
+
+        stepperToggle.labelTextGapProperty().addListener(invalidated -> stepperToggle.requestLayout());
         stepperToggle.textPositionProperty().addListener(invalidated -> stepperToggle.requestLayout());
 
         validator.validProperty().addListener((observable, oldValue, newValue) -> {
@@ -150,11 +152,11 @@ public class MFXStepperToggleSkin extends SkinBase<MFXStepperToggle> {
 
         if (stepperToggle.getTextPosition() == TextPosition.BOTTOM) {
             label.setTranslateY(0);
-            ly = snapPositionY(circle.getBoundsInParent().getMaxY() + stepperToggle.getTextGap());
+            ly = snapPositionY(circle.getBoundsInParent().getMaxY() + stepperToggle.getLabelTextGap());
             label.resizeRelocate(lx, ly, lw, lh);
         } else {
             label.resizeRelocate(lx, ly, lw, lh);
-            label.setTranslateY(-stepperToggle.getTextGap() - lh);
+            label.setTranslateY(-stepperToggle.getLabelTextGap() - lh);
         }
 
         double ix = snapPositionX(circle.getBoundsInParent().getMaxX());

+ 0 - 115
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXTableColumnCellSkin.java

@@ -1,115 +0,0 @@
-/*
- *     Copyright (C) 2021 Parisi Alessandro
- *     This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
- *
- *     MaterialFX is free software: you can redistribute it and/or modify
- *     it under the terms of the GNU General Public License as published by
- *     the Free Software Foundation, either version 3 of the License, or
- *     (at your option) any later version.
- *
- *     MaterialFX is distributed in the hope that it will be useful,
- *     but WITHOUT ANY WARRANTY; without even the implied warranty of
- *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *     GNU General Public License for more details.
- *
- *     You should have received a copy of the GNU General Public License
- *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package io.github.palexdev.materialfx.skins;
-
-import io.github.palexdev.materialfx.controls.MFXIconWrapper;
-import io.github.palexdev.materialfx.controls.cell.MFXTableColumnCell;
-import io.github.palexdev.materialfx.font.MFXFontIcon;
-import io.github.palexdev.materialfx.utils.NodeUtils;
-import javafx.geometry.Insets;
-import javafx.scene.Node;
-import javafx.scene.control.ContentDisplay;
-import javafx.scene.control.Label;
-import javafx.scene.control.Tooltip;
-import javafx.scene.control.skin.LabelSkin;
-import javafx.scene.layout.Region;
-
-/**
- * This is the implementation of the {@code Skin} associated with every {@link MFXTableColumnCell}.
- */
-public class MFXTableColumnCellSkin<T> extends LabelSkin {
-    //================================================================================
-    // Constructors
-    //================================================================================
-    public MFXTableColumnCellSkin(MFXTableColumnCell<T> column) {
-        super(column);
-
-        column.setMinWidth(Region.USE_PREF_SIZE);
-        column.setMaxWidth(Region.USE_PREF_SIZE);
-
-        column.setPadding(new Insets(0, 5, 0, 5));
-        column.setGraphic(createSortIcon());
-        column.setGraphicTextGap(5);
-        addIcon();
-
-        if (column.hasTooltip()) {
-            column.setTooltip(buildTooltip());
-        }
-        setListeners();
-    }
-
-    /**
-     * Adds listeners to the following properties: hasTooltipProperty
-     */
-    @SuppressWarnings("unchecked")
-    private void setListeners() {
-        MFXTableColumnCell<T> column = (MFXTableColumnCell<T>) getSkinnable();
-
-        column.hasTooltipProperty().addListener((observable, oldValue, newValue) -> {
-            if (!newValue) {
-                column.setTooltip(null);
-            } else {
-                column.setTooltip(buildTooltip());
-            }
-        });
-    }
-
-    /**
-     * Creates the sort icon.
-     */
-    protected Node createSortIcon() {
-        MFXFontIcon caret = new MFXFontIcon("mfx-caret-up", 12);
-        caret.setMouseTransparent(true);
-        MFXIconWrapper icon = new MFXIconWrapper(caret, 18);
-        icon.setVisible(false);
-        NodeUtils.makeRegionCircular(icon);
-        return icon;
-    }
-
-    /**
-     * Adds the sort icon according to the column alignment.
-     */
-    private void addIcon() {
-        Label column = getSkinnable();
-        Node leading = column.getGraphic();
-
-        if (NodeUtils.isRightAlignment(column.getAlignment())) {
-            if (leading != null) {
-                column.setContentDisplay(ContentDisplay.LEFT);
-            }
-        } else {
-            if (leading != null) {
-                column.setContentDisplay(ContentDisplay.RIGHT);
-            }
-        }
-    }
-
-    /**
-     * Responsible for building the column cell tooltip. By default creates
-     * a tooltip with the name of the column as text.
-     */
-    @SuppressWarnings("unchecked")
-    private Tooltip buildTooltip() {
-        MFXTableColumnCell<T> column = (MFXTableColumnCell<T>) getSkinnable();
-
-        Tooltip tooltip = new Tooltip();
-        tooltip.textProperty().bind(column.tooltipTextProperty());
-        return tooltip;
-    }
-}

+ 116 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXTableColumnSkin.java

@@ -0,0 +1,116 @@
+package io.github.palexdev.materialfx.skins;
+
+import io.github.palexdev.materialfx.controls.MFXIconWrapper;
+import io.github.palexdev.materialfx.controls.cell.MFXTableColumn;
+import io.github.palexdev.materialfx.utils.DragResizer;
+import io.github.palexdev.materialfx.utils.NodeUtils;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.control.Label;
+import javafx.scene.control.SkinBase;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Region;
+
+/**
+ * This is the implementation of the {@code Skin} associated with every {@link MFXTableColumn}.
+ * <p></p>
+ * Simply an HBox with a label and an icon for sorting positioned manually based on the column's alignment.
+ * It also has support for resizing the column on drag.
+ */
+public class MFXTableColumnSkin<T> extends SkinBase<MFXTableColumn<T>> {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private final HBox container;
+    private final Label label;
+
+    private final DragResizer dragResizer;
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXTableColumnSkin(MFXTableColumn<T> column) {
+        super(column);
+
+        label = new Label();
+        label.textProperty().bind(column.textProperty());
+
+        container = new HBox(label, column.getSortIcon());
+        container.setMinWidth(Region.USE_PREF_SIZE);
+        container.setPadding(new Insets(0,10, 0, 0));
+        container.alignmentProperty().bind(column.columnAlignmentProperty());
+        container.boundsInParentProperty().addListener((observable, oldValue, newValue) -> {
+            if (newValue.getMinX() <= 0) {
+                container.relocate(1, 1);
+            }
+        });
+
+        dragResizer = new DragResizer(column, DragResizer.RIGHT);
+
+        if (column.isResizable()) {
+            dragResizer.makeResizable();
+        }
+
+        setListeners();
+
+        getChildren().setAll(container);
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+
+    /**
+     * Adds listeners for:
+     * <p>
+     * <p> - {@link MFXTableColumn#resizableProperty()} ()}: to enable/disable this column {@link DragResizer} by calling
+     * {@link DragResizer#makeResizable()} or {@link DragResizer#uninstall()} depending on its value.
+     */
+    private void setListeners() {
+        MFXTableColumn<T> tableColumn = getSkinnable();
+
+        tableColumn.resizableProperty().addListener((observable, oldValue, newValue) -> {
+            if (!newValue) {
+                dragResizer.uninstall();
+            } else {
+                dragResizer.makeResizable();
+            }
+        });
+    }
+
+    //================================================================================
+    // Override Methods
+    //================================================================================
+    @Override
+    protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+        double computed;
+        MFXTableColumn<T> column = getSkinnable();
+        if (NodeUtils.isRightAlignment(column.getColumnAlignment())) {
+            computed = leftInset + label.getWidth() + column.getSortIcon().getSize() + rightInset + 20;
+        } else {
+            computed = leftInset + label.getWidth() + getSkinnable().getSortIcon().getSize() + rightInset + 10;
+        }
+        return computed;
+    }
+
+    @Override
+    protected void layoutChildren(double x, double y, double w, double h) {
+        super.layoutChildren(x, y, w, h);
+
+        MFXTableColumn<T> column = getSkinnable();
+        MFXIconWrapper sortIcon = column.getSortIcon();
+        Pos alignment = column.getColumnAlignment();
+
+        double sortSize = sortIcon.getSize();
+        double sX;
+        double sY = snapPositionY((h / 2) - (sortSize / 2));
+
+        if (!NodeUtils.isRightAlignment(alignment)) {
+            sX = snapPositionX(w - sortSize - 5);
+        } else {
+            sX = 5;
+        }
+
+        sortIcon.resizeRelocate(sX, sY, sortSize, sortSize);
+    }
+}

+ 79 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXTableRowCellSkin.java

@@ -0,0 +1,79 @@
+package io.github.palexdev.materialfx.skins;
+
+import io.github.palexdev.materialfx.controls.cell.MFXTableRowCell;
+import javafx.scene.control.Label;
+import javafx.scene.control.SkinBase;
+import javafx.scene.layout.HBox;
+
+/**
+ * Default skin implementation for {@link MFXTableRowCell}.
+ * <p>
+ * Simply an HBox which contains a label used to show the cell's text,
+ * the leading and the trailing nodes specified by {@link MFXTableRowCell#leadingGraphicProperty()},
+ * {@link MFXTableRowCell#trailingGraphicProperty()}
+ */
+public class MFXTableRowCellSkin extends SkinBase<MFXTableRowCell> {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private final HBox container;
+    private final Label label;
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+    public MFXTableRowCellSkin(MFXTableRowCell rowCell) {
+        super(rowCell);
+
+        label = new Label();
+        label.setId("dataLabel");
+        label.textProperty().bind(rowCell.textProperty());
+
+        container = new HBox(label);
+        container.spacingProperty().bind(rowCell.graphicTextGapProperty());
+        container.alignmentProperty().bind(rowCell.rowAlignmentProperty());
+
+        if (rowCell.getLeadingGraphic() != null) {
+            container.getChildren().add(0, rowCell.getLeadingGraphic());
+        }
+        if (rowCell.getTrailingGraphic() != null) {
+            container.getChildren().add(rowCell.getTrailingGraphic());
+        }
+
+        getChildren().setAll(container);
+
+        setListeners();
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+
+    /**
+     * Adds listeners for:
+     * <p>
+     * <p> - {@link MFXTableRowCell#leadingGraphicProperty()}: to allow changing the leading icon.
+     * <p> - {@link MFXTableRowCell#trailingGraphicProperty()}: to allow changing the trailing icon.
+     */
+    private void setListeners() {
+        MFXTableRowCell rowCell = getSkinnable();
+
+        rowCell.leadingGraphicProperty().addListener((observable, oldValue, newValue) -> {
+            if (oldValue != null) {
+                container.getChildren().remove(oldValue);
+            }
+            if (newValue != null) {
+                container.getChildren().add(0, newValue);
+            }
+        });
+
+        rowCell.trailingGraphicProperty().addListener((observable, oldValue, newValue) -> {
+            if (oldValue != null) {
+                container.getChildren().remove(oldValue);
+            }
+            if (newValue != null) {
+                container.getChildren().add(newValue);
+            }
+        });
+    }
+}

File diff suppressed because it is too large
+ 496 - 569
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXTableViewSkin.java


+ 66 - 3
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXTextFieldSkin.java

@@ -26,7 +26,9 @@ import io.github.palexdev.materialfx.font.MFXFontIcon;
 import io.github.palexdev.materialfx.utils.LabelUtils;
 import io.github.palexdev.materialfx.validation.MFXDialogValidator;
 import javafx.animation.ScaleTransition;
+import javafx.beans.binding.Bindings;
 import javafx.scene.Cursor;
+import javafx.scene.Node;
 import javafx.scene.control.skin.TextFieldSkin;
 import javafx.scene.input.MouseEvent;
 import javafx.scene.paint.Color;
@@ -35,6 +37,15 @@ import javafx.util.Duration;
 
 /**
  * This is the implementation of the {@code Skin} associated with every {@link MFXTextField}.
+ * <p></p>
+ * A little note on the icon positioning and the text field width.
+ * If you use the control in SceneBuilder you will immediately notice that the width of the text field doesn't take into
+ * account the icon. The icon is placed "outside" the control because otherwise the input text would end under the icon,
+ * and that's not a pleasant view.
+ * <p>
+ * Another solution would be to entirely recreate the TextFieldSkin using an HBox to contain the field and the icon,
+ * but I don't think it's necessary since this strategy seems to work fine. Also don't forget that you can position the icon manually,
+ * you should be able to put the icon "inside" the control by specifying a right inset equal to the icon's width, see {@link MFXTextField#iconInsetsProperty()}.
  */
 public class MFXTextFieldSkin extends TextFieldSkin {
     //================================================================================
@@ -58,7 +69,14 @@ public class MFXTextFieldSkin extends TextFieldSkin {
         unfocusedLine.strokeWidthProperty().bind(textField.lineStrokeWidthProperty());
         unfocusedLine.strokeLineCapProperty().bind(textField.lineStrokeCapProperty());
         unfocusedLine.strokeProperty().bind(textField.unfocusedLineColorProperty());
-        unfocusedLine.endXProperty().bind(textField.widthProperty());
+        unfocusedLine.endXProperty().bind(Bindings.createDoubleBinding(() -> {
+            Node icon = textField.getIcon();
+            if (icon != null) {
+                return textField.getWidth() + icon.getLayoutBounds().getWidth() +
+                        textField.getIconInsets().getLeft() - textField.getIconInsets().getRight();
+            }
+            return textField.getWidth();
+        }, textField.widthProperty(), textField.iconProperty()));
         unfocusedLine.setSmooth(true);
         unfocusedLine.setManaged(false);
 
@@ -69,7 +87,14 @@ public class MFXTextFieldSkin extends TextFieldSkin {
         focusedLine.strokeLineCapProperty().bind(textField.lineStrokeCapProperty());
         focusedLine.strokeProperty().bind(textField.lineColorProperty());
         focusedLine.setSmooth(true);
-        focusedLine.endXProperty().bind(textField.widthProperty());
+        focusedLine.endXProperty().bind(Bindings.createDoubleBinding(() -> {
+            Node icon = textField.getIcon();
+            if (icon != null) {
+                return textField.getWidth() + icon.getLayoutBounds().getWidth() +
+                        textField.getIconInsets().getLeft() - textField.getIconInsets().getRight();
+            }
+            return textField.getWidth();
+        }, textField.widthProperty(), textField.iconProperty()));
         focusedLine.setScaleX(0.0);
         focusedLine.setManaged(false);
 
@@ -90,6 +115,11 @@ public class MFXTextFieldSkin extends TextFieldSkin {
         }
 
         getChildren().addAll(unfocusedLine, focusedLine, validate);
+        Node icon = textField.getIcon();
+        if (icon != null) {
+            icon.addEventFilter(MouseEvent.MOUSE_ENTERED, event -> icon.setCursor(Cursor.DEFAULT));
+            getChildren().add(icon);
+        }
 
         setListeners();
     }
@@ -99,7 +129,7 @@ public class MFXTextFieldSkin extends TextFieldSkin {
     //================================================================================
 
     /**
-     * Adds listeners for: line, focus, disabled and validator properties.
+     * Adds listeners for: icon, icon insets, line, focus, disabled and validator properties.
      * <p>
      * Validator: when the control is not focused, and of course if {@code isValidated} is true,
      * all the conditions in the validator are evaluated and if one is false the {@code validate} label is shown.
@@ -112,6 +142,18 @@ public class MFXTextFieldSkin extends TextFieldSkin {
         MFXTextField textField = (MFXTextField) getSkinnable();
         MFXDialogValidator validator = textField.getValidator();
 
+        textField.iconProperty().addListener((observable, oldValue, newValue) -> {
+            if (newValue == null) {
+                getChildren().remove(oldValue);
+            } else {
+                getChildren().remove(oldValue);
+                newValue.addEventFilter(MouseEvent.MOUSE_ENTERED, event -> newValue.setCursor(Cursor.DEFAULT));
+                getChildren().add(newValue);
+            }
+        });
+
+        textField.iconInsetsProperty().addListener(invalidated -> textField.requestLayout());
+
         textField.focusedProperty().addListener((observable, oldValue, newValue) -> {
             if (!newValue && textField.isValidated()) {
                 textField.getValidator().update();
@@ -181,9 +223,15 @@ public class MFXTextFieldSkin extends TextFieldSkin {
     //================================================================================
     // Override Methods
     //================================================================================
+    @Override
+    protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return Math.max(super.computeMinWidth(height, topInset, rightInset, bottomInset, leftInset), 120);
+    }
+
     @Override
     protected void layoutChildren(double x, double y, double w, double h) {
         super.layoutChildren(x, y, w, h);
+        MFXTextField textField = (MFXTextField) getSkinnable();
 
         double lw = snapSizeX(
                 LabelUtils.computeLabelWidth(validate.getFont(), validate.getText()) +
@@ -204,5 +252,20 @@ public class MFXTextFieldSkin extends TextFieldSkin {
         focusedLine.relocate(0, h + padding * 0.7);
         unfocusedLine.relocate(0, h + padding * 0.7);
 
+        Node icon = textField.getIcon();
+        if (icon != null) {
+            icon.setManaged(false);
+
+            double iX = snapPositionX(w +
+                    textField.getIconInsets().getLeft() -
+                    textField.getIconInsets().getRight()
+            );
+            double iY = snapPositionY(h - (icon.getLayoutBounds().getHeight() / 2.0) +
+                    textField.getIconInsets().getTop() -
+                    textField.getIconInsets().getBottom() -
+                    padding * 0.7
+            );
+            icon.relocate(iX, iY);
+        }
     }
 }

+ 9 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/utils/BindingUtils.java

@@ -31,6 +31,15 @@ public class BindingUtils {
 
     private BindingUtils() {}
 
+    public static <T> ObjectProperty<T> toProperty(ObjectExpression<T> expression) {
+        if (expression == null) {
+            throw  new IllegalArgumentException("The argument cannot be null!");
+        }
+        ObjectProperty<T> property = new SimpleObjectProperty<>();
+        property.bind(expression);
+        return property;
+    }
+
     /**
      * Creates a new {@link IntegerProperty} and binds it to the given binding/expression.
      */

+ 25 - 7
materialfx/src/main/java/io/github/palexdev/materialfx/utils/DragResizer.java

@@ -18,6 +18,7 @@
 
 package io.github.palexdev.materialfx.utils;
 
+import javafx.event.EventHandler;
 import javafx.scene.Cursor;
 import javafx.scene.input.MouseEvent;
 import javafx.scene.layout.Region;
@@ -46,10 +47,15 @@ public class DragResizer {
     public static final short LEFT = 8;
     public static final short ALL_DIRECTIONS = 15;
 
+    private final EventHandler<MouseEvent> pressedHandler = this::mousePressed;
+    private final EventHandler<MouseEvent> draggedHandler = this::mouseDragged;
+    private final EventHandler<MouseEvent> movedHandler = this::mouseOver;
+    private final EventHandler<MouseEvent> releasedHandler = this::mouseReleased;
+
     //================================================================================
     // Constructors
     //================================================================================
-    private DragResizer(Region region, int allowedDirection) {
+    public DragResizer(Region region, int allowedDirection) {
         this.region = region;
         this.allowedDirection = allowedDirection;
     }
@@ -57,13 +63,25 @@ public class DragResizer {
     //================================================================================
     // Methods
     //================================================================================
-    public static void makeResizable(Region region, int allowedDirection) {
-        final DragResizer resizer = new DragResizer(region, allowedDirection);
 
-        region.addEventFilter(MouseEvent.MOUSE_PRESSED, resizer::mousePressed);
-        region.addEventFilter(MouseEvent.MOUSE_DRAGGED, resizer::mouseDragged);
-        region.addEventFilter(MouseEvent.MOUSE_MOVED, resizer::mouseOver);
-        region.addEventFilter(MouseEvent.MOUSE_RELEASED, resizer::mouseReleased);
+    /**
+     * Adds the necessary listeners to the specified region to make it resizable.
+     */
+    public void makeResizable() {
+        region.addEventFilter(MouseEvent.MOUSE_PRESSED, pressedHandler);
+        region.addEventFilter(MouseEvent.MOUSE_DRAGGED, draggedHandler);
+        region.addEventFilter(MouseEvent.MOUSE_MOVED, movedHandler);
+        region.addEventFilter(MouseEvent.MOUSE_RELEASED, releasedHandler);
+    }
+
+    /**
+     * Removes all the listeners from the region thus making it not resizable anymore.
+     */
+    public void uninstall() {
+        region.removeEventFilter(MouseEvent.MOUSE_PRESSED, pressedHandler);
+        region.removeEventFilter(MouseEvent.MOUSE_DRAGGED, draggedHandler);
+        region.removeEventFilter(MouseEvent.MOUSE_MOVED, movedHandler);
+        region.removeEventFilter(MouseEvent.MOUSE_RELEASED, releasedHandler);
     }
 
     protected void mouseReleased(MouseEvent event) {

+ 15 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/utils/StringUtils.java

@@ -125,6 +125,21 @@ public class StringUtils {
         return false;
     }
 
+    /**
+     * Checks if thee given string starts with the specifies prefix, ignores case.
+     */
+    public static boolean startsWithIgnoreCase(String str, String prefix) {
+        return str.regionMatches(true, 0, prefix, 0, prefix.length());
+    }
+
+    /**
+     * Checks if the given string ends with the given prefix, ignores case.
+     */
+    public static boolean endsWithIgnoreCase(String str, String suffix) {
+        int suffixLength = suffix.length();
+        return str.regionMatches(true, str.length() - suffixLength, suffix, 0, suffixLength);
+    }
+
     private static boolean regionMatches(final CharSequence cs, final int thisStart,
                                          final CharSequence substring, final int length) {
         if (cs instanceof String && substring instanceof String) {

+ 28 - 32
materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-filter-dialog.css → materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-circle-togglenode.css

@@ -18,45 +18,41 @@
 
 @import "fonts.css";
 
-.mfx-button {
-    -fx-border-color: rgb(82, 0, 237);
-    -fx-border-width: 0.5;
-    -fx-border-radius: 4;
-    -fx-background-radius: 4;
-
-    -fx-font-family: "Open Sans Regular";
-    -fx-font-smoothing-type: grey;
-    -fx-text-fill: rgb(85, 85, 85);
+.mfx-toggle-node {
+    -mfx-unselected-color: #F9F9F9;
+    -mfx-selected-color: #EDEDED;
+    -mfx-unselected-border-color: #E1E1E1;
+    -mfx-selected-border-color: #E1E1E1;
 }
 
-.mfx-button .mfx-ripple-generator {
-    -mfx-ripple-color: rgba(82, 0, 237, 0.3);
-    -mfx-ripple-radius: 40;
+.mfx-toggle-node,
+.mfx-toggle-node:armed,
+.mfx-toggle-node:hover,
+.mfx-toggle-node:focused,
+.mfx-toggle-node:selected,
+.mfx-toggle-node:focused:selected {
+    -fx-background-color: TRANSPARENT, TRANSPARENT, TRANSPARENT, TRANSPARENT;
+    -fx-background-radius: 3px;
+    -fx-background-insets: 0px;
 }
 
-.mfx-label {
-    -mfx-line-color: transparent;
-    -mfx-unfocused-line-color: transparent;
-
-    -fx-background-color: white;
-    -fx-background-radius: 4;
-
-    -fx-border-color: rgb(82, 0, 237);
-    -fx-border-radius: 4;
-
-    -fx-font-family: "Open Sans Regular";
-    -fx-font-smoothing-type: grey;
-    -fx-text-fill: rgb(85, 85, 85);
+.mfx-toggle-node .container {
+    -fx-background-color: transparent;
 }
 
-.mfx-toggle-button {
-    -fx-font-family: "Open Sans Regular";
-    -fx-font-smoothing-type: grey;
-    -fx-text-fill: rgb(85, 85, 85);
+.mfx-toggle-node #circle {
+    -fx-fill: -mfx-unselected-color;
+    -fx-stroke: -mfx-unselected-border-color;
 }
 
-.mfx-toggle-button .mfx-ripple-generator {
-    -mfx-ripple-radius: 10;
+.mfx-toggle-node:selected #circle {
+    -fx-fill: -mfx-selected-color;
+    -fx-stroke: -mfx-selected-border-color;
 }
 
-
+.mfx-toggle-node .mfx-label {
+    -mfx-line-color: transparent;
+    -mfx-unfocused-line-color: transparent;
+    -mfx-font-family: 'Open Sans Regular';
+    -fx-background-color: transparent;
+}

+ 26 - 0
materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-evaluationbox.css

@@ -0,0 +1,26 @@
+.mfx-evaluation-box {
+    -mfx-base-color: rgb(76, 0, 225);
+}
+
+.mfx-evaluation-box #modeLabel {
+    -fx-border-color: -mfx-base-color;
+    -fx-border-radius: 3;
+    -fx-font-family: "Open Sans SemiBold";
+}
+
+.mfx-evaluation-box .mfx-text-field {
+    -mfx-line-color: -mfx-base-color;
+    -fx-font-family: "Open Sans Regular";
+}
+
+.mfx-evaluation-box .label {
+    -fx-font-family: "Open Sans SemiBold";
+}
+
+.mfx-evaluation-box .mfx-font-icon {
+    -mfx-color: #4D4D4D
+}
+
+.mfx-evaluation-box:hover .mfx-font-icon {
+    -mfx-color: #EF6E6B
+}

+ 23 - 0
materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-filterdialog.css

@@ -0,0 +1,23 @@
+.mfx-filter-dialog {
+    -mfx-base-color: rgb(76, 0, 225);
+}
+
+.mfx-filter-dialog #headerLabel {
+    -fx-background-color: transparent;
+    -fx-border-color: transparent;
+    -mfx-font-family: "Open Sans SemiBold";
+}
+
+.mfx-filter-dialog .mfx-button {
+    -fx-border-color: -mfx-base-color;
+    -fx-border-radius: 5;
+    -fx-font-family: "Open Sans SemiBold";
+}
+
+.mfx-filter-dialog .mfx-button .text {
+    -fx-fill: -mfx-base-color;
+}
+
+.mfx-filter-dialog .mfx-button .mfx-ripple-generator {
+    -mfx-ripple-color: derive(-mfx-base-color, 150%)
+}

+ 9 - 14
materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-togglenode.css → materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-passwordfield.css

@@ -16,21 +16,16 @@
  *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-.mfx-toggle-node,
-.mfx-toggle-node:armed,
-.mfx-toggle-node:hover,
-.mfx-toggle-node:focused,
-.mfx-toggle-node:selected,
-.mfx-toggle-node:focused:selected {
-    -fx-background-color: TRANSPARENT, TRANSPARENT, TRANSPARENT, TRANSPARENT;
-    -fx-background-radius: 3px;
-    -fx-background-insets: 0px;
-}
+@import "mfx-textfield.css";
 
-.mfx-toggle-node {
-    -fx-opacity: 0.5;
+.mfx-password-field .mfx-icon-wrapper {
+    -fx-opacity: 0.7;
 }
 
-.mfx-toggle-node:selected {
+.mfx-password-field .mfx-icon-wrapper:hover {
     -fx-opacity: 1.0;
-}
+}
+
+.mfx-password-field .mfx-icon-wrapper .mfx-ripple-generator {
+    -mfx-animation-speed: 2;
+}

+ 58 - 0
materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-rectangle-togglenode.css

@@ -0,0 +1,58 @@
+/*
+ *     Copyright (C) 2021 Parisi Alessandro
+ *     This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
+ *
+ *     MaterialFX is free software: you can redistribute it and/or modify
+ *     it under the terms of the GNU General Public License as published by
+ *     the Free Software Foundation, either version 3 of the License, or
+ *     (at your option) any later version.
+ *
+ *     MaterialFX is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *     GNU General Public License for more details.
+ *
+ *     You should have received a copy of the GNU General Public License
+ *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+@import "fonts.css";
+
+.mfx-toggle-node {
+    -mfx-unselected-color: #F9F9F9;
+    -mfx-selected-color: #EDEDED;
+    -mfx-unselected-border-color: #E1E1E1;
+    -mfx-selected-border-color: #E1E1E1;
+}
+
+.mfx-toggle-node,
+.mfx-toggle-node:armed,
+.mfx-toggle-node:hover,
+.mfx-toggle-node:focused,
+.mfx-toggle-node:selected,
+.mfx-toggle-node:focused:selected {
+    -fx-background-color: TRANSPARENT, TRANSPARENT, TRANSPARENT, TRANSPARENT;
+    -fx-background-radius: 3px;
+    -fx-background-insets: 0px;
+}
+
+.mfx-toggle-node {
+    -fx-background-color: -mfx-unselected-color;
+    -fx-background-radius: 10;
+    -fx-border-color: -mfx-unselected-border-color;
+    -fx-border-radius: 10;
+}
+
+.mfx-toggle-node:selected {
+    -fx-background-color: -mfx-selected-color;
+    -fx-background-radius: 10;
+    -fx-border-color: -mfx-selected-border-color;
+    -fx-border-radius: 10;
+}
+
+.mfx-toggle-node .mfx-label {
+    -mfx-line-color: transparent;
+    -mfx-unfocused-line-color: transparent;
+    -mfx-font-family: 'Open Sans Regular';
+    -fx-background-color: transparent;
+}

+ 0 - 53
materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-table-column-cell.css

@@ -1,53 +0,0 @@
-/*
- *     Copyright (C) 2021 Parisi Alessandro
- *     This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
- *
- *     MaterialFX is free software: you can redistribute it and/or modify
- *     it under the terms of the GNU General Public License as published by
- *     the Free Software Foundation, either version 3 of the License, or
- *     (at your option) any later version.
- *
- *     MaterialFX is distributed in the hope that it will be useful,
- *     but WITHOUT ANY WARRANTY; without even the implied warranty of
- *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *     GNU General Public License for more details.
- *
- *     You should have received a copy of the GNU General Public License
- *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-@import "mfx-label-style2.css";
-
-.mfx-table-column-cell {
-    -fx-background-radius: 0;
-    -fx-border-radius: 0;
-    -fx-border-width: 0.5;
-}
-
-.mfx-table-column-cell,
-.mfx-table-column-cell:focused {
-    -fx-border-color: transparent;
-}
-
-.mfx-table-column-cell:hover,
-.mfx-table-column-cell:dragged {
-    -fx-border-color: transparent rgba(46, 52, 64, 0.3) transparent transparent;
-    -fx-border-width: 0.5;
-}
-
-/* POPUP */
-
-.filter-box {
-    -fx-background-color: rgb(250, 250, 250);
-    -fx-border-color: rgba(46, 52, 64, 0.3);
-    -fx-border-width: 0.5;
-    -fx-border-radius: 3;
-}
-
-.filter-box .mfx-label {
-    -fx-border-color: transparent;
-}
-
-.filter-box .separator {
-    -fx-background-color: rgba(46, 52, 64, 0.3);
-}

+ 8 - 0
materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-tablecolumn.css

@@ -0,0 +1,8 @@
+.mfx-table-column {
+    -fx-border-color: white;
+}
+
+.mfx-table-column:resizable:hover,
+.mfx-table-column:resizable:dragged {
+    -fx-border-color: transparent lightgray transparent transparent;
+}

+ 9 - 0
materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-tablerow.css

@@ -0,0 +1,9 @@
+.mfx-table-row {
+    -fx-background-insets: 0 -5 0 -5;
+    -fx-background-color: white;
+    -fx-border-color: transparent;
+}
+
+.mfx-table-row:selected {
+    -fx-background-color: #F4F4F4;
+}

+ 7 - 54
materialfx/src/main/resources/io/github/palexdev/materialfx/css/mfx-tableview.css

@@ -1,61 +1,14 @@
-/*
- *     Copyright (C) 2021 Parisi Alessandro
- *     This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
- *
- *     MaterialFX is free software: you can redistribute it and/or modify
- *     it under the terms of the GNU General Public License as published by
- *     the Free Software Foundation, either version 3 of the License, or
- *     (at your option) any later version.
- *
- *     MaterialFX is distributed in the hope that it will be useful,
- *     but WITHOUT ANY WARRANTY; without even the implied warranty of
- *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *     GNU General Public License for more details.
- *
- *     You should have received a copy of the GNU General Public License
- *     along with MaterialFX.  If not, see <http://www.gnu.org/licenses/>.
- */
-
 .mfx-table-view {
-    -fx-table-borders: rgba(46, 52, 64, 0.3);
-}
-
-.mfx-table-view .container {
-    -fx-border-color: -fx-table-borders;
-    -fx-border-radius: 3;
     -fx-background-color: white;
-    -fx-background-radius: 3;
-}
-
-.mfx-table-view .container .columns-container {
-    -fx-border-color: transparent transparent -fx-table-borders transparent;
-}
-
-.mfx-table-view .container .rows-container {
-    -fx-border-color: transparent;
+    -fx-background-radius: 5;
+    -fx-border-color: lightgray;
+    -fx-border-radius: 5;
 }
 
-.mfx-table-view .pagination {
-    -fx-border-color: -fx-table-borders transparent transparent transparent;
-}
-
-.mfx-table-view .pagination .mfx-icon-wrapper:disabled .mfx-font-icon {
-    -mfx-color: rgba(46, 52, 64, 0.3);
-
-}
-
-.mfx-table-view .pagination * .mfx-ripple-generator {
-    -mfx-ripple-color: rgba(127, 15, 255, 0.6);
-    -mfx-animation-speed: 2.0;
-}
-
-/* END */
-
-#rowLoading .track {
-    -fx-background-radius: 0;
-    -fx-background-color: rgba(200, 200, 200, 0.1);
+.mfx-table-view #columns-container {
+    -fx-border-color: transparent transparent lightgray transparent;
 }
 
-#rowLoading .bar {
-    -fx-background-color: rgba(127, 15, 255, 0.8);
+.mfx-table-view .pagination-controls-container {
+    -fx-border-color: lightgray transparent transparent transparent;
 }

BIN
materialfx/src/main/resources/io/github/palexdev/materialfx/fonts/materialfx-resources.ttf


Some files were not shown because too many files changed in this diff