View Javadoc
1   /*
2    * Copyright 2013-2023 Medical Information Systems Research Group (https://medical.zcu.cz),
3    * Department of Computer Science and Engineering, University of West Bohemia.
4    * Address: Univerzitni 8, 306 14 Plzen, Czech Republic.
5    *
6    * This file is part of Sparkle project.
7    *
8    * Sparkle is free software: you can redistribute it and/or modify
9    * it under the terms of the GNU General Public License as published by
10   * the Free Software Foundation, either version 3 of the License.
11   *
12   * Sparkle is distributed in the hope that it will be useful,
13   * but WITHOUT ANY WARRANTY; without even the implied warranty of
14   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15   * GNU General Public License for more details.
16   *
17   * You should have received a copy of the GNU General Public License
18   * along with Sparkle. If not, see <http://www.gnu.org/licenses/>.
19   */
20  package cz.zcu.mre.sparkle.gui.query.helpers;
21  
22  import cz.zcu.mre.sparkle.gui.tools.ReferenceKeeper;
23  import javafx.application.Platform;
24  import javafx.beans.InvalidationListener;
25  import javafx.beans.value.ChangeListener;
26  import javafx.geometry.Bounds;
27  import javafx.geometry.Dimension2D;
28  import javafx.geometry.Point2D;
29  import javafx.scene.Node;
30  import javafx.scene.layout.Region;
31  import javafx.stage.Popup;
32  import javafx.stage.WindowEvent;
33  import java.util.Collection;
34  
35  /**
36   * Implementace plovoucího okna vztahujícího svou pozici ke stanovenému
37   * {@link Region}u.
38   *
39   * @author Jan Smucr
40   * @author Klara Hlavacova
41   * @author Petr Vcelak (vcelak@kiv.zcu.cz)
42   */
43  public final class RegionPopup
44          extends Popup {
45  
46      /**
47       * Stanovuje pozici okna vzhledem k nějakému objektu.
48       *
49       * @author Jan Smucr
50       */
51      public enum PopupPlacement {
52          LEFT, RIGHT, TOP, BOTTOM
53      }
54  
55      /**
56       * Určuje způsob zarovnání okna vzhledem k nějakému objektu.
57       *
58       * @author Jan Smucr
59       */
60      public enum PopupAlignment {
61          LEFT_OR_TOP, CENTER, RIGHT_OR_BOTTOM
62      }
63  
64      private Region owner;
65      private final PopupPlacement placement;
66      private final PopupAlignment alignment;
67      private double offsetX, offsetY;
68      private final ChangeListener<Number> windowXMoveListener, windowYMoveListener;
69      private boolean visible = false;
70      private final ReferenceKeeper keeper = new ReferenceKeeper();
71  
72      public RegionPopup(final Region owner, final PopupPlacement placement) {
73          this(owner, placement, PopupAlignment.LEFT_OR_TOP);
74      }
75  
76      public RegionPopup(final Region owner, final PopupPlacement placement, final PopupAlignment alignment) {
77          this.owner = owner;
78          this.placement = placement;
79          this.alignment = alignment;
80          this.setOffsetX(0.0);
81          this.setOffsetY(0.0);
82  
83          final InvalidationListener sizeChangeListener = observable
84                  -> {
85              if (isVisible()) {
86                  updatePosition();
87              }
88          };
89  
90          widthProperty().addListener(keeper.toWeak(sizeChangeListener));
91          heightProperty().addListener(keeper.toWeak(sizeChangeListener));
92          owner.widthProperty().addListener(keeper.toWeak(sizeChangeListener));
93          owner.heightProperty().addListener(keeper.toWeak(sizeChangeListener));
94  
95          windowXMoveListener = (observable, oldValue, newValue) -> setAnchorX(
96                  getAnchorX() + (newValue.doubleValue() - oldValue.doubleValue()));
97  
98          windowYMoveListener = (observable, oldValue, newValue) -> setAnchorY(
99                  getAnchorY() + (newValue.doubleValue() - oldValue.doubleValue()));
100 
101         setOnShowing(keeper.toWeak((final WindowEvent event)
102                 -> {
103             getOwnerWindow().widthProperty().addListener(sizeChangeListener);
104             getOwnerWindow().heightProperty().addListener(sizeChangeListener);
105             getOwnerWindow().xProperty().addListener(windowXMoveListener);
106             owner.layoutXProperty().addListener(windowXMoveListener);
107             getOwnerWindow().yProperty().addListener(windowYMoveListener);
108             owner.layoutYProperty().addListener(windowYMoveListener);
109         }));
110 
111         setOnHiding(keeper.toWeak((final WindowEvent event)
112                 -> {
113             getOwnerWindow().widthProperty().removeListener(sizeChangeListener);
114             getOwnerWindow().heightProperty().removeListener(sizeChangeListener);
115             getOwnerWindow().xProperty().removeListener(windowXMoveListener);
116             owner.layoutXProperty().removeListener(windowXMoveListener);
117             getOwnerWindow().yProperty().removeListener(windowYMoveListener);
118             owner.layoutYProperty().removeListener(windowYMoveListener);
119         }));
120 
121         setOnShown(keeper.toWeak((final WindowEvent event) -> visible = true));
122         setOnHidden(keeper.toWeak((final WindowEvent event) -> visible = false));
123     }
124 
125     public final Node getOwner() {
126         return owner;
127     }
128 
129     public final void setOwner(final Region owner) {
130         this.owner = owner;
131     }
132 
133     public final PopupPlacement getPlacement() {
134         return placement;
135     }
136 
137     public final PopupAlignment getAlignment() {
138         return alignment;
139     }
140 
141     private Point2D computePosition() {
142         double xOffset = 0.0;
143         double yOffset = 0.0;
144 
145         switch (placement) {
146             case LEFT:
147             case RIGHT:
148                 switch (alignment) {
149                     case CENTER:
150                         yOffset = (owner.getHeight() - getContentSize().getHeight()) / 2.0;
151                         break;
152                     case RIGHT_OR_BOTTOM:
153                         yOffset = owner.getHeight() - getContentSize().getHeight();
154                         break;
155                     case LEFT_OR_TOP:
156                     default:
157                         break;
158                 }
159                 break;
160             case BOTTOM:
161             case TOP:
162                 switch (alignment) {
163                     case CENTER:
164                         xOffset = (owner.getWidth() - getContentSize().getWidth()) / 2.0;
165                         break;
166                     case RIGHT_OR_BOTTOM:
167                         xOffset = owner.getWidth() - getContentSize().getWidth();
168                         break;
169                     case LEFT_OR_TOP:
170                     default:
171                         break;
172                 }
173             default:
174                 break;
175         }
176 
177         switch (placement) {
178             case BOTTOM:
179                 xOffset += offsetX;
180                 yOffset += owner.getHeight() + offsetY;
181                 break;
182             case LEFT:
183                 xOffset += -getWidth() + offsetX;
184                 yOffset += offsetY;
185                 break;
186             case RIGHT:
187                 xOffset += owner.getWidth() + offsetX;
188                 yOffset += offsetY;
189                 break;
190             case TOP:
191                 xOffset += offsetX;
192                 yOffset += -getHeight() + offsetY;
193                 break;
194         }
195 
196         final Point2D p = owner.localToScreen(0.0, 0.0);
197 
198         return p.add(xOffset, yOffset);
199     }
200 
201     private void updatePosition() {
202         updatePosition(computePosition());
203     }
204 
205     private void updatePosition(final Point2D p) {
206         setAnchorX(p.getX());
207         setAnchorY(p.getY());
208     }
209 
210     public final void showPositioned() {
211         final Point2D p = computePosition();
212         show(owner, p.getX(), p.getY());
213 
214         /* JavaFX RT-36089 workaround */
215         Platform.runLater(new Runnable() {
216             private int repeatsLeft = 20;
217 
218             @Override
219             public final void run() {
220                 Point2D p;
221 
222                 p = computePosition();
223                 updatePosition(p);
224 
225                 if ((p.getX() < 0.0) && (p.getY() < 0.0) && isVisible() && (repeatsLeft > 0)) {
226                     repeatsLeft--;
227                     Platform.runLater(this); // Zkusíme znovu později
228                 }
229             }
230         });
231 
232         // updatePosition();
233     }
234 
235     public final boolean isVisible() {
236         return visible;
237     }
238 
239     public final double getOffsetX() {
240         return offsetX;
241     }
242 
243     public final void setOffsetX(final double offsetX) {
244         this.offsetX = offsetX;
245         if (isVisible()) {
246             updatePosition();
247         }
248     }
249 
250     public final double getOffsetY() {
251         return offsetY;
252     }
253 
254     private void setOffsetY(final double offsetY) {
255         this.offsetY = offsetY;
256         if (isVisible()) {
257             updatePosition();
258         }
259     }
260 
261     /**
262      * Pokusí se spočítat velikost vlastního obsahu.
263      *
264      * @return Velikost obsahu.
265      */
266     private Dimension2D getContentSize() {
267         final Collection<Node> content = getContent();
268         if (content.isEmpty()) {
269             return new Dimension2D(0.0, 0.0);
270         }
271 
272         double minX = Double.MAX_VALUE;
273         double minY = Double.MAX_VALUE;
274         double maxX = Double.MIN_VALUE;
275         double maxY = Double.MIN_VALUE;
276 
277         for (final Node node : getContent()) {
278             final Bounds b = node.getBoundsInLocal();
279             minX = Math.min(minX, b.getMinX());
280             minY = Math.min(minY, b.getMinY());
281             maxX = Math.max(maxX, b.getMaxX());
282             maxY = Math.max(maxY, b.getMaxY());
283         }
284 
285         return new Dimension2D(maxX - minX, maxY - minY);
286     }
287 }