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;
019
020import static javafx.collections.FXCollections.observableMap;
021import static javafx.collections.FXCollections.unmodifiableObservableMap;
022import static org.apiguardian.api.API.Status.INTERNAL;
023import static org.apiguardian.api.API.Status.STABLE;
024import static org.tquadrat.foundation.lang.CommonConstants.EMPTY_STRING;
025import static org.tquadrat.foundation.lang.Objects.nonNull;
026import static org.tquadrat.foundation.lang.Objects.requireNonNullArgument;
027import static org.tquadrat.foundation.util.StringUtils.isNotEmptyOrBlank;
028
029import java.util.HashMap;
030import java.util.LinkedHashMap;
031import java.util.Map;
032import java.util.function.Supplier;
033
034import org.apiguardian.api.API;
035import org.tquadrat.foundation.annotation.ClassVersion;
036import org.tquadrat.foundation.fx.control.skin.ErrorDisplaySkin;
037import org.tquadrat.foundation.fx.internal.FoundationFXControl;
038import javafx.beans.Observable;
039import javafx.beans.binding.BooleanBinding;
040import javafx.beans.property.MapProperty;
041import javafx.beans.property.ReadOnlyBooleanProperty;
042import javafx.beans.property.ReadOnlyMapProperty;
043import javafx.beans.property.SimpleMapProperty;
044import javafx.collections.ObservableMap;
045import javafx.scene.control.Skin;
046
047/**
048 *  <p>{@summary A control to display multiple error messages inside a
049 *  window.}</p>
050 *  <p>The error messages will be added through a call to
051 *  {@link #addMessage(String,String) addMessage()}
052 *  and they can be removed again by calling
053 *  {@link #removeMessage(String) removeMessage()}. The id identifies the
054 *  respective message.</p>
055 *  <p>The messages are displayed in the sequence they were added.</p>
056 *  <p>To automate the display of error messages, you can create a message
057 *  trigger, by calling
058 *  {@link #addMessageTrigger(String,Supplier,BooleanBinding) addMessageTrigger()}.
059 *  The id of the trigger is also the id of the message that is controlled
060 *  through the message trigger.</p>
061 *  <p>The messages itself are displayed through instances of
062 *  {@link javafx.scene.control.Label Label}
063 *  that has the CSS Style Class
064 *  {@value #STYLE_CLASS_MessageDisplayLabel}.</p>
065 *
066 *  @note The minimum height for an {@code ErrorDisplay} control is 55.0. The
067 *      vertical scrollbar does not work properly for smaller values.
068 *
069 *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
070 *  @version $Id: ErrorDisplay.java 1112 2024-03-10 14:16:51Z tquadrat $
071 *  @since 0.4.3
072 *
073 *  @UMLGraph.link
074 */
075@ClassVersion( sourceVersion = "$Id: ErrorDisplay.java 1112 2024-03-10 14:16:51Z tquadrat $" )
076@API( status = STABLE, since = "0.4.3" )
077public final class ErrorDisplay extends FoundationFXControl
078{
079        /*---------------*\
080    ====** Inner Classes **====================================================
081        \*---------------*/
082    /**
083     *  <p>{@summary Wraps a
084     *  {@link BooleanBinding}
085     *  with a message.}</p>
086     *  <p>Each time the binding invalidates, it will be verified again and the
087     *  error message will be added or removed from the error display.</p>
088     *
089     *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
090     *  @version $Id: ErrorDisplay.java 1112 2024-03-10 14:16:51Z tquadrat $
091     *  @since 0.4.3
092     *
093     *  @UMLGraph.link
094     */
095    @ClassVersion( sourceVersion = "$Id: ErrorDisplay.java 1112 2024-03-10 14:16:51Z tquadrat $" )
096    @API( status = INTERNAL, since = "0.4.3" )
097    private final class MessageTrigger
098    {
099            /*------------*\
100        ====** Attributes **===================================================
101            \*------------*/
102        /**
103         *  The binding.
104         */
105        private final BooleanBinding m_Binding;
106
107        /**
108         *  The message id.
109         */
110        private final String m_Id;
111
112        /**
113         *  The supplier for the message text.
114         */
115        private final Supplier<String> m_MessageSupplier;
116
117            /*--------------*\
118        ====** Constructors **=================================================
119            \*--------------*/
120        /**
121         *  <p>{@summary Creates a new instance of {@code MessageTrigger}.}</p>
122         *  <p>The message will be displayed when the given instance of
123         *  {@link BooleanBinding}
124         *  evaluates to {@code true}.</p>
125         *
126         *  @param  id  The message id.
127         *  @param  messageSupplier The supplier for the message text.
128         *  @param  binding The binding that controls the appearance of the
129         *      message.
130         */
131        public MessageTrigger( final String id, final Supplier<String> messageSupplier, final BooleanBinding binding )
132        {
133            m_Id = requireNonNullArgument( id, "id" );
134            m_MessageSupplier = requireNonNullArgument( messageSupplier, "messageSupplier" );
135            m_Binding = requireNonNullArgument( binding, "binding" );
136
137            m_Binding.addListener( this::triggerMessage );
138        }   //  MessageTrigger()
139
140            /*---------*\
141        ====** Methods **======================================================
142            \*---------*/
143        /**
144         *  Disables this message trigger in preparation of its disposal.
145         */
146        public final void disable()
147        {
148            m_Binding.removeListener( this::triggerMessage );
149        }   //  disable()
150
151        /**
152         *  The invalidation listener that updates the error display.
153         *
154         *  @param  observable  The observable that became invalid.
155         */
156        private final void triggerMessage( final Observable observable )
157        {
158            assert observable == m_Binding;
159
160            if( m_Binding.get() )
161            {
162                addMessage( m_Id, m_MessageSupplier.get() );
163            }
164            else
165            {
166                removeMessage( m_Id );
167            }
168        }   //  triggerMesssage()
169    }
170    //  class MessageTrigger
171
172        /*-----------*\
173    ====** Constants **========================================================
174        \*-----------*/
175    /**
176     *  The style class for the
177     *  {@link javafx.scene.control.Label}
178     *  instances that show the messages: {@value}.
179     */
180    public static final String STYLE_CLASS_MessageDisplayLabel = "errorDisplay";
181
182        /*------------*\
183    ====** Attributes **=======================================================
184        \*------------*/
185    /**
186     *  <p>{@summary The error messages to display.}</p>
187     *  <p>The key is the message id, the value is the message text; only the
188     *  text is shown.</p>
189     */
190    private final ObservableMap<String,String> m_Messages;
191
192    /**
193     *  The property for the messages.
194     */
195    private final MapProperty<String,String> m_MessagesProperty;
196
197    /**
198     *  The message triggers.
199     */
200    private final Map<String,MessageTrigger> m_MessageTriggers = new HashMap<>();
201
202        /*--------------*\
203    ====** Constructors **=====================================================
204        \*--------------*/
205    /**
206     *  Creates a new instance of {@code ErrorDisplay}.
207     */
208    public ErrorDisplay()
209    {
210        super();
211
212        //---* Initialise the attributes *-------------------------------------
213        final Map<String,String> messages = new LinkedHashMap<>();
214        m_Messages = observableMap( messages );
215        //noinspection ThisEscapedInObjectConstruction
216        m_MessagesProperty = new SimpleMapProperty<>( this, "messages", m_Messages );
217
218        setSkin( createDefaultSkin() );
219    }   //  ErrorDisplay()
220
221        /*---------*\
222    ====** Methods **==========================================================
223        \*---------*/
224    /**
225     *  Adds a message to display.
226     *
227     *  @param  id  The id for the message; this allows to remove the message
228     *      again later, when the error condition has been removed.
229     *  @param  message This is the text to display.
230     */
231    public final void addMessage( final String id, final String message )
232    {
233        if( nonNull( id ) )
234        {
235            if( isNotEmptyOrBlank( message ) )
236            {
237                m_Messages.put( id, message );
238            }
239            else
240            {
241                m_Messages.remove( id );
242            }
243        }
244    }   //  addMessage()
245
246    /**
247     *  Adds a message to display, using the empty string as the id for the
248     *  message.
249     *
250     *  @param  message This is the text to display.
251     */
252    public final void addMessage( final String message )
253    {
254        addMessage( EMPTY_STRING, message );
255    }   //  addMessage()
256
257    /**
258     *  <p>{@summary Adds a message trigger.}</p>
259     *  <p>The message provided by the given supplier will be displayed when
260     *  the given instance of
261     *  {@link BooleanBinding}
262     *  evaluates to {@code true}.</p>
263     *  <p>Use this to control the visibility of a particular message based on
264     *  the status of the given binding.</p>
265     *
266     *  @param  id  The message id.
267     *  @param  messageSupplier The supplier for the message text.
268     *  @param  binding The binding that controls the appearance of the
269     *      message.
270     *
271     *  @see BooleanBinding#get()
272     */
273    public final void addMessageTrigger( final String id, final Supplier<String> messageSupplier, final BooleanBinding binding )
274    {
275        var messageTrigger = m_MessageTriggers.get( requireNonNullArgument( id, "id" ) );
276        if( nonNull( messageTrigger ) ) messageTrigger.disable();
277        messageTrigger = new MessageTrigger( id, messageSupplier, binding );
278        m_MessageTriggers.put( id, messageTrigger );
279    }   //  addMessageTrigger()
280
281    /**
282     *  <p>{@summary Adds a message trigger, using the empty string as id.}</p>
283     *  <p>Use this to control the visibility of a particular message based on
284     *  the status of the given binding.</p>
285     *
286     *  @param  messageSupplier The supplier for the message text.
287     *  @param  binding The binding that controls the appearance of the
288     *      message.
289     */
290    public final void addMessageTrigger( final Supplier<String> messageSupplier, final BooleanBinding binding )
291    {
292        addMessageTrigger( EMPTY_STRING, messageSupplier, binding );
293    }   //  addMessageTrigger()
294
295    /**
296     *  {@inheritDoc}
297     */
298    @Override
299    protected final Skin<?> createDefaultSkin()
300    {
301        final var retValue = new ErrorDisplaySkin( this );
302
303        //---* Done *----------------------------------------------------------
304        return retValue;
305    }   //  createDefaultSkin()
306
307    /**
308     *  Returns the reference to a boolean property that is {@code true} if
309     *  currently no messages are displayed.
310     *
311     *  @return The property reference.
312     */
313    public final ReadOnlyBooleanProperty emptyProperty() { return m_MessagesProperty.emptyProperty(); }
314
315    /**
316     *  Returns the messages.
317     *
318     *  @return The messages.
319     */
320    public final Map<String,String> getMessages() { return unmodifiableObservableMap( m_Messages ); }
321
322    /**
323     *  Checks whether there are any messages to display.
324     *
325     *  @return {@code true} if there are no messages to show, {@code false}
326     *      otherwise.
327     */
328    public final boolean isEmpty() { return m_Messages.isEmpty(); }
329
330    /**
331     *  Provides a reference to the messages property.
332     *
333     *  @return The property reference.
334     */
335    @SuppressWarnings( "AssignmentOrReturnOfFieldWithMutableType" )
336    public final ReadOnlyMapProperty<String,String> messagesProperty() { return m_MessagesProperty; }
337
338    /**
339     *  <p>{@summary Removes the message with the given id.}</p>
340     *  <p>If there is no message with that id, nothing happens.</p>
341     *
342     *  @param  id  The id for the message to remove.
343     */
344    public final void removeMessage( final String id )
345    {
346        if( nonNull( id ) )
347        {
348            m_Messages.remove( id );
349        }
350    }   //  removeMessage()
351
352    /**
353     *  <p>{@summary Removes the message with the empty string as its id.}</p>
354     *  <p>If there is no message with that id, nothing happens.</p>
355     */
356    public final void removeMessage() { removeMessage( EMPTY_STRING ); }
357
358    /**
359     *  <p>{@summary Removes the message trigger with the given id.}</p>
360     *  <p>If there is no message trigger with that id, nothing happens.</p>
361     *
362     *  @param  id  The id for the message trigger to remove.
363     */
364    public final void removeMessageTrigger( final String id )
365    {
366        if( nonNull( id ) )
367        {
368            final var messageTrigger = m_MessageTriggers.remove( id );
369            if( nonNull( messageTrigger ) ) messageTrigger.disable();
370        }
371    }   //  removeMessageTrigger()
372
373    /**
374     *  <p>{@summary Removes the message trigger with the empty string as its
375     *  id.}</p>
376     *  <p>If there is no message trigger with that id, nothing happens.</p>
377     */
378    public final void removeMessageTrigger() { removeMessageTrigger( EMPTY_STRING );}
379}
380//  class ErrorDisplay
381
382/*
383 *  End of File
384 */