001/* 002 * ============================================================================ 003 * Copyright © 2002-2024 by Thomas Thrien. 004 * All Rights Reserved. 005 * ============================================================================ 006 * Licensed to the public under the agreements of the GNU Lesser General Public 007 * License, version 3.0 (the "License"). You may obtain a copy of the License at 008 * 009 * http://www.gnu.org/licenses/lgpl.html 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 013 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 014 * License for the specific language governing permissions and limitations 015 * under the License. 016 */ 017 018package org.tquadrat.foundation.fx.control.skin; 019 020import static java.lang.Double.max; 021import static javafx.scene.layout.Region.USE_COMPUTED_SIZE; 022import static org.apiguardian.api.API.Status.STABLE; 023import static org.tquadrat.foundation.lang.Objects.nonNull; 024import static org.tquadrat.foundation.lang.Objects.requireNonNullArgument; 025 026import java.util.Collection; 027 028import org.apiguardian.api.API; 029import org.tquadrat.foundation.annotation.ClassVersion; 030import org.tquadrat.foundation.fx.control.ErrorDisplay; 031import javafx.beans.Observable; 032import javafx.collections.ObservableMap; 033import javafx.scene.Node; 034import javafx.scene.control.Control; 035import javafx.scene.control.Label; 036import javafx.scene.control.ScrollPane; 037import javafx.scene.control.SkinBase; 038import javafx.scene.control.skin.ScrollPaneSkin; 039import javafx.scene.image.Image; 040import javafx.scene.image.ImageView; 041import javafx.scene.layout.VBox; 042 043/** 044 * {@summary The skin for the 045 * {@link ErrorDisplay } 046 * control.} 047 * <p>The icon that is displayed with an error message was taken from 048 * {@href https://www.iconfinder.com/icons/216514/warning_icon}.</p> 049 * 050 * 051 * @extauthor Thomas Thrien - thomas.thrien@tquadrat.org 052 * @version $Id: ErrorDisplaySkin.java 1114 2024-03-12 23:07:59Z tquadrat $ 053 * @since 0.4.3 054 * 055 * @UMLGraph.link 056 */ 057@SuppressWarnings( "JavadocLinkAsPlainText" ) 058@ClassVersion( sourceVersion = "$Id: ErrorDisplaySkin.java 1114 2024-03-12 23:07:59Z tquadrat $" ) 059@API( status = STABLE, since = "0.4.3" ) 060public final class ErrorDisplaySkin extends SkinBase<ErrorDisplay> 061{ 062 /*-----------*\ 063 ====** Constants **======================================================== 064 \*-----------*/ 065 /** 066 * The spacing for the message entries: {@value}. 067 */ 068 public static final double MESSAGE_ENTRY_SPACING = 7.0; 069 070 /** 071 * The minimum height for an 072 * {@link ErrorDisplay} 073 * control: {@value}. 074 */ 075 public static final double MIN_HEIGHT = 55.0; 076 077 /*------------*\ 078 ====** Attributes **======================================================= 079 \*------------*/ 080 /** 081 * The node that is used to display the messages. 082 */ 083 private final VBox m_Content; 084 085 /** 086 * The icon for the message. 087 */ 088 private final Image m_Icon; 089 090 /*--------------*\ 091 ====** Constructors **===================================================== 092 \*--------------*/ 093 /** 094 * Creates a new {@code ErrorDisplaySkin} instance, installing the 095 * necessary child nodes into the 096 * {@link Control}'s 097 * children list. 098 * 099 * @param control The control that this skin should be installed onto. 100 */ 101 public ErrorDisplaySkin( final ErrorDisplay control ) 102 { 103 super( requireNonNullArgument( control, "control" ) ); 104 105 //---* Get the icon for the messages *--------------------------------- 106 final var inputStream = control.getClass().getResourceAsStream( "ErrorDisplay.png" ); 107 m_Icon = nonNull( inputStream ) ? new Image( inputStream ) : null; 108 109 //---* Create the children and add them *------------------------------ 110 m_Content = new VBox(); 111 m_Content.setSpacing( MESSAGE_ENTRY_SPACING ); 112 113 final var scrollPane = new ScrollPane( m_Content ); 114 scrollPane.setSkin( new ScrollPaneSkin( scrollPane ) ); 115 116 getChildren().add( scrollPane ); 117 118 //---* Set the listener that repaint the scroll pane on any change *--- 119 control.messagesProperty().addListener( this::messagesInvalidated ); 120 } // ErrorDisplaySkin() 121 122 /*---------*\ 123 ====** Methods **========================================================== 124 \*---------*/ 125 /** 126 * Calculates the preferred entry width based on the size of the control. 127 * 128 * @return The preferred width for a new entry. 129 */ 130 private double calcNewEntryWidth() 131 { 132 final var control = getControl(); 133 134 var value = control.getLayoutBounds().getWidth(); 135 if( value <= 0.0 ) value = getControl().getPrefWidth(); 136 if( value <= 0.0 ) value = getControl().getMinWidth(); 137 final var retValue = value < 0.0 ? USE_COMPUTED_SIZE : value - 20.0; 138 139 //---* Done *---------------------------------------------------------- 140 return retValue; 141 } // calcNewEntryWidth() 142 143 /** 144 * {@inheritDoc} 145 */ 146 @Override 147 protected final double computeMinHeight( final double width, final double topInset, final double rightInset, final double bottomInset, final double leftInset ) 148 { 149 final var retValue = max( MIN_HEIGHT, super.computeMinHeight( width, topInset, rightInset, bottomInset, leftInset ) ); 150 151 //---* Done *---------------------------------------------------------- 152 return retValue; 153 } // computeMinHeight() 154 155 /** 156 * Creates an entry. 157 * 158 * @param text The message text. 159 * @return The entry node. 160 */ 161 private final Node createEntry( final String text ) 162 { 163 final var retValue = new Label( requireNonNullArgument( text, "text" ) ); 164 retValue.getStyleClass().clear(); 165 retValue.getStyleClass().add( ErrorDisplay.STYLE_CLASS_MessageDisplayLabel ); 166 retValue.setWrapText( true ); 167 retValue.setPrefWidth( calcNewEntryWidth() ); 168 169 if( nonNull( m_Icon ) ) 170 { 171 //---* Add the icon *---------------------------------------------- 172 final var imageView = new ImageView( m_Icon ); 173 imageView.setFitHeight( 20.0 ); 174 imageView.setPreserveRatio( true ); 175 retValue.setGraphic( imageView ); 176 } 177 178 //---* Done *---------------------------------------------------------- 179 return retValue; 180 } // createEntry() 181 182 /** 183 * Returns a reference to the control. 184 * 185 * @return The control. 186 */ 187 private final ErrorDisplay getControl() { return (ErrorDisplay) getNode(); } 188 189 /** 190 * The invalidation listener for the messages property. 191 * 192 * @param source The source. 193 */ 194 @SuppressWarnings( {"rawtypes", "unchecked"} ) 195 private final void messagesInvalidated( final Observable source ) 196 { 197 if( source instanceof final ObservableMap map ) 198 { 199 m_Content.getChildren().clear(); 200 final Collection<Node> entries = ((ObservableMap<String,String>) map).values() 201 .stream() 202 .map( this::createEntry ) 203 .toList(); 204 m_Content.getChildren().addAll( entries ); 205 } 206 } // messagesInvalidated() 207} 208// class ErrorDisplaySkin 209 210/* 211 * End of File 212 */