/* * Copyright 2000-2009 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.intellij.openapi.options.newEditor; import com.intellij.ide.ui.search.ConfigurableHit; import com.intellij.ide.ui.search.SearchUtil; import com.intellij.ide.ui.search.SearchableOptionsRegistrar; import com.intellij.ide.util.PropertiesComponent; import com.intellij.openapi.Disposable; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.actionSystem.ex.AnActionListener; import com.intellij.openapi.application.Application; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.options.Configurable; import com.intellij.openapi.options.ConfigurationException; import com.intellij.openapi.options.MasterDetails; import com.intellij.openapi.options.SearchableConfigurable; import com.intellij.openapi.options.ex.ConfigurableWrapper; import com.intellij.openapi.options.ex.GlassPanel; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.*; import com.intellij.openapi.util.ActionCallback; import com.intellij.openapi.util.Conditions; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.EdtRunnable; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.wm.IdeFocusManager; import com.intellij.openapi.wm.IdeGlassPaneUtil; import com.intellij.ui.*; import com.intellij.ui.components.panels.NonOpaquePanel; import com.intellij.ui.components.panels.Wrapper; import com.intellij.ui.navigation.History; import com.intellij.ui.navigation.Place; import com.intellij.ui.speedSearch.ElementFilter; import com.intellij.ui.treeStructure.SimpleNode; import com.intellij.util.ui.JBUI; import com.intellij.util.ui.UIUtil; import com.intellij.util.ui.update.Activatable; import com.intellij.util.ui.update.MergingUpdateQueue; import com.intellij.util.ui.update.UiNotifyConnector; import com.intellij.util.ui.update.Update; import consulo.annotations.RequiredDispatchThread; import consulo.options.ConfigurableUIMigrationUtil; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import javax.swing.border.EmptyBorder; import javax.swing.event.DocumentEvent; import java.awt.*; import java.awt.event.*; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.lang.reflect.Field; import java.util.*; import java.util.List; public class OptionsEditor implements DataProvider, Place.Navigator, Disposable, AWTEventListener { public static DataKey<OptionsEditor> KEY = DataKey.create("options.editor"); private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.options.newEditor.OptionsEditor"); @NonNls public static final String MAIN_SPLITTER_PROPORTION = "options.splitter.main.proportions"; @NonNls private static final String DETAILS_SPLITTER_PROPORTION = "options.splitter.details.proportions"; @NonNls private static final String SEARCH_VISIBLE = "options.searchVisible"; @NonNls private static final String NOT_A_NEW_COMPONENT = "component.was.already.instantiated"; private final Project myProject; private final OptionsEditorContext myContext; private final History myHistory = new History(this); private final OptionsTree myTree; private final MySearchField mySearch; //private final Splitter myMainSplitter; //[back/forward] JComponent myToolbarComponent; private final DetailsComponent myOwnDetails = new DetailsComponent(false, false).setEmptyContentText("Select configuration element in the tree to edit its settings"); private final ContentWrapper myContentWrapper = new ContentWrapper(); private final Map<Configurable, ConfigurableContent> myConfigurable2Content = new HashMap<Configurable, ConfigurableContent>(); private final Map<Configurable, ActionCallback> myConfigurable2LoadCallback = new HashMap<Configurable, ActionCallback>(); private final MergingUpdateQueue myModificationChecker; private final Configurable[] myConfigurables; private JPanel myRootPanel; private final SpotlightPainter mySpotlightPainter = new SpotlightPainter(); private final MergingUpdateQueue mySpotlightUpdate; private final LoadingDecorator myLoadingDecorator; private final Filter myFilter; private final Wrapper mySearchWrapper = new Wrapper(); private final JPanel myLeftSide; private boolean myFilterDocumentWasChanged; //[back/forward] private ActionToolbar myToolbar; private Window myWindow; private final PropertiesComponent myProperties; private volatile boolean myDisposed; public OptionsEditor(Project project, Configurable[] configurables, Configurable preselectedConfigurable, final JPanel rootPanel) { myProject = project; myConfigurables = configurables; myRootPanel = rootPanel; myProperties = PropertiesComponent.getInstance(project); myFilter = new Filter(); myContext = new OptionsEditorContext(myFilter); mySearch = new MySearchField() { @Override protected void onTextKeyEvent(final KeyEvent e) { myTree.processTextEvent(e); } }; mySearch.getTextEditor().addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { boolean hasText = mySearch.getText().length() > 0; if (!myContext.isHoldingFilter() && hasText) { myFilter.reenable(); } if (!isSearchFieldFocused() && hasText) { mySearch.selectText(); } } }); myTree = new OptionsTree(myProject, configurables, getContext()) { @Override protected void onTreeKeyEvent(final KeyEvent e) { myFilterDocumentWasChanged = false; try { mySearch.keyEventToTextField(e); } finally { if (myFilterDocumentWasChanged && !isFilterFieldVisible()) { setFilterFieldVisible(true, false, false); } } } }; getContext().addColleague(myTree); Disposer.register(this, myTree); mySearch.addDocumentListener(new DocumentAdapter() { @Override protected void textChanged(DocumentEvent e) { myFilter.update(e.getType(), true, false); } }); /* [back/forward] final DefaultActionGroup toolbarActions = new DefaultActionGroup(); toolbarActions.add(new BackAction(myTree)); toolbarActions.add(new ForwardAction(myTree)); toolbarActions.addSeparator(); toolbarActions.add(new ShowSearchFieldAction(this)); myToolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.UNKNOWN, toolbarActions, true); myToolbar.setTargetComponent(this); myToolbarComponent = myToolbar.getComponent(); myHistory.addListener(new HistoryListener.Adapter() { @Override public void navigationFinished(final Place from, final Place to) { UIUtil.invokeLaterIfNeeded(new Runnable() { public void run() { if (myToolbarComponent.isShowing()) { myToolbar.updateActionsImmediately(); } } }); } }, this); */ myLeftSide = new JPanel(new BorderLayout()) { @Override public Dimension getMinimumSize() { Dimension dimension = super.getMinimumSize(); dimension.width = Math.max(myTree.getMinimumSize().width, mySearchWrapper.getPreferredSize().width); return dimension; } }; /* [back/forward] final NonOpaquePanel toolbarPanel = new NonOpaquePanel(new BorderLayout()); toolbarPanel.add(myToolbarComponent, BorderLayout.WEST); toolbarPanel.add(mySearchWrapper, BorderLayout.CENTER); */ myLeftSide.add(mySearchWrapper, BorderLayout.NORTH); myLeftSide.add(myTree, BorderLayout.CENTER); myLoadingDecorator = new LoadingDecorator(myOwnDetails.getComponent(), this, 150); //myMainSplitter.setProportion(readProportion(0.3f, MAIN_SPLITTER_PROPORTION)); myContentWrapper.mySplitter.setProportion(readProportion(0.2f, DETAILS_SPLITTER_PROPORTION)); MyColleague colleague = new MyColleague(); getContext().addColleague(colleague); mySpotlightUpdate = new MergingUpdateQueue("OptionsSpotlight", 200, false, rootPanel, this, rootPanel); if (preselectedConfigurable != null) { myTree.select(preselectedConfigurable); } else { myTree.selectFirst(); } Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.MOUSE_EVENT_MASK | AWTEvent.KEY_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK); ActionManager.getInstance().addAnActionListener(new AnActionListener() { @Override public void beforeActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) { } @Override public void afterActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) { queueModificationCheck(); } @Override public void beforeEditorTyping(char c, DataContext dataContext) { } }, this); myModificationChecker = new MergingUpdateQueue("OptionsModificationChecker", 1000, false, rootPanel, this, rootPanel); IdeGlassPaneUtil.installPainter(myOwnDetails.getContentGutter(), mySpotlightPainter, this); /* String visible = PropertiesComponent.getInstance(myProject).getValue(SEARCH_VISIBLE); if (visible == null) { visible = "true"; } */ setFilterFieldVisible(true, false, false); new UiNotifyConnector.Once(myRootPanel, new Activatable() { @Override public void showNotify() { myWindow = SwingUtilities.getWindowAncestor(rootPanel); } @Override public void hideNotify() { } }); } public JPanel getLeftSide() { return myLeftSide; } public JComponent getRightSide() { return myLoadingDecorator.getComponent(); } /** * @see #select(com.intellij.openapi.options.Configurable) */ @Deprecated public ActionCallback select(Class<? extends Configurable> configurableClass) { final Configurable configurable = findConfigurable(configurableClass); if (configurable == null) { return new ActionCallback.Rejected(); } return select(configurable); } /** * @see #findConfigurableById(String) */ @Deprecated @Nullable public <T extends Configurable> T findConfigurable(Class<T> configurableClass) { return myTree.findConfigurable(configurableClass); } @Nullable public SearchableConfigurable findConfigurableById(@NotNull String configurableId) { return myTree.findConfigurableById(configurableId); } public ActionCallback clearSearchAndSelect(Configurable configurable) { clearFilter(); return select(configurable, ""); } public ActionCallback select(Configurable configurable) { if (StringUtil.isEmpty(mySearch.getText())) { return select(configurable, ""); } else { return myFilter.refilterFor(mySearch.getText(), true, true); } } public ActionCallback select(Configurable configurable, final String text) { myFilter.refilterFor(text, false, true); return myTree.select(configurable); } private float readProportion(final float defaultValue, final String propertyName) { float proportion = defaultValue; try { final String p = myProperties.getValue(propertyName); if (p != null) { proportion = Float.valueOf(p); } } catch (NumberFormatException e) { LOG.debug(e); } return proportion; } private ActionCallback processSelected(final Configurable configurable, final Configurable oldConfigurable) { if (isShowing(configurable)) return new ActionCallback.Done(); final ActionCallback result = new ActionCallback(); if (configurable == null) { myOwnDetails.setContent(null); updateSpotlight(true); checkModified(oldConfigurable); result.setDone(); } else { getUiFor(configurable).doWhenDone(new EdtRunnable() { @Override public void runEdt() { if (myDisposed) return; final Configurable current = getContext().getCurrentConfigurable(); if (current != configurable) { result.setRejected(); return; } myHistory.pushQueryPlace(); updateDetails(); myOwnDetails.setContent(myContentWrapper); myOwnDetails.setBannerMinHeight(mySearchWrapper.getHeight()); myOwnDetails.setText(getBannerText(configurable)); final ConfigurableContent content = myConfigurable2Content.get(current); content.setText(getBannerText(configurable)); content.setBannerActions(new Action[]{new ResetAction(configurable)}); content.updateBannerActions(); myLoadingDecorator.stopLoading(); updateSpotlight(false); checkModified(oldConfigurable); checkModified(configurable); if (myTree.myBuilder.getSelectedElements().size() == 0) { select(configurable).notify(result); } else { result.setDone(); } } }); } return result; } private static void assertIsDispatchThread() { ApplicationManager.getApplication().assertIsDispatchThread(); } private ActionCallback getUiFor(final Configurable configurable) { assertIsDispatchThread(); if (myDisposed) { return new ActionCallback.Rejected(); } final ActionCallback result = new ActionCallback(); if (!myConfigurable2Content.containsKey(configurable)) { final ActionCallback readyCallback = myConfigurable2LoadCallback.get(configurable); if (readyCallback != null) { return readyCallback; } myConfigurable2LoadCallback.put(configurable, result); myLoadingDecorator.startLoading(false); final Application app = ApplicationManager.getApplication(); Runnable action = new Runnable() { @Override public void run() { UIUtil.invokeAndWaitIfNeeded(new Runnable() { @Override public void run() { if (myProject.isDisposed()) { result.setRejected(); } else { initConfigurable(configurable).notifyWhenDone(result); } } }); } }; if (app.isUnitTestMode()) { action.run(); } else { app.executeOnPooledThread(action); } } else { result.setDone(); } return result; } private ActionCallback initConfigurable(@NotNull final Configurable configurable) { final ActionCallback result = new ActionCallback(); final ConfigurableContent content; if (configurable instanceof MasterDetails) { content = new Details((MasterDetails)configurable); } else { content = new Simple(configurable); } if (!myConfigurable2Content.containsKey(configurable)) { if (configurable instanceof Place.Navigator) { ((Place.Navigator)configurable).setHistory(myHistory); } configurable.reset(); } UIUtil.invokeLaterIfNeeded(new Runnable() { @Override public void run() { if (myDisposed) return; myConfigurable2Content.put(configurable, content); result.setDone(); } }); return result; } private void updateSpotlight(boolean now) { if (now) { final boolean success = mySpotlightPainter.updateForCurrentConfigurable(); if (!success) { updateSpotlight(false); } } else { mySpotlightUpdate.queue(new Update(this) { @Override public void run() { final boolean success = mySpotlightPainter.updateForCurrentConfigurable(); if (!success) { updateSpotlight(false); } } }); } } private String[] getBannerText(Configurable configurable) { final List<Configurable> list = myTree.getPathToRoot(configurable); final String[] result = new String[list.size()]; int add = 0; for (int i = list.size() - 1; i >= 0; i--) { result[add++] = list.get(i).getDisplayName().replace('\n', ' '); } return result; } private void checkModified(final Configurable configurable) { fireModification(configurable); } private void fireModification(final Configurable actual) { Collection<Configurable> toCheck = collectAllParentsAndSiblings(actual); for (Configurable configurable : toCheck) { fireModificationForItem(configurable); } } private Collection<Configurable> collectAllParentsAndSiblings(final Configurable actual) { ArrayList<Configurable> result = new ArrayList<Configurable>(); Configurable nearestParent = getContext().getParentConfigurable(actual); if (nearestParent != null) { Configurable parent = nearestParent; while (parent != null) { result.add(parent); parent = getContext().getParentConfigurable(parent); } result.addAll(getContext().getChildren(nearestParent)); } else { result.add(actual); } return result; } private void fireModificationForItem(final Configurable configurable) { if (configurable != null) { if (!myConfigurable2Content.containsKey(configurable) && ConfigurableWrapper.hasOwnContent(configurable)) { ApplicationManager.getApplication().invokeLater(new Runnable() { @Override public void run() { if (myDisposed) return; initConfigurable(configurable).doWhenDone(new Runnable() { @Override public void run() { if (myDisposed) return; fireModificationInt(configurable); } }); } }); } else if (myConfigurable2Content.containsKey(configurable)) { fireModificationInt(configurable); } } } private void fireModificationInt(final Configurable configurable) { if (configurable.isModified()) { getContext().fireModifiedAdded(configurable, null); } else if (!configurable.isModified() && !getContext().getErrors().containsKey(configurable)) { getContext().fireModifiedRemoved(configurable, null); } } private void updateDetails() { final Configurable current = getContext().getCurrentConfigurable(); assert current != null; final ConfigurableContent content = myConfigurable2Content.get(current); content.set(myContentWrapper); } private boolean isShowing(Configurable configurable) { final ConfigurableContent content = myConfigurable2Content.get(configurable); return content != null && content.isShowing(); } @Nullable public String getHelpTopic() { Configurable current = getContext().getCurrentConfigurable(); while (current != null) { String topic = current.getHelpTopic(); if (topic != null) return topic; current = getContext().getParentConfigurable(current); } return null; } public boolean isFilterFieldVisible() { return mySearch.getParent() == mySearchWrapper; } public void setFilterFieldVisible(final boolean visible, boolean requestFocus, boolean checkFocus) { if (isFilterFieldVisible() && checkFocus && requestFocus && !isSearchFieldFocused()) { IdeFocusManager.getGlobalInstance().doWhenFocusSettlesDown(() -> IdeFocusManager.getGlobalInstance().requestFocus(mySearch, true)); return; } mySearchWrapper.setContent(visible ? mySearch : null); myLeftSide.revalidate(); myLeftSide.repaint(); if (visible && requestFocus) { IdeFocusManager.getGlobalInstance().doWhenFocusSettlesDown(() -> IdeFocusManager.getGlobalInstance().requestFocus(mySearch, true)); } } public boolean isSearchFieldFocused() { return mySearch.getTextEditor().isFocusOwner(); } public void repaint() { myRootPanel.invalidate(); myRootPanel.repaint(); } private class ResetAction extends AbstractAction { Configurable myConfigurable; ResetAction(final Configurable configurable) { myConfigurable = configurable; putValue(NAME, "Reset"); putValue(SHORT_DESCRIPTION, "Rollback changes for this configuration element"); } @Override public void actionPerformed(final ActionEvent e) { reset(myConfigurable, true); checkModified(myConfigurable); } @Override public boolean isEnabled() { return myContext.isModified(myConfigurable) || getContext().getErrors().containsKey(myConfigurable); } } private static class ContentWrapper extends NonOpaquePanel { private final JLabel myErrorLabel; private JComponent mySimpleContent; private ConfigurationException myException; private JComponent myMaster; private JComponent myToolbar; private DetailsComponent myDetails; private final Splitter mySplitter = new OnePixelSplitter(false); private JPanel myLeft = new JPanel(new BorderLayout()); public float myLastSplitterProportion; private ContentWrapper() { setLayout(new BorderLayout()); myErrorLabel = new JLabel(); myErrorLabel.setOpaque(true); myErrorLabel.setBackground(LightColors.RED); myLeft = new JPanel(new BorderLayout()); mySplitter.addPropertyChangeListener(Splitter.PROP_PROPORTION, new PropertyChangeListener() { @Override public void propertyChange(final PropertyChangeEvent evt) { myLastSplitterProportion = ((Float)evt.getNewValue()).floatValue(); } }); } void setContent(final JComponent component, ConfigurationException e, @NotNull Configurable configurable) { if (component != null && mySimpleContent == component && myException == e) { return; } removeAll(); if (component != null) { boolean noMargin = ConfigurableWrapper.isNoMargin(configurable); JComponent wrapComponent = component; if (!noMargin) { wrapComponent = JBUI.Panels.simplePanel().addToCenter(wrapComponent); wrapComponent.setBorder(new EmptyBorder(UIUtil.PANEL_SMALL_INSETS)); } boolean noScroll = ConfigurableWrapper.isNoScroll(configurable); if (!noScroll) { JScrollPane scroll = ScrollPaneFactory.createScrollPane(wrapComponent, true); scroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); scroll.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); add(scroll, BorderLayout.CENTER); } else { add(wrapComponent, BorderLayout.CENTER); } } if (e != null) { myErrorLabel.setText(UIUtil.toHtml(e.getMessage())); add(myErrorLabel, BorderLayout.NORTH); } mySimpleContent = component; myException = e; myMaster = null; myToolbar = null; myDetails = null; mySplitter.setFirstComponent(null); mySplitter.setSecondComponent(null); } void setContent(JComponent master, JComponent toolbar, DetailsComponent details, ConfigurationException e) { if (myMaster == master && myToolbar == toolbar && myDetails == details && myException == e) return; myMaster = master; myToolbar = toolbar; myDetails = details; myException = e; removeAll(); myLeft.removeAll(); myLeft.add(myToolbar, BorderLayout.NORTH); myLeft.add(myMaster, BorderLayout.CENTER); myDetails.setBannerMinHeight(myToolbar.getPreferredSize().height); mySplitter.setFirstComponent(myLeft); mySplitter.setSecondComponent(myDetails.getComponent()); mySplitter.setProportion(myLastSplitterProportion); add(mySplitter, BorderLayout.CENTER); mySimpleContent = null; } @Override public boolean isNull() { final boolean superNull = super.isNull(); if (superNull) return superNull; if (myMaster == null) { return NullableComponent.Check.isNull(mySimpleContent); } else { return NullableComponent.Check.isNull(myMaster); } } } public void reset(Configurable configurable, boolean notify) { configurable.reset(); if (notify) { getContext().fireReset(configurable); } } public void apply() { Map<Configurable, ConfigurationException> errors = new LinkedHashMap<Configurable, ConfigurationException>(); final Set<Configurable> modified = getContext().getModified(); for (Configurable each : modified) { try { each.apply(); if (!each.isModified()) { getContext().fireModifiedRemoved(each, null); } } catch (ConfigurationException e) { errors.put(each, e); LOG.debug(e); } } getContext().fireErrorsChanged(errors, null); if (!errors.isEmpty()) { myTree.select(errors.keySet().iterator().next()); } } @Override public Object getData(@NonNls final String dataId) { if (KEY.is(dataId)) { return this; } return History.KEY.is(dataId) ? myHistory : null; } public JTree getPreferredFocusedComponent() { return myTree.getTree(); } private class Filter extends ElementFilter.Active.Impl<SimpleNode> { SearchableOptionsRegistrar myIndex = SearchableOptionsRegistrar.getInstance(); Set<Configurable> myFiltered = null; ConfigurableHit myHits; boolean myUpdateEnabled = true; private Configurable myLastSelected; @Override public boolean shouldBeShowing(final SimpleNode value) { if (myFiltered == null) return true; if (value instanceof OptionsTree.EditorNode) { final OptionsTree.EditorNode node = (OptionsTree.EditorNode)value; return myFiltered.contains(node.getConfigurable()) || isChildOfNameHit(node); } return true; } private boolean isChildOfNameHit(OptionsTree.EditorNode node) { if (myHits != null) { OptionsTree.Base eachParent = node; while (eachParent != null) { if (eachParent instanceof OptionsTree.EditorNode) { final OptionsTree.EditorNode eachEditorNode = (OptionsTree.EditorNode)eachParent; if (myHits.getNameFullHits().contains(eachEditorNode.myConfigurable)) return true; } eachParent = (OptionsTree.Base)eachParent.getParent(); } return false; } return false; } public ActionCallback refilterFor(String text, boolean adjustSelection, final boolean now) { try { myUpdateEnabled = false; mySearch.setText(text); } finally { myUpdateEnabled = true; } return update(DocumentEvent.EventType.CHANGE, adjustSelection, now); } public void clearTemporary() { myContext.setHoldingFilter(false); updateSpotlight(false); } public void reenable() { myContext.setHoldingFilter(true); updateSpotlight(false); } public ActionCallback update(DocumentEvent.EventType type, boolean adjustSelection, boolean now) { if (!myUpdateEnabled) return new ActionCallback.Rejected(); final String text = mySearch.getText(); if (getFilterText().length() == 0) { myContext.setHoldingFilter(false); myFiltered = null; } else { myContext.setHoldingFilter(true); myHits = myIndex.getConfigurables(myConfigurables, type, myFiltered, text, myProject); myFiltered = myHits.getAll(); } if (myFiltered != null && myFiltered.isEmpty()) { mySearch.getTextEditor().setBackground(LightColors.RED); } else { mySearch.getTextEditor().setBackground(UIUtil.getTextFieldBackground()); } final Configurable current = getContext().getCurrentConfigurable(); boolean shouldMoveSelection = true; if (myHits != null && (myHits.getNameFullHits().contains(current) || myHits.getContentHits().contains(current))) { shouldMoveSelection = false; } if (shouldMoveSelection && type != DocumentEvent.EventType.INSERT && (myFiltered == null || myFiltered.contains(current))) { shouldMoveSelection = false; } Configurable toSelect = adjustSelection ? current : null; if (shouldMoveSelection && myHits != null) { if (!myHits.getNameHits().isEmpty()) { toSelect = suggestToSelect(myHits.getNameHits(), myHits.getNameFullHits()); } else if (!myHits.getContentHits().isEmpty()) { toSelect = suggestToSelect(myHits.getContentHits(), null); } } updateSpotlight(false); if ((myFiltered == null || !myFiltered.isEmpty()) && toSelect == null && myLastSelected != null) { toSelect = myLastSelected; myLastSelected = null; } if (toSelect == null && current != null) { myLastSelected = current; } final ActionCallback callback = fireUpdate(adjustSelection ? myTree.findNodeFor(toSelect) : null, adjustSelection, now); myFilterDocumentWasChanged = true; return callback; } private boolean isEmptyParent(Configurable configurable) { return configurable instanceof SearchableConfigurable.Parent && !((SearchableConfigurable.Parent)configurable).hasOwnContent(); } @Nullable private Configurable suggestToSelect(Set<Configurable> set, Set<Configurable> fullHits) { Configurable candidate = null; for (Configurable each : set) { if (fullHits != null && fullHits.contains(each)) return each; if (!isEmptyParent(each) && candidate == null) { candidate = each; } } return candidate; } } @Override public ActionCallback navigateTo(@Nullable final Place place, final boolean requestFocus) { final Configurable config = (Configurable)place.getPath("configurable"); final String filter = (String)place.getPath("filter"); final ActionCallback result = new ActionCallback(); myFilter.refilterFor(filter, false, true).doWhenDone(new Runnable() { @Override public void run() { myTree.select(config).notifyWhenDone(result); } }); return result; } @Override public void queryPlace(@NotNull final Place place) { final Configurable current = getContext().getCurrentConfigurable(); place.putPath("configurable", current); place.putPath("filter", getFilterText()); if (current instanceof Place.Navigator) { ((Place.Navigator)current).queryPlace(place); } } @Override public void dispose() { assertIsDispatchThread(); if (myDisposed) { return; } myDisposed = true; //myProperties.setValue(MAIN_SPLITTER_PROPORTION, String.valueOf(myMainSplitter.getProportion())); myProperties.setValue(DETAILS_SPLITTER_PROPORTION, String.valueOf(myContentWrapper.myLastSplitterProportion)); myProperties.setValue(SEARCH_VISIBLE, Boolean.valueOf(isFilterFieldVisible()).toString()); Toolkit.getDefaultToolkit().removeAWTEventListener(this); final Set<Configurable> configurables = new HashSet<Configurable>(); configurables.addAll(myConfigurable2Content.keySet()); configurables.addAll(myConfigurable2LoadCallback.keySet()); for (final Configurable each : configurables) { ActionCallback loadCb = myConfigurable2LoadCallback.get(each); if (loadCb != null) { loadCb.doWhenProcessed(new Runnable() { @Override public void run() { assertIsDispatchThread(); each.disposeUIResources(); } }); } else { each.disposeUIResources(); } } Disposer.clearOwnFields(this, Conditions.<Field>alwaysTrue()); } public OptionsEditorContext getContext() { return myContext; } private class MyColleague extends OptionsEditorColleague.Adapter { @Override public ActionCallback onSelected(final Configurable configurable, final Configurable oldConfigurable) { return processSelected(configurable, oldConfigurable); } @Override public ActionCallback onModifiedRemoved(final Configurable configurable) { return updateIfCurrent(configurable); } @Override public ActionCallback onModifiedAdded(final Configurable configurable) { return updateIfCurrent(configurable); } @Override public ActionCallback onErrorsChanged() { return updateIfCurrent(getContext().getCurrentConfigurable()); } private ActionCallback updateIfCurrent(final Configurable configurable) { if (getContext().getCurrentConfigurable() == configurable && configurable != null) { updateDetails(); final ConfigurableContent content = myConfigurable2Content.get(configurable); content.updateBannerActions(); return new ActionCallback.Done(); } else { return new ActionCallback.Rejected(); } } } public void flushModifications() { fireModification(getContext().getCurrentConfigurable()); } public boolean canApply() { return !getContext().getModified().isEmpty(); } @Override public void eventDispatched(final AWTEvent event) { if (event.getID() == MouseEvent.MOUSE_PRESSED || event.getID() == MouseEvent.MOUSE_RELEASED || event.getID() == MouseEvent.MOUSE_DRAGGED) { final MouseEvent me = (MouseEvent)event; if (SwingUtilities.isDescendingFrom(me.getComponent(), SwingUtilities.getWindowAncestor(myContentWrapper)) || isPopupOverEditor(me.getComponent())) { queueModificationCheck(); myFilter.clearTemporary(); } } else if (event.getID() == KeyEvent.KEY_PRESSED || event.getID() == KeyEvent.KEY_RELEASED) { final KeyEvent ke = (KeyEvent)event; if (SwingUtilities.isDescendingFrom(ke.getComponent(), myContentWrapper)) { queueModificationCheck(); } } } private void queueModificationCheck() { final Configurable configurable = getContext().getCurrentConfigurable(); myModificationChecker.queue(new Update(this) { @Override public void run() { checkModified(configurable); } @Override public boolean isExpired() { return getContext().getCurrentConfigurable() != configurable; } }); } private boolean isPopupOverEditor(Component c) { final Window wnd = SwingUtilities.getWindowAncestor(c); return (wnd instanceof JWindow || wnd instanceof JDialog && ((JDialog)wnd).getModalityType() == Dialog.ModalityType.MODELESS) && myWindow != null && wnd.getParent() == myWindow; } private static class MySearchField extends SearchTextField { private boolean myDelegatingNow; private MySearchField() { super(false); addKeyListener(new KeyAdapter() { }); } @Override protected boolean preprocessEventForTextField(final KeyEvent e) { final KeyStroke stroke = KeyStroke.getKeyStrokeForEvent(e); if (!myDelegatingNow) { if ("pressed ESCAPE".equals(stroke.toString()) && getText().length() > 0) { setText(""); // reset filter on ESC return true; } if (getTextEditor().isFocusOwner()) { try { myDelegatingNow = true; boolean treeNavigation = stroke.getModifiers() == 0 && (stroke.getKeyCode() == KeyEvent.VK_UP || stroke.getKeyCode() == KeyEvent.VK_DOWN); if ("pressed ENTER".equals(stroke.toString())) { return true; // avoid closing dialog on ENTER } final Object action = getTextEditor().getInputMap().get(stroke); if (action == null || treeNavigation) { onTextKeyEvent(e); return true; } } finally { myDelegatingNow = false; } } } return false; } protected void onTextKeyEvent(final KeyEvent e) { } } private class SpotlightPainter extends AbstractPainter { Map<Configurable, String> myConfigurableToLastOption = new HashMap<Configurable, String>(); GlassPanel myGP = new GlassPanel(myOwnDetails.getContentGutter()); boolean myVisible; @Override public void executePaint(final Component component, final Graphics2D g) { if (myVisible && myGP.isVisible()) { myGP.paintSpotlight(g, myOwnDetails.getContentGutter()); } } public boolean updateForCurrentConfigurable() { final Configurable current = getContext().getCurrentConfigurable(); if (current != null && !myConfigurable2Content.containsKey(current)) { return ApplicationManager.getApplication().isUnitTestMode(); } String text = getFilterText(); try { final boolean sameText = myConfigurableToLastOption.containsKey(current) && text.equals(myConfigurableToLastOption.get(current)); if (current == null) { myVisible = false; myGP.clear(); return true; } SearchableConfigurable searchable; if (current instanceof SearchableConfigurable) { searchable = (SearchableConfigurable)current; } else { searchable = new SearachableWrappper(current); } myGP.clear(); final Runnable runnable = SearchUtil.lightOptions(searchable, myContentWrapper, text, myGP); if (runnable != null) { myVisible = true;//myContext.isHoldingFilter(); runnable.run(); boolean pushFilteringFurther = true; if (sameText) { pushFilteringFurther = false; } else { if (myFilter.myHits != null) { pushFilteringFurther = !myFilter.myHits.getNameHits().contains(current); } } final Runnable ownSearch = searchable.enableSearch(text); if (pushFilteringFurther && ownSearch != null) { ownSearch.run(); } fireNeedsRepaint(myOwnDetails.getComponent()); } else { myVisible = false; } } finally { myConfigurableToLastOption.put(current, text); } return true; } @Override public boolean needsRepaint() { return true; } } private String getFilterText() { return mySearch.getText() != null ? mySearch.getText().trim() : ""; } private static class SearachableWrappper implements SearchableConfigurable { private final Configurable myConfigurable; private SearachableWrappper(final Configurable configurable) { myConfigurable = configurable; } @Override @NotNull public String getId() { return myConfigurable.getClass().getName(); } @Override public Runnable enableSearch(final String option) { return null; } @Override @Nls public String getDisplayName() { return myConfigurable.getDisplayName(); } @Override public String getHelpTopic() { return myConfigurable.getHelpTopic(); } @Override public JComponent createComponent() { return myConfigurable.createComponent(); } @Override public boolean isModified() { return myConfigurable.isModified(); } @Override public void apply() throws ConfigurationException { myConfigurable.apply(); } @Override public void reset() { myConfigurable.reset(); } @Override public void disposeUIResources() { myConfigurable.disposeUIResources(); } } private abstract static class ConfigurableContent { abstract void set(ContentWrapper wrapper); abstract boolean isShowing(); abstract void setBannerActions(Action[] actions); abstract void updateBannerActions(); abstract void setText(final String[] bannerText); } private class Simple extends ConfigurableContent { JComponent myComponent; Configurable myConfigurable; @RequiredDispatchThread Simple(final Configurable configurable) { myConfigurable = configurable; myComponent = ConfigurableUIMigrationUtil.createComponent(configurable); if (myComponent != null) { final Object clientProperty = myComponent.getClientProperty(NOT_A_NEW_COMPONENT); if (clientProperty != null && ApplicationManager.getApplication().isInternal()) { LOG.warn(String.format( "Settings component for '%s' MUST be recreated, please dispose it in disposeUIResources() and create a new instance in createComponent()!", configurable.getClass().getCanonicalName())); } else { myComponent.putClientProperty(NOT_A_NEW_COMPONENT, Boolean.TRUE); } } } @Override void set(final ContentWrapper wrapper) { myOwnDetails.setDetailsModeEnabled(true); wrapper.setContent(myComponent, getContext().getErrors().get(myConfigurable), myConfigurable); } @Override boolean isShowing() { return myComponent != null && myComponent.isShowing(); } @Override void setBannerActions(final Action[] actions) { myOwnDetails.setBannerActions(actions); } @Override void updateBannerActions() { myOwnDetails.updateBannerActions(); } @Override void setText(final String[] bannerText) { myOwnDetails.setText(bannerText); } } private class Details extends ConfigurableContent { MasterDetails myConfigurable; DetailsComponent myDetails; JComponent myMaster; JComponent myToolbar; Details(final MasterDetails configurable) { myConfigurable = configurable; myConfigurable.initUi(); myDetails = myConfigurable.getDetails(); myMaster = myConfigurable.getMaster(); myToolbar = myConfigurable.getToolbar(); } @Override void set(final ContentWrapper wrapper) { myOwnDetails.setDetailsModeEnabled(false); myDetails.setPrefix(getBannerText((Configurable)myConfigurable)); wrapper.setContent(myMaster, myToolbar, myDetails, getContext().getErrors().get(myConfigurable)); } @Override void setBannerActions(final Action[] actions) { myDetails.setBannerActions(actions); } @Override boolean isShowing() { return myDetails.getComponent().isShowing(); } @Override void updateBannerActions() { myDetails.updateBannerActions(); } @Override void setText(final String[] bannerText) { myDetails.update(); } } public void clearFilter() { mySearch.setText(""); } @Override public void setHistory(final History history) { } }