├── contributors.txt ├── settings.gradle ├── .gitignore ├── iOSFX.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src └── main │ ├── resources │ └── eu │ │ └── hansolo │ │ └── iosfx │ │ ├── fonts │ │ ├── Roboto-Bold.ttf │ │ ├── Roboto-Thin.ttf │ │ ├── Roboto-Light.ttf │ │ ├── Roboto-Medium.ttf │ │ └── Roboto-Regular.ttf │ │ ├── iosentry │ │ └── ios-entry.css │ │ ├── iosplusminusbutton │ │ └── ios-plus-minus-button.css │ │ ├── ios.css │ │ ├── ioslistview │ │ └── ios-listview.css │ │ ├── iosswitch │ │ └── ios-switch.css │ │ ├── iossegmentedbuttonbar │ │ └── ios-segmented-buttonbar.css │ │ ├── iosslider │ │ └── ios-slider.css │ │ └── iosmultibutton │ │ └── ios-multibutton.css │ └── java │ └── eu │ └── hansolo │ └── iosfx │ ├── events │ ├── IosEventListener.java │ ├── IosEventType.java │ └── IosEvent.java │ ├── iosentry │ ├── IosEntryCell.java │ └── IosEntry.java │ ├── tools │ └── Helper.java │ ├── common │ └── IosColor.java │ ├── iosslider │ ├── IosSlider.java │ └── IosSliderSkin.java │ ├── fonts │ └── Fonts.java │ ├── iossegmentedbuttonbar │ └── IosSegmentedButtonBar.java │ ├── ioslistview │ └── IosListView.java │ ├── iosmultibutton │ ├── IosMultiButtonBuilder.java │ └── IosMultiButton.java │ ├── iosswitch │ ├── IosSwitchBuilder.java │ └── IosSwitch.java │ ├── iosplusminusbutton │ └── IosPlusMinusButton.java │ └── Demo.java ├── README.md ├── gradlew.bat ├── gradlew └── license.txt /contributors.txt: -------------------------------------------------------------------------------- 1 | Gerrit Grunwald 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'iosfx' 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /out 2 | /.idea 3 | /.gradle 4 | /build 5 | .DS_Store -------------------------------------------------------------------------------- /iOSFX.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanSolo/iosfx/HEAD/iOSFX.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanSolo/iosfx/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/resources/eu/hansolo/iosfx/fonts/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanSolo/iosfx/HEAD/src/main/resources/eu/hansolo/iosfx/fonts/Roboto-Bold.ttf -------------------------------------------------------------------------------- /src/main/resources/eu/hansolo/iosfx/fonts/Roboto-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanSolo/iosfx/HEAD/src/main/resources/eu/hansolo/iosfx/fonts/Roboto-Thin.ttf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## iOS FX 2 | A JavaFX collection of iOS controls. 3 | 4 | ## Overview 5 | ![Overview](https://raw.githubusercontent.com/HanSolo/iosfx/master/iOSFX.png) 6 | -------------------------------------------------------------------------------- /src/main/resources/eu/hansolo/iosfx/fonts/Roboto-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanSolo/iosfx/HEAD/src/main/resources/eu/hansolo/iosfx/fonts/Roboto-Light.ttf -------------------------------------------------------------------------------- /src/main/resources/eu/hansolo/iosfx/fonts/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanSolo/iosfx/HEAD/src/main/resources/eu/hansolo/iosfx/fonts/Roboto-Medium.ttf -------------------------------------------------------------------------------- /src/main/resources/eu/hansolo/iosfx/fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanSolo/iosfx/HEAD/src/main/resources/eu/hansolo/iosfx/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-bin.zip 6 | -------------------------------------------------------------------------------- /src/main/java/eu/hansolo/iosfx/events/IosEventListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 by Gerrit Grunwald 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package eu.hansolo.iosfx.events; 18 | 19 | @FunctionalInterface 20 | public interface IosEventListener { 21 | void onIosEvent(final IosEvent EVT); 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/eu/hansolo/iosfx/events/IosEventType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 by Gerrit Grunwald 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package eu.hansolo.iosfx.events; 18 | 19 | public enum IosEventType { 20 | SELECTED, DESELECTED, DELETE_ENTRY, ADD_ENTRY, PRESSED, RELEASED, INCREASE, DECREASE 21 | } 22 | -------------------------------------------------------------------------------- /src/main/resources/eu/hansolo/iosfx/iosentry/ios-entry.css: -------------------------------------------------------------------------------- 1 | .ios-entry { 2 | -title-fill : rgb(0, 0, 0); 3 | -subtitle-fill : rgb(216, 216, 216); 4 | -action-fill : rgb(142, 142, 147); 5 | -delete-fill : rgb(255, 59, 48); 6 | -button-text-fill: rgb( 255, 255, 255); 7 | } 8 | 9 | .ios-entry .title { 10 | -fx-font-size: 17px; 11 | -fx-text-fill: -title-fill; 12 | } 13 | 14 | .ios-entry .subtitle { 15 | -fx-font-size: 12px; 16 | -fx-text-fill: -subtitle-fill; 17 | } 18 | 19 | .ios-entry .action { 20 | -fx-text-fill : -button-text-fill; 21 | -fx-alignment : center; 22 | -fx-font-size : 18px; 23 | -fx-background-color: -action-fill; 24 | } 25 | 26 | .ios-entry .delete { 27 | -fx-text-fill : -button-text-fill; 28 | -fx-alignment : center; 29 | -fx-font-size : 18px; 30 | -fx-background-color: -delete-fill; 31 | } 32 | -------------------------------------------------------------------------------- /src/main/resources/eu/hansolo/iosfx/iosplusminusbutton/ios-plus-minus-button.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 by Gerrit Grunwald 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | .ios-plus-minus-button { 18 | 19 | } 20 | 21 | .ios-plus-minus-button .minus-button .minus-sign { 22 | -fx-fill: rgb(0, 122, 255); 23 | } 24 | .ios-plus-minus-button .minus-button:pressed .minus-sign { 25 | -fx-fill: white; 26 | } 27 | 28 | .ios-plus-minus-button .plus-button .plus-sign { 29 | -fx-fill: rgb(0, 122, 255); 30 | } 31 | .ios-plus-minus-button .plus-button:pressed .plus-sign { 32 | -fx-fill: white; 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/main/java/eu/hansolo/iosfx/events/IosEvent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 by Gerrit Grunwald 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package eu.hansolo.iosfx.events; 18 | 19 | 20 | public class IosEvent { 21 | public final Object SRC; 22 | public final IosEventType TYPE; 23 | 24 | 25 | // ******************** Constructors ************************************** 26 | public IosEvent(final Object SRC, final IosEventType TYPE) { 27 | this.SRC = SRC; 28 | this.TYPE = TYPE; 29 | } 30 | 31 | 32 | // ******************** Methods ******************************************* 33 | public Object getSource() { return SRC; } 34 | 35 | public IosEventType getType() { return TYPE; } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/eu/hansolo/iosfx/iosentry/IosEntryCell.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 by Gerrit Grunwald 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package eu.hansolo.iosfx.iosentry; 18 | 19 | import javafx.geometry.Insets; 20 | import javafx.scene.control.ContentDisplay; 21 | import javafx.scene.control.ListCell; 22 | 23 | 24 | public class IosEntryCell extends ListCell { 25 | 26 | @Override protected void updateItem(final IosEntry ENTRY, final boolean IS_EMPTY) { 27 | super.updateItem(ENTRY, IS_EMPTY); 28 | setContentDisplay(ContentDisplay.GRAPHIC_ONLY); 29 | setPadding(Insets.EMPTY); 30 | if (IS_EMPTY) { 31 | setText(null); 32 | setGraphic(null); 33 | } else { 34 | setText(null); 35 | setGraphic(ENTRY); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/resources/eu/hansolo/iosfx/ios.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 by Gerrit Grunwald 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 18 | /********** iOS General *********/ 19 | .ios { 20 | -RED : rgb(255, 59, 48); 21 | -ORANGE : rgb(255, 149, 0); 22 | -YELLOW : rgb(255, 204, 0); 23 | -GREEN : rgb(76, 217, 100); 24 | -TEAL-BLUE : rgb(90, 200, 250); 25 | -BLUE : rgb(0, 122, 255); 26 | -PURPLE : rgb(88, 86, 214); 27 | -PINK : rgb(255, 45, 85); 28 | -WHITE : rgb(255, 255, 255); 29 | -CUSTOM-GRAY: rgb(229, 239, 244); 30 | -LIGHT-GRAY : rgb(229 ,229, 234); 31 | -LIGHT-GRAY2: rgb(209, 209, 214); 32 | -MID-GRAY : rgb(199, 199, 204); 33 | -GRAY : rgb(142, 142, 147); 34 | -DARK-GRAY : rgb(102, 102, 102); 35 | -BLACK : rgb(0, 0, 0); 36 | } 37 | 38 | -------------------------------------------------------------------------------- /src/main/resources/eu/hansolo/iosfx/ioslistview/ios-listview.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 by Gerrit Grunwald 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | .ios-list-view { 18 | 19 | } 20 | 21 | .list-cell { 22 | -fx-background : -fx-control-inner-background; 23 | -fx-background-color: rgb(255, 255, 255); 24 | -fx-text-fill : -fx-text-background-color; 25 | -fx-border-color : rgb(229 ,229, 234); 26 | -fx-border-width : 0 0 1 0; 27 | } 28 | 29 | .list-cell:filled:selected:focused, .list-cell:filled:selected { 30 | -fx-background-color: rgb(255, 255, 255); 31 | -fx-text-fill : white; 32 | } 33 | 34 | .list-cell:odd { 35 | -fx-cell-hover-color: transparent; 36 | -fx-background-color: rgb(255, 255, 255); 37 | } 38 | 39 | .list-cell:filled:hover { 40 | -fx-cell-hover-color: transparent; 41 | -fx-text-fill : rgb(0, 0, 0); 42 | } 43 | 44 | .list-cell:empty { 45 | visibility:hidden; 46 | } -------------------------------------------------------------------------------- /src/main/java/eu/hansolo/iosfx/tools/Helper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 by Gerrit Grunwald 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package eu.hansolo.iosfx.tools; 18 | 19 | import javafx.scene.Node; 20 | 21 | import java.util.HashMap; 22 | 23 | 24 | public class Helper { 25 | public static final int ANIMATION_DURATION = 75; 26 | 27 | public static final void enableNode(final Node NODE, final boolean ENABLE) { 28 | NODE.setVisible(ENABLE); 29 | NODE.setManaged(ENABLE); 30 | } 31 | 32 | public static final HashMap copy(final HashMap ORIGINAL) { 33 | HashMap COPY = new HashMap<>(); 34 | ORIGINAL.entrySet().forEach(entry -> COPY.put(entry.getKey(), entry.getValue())); 35 | return COPY; 36 | } 37 | 38 | public static final double clamp(final double MIN, final double MAX, final double VALUE) { 39 | if (VALUE < MIN) return MIN; 40 | if (VALUE > MAX) return MAX; 41 | return VALUE; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/resources/eu/hansolo/iosfx/iosswitch/ios-switch.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 by Gerrit Grunwald 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | .ios-switch { 18 | -background-color : rgb(229, 229, 229); 19 | -dark-main-color : rgb(0, 0, 0); 20 | -selected-color : rgb(75, 216, 99); 21 | -knob-color : rgb(255, 255, 255); 22 | } 23 | 24 | 25 | /* Bright scheme */ 26 | .ios-switch .background-area { 27 | -fx-fill: -background-color; 28 | } 29 | 30 | .ios-switch .one { 31 | -fx-fill: rgb(255, 255, 255); 32 | } 33 | 34 | 35 | .ios-switch .main-area { 36 | -fx-fill: linear-gradient(from 0% 0% to 100% 0%, rgb(244, 244, 244) 35%, rgb(255, 255, 255) 80%); 37 | } 38 | 39 | .ios-switch .zero { 40 | -fx-fill : null; 41 | -fx-stroke: rgb(175, 175, 175); 42 | } 43 | 44 | .ios-switch .knob { 45 | -fx-fill: -knob-color; 46 | } 47 | 48 | 49 | /* Dark scheme */ 50 | .ios-switch:dark .background-area { 51 | -fx-fill: -background-color; 52 | } 53 | 54 | .ios-switch:dark .one { 55 | -fx-fill: rgb(255, 255, 255); 56 | } 57 | 58 | 59 | .ios-switch:dark .main-area { 60 | -fx-fill: -dark-main-color; 61 | } 62 | 63 | .ios-switch:dark .zero { 64 | -fx-fill : null; 65 | -fx-stroke: rgb(175, 175, 175); 66 | } 67 | 68 | .ios-switch:dark .knob { 69 | -fx-fill: -knob-color; 70 | } -------------------------------------------------------------------------------- /src/main/java/eu/hansolo/iosfx/common/IosColor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 by Gerrit Grunwald 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package eu.hansolo.iosfx.common; 18 | 19 | import javafx.scene.paint.Color; 20 | 21 | 22 | public enum IosColor { 23 | RED(Color.rgb(255, 59, 48), "RED"), 24 | ORANGE(Color.rgb(255, 149, 0), "ORANGE"), 25 | YELLOW(Color.rgb(255, 204, 0), "YELLOW"), 26 | GREEN(Color.rgb(76, 217, 100), "GREEN"), 27 | TEAL_BLUE(Color.rgb(90, 200, 250), "TEAL-BLUE"), 28 | BLUE(Color.rgb(0, 122, 255), "BLUE"), 29 | PURPLE(Color.rgb(88, 86, 214), "PURPLE"), 30 | PINK(Color.rgb(255, 45, 85), "PINK"), 31 | WHITE(Color.rgb(255, 255, 255), "WHITE"), 32 | CUSTOM_GRAY(Color.rgb(229, 239, 244), "CUSTOM-GRAY"), 33 | LIGHT_GRAY(Color.rgb(229 ,229, 234), "LIGHT-GRAY"), 34 | LIGHT_GRAY2(Color.rgb(209, 209, 214), "LIGHT-GRAY2"), 35 | MID_GRAY(Color.rgb(199, 199, 204), "MID-GRAY"), 36 | GRAY(Color.rgb(142, 142, 147), "GRAY"), 37 | DARK_GRAY(Color.rgb(102, 102, 102), "DARK-GRAY"), 38 | BLACK(Color.rgb(0, 0, 0), "BLACK"); 39 | 40 | 41 | private final Color COLOR; 42 | private final String STYLE_CLASS; 43 | 44 | 45 | IosColor(final Color COLOR, final String STYLE_CLASS) { 46 | this.COLOR = COLOR; 47 | this.STYLE_CLASS = STYLE_CLASS; 48 | } 49 | 50 | 51 | public Color color() { return COLOR; } 52 | 53 | public String styleClass() { return STYLE_CLASS; } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/resources/eu/hansolo/iosfx/iossegmentedbuttonbar/ios-segmented-buttonbar.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 by Gerrit Grunwald 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | ios-segmented-button-bar { 18 | -blue: rgb(0, 122, 255);; 19 | } 20 | 21 | .ios-segmented-button-bar .button, 22 | .ios-segmented-button-bar .toggle-button { 23 | -fx-text-fill : rgb(0, 122, 255); 24 | -fx-background-color : transparent; 25 | -fx-background-insets: 0, 1 1 1 0, 2 1 1 1; 26 | -fx-background-radius: 0; 27 | -fx-padding : 5 15 5 15; 28 | -fx-border-color : rgb(0, 122, 255) rgb(0, 122, 255) rgb(0, 122, 255) transparent; 29 | -fx-border-width : 1px; 30 | -fx-border-radius : 0; 31 | } 32 | 33 | .ios-segmented-button-bar .button.first, 34 | .ios-segmented-button-bar .toggle-button.first { 35 | -fx-background-insets: 0, 1, 2 1 1 1; 36 | -fx-background-radius: 3 0 0 3, 2 0 0 2, 2 0 0 2; 37 | -fx-border-radius : 3 0 0 3, 2 0 0 2, 2 0 0 2; 38 | -fx-border-color : rgb(0, 122, 255); 39 | } 40 | 41 | .ios-segmented-button-bar .button.last, 42 | .ios-segmented-button-bar .toggle-button.last { 43 | -fx-background-insets: 0, 1 1 1 0, 2 1 1 1; 44 | -fx-background-radius: 0 3 3 0, 0 2 2 0, 0 2 2 0; 45 | -fx-border-radius : 0 3 3 0, 0 2 2 0, 0 2 2 0; 46 | -fx-border-insets : 0 0 0 -1; 47 | } 48 | 49 | .ios-segmented-button-bar .button:pressed, 50 | .ios-segmented-button-bar .toggle-button:selected { 51 | -fx-background-color: rgb(0, 122, 255); 52 | -fx-text-fill : white; 53 | } 54 | 55 | .tool-bar { 56 | -fx-base : black; 57 | -fx-font-size : 13px; 58 | -fx-background-color : transparent; 59 | -fx-background-insets: 0, 0 0 1 0; 60 | -fx-effect : null; 61 | } 62 | 63 | .tool-bar .spacer { 64 | -fx-padding: 0 5.417em 0 0; 65 | } -------------------------------------------------------------------------------- /src/main/java/eu/hansolo/iosfx/iosslider/IosSlider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 by Gerrit Grunwald 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package eu.hansolo.iosfx.iosslider; 18 | 19 | import javafx.beans.property.BooleanProperty; 20 | import javafx.beans.property.BooleanPropertyBase; 21 | import javafx.css.PseudoClass; 22 | import javafx.scene.control.Skin; 23 | import javafx.scene.control.Slider; 24 | 25 | 26 | public class IosSlider extends Slider { 27 | 28 | private static final PseudoClass BALANCE_PSEUDO_CLASS = PseudoClass.getPseudoClass("balance"); 29 | 30 | private BooleanProperty balance = new BooleanPropertyBase(false) { 31 | @Override protected void invalidated() { pseudoClassStateChanged(BALANCE_PSEUDO_CLASS, get()); } 32 | @Override public Object getBean() { return IosSlider.this; } 33 | @Override public String getName() { return "balance"; } 34 | }; 35 | 36 | 37 | public IosSlider() { 38 | super(); 39 | } 40 | public IosSlider(final double MIN, final double MAX, final double VALUE) { 41 | super(MIN, MAX, VALUE); 42 | } 43 | 44 | 45 | public boolean getBalance() { return balance.get(); } 46 | public void setBalance(final boolean BALANCE) { balance.set(BALANCE); } 47 | public BooleanProperty balanceProperty() { return balance; } 48 | 49 | public double getRange() { return (getMax() - getMin()); } 50 | 51 | public double getBalanceValue() { return getValue() - (getRange() * 0.5); } 52 | 53 | 54 | @Override protected Skin createDefaultSkin() { 55 | return new IosSliderSkin(this); 56 | } 57 | 58 | 59 | // ******************** Style related ************************************* 60 | @Override public String getUserAgentStylesheet() { 61 | return IosSlider.class.getResource("ios-slider.css").toExternalForm(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /src/main/java/eu/hansolo/iosfx/fonts/Fonts.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 by Gerrit Grunwald 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package eu.hansolo.iosfx.fonts; 18 | 19 | import javafx.scene.text.Font; 20 | 21 | 22 | public class Fonts { 23 | private static final String ROBOTO_THIN_NAME; 24 | private static final String ROBOTO_LIGHT_NAME; 25 | private static final String ROBOTO_REGULAR_NAME; 26 | private static final String ROBOTO_MEDIUM_NAME; 27 | private static final String ROBOTO_BOLD_NAME; 28 | 29 | private static String robotoThinName; 30 | private static String robotoLightName; 31 | private static String robotoRegularName; 32 | private static String robotoMediumName; 33 | private static String robotoBoldName; 34 | 35 | 36 | static { 37 | try { 38 | robotoThinName = Font.loadFont(Fonts.class.getResourceAsStream("/eu/hansolo/iosfx/fonts/Roboto-Thin.ttf"), 10).getName(); 39 | robotoLightName = Font.loadFont(Fonts.class.getResourceAsStream("/eu/hansolo/iosfx/fonts/Roboto-Light.ttf"), 10).getName(); 40 | robotoRegularName = Font.loadFont(Fonts.class.getResourceAsStream("/eu/hansolo/iosfx/fonts/Roboto-Regular.ttf"), 10).getName(); 41 | robotoMediumName = Font.loadFont(Fonts.class.getResourceAsStream("/eu/hansolo/iosfx/fonts/Roboto-Medium.ttf"), 10).getName(); 42 | robotoBoldName = Font.loadFont(Fonts.class.getResourceAsStream("/eu/hansolo/iosfx/fonts/Roboto-Bold.ttf"), 10).getName(); 43 | } catch (Exception exception) { } 44 | ROBOTO_THIN_NAME = robotoThinName; 45 | ROBOTO_LIGHT_NAME = robotoLightName; 46 | ROBOTO_REGULAR_NAME = robotoRegularName; 47 | ROBOTO_MEDIUM_NAME = robotoMediumName; 48 | ROBOTO_BOLD_NAME = robotoBoldName; 49 | } 50 | 51 | 52 | // ******************** Methods ******************************************* 53 | public static Font robotoThin(final double SIZE) { return new Font(ROBOTO_THIN_NAME, SIZE); } 54 | public static Font robotoLight(final double SIZE) { return new Font(ROBOTO_LIGHT_NAME, SIZE); } 55 | public static Font robotoRegular(final double SIZE) { return new Font(ROBOTO_REGULAR_NAME, SIZE); } 56 | public static Font robotoMedium(final double SIZE) { return new Font(ROBOTO_MEDIUM_NAME, SIZE); } 57 | public static Font robotoBold(final double SIZE) { return new Font(ROBOTO_BOLD_NAME, SIZE); } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/resources/eu/hansolo/iosfx/iosslider/ios-slider.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 by Gerrit Grunwald 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | .slider { 18 | -thumb-fill : white; 19 | -track-fill : rgba(0, 0, 0, 0.3); 20 | -track-progress-fill: rgb(0, 122, 255); 21 | } 22 | 23 | .slider .track { 24 | -fx-background-color : -track-fill; 25 | -fx-background-insets: 0; 26 | -fx-background-radius: 1 1 1 1; 27 | -fx-padding : 0.5px; 28 | } 29 | .slider:vertical .track { 30 | -fx-background-color: -track-fill; 31 | } 32 | 33 | .slider:balance .track { 34 | -fx-background-color : -track-fill; 35 | -fx-background-insets: 0; 36 | -fx-background-radius: 1 1 1 1; 37 | -fx-padding : 0.5px; 38 | -fx-border-color : transparent -track-fill transparent -track-fill; 39 | /*-fx-border : 0 1 0 1;*/ 40 | -fx-border-width : 1; 41 | -fx-border-insets : -3 0 -3 0; 42 | 43 | } 44 | .slider:balance:vertical .track { 45 | -fx-background-color: -track-fill; 46 | -fx-border-color : transparent -track-fill transparent -track-fill; 47 | /*-fx-border : 1 0 1 0;*/ 48 | -fx-border-width : 1; 49 | -fx-border-insets : 0 -3 0 -3; 50 | } 51 | 52 | .slider .center-line { 53 | -fx-stroke : transparent; 54 | -fx-stroke-width: 1px; 55 | } 56 | .slider:balance .center-line, 57 | .slider:balance:vertical .center-line { 58 | -fx-stroke: -track-fill; 59 | } 60 | 61 | .slider .track-progress { 62 | -fx-background-color : -track-progress-fill; 63 | -fx-background-insets: 0; 64 | -fx-background-radius: 1 1 1 1; 65 | -fx-padding : 0.5px; 66 | } 67 | .slider:vertical .track-progress { 68 | -fx-background-color: -track-progress-fill; 69 | } 70 | 71 | .slider .axis { 72 | -fx-tick-label-fill : derive(-fx-text-background-color, 30%); 73 | -fx-tick-length : 5px; 74 | -fx-minor-tick-length: 3px; 75 | -fx-border-color : null; 76 | } 77 | .slider .thumb { 78 | -fx-padding : 14px; 79 | -fx-background-color : -thumb-fill; 80 | -fx-background-insets: 0; 81 | -fx-background-radius: 14px; 82 | -fx-effect : dropshadow(two-pass-box, rgba(0, 0, 0, 0.25), 5, 0.0, 0, 2); 83 | } 84 | .slider .thumb:hover { 85 | -fx-background-color: -thumb-fill; 86 | } 87 | 88 | .slider:focused .thumb { 89 | -fx-background-color: -thumb-fill; 90 | } 91 | .slider:pressed .thumb { 92 | -fx-background-color: -thumb-fill; 93 | } -------------------------------------------------------------------------------- /src/main/java/eu/hansolo/iosfx/iossegmentedbuttonbar/IosSegmentedButtonBar.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 by Gerrit Grunwald 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package eu.hansolo.iosfx.iossegmentedbuttonbar; 18 | 19 | import javafx.collections.ListChangeListener; 20 | import javafx.scene.Node; 21 | import javafx.scene.layout.HBox; 22 | 23 | 24 | public class IosSegmentedButtonBar extends HBox { 25 | 26 | public IosSegmentedButtonBar() { 27 | super(); 28 | getStyleClass().add("ios-segmented-button-bar"); 29 | 30 | registerListeners(); 31 | } 32 | public IosSegmentedButtonBar(final double SPACING) { 33 | this(); 34 | setSpacing(0); 35 | } 36 | public IosSegmentedButtonBar(final Node... CHILDREN) { 37 | super(); 38 | getChildren().addAll(CHILDREN); 39 | getStyleClass().add("ios-segmented-button-bar"); 40 | adjustStyles(); 41 | 42 | registerListeners(); 43 | } 44 | public IosSegmentedButtonBar(final double SPACING, final Node... CHILDREN) { 45 | this(); 46 | setSpacing(0); 47 | getChildren().addAll(CHILDREN); 48 | } 49 | 50 | 51 | private void registerListeners() { 52 | spacingProperty().addListener(o -> setSpacing(0)); 53 | 54 | getChildren().addListener((ListChangeListener) CHANGE -> { 55 | while (CHANGE.next()) { 56 | if (CHANGE.wasAdded()) { 57 | adjustStyles(); 58 | } else if (CHANGE.wasRemoved()) { 59 | adjustStyles(); 60 | } 61 | } 62 | }); 63 | } 64 | 65 | private void adjustStyles() { 66 | if (getChildren().isEmpty()) { return; } 67 | int noOfChildren = getChildren().size(); 68 | for (int i = 0 ; i < noOfChildren ; i++) { 69 | Node node = getChildren().get(i); 70 | if (node.getStyleClass().contains("first")) { node.getStyleClass().remove("first"); } 71 | if (node.getStyleClass().contains("last")) { node.getStyleClass().remove("last"); } 72 | 73 | if (i == 0) { node.getStyleClass().add("first"); } 74 | if (i > 0 && i == noOfChildren - 1) { node.getStyleClass().add("last"); } 75 | } 76 | } 77 | 78 | 79 | // ******************** Style related ************************************* 80 | @Override public String getUserAgentStylesheet() { 81 | return IosSegmentedButtonBar.class.getResource("ios-segmented-buttonbar.css").toExternalForm(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/eu/hansolo/iosfx/ioslistview/IosListView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 by Gerrit Grunwald 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package eu.hansolo.iosfx.ioslistview; 18 | 19 | import eu.hansolo.iosfx.events.IosEvent; 20 | import eu.hansolo.iosfx.events.IosEventListener; 21 | import eu.hansolo.iosfx.iosentry.IosEntry; 22 | import eu.hansolo.iosfx.tools.Helper; 23 | import javafx.animation.Interpolator; 24 | import javafx.animation.KeyFrame; 25 | import javafx.animation.KeyValue; 26 | import javafx.animation.Timeline; 27 | import javafx.collections.FXCollections; 28 | import javafx.collections.ListChangeListener; 29 | import javafx.collections.ObservableList; 30 | import javafx.scene.control.ListView; 31 | import javafx.util.Duration; 32 | 33 | 34 | public class IosListView extends ListView implements IosEventListener { 35 | private Timeline timeline; 36 | 37 | public IosListView() { 38 | this(FXCollections.observableArrayList()); 39 | } 40 | public IosListView(final ObservableList ENTRIES) { 41 | super(ENTRIES); 42 | timeline = new Timeline(); 43 | getStylesheets().add(IosListView.class.getResource("ios-listview.css").toExternalForm()); 44 | getStyleClass().add("ios-list-view"); 45 | 46 | registerListeners(); 47 | } 48 | 49 | 50 | private void registerListeners() { 51 | getItems().forEach(entry -> entry.addOnIosEvent(IosListView.this)); 52 | 53 | getItems().addListener((ListChangeListener) change -> { 54 | while (change.next()) { 55 | if (change.wasAdded()) { 56 | change.getAddedSubList().forEach(addedItem -> addedItem.addOnIosEvent(IosListView.this)); 57 | } else if (change.wasRemoved()) { 58 | change.getRemoved().forEach(removedItem -> removedItem.removeOnIosEvent(IosListView.this)); 59 | } 60 | } 61 | }); 62 | } 63 | 64 | @Override public void onIosEvent(final IosEvent EVT) { 65 | switch(EVT.TYPE) { 66 | case DELETE_ENTRY: 67 | IosEntry entry = (IosEntry) EVT.SRC; 68 | KeyValue kvEntryHeightStart = new KeyValue(entry.prefHeightProperty(), entry.getPrefHeight(), Interpolator.EASE_BOTH); 69 | KeyValue kvEntryHeightEnd = new KeyValue(entry.prefHeightProperty(), 0, Interpolator.EASE_BOTH); 70 | 71 | KeyFrame kf0 = new KeyFrame(Duration.ZERO, kvEntryHeightStart); 72 | KeyFrame kf1 = new KeyFrame(Duration.millis(2 * Helper.ANIMATION_DURATION), kvEntryHeightEnd); 73 | 74 | timeline.getKeyFrames().setAll(kf0, kf1); 75 | timeline.setOnFinished(e -> getItems().remove(entry)); 76 | timeline.play(); 77 | break; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/resources/eu/hansolo/iosfx/iosmultibutton/ios-multibutton.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 by Gerrit Grunwald 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | .ios-multi-button { 18 | -circle-color : rgb(199, 199, 204); 19 | -add-color : rgb( 75, 216, 99); 20 | -delete-color : rgb(255, 59, 48); 21 | -symbol-color : rgb(255, 255, 255); 22 | -selected-color : rgb( 0, 122, 255); 23 | } 24 | 25 | .ios-multi-button .circle { 26 | -fx-stroke: -circle-color; 27 | -fx-fill : transparent; 28 | } 29 | .ios-multi-button .icon { 30 | -fx-fill: transparent; 31 | } 32 | 33 | .ios-multi-button:checkbox:selected .circle { 34 | -fx-stroke: transparent; 35 | -fx-fill : -selected-color; 36 | } 37 | .ios-multi-button:checkbox:selected .icon { 38 | -fx-background-color: -symbol-color; 39 | -fx-rotate : 225; 40 | -fx-translate-y : -2px; 41 | -fx-scale-shape : false; 42 | -fx-shape : "M7,6L12.436,5.993L12.437,7.038L8,7L8,18L7,18L7,6Z"; 43 | } 44 | 45 | .ios-multi-button:add .circle { 46 | -fx-stroke: transparent; 47 | -fx-fill : -add-color; 48 | } 49 | .ios-multi-button:add .icon { 50 | -fx-background-color: -symbol-color; 51 | -fx-scale-shape : false; 52 | -fx-shape : "M6,10.5L10.5,10.5L10.5,6L11.5,6L11.5,10.5L16,10.5L16,11.5L11.5,11.5L11.5,16L10.5,16L10.5,11.5L6,11.5L6,10.5Z"; 53 | } 54 | 55 | .ios-multi-button:delete .circle { 56 | -fx-stroke: transparent; 57 | -fx-fill : -delete-color; 58 | } 59 | .ios-multi-button:delete .icon { 60 | -fx-background-color: -symbol-color; 61 | -fx-scale-shape : false; 62 | -fx-shape : "M5.5,5.5L16.5,5.5L16.5,7L5.5,7L5.5,5.5Z"; 63 | } 64 | 65 | .ios-multi-button:dot .circle { 66 | -fx-stroke: transparent; 67 | -fx-fill : transparent; 68 | } 69 | .ios-multi-button:dot .icon { 70 | -fx-background-color: -selected-color; 71 | -fx-scale-shape : false; 72 | -fx-shape : "M3,11a8,8 0 1,0 16,0a8,8 0 1,0 -16,0Z"; 73 | } 74 | 75 | .ios-multi-button:smalldot .circle { 76 | -fx-stroke: transparent; 77 | -fx-fill : transparent; 78 | } 79 | .ios-multi-button:smalldot .icon { 80 | -fx-background-color: -selected-color; 81 | -fx-scale-shape : false; 82 | -fx-shape : "M7,11a4,4 0 1,0 8,0a4,4 0 1,0 -8,0"; 83 | } 84 | 85 | .ios-multi-button:info .circle { 86 | -fx-stroke: transparent; 87 | -fx-fill : transparent; 88 | } 89 | .ios-multi-button:info .icon { 90 | -fx-background-color: -selected-color; 91 | -fx-scale-shape : false; 92 | -fx-shape : "M11,28c-6.075,0 -11,-4.925 -11,-11c0,-6.075 4.925,-11 11,-11c6.075,0 11,4.925 11,11c0,6.075 -4.925,11 -11,11Zm0,-1c5.523,0 10,-4.477 10,-10c0,-5.523 -4.477,-10 -10,-10c-5.523,0 -10,4.477 -10,10c0,5.523 4.477,10 10,10Zm-2,-12.5l3,0l0,8l1,0l0,0.5l-4,0l0,-0.5l1,0l0,-7.5l-1,0l0,-0.5Zm1.75,-1.5c-0.69,0 -1.25,-0.56 -1.25,-1.25c0,-0.69 0.56,-1.25 1.25,-1.25c0.69,0 1.25,0.56 1.25,1.25c0,0.69 -0.56,1.25 -1.25,1.25Z"; 93 | } 94 | 95 | .ios-multi-button:plus .circle { 96 | -fx-stroke: transparent; 97 | -fx-fill : transparent; 98 | } 99 | .ios-multi-button:plus .icon { 100 | -fx-background-color: -selected-color; 101 | -fx-scale-shape : false; 102 | -fx-shape : "M11,22c-6.075,0 -11,-4.925 -11,-11c0,-6.075 4.925,-11 11,-11c6.075,0 11,4.925 11,11c0,6.075 -4.925,11 -11,11Zm0,-1c5.523,0 10,-4.477 10,-10c0,-5.523 -4.477,-10 -10,-10c-5.523,0 -10,4.477 -10,10c0,5.523 4.477,10 10,10Zm-0.5,-10.5l0,-5l1,0l0,5l5,0l0,1l-5,0l0,5l-1,0l0,-5l-5,0l0,-1l5,0Z"; 103 | } 104 | 105 | .ios-multi-button:checkmark .circle { 106 | -fx-stroke: transparent; 107 | -fx-fill : transparent; 108 | } 109 | .ios-multi-button:checkmark .icon { 110 | -fx-background-color: transparent; 111 | -fx-rotate : 45; 112 | -fx-scale-shape : false; 113 | -fx-shape : "M13.753,3.867L11.633,3.868L11.278,14.122L7.743,14.12L7.743,16.24L13.402,16.245L13.753,3.867Z"; 114 | } 115 | .ios-multi-button:checkmark:selected .icon { 116 | -fx-background-color: -selected-color; 117 | -fx-rotate : 45; 118 | -fx-scale-shape : false; 119 | -fx-shape : "M13.753,3.867L11.633,3.868L11.278,14.122L7.743,14.12L7.743,16.24L13.402,16.245L13.753,3.867Z"; 120 | } 121 | 122 | .ios-multi-button:forward .circle { 123 | -fx-stroke: transparent; 124 | -fx-fill : transparent; 125 | } 126 | .ios-multi-button:forward .icon { 127 | -fx-background-color: -selected-color; 128 | -fx-scale-shape : false; 129 | -fx-shape : "M1.333,13l6.667,-6.5l-6.667,-6.5l-1.333,1.3l5.333,5.2l-5.333,5.2l1.333,1.3Z"; 130 | } -------------------------------------------------------------------------------- /src/main/java/eu/hansolo/iosfx/iosmultibutton/IosMultiButtonBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 by Gerrit Grunwald 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package eu.hansolo.iosfx.iosmultibutton; 18 | 19 | import javafx.beans.property.Property; 20 | import javafx.beans.property.SimpleBooleanProperty; 21 | import javafx.beans.property.SimpleDoubleProperty; 22 | import javafx.beans.property.SimpleObjectProperty; 23 | import javafx.geometry.Dimension2D; 24 | import javafx.geometry.Insets; 25 | import javafx.scene.paint.Color; 26 | 27 | import java.util.HashMap; 28 | 29 | 30 | public class IosMultiButtonBuilder> { 31 | private HashMap properties = new HashMap<>(); 32 | 33 | 34 | // ******************** Constructors ************************************** 35 | protected IosMultiButtonBuilder() {} 36 | 37 | 38 | // ******************** Methods ******************************************* 39 | public static final IosMultiButtonBuilder create() { 40 | return new IosMultiButtonBuilder(); 41 | } 42 | 43 | public final B selected(final boolean SELECTED) { 44 | properties.put("selected", new SimpleBooleanProperty(SELECTED)); 45 | return (B)this; 46 | } 47 | 48 | public final B selectedColor(final Color COLOR) { 49 | properties.put("selectedColor", new SimpleObjectProperty<>(COLOR)); 50 | return (B)this; 51 | } 52 | 53 | public final B type(final IosMultiButton.Type TYPE) { 54 | properties.put("type", new SimpleObjectProperty(TYPE)); 55 | return (B)this; 56 | } 57 | 58 | 59 | // General properties 60 | public final B prefSize(final double WIDTH, final double HEIGHT) { 61 | properties.put("prefSize", new SimpleObjectProperty<>(new Dimension2D(WIDTH, HEIGHT))); 62 | return (B) this; 63 | } 64 | public final B minSize(final double WIDTH, final double HEIGHT) { 65 | properties.put("minSize", new SimpleObjectProperty<>(new Dimension2D(WIDTH, HEIGHT))); 66 | return (B) this; 67 | } 68 | public final B maxSize(final double WIDTH, final double HEIGHT) { 69 | properties.put("maxSize", new SimpleObjectProperty<>(new Dimension2D(WIDTH, HEIGHT))); 70 | return (B) this; 71 | } 72 | 73 | public final B prefWidth(final double PREF_WIDTH) { 74 | properties.put("prefWidth", new SimpleDoubleProperty(PREF_WIDTH)); 75 | return (B) this; 76 | } 77 | public final B prefHeight(final double PREF_HEIGHT) { 78 | properties.put("prefHeight", new SimpleDoubleProperty(PREF_HEIGHT)); 79 | return (B) this; 80 | } 81 | 82 | public final B minWidth(final double MIN_WIDTH) { 83 | properties.put("minWidth", new SimpleDoubleProperty(MIN_WIDTH)); 84 | return (B) this; 85 | } 86 | public final B minHeight(final double MIN_HEIGHT) { 87 | properties.put("minHeight", new SimpleDoubleProperty(MIN_HEIGHT)); 88 | return (B) this; 89 | } 90 | 91 | public final B maxWidth(final double MAX_WIDTH) { 92 | properties.put("maxWidth", new SimpleDoubleProperty(MAX_WIDTH)); 93 | return (B) this; 94 | } 95 | public final B maxHeight(final double MAX_HEIGHT) { 96 | properties.put("maxHeight", new SimpleDoubleProperty(MAX_HEIGHT)); 97 | return (B) this; 98 | } 99 | 100 | public final B scaleX(final double SCALE_X) { 101 | properties.put("scaleX", new SimpleDoubleProperty(SCALE_X)); 102 | return (B) this; 103 | } 104 | public final B scaleY(final double SCALE_Y) { 105 | properties.put("scaleY", new SimpleDoubleProperty(SCALE_Y)); 106 | return (B) this; 107 | } 108 | 109 | public final B layoutX(final double LAYOUT_X) { 110 | properties.put("layoutX", new SimpleDoubleProperty(LAYOUT_X)); 111 | return (B) this; 112 | } 113 | public final B layoutY(final double LAYOUT_Y) { 114 | properties.put("layoutY", new SimpleDoubleProperty(LAYOUT_Y)); 115 | return (B) this; 116 | } 117 | 118 | public final B translateX(final double TRANSLATE_X) { 119 | properties.put("translateX", new SimpleDoubleProperty(TRANSLATE_X)); 120 | return (B) this; 121 | } 122 | public final B translateY(final double TRANSLATE_Y) { 123 | properties.put("translateY", new SimpleDoubleProperty(TRANSLATE_Y)); 124 | return (B) this; 125 | } 126 | 127 | public final B padding(final Insets INSETS) { 128 | properties.put("padding", new SimpleObjectProperty<>(INSETS)); 129 | return (B) this; 130 | } 131 | 132 | 133 | public final IosMultiButton build() { 134 | final IosMultiButton CONTROL = new IosMultiButton(properties); 135 | properties.clear(); 136 | return CONTROL; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/main/java/eu/hansolo/iosfx/iosswitch/IosSwitchBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 by Gerrit Grunwald 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package eu.hansolo.iosfx.iosswitch; 18 | 19 | import javafx.beans.property.Property; 20 | import javafx.beans.property.SimpleBooleanProperty; 21 | import javafx.beans.property.SimpleDoubleProperty; 22 | import javafx.beans.property.SimpleObjectProperty; 23 | import javafx.geometry.Dimension2D; 24 | import javafx.geometry.Insets; 25 | import javafx.scene.paint.Color; 26 | 27 | import java.util.HashMap; 28 | 29 | 30 | /** 31 | * User: hansolo 32 | * Date: 14.05.18 33 | * Time: 16:30 34 | */ 35 | public class IosSwitchBuilder> { 36 | private HashMap properties = new HashMap<>(); 37 | 38 | 39 | // ******************** Constructors ************************************** 40 | protected IosSwitchBuilder() {} 41 | 42 | 43 | // ******************** Methods ******************************************* 44 | public static final IosSwitchBuilder create() { 45 | return new IosSwitchBuilder(); 46 | } 47 | 48 | public final B selected(final boolean SELECTED) { 49 | properties.put("selected", new SimpleBooleanProperty(SELECTED)); 50 | return (B)this; 51 | } 52 | 53 | public final B selectedColor(final Color COLOR) { 54 | properties.put("selectedColor", new SimpleObjectProperty<>(COLOR)); 55 | return (B)this; 56 | } 57 | 58 | public final B dark(final boolean DARK) { 59 | properties.put("dark", new SimpleBooleanProperty(DARK)); 60 | return (B)this; 61 | } 62 | 63 | public final B showOnOffText(final boolean SHOW) { 64 | properties.put("showOnOffText", new SimpleBooleanProperty(SHOW)); 65 | return (B)this; 66 | } 67 | 68 | public final B duration(final double DURATION) { 69 | properties.put("duration", new SimpleDoubleProperty(DURATION)); 70 | return (B)this; 71 | } 72 | 73 | 74 | // General properties 75 | public final B prefSize(final double WIDTH, final double HEIGHT) { 76 | properties.put("prefSize", new SimpleObjectProperty<>(new Dimension2D(WIDTH, HEIGHT))); 77 | return (B) this; 78 | } 79 | public final B minSize(final double WIDTH, final double HEIGHT) { 80 | properties.put("minSize", new SimpleObjectProperty<>(new Dimension2D(WIDTH, HEIGHT))); 81 | return (B) this; 82 | } 83 | public final B maxSize(final double WIDTH, final double HEIGHT) { 84 | properties.put("maxSize", new SimpleObjectProperty<>(new Dimension2D(WIDTH, HEIGHT))); 85 | return (B) this; 86 | } 87 | 88 | public final B prefWidth(final double PREF_WIDTH) { 89 | properties.put("prefWidth", new SimpleDoubleProperty(PREF_WIDTH)); 90 | return (B) this; 91 | } 92 | public final B prefHeight(final double PREF_HEIGHT) { 93 | properties.put("prefHeight", new SimpleDoubleProperty(PREF_HEIGHT)); 94 | return (B) this; 95 | } 96 | 97 | public final B minWidth(final double MIN_WIDTH) { 98 | properties.put("minWidth", new SimpleDoubleProperty(MIN_WIDTH)); 99 | return (B) this; 100 | } 101 | public final B minHeight(final double MIN_HEIGHT) { 102 | properties.put("minHeight", new SimpleDoubleProperty(MIN_HEIGHT)); 103 | return (B) this; 104 | } 105 | 106 | public final B maxWidth(final double MAX_WIDTH) { 107 | properties.put("maxWidth", new SimpleDoubleProperty(MAX_WIDTH)); 108 | return (B) this; 109 | } 110 | public final B maxHeight(final double MAX_HEIGHT) { 111 | properties.put("maxHeight", new SimpleDoubleProperty(MAX_HEIGHT)); 112 | return (B) this; 113 | } 114 | 115 | public final B scaleX(final double SCALE_X) { 116 | properties.put("scaleX", new SimpleDoubleProperty(SCALE_X)); 117 | return (B) this; 118 | } 119 | public final B scaleY(final double SCALE_Y) { 120 | properties.put("scaleY", new SimpleDoubleProperty(SCALE_Y)); 121 | return (B) this; 122 | } 123 | 124 | public final B layoutX(final double LAYOUT_X) { 125 | properties.put("layoutX", new SimpleDoubleProperty(LAYOUT_X)); 126 | return (B) this; 127 | } 128 | public final B layoutY(final double LAYOUT_Y) { 129 | properties.put("layoutY", new SimpleDoubleProperty(LAYOUT_Y)); 130 | return (B) this; 131 | } 132 | 133 | public final B translateX(final double TRANSLATE_X) { 134 | properties.put("translateX", new SimpleDoubleProperty(TRANSLATE_X)); 135 | return (B) this; 136 | } 137 | public final B translateY(final double TRANSLATE_Y) { 138 | properties.put("translateY", new SimpleDoubleProperty(TRANSLATE_Y)); 139 | return (B) this; 140 | } 141 | 142 | public final B padding(final Insets INSETS) { 143 | properties.put("padding", new SimpleObjectProperty<>(INSETS)); 144 | return (B) this; 145 | } 146 | 147 | 148 | public final IosSwitch build() { 149 | final IosSwitch CONTROL = new IosSwitch(properties); 150 | properties.clear(); 151 | return CONTROL; 152 | } 153 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /src/main/java/eu/hansolo/iosfx/iosplusminusbutton/IosPlusMinusButton.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 by Gerrit Grunwald 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package eu.hansolo.iosfx.iosplusminusbutton; 18 | 19 | import eu.hansolo.iosfx.events.IosEvent; 20 | import eu.hansolo.iosfx.events.IosEventListener; 21 | import eu.hansolo.iosfx.events.IosEventType; 22 | import eu.hansolo.iosfx.iossegmentedbuttonbar.IosSegmentedButtonBar; 23 | import javafx.beans.DefaultProperty; 24 | import javafx.collections.ObservableList; 25 | import javafx.event.EventHandler; 26 | import javafx.event.EventType; 27 | import javafx.geometry.Insets; 28 | import javafx.scene.Node; 29 | import javafx.scene.control.Button; 30 | import javafx.scene.input.MouseEvent; 31 | import javafx.scene.layout.Pane; 32 | import javafx.scene.layout.Region; 33 | import javafx.scene.shape.Rectangle; 34 | import javafx.scene.shape.Shape; 35 | 36 | import java.util.List; 37 | import java.util.concurrent.CopyOnWriteArrayList; 38 | 39 | 40 | /** 41 | * User: hansolo 42 | * Date: 06.06.18 43 | * Time: 13:55 44 | */ 45 | @DefaultProperty("children") 46 | public class IosPlusMinusButton extends Region { 47 | private static final double PREFERRED_WIDTH = 250; 48 | private static final double PREFERRED_HEIGHT = 250; 49 | private static final double MINIMUM_WIDTH = 50; 50 | private static final double MINIMUM_HEIGHT = 50; 51 | private static final double MAXIMUM_WIDTH = 1024; 52 | private static final double MAXIMUM_HEIGHT = 1024; 53 | private static double aspectRatio; 54 | private final IosEvent INCREASE_EVT = new IosEvent(IosPlusMinusButton.this, IosEventType.INCREASE); 55 | private final IosEvent DECREASE_EVT = new IosEvent(IosPlusMinusButton.this, IosEventType.DECREASE); 56 | private double width; 57 | private double height; 58 | private Pane pane; 59 | private Button minus; 60 | private Button plus; 61 | private IosSegmentedButtonBar buttonBar; 62 | private List listeners; 63 | private EventHandler mouseHandler; 64 | 65 | 66 | // ******************** Constructors ************************************** 67 | public IosPlusMinusButton() { 68 | getStylesheets().add(IosPlusMinusButton.class.getResource("ios-plus-minus-button.css").toExternalForm()); 69 | aspectRatio = PREFERRED_HEIGHT / PREFERRED_WIDTH; 70 | listeners = new CopyOnWriteArrayList<>(); 71 | mouseHandler = e -> { 72 | final EventType TYPE = e.getEventType(); 73 | final Object SRC = e.getSource(); 74 | if (MouseEvent.MOUSE_PRESSED.equals(TYPE)) { 75 | if (SRC.equals(minus)) { 76 | fireIosEvent(DECREASE_EVT); 77 | } else if (SRC.equals(plus)) { 78 | fireIosEvent(INCREASE_EVT); 79 | } 80 | } 81 | }; 82 | 83 | 84 | initGraphics(); 85 | registerListeners(); 86 | } 87 | 88 | 89 | // ******************** Initialization ************************************ 90 | private void initGraphics() { 91 | if (Double.compare(getPrefWidth(), 0.0) <= 0 || Double.compare(getPrefHeight(), 0.0) <= 0 || Double.compare(getWidth(), 0.0) <= 0 || 92 | Double.compare(getHeight(), 0.0) <= 0) { 93 | if (getPrefWidth() > 0 && getPrefHeight() > 0) { 94 | setPrefSize(getPrefWidth(), getPrefHeight()); 95 | } else { 96 | setPrefSize(PREFERRED_WIDTH, PREFERRED_HEIGHT); 97 | } 98 | } 99 | 100 | getStyleClass().add("ios-plus-minus-button"); 101 | 102 | Rectangle minusSign = new Rectangle(15.5, 1.5); 103 | minusSign.getStyleClass().setAll("minus-sign"); 104 | 105 | minus = new Button(); 106 | minus.getStyleClass().add("minus-button"); 107 | minus.setGraphic(minusSign); 108 | minus.setPadding(new Insets(6, 15, 6, 15)); 109 | 110 | Rectangle v = new Rectangle(7, 0, 1.5, 15.5); 111 | Rectangle h = new Rectangle(0, 7, 15.5, 1.5); 112 | Shape plusSign = Shape.union(v, h); 113 | plusSign.getStyleClass().setAll("plus-sign"); 114 | 115 | plus = new Button(); 116 | plus.getStyleClass().add("plus-button"); 117 | plus.setGraphic(plusSign); 118 | plus.setPadding(new Insets(6, 15, 6, 15)); 119 | 120 | buttonBar = new IosSegmentedButtonBar(minus, plus); 121 | 122 | pane = new Pane(buttonBar); 123 | 124 | getChildren().setAll(pane); 125 | } 126 | 127 | private void registerListeners() { 128 | widthProperty().addListener(o -> resize()); 129 | heightProperty().addListener(o -> resize()); 130 | minus.addEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler); 131 | plus.addEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler); 132 | } 133 | 134 | public void dispose() { 135 | minus.removeEventHandler(MouseEvent.MOUSE_CLICKED, mouseHandler); 136 | plus.removeEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler); 137 | } 138 | 139 | 140 | // ******************** Methods ******************************************* 141 | @Override protected double computeMinWidth(final double HEIGHT) { return MINIMUM_WIDTH; } 142 | @Override protected double computeMinHeight(final double WIDTH) { return MINIMUM_HEIGHT; } 143 | @Override protected double computePrefWidth(final double HEIGHT) { return super.computePrefWidth(HEIGHT); } 144 | @Override protected double computePrefHeight(final double WIDTH) { return super.computePrefHeight(WIDTH); } 145 | @Override protected double computeMaxWidth(final double HEIGHT) { return MAXIMUM_WIDTH; } 146 | @Override protected double computeMaxHeight(final double WIDTH) { return MAXIMUM_HEIGHT; } 147 | 148 | @Override public ObservableList getChildren() { return super.getChildren(); } 149 | 150 | 151 | // ******************** Event Handling ************************************ 152 | public void addOnIosEvent(final IosEventListener LISTENER) { if (!listeners.contains(LISTENER)) { listeners.add(LISTENER); } } 153 | public void removeOnIosEvent(final IosEventListener LISTENER) { if (listeners.contains(LISTENER)) { listeners.remove(LISTENER); } } 154 | 155 | private void fireIosEvent(final IosEvent EVENT) { 156 | listeners.forEach(listener -> listener.onIosEvent(EVENT)); 157 | } 158 | 159 | 160 | // ******************** Resizing ****************************************** 161 | private void resize() { 162 | width = getWidth() - getInsets().getLeft() - getInsets().getRight(); 163 | height = getHeight() - getInsets().getTop() - getInsets().getBottom(); 164 | 165 | if (aspectRatio * width > height) { 166 | width = 1 / (aspectRatio / height); 167 | } else if (1 / (aspectRatio / height) > width) { 168 | height = aspectRatio * width; 169 | } 170 | 171 | if (width > 0 && height > 0) { 172 | double w = buttonBar.getLayoutBounds().getWidth(); 173 | double h = buttonBar.getLayoutBounds().getHeight(); 174 | pane.setMaxSize(w, h); 175 | pane.setPrefSize(w, h); 176 | pane.relocate(getInsets().getLeft(), getInsets().getTop()); 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/main/java/eu/hansolo/iosfx/Demo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 by Gerrit Grunwald 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package eu.hansolo.iosfx; 18 | 19 | import eu.hansolo.iosfx.common.IosColor; 20 | import eu.hansolo.iosfx.events.IosEventType; 21 | import eu.hansolo.iosfx.iosentry.IosEntry; 22 | import eu.hansolo.iosfx.iosentry.IosEntryCell; 23 | import eu.hansolo.iosfx.ioslistview.IosListView; 24 | import eu.hansolo.iosfx.iosmultibutton.IosMultiButton; 25 | import eu.hansolo.iosfx.iosmultibutton.IosMultiButton.Type; 26 | import eu.hansolo.iosfx.iosmultibutton.IosMultiButtonBuilder; 27 | import eu.hansolo.iosfx.iosplusminusbutton.IosPlusMinusButton; 28 | import eu.hansolo.iosfx.iossegmentedbuttonbar.IosSegmentedButtonBar; 29 | import eu.hansolo.iosfx.iosslider.IosSlider; 30 | import eu.hansolo.iosfx.iosswitch.IosSwitch; 31 | import eu.hansolo.iosfx.iosswitch.IosSwitchBuilder; 32 | import javafx.application.Application; 33 | import javafx.collections.FXCollections; 34 | import javafx.collections.ObservableList; 35 | import javafx.geometry.Insets; 36 | import javafx.scene.Node; 37 | import javafx.scene.Scene; 38 | import javafx.scene.control.Button; 39 | import javafx.scene.control.Label; 40 | import javafx.scene.control.ToggleButton; 41 | import javafx.scene.control.ToggleGroup; 42 | import javafx.scene.layout.VBox; 43 | import javafx.scene.paint.Color; 44 | import javafx.scene.shape.Rectangle; 45 | import javafx.scene.shape.Shape; 46 | import javafx.scene.text.Font; 47 | import javafx.stage.Stage; 48 | 49 | 50 | public class Demo extends Application { 51 | private ObservableList entries; 52 | private IosListView listView; 53 | private IosEntry entry1; 54 | private IosEntry entry2; 55 | private IosEntry entry3; 56 | private IosEntry entry4; 57 | private IosEntry entry5; 58 | private IosEntry entry6; 59 | private IosEntry entry7; 60 | private IosEntry entry8; 61 | private IosEntry entry9; 62 | private IosSlider slider; 63 | private IosSlider balanceSlider; 64 | private IosSegmentedButtonBar buttonBar1; 65 | private IosSegmentedButtonBar buttonBar2; 66 | private IosPlusMinusButton plusMinusButton; 67 | 68 | 69 | @Override public void init() { 70 | entry1 = createIosEntry("Title 1", "Subtitle 1", createMultiButton(Type.SMALL_DOT, IosColor.PURPLE.color(), false), createSwitch(IosColor.PURPLE.color(), false), true, true); 71 | entry2 = createIosEntry("Title 2", "Subtitle 2", createMultiButton(Type.ADD, IosColor.GREEN.color(), false), createSwitch(IosColor.PINK.color(), true), true, false); 72 | entry3 = createIosEntry("Title 3", "Subtitle 3", createMultiButton(Type.DELETE, IosColor.RED.color(), false), createSwitch(IosColor.GREEN.color(), false), false, false); 73 | entry4 = createIosEntry("Title 4", "Subtitle 4", createMultiButton(Type.DOT, IosColor.ORANGE.color(), false), createMultiButton(Type.CHECKBOX, IosColor.GREEN.color(), true), false, true); 74 | entry5 = createIosEntry("Title 5", "Subtitle 5", createMultiButton(Type.INFO, IosColor.BLUE.color(), false), createMultiButton(Type.CHECKBOX, IosColor.GREEN.color(), false), false, false); 75 | entry6 = createIosEntry("Title 6", "Subtitle 6", createMultiButton(Type.PLUS, IosColor.ORANGE.color(), false), createMultiButton(Type.ADD, IosColor.GREEN.color(), false), false, false); 76 | entry7 = createIosEntry("Title 7", "Subtitle 7", null, createMultiButton(Type.DELETE, IosColor.GREEN.color(), false), false, false); 77 | entry8 = createIosEntry("Title 8", "Subtitle 8", createMultiButton(Type.DOT, IosColor.GREEN.color(), false), createMultiButton(Type.CHECK_MARK, IosColor.BLUE.color(), true), false, false); 78 | entry9 = createIosEntry("Title 9", "Subtitle 9", null, createMultiButton(Type.FORWARD, Color.rgb(0, 0, 0, 0.2), true), false, false); 79 | 80 | entry1.setActionLabel("Press"); 81 | 82 | entry9.addOnIosEvent(e -> { 83 | if (IosEventType.PRESSED == e.getType()) { 84 | System.out.println("Move to next screen"); 85 | } 86 | }); 87 | 88 | entries = FXCollections.observableArrayList(); 89 | entries.addAll(entry1, entry2, entry3, entry4, entry5, entry6, entry7, entry8, entry9); 90 | 91 | listView = new IosListView(entries); 92 | listView.setPrefSize(375, 600); 93 | listView.setPlaceholder(new Label("No entries loaded")); 94 | listView.setCellFactory(p -> new IosEntryCell()); 95 | 96 | slider = new IosSlider(); 97 | 98 | balanceSlider = new IosSlider(); 99 | balanceSlider.setBalance(true); 100 | 101 | Button button1 = new Button("Label"); 102 | Button button2 = new Button("Label"); 103 | Button button3 = new Button("Label"); 104 | Button button4 = new Button("Label"); 105 | Button button5 = new Button("Label"); 106 | 107 | buttonBar1 = new IosSegmentedButtonBar(button1, button2, button3, button4, button5); 108 | buttonBar1.setPadding(new Insets(5)); 109 | 110 | 111 | ToggleButton toggleButton1 = new ToggleButton("Label"); 112 | ToggleButton toggleButton2 = new ToggleButton("Label"); 113 | ToggleButton toggleButton3 = new ToggleButton("Label"); 114 | ToggleButton toggleButton4 = new ToggleButton("Label"); 115 | ToggleButton toggleButton5 = new ToggleButton("Label"); 116 | 117 | ToggleGroup toggleGroup = new ToggleGroup(); 118 | toggleButton1.setToggleGroup(toggleGroup); 119 | toggleButton2.setToggleGroup(toggleGroup); 120 | toggleButton3.setToggleGroup(toggleGroup); 121 | toggleButton4.setToggleGroup(toggleGroup); 122 | toggleButton5.setToggleGroup(toggleGroup); 123 | 124 | buttonBar2 = new IosSegmentedButtonBar(toggleButton1, toggleButton2, toggleButton3, toggleButton4, toggleButton5); 125 | buttonBar2.setPadding(new Insets(5)); 126 | 127 | plusMinusButton = new IosPlusMinusButton(); 128 | plusMinusButton.setPadding(new Insets(5)); 129 | 130 | registerListeners(); 131 | } 132 | 133 | private void registerListeners() { 134 | entry1.addOnActionPressed(e -> System.out.println("entry1 pressed")); 135 | balanceSlider.valueProperty().addListener(o -> System.out.println(balanceSlider.getBalanceValue())); 136 | plusMinusButton.addOnIosEvent(e -> { 137 | switch(e.getType()) { 138 | case INCREASE: System.out.println("Increase"); break; 139 | case DECREASE: System.out.println("Decrease"); break; 140 | } 141 | }); 142 | } 143 | 144 | @Override public void start(Stage stage) { 145 | VBox pane = new VBox(10, listView, slider, balanceSlider, buttonBar1, buttonBar2, plusMinusButton); 146 | pane.setPrefSize(400, 650); 147 | //pane.setPadding(new Insets(10)); 148 | 149 | Scene scene = new Scene(pane); 150 | scene.getStylesheets().add(getClass().getResource("ios.css").toExternalForm()); 151 | 152 | stage.setTitle("iOS FX"); 153 | stage.setScene(scene); 154 | stage.show(); 155 | } 156 | 157 | @Override public void stop() { 158 | System.exit(0); 159 | } 160 | 161 | private IosEntry createIosEntry(final String TITLE, final String SUB_TITLE, final Node LEFT_NODE, final Node RIGHT_NODE, final boolean HAS_ACTION, final boolean HAS_DELETE) { 162 | IosEntry entry = new IosEntry(LEFT_NODE, TITLE, SUB_TITLE, RIGHT_NODE); 163 | entry.setHasAction(HAS_ACTION); 164 | entry.setHasDelete(HAS_DELETE); 165 | return entry; 166 | } 167 | 168 | private IosSwitch createSwitch(final Color SELECTED_COLOR, final boolean SHOW_ON_OFF_TEXT) { 169 | IosSwitch iosSwitch = IosSwitchBuilder.create() 170 | .minSize(51, 31) 171 | .maxSize(51, 31) 172 | .prefSize(51, 31) 173 | .showOnOffText(SHOW_ON_OFF_TEXT) 174 | .selectedColor(null == SELECTED_COLOR ? IosSwitch.DEFAULT_SELECTED_COLOR : SELECTED_COLOR) 175 | .build(); 176 | return iosSwitch; 177 | } 178 | 179 | private IosMultiButton createMultiButton(final Type MULTI_BUTTON_TYPE, final Color SELECTED_COLOR, final boolean SELECTED) { 180 | IosMultiButton iosMultiButton = IosMultiButtonBuilder.create() 181 | .type(MULTI_BUTTON_TYPE) 182 | .selectedColor(SELECTED_COLOR) 183 | .selected(SELECTED) 184 | .build(); 185 | return iosMultiButton; 186 | } 187 | 188 | public static void main(String[] args) { 189 | launch(args); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /src/main/java/eu/hansolo/iosfx/iosmultibutton/IosMultiButton.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 by Gerrit Grunwald 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package eu.hansolo.iosfx.iosmultibutton; 18 | 19 | import eu.hansolo.iosfx.events.IosEvent; 20 | import eu.hansolo.iosfx.events.IosEventListener; 21 | import eu.hansolo.iosfx.events.IosEventType; 22 | import javafx.beans.DefaultProperty; 23 | import javafx.beans.binding.Bindings; 24 | import javafx.beans.binding.BooleanBinding; 25 | import javafx.beans.property.BooleanProperty; 26 | import javafx.beans.property.BooleanPropertyBase; 27 | import javafx.beans.property.DoubleProperty; 28 | import javafx.beans.property.ObjectProperty; 29 | import javafx.beans.property.ObjectPropertyBase; 30 | import javafx.beans.property.Property; 31 | import javafx.beans.value.ChangeListener; 32 | import javafx.collections.ObservableList; 33 | import javafx.css.CssMetaData; 34 | import javafx.css.PseudoClass; 35 | import javafx.css.Styleable; 36 | import javafx.css.StyleableProperty; 37 | import javafx.css.StyleablePropertyFactory; 38 | import javafx.event.EventHandler; 39 | import javafx.geometry.Dimension2D; 40 | import javafx.geometry.Insets; 41 | import javafx.scene.Node; 42 | import javafx.scene.input.MouseEvent; 43 | import javafx.scene.layout.Pane; 44 | import javafx.scene.layout.Region; 45 | import javafx.scene.paint.Color; 46 | import javafx.scene.shape.Circle; 47 | 48 | import java.util.HashMap; 49 | import java.util.List; 50 | import java.util.Map; 51 | import java.util.concurrent.CopyOnWriteArrayList; 52 | 53 | 54 | /** 55 | * User: hansolo 56 | * Date: 29.05.18 57 | * Time: 09:03 58 | */ 59 | @DefaultProperty("children") 60 | public class IosMultiButton extends Region { 61 | public enum Type { CHECKBOX, ADD, DELETE, CHECK_MARK, DOT, SMALL_DOT, INFO, PLUS, FORWARD } 62 | public static final Color DEFAULT_SELECTED_COLOR = Color.rgb(0, 122, 255); 63 | private static final double PREFERRED_WIDTH = 22; 64 | private static final double PREFERRED_HEIGHT = 22; 65 | private static final double MINIMUM_WIDTH = 11; 66 | private static final double MINIMUM_HEIGHT = 11; 67 | private static final double MAXIMUM_WIDTH = 1024; 68 | private static final double MAXIMUM_HEIGHT = 1024; 69 | private static final StyleablePropertyFactory FACTORY = new StyleablePropertyFactory<>(Region.getClassCssMetaData()); 70 | private final IosEvent SELECTED_EVT = new IosEvent(IosMultiButton.this, IosEventType.SELECTED); 71 | private final IosEvent DESELECTED_EVT = new IosEvent(IosMultiButton.this, IosEventType.DESELECTED); 72 | private final IosEvent PRESSED_EVT = new IosEvent(IosMultiButton.this, IosEventType.PRESSED); 73 | private final IosEvent RELEASED_EVT = new IosEvent(IosMultiButton.this, IosEventType.RELEASED); 74 | private static final PseudoClass CHECKBOX_PSEUDO_CLASS = PseudoClass.getPseudoClass("checkbox"); 75 | private static final PseudoClass ADD_PSEUDO_CLASS = PseudoClass.getPseudoClass("add"); 76 | private static final PseudoClass DELETE_PSEUDO_CLASS = PseudoClass.getPseudoClass("delete"); 77 | private static final PseudoClass CHECK_MARK_PSEUDO_CLASS = PseudoClass.getPseudoClass("checkmark"); 78 | private static final PseudoClass DOT_PSEUDO_CLASS = PseudoClass.getPseudoClass("dot"); 79 | private static final PseudoClass SMALL_DOT_PSEUDO_CLASS = PseudoClass.getPseudoClass("smalldot"); 80 | private static final PseudoClass INFO_PSEUDO_CLASS = PseudoClass.getPseudoClass("info"); 81 | private static final PseudoClass PLUS_PSEUDO_CLASS = PseudoClass.getPseudoClass("plus"); 82 | private static final PseudoClass FORWARD_PSEUDO_CLASS = PseudoClass.getPseudoClass("forward"); 83 | private static final PseudoClass SELECTED_PSEUDO_CLASS = PseudoClass.getPseudoClass("selected"); 84 | private final StyleableProperty selectedColor; 85 | private double size; 86 | private double width; 87 | private double height; 88 | private Circle circle; 89 | private Region icon; 90 | private Pane pane; 91 | private Type _type; 92 | private ObjectProperty type; 93 | private boolean _selected; 94 | private BooleanProperty selected; 95 | private List listeners; 96 | private BooleanBinding showing; 97 | private ChangeListener showingListener; 98 | private EventHandler pressedHandler; 99 | private EventHandler releasedHandler; 100 | private HashMap settings; 101 | 102 | 103 | // ******************** Constructors ************************************** 104 | public IosMultiButton() { 105 | this(new HashMap<>()); 106 | } 107 | public IosMultiButton(final Map SETTINGS) { 108 | _type = Type.CHECKBOX; 109 | _selected = false; 110 | selectedColor = FACTORY.createStyleableColorProperty(IosMultiButton.this, "selectedColor", "-selected-color", s -> s.selectedColor, DEFAULT_SELECTED_COLOR); 111 | listeners = new CopyOnWriteArrayList<>(); 112 | showingListener = (o, ov, nv) -> { if (nv) { applySettings(); } }; 113 | pressedHandler = e -> { 114 | fireIosEvent(PRESSED_EVT); 115 | if (Type.CHECKBOX == getType() && !isDisabled()) { setSelected(!isSelected()); } 116 | }; 117 | releasedHandler = e -> fireIosEvent(RELEASED_EVT); 118 | settings = new HashMap<>(SETTINGS); 119 | 120 | initGraphics(); 121 | registerListeners(); 122 | applySettings(); 123 | } 124 | 125 | 126 | // ******************** Initialization ************************************ 127 | private void initGraphics() { 128 | if (Double.compare(getPrefWidth(), 0.0) <= 0 || Double.compare(getPrefHeight(), 0.0) <= 0 || Double.compare(getWidth(), 0.0) <= 0 || 129 | Double.compare(getHeight(), 0.0) <= 0) { 130 | if (getPrefWidth() > 0 && getPrefHeight() > 0) { 131 | setPrefSize(getPrefWidth(), getPrefHeight()); 132 | } else { 133 | setPrefSize(PREFERRED_WIDTH, PREFERRED_HEIGHT); 134 | } 135 | } 136 | 137 | getStyleClass().add("ios-multi-button"); 138 | 139 | circle = new Circle(PREFERRED_HEIGHT * 0.5); 140 | circle.getStyleClass().add("circle"); 141 | 142 | icon = new Region(); 143 | icon.getStyleClass().setAll("icon"); 144 | icon.setMouseTransparent(true); 145 | 146 | pane = new Pane(circle, icon); 147 | 148 | getChildren().setAll(pane); 149 | } 150 | 151 | private void registerListeners() { 152 | widthProperty().addListener(o -> resize()); 153 | heightProperty().addListener(o -> resize()); 154 | addEventHandler(MouseEvent.MOUSE_PRESSED, pressedHandler); 155 | addEventHandler(MouseEvent.MOUSE_RELEASED, releasedHandler); 156 | selectedColorProperty().addListener(o -> icon.setStyle(String.join("", "-selected-color: ", (getSelectedColor()).toString().replace("0x", "#")))); 157 | if (null != getScene()) { 158 | setupBinding(); 159 | } else { 160 | sceneProperty().addListener((o1, ov1, nv1) -> { 161 | if (null == nv1) { return; } 162 | if (null != getScene().getWindow()) { 163 | setupBinding(); 164 | } else { 165 | sceneProperty().get().windowProperty().addListener((o2, ov2, nv2) -> { 166 | if (null == nv2) { return; } 167 | setupBinding(); 168 | }); 169 | } 170 | }); 171 | } 172 | } 173 | 174 | private void setupBinding() { 175 | showing = Bindings.selectBoolean(sceneProperty(), "window", "showing"); 176 | showing.addListener(showingListener); 177 | } 178 | 179 | private void applySettings() { 180 | if (settings.isEmpty()) { return; } 181 | for (String key : settings.keySet()) { 182 | if ("prefSize".equals(key)) { 183 | Dimension2D dim = ((ObjectProperty) settings.get(key)).get(); 184 | setPrefSize(dim.getWidth(), dim.getHeight()); 185 | } else if ("minSize".equals(key)) { 186 | Dimension2D dim = ((ObjectProperty) settings.get(key)).get(); 187 | setMinSize(dim.getWidth(), dim.getHeight()); 188 | } else if ("maxSize".equals(key)) { 189 | Dimension2D dim = ((ObjectProperty) settings.get(key)).get(); 190 | setMaxSize(dim.getWidth(), dim.getHeight()); 191 | } else if ("prefWidth".equals(key)) { 192 | setPrefWidth(((DoubleProperty) settings.get(key)).get()); 193 | } else if ("prefHeight".equals(key)) { 194 | setPrefHeight(((DoubleProperty) settings.get(key)).get()); 195 | } else if ("minWidth".equals(key)) { 196 | setMinWidth(((DoubleProperty) settings.get(key)).get()); 197 | } else if ("minHeight".equals(key)) { 198 | setMinHeight(((DoubleProperty) settings.get(key)).get()); 199 | } else if ("maxWidth".equals(key)) { 200 | setMaxWidth(((DoubleProperty) settings.get(key)).get()); 201 | } else if ("maxHeight".equals(key)) { 202 | setMaxHeight(((DoubleProperty) settings.get(key)).get()); 203 | } else if ("scaleX".equals(key)) { 204 | setScaleX(((DoubleProperty) settings.get(key)).get()); 205 | } else if ("scaleY".equals(key)) { 206 | setScaleY(((DoubleProperty) settings.get(key)).get()); 207 | } else if ("layoutX".equals(key)) { 208 | setLayoutX(((DoubleProperty) settings.get(key)).get()); 209 | } else if ("layoutY".equals(key)) { 210 | setLayoutY(((DoubleProperty) settings.get(key)).get()); 211 | } else if ("translateX".equals(key)) { 212 | setTranslateX(((DoubleProperty) settings.get(key)).get()); 213 | } else if ("translateY".equals(key)) { 214 | setTranslateY(((DoubleProperty) settings.get(key)).get()); 215 | } else if ("padding".equals(key)) { 216 | setPadding(((ObjectProperty) settings.get(key)).get()); 217 | } // Control specific settings 218 | else if ("selectedColor".equals(key)) { 219 | setSelectedColor(((ObjectProperty) settings.get(key)).get()); 220 | } else if("type".equals(key)) { 221 | setType(((ObjectProperty) settings.get(key)).get()); 222 | } 223 | } 224 | 225 | if (settings.containsKey("selected")) { setSelected(((BooleanProperty) settings.get("selected")).get()); } 226 | 227 | settings.clear(); 228 | if (null == showing) { return; } 229 | showing.removeListener(showingListener); 230 | } 231 | 232 | public void dispose() { 233 | removeEventHandler(MouseEvent.MOUSE_RELEASED, releasedHandler); 234 | removeEventHandler(MouseEvent.MOUSE_PRESSED, pressedHandler); 235 | } 236 | 237 | 238 | // ******************** Methods ******************************************* 239 | @Override public void layoutChildren() { 240 | super.layoutChildren(); 241 | } 242 | 243 | @Override protected double computeMinWidth(final double HEIGHT) { return MINIMUM_WIDTH; } 244 | @Override protected double computeMinHeight(final double WIDTH) { return MINIMUM_HEIGHT; } 245 | @Override protected double computePrefWidth(final double HEIGHT) { return super.computePrefWidth(HEIGHT); } 246 | @Override protected double computePrefHeight(final double WIDTH) { return super.computePrefHeight(WIDTH); } 247 | @Override protected double computeMaxWidth(final double HEIGHT) { return MAXIMUM_WIDTH; } 248 | @Override protected double computeMaxHeight(final double WIDTH) { return MAXIMUM_HEIGHT; } 249 | 250 | @Override public ObservableList getChildren() { return super.getChildren(); } 251 | 252 | public boolean isSelected() { return null == selected ? _selected : selected.get(); } 253 | public void setSelected(final boolean SELECTED) { 254 | if (null == selected) { 255 | fireIosEvent(SELECTED ? SELECTED_EVT : DESELECTED_EVT); 256 | _selected = SELECTED; 257 | pseudoClassStateChanged(SELECTED_PSEUDO_CLASS, SELECTED); 258 | } else { 259 | selected.set(SELECTED); 260 | } 261 | } 262 | public BooleanProperty selectedProperty() { 263 | if (null == selected) { 264 | selected = new BooleanPropertyBase(_selected) { 265 | @Override protected void invalidated() { 266 | fireIosEvent(get() ? SELECTED_EVT : DESELECTED_EVT); 267 | pseudoClassStateChanged(SELECTED_PSEUDO_CLASS, get()); 268 | } 269 | @Override public Object getBean() { return IosMultiButton.this; } 270 | @Override public String getName() { return "selected"; } 271 | }; 272 | } 273 | return selected; 274 | } 275 | 276 | public Color getSelectedColor() { return selectedColor.getValue(); } 277 | public void setSelectedColor(final Color COLOR) { selectedColor.setValue(COLOR); } 278 | public ObjectProperty selectedColorProperty() { return (ObjectProperty) selectedColor; } 279 | 280 | public Type getType() { return null == type ? _type : type.get(); } 281 | public void setType(final Type TYPE) { 282 | if (null == type) { 283 | _type = TYPE; 284 | adjustStyle(); 285 | } else { 286 | type.set(TYPE); 287 | } 288 | } 289 | public ObjectProperty typeProperty() { 290 | if (null == type) { 291 | type = new ObjectPropertyBase(_type) { 292 | @Override protected void invalidated() { adjustStyle(); } 293 | @Override public Object getBean() { return IosMultiButton.this; } 294 | @Override public String getName() { return "type"; } 295 | }; 296 | _type = null; 297 | } 298 | return type; 299 | } 300 | 301 | protected HashMap getSettings() { return settings; } 302 | 303 | private void adjustStyle() { 304 | switch(getType()) { 305 | case ADD : pseudoClassStateChanged(ADD_PSEUDO_CLASS, true); break; 306 | case DELETE : pseudoClassStateChanged(DELETE_PSEUDO_CLASS, true); break; 307 | case CHECK_MARK: pseudoClassStateChanged(CHECK_MARK_PSEUDO_CLASS, true); break; 308 | case DOT : pseudoClassStateChanged(DOT_PSEUDO_CLASS, true); break; 309 | case SMALL_DOT : pseudoClassStateChanged(SMALL_DOT_PSEUDO_CLASS, true); break; 310 | case INFO : pseudoClassStateChanged(INFO_PSEUDO_CLASS, true); break; 311 | case PLUS : pseudoClassStateChanged(PLUS_PSEUDO_CLASS, true); break; 312 | case FORWARD : pseudoClassStateChanged(FORWARD_PSEUDO_CLASS, true); break; 313 | case CHECKBOX : 314 | default: 315 | pseudoClassStateChanged(CHECKBOX_PSEUDO_CLASS, true); 316 | pseudoClassStateChanged(SELECTED_PSEUDO_CLASS, isSelected()); 317 | break; 318 | } 319 | } 320 | 321 | 322 | // ******************** Event Handling ************************************ 323 | public void addOnIosEvent(final IosEventListener LISTENER) { if (!listeners.contains(LISTENER)) { listeners.add(LISTENER); } } 324 | public void removeOnIosEvent(final IosEventListener LISTENER) { if (listeners.contains(LISTENER)) { listeners.remove(LISTENER); } } 325 | 326 | private void fireIosEvent(final IosEvent EVENT) { 327 | listeners.forEach(listener -> listener.onIosEvent(EVENT)); 328 | } 329 | 330 | 331 | // ******************** Resizing ****************************************** 332 | private void resize() { 333 | width = getWidth() - getInsets().getLeft() - getInsets().getRight(); 334 | height = getHeight() - getInsets().getTop() - getInsets().getBottom(); 335 | size = width < height ? width : height; 336 | 337 | if (width > 0 && height > 0) { 338 | pane.setMaxSize(size, size); 339 | pane.setPrefSize(size, size); 340 | pane.relocate((getWidth() - size) * 0.5, (getHeight() - size) * 0.5); 341 | 342 | circle.setCenterX(size * 0.5); 343 | circle.setCenterY(size * 0.5); 344 | 345 | icon.setPrefSize(size, size); 346 | } 347 | } 348 | 349 | 350 | // ******************** Style related ************************************* 351 | @Override public String getUserAgentStylesheet() { 352 | return IosMultiButton.class.getResource("ios-multibutton.css").toExternalForm(); 353 | } 354 | 355 | public static List> getClassCssMetaData() { return FACTORY.getCssMetaData(); } 356 | @Override public List> getCssMetaData() { return FACTORY.getCssMetaData(); } 357 | } 358 | -------------------------------------------------------------------------------- /src/main/java/eu/hansolo/iosfx/iosslider/IosSliderSkin.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 by Gerrit Grunwald 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package eu.hansolo.iosfx.iosslider; 18 | 19 | import com.sun.javafx.scene.control.behavior.SliderBehavior; 20 | import com.sun.javafx.scene.control.skin.BehaviorSkinBase; 21 | import javafx.animation.Transition; 22 | import javafx.geometry.Orientation; 23 | import javafx.geometry.Point2D; 24 | import javafx.geometry.Side; 25 | import javafx.scene.AccessibleAttribute; 26 | import javafx.scene.AccessibleRole; 27 | import javafx.scene.chart.NumberAxis; 28 | import javafx.scene.control.Slider; 29 | import javafx.scene.layout.StackPane; 30 | import javafx.scene.shape.Line; 31 | import javafx.util.Duration; 32 | import javafx.util.StringConverter; 33 | 34 | 35 | public class IosSliderSkin extends BehaviorSkinBase { 36 | private NumberAxis tickLine = null; 37 | private double trackToTickGap = 2; 38 | 39 | private boolean showTickMarks; 40 | private double thumbWidth; 41 | private double thumbHeight; 42 | 43 | private double trackStart; 44 | private double trackLength; 45 | private double thumbTop; 46 | private double thumbLeft; 47 | private double preDragThumbPos; 48 | private Point2D dragStart; // in skin coordinates 49 | 50 | private StackPane thumb; 51 | private StackPane track; 52 | private StackPane trackProgress; 53 | private Line centerLine; 54 | private boolean trackClicked = false; 55 | private StringConverter stringConverterWrapper; 56 | 57 | 58 | public IosSliderSkin(final Slider SLIDER) { 59 | super(SLIDER, new SliderBehavior(SLIDER)); 60 | 61 | initialize(); 62 | SLIDER.requestLayout(); 63 | registerChangeListener(SLIDER.minProperty(), "MIN"); 64 | registerChangeListener(SLIDER.maxProperty(), "MAX"); 65 | registerChangeListener(SLIDER.valueProperty(), "VALUE"); 66 | registerChangeListener(SLIDER.orientationProperty(), "ORIENTATION"); 67 | registerChangeListener(SLIDER.showTickMarksProperty(), "SHOW_TICK_MARKS"); 68 | registerChangeListener(SLIDER.showTickLabelsProperty(), "SHOW_TICK_LABELS"); 69 | registerChangeListener(SLIDER.majorTickUnitProperty(), "MAJOR_TICK_UNIT"); 70 | registerChangeListener(SLIDER.minorTickCountProperty(), "MINOR_TICK_COUNT"); 71 | registerChangeListener(SLIDER.labelFormatterProperty(), "TICK_LABEL_FORMATTER"); 72 | registerChangeListener(SLIDER.snapToTicksProperty(), "SNAP_TO_TICKS"); 73 | 74 | if (SLIDER instanceof IosSlider) { 75 | IosSlider iosSlider = (IosSlider) SLIDER; 76 | registerChangeListener(iosSlider.balanceProperty(), "BALANCE"); 77 | iosSlider = null; 78 | } 79 | 80 | stringConverterWrapper = new StringConverter() { 81 | Slider slider = getSkinnable(); 82 | @Override public String toString(Number object) { 83 | return(object != null) ? slider.getLabelFormatter().toString(object.doubleValue()) : ""; 84 | } 85 | @Override public Number fromString(String string) { 86 | return slider.getLabelFormatter().fromString(string); 87 | } 88 | }; 89 | } 90 | 91 | 92 | private void initialize() { 93 | thumb = new StackPane() { 94 | @Override 95 | public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 96 | switch (attribute) { 97 | case VALUE: return getSkinnable().getValue(); 98 | default: return super.queryAccessibleAttribute(attribute, parameters); 99 | } 100 | } 101 | }; 102 | thumb.getStyleClass().setAll("thumb"); 103 | thumb.setAccessibleRole(AccessibleRole.THUMB); 104 | 105 | centerLine = new Line(); 106 | centerLine.getStyleClass().setAll("center-line"); 107 | 108 | track = new StackPane(); 109 | track.getStyleClass().setAll("track"); 110 | 111 | trackProgress = new StackPane(); 112 | trackProgress.getStyleClass().setAll("track-progress"); 113 | if (getSkinnable() instanceof IosSlider) { 114 | IosSlider iosSlider = (IosSlider) getSkinnable(); 115 | boolean isBalance = iosSlider.getBalance(); 116 | trackProgress.setVisible(!isBalance); 117 | if (isBalance) { iosSlider.setValue(iosSlider.getRange() * 0.5); } 118 | iosSlider = null; 119 | } 120 | 121 | getChildren().setAll(track, centerLine, trackProgress, thumb); 122 | setShowTickMarks(getSkinnable().isShowTickMarks(), getSkinnable().isShowTickLabels()); 123 | 124 | track.setOnMousePressed(me -> { 125 | if (!thumb.isPressed()) { 126 | trackClicked = true; 127 | if (getSkinnable().getOrientation() == Orientation.HORIZONTAL) { 128 | getBehavior().trackPress(me, (me.getX() / trackLength)); 129 | } else { 130 | getBehavior().trackPress(me, (me.getY() / trackLength)); 131 | } 132 | trackClicked = false; 133 | } 134 | }); 135 | track.setOnMouseDragged(me -> { 136 | if (!thumb.isPressed()) { 137 | if (getSkinnable().getOrientation() == Orientation.HORIZONTAL) { 138 | getBehavior().trackPress(me, (me.getX() / trackLength)); 139 | } else { 140 | getBehavior().trackPress(me, (me.getY() / trackLength)); 141 | } 142 | } 143 | }); 144 | 145 | thumb.setOnMousePressed(me -> { 146 | getBehavior().thumbPressed(me, 0.0f); 147 | dragStart = thumb.localToParent(me.getX(), me.getY()); 148 | preDragThumbPos = (getSkinnable().getValue() - getSkinnable().getMin()) / 149 | (getSkinnable().getMax() - getSkinnable().getMin()); 150 | }); 151 | thumb.setOnMouseReleased(me -> { 152 | getBehavior().thumbReleased(me); 153 | }); 154 | thumb.setOnMouseDragged(me -> { 155 | Point2D cur = thumb.localToParent(me.getX(), me.getY()); 156 | double dragPos = (getSkinnable().getOrientation() == Orientation.HORIZONTAL)? 157 | cur.getX() - dragStart.getX() : -(cur.getY() - dragStart.getY()); 158 | getBehavior().thumbDragged(me, preDragThumbPos + dragPos / trackLength); 159 | }); 160 | } 161 | 162 | @Override protected void handleControlPropertyChanged(final String PROPERTY) { 163 | super.handleControlPropertyChanged(PROPERTY); 164 | Slider slider = getSkinnable(); 165 | if ("ORIENTATION".equals(PROPERTY)) { 166 | if (showTickMarks && tickLine != null) { 167 | boolean isVertical = slider.getOrientation() == Orientation.VERTICAL; 168 | tickLine.setSide(isVertical ? Side.RIGHT : (slider.getOrientation() == null) ? Side.RIGHT: Side.BOTTOM); 169 | } 170 | getSkinnable().requestLayout(); 171 | } else if ("VALUE".equals(PROPERTY)) { 172 | positionThumb(trackClicked); 173 | } else if ("MIN".equals(PROPERTY) ) { 174 | if (showTickMarks && tickLine != null) { 175 | tickLine.setLowerBound(slider.getMin()); 176 | } 177 | getSkinnable().requestLayout(); 178 | } else if ("MAX".equals(PROPERTY)) { 179 | if (showTickMarks && tickLine != null) { 180 | tickLine.setUpperBound(slider.getMax()); 181 | } 182 | getSkinnable().requestLayout(); 183 | } else if ("SHOW_TICK_MARKS".equals(PROPERTY) || "SHOW_TICK_LABELS".equals(PROPERTY)) { 184 | setShowTickMarks(slider.isShowTickMarks(), slider.isShowTickLabels()); 185 | } else if ("MAJOR_TICK_UNIT".equals(PROPERTY)) { 186 | if (tickLine != null) { 187 | tickLine.setTickUnit(slider.getMajorTickUnit()); 188 | getSkinnable().requestLayout(); 189 | } 190 | } else if ("MINOR_TICK_COUNT".equals(PROPERTY)) { 191 | if (tickLine != null) { 192 | tickLine.setMinorTickCount(Math.max(slider.getMinorTickCount(),0) + 1); 193 | getSkinnable().requestLayout(); 194 | } 195 | } else if ("TICK_LABEL_FORMATTER".equals(PROPERTY)) { 196 | if (tickLine != null) { 197 | if (slider.getLabelFormatter() == null) { 198 | tickLine.setTickLabelFormatter(null); 199 | } else { 200 | tickLine.setTickLabelFormatter(stringConverterWrapper); 201 | tickLine.requestAxisLayout(); 202 | } 203 | } 204 | } else if ("SNAP_TO_TICKS".equals(PROPERTY)) { 205 | slider.adjustValue(slider.getValue()); 206 | } else if ("BALANCE".equals(PROPERTY)) { 207 | if (slider instanceof IosSlider) { 208 | IosSlider iosSlider = (IosSlider) slider; 209 | boolean isBalance = iosSlider.getBalance(); 210 | trackProgress.setVisible(!isBalance); 211 | centerLine.setVisible(isBalance); 212 | if (isBalance) { iosSlider.setValue(iosSlider.getRange() * 0.5); } 213 | iosSlider = null; 214 | } 215 | } 216 | } 217 | 218 | 219 | @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 220 | final Slider s = getSkinnable(); 221 | if (s.getOrientation() == Orientation.HORIZONTAL) { 222 | return (leftInset + minTrackLength() + thumb.minWidth(-1) + rightInset); 223 | } else { 224 | return(leftInset + thumb.prefWidth(-1) + rightInset); 225 | } 226 | } 227 | @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 228 | final Slider s = getSkinnable(); 229 | if (s.getOrientation() == Orientation.HORIZONTAL) { 230 | double axisHeight = showTickMarks ? (tickLine.prefHeight(-1) + trackToTickGap) : 0; 231 | return topInset + thumb.prefHeight(-1) + axisHeight + bottomInset; 232 | } else { 233 | return topInset + minTrackLength() + thumb.prefHeight(-1) + bottomInset; 234 | } 235 | } 236 | @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 237 | final Slider s = getSkinnable(); 238 | if (s.getOrientation() == Orientation.HORIZONTAL) { 239 | if(showTickMarks) { 240 | return Math.max(140, tickLine.prefWidth(-1)); 241 | } else { 242 | return 140; 243 | } 244 | } else { 245 | double axisWidth = showTickMarks ? (tickLine.prefWidth(-1) + trackToTickGap) : 0; 246 | return leftInset + Math.max(thumb.prefWidth(-1), track.prefWidth(-1)) + axisWidth + rightInset; 247 | } 248 | } 249 | @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 250 | final Slider s = getSkinnable(); 251 | if (s.getOrientation() == Orientation.HORIZONTAL) { 252 | return topInset + Math.max(thumb.prefHeight(-1), track.prefHeight(-1)) + 253 | ((showTickMarks) ? (trackToTickGap+tickLine.prefHeight(-1)) : 0) + bottomInset; 254 | } else { 255 | if(showTickMarks) { 256 | return Math.max(140, tickLine.prefHeight(-1)); 257 | } else { 258 | return 140; 259 | } 260 | } 261 | } 262 | @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 263 | if (getSkinnable().getOrientation() == Orientation.HORIZONTAL) { 264 | return Double.MAX_VALUE; 265 | } else { 266 | return getSkinnable().prefWidth(-1); 267 | } 268 | } 269 | @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 270 | if (getSkinnable().getOrientation() == Orientation.HORIZONTAL) { 271 | return getSkinnable().prefHeight(width); 272 | } else { 273 | return Double.MAX_VALUE; 274 | } 275 | } 276 | 277 | 278 | private void setShowTickMarks(final boolean TICKS_VISIBLE, final boolean LABELS_VISIBLE) { 279 | showTickMarks = (TICKS_VISIBLE || LABELS_VISIBLE); 280 | Slider slider = getSkinnable(); 281 | if (showTickMarks) { 282 | if (tickLine == null) { 283 | tickLine = new NumberAxis(); 284 | tickLine.setAutoRanging(false); 285 | tickLine.setSide(slider.getOrientation() == Orientation.VERTICAL ? Side.RIGHT : (slider.getOrientation() == null) ? Side.RIGHT : Side.BOTTOM); 286 | tickLine.setUpperBound(slider.getMax()); 287 | tickLine.setLowerBound(slider.getMin()); 288 | tickLine.setTickUnit(slider.getMajorTickUnit()); 289 | tickLine.setTickMarkVisible(TICKS_VISIBLE); 290 | tickLine.setTickLabelsVisible(LABELS_VISIBLE); 291 | tickLine.setMinorTickVisible(TICKS_VISIBLE); 292 | // add 1 to the slider minor tick count since the axis draws one 293 | // less minor ticks than the number given. 294 | tickLine.setMinorTickCount(Math.max(slider.getMinorTickCount(),0) + 1); 295 | if (slider.getLabelFormatter() != null) { 296 | tickLine.setTickLabelFormatter(stringConverterWrapper); 297 | } 298 | getChildren().setAll(tickLine, track, centerLine, trackProgress, thumb); 299 | } else { 300 | tickLine.setTickLabelsVisible(LABELS_VISIBLE); 301 | tickLine.setTickMarkVisible(TICKS_VISIBLE); 302 | tickLine.setMinorTickVisible(TICKS_VISIBLE); 303 | } 304 | } 305 | else { 306 | getChildren().setAll(track, centerLine, trackProgress, thumb); 307 | // tickLine = null; 308 | } 309 | 310 | getSkinnable().requestLayout(); 311 | } 312 | 313 | private void positionThumb(final boolean ANIMATE) { 314 | Slider s = getSkinnable(); 315 | if (s.getValue() > s.getMax()) return;// this can happen if we are bound to something 316 | final boolean HORIZONTAL = s.getOrientation() == Orientation.HORIZONTAL; 317 | final double END_X = (HORIZONTAL) ? trackStart + (((trackLength * ((s.getValue() - s.getMin()) / (s.getMax() - s.getMin()))) - thumbWidth/2)) : thumbLeft; 318 | final double END_Y = (HORIZONTAL) ? thumbTop : snappedTopInset() + trackLength - (trackLength * ((s.getValue() - s.getMin()) / (s.getMax() - s.getMin()))); // - thumbHeight/2 319 | 320 | if (ANIMATE) { 321 | // lets animate the thumb transition 322 | final double START_X = thumb.getLayoutX(); 323 | final double START_Y = thumb.getLayoutY(); 324 | Transition transition = new Transition() { 325 | { 326 | setCycleDuration(Duration.millis(200)); 327 | } 328 | 329 | @Override protected void interpolate(double frac) { 330 | if (!Double.isNaN(START_X)) { 331 | thumb.setLayoutX(START_X + frac * (END_X - START_X)); 332 | } 333 | if (!Double.isNaN(START_Y)) { 334 | thumb.setLayoutY(START_Y + frac * (END_Y - START_Y)); 335 | } 336 | } 337 | }; 338 | transition.play(); 339 | } else { 340 | thumb.setLayoutX(END_X); 341 | thumb.setLayoutY(END_Y); 342 | } 343 | } 344 | 345 | private double minTrackLength() { return 2 * thumb.prefWidth(-1); } 346 | 347 | @Override protected void layoutChildren(final double X, final double Y, final double W, final double H) { 348 | double value = getSkinnable().getValue(); 349 | double range = Math.abs(getSkinnable().getMax() - getSkinnable().getMin()); 350 | 351 | thumbWidth = snapSize(thumb.prefWidth(-1)); 352 | thumbHeight = snapSize(thumb.prefHeight(-1)); 353 | thumb.resize(thumbWidth, thumbHeight); 354 | 355 | double trackRadius = track.getBackground() == null ? 0 : track.getBackground().getFills().size() > 0 ? 356 | track.getBackground().getFills().get(0).getRadii().getTopLeftHorizontalRadius() : 0; 357 | 358 | if (getSkinnable().getOrientation() == Orientation.HORIZONTAL) { 359 | double tickLineHeight = (showTickMarks) ? tickLine.prefHeight(-1) : 0; 360 | double trackHeight = snapSize(track.prefHeight(-1)); 361 | double trackAreaHeight = Math.max(trackHeight,thumbHeight); 362 | double totalHeightNeeded = trackAreaHeight + ((showTickMarks) ? trackToTickGap+tickLineHeight : 0); 363 | double startY = Y + ((H - totalHeightNeeded)/2); 364 | double trackTop = (int)(startY + ((trackAreaHeight-trackHeight)/2)); 365 | trackLength = snapSize(W - thumbWidth); 366 | trackStart = snapPosition(X + (thumbWidth/2)); 367 | thumbTop = (int)(startY + ((trackAreaHeight-thumbHeight)/2)); 368 | 369 | positionThumb(false); 370 | // layout track 371 | track.resizeRelocate((int)(trackStart - trackRadius), trackTop , 372 | (int)(trackLength + trackRadius + trackRadius), trackHeight); 373 | 374 | // layout center line 375 | centerLine.setStartX(W * 0.5); 376 | centerLine.setStartY(H * 0.5 - 3); 377 | centerLine.setEndX(W * 0.5); 378 | centerLine.setEndY(H * 0.5 + 3); 379 | 380 | // layout trackProgress 381 | trackProgress.resizeRelocate((int)(trackStart - trackRadius), trackTop , 382 | (int)((trackLength * (value / range)) + trackRadius + trackRadius), trackHeight); 383 | 384 | // layout tick line 385 | if (showTickMarks) { 386 | tickLine.setLayoutX(trackStart); 387 | tickLine.setLayoutY(trackTop+trackHeight+trackToTickGap); 388 | tickLine.resize(trackLength, tickLineHeight); 389 | tickLine.requestAxisLayout(); 390 | } else { 391 | if (tickLine != null) { 392 | tickLine.resize(0,0); 393 | tickLine.requestAxisLayout(); 394 | } 395 | tickLine = null; 396 | } 397 | } else { 398 | double tickLineWidth = (showTickMarks) ? tickLine.prefWidth(-1) : 0; 399 | double trackWidth = snapSize(track.prefWidth(-1)); 400 | double trackAreaWidth = Math.max(trackWidth,thumbWidth); 401 | double totalWidthNeeded = trackAreaWidth + ((showTickMarks) ? trackToTickGap+tickLineWidth : 0) ; 402 | double startX = X + ((W - totalWidthNeeded)/2); 403 | double trackLeft = (int)(startX + ((trackAreaWidth-trackWidth)/2)); 404 | trackLength = snapSize(H - thumbHeight); 405 | trackStart = snapPosition(Y + (thumbHeight/2)); 406 | thumbLeft = (int)(startX + ((trackAreaWidth-thumbWidth)/2)); 407 | 408 | positionThumb(false); 409 | // layout track 410 | track.resizeRelocate(trackLeft, 411 | (int)(trackStart - trackRadius), 412 | trackWidth, 413 | (int)(trackLength + trackRadius + trackRadius)); 414 | 415 | // layout center line 416 | // layout center line 417 | centerLine.setStartX(W * 0.5 - 3); 418 | centerLine.setStartY(H * 0.5); 419 | centerLine.setEndX(W * 0.5 + 3); 420 | centerLine.setEndY(H * 0.5); 421 | 422 | // layout trackProgress 423 | trackProgress.resizeRelocate(trackLeft, 424 | (int)(trackStart - trackRadius), 425 | trackWidth, 426 | (int)((trackLength * (value / range)) + trackRadius + trackRadius)); 427 | 428 | // layout tick line 429 | if (showTickMarks) { 430 | tickLine.setLayoutX(trackLeft+trackWidth+trackToTickGap); 431 | tickLine.setLayoutY(trackStart); 432 | tickLine.resize(tickLineWidth, trackLength); 433 | tickLine.requestAxisLayout(); 434 | } else { 435 | if (tickLine != null) { 436 | tickLine.resize(0,0); 437 | tickLine.requestAxisLayout(); 438 | } 439 | tickLine = null; 440 | } 441 | } 442 | } 443 | } 444 | -------------------------------------------------------------------------------- /src/main/java/eu/hansolo/iosfx/iosentry/IosEntry.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 by Gerrit Grunwald 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package eu.hansolo.iosfx.iosentry; 18 | 19 | import eu.hansolo.iosfx.events.IosEvent; 20 | import eu.hansolo.iosfx.events.IosEventListener; 21 | import eu.hansolo.iosfx.events.IosEventType; 22 | import eu.hansolo.iosfx.fonts.Fonts; 23 | import eu.hansolo.iosfx.iosmultibutton.IosMultiButton; 24 | import eu.hansolo.iosfx.iosmultibutton.IosMultiButton.Type; 25 | import eu.hansolo.iosfx.tools.Helper; 26 | import javafx.animation.Interpolator; 27 | import javafx.animation.KeyFrame; 28 | import javafx.animation.KeyValue; 29 | import javafx.animation.Timeline; 30 | import javafx.beans.DefaultProperty; 31 | import javafx.beans.property.BooleanProperty; 32 | import javafx.beans.property.BooleanPropertyBase; 33 | import javafx.beans.property.StringProperty; 34 | import javafx.beans.property.StringPropertyBase; 35 | import javafx.collections.ObservableList; 36 | import javafx.event.EventHandler; 37 | import javafx.event.EventType; 38 | import javafx.geometry.Insets; 39 | import javafx.geometry.Pos; 40 | import javafx.scene.Node; 41 | import javafx.scene.control.Label; 42 | import javafx.scene.input.MouseEvent; 43 | import javafx.scene.layout.HBox; 44 | import javafx.scene.layout.Priority; 45 | import javafx.scene.layout.Region; 46 | import javafx.scene.layout.VBox; 47 | import javafx.util.Duration; 48 | 49 | import java.util.List; 50 | import java.util.concurrent.CopyOnWriteArrayList; 51 | 52 | 53 | /** 54 | * User: hansolo 55 | * Date: 25.05.18 56 | * Time: 06:14 57 | */ 58 | @DefaultProperty("children") 59 | public class IosEntry extends Region { 60 | private static final double PREFERRED_WIDTH = 375; 61 | private static final double PREFERRED_HEIGHT = 44; 62 | private static final double MINIMUM_WIDTH = 100; 63 | private static final double MINIMUM_HEIGHT = 10; 64 | private static final double MAXIMUM_WIDTH = 2048; 65 | private static final double MAXIMUM_HEIGHT = 1024; 66 | private static final double BUTTON_WIDTH = 82; 67 | private final IosEvent DELETE_ENTRY_EVT = new IosEvent(IosEntry.this, IosEventType.DELETE_ENTRY); 68 | private final IosEvent PRESSED_EVT = new IosEvent(IosEntry.this, IosEventType.PRESSED); 69 | private final IosEvent RELEASED_EVT = new IosEvent(IosEntry.this, IosEventType.RELEASED); 70 | private double size; 71 | private double width; 72 | private double height; 73 | private Label titleLabel; 74 | private Label subtitleLabel; 75 | 76 | private HBox pane; 77 | 78 | private Node leftNode; 79 | private String _title; 80 | private StringProperty title; 81 | private String _subtitle; 82 | private StringProperty subtitle; 83 | private VBox textBox; 84 | private Node rightNode; 85 | private Label action; 86 | private Label delete; 87 | 88 | private boolean _hasDelete; 89 | private BooleanProperty hasDelete; 90 | 91 | private boolean _hasAction; 92 | private BooleanProperty hasAction; 93 | 94 | private boolean preDelete; 95 | 96 | private boolean hasForward; 97 | 98 | private Timeline timeline; 99 | private EventHandler mouseHandler; 100 | private double draggedStartX; 101 | 102 | private List listeners; 103 | 104 | 105 | // ******************** Constructors ************************************** 106 | public IosEntry() { 107 | this(null, "", "", null); 108 | } 109 | public IosEntry(final Node LEFT_NODE, final String TITLE, final String SUB_TITLE, final Node RIGHT_NODE) { 110 | getStylesheets().add(IosEntry.class.getResource("ios-entry.css").toExternalForm()); 111 | 112 | leftNode = LEFT_NODE; 113 | _title = TITLE; 114 | _subtitle = SUB_TITLE; 115 | rightNode = RIGHT_NODE; 116 | _hasDelete = true; 117 | _hasAction = true; 118 | preDelete = false; 119 | hasForward = false; 120 | timeline = new Timeline(); 121 | 122 | if (null != rightNode) { 123 | if (rightNode instanceof IosMultiButton) { 124 | IosMultiButton mb = (IosMultiButton) rightNode; 125 | hasForward = Type.FORWARD == mb.getType(); 126 | mb = null; 127 | } 128 | } 129 | 130 | mouseHandler = e -> { 131 | final EventType TYPE = e.getEventType(); 132 | double translateX = getTranslateX(); 133 | boolean hasAction = getHasAction(); 134 | boolean hasDelete = getHasDelete(); 135 | double x = e.getSceneX(); 136 | 137 | if (TYPE.equals(MouseEvent.MOUSE_PRESSED)) { 138 | if (hasForward) { fireIosEvent(PRESSED_EVT); } 139 | draggedStartX = x; 140 | } else if (TYPE.equals(MouseEvent.MOUSE_DRAGGED)) { 141 | double delta = (draggedStartX - x) * -1; 142 | 143 | if (hasDelete && !preDelete && delta < -pane.getPrefWidth() * 0.5) { 144 | preDelete = true; 145 | animateToDirectDelete(); 146 | return; 147 | } else if (Double.compare(translateX, 0) == 0 && delta > 0 || 148 | Double.compare(x, draggedStartX) == 0 || 149 | (hasAction && hasDelete && Double.compare(translateX, -2 * BUTTON_WIDTH) == 0 && delta < 0) || 150 | ((hasAction && !hasDelete) && Double.compare(translateX, -BUTTON_WIDTH) == 0 && delta < 0) || 151 | ((!hasAction && hasDelete) && Double.compare(translateX, -BUTTON_WIDTH) == 0 && delta < 0)) { 152 | return; 153 | } 154 | 155 | if (hasAction && hasDelete) { 156 | if (delta > 0) { 157 | setTranslateX(-2 * BUTTON_WIDTH + Helper.clamp(0, 2 * BUTTON_WIDTH, delta)); 158 | action.setTranslateX(Helper.clamp(0, BUTTON_WIDTH, delta * 0.75)); 159 | } else { 160 | setTranslateX(Helper.clamp(-2 * BUTTON_WIDTH, 0, delta)); 161 | action.setTranslateX(Helper.clamp(0, BUTTON_WIDTH, BUTTON_WIDTH + delta * 0.75)); 162 | } 163 | } else if (hasAction || hasDelete) { 164 | if (preDelete) { return; } 165 | if (delta > 0) { 166 | setTranslateX(-BUTTON_WIDTH + Helper.clamp(0, BUTTON_WIDTH, delta)); 167 | } else { 168 | setTranslateX(Helper.clamp(-BUTTON_WIDTH, 0, delta)); 169 | } 170 | } 171 | } else if (TYPE.equals(MouseEvent.MOUSE_RELEASED)) { 172 | if (preDelete) { 173 | fireIosEvent(DELETE_ENTRY_EVT); 174 | return; 175 | } else if (hasForward) { 176 | fireIosEvent(RELEASED_EVT); 177 | } else if (Double.compare(x, draggedStartX) == 0 || 178 | hasAction && hasDelete && Double.compare(translateX, -2 * BUTTON_WIDTH) == 0 || 179 | (hasAction || hasDelete) && Double.compare(translateX, -BUTTON_WIDTH) == 0) { 180 | return; 181 | } 182 | animate(); 183 | } 184 | }; 185 | listeners = new CopyOnWriteArrayList<>(); 186 | 187 | initGraphics(); 188 | registerListeners(); 189 | } 190 | 191 | 192 | // ******************** Initialization ************************************ 193 | private void initGraphics() { 194 | if (Double.compare(getPrefWidth(), 0.0) <= 0 || Double.compare(getPrefHeight(), 0.0) <= 0 || Double.compare(getWidth(), 0.0) <= 0 || 195 | Double.compare(getHeight(), 0.0) <= 0) { 196 | if (getPrefWidth() > 0 && getPrefHeight() > 0) { 197 | setPrefSize(getPrefWidth(), getPrefHeight()); 198 | } else { 199 | setPrefSize(PREFERRED_WIDTH, PREFERRED_HEIGHT); 200 | } 201 | } 202 | 203 | getStyleClass().add("ios-entry"); 204 | 205 | titleLabel = new Label(getTitle()); 206 | titleLabel.getStyleClass().add("title"); 207 | titleLabel.setAlignment(Pos.CENTER_LEFT); 208 | titleLabel.setMaxWidth(Double.MAX_VALUE); 209 | if (null == getTitle() || getTitle().isEmpty()) { Helper.enableNode(titleLabel, false); } 210 | 211 | subtitleLabel = new Label(getSubtitle()); 212 | subtitleLabel.getStyleClass().add("subtitle"); 213 | subtitleLabel.setAlignment(Pos.CENTER_LEFT); 214 | subtitleLabel.setMaxWidth(Double.MAX_VALUE); 215 | 216 | if (null == getSubtitle() || getSubtitle().isEmpty()) { Helper.enableNode(subtitleLabel, false); } 217 | 218 | textBox = new VBox(2, titleLabel, subtitleLabel); 219 | textBox.setAlignment(Pos.CENTER_LEFT); 220 | 221 | action = new Label("Action"); 222 | action.getStyleClass().add("action"); 223 | action.setFont(Fonts.robotoRegular(18)); 224 | action.setManaged(false); 225 | action.setVisible(false); 226 | action.setPrefSize(BUTTON_WIDTH, PREFERRED_HEIGHT); 227 | 228 | delete = new Label("Delete"); 229 | delete.getStyleClass().add("delete"); 230 | delete.setFont(Fonts.robotoRegular(18)); 231 | delete.setManaged(false); 232 | delete.setVisible(false); 233 | delete.setPrefSize(BUTTON_WIDTH, PREFERRED_HEIGHT); 234 | 235 | pane = new HBox(15); 236 | 237 | HBox.setHgrow(titleLabel, Priority.ALWAYS); 238 | HBox.setHgrow(subtitleLabel, Priority.ALWAYS); 239 | HBox.setHgrow(textBox, Priority.ALWAYS); 240 | HBox.setHgrow(action, Priority.NEVER); 241 | HBox.setHgrow(delete, Priority.NEVER); 242 | HBox.setMargin(action, Insets.EMPTY); 243 | HBox.setMargin(delete, Insets.EMPTY); 244 | 245 | if (null != getLeftNode()) { 246 | pane.getChildren().add(getLeftNode()); 247 | HBox.setMargin(getLeftNode(), new Insets(0, 0, 0, 15)); 248 | } 249 | if (null != textBox) { pane.getChildren().add(textBox); } 250 | if (null != getRightNode()) { pane.getChildren().add(getRightNode()); } 251 | pane.getChildren().addAll(action, delete); 252 | pane.setAlignment(Pos.CENTER); 253 | 254 | getChildren().setAll(pane); 255 | } 256 | 257 | private void registerListeners() { 258 | widthProperty().addListener(o -> resize()); 259 | heightProperty().addListener(o -> resize()); 260 | addEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler); 261 | addEventHandler(MouseEvent.MOUSE_DRAGGED, mouseHandler); 262 | addEventHandler(MouseEvent.MOUSE_RELEASED, mouseHandler); 263 | delete.setOnMousePressed(e -> fireIosEvent(DELETE_ENTRY_EVT)); 264 | } 265 | 266 | 267 | // ******************** Methods ******************************************* 268 | @Override public void layoutChildren() { 269 | super.layoutChildren(); 270 | } 271 | 272 | @Override protected double computeMinWidth(final double HEIGHT) { return MINIMUM_WIDTH; } 273 | @Override protected double computeMinHeight(final double WIDTH) { return MINIMUM_HEIGHT; } 274 | @Override protected double computePrefWidth(final double HEIGHT) { return super.computePrefWidth(HEIGHT); } 275 | @Override protected double computePrefHeight(final double WIDTH) { return super.computePrefHeight(WIDTH); } 276 | @Override protected double computeMaxWidth(final double HEIGHT) { return MAXIMUM_WIDTH; } 277 | @Override protected double computeMaxHeight(final double WIDTH) { return MAXIMUM_HEIGHT; } 278 | 279 | private void handleControlPropertyChanged(final String PROPERTY) { 280 | if ("".equals(PROPERTY)) { 281 | 282 | } 283 | } 284 | 285 | @Override public ObservableList getChildren() { return super.getChildren(); } 286 | 287 | public Node getLeftNode() { return leftNode; } 288 | public void setLeftNode(final Node NODE) { leftNode = NODE; } 289 | 290 | public String getTitle() { return null == title ? _title : title.get(); } 291 | public void setTitle(final String TITLE) { 292 | if (null == title) { 293 | _title = TITLE; 294 | titleLabel.setText(_title); 295 | } else { 296 | title.set(TITLE); 297 | } 298 | } 299 | public StringProperty titleProperty() { 300 | if (null == title) { 301 | title = new StringPropertyBase(_title) { 302 | @Override protected void invalidated() { titleLabel.setText(get());} 303 | @Override public Object getBean() { return IosEntry.this; } 304 | @Override public String getName() { return "title"; } 305 | }; 306 | _title = null; 307 | } 308 | return title; 309 | } 310 | 311 | public String getSubtitle() { return null == subtitle ? _subtitle : subtitle.get(); } 312 | public void setSubtitle(final String SUB_TITLE) { 313 | if (null == subtitle) { 314 | _subtitle = SUB_TITLE; 315 | subtitleLabel.setText(_subtitle); 316 | } else { 317 | subtitle.set(SUB_TITLE); 318 | } 319 | } 320 | public StringProperty subtitleProperty() { 321 | if (null == subtitle) { 322 | subtitle = new StringPropertyBase(_subtitle) { 323 | @Override protected void invalidated() { subtitleLabel.setText(get()); } 324 | @Override public Object getBean() { return IosEntry.this; } 325 | @Override public String getName() { return "subtitle"; } 326 | }; 327 | _subtitle = null; 328 | } 329 | return subtitle; 330 | } 331 | 332 | public Node getRightNode() { return rightNode; } 333 | public void setRightNode(final Node NODE) { rightNode = NODE; } 334 | 335 | public boolean getHasDelete() { return null == hasDelete ? _hasDelete : hasDelete.get(); } 336 | public void setHasDelete(final boolean HAS_DELETE) { 337 | if (null == hasDelete) { 338 | _hasDelete = HAS_DELETE; 339 | delete.setManaged(HAS_DELETE); 340 | delete.setVisible(HAS_DELETE); 341 | adjustMargins(); 342 | } else { 343 | hasDelete.set(HAS_DELETE); 344 | } 345 | } 346 | public BooleanProperty hasDeleteProperty() { 347 | if (null == hasDelete) { 348 | hasDelete = new BooleanPropertyBase(_hasDelete) { 349 | @Override protected void invalidated() { 350 | delete.setManaged(get()); 351 | delete.setVisible(get()); 352 | adjustMargins(); 353 | } 354 | @Override public Object getBean() { return IosEntry.this; } 355 | @Override public String getName() { return "hasDelete"; } 356 | }; 357 | } 358 | return hasDelete; 359 | } 360 | 361 | public boolean getHasAction() { return null == hasAction ? _hasAction : hasAction.get(); } 362 | public void setHasAction(final boolean HAS_ACTION) { 363 | if (null == hasAction) { 364 | _hasAction = HAS_ACTION; 365 | action.setManaged(HAS_ACTION); 366 | action.setVisible(HAS_ACTION); 367 | adjustMargins(); 368 | } else { 369 | hasAction.set(HAS_ACTION); 370 | } 371 | } 372 | public BooleanProperty hasActionProperty() { 373 | if (null == hasAction) { 374 | hasAction = new BooleanPropertyBase(_hasAction) { 375 | @Override protected void invalidated() { 376 | action.setManaged(get()); 377 | action.setVisible(get()); 378 | adjustMargins(); 379 | } 380 | @Override public Object getBean() { return IosEntry.this; } 381 | @Override public String getName() { return "hasAction"; } 382 | }; 383 | } 384 | return hasAction; 385 | } 386 | 387 | public void setActionLabel(final String TEXT) { action.setText(TEXT); } 388 | 389 | public void addOnActionPressed(final EventHandler HANDLER) { action.addEventHandler(MouseEvent.MOUSE_PRESSED, HANDLER); } 390 | public void removeOnActionPressed(final EventHandler HANDLER) { action.removeEventHandler(MouseEvent.MOUSE_PRESSED, HANDLER);} 391 | 392 | private void adjustMargins() { 393 | action.setTranslateX(0); 394 | if (getHasAction() && getHasDelete()) { 395 | HBox.setMargin(action, new Insets(0, -15, 0, 0)); 396 | action.setTranslateX(BUTTON_WIDTH); 397 | } else if (getHasAction() && !getHasDelete()) { 398 | HBox.setMargin(action, new Insets(0, 0, 0, 0)); 399 | } else if (!getHasAction() && !getHasDelete() && getRightNode() != null) { 400 | HBox.setMargin(getRightNode(), new Insets(0, 15, 0, 0)); 401 | } else if (null == getLeftNode()) { 402 | HBox.setMargin(textBox, new Insets(0, 0, 0, 15)); 403 | } 404 | } 405 | 406 | private void animate() { 407 | double translateX = getTranslateX(); 408 | if (Double.compare(translateX, 0) == 0) { return; } 409 | if (getHasAction() && getHasDelete()) { 410 | if (Double.compare(translateX, -2 * BUTTON_WIDTH) == 0) { return; } 411 | if (translateX < -BUTTON_WIDTH) { 412 | animateToShowButtons(true); 413 | } else { 414 | animateToHideButtons(); 415 | } 416 | } else if (getHasAction() || getHasDelete()) { 417 | if (Double.compare(translateX, -BUTTON_WIDTH) == 0) { return; } 418 | if (translateX < -BUTTON_WIDTH * 0.5) { 419 | animateToShowButtons(false); 420 | } else { 421 | animateToHideButtons(); 422 | } 423 | } 424 | } 425 | 426 | private void animateToShowButtons(final boolean TWO_BUTTONS) { 427 | KeyValue kvTranslateXStart = new KeyValue(translateXProperty(), getTranslateX(), Interpolator.EASE_BOTH); 428 | KeyValue kvTranslateXEnd = new KeyValue(translateXProperty(), TWO_BUTTONS ? (-2 * BUTTON_WIDTH) : -BUTTON_WIDTH, Interpolator.EASE_BOTH); 429 | KeyValue kvActionTranslateXStart = new KeyValue(action.translateXProperty(), action.getTranslateX(), Interpolator.EASE_BOTH); 430 | KeyValue kvActionTranslateXEnd = new KeyValue(action.translateXProperty(), 0, Interpolator.EASE_BOTH); 431 | 432 | KeyFrame kf0 = TWO_BUTTONS ? new KeyFrame(Duration.ZERO, kvTranslateXStart, kvActionTranslateXStart) : new KeyFrame(Duration.ZERO, kvTranslateXStart); 433 | KeyFrame kf1 = TWO_BUTTONS ? new KeyFrame(Duration.millis(Helper.ANIMATION_DURATION), kvTranslateXEnd, kvActionTranslateXEnd) : new KeyFrame(Duration.millis(Helper.ANIMATION_DURATION), kvTranslateXEnd); 434 | 435 | timeline.getKeyFrames().setAll(kf0, kf1); 436 | timeline.play(); 437 | } 438 | private void animateToHideButtons() { 439 | KeyValue kvTranslateXStart = new KeyValue(translateXProperty(), getTranslateX(), Interpolator.EASE_BOTH); 440 | KeyValue kvTranslateXEnd = new KeyValue(translateXProperty(), 0, Interpolator.EASE_BOTH); 441 | KeyValue kvActionTranslateXStart = new KeyValue(action.translateXProperty(), action.getTranslateX(), Interpolator.EASE_BOTH); 442 | KeyValue kvActionTranslateXEnd = new KeyValue(action.translateXProperty(), BUTTON_WIDTH, Interpolator.EASE_BOTH); 443 | 444 | KeyFrame kf0 = new KeyFrame(Duration.ZERO, kvTranslateXStart, kvActionTranslateXStart); 445 | KeyFrame kf1 = new KeyFrame(Duration.millis(Helper.ANIMATION_DURATION), kvTranslateXEnd, kvActionTranslateXEnd); 446 | 447 | timeline.getKeyFrames().setAll(kf0, kf1); 448 | timeline.play(); 449 | } 450 | 451 | private void animateToDirectDelete() { 452 | pane.setMaxWidth(PREFERRED_WIDTH + BUTTON_WIDTH + BUTTON_WIDTH); 453 | pane.setPrefWidth(PREFERRED_WIDTH + BUTTON_WIDTH + BUTTON_WIDTH); 454 | setTranslateX(-BUTTON_WIDTH - BUTTON_WIDTH); 455 | 456 | double targetWidth = pane.getPrefWidth(); 457 | 458 | KeyValue kvDeleteWidthStart = new KeyValue(delete.prefWidthProperty(), delete.getPrefWidth(), Interpolator.EASE_BOTH); 459 | KeyValue kvDeleteWidthEnd = new KeyValue(delete.prefWidthProperty(), targetWidth, Interpolator.EASE_BOTH); 460 | 461 | KeyFrame kf0 = new KeyFrame(Duration.ZERO, kvDeleteWidthStart); 462 | KeyFrame kf1 = new KeyFrame(Duration.millis(Helper.ANIMATION_DURATION), kvDeleteWidthEnd); 463 | timeline.getKeyFrames().setAll(kf0, kf1); 464 | 465 | timeline.play(); 466 | } 467 | 468 | 469 | // ******************** Event Handling ************************************ 470 | public void addOnIosEvent(final IosEventListener LISTENER) { if (!listeners.contains(LISTENER)) { listeners.add(LISTENER); } } 471 | public void removeOnIosEvent(final IosEventListener LISTENER) { if (listeners.contains(LISTENER)) { listeners.remove(LISTENER); } } 472 | 473 | private void fireIosEvent(final IosEvent EVENT) { 474 | listeners.forEach(listener -> listener.onIosEvent(EVENT)); 475 | } 476 | 477 | 478 | // ******************** Resizing ****************************************** 479 | private void resize() { 480 | width = getWidth() - getInsets().getLeft() - getInsets().getRight(); 481 | height = getHeight() - getInsets().getTop() - getInsets().getBottom(); 482 | size = width < height ? width : height; 483 | 484 | if (width > 0 && height > 0) { 485 | if (getHasAction() && getHasDelete()) { 486 | pane.setPrefSize(width + BUTTON_WIDTH + BUTTON_WIDTH, PREFERRED_HEIGHT); 487 | pane.setMaxSize(width + BUTTON_WIDTH + BUTTON_WIDTH, PREFERRED_HEIGHT); 488 | } else if (getHasAction() || getHasDelete()) { 489 | pane.setPrefSize(width + BUTTON_WIDTH, PREFERRED_HEIGHT); 490 | pane.setMaxSize(width + BUTTON_WIDTH, PREFERRED_HEIGHT); 491 | } else { 492 | pane.setPrefSize(width, PREFERRED_HEIGHT); 493 | pane.setMaxSize(width, PREFERRED_HEIGHT); 494 | } 495 | 496 | pane.relocate((getWidth() - width) * 0.5, (getHeight() - height) * 0.5); 497 | 498 | //action.setPrefSize(BUTTON_WIDTH, PREFERRED_HEIGHT); 499 | 500 | //delete.setPrefSize(BUTTON_WIDTH, PREFERRED_HEIGHT); 501 | 502 | redraw(); 503 | } 504 | } 505 | 506 | private void redraw() { 507 | 508 | } 509 | } 510 | -------------------------------------------------------------------------------- /src/main/java/eu/hansolo/iosfx/iosswitch/IosSwitch.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 by Gerrit Grunwald 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package eu.hansolo.iosfx.iosswitch; 18 | 19 | import eu.hansolo.iosfx.events.IosEvent; 20 | import eu.hansolo.iosfx.events.IosEventListener; 21 | import eu.hansolo.iosfx.events.IosEventType; 22 | import eu.hansolo.iosfx.tools.Helper; 23 | import javafx.animation.AnimationTimer; 24 | import javafx.animation.Interpolator; 25 | import javafx.animation.KeyFrame; 26 | import javafx.animation.KeyValue; 27 | import javafx.animation.Timeline; 28 | import javafx.beans.DefaultProperty; 29 | import javafx.beans.binding.Bindings; 30 | import javafx.beans.binding.BooleanBinding; 31 | import javafx.beans.property.BooleanProperty; 32 | import javafx.beans.property.BooleanPropertyBase; 33 | import javafx.beans.property.DoubleProperty; 34 | import javafx.beans.property.DoublePropertyBase; 35 | import javafx.beans.property.ObjectProperty; 36 | import javafx.beans.property.Property; 37 | import javafx.beans.value.ChangeListener; 38 | import javafx.collections.ObservableList; 39 | import javafx.css.CssMetaData; 40 | import javafx.css.PseudoClass; 41 | import javafx.css.Styleable; 42 | import javafx.css.StyleableProperty; 43 | import javafx.css.StyleablePropertyFactory; 44 | import javafx.event.EventHandler; 45 | import javafx.geometry.Dimension2D; 46 | import javafx.geometry.Insets; 47 | import javafx.scene.Node; 48 | import javafx.scene.effect.BlurType; 49 | import javafx.scene.effect.DropShadow; 50 | import javafx.scene.input.MouseEvent; 51 | import javafx.scene.layout.Pane; 52 | import javafx.scene.layout.Region; 53 | import javafx.scene.paint.Color; 54 | import javafx.scene.shape.Circle; 55 | import javafx.scene.shape.Rectangle; 56 | import javafx.util.Duration; 57 | 58 | import java.util.HashMap; 59 | import java.util.List; 60 | import java.util.Map; 61 | import java.util.concurrent.CopyOnWriteArrayList; 62 | 63 | 64 | /** 65 | * User: hansolo 66 | * Date: 14.05.18 67 | * Time: 11:15 68 | */ 69 | @DefaultProperty("children") 70 | public class IosSwitch extends Region { 71 | public static final double MIN_DURATION = 10; 72 | public static final double MAX_DURATION = 500; 73 | public static final Color DEFAULT_SELECTED_COLOR = Color.rgb(75, 216, 99); 74 | private static final double PREFERRED_WIDTH = 38; 75 | private static final double PREFERRED_HEIGHT = 23; 76 | private static final double MINIMUM_WIDTH = 20; 77 | private static final double MINIMUM_HEIGHT = 12; 78 | private static final double MAXIMUM_WIDTH = 1024; 79 | private static final double MAXIMUM_HEIGHT = 1024; 80 | private static final double ASPECT_RATIO = PREFERRED_HEIGHT / PREFERRED_WIDTH; 81 | private static final long LONG_PRESS_TIME = 200_000_000l; 82 | private static final StyleablePropertyFactory FACTORY = new StyleablePropertyFactory<>(Region.getClassCssMetaData()); 83 | private static final PseudoClass DARK_PSEUDO_CLASS = PseudoClass.getPseudoClass("dark"); 84 | private final IosEvent SELECTED_EVT = new IosEvent(IosSwitch.this, IosEventType.SELECTED); 85 | private final IosEvent DESELECTED_EVT = new IosEvent(IosSwitch.this, IosEventType.DESELECTED); 86 | private final StyleableProperty selectedColor; 87 | private boolean _dark; 88 | private BooleanProperty dark; 89 | private double width; 90 | private double height; 91 | private DropShadow dropShadow; 92 | private Rectangle backgroundArea; 93 | private Rectangle mainArea; 94 | private Rectangle knob; 95 | private Circle zero; 96 | private Rectangle one; 97 | private Pane pane; 98 | private long pressStart; 99 | private AnimationTimer holdTimer; 100 | private boolean _selected; 101 | private BooleanProperty selected; 102 | private double _duration; 103 | private DoubleProperty duration; 104 | private boolean _showOnOffText; 105 | private BooleanProperty showOnOffText; 106 | private Timeline timeline; 107 | private BooleanBinding showing; 108 | private ChangeListener showingListener; 109 | private HashMap settings; 110 | private EventHandler clickedHandler; 111 | private EventHandler pressedHandler; 112 | private List listeners; 113 | 114 | 115 | 116 | // ******************** Constructors ************************************** 117 | public IosSwitch() { 118 | this(new HashMap<>()); 119 | } 120 | public IosSwitch(final Map SETTINGS) { 121 | pressStart = System.nanoTime(); 122 | holdTimer = new AnimationTimer() { 123 | @Override public void handle(final long now) { 124 | if (now - pressStart > LONG_PRESS_TIME) { 125 | holdTimer.stop(); 126 | if (isSelected()) { 127 | animateToPreDeselect(); 128 | } else { 129 | animateToPreSelect(); 130 | } 131 | } 132 | } 133 | }; 134 | _selected = false; 135 | selectedColor = FACTORY.createStyleableColorProperty(IosSwitch.this, "selectedColor", "-selected-color", s -> s.selectedColor, DEFAULT_SELECTED_COLOR); 136 | _dark = false; 137 | _duration = 250; 138 | _showOnOffText = false; 139 | showingListener = (o, ov, nv) -> { if (nv) { applySettings(); } }; 140 | settings = new HashMap<>(SETTINGS); 141 | timeline = new Timeline(); 142 | listeners = new CopyOnWriteArrayList<>(); 143 | clickedHandler = e -> setSelected(!isSelected()); 144 | pressedHandler = e -> { 145 | pressStart = System.nanoTime(); 146 | holdTimer.start(); 147 | }; 148 | 149 | initGraphics(); 150 | registerListeners(); 151 | applySettings(); 152 | } 153 | 154 | 155 | // ******************** Initialization ************************************ 156 | private void initGraphics() { 157 | if (Double.compare(getPrefWidth(), 0.0) <= 0 || Double.compare(getPrefHeight(), 0.0) <= 0 || Double.compare(getWidth(), 0.0) <= 0 || 158 | Double.compare(getHeight(), 0.0) <= 0) { 159 | if (getPrefWidth() > 0 && getPrefHeight() > 0) { 160 | setPrefSize(getPrefWidth(), getPrefHeight()); 161 | } else { 162 | setPrefSize(PREFERRED_WIDTH, PREFERRED_HEIGHT); 163 | } 164 | } 165 | 166 | getStyleClass().add("ios-switch"); 167 | 168 | dropShadow = new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.25), 10.0, 0.0, 0, 5); 169 | 170 | backgroundArea = new Rectangle(); 171 | backgroundArea.getStyleClass().add("background-area"); 172 | if (isSelected()) { 173 | backgroundArea.setFill(getSelectedColor()); 174 | } 175 | 176 | one = new Rectangle(); 177 | one.getStyleClass().add("one"); 178 | one.setMouseTransparent(true); 179 | one.setVisible(false); 180 | 181 | mainArea = new Rectangle(); 182 | mainArea.getStyleClass().add("main-area"); 183 | mainArea.setMouseTransparent(true); 184 | if (isSelected()) { 185 | mainArea.setOpacity(0); 186 | mainArea.setScaleX(0); 187 | mainArea.setScaleY(0); 188 | } 189 | 190 | zero = new Circle(); 191 | zero.getStyleClass().add("zero"); 192 | zero.setMouseTransparent(true); 193 | zero.setVisible(false); 194 | 195 | knob = new Rectangle(); 196 | knob.getStyleClass().add("knob"); 197 | knob.setEffect(dropShadow); 198 | knob.setMouseTransparent(true); 199 | 200 | pane = new Pane(backgroundArea, one, mainArea, zero, knob); 201 | 202 | getChildren().setAll(pane); 203 | } 204 | 205 | private void registerListeners() { 206 | widthProperty().addListener(o -> resize()); 207 | heightProperty().addListener(o -> resize()); 208 | disabledProperty().addListener(o -> setOpacity(isDisabled() ? 0.5 : 1.0)); 209 | backgroundArea.addEventHandler(MouseEvent.MOUSE_CLICKED, clickedHandler); 210 | backgroundArea.addEventHandler(MouseEvent.MOUSE_PRESSED, pressedHandler); 211 | if (null != getScene()) { 212 | setupBinding(); 213 | } else { 214 | sceneProperty().addListener((o1, ov1, nv1) -> { 215 | if (null == nv1) { return; } 216 | if (null != getScene().getWindow()) { 217 | setupBinding(); 218 | } else { 219 | sceneProperty().get().windowProperty().addListener((o2, ov2, nv2) -> { 220 | if (null == nv2) { return; } 221 | setupBinding(); 222 | }); 223 | } 224 | }); 225 | } 226 | } 227 | 228 | private void setupBinding() { 229 | showing = Bindings.selectBoolean(sceneProperty(), "window", "showing"); 230 | showing.addListener(showingListener); 231 | } 232 | 233 | private void applySettings() { 234 | if (settings.isEmpty()) { return; } 235 | for (String key : settings.keySet()) { 236 | if ("prefSize".equals(key)) { 237 | Dimension2D dim = ((ObjectProperty) settings.get(key)).get(); 238 | setPrefSize(dim.getWidth(), dim.getHeight()); 239 | } else if ("minSize".equals(key)) { 240 | Dimension2D dim = ((ObjectProperty) settings.get(key)).get(); 241 | setMinSize(dim.getWidth(), dim.getHeight()); 242 | } else if ("maxSize".equals(key)) { 243 | Dimension2D dim = ((ObjectProperty) settings.get(key)).get(); 244 | setMaxSize(dim.getWidth(), dim.getHeight()); 245 | } else if ("prefWidth".equals(key)) { 246 | setPrefWidth(((DoubleProperty) settings.get(key)).get()); 247 | } else if ("prefHeight".equals(key)) { 248 | setPrefHeight(((DoubleProperty) settings.get(key)).get()); 249 | } else if ("minWidth".equals(key)) { 250 | setMinWidth(((DoubleProperty) settings.get(key)).get()); 251 | } else if ("minHeight".equals(key)) { 252 | setMinHeight(((DoubleProperty) settings.get(key)).get()); 253 | } else if ("maxWidth".equals(key)) { 254 | setMaxWidth(((DoubleProperty) settings.get(key)).get()); 255 | } else if ("maxHeight".equals(key)) { 256 | setMaxHeight(((DoubleProperty) settings.get(key)).get()); 257 | } else if ("scaleX".equals(key)) { 258 | setScaleX(((DoubleProperty) settings.get(key)).get()); 259 | } else if ("scaleY".equals(key)) { 260 | setScaleY(((DoubleProperty) settings.get(key)).get()); 261 | } else if ("layoutX".equals(key)) { 262 | setLayoutX(((DoubleProperty) settings.get(key)).get()); 263 | } else if ("layoutY".equals(key)) { 264 | setLayoutY(((DoubleProperty) settings.get(key)).get()); 265 | } else if ("translateX".equals(key)) { 266 | setTranslateX(((DoubleProperty) settings.get(key)).get()); 267 | } else if ("translateY".equals(key)) { 268 | setTranslateY(((DoubleProperty) settings.get(key)).get()); 269 | } else if ("padding".equals(key)) { 270 | setPadding(((ObjectProperty) settings.get(key)).get()); 271 | } // Control specific settings 272 | else if ("selectedColor".equals(key)) { 273 | setSelectedColor(((ObjectProperty) settings.get(key)).get()); 274 | } else if("dark".equals(key)) { 275 | setDark(((BooleanProperty) settings.get(key)).get()); 276 | } else if ("showOnOffText".equals(key)) { 277 | setShowOnOffText(((BooleanProperty) settings.get(key)).get()); 278 | } else if ("duration".equals(key)) { 279 | setDuration(((DoubleProperty) settings.get(key)).get()); 280 | } 281 | } 282 | 283 | if (settings.containsKey("selected")) { setSelected(((BooleanProperty) settings.get("selected")).get()); } 284 | 285 | settings.clear(); 286 | if (null == showing) { return; } 287 | showing.removeListener(showingListener); 288 | } 289 | 290 | public void dispose() { 291 | backgroundArea.removeEventHandler(MouseEvent.MOUSE_CLICKED, clickedHandler); 292 | backgroundArea.removeEventHandler(MouseEvent.MOUSE_PRESSED, pressedHandler); 293 | } 294 | 295 | 296 | // ******************** Methods ******************************************* 297 | @Override public void layoutChildren() { 298 | super.layoutChildren(); 299 | } 300 | 301 | @Override protected double computeMinWidth(final double HEIGHT) { return MINIMUM_WIDTH; } 302 | @Override protected double computeMinHeight(final double WIDTH) { return MINIMUM_HEIGHT; } 303 | @Override protected double computePrefWidth(final double HEIGHT) { return super.computePrefWidth(HEIGHT); } 304 | @Override protected double computePrefHeight(final double WIDTH) { return super.computePrefHeight(WIDTH); } 305 | @Override protected double computeMaxWidth(final double HEIGHT) { return MAXIMUM_WIDTH; } 306 | @Override protected double computeMaxHeight(final double WIDTH) { return MAXIMUM_HEIGHT; } 307 | 308 | @Override public ObservableList getChildren() { return super.getChildren(); } 309 | 310 | public boolean isSelected() { return null == selected ? _selected : selected.get(); } 311 | public void setSelected(final boolean SELECTED) { 312 | holdTimer.stop(); 313 | if (null == selected) { 314 | _selected = SELECTED; 315 | if (_selected) { 316 | fireIosEvent(SELECTED ? SELECTED_EVT : DESELECTED_EVT); 317 | animateToSelect(); 318 | } else { 319 | animateToDeselect(); 320 | } 321 | } else { 322 | selected.set(SELECTED); 323 | } 324 | } 325 | public BooleanProperty selectedProperty() { 326 | if (null == selected) { 327 | selected = new BooleanPropertyBase(_selected) { 328 | @Override protected void invalidated() { 329 | fireIosEvent(get() ? SELECTED_EVT : DESELECTED_EVT); 330 | if (get()) { 331 | animateToSelect(); 332 | } else { 333 | animateToDeselect(); 334 | } 335 | } 336 | @Override public Object getBean() { return IosSwitch.this; } 337 | @Override public String getName() { return "selected"; } 338 | }; 339 | } 340 | return selected; 341 | } 342 | 343 | public Color getSelectedColor() { return selectedColor.getValue(); } 344 | public void setSelectedColor(final Color COLOR) { selectedColor.setValue(COLOR); } 345 | public ObjectProperty selectedColorProperty() { return (ObjectProperty) selectedColor; } 346 | 347 | public final boolean isDark() { 348 | return null == dark ? _dark : dark.get(); 349 | } 350 | public final void setDark(final boolean DARK) { 351 | if (null == dark) { 352 | _dark = DARK; 353 | pseudoClassStateChanged(DARK_PSEUDO_CLASS, DARK); 354 | } else { 355 | darkProperty().set(DARK); 356 | } 357 | } 358 | public final BooleanProperty darkProperty() { 359 | if (null == dark) { 360 | dark = new BooleanPropertyBase() { 361 | @Override protected void invalidated() { 362 | pseudoClassStateChanged(DARK_PSEUDO_CLASS, get()); 363 | } 364 | @Override public Object getBean() { return IosSwitch.this; } 365 | @Override public String getName() { return "dark"; } 366 | }; 367 | } 368 | return dark; 369 | } 370 | 371 | public double getDuration() { return null == duration ? _duration : duration.get(); } 372 | public void setDuration(final double DURATION) { 373 | if (null == duration) { 374 | _duration = Helper.clamp(MIN_DURATION, MAX_DURATION, DURATION); 375 | } else { 376 | duration.set(DURATION); 377 | } 378 | } 379 | public DoubleProperty durationProperty() { 380 | if (null == duration) { 381 | duration = new DoublePropertyBase(_duration) { 382 | @Override protected void invalidated() { set(Helper.clamp(MIN_DURATION, MAX_DURATION, get())); } 383 | @Override public Object getBean() { return IosSwitch.this; } 384 | @Override public String getName() { return "duration"; } 385 | }; 386 | } 387 | return duration; 388 | } 389 | 390 | public boolean getShowOnOffText() { return null == showOnOffText ? _showOnOffText : showOnOffText.get(); } 391 | public void setShowOnOffText(final boolean SHOW) { 392 | if (null == showOnOffText) { 393 | _showOnOffText = SHOW; 394 | one.setVisible(SHOW); 395 | zero.setVisible(SHOW); 396 | } else { 397 | showOnOffText.set(SHOW); 398 | } 399 | } 400 | public BooleanProperty showOnOffTextProperty() { 401 | if (null == showOnOffText) { 402 | showOnOffText = new BooleanPropertyBase(_showOnOffText) { 403 | @Override protected void invalidated() { 404 | one.setVisible(get()); 405 | zero.setVisible(get()); 406 | } 407 | @Override public Object getBean() { return IosSwitch.this; } 408 | @Override public String getName() { return "showOnOffText"; } 409 | }; 410 | } 411 | return showOnOffText; 412 | } 413 | 414 | protected HashMap getSettings() { return settings; } 415 | 416 | private void animateToPreSelect() { 417 | KeyValue kvKnobWidthStart = new KeyValue(knob.widthProperty(), height * 0.89130435, Interpolator.EASE_BOTH); 418 | KeyValue kvKnobWidthEnd = new KeyValue(knob.widthProperty(), height * 0.89130435 * 1.2, Interpolator.EASE_BOTH); 419 | KeyValue kvMainScaleXStart = new KeyValue(mainArea.scaleXProperty(), 1, Interpolator.EASE_BOTH); 420 | KeyValue kvMainScaleXEnd = new KeyValue(mainArea.scaleXProperty(), 0, Interpolator.EASE_BOTH); 421 | KeyValue kvMainScaleYStart = new KeyValue(mainArea.scaleYProperty(), 1, Interpolator.EASE_BOTH); 422 | KeyValue kvMainScaleYEnd = new KeyValue(mainArea.scaleYProperty(), 0, Interpolator.EASE_BOTH); 423 | KeyValue kvMainOpacityStart = new KeyValue(mainArea.opacityProperty(), 1, Interpolator.EASE_BOTH); 424 | KeyValue kvMainOpacityEnd = new KeyValue(mainArea.opacityProperty(), 0, Interpolator.EASE_BOTH); 425 | KeyValue kvZeroOpacityStart = new KeyValue(zero.opacityProperty(), 1, Interpolator.EASE_BOTH); 426 | KeyValue kvZeroOpacityEnd = new KeyValue(zero.opacityProperty(), 0, Interpolator.EASE_BOTH); 427 | KeyValue kvOneOpacityStart = new KeyValue(one.opacityProperty(), 0, Interpolator.EASE_BOTH); 428 | KeyValue kvOneOpacityEnd = new KeyValue(one.opacityProperty(), 1, Interpolator.EASE_BOTH); 429 | 430 | KeyFrame kf0; 431 | KeyFrame kf1; 432 | 433 | if (isDark()) { 434 | kf0 = new KeyFrame(Duration.ZERO, kvKnobWidthStart, kvZeroOpacityStart, kvOneOpacityStart); 435 | kf1 = new KeyFrame(Duration.millis(125), kvKnobWidthEnd, kvZeroOpacityEnd, kvOneOpacityEnd); 436 | } else { 437 | kf0 = new KeyFrame(Duration.ZERO, kvKnobWidthStart, kvMainScaleXStart, kvMainScaleYStart, kvMainOpacityStart, kvZeroOpacityStart, kvOneOpacityStart); 438 | kf1 = new KeyFrame(Duration.millis(125), kvKnobWidthEnd, kvMainScaleXEnd, kvMainScaleYEnd, kvMainOpacityEnd, kvZeroOpacityEnd, kvOneOpacityEnd); 439 | } 440 | 441 | timeline.getKeyFrames().setAll(kf0, kf1); 442 | timeline.play(); 443 | } 444 | private void animateToPreDeselect() { 445 | KeyValue kvKnobWidthStart = new KeyValue(knob.widthProperty(), height * 0.89130435, Interpolator.EASE_BOTH); 446 | KeyValue kvKnobWidthEnd = new KeyValue(knob.widthProperty(), height * 0.89130435 * 1.2, Interpolator.EASE_BOTH); 447 | KeyValue kvKnobXStart = new KeyValue(knob.xProperty(), mainArea.getLayoutBounds().getMaxX() - height * 0.89130435, Interpolator.EASE_BOTH); 448 | KeyValue kvKnobXEnd = new KeyValue(knob.xProperty(), mainArea.getLayoutBounds().getMaxX() - height * 0.89130435 * 1.2, Interpolator.EASE_BOTH); 449 | KeyValue kvZeroOpacityStart = new KeyValue(zero.opacityProperty(), 0, Interpolator.EASE_BOTH); 450 | KeyValue kvZeroOpacityEnd = new KeyValue(zero.opacityProperty(), 1, Interpolator.EASE_BOTH); 451 | KeyValue kvOneOpacityStart = new KeyValue(one.opacityProperty(), 1, Interpolator.EASE_BOTH); 452 | KeyValue kvOneOpacityEnd = new KeyValue(one.opacityProperty(), 0, Interpolator.EASE_BOTH); 453 | 454 | KeyFrame kf0 = new KeyFrame(Duration.ZERO, kvKnobWidthStart, kvKnobXStart, kvZeroOpacityStart, kvOneOpacityStart); 455 | KeyFrame kf1 = new KeyFrame(Duration.millis(Helper.ANIMATION_DURATION), kvKnobWidthEnd, kvKnobXEnd, kvZeroOpacityEnd, kvOneOpacityEnd); 456 | 457 | timeline.getKeyFrames().setAll(kf0, kf1); 458 | timeline.play(); 459 | } 460 | 461 | private void animateToSelect() { 462 | KeyValue kvMainScaleXStart = new KeyValue(mainArea.scaleXProperty(), mainArea.getScaleX(), Interpolator.EASE_BOTH); 463 | KeyValue kvMainScaleXEnd = new KeyValue(mainArea.scaleXProperty(), 0, Interpolator.EASE_BOTH); 464 | KeyValue kvMainScaleYStart = new KeyValue(mainArea.scaleYProperty(), mainArea.getScaleY(), Interpolator.EASE_BOTH); 465 | KeyValue kvMainScaleYEnd = new KeyValue(mainArea.scaleYProperty(), 0, Interpolator.EASE_BOTH); 466 | KeyValue kvMainOpacityStart = new KeyValue(mainArea.opacityProperty(), mainArea.getOpacity(), Interpolator.EASE_BOTH); 467 | KeyValue kvMainOpacityEnd = new KeyValue(mainArea.opacityProperty(), 0, Interpolator.EASE_BOTH); 468 | KeyValue kvBackgroundFillStart = new KeyValue(backgroundArea.fillProperty(), Color.rgb(229, 229, 229), Interpolator.EASE_BOTH); 469 | KeyValue kvBackgroundFillEnd = new KeyValue(backgroundArea.fillProperty(), getSelectedColor(), Interpolator.EASE_BOTH); 470 | KeyValue kvKnobXStart = new KeyValue(knob.xProperty(), mainArea.getLayoutBounds().getMinX(), Interpolator.EASE_BOTH); 471 | KeyValue kvKnobXEnd = new KeyValue(knob.xProperty(), mainArea.getLayoutBounds().getMaxX() - height * 0.89130435, Interpolator.EASE_BOTH); 472 | KeyValue kvOneOpacityStart = new KeyValue(one.opacityProperty(), 0, Interpolator.EASE_BOTH); 473 | KeyValue kvOneOpacityEnd = new KeyValue(one.opacityProperty(), 1, Interpolator.EASE_BOTH); 474 | KeyValue kvZeroOpacityStart = new KeyValue(zero.opacityProperty(), zero.getOpacity(), Interpolator.EASE_BOTH); 475 | KeyValue kvZeroOpacityEnd = new KeyValue(zero.opacityProperty(), 0, Interpolator.EASE_BOTH); 476 | KeyValue kvKnobWidthStart = new KeyValue(knob.widthProperty(), knob.getWidth(), Interpolator.EASE_BOTH); 477 | KeyValue kvKnobWidthEnd = new KeyValue(knob.widthProperty(), height * 0.89130435, Interpolator.EASE_BOTH); 478 | 479 | KeyFrame kf0 = new KeyFrame(Duration.ZERO, kvMainScaleXStart, kvMainScaleYStart, kvMainOpacityStart, kvBackgroundFillStart, kvKnobXStart, kvOneOpacityStart, kvZeroOpacityStart, kvKnobWidthStart); 480 | KeyFrame kf1 = new KeyFrame(Duration.millis(getDuration() * 0.5), kvZeroOpacityEnd); 481 | KeyFrame kf2 = new KeyFrame(Duration.millis(getDuration()), kvMainScaleXEnd, kvMainScaleYEnd, kvMainOpacityEnd, kvBackgroundFillEnd, kvKnobXEnd, kvOneOpacityEnd, kvKnobWidthEnd); 482 | 483 | timeline.getKeyFrames().setAll(kf0, kf1, kf2); 484 | timeline.play(); 485 | } 486 | private void animateToDeselect() { 487 | KeyValue kvMainScaleXStart = new KeyValue(mainArea.scaleXProperty(), 0, Interpolator.EASE_BOTH); 488 | KeyValue kvMainScaleXEnd = new KeyValue(mainArea.scaleXProperty(), 1, Interpolator.EASE_BOTH); 489 | KeyValue kvMainScaleYStart = new KeyValue(mainArea.scaleYProperty(), 0, Interpolator.EASE_BOTH); 490 | KeyValue kvMainScaleYEnd = new KeyValue(mainArea.scaleYProperty(), 1, Interpolator.EASE_BOTH); 491 | KeyValue kvMainOpacityStart = new KeyValue(mainArea.opacityProperty(), 0, Interpolator.EASE_BOTH); 492 | KeyValue kvMainOpacityEnd = new KeyValue(mainArea.opacityProperty(), 1, Interpolator.EASE_BOTH); 493 | KeyValue kvBackgroundFillStart = new KeyValue(backgroundArea.fillProperty(), getSelectedColor(), Interpolator.EASE_BOTH); 494 | KeyValue kvBackgroundFillEnd = new KeyValue(backgroundArea.fillProperty(), Color.rgb(229, 229, 229), Interpolator.EASE_BOTH); 495 | KeyValue kvKnobXStart = new KeyValue(knob.xProperty(), mainArea.getLayoutBounds().getMaxX() - knob.getWidth(), Interpolator.EASE_BOTH); 496 | KeyValue kvKnobXEnd = new KeyValue(knob.xProperty(), mainArea.getLayoutBounds().getMinX(), Interpolator.EASE_BOTH); 497 | KeyValue kvOneOpacityStart = new KeyValue(one.opacityProperty(), one.getOpacity(), Interpolator.EASE_BOTH); 498 | KeyValue kvOneOpacityEnd = new KeyValue(one.opacityProperty(), 0, Interpolator.EASE_BOTH); 499 | KeyValue kvZeroOpacityStart = new KeyValue(zero.opacityProperty(), 0, Interpolator.EASE_BOTH); 500 | KeyValue kvZeroOpacityEnd = new KeyValue(zero.opacityProperty(), 1, Interpolator.EASE_BOTH); 501 | KeyValue kvKnobWidthStart = new KeyValue(knob.widthProperty(), knob.getWidth(), Interpolator.EASE_BOTH); 502 | KeyValue kvKnobWidthEnd = new KeyValue(knob.widthProperty(), height * 0.89130435, Interpolator.EASE_BOTH); 503 | 504 | KeyFrame kf0 = new KeyFrame(Duration.ZERO, kvMainScaleXStart, kvMainScaleYStart, kvMainOpacityStart, kvBackgroundFillStart, kvKnobXStart, kvOneOpacityStart, kvZeroOpacityStart, kvKnobWidthStart); 505 | KeyFrame kf1 = new KeyFrame(Duration.millis(getDuration() * 0.5), kvOneOpacityEnd); 506 | KeyFrame kf2 = new KeyFrame(Duration.millis(getDuration()), kvMainScaleXEnd, kvMainScaleYEnd, kvMainOpacityEnd, kvBackgroundFillEnd, kvKnobXEnd, kvZeroOpacityEnd, kvKnobWidthEnd); 507 | 508 | timeline.getKeyFrames().setAll(kf0, kf1, kf2); 509 | timeline.play(); 510 | } 511 | 512 | 513 | // ******************** Event Handling ************************************ 514 | public void addOnIosEvent(final IosEventListener LISTENER) { if (!listeners.contains(LISTENER)) { listeners.add(LISTENER); } } 515 | public void removeOnIosEvent(final IosEventListener LISTENER) { if (listeners.contains(LISTENER)) { listeners.remove(LISTENER); } } 516 | 517 | private void fireIosEvent(final IosEvent EVENT) { 518 | listeners.forEach(listener -> listener.onIosEvent(EVENT)); 519 | } 520 | 521 | 522 | // ******************** Resizing ****************************************** 523 | private void resize() { 524 | width = getWidth() - getInsets().getLeft() - getInsets().getRight(); 525 | height = getHeight() - getInsets().getTop() - getInsets().getBottom(); 526 | 527 | if (width > 0 && height > 0) { 528 | if (ASPECT_RATIO * width > height) { 529 | width = 1 / (ASPECT_RATIO / height); 530 | } else if (1 / (ASPECT_RATIO / height) > width) { 531 | height = ASPECT_RATIO * width; 532 | } 533 | 534 | dropShadow.setRadius(height * 0.14); 535 | dropShadow.setOffsetY(height * 0.065); 536 | 537 | backgroundArea.setWidth(width); 538 | backgroundArea.setHeight(height); 539 | backgroundArea.setArcWidth(height); 540 | backgroundArea.setArcHeight(height); 541 | 542 | one.setWidth(height * 0.0326087); 543 | one.setHeight(height * 0.32608696); 544 | one.setX(width * 0.225 - (one.getWidth() * 0.5)); 545 | one.setY((height - one.getHeight()) * 0.5); 546 | 547 | mainArea.setWidth(width * 0.93421053); 548 | mainArea.setHeight(height * 0.89130435); 549 | mainArea.setArcWidth(height * 0.89130435); 550 | mainArea.setArcHeight(height * 0.89130435); 551 | mainArea.setX(height * 0.05434783); 552 | mainArea.setY(height * 0.05434783); 553 | 554 | zero.setRadius(height * 0.1413); 555 | zero.setCenterX(width * 0.765); 556 | zero.setCenterY(height * 0.5); 557 | zero.setStrokeWidth(height * 0.04); 558 | 559 | knob.setWidth(height * 0.89130435); 560 | knob.setHeight(height * 0.89130435); 561 | knob.setArcWidth(height * 0.89130435); 562 | knob.setArcHeight(height * 0.89130435); 563 | if (isSelected()) { 564 | knob.setX(mainArea.getLayoutBounds().getMaxX() - knob.getWidth()); 565 | } else { 566 | knob.setX(mainArea.getLayoutBounds().getMinX()); 567 | } 568 | knob.setY((backgroundArea.getLayoutBounds().getHeight() - knob.getLayoutBounds().getHeight()) * 0.5); 569 | 570 | pane.setMaxSize(width, height); 571 | pane.setPrefSize(width, height); 572 | pane.relocate((getWidth() - width) * 0.5, (getHeight() - height) * 0.5); 573 | } 574 | } 575 | 576 | 577 | // ******************** Style related ************************************* 578 | @Override public String getUserAgentStylesheet() { return IosSwitch.class.getResource("ios-switch.css").toExternalForm(); } 579 | 580 | public static List> getClassCssMetaData() { return FACTORY.getCssMetaData(); } 581 | @Override public List> getCssMetaData() { return FACTORY.getCssMetaData(); } 582 | } 583 | --------------------------------------------------------------------------------