/*
 * jNPad v0.3 - jNPad's an Simple Text Editor written in Java
 *
 * Copyright (C) 2014-2017  rgs
 *
 * Require JDK 1.6 (or later)
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 *
 *
 * Info, Questions, Suggestions & Bugs Report to rgsevero@gmail.com
 */

package jnpad.text;

import java.awt.BorderLayout;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Insets;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.BorderFactory;
import javax.swing.ButtonGroup;
import javax.swing.JComponent;
import javax.swing.JMenu;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JToggleButton;
import javax.swing.JViewport;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.Highlighter;
import javax.swing.text.Segment;

import jnpad.GUIUtilities;
import jnpad.JNPadBundle;
import jnpad.action.ActionManager;
import jnpad.action.JNPadActions;
import jnpad.config.Accelerators;
import jnpad.config.Config;
import jnpad.config.Controlable;
import jnpad.config.Updatable;
import jnpad.search.SearchContext;
import jnpad.text.highlighter.BracketHighlightPainter;
import jnpad.text.highlighter.IHighlightPainter;
import jnpad.text.highlighter.OccurrenceHighlightPainter;
import jnpad.text.highlighter.OccurrencesHighlightable;
import jnpad.text.highlighter.SearchHighlightPainter;
import jnpad.text.syntax.ContentTypes;
import jnpad.ui.ColorUtilities;
import jnpad.ui.JNPadCheckBoxMenuItem;
import jnpad.ui.JNPadMenuItem;
import jnpad.ui.JNPadScrollPane;
import jnpad.ui.icon.IconUtilities;
import jnpad.ui.plaf.LAFUtils;
import jnpad.util.LinePosition;
import jnpad.util.Utilities;

/**
 * The Class EditPane.
 *
 * @version 0.3
 * @since   jNPad v0.1
 */
public final class EditPane extends JPanel implements OccurrencesHighlightable, Controlable, IView {
  JPopupMenu                       popupMenu                       = new JPopupMenu();

  JNPadTextArea                    textArea;
  JScrollPane                      scrollPane                      = new JNPadScrollPane();

  private static IHighlightPainter searchHP                        = new SearchHighlightPainter();
  private static IHighlightPainter occurrenceHP                    = new OccurrenceHighlightPainter();
  private static IHighlightPainter bracketHP                       = new BracketHighlightPainter();

  private ActiveLine               activeLine;
  private Gutter                   gutter;
  private GutterBase               gutterBase;
  private MarkStrip                markStrip;
  private JViewport                originalRowHeader;

  /** */
  public static final String       PROPERTY_OCCURRENCES_CHANGED    = "EditPane.occurrencesChanged";                    //$NON-NLS-1$

  /** */
  public static final String       PROPERTY_BOOKMARKS_CHANGED      = "EditPane.bookmarksChanged";                      //$NON-NLS-1$

  /** */
  public static final String       PROPERTY_SEARCH_CHANGED         = "EditPane.searchChanged";                         //$NON-NLS-1$

  private Object                   bracketHighlightTag;
  private Object                   otherBracketHighlightTag;
  private int                      bracketLine                     = -1;
  private int                      otherBracketLine                = -1;
  private int                      lastBracketPosition             = -1;
  private int                      lastOtherBracketPosition        = -1;
  private boolean                  isBracketHighlighterVisible;

  private Object                   searchHighlightTag;
  private SearchContext            searchContext;

  private List<Object>             l_occurrencesTags               = new ArrayList<Object>();
  private List<DocumentRange>      l_occurrences                   = new ArrayList<DocumentRange>();
  private OccurrencesContext       occurrencesContext;

  private TreeSet<DocumentRange>   bookmarks                       = new TreeSet<DocumentRange>();

  private boolean                  lastHasSelection;
  private int                      lastSelectionLength;

  private boolean                  isMarkStripVisible              = Config.MARKER_VISIBLE.getValue();
  private boolean                  isGutterVisible                 = Config.GUTTER_VISIBLE.getValue();
  private boolean                  isActiveLineVisible             = Config.ACTIVE_LINE_VISIBLE.getValue();
  private boolean                  isOccurrencesHighlighterVisible = Config.OCCURRENCES_HIGHLIGHTER_VISIBLE.getValue();
  private boolean                  isBookmarkingEnabled            = Config.BOOKMARKING_ENABLED.getValue();
  private boolean                  occurrencesCicularNavigation    = Config.OCCURRENCES_CIRCULAR_NAVIGATION.getValue();
  private boolean                  bookmarksCicularNavigation      = Config.BOOKMARKS_CIRCULAR_NAVIGATION.getValue();

  private LinePosition             linePosition                    = new LinePosition();

  JNPadTextActions                 actions;

  Buffer                           buffer;

  // --- Minimap [v0.3] ---
  JToggleButton                    tbShowMinimap                   = new JToggleButton();
  private MinimapWindow            minimapWindow;
  // ---
  
  /** Logger */
  private static Logger            LOGGER                          = Logger.getLogger(EditPane.class.getName());

  /** UID */
  private static final long        serialVersionUID                = 9206830119174114528L;

  /**
   * Instantiates a new edits the pane.
   *
   * @param buffer the buffer
   */
  public EditPane(Buffer buffer) {
    super(new BorderLayout());
    this.buffer = buffer;
    try {
      textArea = new JNPadTextArea(this);
      jbInit();
      setBracketHighlighterVisible(Config.BRACKET_HIGHLIGHTER_VISIBLE.getValue());
    }
    catch (Exception ex) {
      LOGGER.log(Level.WARNING, ex.getMessage(), ex);
    }
  }

  /**
   * Instantiates a new edits the pane.
   *
   * @param buffer the buffer
   * @param text the text
   */
  public EditPane(Buffer buffer, String text) {
    this(buffer);
    textArea.setText(text);
  }

  /**
   * Instantiates a new edits the pane.
   *
   * @param buffer the buffer
   * @param oldEditPane the old edit pane
   */
  EditPane(Buffer buffer, EditPane oldEditPane) {
    super(new BorderLayout());
    this.buffer = buffer;
    try {
      textArea = new JNPadTextArea(this, oldEditPane.textArea);

      isMarkStripVisible              = oldEditPane.isMarkStripVisible;
      isGutterVisible                 = oldEditPane.isGutterVisible;
      isActiveLineVisible             = oldEditPane.isActiveLineVisible;
      isOccurrencesHighlighterVisible = oldEditPane.isOccurrencesHighlighterVisible;
      
      jbInit();
      
      setEditable(oldEditPane.isEditable());
      setDocument(oldEditPane.getDocument());
      setBracketHighlighterVisible(oldEditPane.isBracketHighlighterVisible);
      
      textArea.setCaretPosition(oldEditPane.textArea.getCaretPosition());
    }
    catch (Exception ex) {
      LOGGER.log(Level.WARNING, ex.getMessage(), ex);
    }
  }

