| /* |
| * Copyright 2000-2014 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.history.integration.ui.views; |
| |
| import com.intellij.CommonBundle; |
| import com.intellij.history.core.LocalHistoryFacade; |
| import com.intellij.history.integration.IdeaGateway; |
| import com.intellij.history.integration.LocalHistoryBundle; |
| import com.intellij.history.integration.LocalHistoryImpl; |
| import com.intellij.history.integration.revertion.Reverter; |
| import com.intellij.history.integration.ui.models.FileDifferenceModel; |
| import com.intellij.history.integration.ui.models.HistoryDialogModel; |
| import com.intellij.history.integration.ui.models.RevisionProcessingProgress; |
| import com.intellij.history.utils.LocalHistoryLog; |
| import com.intellij.icons.AllIcons; |
| import com.intellij.ide.actions.ContextHelpAction; |
| import com.intellij.ide.actions.ShowFilePathAction; |
| import com.intellij.ide.ui.SplitterProportionsDataImpl; |
| import com.intellij.openapi.Disposable; |
| import com.intellij.openapi.actionSystem.*; |
| import com.intellij.openapi.diff.DiffContent; |
| import com.intellij.openapi.diff.SimpleDiffRequest; |
| import com.intellij.openapi.help.HelpManager; |
| import com.intellij.openapi.progress.ProgressIndicator; |
| import com.intellij.openapi.progress.Task; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.ui.*; |
| import com.intellij.openapi.ui.popup.Balloon; |
| import com.intellij.openapi.ui.popup.JBPopupFactory; |
| import com.intellij.openapi.util.Computable; |
| import com.intellij.openapi.util.Couple; |
| import com.intellij.openapi.util.Disposer; |
| import com.intellij.openapi.util.Pair; |
| import com.intellij.openapi.util.io.FileUtil; |
| import com.intellij.openapi.vcs.VcsException; |
| import com.intellij.openapi.vcs.changes.patch.CreatePatchConfigurationPanel; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.ui.*; |
| import com.intellij.ui.awt.RelativePoint; |
| import com.intellij.ui.components.JBLayeredPane; |
| import com.intellij.util.Consumer; |
| import com.intellij.util.ImageLoader; |
| import com.intellij.util.ui.AbstractLayoutManager; |
| import com.intellij.util.ui.AnimatedIcon; |
| import com.intellij.util.ui.AsyncProcessIcon; |
| import com.intellij.util.ui.update.MergingUpdateQueue; |
| import com.intellij.util.ui.update.Update; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import javax.swing.*; |
| import javax.swing.border.Border; |
| import java.awt.*; |
| import java.io.File; |
| import java.io.IOException; |
| import java.lang.reflect.InvocationTargetException; |
| import java.util.List; |
| |
| import static com.intellij.history.integration.LocalHistoryBundle.message; |
| |
| public abstract class HistoryDialog<T extends HistoryDialogModel> extends FrameWrapper { |
| private static final int UPDATE_DIFFS = 1; |
| private static final int UPDATE_REVS = UPDATE_DIFFS + 1; |
| |
| protected final Project myProject; |
| protected final IdeaGateway myGateway; |
| protected final VirtualFile myFile; |
| private Splitter mySplitter; |
| private RevisionsList myRevisionsList; |
| private MyDiffContainer myDiffView; |
| private ActionToolbar myToolBar; |
| |
| private T myModel; |
| |
| private MergingUpdateQueue myUpdateQueue; |
| private boolean isUpdating; |
| |
| protected HistoryDialog(@NotNull Project project, IdeaGateway gw, VirtualFile f, boolean doInit) { |
| super(project); |
| myProject = project; |
| myGateway = gw; |
| myFile = f; |
| |
| setProject(project); |
| setDimensionKey(getPropertiesKey()); |
| setImage(ImageLoader.loadFromResource("/diff/Diff.png")); |
| closeOnEsc(); |
| |
| if (doInit) init(); |
| } |
| |
| protected void init() { |
| LocalHistoryFacade facade = LocalHistoryImpl.getInstanceImpl().getFacade(); |
| |
| myModel = createModel(facade); |
| setTitle(myModel.getTitle()); |
| JComponent root = createComponent(); |
| setComponent(root); |
| |
| setPreferredFocusedComponent(showRevisionsList() ? myRevisionsList.getComponent() : myDiffView); |
| |
| myUpdateQueue = new MergingUpdateQueue(getClass() + ".revisionsUpdate", 500, true, root, this, null, false); |
| myUpdateQueue.setRestartTimerOnAdd(true); |
| |
| facade.addListener(new LocalHistoryFacade.Listener() { |
| public void changeSetFinished() { |
| scheduleRevisionsUpdate(null); |
| } |
| }, this); |
| |
| scheduleRevisionsUpdate(null); |
| } |
| |
| protected void scheduleRevisionsUpdate(@Nullable final Consumer<T> configRunnable) { |
| doScheduleUpdate(UPDATE_REVS, new Computable<Runnable>() { |
| public Runnable compute() { |
| synchronized (myModel) { |
| if (configRunnable != null) configRunnable.consume(myModel); |
| myModel.clearRevisions(); |
| myModel.getRevisions();// force load |
| } |
| return new Runnable() { |
| public void run() { |
| myRevisionsList.updateData(myModel); |
| } |
| }; |
| } |
| }); |
| } |
| |
| protected abstract T createModel(LocalHistoryFacade vcs); |
| |
| protected JComponent createComponent() { |
| JPanel root = new JPanel(new BorderLayout()); |
| |
| ExcludingTraversalPolicy traversalPolicy = new ExcludingTraversalPolicy(); |
| root.setFocusTraversalPolicy(traversalPolicy); |
| root.setFocusTraversalPolicyProvider(true); |
| |
| Pair<JComponent, Dimension> diffAndToolbarSize = createDiffPanel(root, traversalPolicy); |
| myDiffView = new MyDiffContainer(diffAndToolbarSize.first); |
| Disposer.register(this, myDiffView); |
| |
| JComponent revisionsSide = createRevisionsSide(diffAndToolbarSize.second); |
| |
| if (showRevisionsList()) { |
| mySplitter = new Splitter(false, 0.3f); |
| |
| mySplitter.setFirstComponent(revisionsSide); |
| mySplitter.setSecondComponent(myDiffView); |
| |
| restoreSplitterProportion(); |
| |
| root.add(mySplitter); |
| setDiffBorder(IdeBorderFactory.createBorder(SideBorder.TOP | SideBorder.LEFT)); |
| } |
| else { |
| setDiffBorder(IdeBorderFactory.createBorder(SideBorder.TOP | SideBorder.BOTTOM)); |
| root.add(myDiffView); |
| } |
| |
| return root; |
| } |
| |
| protected boolean showRevisionsList() { |
| return true; |
| } |
| |
| protected abstract void setDiffBorder(Border border); |
| |
| @Override |
| public void dispose() { |
| saveSplitterProportion(); |
| super.dispose(); |
| } |
| |
| protected abstract Pair<JComponent, Dimension> createDiffPanel(JPanel root, ExcludingTraversalPolicy traversalPolicy); |
| |
| private JComponent createRevisionsSide(Dimension prefToolBarSize) { |
| ActionGroup actions = createRevisionsActions(); |
| |
| myToolBar = createRevisionsToolbar(actions); |
| myRevisionsList = new RevisionsList(new RevisionsList.SelectionListener() { |
| public void revisionsSelected(final int first, final int last) { |
| scheduleDiffUpdate(Couple.of(first, last)); |
| } |
| }); |
| addPopupMenuToComponent(myRevisionsList.getComponent(), actions); |
| |
| |
| JPanel result = new JPanel(new BorderLayout()); |
| JPanel toolBarPanel = new JPanel(new BorderLayout()); |
| toolBarPanel.add(myToolBar.getComponent()); |
| if (prefToolBarSize != null) { |
| toolBarPanel.setPreferredSize(new Dimension(1, prefToolBarSize.height)); |
| } |
| result.add(toolBarPanel, BorderLayout.NORTH); |
| JScrollPane scrollPane = ScrollPaneFactory.createScrollPane(myRevisionsList.getComponent()); |
| scrollPane.setBorder(IdeBorderFactory.createBorder(SideBorder.TOP | SideBorder.RIGHT)); |
| result.add(scrollPane, BorderLayout.CENTER); |
| |
| return result; |
| } |
| |
| private ActionToolbar createRevisionsToolbar(ActionGroup actions) { |
| ActionManager am = ActionManager.getInstance(); |
| return am.createActionToolbar(ActionPlaces.UNKNOWN, actions, true); |
| } |
| |
| private ActionGroup createRevisionsActions() { |
| DefaultActionGroup result = new DefaultActionGroup(); |
| result.add(new RevertAction()); |
| result.add(new CreatePatchAction()); |
| result.add(Separator.getInstance()); |
| result.add(new ContextHelpAction(getHelpId())); |
| return result; |
| } |
| |
| private void addPopupMenuToComponent(JComponent comp, final ActionGroup ag) { |
| comp.addMouseListener(new PopupHandler() { |
| public void invokePopup(Component c, int x, int y) { |
| ActionPopupMenu m = createPopupMenu(ag); |
| m.getComponent().show(c, x, y); |
| } |
| }); |
| } |
| |
| private ActionPopupMenu createPopupMenu(ActionGroup ag) { |
| ActionManager m = ActionManager.getInstance(); |
| return m.createActionPopupMenu(ActionPlaces.UNKNOWN, ag); |
| } |
| |
| private void scheduleDiffUpdate(@Nullable final Couple<Integer> toSelect) { |
| doScheduleUpdate(UPDATE_DIFFS, new Computable<Runnable>() { |
| public Runnable compute() { |
| synchronized (myModel) { |
| if (toSelect == null) { |
| myModel.resetSelection(); |
| } |
| else { |
| myModel.selectRevisions(toSelect.first, toSelect.second); |
| } |
| return doUpdateDiffs(myModel); |
| } |
| } |
| }); |
| } |
| |
| private void doScheduleUpdate(int id, final Computable<Runnable> update) { |
| myUpdateQueue.queue(new Update(HistoryDialog.this, id) { |
| @Override |
| public boolean canEat(Update update1) { |
| return getPriority() >= update1.getPriority(); |
| } |
| |
| public void run() { |
| if (isDisposed() || myProject.isDisposed()) return; |
| |
| invokeAndWait(new Runnable() { |
| public void run() { |
| if (isDisposed() || myProject.isDisposed()) return; |
| |
| isUpdating = true; |
| updateActions(); |
| myDiffView.startUpdating(); |
| } |
| }); |
| |
| Runnable apply = null; |
| try { |
| apply = update.compute(); |
| } |
| catch (Exception e) { |
| LocalHistoryLog.LOG.error(e); |
| } |
| |
| final Runnable finalApply = apply; |
| invokeAndWait(new Runnable() { |
| public void run() { |
| if (isDisposed() || myProject.isDisposed()) return; |
| |
| isUpdating = false; |
| if (finalApply != null) { |
| try { |
| finalApply.run(); |
| } |
| catch (Exception e) { |
| LocalHistoryLog.LOG.error(e); |
| } |
| } |
| updateActions(); |
| myDiffView.finishUpdating(); |
| } |
| }); |
| } |
| }); |
| } |
| |
| private void invokeAndWait(Runnable runnable) { |
| try { |
| if (SwingUtilities.isEventDispatchThread()) { |
| runnable.run(); |
| } |
| else { |
| SwingUtilities.invokeAndWait(runnable); |
| } |
| } |
| catch (InterruptedException e) { |
| throw new RuntimeException(e); |
| } |
| catch (InvocationTargetException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| protected void updateActions() { |
| if (showRevisionsList()) { |
| myToolBar.updateActionsImmediately(); |
| } |
| } |
| |
| protected abstract Runnable doUpdateDiffs(T model); |
| |
| protected SimpleDiffRequest createDifference(final FileDifferenceModel m) { |
| final SimpleDiffRequest r = new SimpleDiffRequest(myProject, m.getTitle()); |
| |
| new Task.Modal(myProject, message("message.processing.revisions"), false) { |
| public void run(@NotNull ProgressIndicator i) { |
| RevisionProcessingProgressAdapter p = new RevisionProcessingProgressAdapter(i); |
| p.processingLeftRevision(); |
| DiffContent left = m.getLeftDiffContent(p); |
| |
| p.processingRightRevision(); |
| DiffContent right = m.getRightDiffContent(p); |
| |
| r.setContents(left, right); |
| r.setContentTitles(m.getLeftTitle(p), m.getRightTitle(p)); |
| } |
| }.queue(); |
| |
| return r; |
| } |
| |
| private void saveSplitterProportion() { |
| SplitterProportionsData d = createSplitterData(); |
| d.saveSplitterProportions(mySplitter); |
| d.externalizeToDimensionService(getPropertiesKey()); |
| } |
| |
| private void restoreSplitterProportion() { |
| SplitterProportionsData d = createSplitterData(); |
| d.externalizeFromDimensionService(getPropertiesKey()); |
| d.restoreSplitterProportions(mySplitter); |
| } |
| |
| private SplitterProportionsData createSplitterData() { |
| return new SplitterProportionsDataImpl(); |
| } |
| |
| protected String getPropertiesKey() { |
| return getClass().getName(); |
| } |
| |
| //todo |
| protected abstract String getHelpId(); |
| |
| protected void revert() { |
| revert(myModel.createReverter()); |
| } |
| |
| private boolean isRevertEnabled() { |
| return myModel.isRevertEnabled(); |
| } |
| |
| protected void revert(Reverter r) { |
| try { |
| if (!askForProceeding(r)) return; |
| |
| List<String> errors = r.checkCanRevert(); |
| if (!errors.isEmpty()) { |
| showError(message("message.cannot.revert.because", formatErrors(errors))); |
| return; |
| } |
| r.revert(); |
| |
| showNotification(r.getCommandName()); |
| } |
| catch (IOException e) { |
| showError(message("message.error.during.revert", e)); |
| } |
| } |
| |
| private boolean askForProceeding(Reverter r) throws IOException { |
| List<String> questions = r.askUserForProceeding(); |
| if (questions.isEmpty()) return true; |
| |
| return Messages.showYesNoDialog(myProject, message("message.do.you.want.to.proceed", formatQuestions(questions)), |
| CommonBundle.getWarningTitle(), Messages.getWarningIcon()) == Messages.YES; |
| } |
| |
| private String formatQuestions(List<String> questions) { |
| // format into something like this: |
| // 1) message one |
| // message one continued |
| // 2) message two |
| // message one continued |
| // ... |
| |
| if (questions.size() == 1) return questions.get(0); |
| |
| String result = ""; |
| for (int i = 0; i < questions.size(); i++) { |
| result += (i + 1) + ") " + questions.get(i) + "\n"; |
| } |
| return result.substring(0, result.length() - 1); |
| } |
| |
| private void showNotification(final String title) { |
| SwingUtilities.invokeLater(new Runnable() { |
| public void run() { |
| final Balloon b = |
| JBPopupFactory.getInstance().createHtmlTextBalloonBuilder(title, null, MessageType.INFO.getPopupBackground(), null) |
| .setFadeoutTime(3000) |
| .setShowCallout(false) |
| .createBalloon(); |
| |
| Dimension size = myDiffView.getSize(); |
| RelativePoint point = new RelativePoint(myDiffView, new Point(size.width / 2, size.height / 2)); |
| b.show(point, Balloon.Position.above); |
| } |
| }); |
| } |
| |
| private String formatErrors(List<String> errors) { |
| if (errors.size() == 1) return errors.get(0); |
| |
| String result = ""; |
| for (String e : errors) { |
| result += "\n -" + e; |
| } |
| return result; |
| } |
| |
| private boolean isCreatePatchEnabled() { |
| return myModel.isCreatePatchEnabled(); |
| } |
| |
| private void createPatch() { |
| try { |
| if (!myModel.canPerformCreatePatch()) { |
| showError(message("message.cannot.create.patch.because.of.unavailable.content")); |
| return; |
| } |
| |
| CreatePatchConfigurationPanel p = new CreatePatchConfigurationPanel(myProject); |
| p.setFileName(getDefaultPatchFile()); |
| if (!showAsDialog(p)) return; |
| myModel.createPatch(p.getFileName(), p.isReversePatch()); |
| |
| showNotification(LocalHistoryBundle.message("message.patch.created")); |
| ShowFilePathAction.openFile(new File(p.getFileName())); |
| } |
| catch (VcsException e) { |
| showError(message("message.error.during.create.patch", e)); |
| } |
| catch (IOException e) { |
| showError(message("message.error.during.create.patch", e)); |
| } |
| } |
| |
| private File getDefaultPatchFile() { |
| return FileUtil.findSequentNonexistentFile(new File(myProject.getBasePath()), "local_history", "patch"); |
| } |
| |
| private boolean showAsDialog(CreatePatchConfigurationPanel p) { |
| final DialogBuilder b = new DialogBuilder(myProject); |
| b.setTitle(message("create.patch.dialog.title")); |
| b.setCenterPanel(p.getPanel()); |
| p.installOkEnabledListener(new Consumer<Boolean>() { |
| public void consume(final Boolean aBoolean) { |
| b.setOkActionEnabled(aBoolean); |
| } |
| }); |
| return b.show() == DialogWrapper.OK_EXIT_CODE; |
| } |
| |
| |
| public void showError(String s) { |
| Messages.showErrorDialog(myProject, s, CommonBundle.getErrorTitle()); |
| } |
| |
| protected void showHelp() { |
| HelpManager.getInstance().invokeHelp(getHelpId()); |
| } |
| |
| protected abstract class MyAction extends AnAction { |
| protected MyAction(String text, String description, Icon icon) { |
| super(text, description, icon); |
| } |
| |
| @Override |
| public void actionPerformed(AnActionEvent e) { |
| doPerform(myModel); |
| } |
| |
| protected abstract void doPerform(T model); |
| |
| @Override |
| public void update(AnActionEvent e) { |
| Presentation p = e.getPresentation(); |
| p.setEnabled(isEnabled()); |
| } |
| |
| private boolean isEnabled() { |
| return !isUpdating && isEnabled(myModel); |
| } |
| |
| protected abstract boolean isEnabled(T model); |
| |
| public void performIfEnabled() { |
| if (isEnabled()) doPerform(myModel); |
| } |
| } |
| |
| private class RevertAction extends MyAction { |
| public RevertAction() { |
| super(message("action.revert"), null, AllIcons.Actions.Rollback); |
| } |
| |
| @Override |
| protected void doPerform(T model) { |
| revert(); |
| } |
| |
| @Override |
| protected boolean isEnabled(T model) { |
| return isRevertEnabled(); |
| } |
| } |
| |
| private class CreatePatchAction extends MyAction { |
| public CreatePatchAction() { |
| super(message("action.create.patch"), null, AllIcons.Actions.CreatePatch); |
| } |
| |
| @Override |
| protected void doPerform(T model) { |
| createPatch(); |
| } |
| |
| @Override |
| protected boolean isEnabled(T model) { |
| return isCreatePatchEnabled(); |
| } |
| } |
| |
| private static class RevisionProcessingProgressAdapter implements RevisionProcessingProgress { |
| private final ProgressIndicator myIndicator; |
| |
| public RevisionProcessingProgressAdapter(ProgressIndicator i) { |
| myIndicator = i; |
| } |
| |
| public void processingLeftRevision() { |
| myIndicator.setText(message("message.processing.left.revision")); |
| } |
| |
| public void processingRightRevision() { |
| myIndicator.setText(message("message.processing.right.revision")); |
| } |
| |
| public void processed(int percentage) { |
| myIndicator.setFraction(percentage / 100.0); |
| } |
| } |
| |
| private static class MyDiffContainer extends JBLayeredPane implements Disposable { |
| private AnimatedIcon myIcon = new AsyncProcessIcon.Big(this.getClass().getName()); |
| |
| private JComponent myContent; |
| private JComponent myLoadingPanel; |
| |
| private MyDiffContainer(JComponent content) { |
| setLayout(new MyOverlayLayout()); |
| myContent = content; |
| myLoadingPanel = new JPanel(new MyPanelLayout()); |
| myLoadingPanel.setOpaque(false); |
| myLoadingPanel.add(myIcon); |
| |
| add(myContent); |
| add(myLoadingPanel, JLayeredPane.POPUP_LAYER); |
| |
| finishUpdating(); |
| } |
| |
| public void dispose() { |
| myIcon.dispose(); |
| } |
| |
| public void startUpdating() { |
| myLoadingPanel.setVisible(true); |
| myIcon.resume(); |
| } |
| |
| public void finishUpdating() { |
| myIcon.suspend(); |
| myLoadingPanel.setVisible(false); |
| } |
| |
| private class MyOverlayLayout extends AbstractLayoutManager { |
| public void layoutContainer(Container parent) { |
| myContent.setBounds(0, 0, getWidth(), getHeight()); |
| myLoadingPanel.setBounds(0, 0, getWidth(), getHeight()); |
| } |
| |
| public Dimension preferredLayoutSize(Container parent) { |
| return myContent.getPreferredSize(); |
| } |
| } |
| |
| private class MyPanelLayout extends AbstractLayoutManager { |
| public void layoutContainer(Container parent) { |
| Dimension size = myIcon.getPreferredSize(); |
| myIcon.setBounds((getWidth() - size.width) / 2, (getHeight() - size.height) / 2, size.width, size.height); |
| } |
| |
| public Dimension preferredLayoutSize(Container parent) { |
| return myContent.getPreferredSize(); |
| } |
| } |
| } |
| } |