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