001package gwt.material.design.addins.client.rating;
002
003/*
004 * #%L
005 * GwtMaterial
006 * %%
007 * Copyright (C) 2016 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.core.client.GWT;
024import com.google.gwt.event.dom.client.*;
025import com.google.gwt.event.logical.shared.ValueChangeEvent;
026import com.google.gwt.event.logical.shared.ValueChangeHandler;
027import com.google.gwt.event.shared.HandlerRegistration;
028import com.google.gwt.user.client.DOM;
029import com.google.gwt.user.client.ui.HasValue;
030import gwt.material.design.client.base.MaterialWidget;
031import gwt.material.design.client.constants.IconType;
032import gwt.material.design.client.ui.MaterialIcon;
033import gwt.material.design.client.ui.MaterialToast;
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 *
050 * <h3>XML Namespace Declaration</h3>
051 *
052 * <pre>
053 * {@code
054 * xmlns:ma='urn:import:gwt.material.design.addins.client'
055 * }
056 * </pre>
057 *
058 * <h3>UiBinder Usage:</h3>
059 *
060 * <pre>
061 * {@code
062 * <ma:rating.MaterialRating ui:field="rating" />
063 * }
064 * </pre>
065 *
066 * To use different icons, for instance, hearts, you can set:
067 *
068 * <pre>
069 * {@code
070 * <ma:rating.MaterialRating ui:field="rating" selectedRatingIcon="FAVORITE" unselectedRatingIcon="FAVORITE_BORDER" textColor="red" />
071 * }
072 * </pre>
073 *
074 * You can also set the maximum rating (the default is 5):
075 *
076 * <pre>
077 * {@code
078 * <ma:rating.MaterialRating ui:field="rating" maxRating="7" />
079 * }
080 * </pre>
081 *
082 * <h3>Example Java Usage:</h3>
083 *
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 *
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 *
104 */
105public class MaterialRating extends MaterialWidget implements HasValue<Integer> {
106
107    private boolean editable = true;
108    private int currentRating = 0;
109    private int maxRating = 5;
110    private IconType selectedRatingIcon = IconType.STAR;
111    private IconType unselectedRatingIcon = IconType.STAR_BORDER;
112    private List<MaterialIcon> iconList = new LinkedList<>();
113
114    /**
115     * Default constructor.
116     */
117    public MaterialRating() {
118        super(DOM.createDiv(), "material-rating");
119        revalidateLayout();
120    }
121
122    /**
123     * Sets the maximum number of icons to show - which represents the maximum
124     * selectable rating. The default is 5.
125     *
126     * @param maxRating
127     *            The maximum selectable rating for this component
128     */
129    public void setMaxRating(int maxRating) {
130        this.maxRating = maxRating;
131        revalidateLayout();
132    }
133
134    /**
135     * Returns the maximum selectable rating in this component. The default is
136     * 5.
137     *
138     * @return The maximum rating
139     */
140    public int getMaxRating() {
141        return maxRating;
142    }
143
144    /**
145     * Sets the {@link IconType} to be used to represent the selected ratings.
146     * The default is {@link IconType#STAR}.
147     *
148     * @param selectedRatingIcon
149     *            The icon of the selected ratings
150     */
151    public void setSelectedRatingIcon(IconType selectedRatingIcon) {
152        this.selectedRatingIcon = selectedRatingIcon;
153        revalidateLayout();
154    }
155
156    /**
157     * Returns the {@link IconType} used to represent the selected ratings. The
158     * default is {@link IconType#STAR}.
159     *
160     * @return The icon for selected ratings
161     */
162    public IconType getSelectedRatingIcon() {
163        return selectedRatingIcon;
164    }
165
166    /**
167     * Sets the {@link IconType} to be used to represent the not selected
168     * ratings. The default is {@link IconType#STAR_BORDER}.
169     *
170     * @param unselectedRatingIcon
171     *            The icon of the unselected ratings
172     */
173    public void setUnselectedRatingIcon(IconType unselectedRatingIcon) {
174        this.unselectedRatingIcon = unselectedRatingIcon;
175        revalidateLayout();
176    }
177
178    /**
179     * Returns the {@link IconType} used to represent the not selected ratings.
180     * The default is {@link IconType#STAR_BORDER}.
181     *
182     * @return The icon for unselected ratings
183     */
184    public IconType getUnselectedRatingIcon() {
185        return unselectedRatingIcon;
186    }
187
188    @Override
189    public void clear() {
190        iconList.clear();
191        super.clear();
192    }
193
194    /**
195     * Method called internally by the component to revalidade the number of
196     * icons when the maximum rating is changed.
197     */
198    protected void revalidateLayout() {
199        for (MaterialIcon icon : iconList) {
200            icon.removeFromParent();
201        }
202        iconList.clear();
203
204        // same mouse-out handler for all icons
205        MouseOutHandler outHandler = new MouseOutHandler() {
206            @Override
207            public void onMouseOut(MouseOutEvent event) {
208                if (!isEnabled() || !isEditable()) {
209                    return;
210                }
211                revalidateSelection(currentRating);
212            }
213        };
214
215        for (int i = 0; i < maxRating; i++) {
216            final int rating = i + 1;
217            MaterialIcon icon = new MaterialIcon(unselectedRatingIcon);
218            icon.addClickHandler(new ClickHandler() {
219                @Override
220                public void onClick(ClickEvent event) {
221                    if (!isEnabled() || !isEditable()) {
222                        return;
223                    }
224                    setValue(rating, true);
225                }
226            });
227            icon.addMouseOverHandler(new MouseOverHandler() {
228                @Override
229                public void onMouseOver(MouseOverEvent event) {
230                    if (!isEnabled() || !isEditable()) {
231                        return;
232                    }
233                    revalidateSelection(rating);
234                }
235            });
236            icon.addMouseOutHandler(outHandler);
237            add(icon);
238            iconList.add(icon);
239        }
240        GWT.log(unselectedRatingIcon.getCssName());
241        revalidateSelection(currentRating);
242    }
243
244    /**
245     * Method called internally by the component to revalidade selections by the
246     * user, switching the icons accordingly.
247     */
248    protected void revalidateSelection(int rating) {
249        for (MaterialIcon icon : iconList) {
250            icon.removeStyleName("material-rating-unselected");
251            icon.removeStyleName("material-rating-selected");
252        }
253
254        for (int i = 0; i < rating && i < iconList.size(); i++) {
255            MaterialIcon icon = iconList.get(i);
256            icon.setIconType(selectedRatingIcon);
257            icon.addStyleName("material-rating-selected");
258        }
259        for (int i = rating; i < iconList.size(); i++) {
260            MaterialIcon icon = iconList.get(i);
261            icon.setIconType(unselectedRatingIcon);
262            icon.addStyleName("material-rating-unselected");
263        }
264    }
265
266    @Override
267    public HandlerRegistration addValueChangeHandler(ValueChangeHandler<Integer> handler) {
268        return addHandler(handler, ValueChangeEvent.getType());
269    }
270
271    @Override
272    public Integer getValue() {
273        return currentRating;
274    }
275
276    @Override
277    public void setValue(Integer value) {
278        setValue(value, false);
279    }
280
281    @Override
282    public void setValue(Integer value, boolean fireEvents) {
283        currentRating = value;
284        revalidateSelection(currentRating);
285        if (fireEvents) {
286            ValueChangeEvent.fire(this, value);
287        }
288    }
289
290    /**
291     * Sets whether the user can interact with the component or not.
292     * Non-editable MaterialRatings can only show values, not allowing users to
293     * change them. The default is <code>true</code> (editable).
294     *
295     * @param editable
296     *            <code>true</code> to allow the user change the state of the
297     *            component, <code>false</code> otherwise
298     */
299    public void setEditable(boolean editable) {
300        this.editable = editable;
301    }
302
303    /**
304     * Returns whether the component is editable by the user. The default is
305     * <code>true</code> (editable).
306     *
307     * @return <code>true</code> if the component is editable by the user,
308     *         <code>false</code> otherwise'
309     */
310    public boolean isEditable() {
311        return editable;
312    }
313}