001 /** 002 * Copyright 2010 The Kuali Foundation Licensed under the 003 * Educational Community License, Version 2.0 (the "License"); you may 004 * not use this file except in compliance with the License. You may 005 * obtain a copy of the License at 006 * 007 * http://www.osedu.org/licenses/ECL-2.0 008 * 009 * Unless required by applicable law or agreed to in writing, 010 * software distributed under the License is distributed on an "AS IS" 011 * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 012 * or implied. See the License for the specific language governing 013 * permissions and limitations under the License. 014 */ 015 016 package org.kuali.student.common.ui.client.configurable.mvc; 017 018 import java.util.HashMap; 019 import java.util.LinkedHashMap; 020 import java.util.List; 021 import java.util.Map; 022 import java.util.Map.Entry; 023 024 import org.kuali.student.common.ui.client.configurable.mvc.layouts.ViewLayoutController; 025 import org.kuali.student.common.ui.client.configurable.mvc.sections.Section; 026 import org.kuali.student.common.ui.client.configurable.mvc.views.SectionView; 027 import org.kuali.student.common.ui.client.event.ActionEvent; 028 import org.kuali.student.common.ui.client.event.SaveActionEvent; 029 import org.kuali.student.common.ui.client.event.SectionUpdateEvent; 030 import org.kuali.student.common.ui.client.event.SectionUpdateHandler; 031 import org.kuali.student.common.ui.client.event.ValidateRequestEvent; 032 import org.kuali.student.common.ui.client.event.ValidateRequestHandler; 033 import org.kuali.student.common.ui.client.mvc.ActionCompleteCallback; 034 import org.kuali.student.common.ui.client.mvc.Callback; 035 import org.kuali.student.common.ui.client.mvc.Controller; 036 import org.kuali.student.common.ui.client.mvc.DataModel; 037 import org.kuali.student.common.ui.client.mvc.ModelRequestCallback; 038 import org.kuali.student.common.ui.client.mvc.View; 039 import org.kuali.student.common.ui.client.mvc.history.HistoryManager; 040 import org.kuali.student.common.ui.client.widgets.KSButton; 041 import org.kuali.student.common.ui.client.widgets.KSLightBox; 042 import org.kuali.student.common.ui.client.widgets.field.layout.element.FieldElement; 043 import org.kuali.student.core.validation.dto.ValidationResultInfo; 044 import org.kuali.student.core.validation.dto.ValidationResultInfo.ErrorLevel; 045 046 import com.google.gwt.core.client.GWT; 047 import com.google.gwt.event.dom.client.ClickEvent; 048 import com.google.gwt.event.dom.client.ClickHandler; 049 import com.google.gwt.user.client.ui.FlowPanel; 050 import com.google.gwt.user.client.ui.Widget; 051 052 /** 053 * The LayoutController is a central piece of the UIF. This controller is also itself a view. 054 * As such, LayoutControllers can also have other LayoutControllers as their views. 055 * 056 * @see Controller 057 * @author Kuali Student Team 058 * 059 */ 060 public abstract class LayoutController extends Controller implements ViewLayoutController, View { 061 062 protected Map<Enum<?>, View> viewMap = new LinkedHashMap<Enum<?>, View>(); 063 protected Map<String, Enum<?>> viewEnumMap = new HashMap<String, Enum<?>>(); 064 protected Enum<?> defaultView; 065 066 protected String name; 067 protected Enum<?> viewType; 068 069 protected View startPopupView; 070 protected KSLightBox startViewWindow; 071 072 /** 073 * Constructor 074 * Sets up event handlers fro section update events and validation request events. 075 * @param controllerId - not used 076 */ 077 public LayoutController(String controllerId){ 078 super(controllerId); 079 //Global section update Event handling 080 addApplicationEventHandler(SectionUpdateEvent.TYPE, new SectionUpdateHandler(){ 081 082 @Override 083 public void onSectionUpdate(final SectionUpdateEvent event) { 084 LayoutController.this.requestModel(new ModelRequestCallback<DataModel>(){ 085 086 @Override 087 public void onRequestFail(Throwable cause) { 088 GWT.log("Unable to retrieve model for section update", cause); 089 } 090 091 @Override 092 public void onModelReady(DataModel model) { 093 event.getSection().updateModel(model); 094 event.getSection().updateWidgetData(model); 095 096 } 097 }); 098 099 } 100 }); 101 //Global validation Event handling 102 addApplicationEventHandler(ValidateRequestEvent.TYPE, new ValidateRequestHandler() { 103 104 @Override 105 public void onValidateRequest(final ValidateRequestEvent event) { 106 FieldDescriptor originatingField = event.getFieldDescriptor(); 107 String modelId = null; 108 if (originatingField != null) { 109 modelId = originatingField.getModelId(); 110 } 111 if (modelId == null) { 112 requestModel(new ModelRequestCallback<DataModel>() { 113 @Override 114 public void onModelReady(DataModel model) { 115 validate(model, event); 116 } 117 118 @Override 119 public void onRequestFail(Throwable cause) { 120 GWT.log("Unable to retrieve model for validation", cause); 121 } 122 123 }); 124 } else { 125 requestModel(modelId, new ModelRequestCallback<DataModel>() { 126 @Override 127 public void onModelReady(DataModel model) { 128 validate(model, event); 129 } 130 131 @Override 132 public void onRequestFail(Throwable cause) { 133 GWT.log("Unable to retrieve model for validation", cause); 134 } 135 136 }); 137 } 138 } 139 140 }); 141 } 142 143 private void validate(DataModel model, final ValidateRequestEvent event) { 144 if(event.validateSingleField()){ 145 model.validateField(event.getFieldDescriptor(), new Callback<List<ValidationResultInfo>>() { 146 @Override 147 public void exec(List<ValidationResultInfo> result) { 148 if(event.getFieldDescriptor() != null){ 149 //We dont need to traverse since it is single field, so don't do isValid call here 150 //instead add the error messages directly 151 FieldElement element = event.getFieldDescriptor().getFieldElement(); 152 if(element != null){ 153 element.clearValidationPanel(); 154 for(int i = 0; i < result.size(); i++){ 155 ValidationResultInfo vr = result.get(i); 156 if(vr.getElement().equals(event.getFieldDescriptor().getFieldKey()) 157 && event.getFieldDescriptor().hasHadFocus()){ 158 element.processValidationResult(vr); 159 } 160 } 161 } 162 } 163 164 } 165 }); 166 } 167 else{ 168 model.validate(new Callback<List<ValidationResultInfo>>() { 169 @Override 170 public void exec(List<ValidationResultInfo> result) { 171 isValid(result, false, true); 172 } 173 }); 174 } 175 } 176 177 /** 178 * Check to see if the list of validation results have an error. 179 * @param list 180 * @return 181 */ 182 public ErrorLevel checkForErrors(List<ValidationResultInfo> list){ 183 ErrorLevel errorLevel = ErrorLevel.OK; 184 185 for(ValidationResultInfo vr: list){ 186 if(vr.getErrorLevel().getLevel() > errorLevel.getLevel()){ 187 errorLevel = vr.getErrorLevel(); 188 } 189 if(errorLevel.equals(ErrorLevel.ERROR)){ 190 break; 191 } 192 } 193 194 return errorLevel; 195 196 } 197 198 /** 199 * Finds the first parent LayoutController of this LayoutController, returns null if this 200 * is the top level LayoutController. 201 * @param w 202 * @return 203 */ 204 public static LayoutController findParentLayout(Widget w){ 205 LayoutController result = null; 206 while (true) { 207 if (w == null) { 208 break; 209 } else if (w instanceof HasLayoutController) { 210 result = ((HasLayoutController)w).getLayoutController(); 211 if (result != null) { 212 break; 213 } 214 } else if (w instanceof LayoutController) { 215 result = (LayoutController) w; 216 break; 217 } 218 w = w.getParent(); 219 220 } 221 return result; 222 } 223 224 /** 225 * @see org.kuali.student.common.ui.client.configurable.mvc.layouts.ViewLayoutController#addStartViewPopup(org.kuali.student.common.ui.client.mvc.View) 226 */ 227 public void addStartViewPopup(final View view){ 228 startPopupView = view; 229 if(startViewWindow == null){ 230 startViewWindow = new KSLightBox(); 231 } 232 233 FlowPanel panel = new FlowPanel(); 234 panel.add(view.asWidget()); 235 KSButton save = new KSButton("Save",new ClickHandler(){ 236 public void onClick(ClickEvent event) { 237 view.updateModel(); 238 SaveActionEvent saveActionEvent = new SaveActionEvent(true); 239 240 saveActionEvent.setActionCompleteCallback(new ActionCompleteCallback(){ 241 public void onActionComplete(ActionEvent action) { 242 startViewWindow.hide(); 243 } 244 }); 245 246 247 fireApplicationEvent(saveActionEvent); 248 } 249 }); 250 startViewWindow.addButton(save); 251 252 KSButton cancel = new KSButton("Cancel", new ClickHandler(){ 253 public void onClick(ClickEvent event) { 254 startViewWindow.hide(); 255 } 256 }); 257 startViewWindow.addButton(cancel); 258 259 if(view instanceof SectionView){ 260 ((SectionView) view).setController(this); 261 } 262 startViewWindow.setWidget(panel); 263 } 264 265 /** 266 * @return true if the start popup is showing 267 */ 268 public boolean isStartViewShowing(){ 269 if(startViewWindow == null){ 270 return false; 271 } 272 return startViewWindow.isShowing(); 273 } 274 275 public View getStartPopupView(){ 276 return startPopupView; 277 } 278 279 public void showStartPopup(final Callback<Boolean> onReadyCallback){ 280 startPopupView.beforeShow(new Callback<Boolean>() { 281 @Override 282 public void exec(Boolean result) { 283 if (result) { 284 startViewWindow.show(); 285 } 286 onReadyCallback.exec(result); 287 } 288 }); 289 } 290 291 public KSLightBox getStartPopup(){ 292 return startViewWindow; 293 } 294 295 296 /*New methods*/ 297 298 /** 299 * @see org.kuali.student.common.ui.client.configurable.mvc.layouts.ViewLayoutController#addView(org.kuali.student.common.ui.client.mvc.View) 300 */ 301 public void addView(View view){ 302 viewMap.put(view.getViewEnum(), view); 303 viewEnumMap.put(view.getViewEnum().toString(), view.getViewEnum()); 304 if(view instanceof SectionView){ 305 ((SectionView) view).setController(this); 306 } 307 else if(view instanceof ToolView){ 308 ((ToolView) view).setController(this); 309 } 310 } 311 312 /** 313 * @see org.kuali.student.common.ui.client.configurable.mvc.layouts.ViewLayoutController#setDefaultView(java.lang.Enum) 314 */ 315 public <V extends Enum<?>> void setDefaultView(V viewType){ 316 this.defaultView = viewType; 317 } 318 319 public Enum<?> getDefaultView(){ 320 return this.defaultView; 321 } 322 323 /** 324 * @see org.kuali.student.common.ui.client.mvc.View#updateModel() 325 */ 326 public abstract void updateModel(); 327 328 /** 329 * Update the model with a single views information 330 * @param viewType 331 */ 332 public void updateModelFromView(Enum<?> viewType){ 333 View v = viewMap.get(viewType); 334 if(v != null){ 335 v.updateModel(); 336 } 337 } 338 339 /** 340 * Update a the model from the view that is currently being shown by this controller 341 */ 342 public void updateModelFromCurrentView(){ 343 if(this.getCurrentView() != null){ 344 this.getCurrentView().updateModel(); 345 } 346 } 347 348 @Override 349 public <V extends Enum<?>> void getView(V viewType, Callback<View> callback) { 350 callback.exec(viewMap.get(viewType)); 351 } 352 353 @Override 354 public Enum<?> getViewEnumValue(String enumValue) { 355 return viewEnumMap.get(enumValue); 356 } 357 358 @Override 359 public void showDefaultView(final Callback<Boolean> onReadyCallback) { 360 HistoryManager.setLogNavigationHistory(false); 361 //turn of history support for default showing until view is ready 362 if(defaultView != null){ 363 showView(defaultView, onReadyCallback); 364 } 365 else if(!viewMap.isEmpty()){ 366 if(defaultView == null){ 367 showView(viewMap.entrySet().iterator().next().getKey(), onReadyCallback); 368 } 369 } 370 371 } 372 373 /** 374 * Show the view that was the first one added and will likely be the first one in a layout's menu, for 375 * example. Note that this is different than show default view. 376 * 377 * @param onReadyCallback 378 */ 379 public void showFirstView(Callback<Boolean> onReadyCallback){ 380 HistoryManager.setLogNavigationHistory(false); 381 if(!viewMap.isEmpty()){ 382 showView(viewMap.entrySet().iterator().next().getKey(), onReadyCallback); 383 } 384 else{ 385 showDefaultView(onReadyCallback); 386 } 387 } 388 389 /** 390 * Check to see if current/all section(s) is valid (ie. does not contain any errors) 391 * 392 * @param validationResults List of validation results for the layouts model. 393 * @param checkCurrentSectionOnly true if errors should be checked on current section only, false if all sections should be checked 394 * @return true if the specified sections (all or current) has any validation errors 395 */ 396 public boolean isValid(List<ValidationResultInfo> validationResults, boolean checkCurrentSectionOnly){ 397 return isValid(validationResults, checkCurrentSectionOnly, true); 398 } 399 400 /** 401 * @see LayoutController#isValid(List, boolean) 402 * @param validationResults 403 * @param checkCurrentSectionOnly 404 * @param allFields 405 * @return 406 */ 407 public boolean isValid(List<ValidationResultInfo> validationResults, boolean checkCurrentSectionOnly, boolean allFields){ 408 boolean isValid = true; 409 410 if (checkCurrentSectionOnly){ 411 //Check for validation errors on the currently displayed section only 412 View v = getCurrentView(); 413 if(v instanceof Section){ 414 isValid = isValid(validationResults, (Section)v, allFields); 415 } 416 if(this.isStartViewShowing()){ 417 if(startPopupView instanceof Section){ 418 isValid = isValid(validationResults, ((Section) startPopupView), allFields) && isValid; 419 } 420 } 421 } else { 422 //Check for validation errors on all sections 423 String errorSections = ""; 424 StringBuilder errorSectionsbuffer = new StringBuilder(); 425 errorSectionsbuffer.append(errorSections); 426 for (Entry<Enum<?>, View> entry:viewMap.entrySet()) { 427 View v = entry.getValue(); 428 if (v instanceof Section){ 429 if (!isValid(validationResults, (Section)v, allFields)){ 430 isValid = false; 431 errorSectionsbuffer.append(((SectionView)v).getName() + ", "); 432 } 433 } 434 } 435 if(this.isStartViewShowing()){ 436 if(startPopupView instanceof Section){ 437 isValid = isValid(validationResults, ((Section) startPopupView), allFields) && isValid; 438 } 439 } 440 errorSections = errorSectionsbuffer.toString(); 441 if (!errorSections.isEmpty()){ 442 errorSections = errorSections.substring(0, errorSections.length()-2); 443 //container.addMessage("Following section(s) has errors & must be corrected: " + errorSections); 444 } 445 } 446 447 return isValid; 448 } 449 450 private boolean isValid(List<ValidationResultInfo> validationResults, Section section, boolean allFields){ 451 ErrorLevel status; 452 if(allFields){ 453 section.setFieldHasHadFocusFlags(true); 454 status = section.processValidationResults(validationResults); 455 } 456 else{ 457 status = section.processValidationResults(validationResults, false); 458 } 459 460 return (status != ErrorLevel.ERROR); 461 } 462 463 /** 464 * This particular implementation of beforeViewChange checks to see if all its view contains a Controller 465 * and if it does checks with that controller to see if it is ok to change the view. OkToChange callback 466 * will be exec with true if the view is allowed to be changed at this time. This method can be overriden 467 * to provide additional functionality to stop a view from being changed when there is some additional 468 * processing that needs to occur in the ui before the view changes. 469 * 470 * @see org.kuali.student.common.ui.client.mvc.Controller#beforeViewChange(java.lang.Enum, org.kuali.student.common.ui.client.mvc.Callback) 471 */ 472 @Override 473 public void beforeViewChange(Enum<?> viewChangingTo, Callback<Boolean> okToChange) { 474 if(this.getCurrentView() instanceof Controller){ 475 ((Controller)this.getCurrentView()).beforeViewChange(viewChangingTo, okToChange); 476 } 477 else{ 478 okToChange.exec(true); 479 } 480 } 481 482 @Override 483 public Widget asWidget() { 484 return this; 485 } 486 487 @Override 488 public boolean beforeHide() { 489 return true; 490 } 491 492 /** 493 * Default implementation does nothing on before show. Override to do other things before THIS view is 494 * shown. 495 * @see org.kuali.student.common.ui.client.mvc.View#beforeShow(org.kuali.student.common.ui.client.mvc.Callback) 496 */ 497 @Override 498 public void beforeShow(Callback<Boolean> onReadyCallback) { 499 onReadyCallback.exec(true); 500 } 501 502 @Override 503 public Controller getController() { 504 return parentController; 505 } 506 507 @Override 508 public String getName() { 509 if(name == null && viewType != null){ 510 return viewType.toString(); 511 } 512 else{ 513 return name; 514 } 515 } 516 517 @Override 518 public Enum<?> getViewEnum() { 519 return viewType; 520 } 521 522 public void setViewEnum(Enum<?> viewType){ 523 this.viewType= viewType; 524 } 525 526 /** 527 * Sets the name of this LayoutController. This name is used in the breadcrumb and window's title. 528 * Setting the name to the empty string will omit the breadcrumb - this is sometimes desired. 529 * @param name 530 */ 531 public void setName(String name){ 532 this.name = name; 533 } 534 535 public void setController(Controller controller){ 536 parentController = controller; 537 } 538 539 @Override 540 public void collectBreadcrumbNames(List<String> names) { 541 names.add(this.getName()); 542 if(this.getCurrentView() != null){ 543 this.getCurrentView().collectBreadcrumbNames(names); 544 } 545 } 546 547 @Override 548 public void clear() { 549 550 } 551 552 }