Skip to main content
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to 'org.eclipse.text/src/org/eclipse/text/undo/DocumentUndoManager.java')
-rw-r--r--org.eclipse.text/src/org/eclipse/text/undo/DocumentUndoManager.java719
1 files changed, 719 insertions, 0 deletions
diff --git a/org.eclipse.text/src/org/eclipse/text/undo/DocumentUndoManager.java b/org.eclipse.text/src/org/eclipse/text/undo/DocumentUndoManager.java
new file mode 100644
index 00000000000..c94c7df7cb7
--- /dev/null
+++ b/org.eclipse.text/src/org/eclipse/text/undo/DocumentUndoManager.java
@@ -0,0 +1,719 @@
+/*******************************************************************************
+ * Copyright (c) 2006 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * IBM Corporation - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.text.undo;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.core.commands.operations.IContextReplacingOperation;
+import org.eclipse.core.commands.operations.IOperationHistory;
+import org.eclipse.core.commands.operations.IOperationHistoryListener;
+import org.eclipse.core.commands.operations.IUndoContext;
+import org.eclipse.core.commands.operations.IUndoableOperation;
+import org.eclipse.core.commands.operations.ObjectUndoContext;
+import org.eclipse.core.commands.operations.OperationHistoryEvent;
+import org.eclipse.core.commands.operations.OperationHistoryFactory;
+
+import org.eclipse.core.runtime.IAdaptable;
+import org.eclipse.core.runtime.ListenerList;
+
+import org.eclipse.jface.text.Assert;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.DocumentEvent;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IDocumentExtension4;
+import org.eclipse.jface.text.IDocumentListener;
+import org.eclipse.jface.text.TextUtilities;
+
+/**
+ * A standard implementation of a document-based undo manager that
+ * creates an undo history based on changes to its document.
+ * <p>
+ * Based on the 3.1 implementation of DefaultUndoManager, it was implemented
+ * using the document-related manipulations defined in the original
+ * DefaultUndoManager, by separating the document manipulations from the
+ * viewer-specific processing.</p>
+ * <p>
+ * The classes representing individual text edits (formerly text commands)
+ * were promoted from inner types to their own classes in order to support
+ * reassignment to a different undo manager.<p>
+ * <p>
+ * This class is not intended to be subclassed.
+ * </p>
+ * <p>
+ * XXX: This is work in progress and can change anytime until API for 3.2 is frozen.
+ * </p>
+ *
+ * @see IDocumentUndoManager
+ * @see DocumentUndoManagerRegistry
+ * @see IDocumentUndoListener
+ * @see org.eclipse.jface.text.IDocument
+ * @since 3.2
+ */
+public class DocumentUndoManager implements IDocumentUndoManager {
+
+ /**
+ * Internal listener to document changes.
+ */
+ private class DocumentListener implements IDocumentListener {
+
+ private String fReplacedText;
+
+ /*
+ * @see org.eclipse.jface.text.IDocumentListener#documentAboutToBeChanged(org.eclipse.jface.text.DocumentEvent)
+ */
+ public void documentAboutToBeChanged(DocumentEvent event) {
+ try {
+ fReplacedText= event.getDocument().get(event.getOffset(),
+ event.getLength());
+ fPreservedUndoModificationStamp= event.getModificationStamp();
+ } catch (BadLocationException x) {
+ fReplacedText= null;
+ }
+ }
+
+ /*
+ * @see org.eclipse.jface.text.IDocumentListener#documentChanged(org.eclipse.jface.text.DocumentEvent)
+ */
+ public void documentChanged(DocumentEvent event) {
+ fPreservedRedoModificationStamp= event.getModificationStamp();
+
+ // record the current valid state for the top operation in case it
+ // remains the
+ // top operation but changes state.
+ IUndoableOperation op= fHistory.getUndoOperation(fUndoContext);
+ boolean wasValid= false;
+ if (op != null)
+ wasValid= op.canUndo();
+ // Process the change, providing the before and after timestamps
+ processChange(event.getOffset(), event.getOffset()
+ + event.getLength(), event.getText(), fReplacedText,
+ fPreservedUndoModificationStamp,
+ fPreservedRedoModificationStamp);
+
+ // now update fCurrent with the latest buffers from the document
+ // change.
+ fCurrent.pretendCommit();
+
+ if (op == fCurrent) {
+ // if the document change did not cause a new fCurrent to be
+ // created, then we should
+ // notify the history that the current operation changed if its
+ // validity has changed.
+ if (wasValid != fCurrent.isValid())
+ fHistory.operationChanged(op);
+ } else {
+ // if the change created a new fCurrent that we did not yet add
+ // to the
+ // stack, do so if it's valid and we are not in the middle of a
+ // compound change.
+ if (fCurrent != fLastAddedTextEdit && fCurrent.isValid()) {
+ addToOperationHistory(fCurrent);
+ }
+ }
+ }
+ }
+
+ /*
+ * @see IOperationHistoryListener
+ */
+ class HistoryListener implements IOperationHistoryListener {
+ private IUndoableOperation fOperation;
+
+ public void historyNotification(final OperationHistoryEvent event) {
+ final int type= event.getEventType();
+ switch (type) {
+ case OperationHistoryEvent.ABOUT_TO_UNDO:
+ case OperationHistoryEvent.ABOUT_TO_REDO:
+ // if this is one of our operations
+ if (event.getOperation().hasContext(fUndoContext)) {
+ // if we are undoing/redoing an operation we generated, then
+ // ignore
+ // the document changes associated with this undo or redo.
+ if (event.getOperation() instanceof UndoableTextChange) {
+ listenToTextChanges(false);
+
+ // in the undo case only, make sure compounds are closed
+ if (type == OperationHistoryEvent.ABOUT_TO_UNDO) {
+ if (fFoldingIntoCompoundChange) {
+ endCompoundChange();
+ }
+ }
+ } else {
+ // the undo or redo has our context, but it is not one
+ // of our edits. We will listen to the changes, but will
+ // reset the state that tracks the undo/redo history.
+ commit();
+ fLastAddedTextEdit= null;
+ }
+ fOperation= event.getOperation();
+ }
+ break;
+ case OperationHistoryEvent.UNDONE:
+ case OperationHistoryEvent.REDONE:
+ case OperationHistoryEvent.OPERATION_NOT_OK:
+ if (event.getOperation() == fOperation) {
+ listenToTextChanges(true);
+ fOperation= null;
+ }
+ break;
+ }
+ }
+
+ }
+
+ /**
+ * The undo context for this document undo manager.
+ */
+ ObjectUndoContext fUndoContext;
+
+ /**
+ * The document whose changes are being tracked.
+ */
+ IDocument fDocument;
+
+ /**
+ * The currently constructed edit.
+ */
+ UndoableTextChange fCurrent;
+
+ /**
+ * The internal document listener.
+ */
+ private DocumentListener fDocumentListener;
+
+ /**
+ * Indicates whether the current change belongs to a compound change.
+ */
+ boolean fFoldingIntoCompoundChange= false;
+
+ /**
+ * The operation history being used to store the undo history.
+ */
+ IOperationHistory fHistory;
+
+ /**
+ * The operation history listener used for managing undo and redo before and
+ * after the individual edits are performed.
+ */
+ private IOperationHistoryListener fHistoryListener;
+
+ /**
+ * The text edit last added to the operation history. This must be tracked
+ * internally instead of asking the history, since outside parties may be
+ * placing items on our undo/redo history.
+ */
+ private UndoableTextChange fLastAddedTextEdit= null;
+
+ /**
+ * The document modification stamp for redo.
+ */
+ protected long fPreservedRedoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
+
+ /**
+ * Text buffer to collect viewer content which has been replaced
+ */
+ StringBuffer fPreservedTextBuffer;
+
+ /**
+ * The document modification stamp for undo.
+ */
+ protected long fPreservedUndoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
+
+ /**
+ * The last delete text edit.
+ */
+ private UndoableTextChange fPreviousDelete;
+
+ /**
+ * Text buffer to collect text which is inserted into the viewer
+ */
+ StringBuffer fTextBuffer;
+
+ /** Indicates inserting state. */
+ private boolean fInserting= false;
+
+ /** Indicates overwriting state. */
+ private boolean fOverwriting= false;
+
+ /** The registered document listeners. */
+ private ListenerList fDocumentUndoListeners;
+
+ /** The list of clients connected. */
+ private List fConnected;
+
+ /**
+ *
+ * Create a DocumentUndoManager for the given document.
+ *
+ * @param document the document whose undo history is being managed.
+ */
+ public DocumentUndoManager(IDocument document) {
+ super();
+ Assert.isNotNull(document);
+ fDocument= document;
+ fHistory= OperationHistoryFactory.getOperationHistory();
+ fUndoContext= new ObjectUndoContext(fDocument);
+ fConnected= new ArrayList();
+ fDocumentUndoListeners= new ListenerList();
+ }
+
+ /*
+ * @see org.eclipse.jface.text.IDocumentUndoManager#addDocumentUndoListener(org.eclipse.jface.text.IDocumentUndoListener)
+ */
+ public void addDocumentUndoListener(IDocumentUndoListener listener) {
+ fDocumentUndoListeners.add(listener);
+ }
+
+ /*
+ * @see org.eclipse.jface.text.IDocumentUndoManager#removeDocumentUndoListener(org.eclipse.jface.text.IDocumentUndoListener)
+ */
+ public void removeDocumentUndoListener(IDocumentUndoListener listener) {
+ fDocumentUndoListeners.remove(listener);
+ }
+
+ /*
+ * @see org.eclipse.jface.text.IDocumentUndoManager#getUndoContext()
+ */
+ public IUndoContext getUndoContext() {
+ return fUndoContext;
+ }
+
+ /*
+ * @see org.eclipse.jface.text.IDocumentUndoManager#commit()
+ */
+ public void commit() {
+
+ fInserting= false;
+ fOverwriting= false;
+ fPreviousDelete.reinitialize();
+
+ // if fCurrent has never been placed on the history, do so now.
+ // this can happen when there are multiple programmatically commits in a
+ // single document change.
+ if (fLastAddedTextEdit != fCurrent) {
+ fCurrent.pretendCommit();
+ if (fCurrent.isValid())
+ addToOperationHistory(fCurrent);
+ }
+ fCurrent.commit();
+ }
+
+ /*
+ * @see org.eclipse.jface.text.IDocumentUndoManager#connect(java.lang.Object)
+ */
+ public void connect(Object client) {
+ if (!isConnected()) {
+ initialize();
+ }
+ if (!fConnected.contains(client))
+ fConnected.add(client);
+ }
+
+ /*
+ * @see org.eclipse.jface.text.IDocumentUndoManager#disconnect(java.lang.Object)
+ */
+ public void disconnect(Object client) {
+ fConnected.remove(client);
+ if (!isConnected()) {
+ shutdown();
+ }
+ }
+
+ /*
+ * @see org.eclipse.jface.text.IDocumentUndoManager#beginCompoundChange()
+ */
+ public void beginCompoundChange() {
+ if (isConnected()) {
+ fFoldingIntoCompoundChange= true;
+ commit();
+ }
+ }
+
+ /*
+ * @see org.eclipse.jface.text.IDocumentUndoManager#endCompoundChange()
+ */
+ public void endCompoundChange() {
+ if (isConnected()) {
+ fFoldingIntoCompoundChange= false;
+ commit();
+ }
+ }
+
+ /*
+ * @see org.eclipse.jface.text.IDocumentUndoManager#setUndoLimit(int)
+ */
+ public void setUndoLimit(int undoLimit) {
+ fHistory.setLimit(fUndoContext, undoLimit);
+ }
+
+ /**
+ * Fires a document undo event to all registered document undo listeners.
+ * Uses a robust iterator.
+ *
+ * @param offset the document offset
+ * @param text the text that was inserted
+ * @param preservedText the text being replaced
+ * @param uiInfo an adapter that may provide additional UI info about the triggering action
+ * @param eventType the type of event causing the change
+ * @param isCompound a flag indicating whether the change is a compound change
+ * @see IDocumentUndoListener
+ */
+ void fireDocumentUndo(int offset, String text, String preservedText, IAdaptable uiInfo, int eventType, boolean isCompound) {
+ eventType= isCompound ? eventType | DocumentUndoEvent.COMPOUND : eventType;
+ DocumentUndoEvent event= new DocumentUndoEvent(fDocument, offset, text, preservedText, eventType, uiInfo);
+ Object[] listeners= fDocumentUndoListeners.getListeners();
+ for (int i= 0; i < listeners.length; i++) {
+ ((IDocumentUndoListener)listeners[i]).documentUndoNotification(event);
+ }
+
+ }
+
+ /**
+ * Adds any listeners needed to track the document and the operations
+ * history.
+ */
+ private void addListeners() {
+ fHistoryListener= new HistoryListener();
+ fHistory.addOperationHistoryListener(fHistoryListener);
+ listenToTextChanges(true);
+ }
+
+ /**
+ * Removes any listeners that were installed by the document.
+ */
+ private void removeListeners() {
+ listenToTextChanges(false);
+ fHistory.removeOperationHistoryListener(fHistoryListener);
+ fHistoryListener= null;
+ }
+
+ /**
+ * Adds the given text edit to the operation history if it is not part of a
+ * compound change.
+ *
+ * @param edit
+ * the edit to be added
+ */
+ private void addToOperationHistory(UndoableTextChange edit) {
+ if (!fFoldingIntoCompoundChange
+ || edit instanceof UndoableCompoundTextChange) {
+ fHistory.add(edit);
+ fLastAddedTextEdit= edit;
+ }
+ }
+
+ /**
+ * Disposes the undo history.
+ */
+ private void disposeUndoHistory() {
+ fHistory.dispose(fUndoContext, true, true, true);
+ }
+
+ /**
+ * Initializes the undo history.
+ */
+ private void initializeUndoHistory() {
+ if (fHistory != null && fUndoContext != null)
+ fHistory.dispose(fUndoContext, true, true, false);
+
+ }
+
+ /**
+ * Checks whether the given text starts with a line delimiter and
+ * subsequently contains a white space only.
+ *
+ * @param text the text to check
+ * @return <code>true</code> if the text is a line delimiter followed by
+ * whitespace, <code>false</code> otherwise
+ */
+ private boolean isWhitespaceText(String text) {
+
+ if (text == null || text.length() == 0)
+ return false;
+
+ String[] delimiters= fDocument.getLegalLineDelimiters();
+ int index= TextUtilities.startsWith(delimiters, text);
+ if (index > -1) {
+ char c;
+ int length= text.length();
+ for (int i= delimiters[index].length(); i < length; i++) {
+ c= text.charAt(i);
+ if (c != ' ' && c != '\t')
+ return false;
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Switches the state of whether there is a text listener or not.
+ *
+ * @param listen the state which should be established
+ */
+ private void listenToTextChanges(boolean listen) {
+ if (listen) {
+ if (fDocumentListener == null && fDocument != null) {
+ fDocumentListener= new DocumentListener();
+ fDocument.addDocumentListener(fDocumentListener);
+ }
+ } else if (!listen) {
+ if (fDocumentListener != null && fDocument != null) {
+ fDocument.removeDocumentListener(fDocumentListener);
+ fDocumentListener= null;
+ }
+ }
+ }
+
+ private void processChange(int modelStart, int modelEnd,
+ String insertedText, String replacedText,
+ long beforeChangeModificationStamp,
+ long afterChangeModificationStamp) {
+
+ if (insertedText == null)
+ insertedText= ""; //$NON-NLS-1$
+
+ if (replacedText == null)
+ replacedText= ""; //$NON-NLS-1$
+
+ int length= insertedText.length();
+ int diff= modelEnd - modelStart;
+
+ if (fCurrent.fUndoModificationStamp == IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP)
+ fCurrent.fUndoModificationStamp= beforeChangeModificationStamp;
+
+ // normalize
+ if (diff < 0) {
+ int tmp= modelEnd;
+ modelEnd= modelStart;
+ modelStart= tmp;
+ }
+
+ if (modelStart == modelEnd) {
+ // text will be inserted
+ if ((length == 1) || isWhitespaceText(insertedText)) {
+ // by typing or whitespace
+ if (!fInserting
+ || (modelStart != fCurrent.fStart
+ + fTextBuffer.length())) {
+ fCurrent.fRedoModificationStamp= beforeChangeModificationStamp;
+ if (fCurrent.attemptCommit())
+ fCurrent.fUndoModificationStamp= beforeChangeModificationStamp;
+
+ fInserting= true;
+ }
+ if (fCurrent.fStart < 0)
+ fCurrent.fStart= fCurrent.fEnd= modelStart;
+ if (length > 0)
+ fTextBuffer.append(insertedText);
+ } else if (length > 0) {
+ // by pasting or model manipulation
+ fCurrent.fRedoModificationStamp= beforeChangeModificationStamp;
+ if (fCurrent.attemptCommit())
+ fCurrent.fUndoModificationStamp= beforeChangeModificationStamp;
+
+ fCurrent.fStart= fCurrent.fEnd= modelStart;
+ fTextBuffer.append(insertedText);
+ fCurrent.fRedoModificationStamp= afterChangeModificationStamp;
+ if (fCurrent.attemptCommit())
+ fCurrent.fUndoModificationStamp= afterChangeModificationStamp;
+
+ }
+ } else {
+ if (length == 0) {
+ // text will be deleted by backspace or DEL key or empty
+ // clipboard
+ length= replacedText.length();
+ String[] delimiters= fDocument.getLegalLineDelimiters();
+
+ if ((length == 1)
+ || TextUtilities.equals(delimiters, replacedText) > -1) {
+
+ // whereby selection is empty
+
+ if (fPreviousDelete.fStart == modelStart
+ && fPreviousDelete.fEnd == modelEnd) {
+ // repeated DEL
+
+ // correct wrong settings of fCurrent
+ if (fCurrent.fStart == modelEnd
+ && fCurrent.fEnd == modelStart) {
+ fCurrent.fStart= modelStart;
+ fCurrent.fEnd= modelEnd;
+ }
+ // append to buffer && extend edit range
+ fPreservedTextBuffer.append(replacedText);
+ ++fCurrent.fEnd;
+
+ } else if (fPreviousDelete.fStart == modelEnd) {
+ // repeated backspace
+
+ // insert in buffer and extend edit range
+ fPreservedTextBuffer.insert(0, replacedText);
+ fCurrent.fStart= modelStart;
+
+ } else {
+ // either DEL or backspace for the first time
+
+ fCurrent.fRedoModificationStamp= beforeChangeModificationStamp;
+ if (fCurrent.attemptCommit())
+ fCurrent.fUndoModificationStamp= beforeChangeModificationStamp;
+
+ // as we can not decide whether it was DEL or backspace
+ // we initialize for backspace
+ fPreservedTextBuffer.append(replacedText);
+ fCurrent.fStart= modelStart;
+ fCurrent.fEnd= modelEnd;
+ }
+
+ fPreviousDelete.set(modelStart, modelEnd);
+
+ } else if (length > 0) {
+ // whereby selection is not empty
+ fCurrent.fRedoModificationStamp= beforeChangeModificationStamp;
+ if (fCurrent.attemptCommit())
+ fCurrent.fUndoModificationStamp= beforeChangeModificationStamp;
+
+ fCurrent.fStart= modelStart;
+ fCurrent.fEnd= modelEnd;
+ fPreservedTextBuffer.append(replacedText);
+ }
+ } else {
+ // text will be replaced
+
+ if (length == 1) {
+ length= replacedText.length();
+ String[] delimiters= fDocument.getLegalLineDelimiters();
+
+ if ((length == 1)
+ || TextUtilities.equals(delimiters, replacedText) > -1) {
+ // because of overwrite mode or model manipulation
+ if (!fOverwriting
+ || (modelStart != fCurrent.fStart
+ + fTextBuffer.length())) {
+ fCurrent.fRedoModificationStamp= beforeChangeModificationStamp;
+ if (fCurrent.attemptCommit())
+ fCurrent.fUndoModificationStamp= beforeChangeModificationStamp;
+
+ fOverwriting= true;
+ }
+
+ if (fCurrent.fStart < 0)
+ fCurrent.fStart= modelStart;
+
+ fCurrent.fEnd= modelEnd;
+ fTextBuffer.append(insertedText);
+ fPreservedTextBuffer.append(replacedText);
+ fCurrent.fRedoModificationStamp= afterChangeModificationStamp;
+ return;
+ }
+ }
+ // because of typing or pasting whereby selection is not empty
+ fCurrent.fRedoModificationStamp= beforeChangeModificationStamp;
+ if (fCurrent.attemptCommit())
+ fCurrent.fUndoModificationStamp= beforeChangeModificationStamp;
+
+ fCurrent.fStart= modelStart;
+ fCurrent.fEnd= modelEnd;
+ fTextBuffer.append(insertedText);
+ fPreservedTextBuffer.append(replacedText);
+ }
+ }
+ // in all cases, the redo modification stamp is updated on the open
+ // text edit
+ fCurrent.fRedoModificationStamp= afterChangeModificationStamp;
+ }
+
+ /**
+ * Initialize the receiver.
+ */
+ private void initialize() {
+ initializeUndoHistory();
+
+ // open up the current text edit
+ fCurrent= new UndoableTextChange(this);
+ fPreviousDelete= new UndoableTextChange(this);
+ fTextBuffer= new StringBuffer();
+ fPreservedTextBuffer= new StringBuffer();
+
+ addListeners();
+ }
+
+ /**
+ * Shutdown the receiver.
+ */
+ private void shutdown() {
+ removeListeners();
+
+ fCurrent= null;
+ fPreviousDelete= null;
+ fTextBuffer= null;
+ fPreservedTextBuffer= null;
+
+ disposeUndoHistory();
+ }
+
+ /**
+ * Return whether or not any clients are connected to the receiver.
+ *
+ * @return <code>true</code> if the receiver is connected to
+ * clients, <code>false</code> if it is not
+ */
+ boolean isConnected() {
+ if (fConnected == null)
+ return false;
+ return !fConnected.isEmpty();
+ }
+
+
+ /*
+ * @see org.eclipse.jface.text.IDocumentUndoManager#transferUndoHistory(IDocumentUndoManager)
+ */
+ public void transferUndoHistory(IDocumentUndoManager manager) {
+ IUndoContext oldUndoContext= manager.getUndoContext();
+ // Get the history for the old undo context.
+ IUndoableOperation [] operations= OperationHistoryFactory.getOperationHistory().getUndoHistory(oldUndoContext);
+ for (int i= 0; i< operations.length; i++) {
+ // First replace the undo context
+ IUndoableOperation op= operations[i];
+ if (op instanceof IContextReplacingOperation) {
+ ((IContextReplacingOperation)op).replaceContext(oldUndoContext, getUndoContext());
+ } else {
+ op.addContext(getUndoContext());
+ op.removeContext(oldUndoContext);
+ }
+ // Now update the manager that owns the text edit.
+ if (op instanceof UndoableTextChange) {
+ ((UndoableTextChange)op).manager= this;
+ }
+ }
+
+ // Record the transfer itself as an undoable change.
+ // If the transfer results from some open operation, recording this change will
+ // cause our undo context to be added to the outer operation. If there is no
+ // outer operation, there will be a local change to signify the transfer.
+ // This also serves to synchronize the modification stamps with the documents.
+ IUndoableOperation op= OperationHistoryFactory.getOperationHistory().getUndoOperation(getUndoContext());
+ UndoableTextChange cmd= new UndoableTextChange(this);
+ cmd.fStart= cmd.fEnd= 0;
+ cmd.fText= cmd.fPreservedText= ""; //$NON-NLS-1$
+ if (fDocument instanceof IDocumentExtension4) {
+ cmd.fRedoModificationStamp= ((IDocumentExtension4)fDocument).getModificationStamp();
+ if (op instanceof UndoableTextChange) {
+ cmd.fUndoModificationStamp= ((UndoableTextChange)op).fRedoModificationStamp;
+ }
+ }
+ addToOperationHistory(cmd);
+ }
+
+}

Back to the top