/*
 * View.java - jEdit view
 * :tabSize=4:indentSize=4:noTabs=false:
 * :folding=explicit:collapseFolds=1:
 *
 * Copyright (C) 1998, 2004 Slava Pestov
 *
 * 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 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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

package org.gjt.sp.jedit;

//{{{ Imports
import java.awt.*;
import java.awt.Desktop.Action;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.IOException;
import java.io.StreamTokenizer;
import java.io.StringReader;
import java.net.Socket;
import java.util.*;
import java.util.List;
import java.util.function.Consumer;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSplitPane;
import javax.swing.LayoutFocusTraversalPolicy;
import javax.swing.MenuSelectionManager;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;

import org.gjt.sp.jedit.manager.BufferManagerImpl;
import org.jedit.options.CombinedOptions;
import org.gjt.sp.jedit.EditBus.EBHandler;
import org.gjt.sp.jedit.bufferset.BufferSet;
import org.gjt.sp.jedit.bufferset.BufferSetManager;
import org.gjt.sp.jedit.gui.AboutDialog;
import org.gjt.sp.jedit.gui.ActionBar;
import org.gjt.sp.jedit.gui.CloseDialog;
import org.gjt.sp.jedit.gui.DefaultInputHandler;
import org.gjt.sp.jedit.gui.DockableWindowFactory;
import org.gjt.sp.jedit.gui.DockableWindowManager;
import org.gjt.sp.jedit.gui.HistoryModel;
import org.gjt.sp.jedit.gui.DockingFrameworkProvider;
import org.gjt.sp.jedit.gui.InputHandler;
import org.gjt.sp.jedit.gui.StatusBar;
import org.gjt.sp.jedit.gui.ToolBarManager;
import org.gjt.sp.jedit.gui.VariableGridLayout;
import org.gjt.sp.jedit.gui.DockableWindowManager.DockingLayout;
import org.gjt.sp.jedit.input.InputHandlerProvider;
import org.gjt.sp.jedit.manager.BufferManager;
import org.gjt.sp.jedit.manager.ViewManager;
import org.gjt.sp.jedit.msg.BufferUpdate;
import org.gjt.sp.jedit.msg.EditPaneUpdate;
import org.gjt.sp.jedit.msg.PropertiesChanged;
import org.gjt.sp.jedit.msg.SearchSettingsChanged;
import org.gjt.sp.jedit.msg.ViewUpdate;
import org.gjt.sp.jedit.options.GeneralOptionPane;
import org.gjt.sp.jedit.search.CurrentBufferSet;
import org.gjt.sp.jedit.search.SearchAndReplace;
import org.gjt.sp.jedit.search.SearchBar;
import org.gjt.sp.jedit.textarea.JEditTextArea;
import org.gjt.sp.jedit.textarea.ScrollListener;
import org.gjt.sp.jedit.textarea.TextArea;
import org.gjt.sp.jedit.visitors.JEditVisitor;
import org.gjt.sp.jedit.visitors.JEditVisitorAdapter;
import org.gjt.sp.util.GenericGUIUtilities;
import org.gjt.sp.util.Log;
import org.gjt.sp.util.StandardUtilities;
//}}}

/**
 * A <code>View</code> is jEdit's top-level frame window.<p>
 *
 * In a BeanShell script, you can obtain the current view instance from the
 * <code>view</code> variable.<p>
 *
 * The largest component it contains is an {@link EditPane} that in turn
 * contains a {@link org.gjt.sp.jedit.textarea.JEditTextArea} that displays a
 * {@link Buffer}.
 * A view can have more than one edit pane in a split window configuration.
 * A view also contains a menu bar, an optional toolbar and other window
 * decorations, as well as docked windows.<p>
 *
 * The <b>View</b> class performs two important operations
 * dealing with plugins: creating plugin menu items, and managing dockable
 * windows.
 *
 * <ul>
 * <li>When a view is being created, its initialization routine
 * iterates through the collection of loaded plugins and constructs the
 * <b>Plugins</b> menu using the properties as specified in the
 * {@link EditPlugin} class.</li>
 * <li>The view also creates and initializes a
 * {@link org.gjt.sp.jedit.gui.DockableWindowManager}
 * object.  This object is
 * responsible for creating, closing and managing dockable windows.</li>
 * </ul>
 *
 * This class does not have a public constructor.
 * Views can be opened and closed using methods in the <code>jEdit</code>
 * class.
 *
 * @see org.gjt.sp.jedit.jEdit#newView(View)
 * @see org.gjt.sp.jedit.jEdit#newView(View,Buffer)
 * @see org.gjt.sp.jedit.jEdit#newView(View,Buffer,boolean)
 * @see org.gjt.sp.jedit.jEdit#closeView(View)
 *
 * @author Slava Pestov
 * @author John Gellene (API documentation)
 * @version $Id: View.java 25698 2023-11-17 01:31:50Z vampire0 $
 */
public class View extends JFrame implements InputHandlerProvider
{
	public static final EditPane[] EMPTY_EDIT_PANES_ARRAY = new EditPane[0];
	//{{{ User interface

	//{{{ ToolBar-related constants

	public static final String VIEW_DOCKING_FRAMEWORK_PROPERTY = "view.docking.framework";
	private static final String ORIGINAL_DOCKING_FRAMEWORK = "Original";
	public static final String DOCKING_FRAMEWORK_PROVIDER_SERVICE =
		"org.gjt.sp.jedit.gui.DockingFrameworkProvider";
	private static DockingFrameworkProvider dockingFrameworkProvider;

	//{{{ Groups
	/**
	 * The group of tool bars above the DockableWindowManager
	 * @see #addToolBar(int,int,java.awt.Component)
	 * @since jEdit 4.0pre7
	 */
	public static final int TOP_GROUP = 0;

	/**
	 * The group of tool bars below the DockableWindowManager
	 * @see #addToolBar(int,int,java.awt.Component)
	 * @since jEdit 4.0pre7
	 */
	public static final int BOTTOM_GROUP = 1;
	public static final int DEFAULT_GROUP = TOP_GROUP;
	//}}}

	//{{{ Layers

	// Common layers
	/**
	 * The highest possible layer.
	 * @see #addToolBar(int,int,java.awt.Component)
	 * @since jEdit 4.0pre7
	 */
	public static final int TOP_LAYER = Integer.MAX_VALUE;

	/**
	 * The default layer for tool bars with no preference.
	 * @see #addToolBar(int,int,java.awt.Component)
	 * @since jEdit 4.0pre7
	 */
	public static final int DEFAULT_LAYER = 0;

	/**
	 * The lowest possible layer.
	 * @see #addToolBar(int,int,java.awt.Component)
	 * @since jEdit 4.0pre7
	 */
	public static final int BOTTOM_LAYER = Integer.MIN_VALUE;

	// Layers for top group
	/**
	 * Above system tool bar layer.
	 * @see #addToolBar(int,int,java.awt.Component)
	 * @since jEdit 4.0pre7
	 */
	public static final int ABOVE_SYSTEM_BAR_LAYER = 150;

	/**
	 * System tool bar layer.
	 * jEdit uses this for the main tool bar.
	 * @see #addToolBar(int,int,java.awt.Component)
	 * @since jEdit 4.0pre7
	 */
	public static final int SYSTEM_BAR_LAYER = 100;

	/**
	 * Below system tool bar layer.
	 * @see #addToolBar(int,int,java.awt.Component)
	 * @since jEdit 4.0pre7
	 */
	public static final int BELOW_SYSTEM_BAR_LAYER = 75;

	/**
	 * Search bar layer.
	 * @see #addToolBar(int,int,java.awt.Component)
	 * @since jEdit 4.0pre7
	 */
	public static final int SEARCH_BAR_LAYER = 75;

	/**
	 * Below search bar layer.
	 * @see #addToolBar(int,int,java.awt.Component)
	 * @since jEdit 4.0pre7
	 */
	public static final int BELOW_SEARCH_BAR_LAYER = 50;

	// Layers for bottom group
	/**
	 * Action bar layer.
	 * @see #addToolBar(int,int,java.awt.Component)
	 * @since jEdit 4.2pre1
	 */
	public static final int ACTION_BAR_LAYER = -75;

	/**
	 * Status bar layer.
	 * @see #addToolBar(int,int,java.awt.Component)
	 * @since jEdit 4.2pre1
	 */
	public static final int STATUS_BAR_LAYER = -100;

	/**
	 * Status bar layer.
	 * @see #addToolBar(int,int,java.awt.Component)
	 * @since jEdit 4.2pre1
	 */
	public static final int BELOW_STATUS_BAR_LAYER = -150;
	//}}}

	//}}}

	//{{{ getDockableWindowManager() method
	/**
	 * Returns the dockable window manager associated with this view.
	 * @since jEdit 2.6pre3
	 */
	public DockableWindowManager getDockableWindowManager()
	{
		return dockableWindowManager;
	} //}}}

