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}