  /**
   * Component initialization.
   *
   * @throws Exception the exception
   */
  private void jbInit() throws Exception {
    actions = JNPadTextActions.getActions(textArea);
    
    markStrip = new MarkStrip(this);

    setOpaque(false);

    scrollPane.getViewport().add(textArea, null);

    scrollPane.setCorner(ScrollPaneConstants.LOWER_LEFT_CORNER, gutterBase = new GutterBase(this));

    originalRowHeader = scrollPane.getRowHeader();
    
    doSetLineNumbersVisible(isLineNumbersVisible());
    doSetActiveLineVisible(isActiveLineVisible());

    textArea.addCaretListener(new CaretListener() {
      @Override
      public void caretUpdate(final CaretEvent e) {
        LinePosition p = TextUtilities.getLinePosition(textArea, e.getDot());
        linePosition.set(p);

        markStrip.caretUpdate();

        updateControls(CTRLS_POSITION);

        updateSelection();

        if (isBracketHighlighterVisible()) {
          updateBracketHighlighters();
        }
      }
    });

    textArea.addMouseListener(new MouseAdapter() {
      @Override
      public void mousePressed(final MouseEvent e) {
        handleMousePressed(e);
      }

      @Override
      public void mouseClicked(final MouseEvent e) {
        handleMouseClicked(e);
      }

      @Override
      public void mouseReleased(final MouseEvent e) {
        handleMouseReleased(e);
      }
    });

    add(scrollPane, BorderLayout.CENTER);
    add(markStrip, BorderLayout.LINE_END);

    if(!isMarkStripVisible()) {
      markStrip.setVisible(false);
    }
    
    // --- set popup menu ---
    final ActionManager actionManager = ActionManager.INSTANCE;

    popupMenu.add(new JNPadMenuItem(actionManager.get(JNPadActions.ACTION_NAME_CUT)));
    popupMenu.add(new JNPadMenuItem(actionManager.get(JNPadActions.ACTION_NAME_CUT_LINE)));
    popupMenu.add(new JNPadMenuItem(actionManager.get(JNPadActions.ACTION_NAME_COPY)));
    popupMenu.add(new JNPadMenuItem(actionManager.get(JNPadActions.ACTION_NAME_COPY_LINE)));
    popupMenu.add(new JNPadMenuItem(actionManager.get(JNPadActions.ACTION_NAME_PASTE)));
    popupMenu.add(new JNPadMenuItem(actionManager.get(JNPadActions.ACTION_NAME_DELETE_LINE)));
    popupMenu.add(new JNPadMenuItem(actionManager.get(JNPadActions.ACTION_NAME_SELECT_ALL)));
    popupMenu.addSeparator();
    popupMenu.add(new JNPadMenuItem(actionManager.get(JNPadActions.ACTION_NAME_FIND)));
    popupMenu.add(new JNPadMenuItem(actionManager.get(JNPadActions.ACTION_NAME_REPLACE)));
    popupMenu.addSeparator();
    ButtonGroup bgScope = new ButtonGroup();
    JMenu mScope = GUIUtilities.createMenu(JNPadBundle.getString("menu.view.scope"), GUIUtilities.EMPTY_ICON); //$NON-NLS-1$
    mScope.add(new JNPadCheckBoxMenuItem(actionManager.get(JNPadActions.ACTION_NAME_SELECT_SCOPE_AS_VIEWER), bgScope));
    mScope.add(new JNPadCheckBoxMenuItem(actionManager.get(JNPadActions.ACTION_NAME_SELECT_SCOPE_AS_BUFFER_SET), bgScope));
    mScope.add(new JNPadCheckBoxMenuItem(actionManager.get(JNPadActions.ACTION_NAME_SELECT_SCOPE_AS_BUFFER), bgScope));
    mScope.add(new JNPadCheckBoxMenuItem(actionManager.get(JNPadActions.ACTION_NAME_SELECT_SCOPE_AS_EDIT_PANE), bgScope));
    popupMenu.add(mScope);
    popupMenu.addSeparator();
    popupMenu.add(new JNPadMenuItem(actionManager.get(JNPadActions.ACTION_NAME_RIGHT_INDENT)));
    popupMenu.add(new JNPadMenuItem(actionManager.get(JNPadActions.ACTION_NAME_LEFT_INDENT)));
    JMenu mCharCaseChange = GUIUtilities.createMenu(JNPadBundle.getString("PopupButton.charCaseChange"), GUIUtilities.EMPTY_ICON); //$NON-NLS-1$
    mCharCaseChange.add(new JNPadMenuItem(actionManager.get(JNPadActions.ACTION_NAME_TO_UPPER_CASE)));
    mCharCaseChange.add(new JNPadMenuItem(actionManager.get(JNPadActions.ACTION_NAME_TO_LOWER_CASE)));
    mCharCaseChange.add(new JNPadMenuItem(actionManager.get(JNPadActions.ACTION_NAME_INVERT_UPPER_LOWER)));
    mCharCaseChange.add(new JNPadMenuItem(actionManager.get(JNPadActions.ACTION_NAME_CAPITALIZE)));
    mCharCaseChange.add(new JNPadMenuItem(actionManager.get(JNPadActions.ACTION_NAME_TO_TITLE)));
    popupMenu.add(mCharCaseChange);
    JMenu mAlignStrings = GUIUtilities.createMenu(JNPadBundle.getString("menu.format.alignStrings"), GUIUtilities.EMPTY_ICON); //$NON-NLS-1$
    mAlignStrings.add(new JNPadMenuItem(actionManager.get(JNPadActions.ACTION_NAME_ALIGN_STRINGS_BY_COMMA)));
    mAlignStrings.add(new JNPadMenuItem(actionManager.get(JNPadActions.ACTION_NAME_ALIGN_STRINGS_BY_EQUAL)));
    mAlignStrings.add(new JNPadMenuItem(actionManager.get(JNPadActions.ACTION_NAME_ALIGN_STRINGS_BY_CLIPBOARD)));
    popupMenu.add(mAlignStrings);
    popupMenu.addSeparator();
    JMenu mSplitting = GUIUtilities.createMenu(JNPadBundle.getString("menu.window.splitting"), GUIUtilities.EMPTY_ICON); //$NON-NLS-1$
    mSplitting.add(new JNPadMenuItem(actionManager.get(JNPadActions.ACTION_NAME_UNSPLIT_CURRENT)));
    mSplitting.add(new JNPadMenuItem(actionManager.get(JNPadActions.ACTION_NAME_UNSPLIT)));
    mSplitting.add(new JNPadMenuItem(actionManager.get(JNPadActions.ACTION_NAME_SPLIT_HORIZONTALLY)));
    mSplitting.add(new JNPadMenuItem(actionManager.get(JNPadActions.ACTION_NAME_SPLIT_VERTICALLY)));
    mSplitting.add(new JNPadMenuItem(actionManager.get(JNPadActions.ACTION_NAME_RESTORE_SPLIT)));
    mSplitting.addSeparator();
    mSplitting.add(new JNPadMenuItem(actionManager.get(JNPadActions.ACTION_NAME_PREVIOUS_EDIT_PANE)));
    mSplitting.add(new JNPadMenuItem(actionManager.get(JNPadActions.ACTION_NAME_NEXT_EDIT_PANE)));
    mSplitting.addSeparator();
    mSplitting.add(new JNPadMenuItem(actionManager.get(JNPadActions.ACTION_NAME_UNSPLIT_VIEWER_CURRENT)));
    mSplitting.add(new JNPadMenuItem(actionManager.get(JNPadActions.ACTION_NAME_UNSPLIT_VIEWER)));
    mSplitting.add(new JNPadMenuItem(actionManager.get(JNPadActions.ACTION_NAME_SPLIT_VIEWER_HORIZONTALLY)));
    mSplitting.add(new JNPadMenuItem(actionManager.get(JNPadActions.ACTION_NAME_SPLIT_VIEWER_VERTICALLY)));
    mSplitting.add(new JNPadMenuItem(actionManager.get(JNPadActions.ACTION_NAME_RESTORE_VIEWER_SPLIT)));
    mSplitting.addSeparator();
    mSplitting.add(new JNPadMenuItem(actionManager.get(JNPadActions.ACTION_NAME_NEXT_BUFFER_SET)));
    mSplitting.add(new JNPadMenuItem(actionManager.get(JNPadActions.ACTION_NAME_PREVIOUS_BUFFER_SET)));
    popupMenu.add(mSplitting);
    // ---
    
    final ActionListener showPopupActionHandler = new ActionListener() {
      @Override
      public void actionPerformed(final ActionEvent e) {
        Point p = textArea.getCaret().getMagicCaretPosition();
        int x = (int) p.getX();
        int y = (int) p.getY();

        SwingUtilities.updateComponentTreeUI(popupMenu);
        popupMenu.pack();

        int popupMenuWidth = popupMenu.getPreferredSize().width;
        int popupMenuHeight = popupMenu.getPreferredSize().height;
        int fontHeight = textArea.getFont().getSize();
        int widthLimit = textArea.getSize().width - popupMenuWidth;
        int heightLimit = textArea.getSize().height - (popupMenuHeight + 2);
        if (x >= widthLimit)
          x = x - popupMenuWidth;
        if (y >= heightLimit)
          y -= (popupMenuHeight + fontHeight);

        popupMenu.show(EditPane.this, x, y);
      }
    };

    textArea.registerKeyboardAction(showPopupActionHandler, 
        "show-popup-menu", //$NON-NLS-1$
        Accelerators.getPropAccelerator("show-popup-menu.shortcut", Accelerators.SHOW_POPUP_MENU), //$NON-NLS-1$
        JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
    textArea.registerKeyboardAction(showPopupActionHandler, 
        "show-popup-menu2", //$NON-NLS-1$
        Accelerators.getPropAccelerator("show-popup-menu.shortcut2", Accelerators.SHOW_POPUP_MENU2), //$NON-NLS-1$
        JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
    
    if (Config.isDistractionFreeMode()) {
      textArea.addMouseWheelListener(new MouseWheelListener() {
        @Override
        public void mouseWheelMoved(final MouseWheelEvent e) {
          if (scrollPane.isWheelScrollingEnabled() && e.getWheelRotation() != 0) {
            JScrollBar toScroll;
            int newValue;
            boolean isHorizontal = (e.getModifiersEx() & InputEvent.SHIFT_DOWN_MASK) != 0;
            if (isHorizontal) {
              toScroll = scrollPane.getHorizontalScrollBar();
              int value = toScroll.getValue();
              FontMetrics fm = textArea.getFontMetrics(textArea.getFont());
              int characterWidth = fm.stringWidth("M"); //$NON-NLS-1$
              newValue = value += e.getUnitsToScroll() * characterWidth;
            }
            else {
              toScroll = scrollPane.getVerticalScrollBar();
              int value = toScroll.getValue();
              FontMetrics fm = textArea.getFontMetrics(textArea.getFont());
              int lineHeight = fm.getHeight();
              newValue = value += e.getUnitsToScroll() * lineHeight;
            }
            toScroll.setValue(newValue);
          }
        }
      });
    }
    
    // --- minimap [v0.3] ---
    tbShowMinimap.setIcon(new IconUtilities.EyeIcon(10, 10, true, ColorUtilities.giveYellowTouch(LAFUtils.getControl())));
    tbShowMinimap.setToolTipText(TextBundle.getString("EditPane.minimap")); //$NON-NLS-1$
    tbShowMinimap.setBorder(BorderFactory.createEmptyBorder());
    tbShowMinimap.setBorderPainted(false);
    tbShowMinimap.setRequestFocusEnabled(false);
    tbShowMinimap.setMargin(LAFUtils.isNimbusLAF() ? new Insets(0, -7, 0, -7) : new Insets(0, 0, 0, 0));
    tbShowMinimap.addItemListener(new ItemListener() {
      @Override
      public void itemStateChanged(ItemEvent e) {
        if (tbShowMinimap.isSelected()) {
          minimapWindow = new MinimapWindow(EditPane.this);
        }
        else if (minimapWindow != null) {
          minimapWindow.dispose();
          minimapWindow = null;
          Utilities.gc();
        }
      }
    });
    scrollPane.setCorner(ScrollPaneConstants.LOWER_RIGHT_CORNER, tbShowMinimap);
    // ---
  }

  /**
   * Sets the document.
   *
   * @param doc the new document
   */
  public void setDocument(Document doc) {
    textArea.setDocument(doc);
  }
  
  /**
   * Gets the document.
   *
   * @return the document
   */
  public Document getDocument() {
    return textArea.getDocument();
  }
  
  /**
   * Gets the search context.
   *
   * @return the search context
   */
  public SearchContext getSearchContext() {
    return searchContext;
  }

  /**
   * Gets the occurrences context.
   *
   * @return the occurrences context
   */
  public OccurrencesContext getOccurrencesContext() {
    return occurrencesContext;
  }

  /**
   * Update selection.
   */
  void updateSelection() {
    final boolean hasSelection = hasSelection();
    if (hasSelection != lastHasSelection) {
      lastHasSelection = hasSelection;
      updateControls(CTRLS_TEXT_SELECTION);
    }
    int selectionLength = textArea.getSelectionEnd() - textArea.getSelectionStart();
    if (selectionLength != lastSelectionLength) {
      lastSelectionLength = selectionLength;
      buffer.getStatusBar().setSelection(selectionLength);
    }
  }

  /**
   * Gets the bookmarks circular navigation.
   *
   * @return the bookmarks circular navigation
   */
  public boolean getBookmarksCircularNavigation() {
    return bookmarksCicularNavigation;
  }

  /**
   * Sets the bookmarks circular navigation.
   *
   * @param b the new bookmarks circular navigation
   */
  public void setBookmarksCircularNavigation(boolean b) {
    bookmarksCicularNavigation = b;
  }

  /**
   * Gets the occurrences circular navigation.
   *
   * @return the occurrences circular navigation
   */
  public boolean getOccurrencesCircularNavigation() {
    return occurrencesCicularNavigation;
  }

  /**
   * Sets the occurrences circular navigation.
   *
   * @param b the new occurrences circular navigation
   */
  public void setOccurrencesCircularNavigation(boolean b) {
    occurrencesCicularNavigation = b;
  }

  /**
   * Checks if is bookmarking enabled.
   *
   * @return true, if is bookmarking enabled
   */
  public boolean isBookmarkingEnabled() {
    return isBookmarkingEnabled;
  }

  /**
   * Sets the bookmarking enabled.
   *
   * @param b the new bookmarking enabled
   */
  public void setBookmarkingEnabled(boolean b) {
    if (b != isBookmarkingEnabled) {
      isBookmarkingEnabled = b;
      if (!b) {
        clearAllBookmarks();
      }
    }
  }

  /**
   * Adds the bookmark.
   *
   * @param range the range
   */
  private void addBookmark(DocumentRange range) {
    if (bookmarks.add(range)) {
      repaintGutter();
      updateControls(Updatable.CTRLS_BOOKMARKING);
      firePropertyChange(PROPERTY_BOOKMARKS_CHANGED, bookmarks.size() - 1, bookmarks.size());
    }
  }

  /**
   * Removes the bookmark.
   *
   * @param range the range
   */
  private void removeBookmark(DocumentRange range) {
    if (bookmarks.remove(range)) {
      repaintGutter();
      updateControls(Updatable.CTRLS_BOOKMARKING);
      firePropertyChange(PROPERTY_BOOKMARKS_CHANGED, bookmarks.size() + 1, bookmarks.size());
    }
  }

  /**
   * Adds the bookmark.
   *
   * @param line the line
   */
  public void addBookmark(int line) {
    if (!isBookmarkingEnabled()) {
      return;
    }
    try {
      removeBookmark(line);
      addBookmark(getRange(line));
    }
    catch (Exception ex) {
      LOGGER.log(Level.WARNING, ex.getMessage(), ex);
    }
  }

  /**
   * Adds the bookmarks.
   *
   * @param lines the lines
   */
  public void addBookmarks(Collection<? extends Integer> lines) {
    if (!isBookmarkingEnabled() || lines == null || lines.isEmpty()) {
      return;
    }
    try {
      int old = bookmarks.size();
      for (Integer line1 : lines) {
        int line = line1;
        removeBookmark(line);
        bookmarks.add(getRange(line));
      }
      if (old != bookmarks.size()) {
        repaintGutter();
        updateControls(Updatable.CTRLS_BOOKMARKING);
        firePropertyChange(PROPERTY_BOOKMARKS_CHANGED, old, bookmarks.size());
      }
    }
    catch (Exception ex) {
      LOGGER.log(Level.WARNING, ex.getMessage(), ex);
    }
  }

  /**
   * Gets the range.
   *
   * @param line the line
   * @return the range
   * @throws BadLocationException the bad location exception
   */
  private DocumentRange getRange(int line) throws BadLocationException {
    Document doc = textArea.getDocument();

    Element lineElem = textArea.getLine(line);
    int start = lineElem.getStartOffset();
    int end = lineElem.getEndOffset();

    DocumentRange range = new DocumentRange();
    range.start = (start == 0) ? doc.createPosition(1) : doc.createPosition(start); // lpm
    range.end = doc.createPosition(end);
    range.first = (start == 0);

    return range;
  }

  /**
   * Removes the bookmark.
   *
   * @param line the line
   */
  public void removeBookmark(int line) {
    if (!isBookmarkingEnabled()) {
      return;
    }
    DocumentRange target = getBookmark(line);
    if (target != null) {
      removeBookmark(target);
    }
  }

  /**
   * Toggle bookmark.
   *
   * @param line the line
   * @return true, if successful
   */
  public boolean toggleBookmark(int line) {
    if (!isBookmarkingEnabled()) {
      return false;
    }
    DocumentRange target = getBookmark(line);
    if (target != null) {
      removeBookmark(target);
      return false;
    }
    addBookmark(line);
    return true;
  }

  /**
   * Gets the bookmark.
   *
   * @param line the line
   * @return the bookmark
   */
  public DocumentRange getBookmark(int line) {
    for (DocumentRange range : bookmarks) {
      try {
        int ln = textArea.getLineOfOffset(range.getStartOffset());
        if (line == ln) {
          return range;
        }
      }
      catch (BadLocationException ex) {
        LOGGER.log(Level.FINE, ex.getMessage(), ex);
      }
    }
    return null;
  }

  /**
   * Gets the bookmarks count.
   *
   * @return the bookmarks count
   */
  public int getBookmarksCount() {
    return bookmarks.size();
  }

  /**
   * Checks for bookmarks.
   *
   * @return true, if successful
   */
  public boolean hasBookmarks() {
    return bookmarks.size() > 0;
  }

  /**
   * Gets the bookmarks.
   *
   * @return the bookmarks
   */
  public DocumentRange[] getBookmarks() {
    return bookmarks.toArray(new DocumentRange[bookmarks.size()]);
  }

  /**
   * Clear all bookmarks.
   */
  public void clearAllBookmarks() {
    int old = bookmarks.size();
    if (old != 0) {
      bookmarks.clear();
      repaintGutter();
      updateControls(Updatable.CTRLS_BOOKMARKING);
      firePropertyChange(PROPERTY_BOOKMARKS_CHANGED, old, 0);
    }
  }

  /**
   * Next bookmark.
   */
  public void nextBookmark() {
    if (!hasBookmarks())return;

    DocumentRange[] bookmarks_ = getBookmarks();

    int actualPosition = textArea.getCaretPosition();
    int result = Integer.MAX_VALUE;

    for (int i = bookmarks_.length - 1; i >= 0; i--) {
      int aa = bookmarks_[i].getStartOffset();
      if (aa > actualPosition) {
        result = Math.min(result, aa);
      }
      else {
        break;
      }
    }

    if (getBookmarksCircularNavigation() && (result < 0 || result == Integer.MAX_VALUE)) {
      result = bookmarks_[0].getStartOffset();
    }

    try {
      if (result >= 0 && result < Integer.MAX_VALUE) {
        textArea.setCaretPosition(result);
        textArea.scrollToMiddle(result);
      }
      else {
        UIManager.getLookAndFeel().provideErrorFeedback(textArea);
      }
    }
    catch (Exception ex) {
      LOGGER.log(Level.WARNING, ex.getMessage(), ex);
    }
  }

  /**
   * Previous bookmark.
   */
  public void previousBookmark() {
    if (!hasBookmarks())return;

    DocumentRange[] bookmarks_ = getBookmarks();

    int actualPosition = textArea.getCaretPosition();
    int result = -1;

    for (DocumentRange aBookmarks_ : bookmarks_) {
      int aa = aBookmarks_.getStartOffset();
      if (aa < actualPosition) {
        result = Math.max(result, aa);
      }
      else {
        break;
      }
    }

    if (getBookmarksCircularNavigation() && (result < 0 || result == Integer.MAX_VALUE)) {
      result = bookmarks_[bookmarks_.length - 1].getStartOffset();
    }

    try {
      if (result >= 0) {
        textArea.setCaretPosition(result);
        textArea.scrollToMiddle(result);
      }
      else {
        UIManager.getLookAndFeel().provideErrorFeedback(textArea);
      }
    }
    catch (Exception ex) {
      LOGGER.log(Level.WARNING, ex.getMessage(), ex);
    }
  }

  /**
   * Gets the file path.
   *
   * @return the file path
   */
  public String getFilePath() {
    return buffer.getFilePath();
  }

  /**
   * Gets the actions.
   *
   * @return the actions
   */
  public JNPadTextActions getActions() {
    return actions;
  }

  /**
   * Handle mouse clicked.
   *
   * @param e MouseEvent
   */
  void handleMouseClicked(final MouseEvent e) {
    if (e.getSource() == textArea && !e.isPopupTrigger()) {
      //clearAllOccurrences();
      clearSearchHighlight();
      if (e.getClickCount() == 2) {
        doubleClicked(e);
      }
      else {
        lineClicked(e);
      }
    }
  }

  /**
   * Handle mouse pressed.
   *
   * @param e MouseEvent
   */
  void handleMousePressed(final MouseEvent e) {
    if (e.getSource() == textArea && e.isPopupTrigger()) {
      requestFocus();
      showPopupMenu(e);
    }
  }

  /**
   * Handle mouse released.
   *
   * @param e MouseEvent
   */
  void handleMouseReleased(final MouseEvent e) {
    if (e.getSource() == textArea && e.isPopupTrigger()) {
      requestFocus();
      showPopupMenu(e);
    }
  }

  /**
   * Show popup menu.
   *
   * @param e MouseEvent
   */
  private void showPopupMenu(final MouseEvent e) {
    SwingUtilities.updateComponentTreeUI(popupMenu);
    popupMenu.pack();
    popupMenu.show(e.getComponent(), e.getX(), e.getY());
  }

  /**
   * Line clicked.
   *
   * @param e MouseEvent
   */
  private void lineClicked(final MouseEvent e) {
    clearAllOccurrences();
  }

  /**
   * Double clicked.
   *
   * @param e MouseEvent
   */
  private void doubleClicked(final MouseEvent e) {
    if (isOccurrencesHighlighterVisible()) {
      if (e.isAltDown() && e.isControlDown())
        doHighlightAllOccurrences(true, true);   // May/min y palabras completas
      else if (e.isAltDown())
        doHighlightAllOccurrences(true, false);  // May/min
      else if (e.isControlDown())
        doHighlightAllOccurrences(false, true);  // sólo palabras completas
      else
        doHighlightAllOccurrences(false, false); //
    }
  }

  /**
   * Do highlight all occurrences.
   *
   * @param matchCase boolean
   * @param wholeWord boolean
   */
  private void doHighlightAllOccurrences(boolean matchCase, boolean wholeWord) {
    int sl = textArea.getSelectionEnd() - textArea.getSelectionStart();
    if (sl > 0 && sl < 1000) {
      String sel = textArea.getSelectedText();
      if (sel == null) {
        clearAllOccurrences();
        return;
      }
      sel = sel.trim();

      SearchContext searchContext = new SearchContext(sel);
      searchContext.setMatchCase(matchCase);
      searchContext.setWholeWord(wholeWord);
      searchContext.setHighlightAll(true);
      searchContext.setSearchForward(true);

      highlightAllOccurrences(searchContext);
    }
  }

  /**
   * Highlight search.
   *
   * @param context the context
   * @param begin the begin
   * @param end the end
   * @throws BadLocationException the bad location exception
   */
  public void highlightSearch(SearchContext context, int begin, int end) throws BadLocationException {
    doClearSearchHighlight();

    searchHighlightTag = textArea.getHighlighter().addHighlight(begin, end, searchHP);

    textArea.setCaretPosition(begin);

    scrollRectToVisible(textArea.modelToView(begin));

    updateControls(CTRLS_SEARCH);

    SearchContext old = searchContext;
    searchContext = context;
    firePropertyChange(PROPERTY_SEARCH_CHANGED, old, searchContext);

    if(context.getHighlightAll()) {
      if (occurrencesContext != null) {
        SearchContext context2 = occurrencesContext.getSearchContext();
        if (context.equals2(context2)) {
          return;
        }
      }
      highlightAllOccurrences(context);
    }
  }

  /**
   * Clear search highlight.
   */
  public void clearSearchHighlight() {
    if (doClearSearchHighlight()) {
      updateControls(CTRLS_SEARCH);

      SearchContext old = searchContext;
      searchContext = null;
      firePropertyChange(PROPERTY_SEARCH_CHANGED, old, searchContext);
    }
  }

  /**
   * Do clear search highlight.
   *
   * @return boolean
   */
  private boolean doClearSearchHighlight() {
    if (searchHighlightTag != null) {
      textArea.getHighlighter().removeHighlight(searchHighlightTag);
      searchHighlightTag = null;
      return true;
    }
    return false;
  }

  /**
   * Clear all occurrences.
   *
   * @see jnpad.text.highlighter.OccurrencesHighlightable#clearAllOccurrences()
   */
  @Override
  public void clearAllOccurrences() {
    if (doClearAllOccurrences()) {
      updateControls(CTRLS_OCCURRENCE);

      OccurrencesContext old = occurrencesContext;
      occurrencesContext = null;
      firePropertyChange(PROPERTY_OCCURRENCES_CHANGED, old, occurrencesContext);
    }
  }

  /**
   * Do clear all occurrences.
   *
   * @return true, if successful
   */
  private boolean doClearAllOccurrences() {
    if (l_occurrencesTags.size() > 0) {
      for (Object tag : l_occurrencesTags) {
        textArea.getHighlighter().removeHighlight(tag);
      }
      l_occurrencesTags.clear();
      l_occurrences.clear();
      return true;
    }
    return false;
  }

  /**
   * Highlight all occurrences.
   *
   * @param searchContext the search context
   * @see jnpad.text.highlighter.OccurrencesHighlightable#highlightAllOccurrences(jnpad.search.SearchContext)
   */
  @Override
  public void highlightAllOccurrences(SearchContext searchContext) {
    doClearAllOccurrences();

    final String  searchFor  = searchContext.getSearchFor();
    final boolean ignoreCase = !searchContext.getMatchCase();
    final boolean wholeWord  = searchContext.getWholeWord();

    if (Utilities.isEmptyString(searchFor)) {
      return;
    }

    final int len = searchFor.length();

    String target = ignoreCase ? searchFor.toLowerCase() : searchFor;

    try {
      int[] visibleBounds = TextUtilities.getVisibleDocPosBounds(textArea, scrollPane);

      // the quick part first:
      String part = textArea.getText(visibleBounds[0], visibleBounds[1] - visibleBounds[0]);
      if (ignoreCase) {
        part = part.toLowerCase();
      }
      int pos = -1;
      while ((pos = Utilities.indexOf(part, target, pos + 1, false, wholeWord)) >= 0) {
        l_occurrencesTags.add(textArea.getHighlighter().addHighlight(pos + visibleBounds[0], pos + visibleBounds[0] + target.length(), occurrenceHP));
      }

      Document doc = textArea.getDocument();

      // the whole document now (ignoring already performed part)
      part = TextUtilities.getText(doc);
      if (ignoreCase) {
        part = part.toLowerCase();
      }
      pos = -1;
      while ( (pos = Utilities.indexOf(part, target, pos + 1, false, wholeWord)) >= 0) {
        DocumentRange range = new DocumentRange();
        range.start = (pos == 0) ? doc.createPosition(1) : doc.createPosition(pos); // lpm
        range.end = doc.createPosition(pos + len);
        range.first = (pos == 0);
        l_occurrences.add(range);

        if (pos < visibleBounds[0] || pos > visibleBounds[1]) {
          l_occurrencesTags.add(textArea.getHighlighter().addHighlight(pos, pos + len, occurrenceHP));
        }
      }

      if (l_occurrences.size() != l_occurrencesTags.size()) { // no debería pasar
        LOGGER.info("l_occurrences.size() != l_occurrencesTags.size()"); //$NON-NLS-1$
      }

      updateControls(CTRLS_OCCURRENCE);

      OccurrencesContext old = occurrencesContext;
      occurrencesContext = new OccurrencesContext(searchContext, l_occurrences.size());
      firePropertyChange(PROPERTY_OCCURRENCES_CHANGED, old, occurrencesContext);
    }
    catch (Exception ex) {
      LOGGER.log(Level.WARNING, ex.getMessage(), ex);
    }
  }

  /**
   * Focus on text area.
   */
  public void focusOnTextArea() {
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        textArea.requestFocus();
      }
    });
  }
  
  /**
   * Refresh caret position.
   */
  void refreshCaretPosition() {
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        textArea.setCaretPosition(textArea.getCaretPosition());
      }
    });
  }

  /**
   * Checks for searched.
   *
   * @return true, if successful
   */
  public boolean hasSearched() {
    return searchHighlightTag != null;
  }

  /**
   * Gets the searched.
   *
   * @return the searched
   */
  public DocumentRange getSearched() {
    DocumentRange range = null;
    if (searchHighlightTag != null && searchHighlightTag instanceof Highlighter.Highlight) {
      Highlighter.Highlight h = (Highlighter.Highlight) searchHighlightTag;

      int start = h.getStartOffset();
      int end = h.getEndOffset();

      Document doc = textArea.getDocument();

      try {
        range = new DocumentRange();
        range.start = (start == 0) ? doc.createPosition(1) : doc.createPosition(start); // lpm
        range.end = doc.createPosition(end);
        range.first = (start == 0);
      }
      catch (BadLocationException ex) {
        LOGGER.log(Level.FINE, ex.getMessage(), ex);
      }
    }
    return range;
  }

  /**
   * Gets the occurrences count.
   *
   * @return the occurrences count
   */
  public int getOccurrencesCount() {
    return l_occurrences.size();
  }

  /**
   * Checks for occurrences.
   *
   * @return true, if successful
   */
  public boolean hasOccurrences() {
    return l_occurrences.size() > 1;
  }

  /**
   * Gets the occurrences.
   *
   * @return the occurrences
   */
  public DocumentRange[] getOccurrences() {
    return l_occurrences.toArray(new DocumentRange[l_occurrences.size()]);
  }

  /**
   * Next occurrence.
   */
  public void nextOccurrence() {
    if (!hasOccurrences())return;

    int actualPosition = textArea.getCaretPosition();
    int result = Integer.MAX_VALUE;

    //Collections.sort(l_occurrences);

    for (int i = l_occurrences.size() - 1; i >= 0; i--) {
      int aa = l_occurrences.get(i).getStartOffset();
      if (aa > actualPosition) {
        result = Math.min(result, aa);
      }
      else {
        break;
      }
    }

    if (getOccurrencesCircularNavigation() && (result < 0 || result == Integer.MAX_VALUE)) {
      result = l_occurrences.get(0).getStartOffset();
    }

    try {
      if (result >= 0 && result < Integer.MAX_VALUE) {
        textArea.setCaretPosition(result);
        textArea.scrollToMiddle(result);
      }
      else {
        UIManager.getLookAndFeel().provideErrorFeedback(textArea);
      }
    }
    catch (Exception ex) {
      LOGGER.log(Level.WARNING, ex.getMessage(), ex);
    }
  }

  /**
   * Previous occurrence.
   */
  public void previousOccurrence() {
    if (!hasOccurrences())return;

    int actualPosition = textArea.getCaretPosition();
    int result = -1;

    //Collections.sort(l_occurrences);

    for (DocumentRange l_occurrence : l_occurrences) {
      int aa = l_occurrence.getStartOffset();
      if (aa < actualPosition) {
        result = Math.max(result, aa);
      }
      else {
        break;
      }
    }

    if (getOccurrencesCircularNavigation() && (result < 0 || result == Integer.MAX_VALUE)) {
      result = l_occurrences.get(l_occurrences.size() - 1).getStartOffset();
    }

    try {
      if (result >= 0) {
        textArea.setCaretPosition(result);
        textArea.scrollToMiddle(result);
      }
      else {
        UIManager.getLookAndFeel().provideErrorFeedback(textArea);
      }
    }
    catch (Exception ex) {
      LOGGER.log(Level.WARNING, ex.getMessage(), ex);
    }
  }

  /**
   * Configure.
   *
   * @param cfg the cfg
   * @see jnpad.config.Configurable#configure(int)
   */
  @Override
  public void configure(final int cfg) {
    textArea.configure(cfg);
    markStrip.configure(cfg);
    gutterBase.configure(cfg);
    if (gutter     != null) gutter.configure(cfg);
    if (activeLine != null) activeLine.configure(cfg);
    searchHP.configure(cfg);
    occurrenceHP.configure(cfg);
    bracketHP.configure(cfg);

    if ( (cfg & CFG_VIEW) != 0) {
      setOccurrencesHighlighterVisible(Config.OCCURRENCES_HIGHLIGHTER_VISIBLE.getValue());
      setLineNumbersVisible(Config.GUTTER_VISIBLE.getValue());
      setActiveLineVisible(Config.ACTIVE_LINE_VISIBLE.getValue());
      setBookmarkingEnabled(Config.BOOKMARKING_ENABLED.getValue());
      setOccurrencesCircularNavigation(Config.OCCURRENCES_CIRCULAR_NAVIGATION.getValue());
      setBookmarksCircularNavigation(Config.BOOKMARKS_CIRCULAR_NAVIGATION.getValue());
    }

    if (minimapWindow != null) minimapWindow.configure(cfg);
  }

  /**
   * Update controls.
   *
   * @see jnpad.config.Updatable#updateControls()
   */
  @Override
  public void updateControls() {
    updateControls(CTRLS_ALL);
  }

  /**
   * Update controls.
   *
   * @param ctrls the ctrls
   * @see jnpad.config.Updatable#updateControls(int)
   */
  @Override
  public void updateControls(final int ctrls) {
    if ( (ctrls & CTRLS_TEXT_SELECTION) != 0 || (ctrls & CTRLS_EDITABLE) != 0) {
      if ( (ctrls & CTRLS_TEXT_SELECTION) != 0) {
        lastSelectionLength = textArea.getSelectionEnd() - textArea.getSelectionStart();
        buffer.getStatusBar().setSelection(lastSelectionLength);
      }
      ActionManager.INSTANCE.setEnabledBy(isEditable(), hasSelection(), buffer.canUndo(), buffer.canRedo(), buffer.isDirty());
    }

    if ((ctrls & CTRLS_VIEW) != 0) {
      ActionManager.INSTANCE.setEnabled(JNPadActions.ACTION_NAME_LINE_WRAP, ContentTypes.PLAIN == textArea.getContentType());
    }

    if ( (ctrls & CTRLS_OCCURRENCE) != 0) {
      boolean b = hasOccurrences();
      ActionManager.INSTANCE.setEnabled(JNPadActions.ACTION_NAME_NEXT_OCCURRENCE, b);
      ActionManager.INSTANCE.setEnabled(JNPadActions.ACTION_NAME_PREVIOUS_OCCURRENCE, b);

      buffer.getStatusBar().setOccurrences(l_occurrences.size());
    }

    if ( (ctrls & CTRLS_POSITION) != 0) {
      buffer.getStatusBar().setLinePosition(linePosition);
      int pos = textArea.getCaretPosition();
      int len = textArea.getDocument().getLength();
      buffer.getStatusBar().setPosition(pos, len);
      if (pos < len) {
        try {
          buffer.getStatusBar().setCharacter(textArea.getText(pos, 1).charAt(0));
        }
        catch (BadLocationException blex) {
          buffer.clearStatusMessageCharacter();
        }
      }
      else {
        buffer.clearStatusMessageCharacter();
      }
    }

    if ((ctrls & CTRLS_SPLITTING) != 0) {
      if (minimapWindow != null) {
        //TODO
      }
    }

    markStrip.updateControls(ctrls);
    gutterBase.updateControls(ctrls);
  }

  /**
   * Gets the text.
   *
   * @return the text
   */
  public String getText() {
    return textArea.getText();
  }

  /**
   * Sets the text.
   *
   * @param text the new text
   */
  public void setText(String text) {
    textArea.setText(text);
    textArea.setCaretPosition(0);
  }

  /**
   * Checks for selection.
   *
   * @return true, if successful
   */
  public boolean hasSelection() {
    return textArea.hasSelection();
  }

  /**
   * Checks if is editable.
   *
   * @return true, if is editable
   */
  public boolean isEditable() {
    return textArea.isEditable();
  }

  /**
   * Sets the editable.
   *
   * @param b the new editable
   */
  public void setEditable(boolean b) {
    textArea.setEditable(b);
    actions.setEditable(b);
  }

  /**
   * Gets the editor font.
   *
   * @return the editor font
   */
  public Font getEditorFont() {
    return textArea.getFont();
  }

  /**
   * Sets the editor font.
   *
   * @param f the new editor font
   */
  public void setEditorFont(Font f) {
    textArea.setFont(f);
  }

  /**
   * Gets the line wrap.
   *
   * @return the line wrap
   */
  public boolean getLineWrap() {
    return textArea.getLineWrap();
  }

  /**
   * Sets the line wrap.
   *
   * @param b the new line wrap
   * @see jnpad.text.IView#setLineWrap(boolean)
   */
  @Override
  public void setLineWrap(boolean b) {
    textArea.setLineWrap(b);
    textArea.setWrapStyleWord(b);

    // --- [added v0.3] ---
    if (b)
      tbShowMinimap.setSelected(false);
    tbShowMinimap.setEnabled(!b);
    // ---
  }

  /**
   * Sets the right margin line visible.
   *
   * @param b the new right margin line visible
   */
  public void setRightMarginLineVisible(boolean b) {
    textArea.setRightMarginLineVisible(b);
  }

  /**
   * Checks if is right margin line visible.
   *
   * @return true, if is right margin line visible
   */
  public boolean isRightMarginLineVisible() {
    return textArea.isRightMarginLineVisible();
  }

  /**
   * Sets the mark strip visible.
   *
   * @param b the new mark strip visible
   * @see jnpad.text.IView#setMarkStripVisible(boolean)
   */
  @Override
  public void setMarkStripVisible(boolean b) {
    if (isMarkStripVisible != b) {
      markStrip.setVisible(b);
      isMarkStripVisible = b;
    }
  }

  /**
   * Checks if is mark strip visible.
   *
   * @return true, if is mark strip visible
   */
  public boolean isMarkStripVisible() {
    return isMarkStripVisible;
  }
  
  /**
   *
   * @return LineNumbers
   */
  public Gutter getLineNumbers() {return gutter;}

  /**
   * Gets the text area.
   *
   * @return the text area
   */
  public JNPadTextArea getTextArea() {return textArea;}
  
  /**
   * Checks if is line numbers visible.
   *
   * @return true, if is line numbers visible
   */
  public boolean isLineNumbersVisible() {
    return isGutterVisible;
  }

  /**
   * Sets the line numbers visible.
   *
   * @param b the new line numbers visible
   * @see jnpad.text.IView#setLineNumbersVisible(boolean)
   */
  @Override
  public void setLineNumbersVisible(boolean b) {
    if (isGutterVisible != b) {
      doSetLineNumbersVisible(b);
      isGutterVisible = b;
      revalidate();
      repaint();
    }
  }
  
  /**
   * Do set line numbers visible.
   *
   * @param b the b
   */
  private void doSetLineNumbersVisible(boolean b) {
    if (b) {
      if (gutter == null)
        gutter = new Gutter(textArea);
      scrollPane.setRowHeaderView(gutter);
    }
    else
      scrollPane.setRowHeader(originalRowHeader);
    
    // --- [added v0.3] ---
    if(minimapWindow != null)
      minimapWindow.setLineNumbersVisible(b);
    // ---
  }

  /**
   * Repaint gutter.
   */
  void repaintGutter() {
    if (gutter != null)        gutter.repaint();
    if (minimapWindow != null) minimapWindow.repaintGutter(); // [added v0.3]
  }

  /**
   * Checks if is active line visible.
   *
   * @return true, if is active line visible
   */
  public boolean isActiveLineVisible() {
    return isActiveLineVisible;
  }

  /**
   * Sets the active line visible.
   *
   * @param b the new active line visible
   * @see jnpad.text.IView#setActiveLineVisible(boolean)
   */
  @Override
  public void setActiveLineVisible(boolean b) {
    if (isActiveLineVisible != b) {
      doSetActiveLineVisible(b);
      isActiveLineVisible = b;
      revalidate();
      repaint();
    }
  }

  /**
   * Do set active line visible.
   *
   * @param b the b
   */
  private void doSetActiveLineVisible(boolean b) {
    if (b) {
      if (activeLine == null)
        activeLine = new ActiveLine(textArea);
      else
        activeLine.setVisible(true);
    }
    else if (activeLine != null)
      activeLine.setVisible(false);

    // --- [added v0.3] ---
    if(minimapWindow != null)
      minimapWindow.setActiveLineVisible(b);
    // ---
  }
  
  /**
   * Checks if is occurrences highlighter visible.
   *
   * @return true, if is occurrences highlighter visible
   */
  public boolean isOccurrencesHighlighterVisible() {
    return isOccurrencesHighlighterVisible;
  }

  /**
   * Sets the occurrences highlighter visible.
   *
   * @param b the new occurrences highlighter visible
   * @see jnpad.text.IView#setOccurrencesHighlighterVisible(boolean)
   */
  @Override
  public void setOccurrencesHighlighterVisible(boolean b) {
    if (isOccurrencesHighlighterVisible != b) {
      if (b) {
        doHighlightAllOccurrences(false, false);
      }
      else {
        clearAllOccurrences();
      }
      actions.highlightAllOccurrencesAction.setEnabled(b);
      actions.nextOccurrenceAction.setEnabled(b);
      actions.previousOccurrenceAction.setEnabled(b);
      isOccurrencesHighlighterVisible = b;
    }
  }

  /**
   * Checks if is bracket highlighter visible.
   *
   * @return true, if is bracket highlighter visible
   */
  public boolean isBracketHighlighterVisible() {
    return isBracketHighlighterVisible;
  }

  /**
   * Sets the bracket highlighter visible.
   *
   * @param b the new bracket highlighter visible
   * @see jnpad.text.IView#setBracketHighlighterVisible(boolean)
   */
  @Override
  public void setBracketHighlighterVisible(boolean b) {
    if (isBracketHighlighterVisible == b)
      return;

    label0: {
      if (bracketHighlightTag != null) {
        if (b) {
          break label0;
        }
        textArea.getHighlighter().removeHighlight(bracketHighlightTag);
        bracketHighlightTag = null;
      }
      else if (b) {
        try {
          bracketHighlightTag = textArea.getHighlighter().addHighlight(0, 0, bracketHP);
        }
        catch (BadLocationException ex) {
          LOGGER.log(Level.WARNING, ex.getMessage(), ex);
        }
      }

      if (otherBracketHighlightTag != null) {
        if (b) {
          break label0;
        }
        textArea.getHighlighter().removeHighlight(otherBracketHighlightTag);
        otherBracketHighlightTag = null;
      }
      else if (b) {
        try {
          otherBracketHighlightTag = textArea.getHighlighter().addHighlight(0, 0, bracketHP);
        }
        catch (BadLocationException ex) {
          LOGGER.log(Level.WARNING, ex.getMessage(), ex);
        }
      }
    }

    isBracketHighlighterVisible = b;
  }

  /**
   * Update bracket highlighters.
   */
  void updateBracketHighlighters() {
    Document doc = textArea.getDocument();
    int caretPosition = textArea.getCaretPosition();

    // Set the bracket highlight.
    try {
      int offset = -1;
      char bracket = ' ';
      Segment segment = new Segment();

      if (caretPosition >= 0 && offset < 0) {
        doc.getText(caretPosition, 1, segment);
        bracket = segment.array[segment.offset];
        switch (bracket) {
          case '(':
          case ')':
          case '{':
          case '}':
          case '[':
          case ']':
            offset = caretPosition;
            break;
          default: //Keep FindBugs happy
            break;
        }
      }

      if (caretPosition > 0 && offset < 0) {
        doc.getText(caretPosition - 1, 1, segment);
        bracket = segment.array[segment.offset];
        switch (bracket) {
          case '(':
          case ')':
          case '{':
          case '}':
          case '[':
          case ']':
            offset = caretPosition - 1;
            break;
          default: //Keep FindBugs happy
            break;
        }
      }

      int iOtherOffset = -1;
      if (offset >= 0) {
        switch (bracket) {
          case '(':
            iOtherOffset = TextUtilities.locateBracketForward(doc, offset, '(', ')');
            break;
          case ')':
            iOtherOffset = TextUtilities.locateBracketBackward(doc, offset, '(', ')');
            break;
          case '[':
            iOtherOffset = TextUtilities.locateBracketForward(doc, offset, '[', ']');
            break;
          case ']':
            iOtherOffset = TextUtilities.locateBracketBackward(doc, offset, '[', ']');
            break;
          case '{':
            iOtherOffset = TextUtilities.locateBracketForward(doc, offset, '{', '}');
            break;
          case '}':
            iOtherOffset = TextUtilities.locateBracketBackward(doc, offset, '{', '}');
            break;
          default: //Keep FindBugs happy
            break;
        }
        
        // Set the highlight on the bracket if the other bracket was found.
        if (iOtherOffset >= 0) {
          setHighlightedBracket(offset);
          setOtherHighlightedBracket(iOtherOffset);
          int iBracketLine = textArea.getLineOfOffset(offset);
          int iOtherBracketLine = textArea.getLineOfOffset(iOtherOffset);
          bracketLine = Math.min(iBracketLine, iOtherBracketLine);
          otherBracketLine = Math.max(iBracketLine, iOtherBracketLine);
        }
        else {
          setHighlightedBracket(-1);
          setOtherHighlightedBracket(-1);
          bracketLine = -1;
          otherBracketLine = -1;
        }
      }
      else {
        setHighlightedBracket(-1);
        setOtherHighlightedBracket(-1);
        bracketLine = -1;
        otherBracketLine = -1;
      }
    }
    catch (BadLocationException ex) {
      LOGGER.log(Level.WARNING, ex.getMessage(), ex);
    }
  }
  
  /**
   * Sets the highlighted bracket.
   *
   * @param pos the new highlighted bracket
   */
  void setHighlightedBracket(int pos) {
    if (bracketHighlightTag == null)
      return;

    if (pos == lastBracketPosition)
      return;

    lastBracketPosition = pos;

    try {
      if (pos == -1) {
        textArea.getHighlighter().changeHighlight(bracketHighlightTag, 0, 0);
      }
      else {
        textArea.getHighlighter().changeHighlight(bracketHighlightTag, pos, pos + 1);
      }
    }
    catch (BadLocationException ex) {
      LOGGER.log(Level.WARNING, ex.getMessage(), ex);
    }

    repaintGutter();
  }

  /**
   * Sets the other highlighted bracket.
   *
   * @param pos the new other highlighted bracket
   */
  void setOtherHighlightedBracket(int pos) {
    if (otherBracketHighlightTag == null)
      return;

    if (pos == lastOtherBracketPosition)
      return;

    lastOtherBracketPosition = pos;

    try {
      if (pos == -1) {
        textArea.getHighlighter().changeHighlight(otherBracketHighlightTag, 0, 0);
      }
      else {
        textArea.getHighlighter().changeHighlight(otherBracketHighlightTag, pos, pos + 1);
      }
    }
    catch (BadLocationException ex) {
      LOGGER.log(Level.WARNING, ex.getMessage(), ex);
    }

    repaintGutter();
  }
  
  /**
   * Gets the bracket line index.
   *
   * @return the bracket line index
   */
  int getBracketLineIndex() {
    return bracketLine;
  }

  /**
   * Gets the other bracket line index.
   *
   * @return the other bracket line index
   */
  int getOtherBracketLineIndex() {
    return otherBracketLine;
  }
  
  /**
   * Request focus.
   *
   * @see javax.swing.JComponent#requestFocus()
   */
  @Override
  public void requestFocus() {
    textArea.requestFocus();
  }

  /**
   * Request focus in window.
   *
   * @return true, if successful
   * @see javax.swing.JComponent#requestFocusInWindow()
   */
  @Override
  public boolean requestFocusInWindow() {
    return textArea.requestFocusInWindow();
  }

}