	//{{{ getDockingFrameworkName() method
	public static String getDockingFrameworkName()
	{
		String framework = jEdit.getProperty(
				VIEW_DOCKING_FRAMEWORK_PROPERTY, ORIGINAL_DOCKING_FRAMEWORK);
		return framework;
	} //}}}

	//{{{ getDockingFrameworkProvider() method
	public static DockingFrameworkProvider getDockingFrameworkProvider()
	{
		if (dockingFrameworkProvider == null)
		{
			String framework = getDockingFrameworkName();
			dockingFrameworkProvider = (DockingFrameworkProvider)
				ServiceManager.getService(
					DOCKING_FRAMEWORK_PROVIDER_SERVICE, framework);

			if (dockingFrameworkProvider == null)
			{
				Log.log(Log.ERROR, View.class, "No docking framework " + framework +
								   " available, using the original one");
				dockingFrameworkProvider = (DockingFrameworkProvider)
				ServiceManager.getService(
					DOCKING_FRAMEWORK_PROVIDER_SERVICE, ORIGINAL_DOCKING_FRAMEWORK);
			}
		}
		return dockingFrameworkProvider;
	} //}}}

	//{{{ getToolBar() method
	/**
	 * Returns the view's tool bar.
	 * @since jEdit 4.2pre1
	 */
	public Container getToolBar()
	{
		return toolBar;
	} //}}}

	//{{{ addToolBar() methods
	/**
	 * Adds a tool bar to this view.
	 * @param toolBar The tool bar
	 */
	public void addToolBar(Component toolBar)
	{
		addToolBar(DEFAULT_GROUP, DEFAULT_LAYER, toolBar);
	}

	/**
	 * Adds a tool bar to this view.
	 * @param group The tool bar group to add to
	 * @param toolBar The tool bar
	 * @see org.gjt.sp.jedit.gui.ToolBarManager
	 * @since jEdit 4.0pre7
	 */
	public void addToolBar(int group, Component toolBar)
	{
		addToolBar(group, DEFAULT_LAYER, toolBar);
	}

	/**
	 * Adds a tool bar to this view.
	 * @param group The tool bar group to add to
	 * @param layer The layer of the group to add to
	 * @param toolBar The tool bar
	 * @see org.gjt.sp.jedit.gui.ToolBarManager
	 * @since jEdit 4.0pre7
	 */
	public void addToolBar(int group, int layer, Component toolBar)
	{
		toolBarManager.addToolBar(group, layer, toolBar);
		getRootPane().revalidate();
	} //}}}

	//{{{ removeToolBar() method
	/**
	 * Removes a tool bar from this view.
	 * @param toolBar The tool bar
	 */
	public void removeToolBar(Component toolBar)
	{
		if (toolBarManager == null) return;
		if (toolBar == null) return;
		toolBarManager.removeToolBar(toolBar);
		getRootPane().revalidate();
	} //}}}

