001package gwt.material.design.addins.client.rating;
002
003/*
004 * #%L
005 * GwtMaterial
006 * %%
007 * Copyright (C) 2015 - 2017 GwtMaterialDesign
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 * 
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 * 
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import com.google.gwt.event.dom.client.MouseOutHandler;
024import com.google.gwt.event.logical.shared.ValueChangeEvent;
025import com.google.gwt.event.logical.shared.ValueChangeHandler;
026import com.google.gwt.event.shared.HandlerRegistration;
027import com.google.gwt.user.client.DOM;
028import com.google.gwt.user.client.ui.HasValue;
029import gwt.material.design.addins.client.base.constants.AddinsCssName;
030import gwt.material.design.client.base.MaterialWidget;
031import gwt.material.design.client.constants.Color;
032import gwt.material.design.client.constants.IconType;
033import gwt.material.design.client.ui.MaterialIcon;
034
035import java.util.LinkedList;
036import java.util.List;
037
038/**
039 * <p>
040 * MaterialRating is the component used by the 5-star rating system, for
041 * example, allowing users to easily express their opinion about a product,
042 * review, video and so on.
043 * </p>
044 * <p>
045 * By default, it uses the {@link IconType#STAR} to represent the selected
046 * rating, but other icons can be set using the
047 * {@link #setSelectedRatingIcon(IconType)} method.
048 * </p>
049 * <p>
050 * <h3>XML Namespace Declaration</h3>
051 * <p>
052 * <pre>
053 * {@code
054 * xmlns:ma='urn:import:gwt.material.design.addins.client'
055 * }
056 * </pre>
057 * <p>
058 * <h3>UiBinder Usage:</h3>
059 * <p>
060 * <pre>
061 * {@code
062 * <ma:rating.MaterialRating ui:field="rating" />
063 * }
064 * </pre>
065 * <p>
066 * To use different icons, for instance, hearts, you can set:
067 * <p>
068 * <pre>
069 * {@code
070 * <ma:rating.MaterialRating ui:field="rating" selectedRatingIcon="FAVORITE" unselectedRatingIcon="FAVORITE_BORDER" textColor="red" />
071 * }
072 * </pre>
073 * <p>
074 * You can also set the maximum rating (the default is 5):
075 * <p>
076 * <pre>
077 * {@code
078 * <ma:rating.MaterialRating ui:field="rating" maxRating="7" />
079 * }
080 * </pre>
081 * <p>
082 * <h3>Example Java Usage:</h3>
083 * <p>
084 * <pre>
085 * {@code
086 * MaterialRating rating = ... //create using new or using UiBinder
087 * rating.addValueChangeHandler(...); // MaterialRating implements HasValue<Integer>
088 * rating.setEditable(false); // disables user interaction
089 * rating.setValue(2); // directly sets the desired rating
090 * int selectedValue = rating.getValue(); // retrieves the selected rating
091 * }
092 * </pre>
093 * <p>
094 * <h3>Custom styling:</h3>
095 * <p>
096 * You use change the MaterialRating style by using the
097 * <code>material-rating</code> CSS class. Selected rating icons have the
098 * <code>material-rating-selected</code> CSS class, and unselected the
099 * <code>material-rating-unselected</code> CSS class.
100 * </p>
101 *
102 * @author gilberto-torrezan
103 */
104public class MaterialRating extends MaterialWidget implements HasValue<Integer> {
105
106    private boolean editable = true;
107    private int currentRating = 0;
108    private int maxRating = 5;
109    private IconType selectedRatingIcon = IconType.STAR;
110    private IconType unselectedRatingIcon = IconType.STAR_BORDER;
111    private List<MaterialIcon> iconList = new LinkedList<>();
112
113    /**
114     * Default constructor.
115     */
116    public MaterialRating() {
117        super(DOM.createDiv(), AddinsCssName.MATERIAL_RATING);
118    }
119
120    public MaterialRating(IconType selectedRatingIcon, IconType unselectedRatingIcon, Color textColor) {
121        this();
122        setSelectedRatingIcon(selectedRatingIcon);
123        setUnselectedRatingIcon(unselectedRatingIcon);
124    }
125
126    public MaterialRating(IconType selectedRatingIcon, IconType unselectedRatingIcon, Color textColor, Integer value) {
127        this(selectedRatingIcon, unselectedRatingIcon, textColor);
128        setValue(value);
129    }
130
131    public MaterialRating(IconType selectedRatingIcon, IconType unselectedRatingIcon, Color textColor, Integer value, Integer maxRating) {
132        this(selectedRatingIcon, unselectedRatingIcon, textColor, value);
133        setMaxRating(maxRating);
134    }
135
136    @Override
137    protected void onLoad() {
138        super.onLoad();
139
140        revalidateLayout();
141    }
142
143    /**
144     * Sets the maximum number of icons to show - which represents the maximum
145     * selectable rating. The default is 5.
146     *
147     * @param maxRating The maximum selectable rating for this component
148     */
149    public void setMaxRating(int maxRating) {
150        this.maxRating = maxRating;
151        revalidateLayout();
152    }
153
154    /**
155     * Returns the maximum selectable rating in this component.
156     * The default is 5.
157     *
158     * @return The maximum rating
159     */
160    public int getMaxRating() {
161        return maxRating;
162    }
163
164    /**
165     * Sets the {@link IconType} to be used to represent the selected ratings.
166     * The default is {@link IconType#STAR}.
167     *
168     * @param selectedRatingIcon The icon of the selected ratings
169     */
170    public void setSelectedRatingIcon(IconType selectedRatingIcon) {
171        this.selectedRatingIcon = selectedRatingIcon;
172        revalidateLayout();
173    }
174
175    /**
176     * Returns the {@link IconType} used to represent the selected ratings. The
177     * default is {@link IconType#STAR}.
178     *
179     * @return The icon for selected ratings
180     */
181    public IconType getSelectedRatingIcon() {
182        return selectedRatingIcon;
183    }
184
185    /**
186     * Sets the {@link IconType} to be used to represent the not selected
187     * ratings. The default is {@link IconType#STAR_BORDER}.
188     *
189     * @param unselectedRatingIcon The icon of the unselected ratings
190     */
191    public void setUnselectedRatingIcon(IconType unselectedRatingIcon) {
192        this.unselectedRatingIcon = unselectedRatingIcon;
193        revalidateLayout();
194    }
195
196    /**
197     * Returns the {@link IconType} used to represent the not selected ratings.
198     * The default is {@link IconType#STAR_BORDER}.
199     *
200     * @return The icon for unselected ratings
201     */
202    public IconType getUnselectedRatingIcon() {
203        return unselectedRatingIcon;
204    }
205
206    @Override
207    public void clear() {
208        iconList.clear();
209        super.clear();
210    }
211
212    /**
213     * Method called internally by the component to re-validate the number of
214     * icons when the maximum rating is changed.
215     */
216    protected void revalidateLayout() {
217        for (MaterialIcon icon : iconList) {
218            icon.removeFromParent();
219        }
220        iconList.clear();
221
222        // same mouse-out handler for all icons
223        MouseOutHandler outHandler = event -> {
224            if (!isEnabled() || !isEditable()) {
225                return;
226            }
227            revalidateSelection(currentRating);
228        };
229
230        for (int i = 0; i < maxRating; i++) {
231            final int rating = i + 1;
232            MaterialIcon icon = new MaterialIcon(unselectedRatingIcon);
233            registerHandler(icon.addClickHandler(event -> {
234                if (!isEnabled() || !isEditable()) {
235                    return;
236                }
237                setValue(rating, true);
238            }));
239
240            registerHandler(icon.addMouseOverHandler(event -> {
241                if (!isEnabled() || !isEditable()) {
242                    return;
243                }
244                revalidateSelection(rating);
245            }));
246
247            registerHandler(icon.addMouseOutHandler(outHandler));
248            add(icon);
249            iconList.add(icon);
250        }
251        revalidateSelection(currentRating);
252    }
253
254    /**
255     * Method called internally by the component to revalidade selections by the
256     * user, switching the icons accordingly.
257     */
258    protected void revalidateSelection(int rating) {
259        for (MaterialIcon icon : iconList) {
260            icon.removeStyleName(AddinsCssName.MATERIAL_RATING_UNSELECTED);
261            icon.removeStyleName(AddinsCssName.MATERIAL_RATING_SELECTED);
262        }
263
264        for (int i = 0; i < rating && i < iconList.size(); i++) {
265            MaterialIcon icon = iconList.get(i);
266            icon.setIconType(selectedRatingIcon);
267            icon.addStyleName(AddinsCssName.MATERIAL_RATING_SELECTED);
268        }
269
270        for (int i = rating; i < iconList.size(); i++) {
271            MaterialIcon icon = iconList.get(i);
272            icon.setIconType(unselectedRatingIcon);
273            icon.addStyleName(AddinsCssName.MATERIAL_RATING_UNSELECTED);
274        }
275    }
276
277    @Override
278    public Integer getValue() {
279        return currentRating;
280    }
281
282    @Override
283    public void setValue(Integer value) {
284        setValue(value, false);
285    }
286
287    @Override
288    public void setValue(Integer value, boolean fireEvents) {
289        currentRating = value;
290        revalidateSelection(currentRating);
291        if (fireEvents) {
292            ValueChangeEvent.fire(this, value);
293        }
294    }
295
296    /**
297     * Sets whether the user can interact with the component or not.
298     * Non-editable MaterialRatings can only show values, not allowing users to
299     * change them. The default is <code>true</code> (editable).
300     *
301     * @param editable <code>true</code> to allow the user change the state of the component,
302     *                 <code>false</code> otherwise.
303     */
304    public void setEditable(boolean editable) {
305        this.editable = editable;
306    }
307
308    /**
309     * Returns whether the component is editable by the user. The default is
310     * <code>true</code> (editable).
311     *
312     * @return <code>true</code> if the component is editable by the user,
313     * <code>false</code> otherwise'
314     */
315    public boolean isEditable() {
316        return editable;
317    }
318
319    @Override
320    public HandlerRegistration addValueChangeHandler(ValueChangeHandler<Integer> handler) {
321        return addHandler(handler, ValueChangeEvent.getType());
322    }
323}