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}