/*
* Copyright 2000-2015 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.diff.tools.util.base;
import com.intellij.diff.DiffContext;
import com.intellij.diff.contents.DiffContent;
import com.intellij.diff.contents.DocumentContent;
import com.intellij.diff.contents.EmptyContent;
import com.intellij.diff.requests.ContentDiffRequest;
import com.intellij.diff.tools.util.FoldingModelSupport;
import com.intellij.diff.tools.util.base.TextDiffSettingsHolder.TextDiffSettings;
import com.intellij.diff.util.DiffUserDataKeys;
import com.intellij.diff.util.DiffUserDataKeysEx;
import com.intellij.diff.util.DiffUtil;
import com.intellij.icons.AllIcons;
import com.intellij.internal.statistic.UsageTrigger;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.actionSystem.ex.ComboBoxAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.actions.EditorActionUtil;
import com.intellij.openapi.editor.event.EditorMouseEvent;
import com.intellij.openapi.editor.ex.EditorEx;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.util.Condition;
import com.intellij.ui.ToggleActionButton;
import com.intellij.util.EditorPopupHandler;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import consulo.annotations.RequiredDispatchThread;
import javax.swing.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class TextDiffViewerUtil {
public static final Logger LOG = Logger.getInstance(TextDiffViewerUtil.class);
@NotNull
public static List<AnAction> createEditorPopupActions() {
List<AnAction> result = new ArrayList<AnAction>();
result.add(ActionManager.getInstance().getAction("CompareClipboardWithSelection"));
result.add(AnSeparator.getInstance());
ContainerUtil.addAll(result, ((ActionGroup)ActionManager.getInstance().getAction(IdeActions.GROUP_DIFF_EDITOR_POPUP)).getChildren(null));
return result;
}
@NotNull
public static FoldingModelSupport.Settings getFoldingModelSettings(@NotNull DiffContext context) {
TextDiffSettings settings = getTextSettings(context);
return new FoldingModelSupport.Settings(settings.getContextRange(), settings.isExpandByDefault());
}
@NotNull
public static TextDiffSettings getTextSettings(@NotNull DiffContext context) {
TextDiffSettings settings = context.getUserData(TextDiffSettingsHolder.KEY);
if (settings == null) {
settings = TextDiffSettings.getSettings(context.getUserData(DiffUserDataKeys.PLACE));
context.putUserData(TextDiffSettingsHolder.KEY, settings);
if (DiffUtil.isUserDataFlagSet(DiffUserDataKeys.DO_NOT_IGNORE_WHITESPACES, context)) {
settings.setIgnorePolicy(IgnorePolicy.DEFAULT);
}
}
return settings;
}
@NotNull
public static boolean[] checkForceReadOnly(@NotNull DiffContext context, @NotNull ContentDiffRequest request) {
int contentCount = request.getContents().size();
boolean[] result = new boolean[contentCount];
if (DiffUtil.isUserDataFlagSet(DiffUserDataKeys.FORCE_READ_ONLY, request, context)) {
Arrays.fill(result, true);
return result;
}
boolean[] data = request.getUserData(DiffUserDataKeys.FORCE_READ_ONLY_CONTENTS);
if (data != null && data.length == contentCount) {
return data;
}
return result;
}
public static void checkDifferentDocuments(@NotNull ContentDiffRequest request) {
// Actually, this should be a valid case. But it has little practical sense and will require explicit checks everywhere.
// Some listeners will be processed once instead of 2 times, some listeners will cause illegal document modifications.
List<DiffContent> contents = request.getContents();
boolean sameDocuments = false;
for (int i = 0; i < contents.size(); i++) {
for (int j = i + 1; j < contents.size(); j++) {
DiffContent content1 = contents.get(i);
DiffContent content2 = contents.get(j);
if (!(content1 instanceof DocumentContent)) continue;
if (!(content2 instanceof DocumentContent)) continue;
sameDocuments |= ((DocumentContent)content1).getDocument() == ((DocumentContent)content2).getDocument();
}
}
if (sameDocuments) {
StringBuilder message = new StringBuilder();
message.append("DiffRequest with same documents detected\n");
message.append(request.toString()).append("\n");
for (DiffContent content : contents) {
message.append(content.toString()).append("\n");
}
LOG.warn(new Throwable(message.toString()));
}
}
public static boolean areEqualLineSeparators(@NotNull List<? extends DiffContent> contents) {
return areEqualDocumentContentProperties(contents, new Function<DocumentContent, Object>() {
@Override
public Object fun(DocumentContent documentContent) {
return documentContent.getLineSeparator();
}
});
}
public static boolean areEqualCharsets(@NotNull List<? extends DiffContent> contents) {
return areEqualDocumentContentProperties(contents, new Function<DocumentContent, Object>() {
@Override
public Object fun(DocumentContent documentContent) {
return documentContent.getCharset();
}
});
}
private static <T> boolean areEqualDocumentContentProperties(@NotNull List<? extends DiffContent> contents,
@NotNull final Function<DocumentContent, T> propertyGetter) {
List<T> properties = ContainerUtil.mapNotNull(contents, new Function<DiffContent, T>() {
@Override
public T fun(DiffContent content) {
if (content instanceof EmptyContent) return null;
return propertyGetter.fun((DocumentContent)content);
}
});
if (properties.size() < 2) return true;
return ContainerUtil.newHashSet(properties).size() == 1;
}
//
// Actions
//
// TODO: pretty icons ?
public static abstract class ComboBoxSettingAction<T> extends ComboBoxAction implements DumbAware {
private DefaultActionGroup myChildren;
public ComboBoxSettingAction() {
setEnabledInModalContext(true);
}
@RequiredDispatchThread
@Override
public void update(AnActionEvent e) {
Presentation presentation = e.getPresentation();
presentation.setText(getText(getCurrentSetting()));
}
@NotNull
public DefaultActionGroup getPopupGroup() {
initChildren();
return myChildren;
}
@NotNull
@Override
protected DefaultActionGroup createPopupActionGroup(JComponent button) {
initChildren();
return myChildren;
}
private void initChildren() {
if (myChildren == null) {
myChildren = new DefaultActionGroup();
for (T setting : getAvailableSettings()) {
myChildren.add(new MyAction(setting));
}
}
}
@NotNull
protected abstract List<T> getAvailableSettings();
@NotNull
protected abstract String getText(@NotNull T setting);
@NotNull
protected abstract T getCurrentSetting();
protected abstract void applySetting(@NotNull T setting, @NotNull AnActionEvent e);
private class MyAction extends AnAction implements DumbAware {
@NotNull private final T mySetting;
public MyAction(@NotNull T setting) {
super(getText(setting));
setEnabledInModalContext(true);
mySetting = setting;
}
@RequiredDispatchThread
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
applySetting(mySetting, e);
}
}
}
public static abstract class HighlightPolicySettingAction extends ComboBoxSettingAction<HighlightPolicy> {
@NotNull protected final TextDiffSettings mySettings;
public HighlightPolicySettingAction(@NotNull TextDiffSettings settings) {
mySettings = settings;
}
@Override
protected void applySetting(@NotNull HighlightPolicy setting, @NotNull AnActionEvent e) {
if (getCurrentSetting() == setting) return;
UsageTrigger.trigger("diff.TextDiffSettings.HighlightPolicy." + setting.name());
mySettings.setHighlightPolicy(setting);
update(e);
onSettingsChanged();
}
@NotNull
@Override
protected HighlightPolicy getCurrentSetting() {
return mySettings.getHighlightPolicy();
}
@NotNull
@Override
protected String getText(@NotNull HighlightPolicy setting) {
return setting.getText();
}
@NotNull
@Override
protected List<HighlightPolicy> getAvailableSettings() {
return Arrays.asList(HighlightPolicy.values());
}
protected abstract void onSettingsChanged();
}
public static abstract class IgnorePolicySettingAction extends ComboBoxSettingAction<IgnorePolicy> {
@NotNull protected final TextDiffSettings mySettings;
public IgnorePolicySettingAction(@NotNull TextDiffSettings settings) {
mySettings = settings;
}
@Override
protected void applySetting(@NotNull IgnorePolicy setting, @NotNull AnActionEvent e) {
if (getCurrentSetting() == setting) return;
UsageTrigger.trigger("diff.TextDiffSettings.IgnorePolicy." + setting.name());
mySettings.setIgnorePolicy(setting);
update(e);
onSettingsChanged();
}
@NotNull
@Override
protected IgnorePolicy getCurrentSetting() {
return mySettings.getIgnorePolicy();
}
@NotNull
@Override
protected String getText(@NotNull IgnorePolicy setting) {
return setting.getText();
}
@NotNull
@Override
protected List<IgnorePolicy> getAvailableSettings() {
return Arrays.asList(IgnorePolicy.values());
}
protected abstract void onSettingsChanged();
}
public static class ToggleAutoScrollAction extends ToggleActionButton implements DumbAware {
@NotNull protected final TextDiffSettings mySettings;
public ToggleAutoScrollAction(@NotNull TextDiffSettings settings) {
super("Synchronize Scrolling", AllIcons.Actions.SynchronizeScrolling);
mySettings = settings;
setEnabledInModalContext(true);
}
@Override
public boolean isSelected(AnActionEvent e) {
return mySettings.isEnableSyncScroll();
}
@Override
public void setSelected(AnActionEvent e, boolean state) {
mySettings.setEnableSyncScroll(state);
}
}
public static abstract class ToggleExpandByDefaultAction extends ToggleActionButton implements DumbAware {
@NotNull protected final TextDiffSettings mySettings;
public ToggleExpandByDefaultAction(@NotNull TextDiffSettings settings) {
super("Collapse unchanged fragments", AllIcons.Actions.Collapseall);
mySettings = settings;
setEnabledInModalContext(true);
}
@Override
public boolean isVisible() {
return mySettings.getContextRange() != -1;
}
@Override
public boolean isSelected(AnActionEvent e) {
return !mySettings.isExpandByDefault();
}
@Override
public void setSelected(AnActionEvent e, boolean state) {
boolean expand = !state;
if (mySettings.isExpandByDefault() == expand) return;
mySettings.setExpandByDefault(expand);
expandAll(expand);
}
protected abstract void expandAll(boolean expand);
}
public static abstract class ReadOnlyLockAction extends ToggleAction implements DumbAware {
@NotNull protected final DiffContext myContext;
@NotNull protected final TextDiffSettings mySettings;
public ReadOnlyLockAction(@NotNull DiffContext context) {
super("Disable editing", null, AllIcons.Nodes.Padlock);
myContext = context;
mySettings = getTextSettings(context);
setEnabledInModalContext(true);
}
protected void applyDefaults() {
if (isVisible()) { // apply default state
setSelected(null, isSelected(null));
}
}
@RequiredDispatchThread
@Override
public void update(@NotNull AnActionEvent e) {
if (!isVisible()) {
e.getPresentation().setEnabledAndVisible(false);
}
else {
super.update(e);
}
}
@Override
public boolean isSelected(AnActionEvent e) {
return mySettings.isReadOnlyLock();
}
@Override
public void setSelected(AnActionEvent e, boolean state) {
mySettings.setReadOnlyLock(state);
doApply(state);
}
private boolean isVisible() {
return myContext.getUserData(DiffUserDataKeysEx.SHOW_READ_ONLY_LOCK) == Boolean.TRUE && canEdit();
}
protected abstract void doApply(boolean readOnly);
protected abstract boolean canEdit();
}
public static class EditorReadOnlyLockAction extends ReadOnlyLockAction {
private final List<? extends EditorEx> myEditableEditors;
public EditorReadOnlyLockAction(@NotNull DiffContext context, @NotNull List<? extends EditorEx> editableEditors) {
super(context);
myEditableEditors = editableEditors;
applyDefaults();
}
@Override
protected void doApply(boolean readOnly) {
for (EditorEx editor : myEditableEditors) {
editor.setViewer(readOnly);
}
}
@Override
protected boolean canEdit() {
return !myEditableEditors.isEmpty();
}
}
@NotNull
public static List<? extends EditorEx> getEditableEditors(@NotNull List<? extends EditorEx> editors) {
return ContainerUtil.filter(editors, new Condition<EditorEx>() {
@Override
public boolean value(EditorEx editor) {
return !editor.isViewer();
}
});
}
public static class EditorFontSizeSynchronizer implements PropertyChangeListener {
@NotNull private final List<? extends EditorEx> myEditors;
private boolean myDuringUpdate = false;
public EditorFontSizeSynchronizer(@NotNull List<? extends EditorEx> editors) {
myEditors = editors;
}
public void install(@NotNull Disposable disposable) {
if (myEditors.size() < 2) return;
for (EditorEx editor : myEditors) {
editor.addPropertyChangeListener(this, disposable);
}
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (myDuringUpdate) return;
if (!EditorEx.PROP_FONT_SIZE.equals(evt.getPropertyName())) return;
if (evt.getOldValue().equals(evt.getNewValue())) return;
int fontSize = ((Integer)evt.getNewValue()).intValue();
for (EditorEx editor : myEditors) {
if (evt.getSource() != editor) updateEditor(editor, fontSize);
}
}
public void updateEditor(@NotNull EditorEx editor, int fontSize) {
try {
myDuringUpdate = true;
editor.setFontSize(fontSize);
}
finally {
myDuringUpdate = false;
}
}
}
public static class EditorActionsPopup extends EditorPopupHandler {
@NotNull private final List<? extends AnAction> myEditorPopupActions;
public EditorActionsPopup(@NotNull List<? extends AnAction> editorPopupActions) {
myEditorPopupActions = editorPopupActions;
}
public void install(@NotNull List<? extends EditorEx> editors) {
for (EditorEx editor : editors) {
editor.addEditorMouseListener(this);
editor.setContextMenuGroupId(null); // disabling default context menu
}
}
@Override
public void invokePopup(final EditorMouseEvent event) {
if (myEditorPopupActions.isEmpty()) return;
ActionGroup group = new DefaultActionGroup(myEditorPopupActions);
EditorPopupHandler handler = EditorActionUtil.createEditorPopupHandler(group);
handler.invokePopup(event);
}
}
}