001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (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.widgets.code;
029
030import org.opencms.acacia.client.css.I_CmsLayoutBundle;
031import org.opencms.acacia.client.widgets.I_CmsEditWidget;
032import org.opencms.gwt.client.CmsCoreProvider;
033import org.opencms.gwt.client.Messages;
034import org.opencms.gwt.client.ui.FontOpenCms;
035import org.opencms.gwt.client.ui.input.CmsSelectBox;
036import org.opencms.gwt.client.util.CmsDomUtil;
037import org.opencms.gwt.client.util.CmsJsUtil;
038import org.opencms.gwt.shared.I_CmsCodeMirrorClientConfiguration;
039
040import java.util.Arrays;
041import java.util.HashMap;
042import java.util.LinkedHashMap;
043import java.util.Map;
044
045import com.google.gwt.core.client.JavaScriptObject;
046import com.google.gwt.dom.client.Document;
047import com.google.gwt.dom.client.Element;
048import com.google.gwt.event.dom.client.FocusEvent;
049import com.google.gwt.event.dom.client.FocusHandler;
050import com.google.gwt.event.logical.shared.HasResizeHandlers;
051import com.google.gwt.event.logical.shared.ResizeEvent;
052import com.google.gwt.event.logical.shared.ResizeHandler;
053import com.google.gwt.event.logical.shared.ValueChangeEvent;
054import com.google.gwt.event.logical.shared.ValueChangeHandler;
055import com.google.gwt.event.shared.HandlerRegistration;
056import com.google.gwt.json.client.JSONObject;
057import com.google.gwt.user.client.ui.ComplexPanel;
058import com.google.gwt.user.client.ui.FlowPanel;
059import com.google.web.bindery.autobean.shared.AutoBeanCodex;
060
061/**
062 * Client-side content editor widget for editing source code using the CodeMirror library.
063 * */
064public class CmsCodeMirrorWidget extends ComplexPanel implements I_CmsEditWidget, HasResizeHandlers {
065
066    /** Map from modes to labels. */
067    private static LinkedHashMap<String, String> m_modes;
068
069    /** Map from simplified mode names to actual mode names (e.g. from 'javascript' to 'text/javascript'). */
070    private static Map<String, String> m_simpleModeTranslations;
071
072    /** Helper for loading the necessary scripts. */
073    private static final CmsCodeMirrorScriptLoader scriptLoader = new CmsCodeMirrorScriptLoader();
074
075    static {
076        m_modes = new LinkedHashMap<>();
077        m_simpleModeTranslations = new HashMap<>();
078        addMode("text/css", "css", "CSS");
079        addMode("text/html", "html", "HTML");
080        addMode("text/plain", "text", "Text");
081        addMode("text/x-java", "java", "Java");
082        addMode("text/javascript", "javascript", "Javascript");
083        addMode("application/x-jsp", "jsp", "JSP");
084        addMode("application/xml", "xml", "XML");
085    }
086
087    /** The token to control activation. */
088    private boolean m_active = true;
089
090    /** The configuration from the server. */
091    private I_CmsCodeMirrorClientConfiguration m_config;
092
093    /** The native CodeMirror instance. */
094    private JavaScriptObject m_editor;
095
096    /** Boolean flag indicating whether syntax highlighting is currently turned on.*/
097    private boolean m_highlighting = true;
098
099    /** The currently selected mode (may be different from the actual mode in the CodeMirror instance). */
100    private String m_mode = "text/plain";
101
102    /** The mode selector. */
103    private CmsSelectBox m_modeSelect;
104
105    /** The initial content (we need to save this because CodeMirror is loaded asynchronously). */
106    private String m_originalContent;
107
108    /** The previous value. */
109    private String m_previousValue;
110
111    /** The 'redo' button. */
112    private CmsCodeMirrorToolbarButton m_redo;
113
114    /** Toggle button for automatically closing brackets / tags. */
115    private CmsCodeMirrorToggleButton m_toggleAutoClose;
116
117    /** The 'show tabs' toggle button. */
118    private CmsCodeMirrorToggleButton m_toggleShowTabs;
119
120    /** The 'undo' button. */
121    private CmsCodeMirrorToolbarButton m_undo;
122
123    private String m_id;
124
125    /**
126     * Creates a new display widget.<p>
127     *
128     * @param config the widget configuration string
129     */
130    public CmsCodeMirrorWidget(String config) {
131
132        super();
133        m_id = "" + Math.random();
134        setElement(Document.get().createDivElement());
135        m_config = AutoBeanCodex.decode(
136            CmsCoreProvider.AUTO_BEAN_FACTORY,
137            I_CmsCodeMirrorClientConfiguration.class,
138            config).as();
139        addStyleName("oc-codemirror-editorwidget");
140        Integer height = m_config.getHeight();
141        if (height != null) {
142            nativeSetProperty(getElement(), "--codemirror-height", height + "px");
143        }
144        add(createToolbar(), getElement().<Element> cast());
145    }
146
147    /**
148     * Helper method used to make a mode available for selection.
149     *
150     * @param name the normal MIME type name of the mode
151     * @param simpleName simple mode name for use in the server-side configuration, can be null
152     * @param title the title to be used in the mode selector
153     */
154    private static void addMode(String name, String simpleName, String title) {
155
156        m_modes.put(name, title);
157        if (simpleName != null) {
158            m_simpleModeTranslations.put(simpleName, name);
159        }
160    }
161
162    /**
163     * @see com.google.gwt.event.dom.client.HasFocusHandlers#addFocusHandler(com.google.gwt.event.dom.client.FocusHandler)
164     */
165    public HandlerRegistration addFocusHandler(FocusHandler handler) {
166
167        return addDomHandler(handler, FocusEvent.getType());
168    }
169
170    /**
171     * @see com.google.gwt.event.logical.shared.HasResizeHandlers#addResizeHandler(com.google.gwt.event.logical.shared.ResizeHandler)
172     */
173
174    public HandlerRegistration addResizeHandler(ResizeHandler handler) {
175
176        return addHandler(handler, ResizeEvent.getType());
177    }
178
179    /**
180     * @see com.google.gwt.event.logical.shared.HasValueChangeHandlers#addValueChangeHandler(com.google.gwt.event.logical.shared.ValueChangeHandler)
181     */
182    public HandlerRegistration addValueChangeHandler(ValueChangeHandler<String> handler) {
183
184        return addHandler(handler, ValueChangeEvent.getType());
185    }
186
187    /**
188     * Represents a value change event.<p>
189     *
190     */
191    public void fireChangeEvent() {
192
193        updateUndoRedo();
194        fireValueChange(false);
195    }
196
197    /**
198     * Represents a resize event.<p>
199     * @param event from text area panel
200     */
201    public void fireResizeeEvent(ResizeEvent event) {
202
203        ResizeEvent.fire(this, event.getWidth(), event.getHeight());
204    }
205
206    /**
207     * Gets the effective mode to use, which depends both on the selected mode and on whether the user has enabled syntax highlighting.
208     *
209     * @return the effective mode
210     */
211    public String getEffectiveMode() {
212
213        return m_highlighting ? m_mode : "text/plain";
214    }
215
216    /**
217     * @see com.google.gwt.user.client.ui.HasValue#getValue()
218     */
219    public String getValue() {
220
221        return getContent();
222    }
223
224    /**
225     * @see org.opencms.acacia.client.widgets.I_CmsEditWidget#isActive()
226     */
227    public boolean isActive() {
228
229        return m_active;
230    }
231
232    /**
233     * @see org.opencms.acacia.client.widgets.I_CmsEditWidget#onAttachWidget()
234     */
235    public void onAttachWidget() {
236
237        onAttach();
238    }
239
240    /**
241     * @see org.opencms.acacia.client.widgets.I_CmsEditWidget#owns(com.google.gwt.dom.client.Element)
242     */
243    public boolean owns(Element element) {
244
245        return getElement().isOrHasChild(element);
246
247    }
248
249    /**
250     * @see org.opencms.acacia.client.widgets.I_CmsEditWidget#setActive(boolean)
251     */
252    public void setActive(boolean active) {
253
254        if (m_active == active) {
255            return;
256        }
257        m_active = active;
258        if (m_editor != null) {
259            if (m_active) {
260                getElement().removeClassName(I_CmsLayoutBundle.INSTANCE.form().inActive());
261                fireValueChange(true);
262            } else {
263                getElement().addClassName(I_CmsLayoutBundle.INSTANCE.form().inActive());
264            }
265        }
266    }
267
268    /**
269     * @see org.opencms.acacia.client.widgets.I_CmsEditWidget#setName(java.lang.String)
270     */
271    public void setName(String name) {
272
273        // not used
274    }
275
276    /**
277     * @see com.google.gwt.user.client.ui.HasValue#setValue(java.lang.Object)
278     */
279    public void setValue(String value) {
280
281        setValue(value, false);
282    }
283
284    /**
285     * @see com.google.gwt.user.client.ui.HasValue#setValue(java.lang.Object, boolean)
286     */
287    public void setValue(String value, boolean fireEvents) {
288
289        if (value != null) {
290            value = value.trim();
291        }
292        setPreviousValue(value);
293        if (m_editor == null) {
294            // editor has not been initialized yet
295            m_originalContent = value;
296        } else {
297            nativeSetContent(value);
298        }
299        if (fireEvents) {
300            fireValueChange(true);
301        }
302
303    }
304
305    /**
306     * Fires the value change event, if the value has changed.<p>
307     *
308     * @param force <code>true</code> to force firing the event, not regarding an actually changed value
309     */
310    protected void fireValueChange(boolean force) {
311
312        String currentValue = getValue();
313        if (force || !currentValue.equals(m_previousValue)) {
314            m_previousValue = currentValue;
315            ValueChangeEvent.fire(this, currentValue);
316        }
317    }
318
319    /**
320     * Returns the previous value.<p>
321     *
322     * @return the previous value
323     */
324    protected String getPreviousValue() {
325
326        return m_previousValue;
327    }
328
329    /**
330     * @see com.google.gwt.user.client.ui.FocusWidget#onAttach()
331     */
332    @Override
333    protected void onAttach() {
334
335        super.onAttach();
336        scriptLoader.load(() -> {
337            int height = -1;
338            if (m_config.getHeight() != null) {
339                height = m_config.getHeight().intValue();
340            }
341            initCodeMirror(
342                CmsCodeMirrorWidget.this,
343                getElement(),
344                CmsJsUtil.parseJSON(m_config.getPhrasesJSON()),
345                height);
346            initializeUserControlledSettings();
347            if (m_originalContent != null) {
348                nativeSetContent(m_originalContent);
349                clearHistory();
350                updateUndoRedo();
351            }
352        });
353    }
354
355    /**
356     * Sets the previous value.<p>
357     *
358     * @param previousValue the previous value to set
359     */
360    protected void setPreviousValue(String previousValue) {
361
362        m_previousValue = previousValue;
363    }
364
365    /**
366     * Clears the undo / redo history.
367     */
368    private native void clearHistory() /*-{
369        var editor = this.@org.opencms.acacia.client.widgets.code.CmsCodeMirrorWidget::m_editor;
370        editor.clearHistory();
371    }-*/;
372
373    /**
374     * Creates a simple toggle button for enabling / disabling a native CodeMirror option.
375     *
376     * @param icon the icon
377     * @param optionName the option name
378     * @param initialValue the initial vale of the option
379     *
380     * @return the newly created toggle button
381     */
382    private CmsCodeMirrorToggleButton createOptionButton(FontOpenCms icon, String optionName, boolean initialValue) {
383
384        CmsCodeMirrorToggleButton result = new CmsCodeMirrorToggleButton(icon);
385        result.setValue(initialValue, false);
386        result.addValueChangeHandler(event -> nativeSetOption(optionName, event.getValue().booleanValue()));
387        return result;
388
389    }
390
391    /**
392     * Creates the editor tool bar.
393     * @return the editor tool bar
394     */
395    private FlowPanel createToolbar() {
396
397        FlowPanel result = new FlowPanel();
398        result.addStyleName("oc-codewidget-toolbar");
399
400        {
401            CmsCodeMirrorToolbarButton undo = new CmsCodeMirrorToolbarButton(FontOpenCms.UNDO);
402            undo.setTitle(Messages.get().key(Messages.GUI_CODEMIRROR_BUTTON_UNDO_0));
403            undo.addClickHandler(event -> executeCommand("undo"));
404            m_undo = undo;
405            result.add(undo);
406        }
407
408        {
409            CmsCodeMirrorToolbarButton redo = new CmsCodeMirrorToolbarButton(FontOpenCms.REDO);
410            redo.setTitle(Messages.get().key(Messages.GUI_CODEMIRROR_BUTTON_REDO_0));
411            redo.addClickHandler(event -> executeCommand("redo"));
412            m_redo = redo;
413            result.add(redo);
414        }
415
416        {
417            CmsCodeMirrorToolbarButton search = new CmsCodeMirrorToolbarButton(FontOpenCms.SEARCH);
418            search.setTitle(Messages.get().key(Messages.GUI_CODEMIRROR_BUTTON_FIND_0));
419            search.addClickHandler(event -> {
420                search();
421            });
422            result.add(search);
423        }
424
425        {
426            CmsCodeMirrorToolbarButton searchReplace = new CmsCodeMirrorToolbarButton(FontOpenCms.SEARCH_REPLACE);
427            searchReplace.setTitle(Messages.get().key(Messages.GUI_CODEMIRROR_BUTTON_REPLACE_0));
428            searchReplace.addClickHandler(event -> searchReplace());
429            result.add(searchReplace);
430        }
431
432        {
433            CmsCodeMirrorToolbarButton lineButton = new CmsCodeMirrorToolbarButton("L");
434            lineButton.setTitle(Messages.get().key(Messages.GUI_CODEMIRROR_BUTTON_JUMP_TO_LINE_0));
435            lineButton.addClickHandler(event -> {
436                executeCommand("jumpToLine");
437            });
438            result.add(lineButton);
439        }
440        {
441            CmsCodeMirrorToggleButton highlight = new CmsCodeMirrorToggleButton(FontOpenCms.HIGHLIGHT);
442            highlight.setValue(true, false);
443            highlight.setTitle(Messages.get().key(Messages.GUI_CODEMIRROR_TOGGLE_SYNTAX_HIGHLIGHTING_0));
444            highlight.addValueChangeHandler(event -> setSyntaxHighlightingEnabled(event.getValue().booleanValue()));
445            result.add(highlight);
446        }
447
448        {
449            CmsCodeMirrorToggleButton wrap = createOptionButton(FontOpenCms.WRAP_LINES, "lineWrapping", false);
450            wrap.setTitle(Messages.get().key(Messages.GUI_CODEMIRROR_TOGGLE_LINE_WRAP_0));
451            result.add(wrap);
452        }
453
454        {
455            CmsCodeMirrorToggleButton showTabs = new CmsCodeMirrorToggleButton(FontOpenCms.INVISIBLE_CHARS);
456            showTabs.setTitle(Messages.get().key(Messages.GUI_CODEMIRROR_TOGGLE_TABS_0));
457            showTabs.addValueChangeHandler(event -> setShowTabs(event.getValue().booleanValue()));
458            m_toggleShowTabs = showTabs;
459            result.add(showTabs);
460        }
461
462        {
463            CmsCodeMirrorToggleButton brackets = new CmsCodeMirrorToggleButton(FontOpenCms.BRACKETS);
464            brackets.setTitle(Messages.get().key(Messages.GUI_CODEMIRROR_TOGGLE_AUTO_CLOSE_BRACKETS_0));
465            brackets.addValueChangeHandler(event -> setAutoCloseBrackets(event.getValue().booleanValue()));
466            m_toggleAutoClose = brackets;
467            result.add(brackets);
468        }
469
470        m_modeSelect = new CmsSelectBox(m_modes);
471        m_modeSelect.addStyleName("oc-codewidget-mode-select");
472
473        Map<String, String> items = new LinkedHashMap<>();
474        for (String fontSize : Arrays.asList("8px", "10px", "12px", "14px", "16px", "18px", "20px")) {
475            items.put(fontSize, fontSize);
476        }
477        CmsSelectBox select = new CmsSelectBox(items);
478        select.addStyleName("oc-codewidget-fontsize-select");
479        select.setFormValue("14px");
480        result.add(select);
481        result.add(m_modeSelect);
482
483        select.addValueChangeHandler(event -> {
484            String fontSize = event.getValue();
485            setFontSize(fontSize);
486        });
487
488        m_modeSelect.addValueChangeHandler(event -> {
489            String mode = event.getValue();
490            setMode(mode);
491
492        });
493        return result;
494
495    }
496
497    /**
498     * Executes a named CodeMirror command.
499     *
500     * @param command the name of the command to execute
501     */
502    private native void executeCommand(String command) /*-{
503        var editor = this.@org.opencms.acacia.client.widgets.code.CmsCodeMirrorWidget::m_editor;
504        $wnd.CodeMirror.commands[command](editor);
505    }-*/;
506
507    /**
508     * Gets the editor content.
509     *
510     * @return the editor content
511     */
512    private native String getContent() /*-{
513        var editor = this.@org.opencms.acacia.client.widgets.code.CmsCodeMirrorWidget::m_editor;
514        return editor.getValue();
515    }-*/;
516
517    /**
518     * Gets the history size.
519     *
520     * @return the history size
521     */
522    private native JavaScriptObject getHistorySize() /*-{
523        var editor = this.@org.opencms.acacia.client.widgets.code.CmsCodeMirrorWidget::m_editor;
524        return editor.historySize();
525    }-*/;
526
527    /**
528     * Checks if a CodeMirror dialog is currently being displayed.
529     *
530     * @return true if a CodeMirror dialog is being displayed
531     */
532    private boolean hasDialog() {
533
534        return CmsDomUtil.getElementsByClass("CodeMirror-dialog", getElement()).size() > 0;
535    }
536
537    /**
538     * Initializes the CodeMirror instance.
539     *
540     * @param elem the parent element
541     * @param phrases the localization phrases
542     * @param height the initial editor height, or -1 if no height should be set
543     */
544    private native void initCodeMirror(CmsCodeMirrorWidget instance, Element elem, JavaScriptObject phrases, int height) /*-{
545        var config = {
546            theme: "eclipse",
547            mode: "text/plain",
548            lineNumbers: true,
549            styleActiveLine: true,
550            fixedGutter: true,
551            indentUnit: 4,
552            indentWithTabs: true,
553            smartIndent: false
554        };
555
556        config.phrases = phrases;
557        var result = $wnd.CodeMirror(elem, config);
558        instance.getEditor = function() {
559            return result;
560        }
561        this.@org.opencms.acacia.client.widgets.code.CmsCodeMirrorWidget::m_editor = result;
562        var that = this;
563        result.on("changes", function(editor, changes) {
564            that.@org.opencms.acacia.client.widgets.code.CmsCodeMirrorWidget::fireChangeEvent()();
565        });
566        if (height > 0) {
567            result.setSize(null, height);
568        }
569        var resizeHandler = new ResizeObserver(function(entries) {
570            result.refresh();
571        });
572        resizeHandler.observe(elem);
573    }-*/;
574
575    /**
576     * Initializes the settings controlled by buttons / select boxes after CodeMirror is loaded.
577     */
578    private void initializeUserControlledSettings() {
579
580        String startMode = m_config.getStartMode();
581        if (startMode != null) {
582            if (!m_modes.containsKey(startMode)) {
583                startMode = m_simpleModeTranslations.get(startMode.toLowerCase());
584            }
585            if (startMode != null) {
586                m_modeSelect.setFormValue(startMode, true);
587            }
588        }
589        m_toggleAutoClose.setValue(true, /*fireEvents=*/true);
590        m_toggleShowTabs.setValue(false, /*fireEvents=*/true);
591    }
592
593    /**
594     * Sets the content in the native CodeMirror instance.
595     *
596     * @param content the new editor content
597     */
598    private native void nativeSetContent(String content) /*-{
599        var editor = this.@org.opencms.acacia.client.widgets.code.CmsCodeMirrorWidget::m_editor;
600        editor.setValue(content);
601    }-*/;
602
603    /**
604     * Sets the mode on the native CodeMirror instance.
605     *
606     * @param mode the mode to set
607     */
608    private native void nativeSetMode(String mode) /*-{
609        var editor = this.@org.opencms.acacia.client.widgets.code.CmsCodeMirrorWidget::m_editor;
610        editor.setOption("mode", mode);
611    }-*/;
612
613    /**
614     * Sets a CodeMirror option of type 'boolean'.
615     *
616     * @param name the name of the option
617     * @param booleanValue the option value
618     */
619    private native void nativeSetOption(String name, boolean booleanValue) /*-{
620        var editor = this.@org.opencms.acacia.client.widgets.code.CmsCodeMirrorWidget::m_editor;
621        if (editor) { // editor may not be fully loaded yet
622            editor.setOption(name, booleanValue);
623        }
624    }-*/;
625
626    /**
627     * Sets a custom CSS property on the given element.
628     *
629     * @param element the element on which to set the property
630     * @param name the name of the CSS property
631     * @param value the value of the CSS property
632     */
633    private native void nativeSetProperty(com.google.gwt.dom.client.Element element, String name, String value) /*-{
634        element.style.setProperty(name, value);
635    }-*/;
636
637    /**
638     * Executes the 'redo' command.
639     */
640    private void redo() {
641
642        executeCommand("redo");
643    }
644
645    /**
646     * Refreshes the editor layout.
647     */
648    private native void refresh() /*-{
649        var editor = this.@org.opencms.acacia.client.widgets.code.CmsCodeMirrorWidget::m_editor;
650        editor.refresh();
651    }-*/;
652
653    /**
654     * Starts a search operation.
655     */
656    private void search() {
657
658        if (!hasDialog()) {
659            // executing a dialog command while a dialog is active leads to weird/confusing UI state
660            executeCommand("find");
661        }
662    }
663
664    /**
665     * Starts a search/replace operation.
666     */
667    private void searchReplace() {
668
669        if (!hasDialog()) {
670            // executing a dialog command while a dialog is active leads to weird/confusing UI state
671            executeCommand("replace");
672        }
673    }
674
675    /**
676     * Enables/disables automatic closing of brackets.
677     *
678     * @param value true if automatic bracket closing should be enabled
679     */
680    private void setAutoCloseBrackets(boolean value) {
681
682        nativeSetOption("autoCloseBrackets", value);
683        nativeSetOption("autoCloseTags", value);
684        nativeSetOption("matchBrackets", value);
685    }
686
687    /**
688     * Updates the editor font size.
689     *
690     * @param value the font size, e.g. '12px'
691     */
692    private void setFontSize(String value) {
693
694        nativeSetProperty(getElement().cast(), "--codemirror-font-size", value);
695        refresh();
696    }
697
698    /**
699     * Sets the mode selected by the user (this may not result in the mode actually being changed in the editor if syntax highlighting is turned off).
700     *
701     * @param mode the new mode
702     */
703    private void setMode(String mode) {
704
705        m_mode = mode;
706        if (m_editor != null) {
707            nativeSetMode(getEffectiveMode());
708        }
709
710    }
711
712    /**
713     * Enables/disables tab visibility.
714     *
715     * @param value true if tabs should be shown
716     */
717    private void setShowTabs(boolean value) {
718
719        nativeSetProperty(getElement(), "--codemirror-tab-state", value ? "visible" : "hidden");
720        nativeSetOption("showTrailingSpace", value);
721
722    }
723
724    /**
725     * Enables / disables syntax highlighting.
726     *
727     * @param booleanValue true if syntax highlighting should be enabled
728     */
729    private void setSyntaxHighlightingEnabled(boolean booleanValue) {
730
731        m_highlighting = booleanValue;
732        nativeSetMode(getEffectiveMode());
733    }
734
735    /**
736     * Updates undo/redo button states based on history size.
737     */
738    private void updateUndoRedo() {
739
740        JavaScriptObject jso = getHistorySize();
741        JSONObject historySize = new JSONObject(jso);
742        int undo = (int)(historySize.get("undo").isNumber().doubleValue());
743        int redo = (int)(historySize.get("redo").isNumber().doubleValue());
744        m_undo.setEnabled(undo > 0);
745        m_redo.setEnabled(redo > 0);
746    }
747
748}