	//{{{ showWaitCursor() method
	/**
	 * Shows the wait cursor. This method and
	 * {@link #hideWaitCursor()} are implemented using a reference
	 * count of requests for wait cursors, so that nested calls work
	 * correctly; however, you should be careful to use these methods in
	 * tandem.<p>
	 *
	 * To ensure that {@link #hideWaitCursor()} is always called
	 * after a {@link #showWaitCursor()}, use a
	 * <code>try</code>/<code>finally</code> block, like this:
	 * <pre>try
	 *{
	 *    view.showWaitCursor();
	 *    // ...
	 *}
	 *finally
	 *{
	 *    view.hideWaitCursor();
	 *}</pre>
	 */
	public synchronized void showWaitCursor()
	{
		if(waitCount++ == 0)
		{
			Cursor cursor = Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR);
			setCursor(cursor);
			visit(new SetCursorVisitor(cursor));
		}
	} //}}}

	//{{{ hideWaitCursor() method
	/**
	 * Hides the wait cursor.
	 */
	public synchronized void hideWaitCursor()
	{
		if(waitCount > 0)
			waitCount--;

		if(waitCount == 0)
		{
			// still needed even though glass pane
			// has a wait cursor
			Cursor cursor = Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR);
			setCursor(cursor);

			visit(new SetCursorVisitor(cursor));
		}
	} //}}}

	//{{{ getSearchBar() method
	/**
	 * Returns the search bar.
	 * @since jEdit 2.4pre4
	 */
	public final SearchBar getSearchBar()
	{
		return searchBar;
	} //}}}

	//{{{ getActionBar() method
	/**
	 * Returns the action bar.
	 * @since jEdit 4.2pre3
	 */
	public final ActionBar getActionBar()
	{
		return actionBar;
	} //}}}

	//{{{ getStatus() method
	/**
	 * Returns the status bar. The
	 * {@link org.gjt.sp.jedit.gui.StatusBar#setMessage(String)} and
	 * {@link org.gjt.sp.jedit.gui.StatusBar#setMessageAndClear(String)} methods can
	 * be called on the return value of this method to display status
	 * information to the user.
	 * @since jEdit 3.2pre2
	 */
	public StatusBar getStatus()
	{
		return status;
	} //}}}

	//{{{ quickIncrementalSearch() method
	/**
	 * Quick search.
	 * @since jEdit 4.0pre3
	 */
	public void quickIncrementalSearch(boolean word)
	{
		if(searchBar == null)
			searchBar = new SearchBar(this,true);
		if(searchBar.getParent() == null)
			addToolBar(TOP_GROUP,SEARCH_BAR_LAYER,searchBar);

		searchBar.setHyperSearch(false);

		JEditTextArea textArea = getTextArea();

		if(word)
		{
			String text = textArea.getSelectedText();
			if(text == null)
			{
				textArea.selectWord();
				text = textArea.getSelectedText();
			}
			else if(text.indexOf('\n') != -1)
				text = null;

			if(text != null && SearchAndReplace.getRegexp())
				text = SearchAndReplace.escapeRegexp(text,false);

			searchBar.getField().setText(text);
		}

		searchBar.getField().requestFocus();
		searchBar.getField().selectAll();
	} //}}}

	//{{{ quickHyperSearch() method
	/**
	 * Quick HyperSearch.
	 * @since jEdit 4.0pre3
	 */
	public void quickHyperSearch(boolean word)
	{
		JEditTextArea textArea = getTextArea();

		if(word)
		{
			String text = textArea.getSelectedText();
			if(text == null)
			{
				textArea.selectWord();
				text = textArea.getSelectedText();
			}

			if(text != null && text.indexOf('\n') == -1)
			{
				if(SearchAndReplace.getRegexp())
				{
					text = SearchAndReplace.escapeRegexp(
						text,false);
				}

				HistoryModel.getModel("find").addItem(text);
				SearchAndReplace.setSearchString(text);
				SearchAndReplace.setSearchFileSet(new CurrentBufferSet());
				SearchAndReplace.hyperSearch(this);

				return;
			}
		}

		if(searchBar == null)
			searchBar = new SearchBar(this,true);
		if(searchBar.getParent() == null)
			addToolBar(TOP_GROUP,SEARCH_BAR_LAYER,searchBar);

		searchBar.setHyperSearch(true);
		searchBar.getField().setText(null);
		searchBar.getField().requestFocus();
		searchBar.getField().selectAll();
	} //}}}

	//{{{ actionBar() method
	/**
	 * Shows the action bar if needed, and sends keyboard focus there.
	 * @since jEdit 4.2pre1
	 */
	public void actionBar()
	{
		if(actionBar == null)
			actionBar = new ActionBar(this,true);
		if(actionBar.getParent() == null)
			addToolBar(BOTTOM_GROUP,ACTION_BAR_LAYER,actionBar);

		actionBar.goToActionBar();
	} //}}}

	//}}}

	//{{{ Input handling

	//{{{ getKeyEventInterceptor() method
	/**
	 * Returns the listener that will handle all key events in this
	 * view, if any.
	 * @return the key event interceptor or null
	 */
	public KeyListener getKeyEventInterceptor()
	{
		return inputHandler.getKeyEventInterceptor();
	} //}}}

	//{{{ setKeyEventInterceptor() method
	/**
	 * Sets the listener that will handle all key events in this
	 * view. For example, the complete word command uses this so
	 * that all key events are passed to the word list popup while
	 * it is visible.
	 * @param listener The key event interceptor.
	 */
	public void setKeyEventInterceptor(KeyListener listener)
	{
		inputHandler.setKeyEventInterceptor(listener);
	} //}}}

	//{{{ getInputHandler() method
	/**
	 * Returns the input handler.
	 */
	@Override
	public InputHandler getInputHandler()
	{
		return inputHandler;
	} //}}}



	//{{{ setInputHandler() method
	/**
	 * Sets the input handler.
	 * @param inputHandler The new input handler
	 */
	public void setInputHandler(InputHandler inputHandler)
	{
		this.inputHandler = inputHandler;
	} //}}}

	//{{{ getMacroRecorder() method
	/**
	 * Returns the macro recorder.
	 */
	public Macros.Recorder getMacroRecorder()
	{
		return recorder;
	} //}}}

	//{{{ setMacroRecorder() method
	/**
	 * Sets the macro recorder.
	 * @param recorder The macro recorder
	 */
	public void setMacroRecorder(Macros.Recorder recorder)
	{
		this.recorder = recorder;
	} //}}}

	//{{{ processKeyEvent() method
	/**
	 * Forwards key events directly to the input handler.
	 * This is slightly faster than using a KeyListener
	 * because some Swing overhead is avoided.
	 */
	@Override
	public void processKeyEvent(KeyEvent evt)
	{
		inputHandler.processKeyEvent(evt,VIEW, false);
		if(!evt.isConsumed())
			super.processKeyEvent(evt);
	} //}}}

	//{{{ processKeyEvent() method
	/**
	 * Forwards key events directly to the input handler.
	 * This is slightly faster than using a KeyListener
	 * because some Swing overhead is avoided.
	 */
	public void processKeyEvent(KeyEvent evt, boolean calledFromTextArea)
	{
		processKeyEvent(evt,calledFromTextArea
			? TEXT_AREA
			: VIEW);
	} //}}}

	//{{{ processKeyEvent() method
	public static final int VIEW = 0;
	public static final int TEXT_AREA = 1;
	public static final int ACTION_BAR = 2;
	/**
	 * Forwards key events directly to the input handler.
	 * This is slightly faster than using a KeyListener
	 * because some Swing overhead is avoided.
	 */
	public void processKeyEvent(KeyEvent evt, int from)
	{
		inputHandler.processKeyEvent(evt, from, false);
		if(!evt.isConsumed())
			super.processKeyEvent(evt);
	}
	//}}}

	//}}}

	//{{{ Buffers, edit panes, split panes

	//{{{ splitHorizontally() method
	/**
	 * Splits the view horizontally.
	 * @return the new editPane
	 * @since jEdit 4.1pre2
	 */
	public EditPane splitHorizontally()
	{
		return split(JSplitPane.VERTICAL_SPLIT);
	} //}}}

	//{{{ splitVertically() method
	/**
	 * Splits the view vertically.
	 * @return the new editPane
	 * @since jEdit 4.1pre2
	 */
	public EditPane splitVertically()
	{
		return split(JSplitPane.HORIZONTAL_SPLIT);
	} //}}}

	//{{{ split() method
	/**
	 * Splits the view.
	 * @param orientation the orientation {@link javax.swing.JSplitPane#HORIZONTAL_SPLIT} or
	 * {@link javax.swing.JSplitPane#VERTICAL_SPLIT}
	 * @return the new editPane
	 * @since jEdit 4.1pre2
	 */
	public EditPane split(int orientation)
	{
		PerspectiveManager.setPerspectiveDirty(true);

		editPane.saveCaretInfo();
		EditPane oldEditPane = editPane;
		EditPane newEditPane = createEditPane(oldEditPane.getBufferSet(), oldEditPane.getBuffer());
//		setEditPane(newEditPane);
		newEditPane.loadCaretInfo();

		JComponent oldParent = (JComponent)oldEditPane.getParent();

		final JSplitPane newSplitPane = new JSplitPane(orientation);
		newSplitPane.setOneTouchExpandable(true);
		newSplitPane.setBorder(null);
		newSplitPane.setMinimumSize(new Dimension(0,0));
		newSplitPane.setResizeWeight(0.5);

		int parentSize = orientation == JSplitPane.VERTICAL_SPLIT
			? oldEditPane.getHeight() : oldEditPane.getWidth();
		final int dividerPosition = (int)((parentSize
			- newSplitPane.getDividerSize()) * 0.5);
		newSplitPane.setDividerLocation(dividerPosition);

		if(oldParent instanceof JSplitPane)
		{
			JSplitPane oldSplitPane = (JSplitPane)oldParent;
			int dividerPos = oldSplitPane.getDividerLocation();

			Component left = oldSplitPane.getLeftComponent();

			if(left == oldEditPane)
				oldSplitPane.setLeftComponent(newSplitPane);
			else
				oldSplitPane.setRightComponent(newSplitPane);

			newSplitPane.setLeftComponent(oldEditPane);
			newSplitPane.setRightComponent(newEditPane);

			oldSplitPane.setDividerLocation(dividerPos);
		}
		else
		{
			splitPane = newSplitPane;

			newSplitPane.setLeftComponent(oldEditPane);
			newSplitPane.setRightComponent(newEditPane);

			setMainContent(newSplitPane);

		}

		EventQueue.invokeLater(() -> newSplitPane.setDividerLocation(dividerPosition));

		newEditPane.focusOnTextArea();

		return newEditPane;
	} //}}}

	//{{{ unsplit() method
	/**
	 * Unsplits the view.
	 * @since jEdit 2.3pre2
	 */
	public void unsplit()
	{
		if(splitPane != null)
		{
			lastSplitConfig = getSplitConfig();

			PerspectiveManager.setPerspectiveDirty(true);
			BufferSet.Scope scope = jEdit.getBufferSetManager().getScope();
			for(EditPane _editPane: getEditPanes())
			{
				if(editPane != _editPane)
				{
					if (scope == BufferSet.Scope.editpane)
						mergeBufferSets(editPane, _editPane);
					_editPane.close();
				}
			}

			setMainContent(editPane);

			splitPane = null;
			updateTitle();

			editPane.focusOnTextArea();
		}
		else
			javax.swing.UIManager.getLookAndFeel().provideErrorFeedback(null);
	} //}}}

	//{{{ unsplitCurrent() method
	/**
	 * Removes the current split.
	 * @since jEdit 2.3pre2
	 */
	public void unsplitCurrent()
	{
		if(splitPane != null)
		{
			lastSplitConfig = getSplitConfig();

			PerspectiveManager.setPerspectiveDirty(true);

			// find first split pane parenting current edit pane
			Component comp = editPane;
			while(!(comp instanceof JSplitPane) && comp != null)
			{
				comp = comp.getParent();
			}

			BufferSet.Scope scope = jEdit.getBufferSetManager().getScope();
			// get rid of any edit pane that is a child
			// of the current edit pane's parent splitter
			for(EditPane _editPane: getEditPanes())
			{
				if(GenericGUIUtilities.isAncestorOf(comp,_editPane)
					&& _editPane != editPane)
				{
					if (scope == BufferSet.Scope.editpane)
						mergeBufferSets(editPane, _editPane);
					_editPane.close();
				}
			}

			JComponent parent = comp == null ? null : (JComponent)comp.getParent();

			if(parent instanceof JSplitPane)
			{
				JSplitPane parentSplit = (JSplitPane)parent;
				int pos = parentSplit.getDividerLocation();
				if(parentSplit.getLeftComponent() == comp)
					parentSplit.setLeftComponent(editPane);
				else
					parentSplit.setRightComponent(editPane);
				parentSplit.setDividerLocation(pos);
				parent.revalidate();
			}
			else
			{
				setMainContent(editPane);
				splitPane = null;
			}

			updateTitle();

			editPane.focusOnTextArea();
		}
		else
			javax.swing.UIManager.getLookAndFeel().provideErrorFeedback(null);
	} //}}}

	//{{{ resplit() method
	/**
	 * Restore the split configuration as it was before unsplitting.
	 *
	 * @since jEdit 4.3pre1
	 */
	public void resplit()
	{
		if(lastSplitConfig == null)
			javax.swing.UIManager.getLookAndFeel().provideErrorFeedback(null);
		else
			setSplitConfig(null,lastSplitConfig);
	} //}}}

	//{{{ getSplitConfig() method
	/**
	*   Split configurations are recorded in a simple RPN "language".
	*   @return The split configuration, describing where splitpanes
	*           are, which buffers are open in each EditPane, etc.
	*
	*/
	public String getSplitConfig()
	{
		StringBuilder splitConfig = new StringBuilder();

		if(splitPane != null)
			getSplitConfig(splitPane,splitConfig);
		else
		{
			appendToSplitConfig(splitConfig, editPane);
		}

		return splitConfig.toString();
	} //}}}

	//{{{ setSplitConfig() method
	/**
	 * sets the split configuration as per the splitConfig.
	 *
	 * @param buffer if null, checks all buffers to restore View's split config.
	 * @param splitConfig the split config, as returned by getSplitConfig()
	 */
	public void setSplitConfig(Buffer buffer, String splitConfig)
	{
		try
		{
			Component comp = restoreSplitConfig(buffer,splitConfig);
			setMainContent(comp);
			updateTitle();
		}
		catch(IOException e)
		{
			// this should never throw an exception.
			throw new InternalError();
		}
	} //}}}

	//{{{ nextTextArea() method
	/**
	 * Moves keyboard focus to the next text area.
	 * @since jEdit 2.7pre4
	 */
	public void nextTextArea()
	{
		EditPane[] editPanes = getEditPanes();
		for(int i = 0; i < editPanes.length; i++)
		{
			if(editPane == editPanes[i])
			{
				if(i == editPanes.length - 1)
					editPanes[0].focusOnTextArea();
				else
					editPanes[i+1].focusOnTextArea();
				break;
			}
		}
	} //}}}

	//{{{ prevTextArea() method
	/**
	 * Moves keyboard focus to the previous text area.
	 * @since jEdit 2.7pre4
	 */
	public void prevTextArea()
	{
		EditPane[] editPanes = getEditPanes();
		for(int i = 0; i < editPanes.length; i++)
		{
			if(editPane == editPanes[i])
			{
				if(i == 0)
					editPanes[editPanes.length - 1].focusOnTextArea();
				else
					editPanes[i-1].focusOnTextArea();
				break;
			}
		}
	} //}}}

	//{{{ getSplitPane() method
	/**
	 * Returns the top-level split pane, if any.
	 * @return the top JSplitPane if any.
	 * @since jEdit 2.3pre2
	 */
	public JSplitPane getSplitPane()
	{
		return splitPane;
	} //}}}

	//{{{ getBuffer() method
	/**
	 * Returns the current edit pane's buffer.
	 * @return the current edit pane's buffer, it can be null
	 */
	public Buffer getBuffer()
	{
		if(editPane == null)
			return null;
		else
			return editPane.getBuffer();
	} //}}}

	//{{{ getBuffers() method
	/**
	 * Returns all Buffers opened in this View,
	 * Sorted according to View options. (as of jEdit 5.2)
	 * With order preserved for unsorted buffersets (as of jEdit 5.3)
	 * @since jEdit 5.1
	 */
	public Buffer[] getBuffers()
	{
		BufferSetManager mgr = jEdit.getBufferSetManager();
		Collection<Buffer> retval = null;
		for (EditPane ep: getEditPanes())
		{
			BufferSet bs = ep.getBufferSet();
			if (retval == null) {
				Comparator<Buffer> sorter = bs.getSorter();
				if (sorter == null)
					retval = new LinkedHashSet<>();
				else
					retval = new TreeSet<>(sorter);
			}
			Collections.addAll(retval, bs.getAllBuffers());
			// If scope is not editpane, then all buffersets
			// are the same and we got what we need.
			if (mgr.getScope() != BufferSet.Scope.editpane)
				break;
		}
		if (retval != null)
			return retval.toArray(BufferManagerImpl.EMPTY_BUFFER_ARRAY);

		return BufferManagerImpl.EMPTY_BUFFER_ARRAY;
	}//}}}

	//{{{ setBuffer() method
	/**
	 * Sets the current edit pane's buffer.
	 * @param buffer The buffer
	 */
	public void setBuffer(Buffer buffer)
	{
		editPane.setBuffer(buffer);
	} //}}}

	//{{{ goToBuffer() method
	/**
	 * If this buffer is open in one of the view's edit panes, sets focus
	 * to that edit pane. Otherwise, opens the buffer in the currently
	 * active edit pane.
	 * @param buffer The buffer
	 * @return the current edit pane
	 * @since jEdit 4.2pre1
	 */
	public EditPane goToBuffer(Buffer buffer)
	{
		return showBuffer(buffer, true);
	} //}}}

	//{{{ showBuffer() method
	/**
	 * If this buffer is open in one of the view's edit panes, activates
	 * that edit pane. Otherwise, opens the buffer in the currently
	 * active edit pane. But the focus is not moved.
	 * @param buffer The buffer to show
	 * @return the current edit pane
	 * @since jEdit 4.3pre13
	 */
	public EditPane showBuffer(Buffer buffer)
	{
		return showBuffer(buffer, false);
	} //}}}

	//{{{ getTextArea() method
	/**
	 * Returns the current edit pane's text area.
	 * @return the current edit pane's text area, or <b>null</b> if there is no edit pane yet
	 */
	public JEditTextArea getTextArea()
	{
		if(editPane == null)
			return null;
		else
			return editPane.getTextArea();
	} //}}}

	//{{{ getEditPane() method
	/**
	 * Returns the current edit pane.
	 * @return the current edit pane
	 * @since jEdit 2.5pre2
	 */
	public EditPane getEditPane()
	{
		return editPane;
	} //}}}

	//{{{ getEditPanes() method
	/**
	 * Returns all edit panes.
	 * @return an array of all edit panes in the view
	 * @since jEdit 2.5pre2
	 */
	public EditPane[] getEditPanes()
	{
		List<EditPane> list = new ArrayList<>();
		forEachEditPane(list::add);
		return list.toArray(EMPTY_EDIT_PANES_ARRAY);
	} //}}}

	/**
	 * Perform the given action on every EditPane
	 *
	 * @param action an action to perform on every EditPane
	 */
	public void forEachEditPane(Consumer<? super EditPane> action)
	{
		Objects.requireNonNull(action);
		if(splitPane == null)
		{
			if (editPane != null)
			{
				// it can be null if it is called during the view creation.
				action.accept(editPane);
			}
		}
		else
		{
			performAction(splitPane, action);
		}
	}

	//{{{ getViewConfig() method
	/**
	 * @return a ViewConfig instance for the current view
	 * @since jEdit 4.2pre1
	 */
	public ViewConfig getViewConfig()
	{
		ViewConfig config = new ViewConfig();
		config.plainView = isPlainView();
		config.splitConfig = getSplitConfig();
		config.extState = getExtendedState();
		config.docking = dockableWindowManager.getDockingLayout(config);
		config.title = userTitle;
		String prefix = config.plainView ? "plain-view" : "view";
		switch (config.extState)
		{
			case Frame.MAXIMIZED_BOTH:
			case Frame.ICONIFIED:
				config.x = jEdit.getIntegerProperty(prefix + ".x",getX());
				config.y = jEdit.getIntegerProperty(prefix + ".y",getY());
				config.width = jEdit.getIntegerProperty(prefix + ".width",getWidth());
				config.height = jEdit.getIntegerProperty(prefix + ".height",getHeight());
				break;

			case Frame.MAXIMIZED_VERT:
				config.x = getX();
				config.y = jEdit.getIntegerProperty(prefix + ".y",getY());
				config.width = getWidth();
				config.height = jEdit.getIntegerProperty(prefix + ".height",getHeight());
				break;

			case Frame.MAXIMIZED_HORIZ:
				config.x = jEdit.getIntegerProperty(prefix + ".x",getX());
				config.y = getY();
				config.width = jEdit.getIntegerProperty(prefix + ".width",getWidth());
				config.height = getHeight();
				break;

			case Frame.NORMAL:
			default:
				config.x = getX();
				config.y = getY();
				config.width = getWidth();
				config.height = getHeight();
				break;
		}
		return config;
	} //}}}

	//}}}

	//{{{ isClosed() method
	/**
	 * Returns true if this view has been closed with
	 * {@link jEdit#closeView(View)}.
	 * @return true if the view is closed
	 */
	public boolean isClosed()
	{
		return closed;
	} //}}}

	//{{{ isPlainView() method
	/**
	 * Returns true if this is an auxilliary view with no dockable windows.
	 * @return true if the view is plain
	 * @since jEdit 4.1pre2
	 */
	public boolean isPlainView()
	{
		return plainView;
	} //}}}

	//{{{ getNext() method
	/**
	 * Returns the next view in the list.
	 * @return the next view
	 */
	public View getNext()
	{
		return next;
	} //}}}

	//{{{ getPrev() method
	/**
	 * Returns the previous view in the list.
	 * @return the preview view
	 */
	public View getPrev()
	{
		return prev;
	} //}}}

	//{{{ setPrev() method

	/**
	 * Set the previous view in linked list.
	 * To be used by {@link BufferManager} only
	 * @since jEdit 5.6pre1
	 * @param prev
	 */
	public void setPrev(View prev)
	{
		this.prev = prev;
	} //}}}

	//{{{ setNext() method

	/**
	 * Set the next view in linked list.
	 * To be used by {@link BufferManager} only
	 * @since jEdit 5.6pre1
	 * @param next
	 */
	public void setNext(View next)
	{
		this.next = next;
	} //}}}

	//{{{ handlePropertiesChanged()
	@EBHandler
	public void handlePropertiesChanged(PropertiesChanged msg)
	{
		propertiesChanged();
	} //}}}

	//{{{ handleSearchSettingsChanged() method
	@EBHandler
	public void handleSearchSettingsChanged(SearchSettingsChanged msg)
	{
		if(searchBar != null)
			searchBar.update();
	} //}}}

	//{{{ getMinimumSize() method
	@Override
	public Dimension getMinimumSize()
	{
		return new Dimension(0,0);
	} //}}}

	//{{{ setWaitSocket() method
	/**
	 * This socket is closed when the buffer is closed.
	 */
	public void setWaitSocket(Socket waitSocket)
	{
		this.waitSocket = waitSocket;
	} //}}}

	//{{{ toString() method
	@Override
	public String toString()
	{
		return getClass().getName() + '['
			+ (jEdit.getActiveView() == this
			? "active" : "inactive")
			+ ']';
	} //}}}

	//{{{ updateTitle() method
	/**
	 * Updates the title bar.
	 */
	public void updateTitle()
	{
		List<Buffer> buffers = new ArrayList<>();
		EditPane[] editPanes = getEditPanes();
		for (EditPane ep : editPanes)
		{
			Buffer buffer = ep.getBuffer();
			if (!buffers.contains(buffer))
				buffers.add(buffer);
		}

		StringBuilder title = new StringBuilder();

		/* On Mac OS X, apps are not supposed to show their name in the
		title bar. */
		if(!OperatingSystem.isMacOS())
		{
			if (userTitle != null)
				title.append(userTitle);
			else
				title.append(jEdit.getProperty("view.title"));
		}

		for(int i = 0; i < buffers.size(); i++)
		{
			if(i != 0)
				title.append(", ");

			Buffer buffer = buffers.get(i);
			title.append(showFullPath && !buffer.isNewFile()
				? buffer.getPath(true) : buffer.getName());
			if(buffer.isDirty())
				title.append(jEdit.getProperty("view.title.dirty"));
		}

		setTitle(title.toString());
	} //}}}

	//{{{ setUserTitle() method
	/**
	 * Sets a user-defined title for this view instead of the "view.title" property.
	 */
	public void setUserTitle(String title)
	{
		userTitle = title + " - ";
		updateTitle();
	} //}}}

	//{{{ showUserTitleDialog() method
	/**
	 * Shows a dialog for selecting a user-defined title for this view.
	 */
	public void showUserTitleDialog()
	{
		String title = JOptionPane.showInputDialog(this, jEdit.getProperty(
			"view.title.select"));
		if (title == null)
			return;
		setUserTitle(title);
	} //}}}

	//{{{ getPrefixFocusOwner() method
	public Component getPrefixFocusOwner()
	{
		return prefixFocusOwner;
	} //}}}

	//{{{ setPrefixFocusOwner() method
	public void setPrefixFocusOwner(Component prefixFocusOwner)
	{
		this.prefixFocusOwner = prefixFocusOwner;
	} //}}}

	//{{{ visit() method
	/**
	 * Visit the the editpanes and textareas of the view
	 * @param visitor the visitor
	 * @since jEdit 4.3pre13
	 */
	public void visit(JEditVisitor visitor)
	{
		EditPane[] panes = getEditPanes();
		for (EditPane editPane : panes)
		{
			visitor.visit(editPane);
			visitor.visit(editPane.getTextArea());
		}
	} //}}}

	// {{{ closeAllMenus()
	/** closes any popup menus that may have been opened
		@since jEdit 4.4pre1
	*/
	public void closeAllMenus()
	{
		MenuSelectionManager.defaultManager().clearSelectedPath();
		KeyboardFocusManager.getCurrentKeyboardFocusManager().clearGlobalFocusOwner();
	} // }}}

	//{{{ Package-private members
	View prev;
	View next;

	//{{{ View constructor
	View(Buffer buffer, ViewConfig config)
	{
		fullScreenMode = false;
		menuBar = null;
		plainView = config.plainView;

		enableEvents(AWTEvent.KEY_EVENT_MASK);

		// OS X users expect a preview of the window rather than an icon
		if (!OperatingSystem.isMacOS())
			setIconImage(GUIUtilities.getEditorIcon());

		mainPanel = new JPanel();
		mainPanel.setLayout(new BorderLayout());
		dockableWindowManager = getDockingFrameworkProvider().create(this,
			DockableWindowFactory.getInstance(), config);
		userTitle = config.title;
		dockableWindowManager.setMainPanel(mainPanel);

		topToolBars = new JPanel(new VariableGridLayout(
			VariableGridLayout.FIXED_NUM_COLUMNS,
			1));
		bottomToolBars = new JPanel(new VariableGridLayout(
			VariableGridLayout.FIXED_NUM_COLUMNS,
			1));

		toolBarManager = new ToolBarManager(topToolBars, bottomToolBars);

		status = new StatusBar(this);

		inputHandler = new DefaultInputHandler(this,(DefaultInputHandler)
			jEdit.getInputHandler());

		setSplitConfig(buffer,config.splitConfig);

		getContentPane().add(BorderLayout.CENTER,dockableWindowManager);

		dockableWindowManager.init();

		// tool bar and status bar gets added in propertiesChanged()
		// depending in the 'tool bar alternate layout' setting.
		propertiesChanged();

		setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
		addWindowListener(new WindowHandler());

		setFocusTraversalPolicy(new MyFocusTraversalPolicy());

		EditBus.addToBus(this);

		GUIUtilities.addSizeSaver(this, null, plainView ? "plain-view" : "view");
	} //}}}

	//{{{ updateFullScreenProps() method
	public void updateFullScreenProps()
	{
		boolean alternateLayout = jEdit.getBooleanProperty(
			"view.toolbar.alternateLayout");
		boolean showMenu = jEdit.getBooleanProperty("fullScreenIncludesMenu");
		boolean showToolbars = jEdit.getBooleanProperty("fullScreenIncludesToolbar");
		boolean showStatus = jEdit.getBooleanProperty("fullScreenIncludesStatus");
		if (! showMenu)
		{
			menuBar = getJMenuBar();
			setJMenuBar(null);
		}
		else if (menuBar != null)
			setJMenuBar(menuBar);
		// Note: Bottom toolbar is the action bar, which is always enabled
		loadToolBars();
		if (alternateLayout)
		{
			if (! showStatus)
				removeToolBar(status);
			else
				addToolBar(BOTTOM_GROUP,STATUS_BAR_LAYER,status);
		}
		else
		{
			if (! showStatus)
				getContentPane().remove(status);
			else
				getContentPane().add(BorderLayout.SOUTH,status);
		}
	} //}}}

	//{{{ isFullScreenMode method
	public boolean isFullScreenMode()
	{
		return fullScreenMode;
	}//}}}

	//{{{ toggleFullScreen() method
	public void toggleFullScreen()
	{
		fullScreenMode = !fullScreenMode;
		GraphicsDevice sd = getGraphicsConfiguration().getDevice();
		dispose();
		if (fullScreenMode)
		{
			updateFullScreenProps();
			windowedBounds = getBounds();
			setUndecorated(true);
			setBounds(sd.getDefaultConfiguration().getBounds());
			validate();
		}
		else
		{
			boolean showStatus = plainView ? jEdit.getBooleanProperty("view.status.plainview.visible") :
				jEdit.getBooleanProperty("view.status.visible");
			if (menuBar != null && getJMenuBar() != menuBar)
				setJMenuBar(menuBar);
			boolean alternateLayout = jEdit.getBooleanProperty(
				"view.toolbar.alternateLayout");
			loadToolBars();
			if (showStatus)
			{
				if (alternateLayout)
					addToolBar(BOTTOM_GROUP,STATUS_BAR_LAYER,status);
				else
					getContentPane().add(BorderLayout.SOUTH,status);
			}
			setUndecorated(false);
			setBounds(windowedBounds);
		}
		setVisible(true);
		toFront();
		closeAllMenus();
		// so you can keep typing in your editpane afterwards...
		editPane.getTextArea().requestFocus();
		EditBus.send(new ViewUpdate(this,ViewUpdate.FULL_SCREEN_TOGGLED));
	} //}}}

	//{{{ confirmToCloseDirty() methods
	/**
	 * If the view contains dirty buffers which will be closed on
	 * closing the view, show the confirmation dialog for user.
	 * @return
	 * 	true if there are no such buffers or user select OK
	 * 	to close the view; false if user select Cancel
	 */
	boolean confirmToCloseDirty()
	{
		boolean autosaveUntitled = jEdit.getBooleanProperty("autosaveUntitled");
		boolean suppressNotSavedConfirmUntitled = jEdit.getBooleanProperty("suppressNotSavedConfirmUntitled") || autosaveUntitled;
		Set<Buffer> checkingBuffers = getOpenBuffers();
		ViewManager viewManager = jEdit.getViewManager();
		viewManager
			.getViews()
			.stream()
			.filter(view -> view != this)
			.forEach(view -> checkingBuffers.removeAll(view.getOpenBuffers()));

		for (Buffer buffer: checkingBuffers)
		{
			if (buffer.isDirty() && !(buffer.isUntitled() && suppressNotSavedConfirmUntitled))
			{
				return new CloseDialog(this, checkingBuffers).isOK();
			}
		}
		return true;
	} //}}}

	//{{{ close() method
	void close()
	{
		EditBus.send(new ViewUpdate(this,ViewUpdate.CLOSED));
		closed = true;

		// save dockable window geometry, and close 'em
		dockableWindowManager.close();

		EditBus.removeFromBus(this);
		dispose();

		EditPane[] editPanes = getEditPanes();
		for (EditPane ep : editPanes)
			ep.close();

		// null some variables so that retaining references
		// to closed views won't hurt as much.
		toolBarManager = null;
		toolBar = null;
		searchBar = null;
		splitPane = null;
		inputHandler = null;
		recorder = null;

		getContentPane().removeAll();

		// notify clients with -wait
		if(waitSocket != null)
		{
			try
			{
				waitSocket.getOutputStream().write('\0');
				waitSocket.getOutputStream().flush();
				waitSocket.getInputStream().close();
				waitSocket.getOutputStream().close();
				waitSocket.close();
			}
			catch(IOException io)
			{
				//Log.log(Log.ERROR,this,io);
			}
		}
	} //}}}

	//}}}

	//{{{ Private members

	//{{{ Instance variables
	private boolean closed;

	private final DockableWindowManager dockableWindowManager;
	private final JPanel mainPanel;

	private final JPanel topToolBars;
	private final JPanel bottomToolBars;
	private ToolBarManager toolBarManager;

	private Container toolBar;
	private SearchBar searchBar;
	private ActionBar actionBar;

	private EditPane editPane;
	private JSplitPane splitPane;
	private String lastSplitConfig;

	private final StatusBar status;

	private InputHandler inputHandler;
	private Macros.Recorder recorder;
	private Component prefixFocusOwner;

	private int waitCount;

	private boolean showFullPath;

	private final boolean plainView;

	private Socket waitSocket;
	private Component mainContent;

	private boolean fullScreenMode;
	private Rectangle windowedBounds;
	private JMenuBar menuBar;
	private String userTitle;
	//}}}

	//{{{ setMainContent() method
	private void setMainContent(Component c)
	{
		if (mainContent != null)
			mainPanel.remove(mainContent);
		mainContent = c;
		mainPanel.add(mainContent, BorderLayout.CENTER);
		if (c instanceof JSplitPane)
		{
			splitPane = (JSplitPane)c;
		}
		else
		{
			splitPane = null;
			editPane = (EditPane)c;
		}
		mainPanel.revalidate();
		mainPanel.repaint();
	} //}}}

	//{{{ getEditPanes() method
	private static void performAction(Component comp, Consumer<? super EditPane> action)
	{
		if(comp instanceof EditPane)
			action.accept((EditPane) comp);
		else if(comp instanceof JSplitPane)
		{
			JSplitPane split = (JSplitPane)comp;
			performAction(split.getLeftComponent(), action);
			performAction(split.getRightComponent(), action);
		}
	} //}}}

	//{{{ showBuffer() method
	private EditPane showBuffer(Buffer buffer, boolean focus)
	{
		if(editPane.getBuffer() == buffer
			&& editPane.getTextArea().getVisibleLines() > 1)
		{
			if (focus)
				editPane.focusOnTextArea();
			return editPane;
		}

		EditPane[] editPanes = getEditPanes();
		for (EditPane ep : editPanes)
		{
			if (ep.getBuffer() == buffer
				/* ignore zero-height splits, etc */
				&& ep.getTextArea().getVisibleLines() > 1)
			{
				setEditPane(ep);
				if (focus)
					ep.focusOnTextArea();
				return ep;
			}
		}

		editPane.setBuffer(buffer, focus);
		return editPane;
	} //}}}

	//{{{ getSplitConfig() method
	/*
	 * The split config is recorded in a simple RPN "language".
	 */
	private static void getSplitConfig(JSplitPane splitPane,
		StringBuilder splitConfig)
	{
		Component right = splitPane.getRightComponent();
		appendToSplitConfig(splitConfig, right);

		splitConfig.append(' ');

		Component left = splitPane.getLeftComponent();
		appendToSplitConfig(splitConfig, left);

		splitConfig.append(' ');
		splitConfig.append(splitPane.getDividerLocation());
		splitConfig.append(' ');
		splitConfig.append(splitPane.getOrientation()
			== JSplitPane.VERTICAL_SPLIT ? "vertical" : "horizontal");
	} //}}}

	//{{{ appendToSplitConfig() method
	/**
	 * Append the Component to the split config.
	 * The component must be a JSplitPane or an EditPane
	 *
	 * @param splitConfig the split config
	 * @param component the component
	 */
	private static void appendToSplitConfig(StringBuilder splitConfig, Component component)
	{
		if(component instanceof JSplitPane)
		{
			// the component is a JSplitPane
			getSplitConfig((JSplitPane)component,splitConfig);
		}
		else
		{
			boolean autosaveUntitled = jEdit.getBooleanProperty("autosaveUntitled");

			// the component is an editPane
			EditPane editPane = (EditPane) component;
			splitConfig.append('"');
			Buffer editPaneBuffer = editPane.getBuffer();
			splitConfig.append(StandardUtilities.charsToEscapes(editPaneBuffer.getPath()));
			splitConfig.append("\" buffer");
			BufferSet bufferSet = editPane.getBufferSet();
			Buffer[] buffers = bufferSet.getAllBuffers();
			for (Buffer buffer : buffers)
			{
				if (!buffer.isNewFile() || (buffer.isUntitled() && autosaveUntitled))
				{
					splitConfig.append(" \"");
					splitConfig.append(StandardUtilities.charsToEscapes(buffer.getPath() ));
					splitConfig.append("\" buff");
				}
			}
			splitConfig.append(" \"");
			splitConfig.append(jEdit.getBufferSetManager().getScope());
			splitConfig.append("\" bufferset");
		}
	} //}}}

	//{{{ restoreSplitConfig() method
	private Component restoreSplitConfig(Buffer buffer, String splitConfig)
		throws IOException
	// this is where checked exceptions piss me off. this method only uses
	// a StringReader which can never throw an exception...
	{
		BufferManager bufferManager = jEdit.getBufferManager();
		if(buffer != null)
		{
			return editPane = createEditPane(buffer);
		}
		else if(splitConfig == null || splitConfig.trim().isEmpty())
		{

			Buffer buf = bufferManager.getFirst();
			if (buf == null)
			{
				buf = BufferSetManager.createUntitledBuffer();
			}
			return editPane = createEditPane(buf);
		}
		List<Buffer> buffers = bufferManager.getBuffers();

		Stack<Object> stack = new Stack<>();

		// we create a stream tokenizer for parsing a simple
		// stack-based language
		StreamTokenizer st = new StreamTokenizer(new StringReader(
			splitConfig));
		st.whitespaceChars(0,' ');
		/* all printable ASCII characters */
		st.wordChars('#','~');
		st.commentChar('!');
		st.quoteChar('"');
		st.eolIsSignificant(false);
		Collection<Buffer> editPaneBuffers = new ArrayList<>();
loop:		while (true)
		{
			switch(st.nextToken())
			{
			case StreamTokenizer.TT_EOF:
				break loop;
			case StreamTokenizer.TT_WORD:
				if("vertical".equals(st.sval) || "horizontal".equals(st.sval))
				{
					int orientation
						= "vertical".equals(st.sval)
						? JSplitPane.VERTICAL_SPLIT
						: JSplitPane.HORIZONTAL_SPLIT;
					int divider = (Integer) stack.pop();
					Object obj1 = stack.pop();
					Object obj2 = stack.pop();
					// Backward compatibility with pre-bufferset versions
					if (obj1 instanceof Buffer)
					{
						Buffer b1 = buffer = (Buffer) obj1;
						obj1 = editPane = createEditPane(b1);
					}
					if (obj2 instanceof Buffer)
					{
						Buffer b2 = (Buffer) obj2;
						obj2 = createEditPane(b2);
					}
					stack.push(splitPane = new JSplitPane(
						orientation,
						(Component)obj1,
						(Component)obj2));
					splitPane.setOneTouchExpandable(true);
					splitPane.setBorder(null);
					splitPane.setMinimumSize(
						new Dimension(0,0));
					splitPane.setDividerLocation(divider);
				}
				else if("buffer".equals(st.sval))
				{
					Object obj = stack.pop();
					if(obj instanceof Integer)
					{
						int index = (Integer) obj;
						if(index >= 0 && index < buffers.size())
							buffer = buffers.get(index);
					}
					else if(obj instanceof String)
					{
						String path = (String)obj;
						Optional<Buffer> bufferOptional = bufferManager.getBuffer(path);
						buffer = bufferOptional.orElseGet(() ->
						{
							Buffer buf = jEdit.openTemporary(jEdit.getActiveView(), null,
								path, true, null, true);
							jEdit.commitTemporary(buf);
							return buf;
						});
					}

					if(buffer == null)
						buffer = bufferManager.getFirst();
					stack.push(buffer);
					editPaneBuffers.add(buffer);
				}
				else if ("buff".equals(st.sval))
				{
					String path = (String)stack.pop();

					Optional<Buffer> bufferOptional = bufferManager.getBuffer(path);
					if (bufferOptional.isEmpty())
					{
						Log.log(Log.WARNING, this, "Buffer " + path + " doesn't exist");
					}
					else
					{
						buffer = bufferOptional.get();
						editPaneBuffers.add(buffer);
					}
				}
				else if ("bufferset".equals(st.sval))
				{
					// pop the bufferset scope. Not used anymore but still here for compatibility
					// with old perspectives
					stack.pop();
					buffer = (Buffer) stack.pop();
					editPane = createEditPane(buffer);
					stack.push(editPane);
					BufferSet bufferSet = editPane.getBufferSet();
					int i = 0;
					for (Buffer buff : editPaneBuffers)
					{
						bufferSet.addBufferAt(buff,i);
						i++;
					}
					editPaneBuffers.clear();
				}
				break;
			case StreamTokenizer.TT_NUMBER:
				stack.push((int)st.nval);
				break;
			case '"':
				stack.push(st.sval);
				break;
			}
		}

		// Backward compatibility with pre-bufferset versions
		Object obj = stack.peek();
		if (obj instanceof Buffer)
		{
			obj = editPane = createEditPane((Buffer)obj);
		}

		updateGutterBorders();

		return (Component)obj;
	} //}}}

	//{{{ propertiesChanged() method
	/**
	 * Reloads various settings from the properties.
	 */
	private void propertiesChanged()
	{
		JMenuBar mbar = GUIUtilities.loadMenuBar("view.mbar");

		// menu bar mnemonics take precedence over other shortcut definitions
		for (int i = 0; i < mbar.getMenuCount(); i++)
		{
			JMenu menu = mbar.getMenu(i);
			if (menu != null)
			{
				int mnemonic = menu.getMnemonic();
				if (mnemonic != 0)
				{
					Object keyBinding = inputHandler.getKeyBinding("A+" + Character.toLowerCase((char) mnemonic));
					if (keyBinding != null)
					{
						menu.setMnemonic(0);
					}
				}
			}
		}

		setJMenuBar(mbar);

		if (Desktop.isDesktopSupported())
		{
			Desktop desktop = Desktop.getDesktop();

			if (desktop.isSupported(Action.APP_QUIT_HANDLER))
			{
				desktop.setQuitHandler((e, response) ->
				{
					jEdit.exit(jEdit.getActiveView(), true);
					response.cancelQuit();
				});
			}

			if (desktop.isSupported(Action.APP_ABOUT))
			{
				desktop.setAboutHandler((e) -> new AboutDialog(jEdit.getActiveView()));
			}

			if (desktop.isSupported(Action.APP_PREFERENCES))
			{
				desktop.setPreferencesHandler((e) -> new CombinedOptions(jEdit.getActiveView(), 0));
			}
		}

		loadToolBars();

		showFullPath = jEdit.getBooleanProperty("view.showFullPath");
		updateTitle();

		status.propertiesChanged();

		removeToolBar(status);
		getContentPane().remove(status);

		boolean showStatus = plainView ? jEdit.getBooleanProperty("view.status.plainview.visible") :
					jEdit.getBooleanProperty("view.status.visible");
		if (jEdit.getBooleanProperty("view.toolbar.alternateLayout"))
		{
			getContentPane().add(BorderLayout.NORTH,topToolBars);
			getContentPane().add(BorderLayout.SOUTH,bottomToolBars);
			if (showStatus)
				addToolBar(BOTTOM_GROUP,STATUS_BAR_LAYER,status);
		}
		else
		{
			mainPanel.add(topToolBars, BorderLayout.NORTH);
			mainPanel.add(bottomToolBars, BorderLayout.SOUTH);
			if (showStatus)
				getContentPane().add(BorderLayout.SOUTH,status);
		}
		updateBufferSwitcherStates();

		getRootPane().revalidate();

		//SwingUtilities.updateComponentTreeUI(getRootPane());

		if (fullScreenMode)
			updateFullScreenProps();
	} //}}}

	//{{{ updateBufferSwitcherStates() method
	/**
	 * Enables or Disables the "Focus Buffer Switcher" menu item in the View menu
	 * depending on the visible state of the buffer switcher.  The menu item
	 * is intended to have the same effect as clicking on the buffer switcher
	 * combo box, and it doesn't make sense to have this action available if
	 * the buffer switcher isn't visible.
	 * Also shows or hides the Buffer Switcher itself, since this can be invoked after
	 * the toggle buffer switcher action.
	 */
	public void updateBufferSwitcherStates()
	{
		boolean show = jEdit.getBooleanProperty("view.showBufferSwitcher");
		JMenuBar menubar = getJMenuBar();
		if (menubar == null)
		{
			return;
		}
		String viewmenu_label = jEdit.getProperty("view.label");
		viewmenu_label = viewmenu_label.replace("$", "");
		String sbs_label = jEdit.getProperty("focus-buffer-switcher.label");
		sbs_label = sbs_label.replace("$", "");
		JMenu viewmenu = null;
		for (int i = 0; i < menubar.getMenuCount(); i++)
		{
			JMenu menu = menubar.getMenu(i);
			if (menu.getText().equals(viewmenu_label))
			{
				viewmenu = menu;
				break;
			}
		}
		if (viewmenu != null)
		{
			for (int i = 0; i < viewmenu.getMenuComponentCount(); i++)
			{
				Component item = viewmenu.getMenuComponent(i);
				if (item instanceof JMenuItem && ((JMenuItem)item).getText().equals(sbs_label))
				{
					item.setEnabled(show);
					// viewmenu.invalidate();
				}
			}
		}
		// Toggle the visibility of the BufferSwitcher itself
		// todo : flatmap
		jEdit.getViewManager()
			.forEach(view -> {
				for (EditPane ep: view.getEditPanes())
					ep.loadBufferSwitcher();
			});
	} //}}}


	//{{{ loadToolBars() method
	private void loadToolBars()
	{
		if(!plainView && (fullScreenMode ?
			jEdit.getBooleanProperty("fullScreenIncludesToolbar") :
			jEdit.getBooleanProperty("view.showToolbar")))
		{
			if(toolBar != null)
				toolBarManager.removeToolBar(toolBar);

			toolBar = GUIUtilities.loadToolBar("view.toolbar");

			addToolBar(TOP_GROUP, SYSTEM_BAR_LAYER, toolBar);
		}
		else if(toolBar != null)
		{
			removeToolBar(toolBar);
			toolBar = null;
		}

		if(searchBar != null)
		{
			searchBar.propertiesChanged();
			removeToolBar(searchBar);
		}

		if(jEdit.getBooleanProperty("view.showSearchbar") && !plainView)
		{
			if(searchBar == null)
				searchBar = new SearchBar(this,false);
			addToolBar(TOP_GROUP,SEARCH_BAR_LAYER,searchBar);
		}
	} //}}}

	//{{{ createEditPane() methods
	private EditPane createEditPane(@Nonnull Buffer buffer)
	{
		return createEditPane(null, buffer);
	}

	private EditPane createEditPane(@Nullable BufferSet bufferSetSource, @Nonnull Buffer buffer)
	{
		EditPane editPane = new EditPane(this, bufferSetSource, buffer);
		JEditTextArea textArea = editPane.getTextArea();
		textArea.addFocusListener(new FocusHandler());
		textArea.addCaretListener(new CaretHandler());
		textArea.addScrollListener(new ScrollHandler());
		EditBus.send(new EditPaneUpdate(editPane,EditPaneUpdate.CREATED));
		return editPane;
	} //}}}

	//{{{ setEditPane() method
	private void setEditPane(EditPane editPane)
	{
		this.editPane = editPane;
		status.updateCaretStatus();
		status.updateBufferStatus();
		status.updateMiscStatus();

		// repaint the gutter so that the border color
		// reflects the focus state
		updateGutterBorders();

		EditBus.send(new ViewUpdate(this,ViewUpdate.EDIT_PANE_CHANGED));
	} //}}}


	//{{{ handleBufferUpdate() method
	@EBHandler
	public void handleBufferUpdate(BufferUpdate msg)
	{
		Buffer buffer = msg.getBuffer();
		if(msg.getWhat() == BufferUpdate.DIRTY_CHANGED
			|| msg.getWhat() == BufferUpdate.LOADED)
		{
			EditPane[] editPanes = getEditPanes();
			for (EditPane ep : editPanes)
			{
				if (ep.getBuffer() == buffer)
				{
					updateTitle();
					break;
				}
			}
		}
	} //}}}

	//{{{ handleEditPaneUpdate() method
	@EBHandler
	public void handleEditPaneUpdate(EditPaneUpdate msg)
	{
		EditPane editPane = msg.getEditPane();
		if(editPane !=  null &&
			editPane.getView() == this
			&& msg.getWhat() == EditPaneUpdate.BUFFER_CHANGED
			&& editPane.getBuffer().isLoaded())
		{
			closeDuplicateBuffers(msg);
			status.updateCaretStatus();
			status.updateBufferStatus();
			status.updateMiscStatus();
		}
	} //}}}

	//{{{ handleViewUpdate() method
	@EBHandler
	public void handleViewUpdate(ViewUpdate msg)
	{
		// only have my view handle each update message
		if (msg.getView() == null || msg.getView() != this) return;
		final int check = jEdit.getIntegerProperty("checkFileStatus");
		if ((check == 0) || !jEdit.isStartupDone()) return;
		// "buffer visit" also includes checking the buffer when you change editpanes.
		// "buffer visit" also includes checking the buffer when you activate view, coming from
		// another program, which could have alterered file on disk.
		if ((msg.getWhat() == ViewUpdate.EDIT_PANE_CHANGED || msg.getWhat() == ViewUpdate.ACTIVATED) &&
			((check & GeneralOptionPane.checkFileStatus_focusBuffer) > 0))
			jEdit.checkBufferStatus(View.this, true);
		else if ((msg.getWhat() == ViewUpdate.ACTIVATED) &&
			(check & GeneralOptionPane.checkFileStatus_focus) > 0)
				jEdit.checkBufferStatus(View.this,
					(check != GeneralOptionPane.checkFileStatus_focus));
	}//}}}

	//{{{ closeDuplicateBuffers() method
	/** Used if exclusive buffersets are enabled */
	private void closeDuplicateBuffers(EditPaneUpdate epu)
	{
		if (!jEdit.getBooleanProperty("buffersets.exclusive"))
			return;
		final BufferSet.Scope scope = jEdit.getBufferSetManager().getScope();
		if (scope == BufferSet.Scope.global)
			return;
		final EditPane ep = epu.getEditPane();
		/* Only one view needs to handle this message, since
		   we iterate through all the other views */
		final View view = ep.getView();
		if (view != this)
			return;
		final Buffer b = ep.getBuffer();
		if (b.isDirty()) return;
		jEdit.visit(new JEditVisitorAdapter()
		{
			@Override
			public void visit(EditPane editPane)
			{
				if (editPane == ep ||
					(scope == BufferSet.Scope.view && editPane.getView() == view))
					return;
				if (!editPane.getBufferSet().contains(b))
					return;
				jEdit.getBufferSetManager().removeBuffer(editPane, b);
			}
		});
	} //}}}

	//{{{ updateGutterBorders() method
	/**
	 * Updates the borders of all gutters in this view to reflect the
	 * currently focused text area.
	 * @since jEdit 2.6final
	 */
	private void updateGutterBorders()
	{
		EditPane[] editPanes = getEditPanes();
		for (EditPane editPane : editPanes)
			editPane.getTextArea().getGutter().updateBorder();
	} //}}}

	//{{{ getOpenBuffers() method
	private Set<Buffer> getOpenBuffers()
	{
		Set<Buffer> openBuffers = new LinkedHashSet<>();
		for (EditPane editPane: getEditPanes())
		{
			openBuffers.addAll(Arrays.asList(
				editPane.getBufferSet().getAllBuffers()));
		}
		return openBuffers;
	} //}}}

	//{{{ mergeBufferSets() method
	/**
	 * Merge a EditPane's BufferSet into another one.
	 * This is used on unsplitting panes not to close buffers.
	 * @param target the target bufferSet where we will merge buffers from source
	 * @param source the source bufferSet
	 */
	private static void mergeBufferSets(EditPane target, EditPane source)
	{
		BufferSetManager manager = jEdit.getBufferSetManager();
		for (Buffer buffer: source.getBufferSet().getAllBuffers())
		{
			manager.addBuffer(target, buffer);
		}
	} //}}}

	//}}}

	//{{{ Inner classes

	//{{{ CaretHandler class
	private class CaretHandler implements CaretListener
	{
		@Override
		public void caretUpdate(CaretEvent evt)
		{
			if(evt.getSource() == getTextArea())
				status.updateCaretStatus();
		}
	} //}}}

	//{{{ FocusHandler class
	private class FocusHandler extends FocusAdapter
	{
		@Override
		public void focusGained(FocusEvent evt)
		{
			// walk up hierarchy, looking for an EditPane
			Component comp = (Component)evt.getSource();
			while(!(comp instanceof EditPane))
			{
				if(comp == null)
					return;

				comp = comp.getParent();
			}

			if(comp != editPane)
				setEditPane((EditPane)comp);
			else
				updateGutterBorders();
		}
	} //}}}

	//{{{ ScrollHandler class
	private class ScrollHandler implements ScrollListener
	{
		@Override
		public void scrolledVertically(TextArea textArea)
		{
			if(getTextArea() == textArea)
				status.updateCaretStatus();
		}

		@Override
		public void scrolledHorizontally(TextArea textArea) {}
	} //}}}

	//{{{ WindowHandler class
	private class WindowHandler extends WindowAdapter
	{
		@Override
		public void windowActivated(WindowEvent evt)
		{
			boolean appFocus = false;
			boolean viewChanged = false;
			View oldView = jEdit.getActiveViewInternal();
			// check if view is changed
			if (oldView != View.this) viewChanged = true;
			/* ACTIVATED currently means whenever the View gets focus.
			Ideally it should be only when the View changes or we are
			focusing on a View after previously using in another application.
			Currently, we also get ACTIVATED messages after a closed jEdit dialog.
			I consider this a bug which we should fix some day. */
			if (evt.getOppositeWindow() == null) appFocus = true;
			jEdit.setActiveView(View.this);
//			Log.log(Log.DEBUG, this, "appFocus:" + appFocus + " viewChanged:" + viewChanged);
			if (appFocus || viewChanged)
				EditBus.send(new ViewUpdate(View.this, ViewUpdate.ACTIVATED));
		}

		@Override
		public void windowClosing(WindowEvent evt)
		{
			jEdit.closeView(View.this);
		}
	} //}}}

	//{{{ ViewConfig class
	public static class ViewConfig
	{
		public int x, y, width, height, extState;
		public boolean plainView;
		public String splitConfig;
		public DockingLayout docking;
		public String title;

		public ViewConfig()
		{
		}

		public ViewConfig(boolean plainView)
		{
			this.plainView = plainView;
			String prefix = plainView ? "plain-view" : "view";
			x = jEdit.getIntegerProperty(prefix + ".x",0);
			y = jEdit.getIntegerProperty(prefix + ".y",0);
			width = jEdit.getIntegerProperty(prefix + ".width",0);
			height = jEdit.getIntegerProperty(prefix + ".height",0);
			extState = jEdit.getIntegerProperty(prefix + ".extendedState", Frame.NORMAL);
		}

		public ViewConfig(boolean plainView, String splitConfig,
			int x, int y, int width, int height, int extState)
		{
			this.plainView = plainView;
			this.splitConfig = splitConfig;
			this.x = x;
			this.y = y;
			this.width = width;
			this.height = height;
			this.extState = extState;
		}
	} //}}}

	//{{{ isInsideScreen() method
	private static boolean isInsideScreen(View parent, Rectangle r)
	{
		Rectangle bounds;
		if (parent == null)
			bounds = GenericGUIUtilities.getScreenBounds();
		else
			bounds = parent.getGraphicsConfiguration().getBounds();
		int minWidth = jEdit.getIntegerProperty("view.minStartupWidth");
		int minHeight = jEdit.getIntegerProperty("view.minStartupHeight");
		return  r.x + r.width	> bounds.x + minWidth &&		// right edge at minWidth pixels on the right of the left bound
			r.x		< bounds.x + bounds.width - minWidth &&	// left edge at minWidth pixels on the left of the right bound
			r.y + r.height	> bounds.y + minHeight &&		// bottom edge at minHeight pixels under the top bound
			r.y		< bounds.y + bounds.height - minHeight;	// top edge at minHeight pixels on the top of the bottom bound
	} //}}}

	public void adjust(View parent, ViewConfig config)
	{
		if(config.width != 0 && config.height != 0)
		{
			Rectangle desired = new Rectangle(
					config.x, config.y, config.width, config.height);
			if (! isInsideScreen(parent, desired))
				setLocationRelativeTo(parent);
			else
			{
				if(OperatingSystem.isX11() && Debug.GEOMETRY_WORKAROUND)
					new GUIUtilities.UnixWorkaround(this,"view",desired,config.extState);
				else
				{
					setBounds(desired);
					setExtendedState(config.extState);
				}
			}
		}
		else
			setLocationRelativeTo(parent);
	}

	//{{{ MyFocusTraversalPolicy class
	private static class MyFocusTraversalPolicy extends LayoutFocusTraversalPolicy
	{
		@Override
		public Component getDefaultComponent(Container focusCycleRoot)
		{
			return GUIUtilities.getView(focusCycleRoot).getTextArea();
		}
	} //}}}

	//{{{ SetCursorVisitor class
	private static class SetCursorVisitor extends JEditVisitorAdapter
	{
		private final Cursor cursor;

		SetCursorVisitor(Cursor cursor)
		{
			this.cursor = cursor;
		}

		@Override
		public void visit(EditPane editPane)
		{
			editPane.setCursor(cursor);
		}
	}//}}}
	 //}}}

}
