1 /*
2 * *************************************************************************************************************************************************************
3 *
4 * SteelBlue: DCI User Interfaces
5 * http://tidalwave.it/projects/steelblue
6 *
7 * Copyright (C) 2015 - 2025 by Tidalwave s.a.s. (http://tidalwave.it)
8 *
9 * *************************************************************************************************************************************************************
10 *
11 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
12 * You may obtain a copy of the License at
13 *
14 * http://www.apache.org/licenses/LICENSE-2.0
15 *
16 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
17 * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
18 *
19 * *************************************************************************************************************************************************************
20 *
21 * git clone https://bitbucket.org/tidalwave/steelblue-src
22 * git clone https://github.com/tidalwave-it/steelblue-src
23 *
24 * *************************************************************************************************************************************************************
25 */
26 package it.tidalwave.ui.javafx.impl.dialog;
27
28 import jakarta.annotation.Nonnull;
29 import java.util.Optional;
30 import java.util.concurrent.Executor;
31 import java.util.function.Supplier;
32 import java.io.IOException;
33 import java.net.URI;
34 import java.net.URISyntaxException;
35 import java.awt.Desktop;
36 import javafx.beans.value.ChangeListener;
37 import javafx.beans.value.ObservableValue;
38 import javafx.scene.Node;
39 import javafx.scene.control.Alert;
40 import javafx.scene.control.ButtonType;
41 import javafx.scene.control.Dialog;
42 import javafx.scene.control.Label;
43 import javafx.scene.paint.Color;
44 import javafx.scene.web.WebView;
45 import javafx.stage.Window;
46 import javafx.application.Platform;
47 import it.tidalwave.ui.core.UserNotificationWithFeedback;
48 import it.tidalwave.ui.javafx.impl.common.DelegateSupport;
49 import it.tidalwave.util.BundleUtilities;
50 import it.tidalwave.util.Callback;
51 import lombok.RequiredArgsConstructor;
52 import lombok.SneakyThrows;
53 import lombok.extern.slf4j.Slf4j;
54
55 /***************************************************************************************************************************************************************
56 *
57 * @author Fabrizio Giudici
58 *
59 **************************************************************************************************************************************************************/
60 @Slf4j
61 public class DialogBindings extends DelegateSupport
62 {
63 @RequiredArgsConstructor
64 private static class UrlOpener implements ChangeListener<String>
65 {
66 @Nonnull
67 private final WebView webView;
68
69 @Nonnull
70 private final String text;
71
72 @Override
73 public void changed (final ObservableValue<? extends String> observableValue, final String oldValue, final String newValue)
74 {
75 if (newValue != null && !newValue.isEmpty())
76 {
77 Platform.runLater(() ->
78 {
79 try
80 {
81 log.debug("Opening {} ...", newValue);
82 Desktop.getDesktop().browse(new URI(newValue));
83 }
84 catch (IOException | URISyntaxException e)
85 {
86 log.warn("", e);
87 }
88 });
89
90 webView.getEngine().loadContent(text);
91 }
92 }
93 }
94
95 /***********************************************************************************************************************************************************
96 *
97 **********************************************************************************************************************************************************/
98 public DialogBindings (@Nonnull final Executor executor, @Nonnull final Supplier<Window> mainWindow)
99 {
100 super(executor, mainWindow);
101 }
102
103 /***********************************************************************************************************************************************************
104 * {@inheritDoc}
105 **********************************************************************************************************************************************************/
106 public void showInModalDialog (@Nonnull final UserNotificationWithFeedback notification,
107 @Nonnull final Optional<Node> node)
108 {
109 // FIXME: should not be needed
110 Platform.runLater(() ->
111 {
112 log.debug("modalDialog({}, {})", node, notification);
113
114 // final Dialog<ButtonType> dialog = new Dialog<>();
115 final Dialog<ButtonType> dialog = new Alert(Alert.AlertType.NONE);
116 dialog.initOwner(mainWindow.get());
117 dialog.setTitle(notification.getCaption());
118 dialog.setResizable(false);
119 final var dialogPane = dialog.getDialogPane();
120 final var text = notification.getText();
121
122 if (!text.startsWith("<html>"))
123 {
124 dialog.setContentText(text);
125 node.ifPresent(dialogPane::setContent);
126 }
127 else
128 {
129 final var webView = new WebView();
130 webView.setPrefSize(600, 300); // FIXME: proportional to screen
131 webView.setContextMenuEnabled(false);
132 final var label = new Label();
133 dialogPane.setContent(label);
134 final var fontFamily = label.getFont().getFamily();
135 // System.err.println("FILL " + label.getTextFill());
136 final var textColor = "white"; // FIXME
137 var backgroundColor = "#252525"; // FIXME
138
139 try
140 {
141 // Available in JDK 18+
142 final var setPageFill = webView.getClass().getDeclaredMethod("setPageFill", Color.class);
143 setPageFill.invoke(webView, Color.TRANSPARENT);
144 backgroundColor = "transparent";
145 }
146 catch (Exception e)
147 {
148 log.trace("WebView.setPageFill() not available", e);
149 }
150
151 // FIXME
152 final var text2 = BundleUtilities.getMessage(getClass(),"htmlTemplate", text, textColor, backgroundColor, fontFamily);
153 // System.err.println(text2);
154 webView.getEngine().loadContent(text2);
155 webView.getEngine().locationProperty().addListener(new UrlOpener(webView, text2));
156 dialogPane.setContent(webView);
157 }
158
159 final var feedback = notification.getFeedback();
160 final var hasOnCancel = feedback.canCancel();
161
162 final var buttonTypes = dialogPane.getButtonTypes();
163 buttonTypes.clear();
164 buttonTypes.add(ButtonType.OK);
165
166 if (hasOnCancel)
167 {
168 buttonTypes.add(ButtonType.CANCEL);
169 }
170
171 // okButton.disableProperty().bind(new PropertyAdapter<>(valid)); // FIXME: doesn't work
172
173 final var result = dialog.showAndWait();
174
175 if (result.isEmpty())
176 {
177 if (hasOnCancel)
178 {
179 wrap(notification::cancel);
180 }
181 else
182 {
183 wrap(notification::confirm);
184 }
185 }
186 else
187 {
188 if (result.get() == ButtonType.OK)
189 {
190 wrap(notification::confirm);
191 }
192 else if (result.get() == ButtonType.CANCEL)
193 {
194 wrap(notification::cancel);
195 }
196 else
197 {
198 throw new IllegalStateException("Unexpected button pressed: " + result.get());
199 }
200 }
201 });
202 }
203
204 /***********************************************************************************************************************************************************
205 *
206 **********************************************************************************************************************************************************/
207 @SneakyThrows(Throwable.class)
208 private static void wrap (@Nonnull final Callback callback)
209 {
210 callback.call();
211 }
212 }