001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (C) Alkacon Software (http://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software, please see the 018 * company website: http://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: http://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.acacia.client; 029 030import org.opencms.acacia.client.CmsUndoRedoHandler.UndoRedoState; 031import org.opencms.acacia.shared.CmsEntity; 032 033import java.util.Stack; 034 035import com.google.gwt.event.logical.shared.HasValueChangeHandlers; 036import com.google.gwt.event.logical.shared.ValueChangeEvent; 037import com.google.gwt.event.logical.shared.ValueChangeHandler; 038import com.google.gwt.event.shared.EventHandler; 039import com.google.gwt.event.shared.GwtEvent; 040import com.google.gwt.event.shared.HandlerRegistration; 041import com.google.gwt.event.shared.SimpleEventBus; 042import com.google.gwt.user.client.Timer; 043 044/** 045 * Handler for the undo redo function.<p> 046 */ 047public final class CmsUndoRedoHandler implements HasValueChangeHandlers<UndoRedoState> { 048 049 /** The change types. */ 050 public enum ChangeType { 051 /** New value added change. */ 052 add, 053 054 /** A choice change. */ 055 choice, 056 057 /** Value removed change. */ 058 remove, 059 060 /** Value sort change. */ 061 sort, 062 063 /** A simple value change. */ 064 value 065 } 066 067 /** Representing the undo/redo state. */ 068 public class UndoRedoState { 069 070 /** Indicating if there are changes to be re done. */ 071 private boolean m_hasRedo; 072 073 /** Indicating if there are changes to be undone. */ 074 private boolean m_hasUndo; 075 076 /** 077 * Constructor.<p> 078 * 079 * @param hasUndo if there are changes to be undone 080 * @param hasRedo if there are changes to be re done 081 */ 082 UndoRedoState(boolean hasUndo, boolean hasRedo) { 083 084 m_hasUndo = hasUndo; 085 m_hasRedo = hasRedo; 086 } 087 088 /** 089 * Returns if there are changes to be re done. 090 * 091 * @return <code>true</code> if there are changes to be re done. 092 */ 093 public boolean hasRedo() { 094 095 return m_hasRedo; 096 } 097 098 /** 099 * Returns if there are changes to be undone. 100 * 101 * @return <code>true</code> if there are changes to be undone. 102 */ 103 public boolean hasUndo() { 104 105 return m_hasUndo; 106 } 107 } 108 109 /** 110 * A timer to delay the addition of a change.<p> 111 */ 112 protected class ChangeTimer extends Timer { 113 114 /** The attribute name. */ 115 private String m_attributeName; 116 117 /** The change type. */ 118 private ChangeType m_changeType; 119 120 /** The value index. */ 121 private int m_valueIndex; 122 123 /** The value path. */ 124 private String m_valuePath; 125 126 /** 127 * Constructor.<p> 128 * 129 * @param valuePath the entity value path 130 * @param attributeName the attribute name 131 * @param valueIndex the value index 132 * @param changeType the change type 133 */ 134 protected ChangeTimer(String valuePath, String attributeName, int valueIndex, ChangeType changeType) { 135 136 m_valuePath = valuePath; 137 m_attributeName = attributeName; 138 m_valueIndex = valueIndex; 139 m_changeType = changeType; 140 } 141 142 /** 143 * @see com.google.gwt.user.client.Timer#run() 144 */ 145 @Override 146 public void run() { 147 148 internalAddChange(m_valuePath, m_attributeName, m_valueIndex, m_changeType); 149 } 150 151 /** 152 * Checks whether the timer change properties match the given ones.<p> 153 * 154 * @param valuePath the entity value path 155 * @param attributeName the attribute name 156 * @param valueIndex the value index 157 * 158 * @return <code>true</code> if the timer change properties match the given ones 159 */ 160 protected boolean matches(String valuePath, String attributeName, int valueIndex) { 161 162 return m_valuePath.equals(valuePath) 163 && m_attributeName.equals(attributeName) 164 && (m_valueIndex == valueIndex); 165 } 166 } 167 168 /** 169 * Representing a change stack entry.<p> 170 */ 171 private class Change { 172 173 /** The attribute name. */ 174 private String m_attributeName; 175 176 /** The entity data. */ 177 private CmsEntity m_entityData; 178 179 /** The entity id. */ 180 private String m_entityId; 181 182 /** The change type. */ 183 private ChangeType m_type; 184 185 /** The value index. */ 186 private int m_valueIndex; 187 188 /** 189 * Constructor.<p> 190 * 191 * @param entity the chane entity data 192 * @param entityId the entity id 193 * @param attributeName the attribute name 194 * @param valueIndex the value index 195 * @param type the change type 196 */ 197 Change(CmsEntity entity, String entityId, String attributeName, int valueIndex, ChangeType type) { 198 199 m_entityId = entityId; 200 m_attributeName = attributeName; 201 m_valueIndex = valueIndex; 202 m_type = type; 203 m_entityData = entity; 204 } 205 206 /** 207 * Returns the attribute name.<p> 208 * 209 * @return the attribute name 210 */ 211 public String getAttributeName() { 212 213 return m_attributeName; 214 } 215 216 /** 217 * Returns the change entity data.<p> 218 * 219 * @return the change entity data 220 */ 221 public CmsEntity getEntityData() { 222 223 return m_entityData; 224 } 225 226 /** 227 * Returns the change entity id.<p> 228 * 229 * @return the entity id 230 */ 231 public String getEntityId() { 232 233 return m_entityId; 234 } 235 236 /** 237 * The change type.<p> 238 * 239 * @return the change type 240 */ 241 public ChangeType getType() { 242 243 return m_type; 244 } 245 246 /** 247 * Returns the value index.<p> 248 * 249 * @return the value index 250 */ 251 public int getValueIndex() { 252 253 return m_valueIndex; 254 } 255 } 256 257 /** The change timer delay. */ 258 private static final int CHANGE_TIMER_DELAY = 500; 259 260 /** The static instance. */ 261 private static CmsUndoRedoHandler INSTANCE; 262 263 /** The ad change timer. */ 264 private ChangeTimer m_changeTimer; 265 266 /** The current data state. */ 267 private Change m_current; 268 269 /** The editor instance. */ 270 private CmsEditorBase m_editor; 271 272 /** The edited entity. */ 273 private CmsEntity m_entity; 274 275 /** The event bus. */ 276 private SimpleEventBus m_eventBus; 277 278 /** The redo stack. */ 279 private Stack<Change> m_redo; 280 281 /** The root attribute handler. */ 282 private CmsRootHandler m_rootHandler; 283 284 /** The undo stack. */ 285 private Stack<Change> m_undo; 286 287 /** 288 * Constructor.<p> 289 */ 290 private CmsUndoRedoHandler() { 291 292 m_undo = new Stack<Change>(); 293 m_redo = new Stack<Change>(); 294 } 295 296 /** 297 * Returns the undo redo handler instance.<p> 298 * 299 * @return the handler instance 300 */ 301 public static CmsUndoRedoHandler getInstance() { 302 303 if (INSTANCE == null) { 304 INSTANCE = new CmsUndoRedoHandler(); 305 } 306 return INSTANCE; 307 } 308 309 /** 310 * Adds a change to the undo stack.<p> 311 * 312 * @param valuePath the entity value path 313 * @param attributeName the attribute name 314 * @param valueIndex the value index 315 * @param changeType the change type 316 */ 317 public void addChange(String valuePath, String attributeName, int valueIndex, ChangeType changeType) { 318 319 if (ChangeType.value.equals(changeType)) { 320 if (m_changeTimer != null) { 321 if (!m_changeTimer.matches(valuePath, attributeName, valueIndex)) { 322 // only in case the change properties of the timer do not match the current change, 323 // add the last change and start a new timer 324 m_changeTimer.cancel(); 325 m_changeTimer.run(); 326 m_changeTimer = new ChangeTimer(valuePath, attributeName, valueIndex, changeType); 327 m_changeTimer.schedule(CHANGE_TIMER_DELAY); 328 } 329 } else { 330 m_changeTimer = new ChangeTimer(valuePath, attributeName, valueIndex, changeType); 331 m_changeTimer.schedule(CHANGE_TIMER_DELAY); 332 } 333 } else { 334 if (m_changeTimer != null) { 335 m_changeTimer.cancel(); 336 m_changeTimer.run(); 337 } 338 internalAddChange(valuePath, attributeName, valueIndex, changeType); 339 } 340 } 341 342 /** 343 * @see com.google.gwt.event.logical.shared.HasValueChangeHandlers#addValueChangeHandler(com.google.gwt.event.logical.shared.ValueChangeHandler) 344 */ 345 public HandlerRegistration addValueChangeHandler(ValueChangeHandler<UndoRedoState> handler) { 346 347 return addHandler(handler, ValueChangeEvent.getType()); 348 } 349 350 /** 351 * Clears the undo/redo stacks and all references.<p> 352 */ 353 public void clear() { 354 355 m_undo.clear(); 356 m_redo.clear(); 357 m_entity = null; 358 m_editor = null; 359 m_rootHandler = null; 360 } 361 362 /** 363 * @see com.google.gwt.event.shared.HasHandlers#fireEvent(com.google.gwt.event.shared.GwtEvent) 364 */ 365 public void fireEvent(GwtEvent<?> event) { 366 367 ensureHandlers().fireEventFromSource(event, this); 368 } 369 370 /** 371 * Indicates if there are changes to be undone.<p> 372 * 373 * @return <code>true</code> if there are changes to be undone 374 */ 375 public boolean hasRedo() { 376 377 return !m_redo.isEmpty(); 378 } 379 380 /** 381 * Indicates if there are changes to be undone.<p> 382 * 383 * @return <code>true</code> if there are changes to be undone 384 */ 385 public boolean hasUndo() { 386 387 return !m_undo.isEmpty(); 388 } 389 390 /** 391 * Initializes the handler to be used for the given entity.<p> 392 * 393 * @param entity the edited entity 394 * @param editor the editor instance 395 * @param rootHandler the root attribute handler 396 */ 397 public void initialize(CmsEntity entity, CmsEditorBase editor, CmsRootHandler rootHandler) { 398 399 m_undo.clear(); 400 m_redo.clear(); 401 m_entity = entity; 402 m_editor = editor; 403 m_rootHandler = rootHandler; 404 m_current = new Change(m_entity.cloneEntity(), null, null, 0, null); 405 fireStateChange(); 406 } 407 408 /** 409 * Indicates if the handler has been initialized.<p> 410 * 411 * @return <code>true</code> if the handler has been initialized 412 */ 413 public boolean isIntitalized() { 414 415 return m_entity != null; 416 } 417 418 /** 419 * Re-applies the latest state in the redo stack.<p> 420 */ 421 public void redo() { 422 423 if (!m_redo.isEmpty()) { 424 m_undo.push(m_current); 425 m_current = m_redo.pop(); 426 changeEntityContentValues( 427 m_current.getEntityData(), 428 m_current.getEntityId(), 429 m_current.getAttributeName(), 430 m_current.getValueIndex(), 431 m_current.getType()); 432 fireStateChange(); 433 } 434 } 435 436 /** 437 * Reverts to the latest state in the undo stack.<p> 438 */ 439 public void undo() { 440 441 if (hasUndo()) { 442 ChangeType type = m_current.getType(); 443 String entityId = m_current.getEntityId(); 444 String attributeName = m_current.getAttributeName(); 445 int valueIndex = m_current.getValueIndex(); 446 m_redo.push(m_current); 447 m_current = m_undo.pop(); 448 changeEntityContentValues(m_current.getEntityData(), entityId, attributeName, valueIndex, type); 449 fireStateChange(); 450 } 451 } 452 453 /** 454 * Adds this handler to the widget. 455 * 456 * @param <H> the type of handler to add 457 * @param type the event type 458 * @param handler the handler 459 * @return {@link HandlerRegistration} used to remove the handler 460 */ 461 protected <H extends EventHandler> HandlerRegistration addHandler(final H handler, GwtEvent.Type<H> type) { 462 463 return ensureHandlers().addHandlerToSource(type, this, handler); 464 } 465 466 /** 467 * Internally adds a change to the undo stack.<p> 468 * 469 * @param valuePath the entity value path 470 * @param attributeName the attribute name 471 * @param valueIndex the value index 472 * @param changeType the change type 473 */ 474 void internalAddChange(String valuePath, String attributeName, int valueIndex, ChangeType changeType) { 475 476 m_changeTimer = null; 477 //TODO: keep the IDs, otherwise redo will not work 478 CmsEntity currentData = m_entity.cloneEntity(); 479 if (!currentData.equals(m_current.getEntityData())) { 480 m_undo.push(m_current); 481 m_current = new Change(currentData, valuePath, attributeName, valueIndex, changeType); 482 m_redo.clear(); 483 fireStateChange(); 484 } 485 } 486 487 /** 488 * Sets the editor to the given state.<p> 489 * 490 * @param newContent the state content 491 * @param entityId the value path elements 492 * @param attributeName the attribute name 493 * @param valueIndex the value index 494 * @param type the change type 495 */ 496 private void changeEntityContentValues( 497 CmsEntity newContent, 498 String entityId, 499 String attributeName, 500 int valueIndex, 501 ChangeType type) { 502 503 switch (type) { 504 case value: 505 CmsAttributeHandler handler = m_rootHandler.getHandlerById(entityId, attributeName); 506 CmsEntity entity = newContent.getEntityById(entityId); 507 if ((entity != null) && (entity.getAttribute(attributeName) != null)) { 508 String value = entity.getAttribute(attributeName).getSimpleValues().get(valueIndex); 509 if ((handler != null) && handler.hasValueView(valueIndex) && (value != null)) { 510 handler.changeValue(value, valueIndex); 511 break; 512 } 513 } 514 //$FALL-THROUGH$ 515 default: 516 m_editor.rerenderForm(newContent); 517 } 518 } 519 520 /** 521 * Lazy initializing the handler manager.<p> 522 * 523 * @return the handler manager 524 */ 525 private SimpleEventBus ensureHandlers() { 526 527 if (m_eventBus == null) { 528 m_eventBus = new SimpleEventBus(); 529 } 530 return m_eventBus; 531 } 532 533 /** 534 * Fires a value change event to indicate the undo/redo state has changed.<p> 535 */ 536 private void fireStateChange() { 537 538 ValueChangeEvent.fire(this, new UndoRedoState(hasUndo(), hasRedo())); 539 } 540}