/* * Copyright 2000-2016 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.vcs.changes.ui; import com.intellij.BundleBase; import com.intellij.diff.util.DiffPlaces; import com.intellij.diff.util.DiffUserDataKeysEx; import com.intellij.ide.util.PropertiesComponent; import com.intellij.openapi.Disposable; import com.intellij.openapi.actionSystem.DataKey; import com.intellij.openapi.actionSystem.DataSink; import com.intellij.openapi.actionSystem.TypeSafeDataProvider; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.extensions.Extensions; import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.*; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.Computable; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vcs.*; import com.intellij.openapi.vcs.changes.*; import com.intellij.openapi.vcs.changes.actions.ScheduleForAdditionAction; import com.intellij.openapi.vcs.checkin.*; import com.intellij.openapi.vcs.impl.CheckinHandlersManager; import com.intellij.openapi.vcs.ui.CommitMessage; import com.intellij.openapi.vcs.ui.Refreshable; import com.intellij.openapi.vcs.ui.RefreshableOnComponent; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.wm.IdeFocusManager; import com.intellij.ui.IdeBorderFactory; import com.intellij.ui.JBColor; import com.intellij.ui.ScrollPaneFactory; import com.intellij.ui.SplitterWithSecondHideable; import com.intellij.util.Alarm; import com.intellij.util.ObjectUtils; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.ui.AbstractLayoutManager; import com.intellij.util.ui.GridBag; import com.intellij.util.ui.JBUI; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.io.File; import java.util.*; import java.util.List; public class CommitChangeListDialog extends DialogWrapper implements CheckinProjectPanel, TypeSafeDataProvider { private final static String outCommitHelpId = "reference.dialogs.vcs.commit"; private static final int LAYOUT_VERSION = 2; @NotNull private final CommitContext myCommitContext; @NotNull private final CommitMessage myCommitMessageArea; private Splitter mySplitter; @Nullable private final JPanel myAdditionalOptionsPanel; @NotNull private final ChangesBrowserBase<?> myBrowser; private CommitLegendPanel myLegend; @NotNull private final MyChangeProcessor myDiffDetails; @NotNull private final List<RefreshableOnComponent> myAdditionalComponents = ContainerUtil.newArrayList(); @NotNull private final List<CheckinHandler> myHandlers = ContainerUtil.newArrayList(); @NotNull private final String myActionName; @NotNull private final Project myProject; @NotNull private final VcsConfiguration myVcsConfiguration; private final List<CommitExecutor> myExecutors; @NotNull private final Alarm myOKButtonUpdateAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD); private String myLastKnownComment = ""; private final boolean myAllOfDefaultChangeListChangesIncluded; @NonNls private static final String SPLITTER_PROPORTION_OPTION = "CommitChangeListDialog.SPLITTER_PROPORTION_" + LAYOUT_VERSION; private final CommitExecutorAction[] myExecutorActions; private final boolean myShowVcsCommit; @NotNull private final Map<AbstractVcs, JPanel> myPerVcsOptionsPanels = ContainerUtil.newHashMap(); @Nullable private final AbstractVcs myVcs; private final boolean myIsAlien; private boolean myDisposed = false; private boolean myUpdateDisabled = false; @NotNull private final JLabel myWarningLabel; @NotNull private final Map<String, CheckinChangeListSpecificComponent> myCheckinChangeListSpecificComponents; @NotNull private final Map<String, String> myListComments; private String myLastSelectedListName; private ChangeInfoCalculator myChangesInfoCalculator; @NotNull private final PseudoMap<Object, Object> myAdditionalData; private String myHelpId; private SplitterWithSecondHideable myDetailsSplitter; private static final String DETAILS_SPLITTER_PROPORTION_OPTION = "CommitChangeListDialog.DETAILS_SPLITTER_PROPORTION_" + LAYOUT_VERSION; private static final String DETAILS_SHOW_OPTION = "CommitChangeListDialog.DETAILS_SHOW_OPTION_"; private final String myOkActionText; private CommitAction myCommitAction; @Nullable private CommitResultHandler myResultHandler; private static final float SPLITTER_PROPORTION_OPTION_DEFAULT = 0.5f; private static final float DETAILS_SPLITTER_PROPORTION_OPTION_DEFAULT = 0.6f; private static final boolean DETAILS_SHOW_OPTION_DEFAULT = true; private static class MyUpdateButtonsRunnable implements Runnable { private CommitChangeListDialog myDialog; private MyUpdateButtonsRunnable(final CommitChangeListDialog dialog) { myDialog = dialog; } public void cancel() { myDialog = null; } @Override public void run() { if (myDialog != null) { myDialog.updateButtons(); myDialog.updateLegend(); } } public void restart(final CommitChangeListDialog dialog) { myDialog = dialog; run(); } } @NotNull private final MyUpdateButtonsRunnable myUpdateButtonsRunnable = new MyUpdateButtonsRunnable(this); public static boolean commitChanges(final Project project, final List<Change> changes, final LocalChangeList initialSelection, final List<CommitExecutor> executors, final boolean showVcsCommit, final String comment, @Nullable CommitResultHandler customResultHandler, boolean cancelIfNoChanges) { return commitChanges(project, changes, initialSelection, executors, showVcsCommit, null, comment, customResultHandler, cancelIfNoChanges); } public static boolean commitChanges(final Project project, final List<Change> changes, final LocalChangeList initialSelection, final List<CommitExecutor> executors, final boolean showVcsCommit, @Nullable final AbstractVcs singleVcs, final String comment, @Nullable CommitResultHandler customResultHandler, boolean cancelIfNoChanges) { if (cancelIfNoChanges && changes.isEmpty() && !ApplicationManager.getApplication().isUnitTestMode()) { Messages.showInfoMessage(project, VcsBundle.message("commit.dialog.no.changes.detected.text"), VcsBundle.message("commit.dialog.no.changes.detected.title")); return false; } for (BaseCheckinHandlerFactory factory : getCheckInFactories(project)) { BeforeCheckinDialogHandler handler = factory.createSystemReadyHandler(project); if (handler != null && !handler.beforeCommitDialogShown(project, changes, executors, showVcsCommit)) { return false; } } final ChangeListManager manager = ChangeListManager.getInstance(project); CommitChangeListDialog dialog = new CommitChangeListDialog(project, changes, initialSelection, executors, showVcsCommit, manager.getDefaultChangeList(), manager.getChangeListsCopy(), singleVcs, false, comment, customResultHandler); if (!ApplicationManager.getApplication().isUnitTestMode()) { dialog.show(); } else { dialog.doOKAction(); } return dialog.isOK(); } private static List<BaseCheckinHandlerFactory> getCheckInFactories(@NotNull Project project) { return CheckinHandlersManager.getInstance().getRegisteredCheckinHandlerFactories( ProjectLevelVcsManager.getInstance(project).getAllActiveVcss()); } // Used in plugins @SuppressWarnings("unused") @NotNull public List<RefreshableOnComponent> getAdditionalComponents() { return Collections.unmodifiableList(myAdditionalComponents); } public static void commitPaths(final Project project, Collection<FilePath> paths, final LocalChangeList initialSelection, @Nullable final CommitExecutor executor, final String comment) { final ChangeListManager manager = ChangeListManager.getInstance(project); final Collection<Change> changes = new HashSet<>(); for (FilePath path : paths) { changes.addAll(manager.getChangesIn(path)); } commitChanges(project, changes, initialSelection, executor, comment); } public static boolean commitChanges(final Project project, final Collection<Change> changes, final LocalChangeList initialSelection, @Nullable final CommitExecutor executor, final String comment) { if (executor == null) { return commitChanges(project, changes, initialSelection, collectExecutors(project, changes), true, comment, null); } else { return commitChanges(project, changes, initialSelection, Collections.singletonList(executor), false, comment, null); } } public static List<CommitExecutor> collectExecutors(@NotNull Project project, @NotNull Collection<Change> changes) { List<CommitExecutor> result = new ArrayList<>(); for (AbstractVcs<?> vcs : ChangesUtil.getAffectedVcses(changes, project)) { result.addAll(vcs.getCommitExecutors()); } result.addAll(ChangeListManager.getInstance(project).getRegisteredExecutors()); return result; } /** * Shows the commit dialog, and performs the selected action: commit, commit & push, create patch, etc. * @param customResultHandler If this is not null, after commit is completed, custom result handler is called instead of * showing the default notification in case of commit or failure. * @return true if user agreed to commit, false if he pressed "Cancel". */ public static boolean commitChanges(final Project project, final Collection<Change> changes, final LocalChangeList initialSelection, final List<CommitExecutor> executors, final boolean showVcsCommit, final String comment, @Nullable CommitResultHandler customResultHandler) { return commitChanges(project, new ArrayList<>(changes), initialSelection, executors, showVcsCommit, comment, customResultHandler, true); } public static void commitAlienChanges(final Project project, final List<Change> changes, final AbstractVcs vcs, final String changelistName, final String comment) { final LocalChangeList lcl = new AlienLocalChangeList(changes, changelistName); new CommitChangeListDialog(project, changes, null, null, true, AlienLocalChangeList.DEFAULT_ALIEN, Collections.singletonList(lcl), vcs, true, comment, null).show(); } private CommitChangeListDialog(@NotNull Project project, @NotNull List<Change> changes, final LocalChangeList initialSelection, final List<CommitExecutor> executors, final boolean showVcsCommit, @NotNull LocalChangeList defaultChangeList, final List<LocalChangeList> changeLists, @Nullable final AbstractVcs singleVcs, final boolean isAlien, final String comment, @Nullable CommitResultHandler customResultHandler) { super(project, true); myCommitContext = new CommitContext(); myProject = project; myVcsConfiguration = ObjectUtils.assertNotNull(VcsConfiguration.getInstance(myProject)); myExecutors = executors; myShowVcsCommit = showVcsCommit; myVcs = singleVcs; myResultHandler = customResultHandler; myListComments = new HashMap<>(); myAdditionalData = new PseudoMap<>(); myDiffDetails = new MyChangeProcessor(myProject); if (!myShowVcsCommit && ContainerUtil.isEmpty(myExecutors)) { throw new IllegalArgumentException("nothing found to execute commit with"); } myAllOfDefaultChangeListChangesIncluded = ContainerUtil.newHashSet(changes).containsAll(ContainerUtil.newHashSet(defaultChangeList.getChanges())); myIsAlien = isAlien; if (isAlien) { myBrowser = new AlienChangeListBrowser(project, changeLists, changes, initialSelection, true, true, singleVcs); } else { //noinspection unchecked boolean unversionedFilesEnabled = myShowVcsCommit && Registry.is("vcs.unversioned.files.in.commit"); MultipleChangeListBrowser browser = new MultipleChangeListBrowser(project, changeLists, (List)changes, initialSelection, true, true, new Runnable() { @Override public void run() { updateWarning(); } }, new Runnable() { @Override public void run() { for (CheckinHandler handler : myHandlers) { handler.includedChangesChanged(); } } }, unversionedFilesEnabled) { @Override protected void afterDiffRefresh() { myBrowser.rebuildList(); myBrowser.setDataIsDirty(false); ApplicationManager.getApplication().invokeLater(new Runnable() { @Override public void run() { IdeFocusManager.findInstance().requestFocus(myBrowser.getViewer().getPreferredFocusedComponent(), true); } }); } }; browser.addSelectedListChangeListener(new SelectedListChangeListener() { @Override public void selectedListChanged() { updateOnListSelection(); } }); myBrowser = browser; myBrowser.setAlwayExpandList(false); } myBrowser.getViewer().addSelectionListener(new Runnable() { @Override public void run() { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { changeDetails(); } }); } }); myCommitMessageArea = new CommitMessage(project); if (!myVcsConfiguration.CLEAR_INITIAL_COMMIT_MESSAGE) { setComment(initialSelection, comment); } myBrowser.setDiffBottomComponent(new DiffCommitMessageEditor(this)); myActionName = VcsBundle.message("commit.dialog.title"); Box optionsBox = Box.createVerticalBox(); boolean hasVcsOptions = false; Box vcsCommitOptions = Box.createVerticalBox(); final List<AbstractVcs> vcses = ContainerUtil.sorted(getAffectedVcses(), new Comparator<AbstractVcs>() { @Override public int compare(@NotNull AbstractVcs o1, @NotNull AbstractVcs o2) { return o1.getKeyInstanceMethod().getName().compareToIgnoreCase(o2.getKeyInstanceMethod().getName()); } }); myCheckinChangeListSpecificComponents = ContainerUtil.newHashMap(); for (AbstractVcs vcs : vcses) { final CheckinEnvironment checkinEnvironment = vcs.getCheckinEnvironment(); if (checkinEnvironment != null) { final RefreshableOnComponent options = checkinEnvironment.createAdditionalOptionsPanel(this, myAdditionalData); if (options != null) { JPanel vcsOptions = new JPanel(new BorderLayout()); vcsOptions.add(options.getComponent(), BorderLayout.CENTER); vcsOptions.setBorder(IdeBorderFactory.createTitledBorder(vcs.getDisplayName(), true)); vcsCommitOptions.add(vcsOptions); myPerVcsOptionsPanels.put(vcs, vcsOptions); myAdditionalComponents.add(options); if (options instanceof CheckinChangeListSpecificComponent) { myCheckinChangeListSpecificComponents.put(vcs.getName(), (CheckinChangeListSpecificComponent) options); } hasVcsOptions = true; } } } if (hasVcsOptions) { vcsCommitOptions.add(Box.createVerticalGlue()); optionsBox.add(vcsCommitOptions); } boolean beforeVisible = false; boolean afterVisible = false; Box beforeBox = Box.createVerticalBox(); Box afterBox = Box.createVerticalBox(); for (BaseCheckinHandlerFactory factory : getCheckInFactories(project)) { final CheckinHandler handler = factory.createHandler(this, myCommitContext); if (CheckinHandler.DUMMY.equals(handler)) continue; myHandlers.add(handler); final RefreshableOnComponent beforePanel = handler.getBeforeCheckinConfigurationPanel(); if (beforePanel != null) { beforeBox.add(beforePanel.getComponent()); beforeVisible = true; myAdditionalComponents.add(beforePanel); } final RefreshableOnComponent afterPanel = handler.getAfterCheckinConfigurationPanel(getDisposable()); if (afterPanel != null) { afterBox.add(afterPanel.getComponent()); afterVisible = true; myAdditionalComponents.add(afterPanel); } } final String actionName = getCommitActionName(); final String borderTitleName = actionName.replace("_", "").replace("&", ""); if (beforeVisible) { beforeBox.add(Box.createVerticalGlue()); JPanel beforePanel = new JPanel(new BorderLayout()); beforePanel.add(beforeBox); beforePanel.setBorder(IdeBorderFactory.createTitledBorder( VcsBundle.message("border.standard.checkin.options.group", borderTitleName), true)); optionsBox.add(beforePanel); } if (afterVisible) { afterBox.add(Box.createVerticalGlue()); JPanel afterPanel = new JPanel(new BorderLayout()); afterPanel.add(afterBox); afterPanel.setBorder(IdeBorderFactory.createTitledBorder( VcsBundle.message("border.standard.after.checkin.options.group", borderTitleName), true)); optionsBox.add(afterPanel); } if (hasVcsOptions || beforeVisible || afterVisible) { optionsBox.add(Box.createVerticalGlue()); myAdditionalOptionsPanel = new JPanel(new BorderLayout()); myAdditionalOptionsPanel.add(optionsBox, BorderLayout.NORTH); } else { myAdditionalOptionsPanel = null; } myOkActionText = actionName.replace(BundleBase.MNEMONIC, '&'); if (myShowVcsCommit) { setTitle(myActionName); } else { setTitle(trimEllipsis(myExecutors.get(0).getActionText())); } restoreState(); if (myExecutors != null) { myExecutorActions = new CommitExecutorAction[myExecutors.size()]; for (int i = 0; i < myExecutors.size(); i++) { final CommitExecutor commitExecutor = myExecutors.get(i); myExecutorActions[i] = new CommitExecutorAction(commitExecutor, i == 0 && !myShowVcsCommit); } } else { myExecutorActions = null; } myWarningLabel = new JLabel(); myWarningLabel.setUI(new MultiLineLabelUI()); myWarningLabel.setForeground(JBColor.RED); updateWarning(); init(); updateButtons(); updateVcsOptionsVisibility(); updateOnListSelection(); myCommitMessageArea.requestFocusInMessage(); for (EditChangelistSupport support : Extensions.getExtensions(EditChangelistSupport.EP_NAME, project)) { support.installSearch(myCommitMessageArea.getEditorField(), myCommitMessageArea.getEditorField()); } showDetailsIfSaved(); } private void setComment(LocalChangeList initialSelection, String comment) { if (comment != null) { setCommitMessage(comment); myLastKnownComment = comment; myLastSelectedListName = initialSelection == null ? myBrowser.getSelectedChangeList().getName() : initialSelection.getName(); } else { updateComment(); if (StringUtil.isEmptyOrSpaces(myCommitMessageArea.getComment())) { setCommitMessage(myVcsConfiguration.LAST_COMMIT_MESSAGE); final String messageFromVcs = getInitialMessageFromVcs(); if (messageFromVcs != null) { myCommitMessageArea.setText(messageFromVcs); } } } } private void showDetailsIfSaved() { boolean showDetails = PropertiesComponent.getInstance().getBoolean(DETAILS_SHOW_OPTION, DETAILS_SHOW_OPTION_DEFAULT); if (showDetails) { myDetailsSplitter.initOn(); } SwingUtilities.invokeLater(new Runnable() { @Override public void run() { changeDetails(); } }); } private void updateOnListSelection() { updateComment(); updateVcsOptionsVisibility(); for (CheckinChangeListSpecificComponent component : myCheckinChangeListSpecificComponents.values()) { component.onChangeListSelected((LocalChangeList) myBrowser.getSelectedChangeList()); } } private void updateWarning() { // check for null since can be called from constructor before field initialization //noinspection ConstantConditions if (myWarningLabel != null) { myWarningLabel.setVisible(false); @SuppressWarnings("ThrowableResultOfMethodCallIgnored") final VcsException updateException = ((ChangeListManagerImpl)ChangeListManager.getInstance(myProject)).getUpdateException(); if (updateException != null) { final String[] messages = updateException.getMessages(); if (messages != null && messages.length > 0) { final String message = messages[0]; myWarningLabel.setText("Warning: not all local changes may be shown due to an error: " + message); myWarningLabel.setVisible(true); } } } } private void updateVcsOptionsVisibility() { Collection<AbstractVcs> affectedVcses = ChangesUtil.getAffectedVcses(myBrowser.getSelectedChangeList().getChanges(), myProject); for (Map.Entry<AbstractVcs, JPanel> entry : myPerVcsOptionsPanels.entrySet()) { entry.getValue().setVisible(affectedVcses.contains(entry.getKey())); } } @Override protected String getHelpId() { return myHelpId; } private class CommitAction extends AbstractAction implements OptionAction { private Action[] myOptions = new Action[0]; private CommitAction() { super(myOkActionText); putValue(DEFAULT_ACTION, Boolean.TRUE); } @Override public void actionPerformed(ActionEvent e) { doOKAction(); } @NotNull @Override public Action[] getOptions() { return myOptions; } public void setOptions(Action[] actions) { myOptions = actions; } } @Override protected void doOKAction() { if (!myIsAlien && !addUnversionedFiles()) return; if (!saveDialogState()) return; saveComments(true); final DefaultListCleaner defaultListCleaner = new DefaultListCleaner(); final Runnable callCommit = new Runnable() { @Override public void run() { try { CheckinHandler.ReturnResult result = runBeforeCommitHandlers(new Runnable() { @Override public void run() { CommitChangeListDialog.super.doOKAction(); doCommit(myResultHandler); } }, null); if (result == CheckinHandler.ReturnResult.COMMIT) { defaultListCleaner.clean(); } } catch (InputException ex) { ex.show(); } } }; if (myBrowser.isDataIsDirty()) { ensureDataIsActual(callCommit); } else { callCommit.run(); } } private boolean addUnversionedFiles() { return ScheduleForAdditionAction .addUnversioned(myProject, myBrowser.getIncludedUnversionedFiles(), ChangeListManagerImpl.getDefaultUnversionedFileCondition(), myBrowser); } @NotNull @Override protected Action getOKAction() { return new CommitAction(); } @Override @NotNull protected Action[] createActions() { final List<Action> actions = new ArrayList<>(); myCommitAction = null; if (myShowVcsCommit) { myCommitAction = new CommitAction(); actions.add(myCommitAction); myHelpId = outCommitHelpId; } if (myExecutors != null) { if (myCommitAction != null) { myCommitAction.setOptions(myExecutorActions); } else { actions.addAll(Arrays.asList(myExecutorActions)); } for (CommitExecutor executor : myExecutors) { if (myHelpId != null) break; if (executor instanceof CommitExecutorWithHelp) { myHelpId = ((CommitExecutorWithHelp) executor).getHelpId(); } } } actions.add(getCancelAction()); if (myHelpId != null) { actions.add(getHelpAction()); } return actions.toArray(new Action[actions.size()]); } private void execute(final CommitExecutor commitExecutor) { if (!saveDialogState()) return; saveComments(true); final CommitSession session = commitExecutor.createCommitSession(); if (session instanceof CommitSessionContextAware) { ((CommitSessionContextAware)session).setContext(myCommitContext); } if (session == CommitSession.VCS_COMMIT) { doOKAction(); return; } boolean isOK = true; final JComponent configurationUI = SessionDialog.createConfigurationUI(session, getIncludedChanges(), getCommitMessage()); if (configurationUI != null) { DialogWrapper sessionDialog = new SessionDialog(commitExecutor.getActionText(), getProject(), session, getIncludedChanges(), getCommitMessage(), configurationUI); isOK = sessionDialog.showAndGet(); } if (isOK) { final DefaultListCleaner defaultListCleaner = new DefaultListCleaner(); runBeforeCommitHandlers(new Runnable() { @Override public void run() { boolean success = false; try { final boolean completed = ProgressManager.getInstance().runProcessWithProgressSynchronously( new Runnable() { @Override public void run() { session.execute(getIncludedChanges(), getCommitMessage()); } }, commitExecutor.getActionText(), true, getProject()); if (completed) { for (CheckinHandler handler : myHandlers) { handler.checkinSuccessful(); } success = true; defaultListCleaner.clean(); close(OK_EXIT_CODE); } else { session.executionCanceled(); } } catch (Throwable e) { Messages.showErrorDialog(VcsBundle.message("error.executing.commit", commitExecutor.getActionText(), e.getLocalizedMessage()), commitExecutor.getActionText()); for (CheckinHandler handler : myHandlers) { handler.checkinFailed(Collections.singletonList(new VcsException(e))); } } finally { if (myResultHandler != null) { if (success) { myResultHandler.onSuccess(getCommitMessage()); } else { myResultHandler.onFailure(); } } } } }, commitExecutor); } else { session.executionCanceled(); } } @Nullable private String getInitialMessageFromVcs() { final List<Change> list = getIncludedChanges(); final Ref<String> result = new Ref<>(); ChangesUtil.processChangesByVcs(myProject, list, (vcs, items) -> { if (result.isNull()) { CheckinEnvironment checkinEnvironment = vcs.getCheckinEnvironment(); if (checkinEnvironment != null) { final Collection<FilePath> paths = ChangesUtil.getPaths(items); String defaultMessage = checkinEnvironment.getDefaultMessageFor(paths.toArray(new FilePath[paths.size()])); if (defaultMessage != null) { result.set(defaultMessage); } } } }); return result.get(); } private void saveCommentIntoChangeList() { if (myLastSelectedListName != null) { final String actualCommentText = myCommitMessageArea.getComment(); final String saved = myListComments.get(myLastSelectedListName); if (! Comparing.equal(saved, actualCommentText)) { myListComments.put(myLastSelectedListName, actualCommentText); } } } private void updateComment() { if (myVcsConfiguration.CLEAR_INITIAL_COMMIT_MESSAGE) return; final LocalChangeList list = (LocalChangeList) myBrowser.getSelectedChangeList(); if (list == null || (list.getName().equals(myLastSelectedListName))) { return; } else if (myLastSelectedListName != null) { saveCommentIntoChangeList(); } myLastSelectedListName = list.getName(); String listComment = list.getComment(); if (StringUtil.isEmptyOrSpaces(listComment)) { final String listTitle = list.getName(); if (!list.hasDefaultName()) { listComment = listTitle; } else { // use last know comment; it is already stored in list listComment = myLastKnownComment; } } myCommitMessageArea.setText(listComment); } @Override public void dispose() { myDisposed = true; Disposer.dispose(myBrowser); Disposer.dispose(myCommitMessageArea); Disposer.dispose(myOKButtonUpdateAlarm); myUpdateButtonsRunnable.cancel(); super.dispose(); Disposer.dispose(myDiffDetails); PropertiesComponent.getInstance().setValue(SPLITTER_PROPORTION_OPTION, mySplitter.getProportion(), SPLITTER_PROPORTION_OPTION_DEFAULT); float usedProportion = myDetailsSplitter.getUsedProportion(); if (usedProportion > 0) { PropertiesComponent.getInstance().setValue(DETAILS_SPLITTER_PROPORTION_OPTION, usedProportion, DETAILS_SPLITTER_PROPORTION_OPTION_DEFAULT); } PropertiesComponent.getInstance().setValue(DETAILS_SHOW_OPTION, myDetailsSplitter.isOn(), DETAILS_SHOW_OPTION_DEFAULT); } @Override public String getCommitActionName() { String name = null; for (AbstractVcs vcs : getAffectedVcses()) { final CheckinEnvironment checkinEnvironment = vcs.getCheckinEnvironment(); if (name == null && checkinEnvironment != null) { name = checkinEnvironment.getCheckinOperationName(); } else { name = VcsBundle.message("commit.dialog.default.commit.operation.name"); } } return name != null ? name : VcsBundle.message("commit.dialog.default.commit.operation.name"); } @Override public boolean isCheckSpelling() { return myVcsConfiguration.CHECK_COMMIT_MESSAGE_SPELLING; } @Override public void setCheckSpelling(boolean checkSpelling) { myVcsConfiguration.CHECK_COMMIT_MESSAGE_SPELLING = checkSpelling; myCommitMessageArea.setCheckSpelling(checkSpelling); } private boolean checkComment() { if (myVcsConfiguration.FORCE_NON_EMPTY_COMMENT && getCommitMessage().isEmpty()) { int requestForCheckin = Messages.showYesNoDialog(VcsBundle.message("confirmation.text.check.in.with.empty.comment"), VcsBundle.message("confirmation.title.check.in.with.empty.comment"), Messages.getWarningIcon()); return requestForCheckin == Messages.YES; } else { return true; } } private void stopUpdate() { myUpdateDisabled = true; myUpdateButtonsRunnable.cancel(); } private void restartUpdate() { myUpdateDisabled = false; myUpdateButtonsRunnable.restart(this); } private CheckinHandler.ReturnResult runBeforeCommitHandlers(final Runnable okAction, final CommitExecutor executor) { final Computable<CheckinHandler.ReturnResult> proceedRunnable = new Computable<CheckinHandler.ReturnResult>() { @Override public CheckinHandler.ReturnResult compute() { FileDocumentManager.getInstance().saveAllDocuments(); for (CheckinHandler handler : myHandlers) { if (!(handler.acceptExecutor(executor))) continue; final CheckinHandler.ReturnResult result = handler.beforeCheckin(executor, myAdditionalData); if (result == CheckinHandler.ReturnResult.COMMIT) continue; if (result == CheckinHandler.ReturnResult.CANCEL) { restartUpdate(); return CheckinHandler.ReturnResult.CANCEL; } if (result == CheckinHandler.ReturnResult.CLOSE_WINDOW) { final ChangeList changeList = myBrowser.getSelectedChangeList(); CommitHelper.moveToFailedList(changeList, getCommitMessage(), getIncludedChanges(), VcsBundle.message("commit.dialog.rejected.commit.template", changeList.getName()), myProject); doCancelAction(); return CheckinHandler.ReturnResult.CLOSE_WINDOW; } } okAction.run(); return CheckinHandler.ReturnResult.COMMIT; } }; stopUpdate(); final Ref<CheckinHandler.ReturnResult> compoundResultRef = Ref.create(); Runnable runnable = new Runnable() { @Override public void run() { compoundResultRef.set(proceedRunnable.compute()); } }; for(final CheckinHandler handler: myHandlers) { if (handler instanceof CheckinMetaHandler) { final Runnable previousRunnable = runnable; runnable = new Runnable() { @Override public void run() { ((CheckinMetaHandler)handler).runCheckinHandlers(previousRunnable); } }; } } runnable.run(); return compoundResultRef.get(); } private boolean saveDialogState() { if (!checkComment()) { return false; } saveCommentIntoChangeList(); myVcsConfiguration.saveCommitMessage(getCommitMessage()); try { saveState(); } catch(InputException ex) { ex.show(); return false; } return true; } private class DefaultListCleaner { private final boolean myToClean; private DefaultListCleaner() { final int selectedSize = getIncludedChanges().size(); final ChangeList selectedList = myBrowser.getSelectedChangeList(); final int totalSize = selectedList.getChanges().size(); myToClean = (totalSize == selectedSize) && (((LocalChangeList)selectedList).hasDefaultName()); } void clean() { if (myToClean) { final ChangeListManager clManager = ChangeListManager.getInstance(myProject); clManager.editComment(LocalChangeList.DEFAULT_NAME, ""); } } } private void saveComments(final boolean isOk) { final ChangeListManager clManager = ChangeListManager.getInstance(myProject); if (isOk) { final int selectedSize = getIncludedChanges().size(); final ChangeList selectedList = myBrowser.getSelectedChangeList(); final int totalSize = selectedList.getChanges().size(); if (totalSize > selectedSize) { myListComments.remove(myLastSelectedListName); } } for (Map.Entry<String, String> entry : myListComments.entrySet()) { final String name = entry.getKey(); final String value = entry.getValue(); clManager.editComment(name, value); } } @Override public void doCancelAction() { for (CheckinChangeListSpecificComponent component : myCheckinChangeListSpecificComponents.values()) { component.saveState(); } saveCommentIntoChangeList(); saveComments(false); //VcsConfiguration.getInstance(myProject).saveCommitMessage(getCommitMessage()); super.doCancelAction(); } private void doCommit(@Nullable CommitResultHandler customResultHandler) { final CommitHelper helper = new CommitHelper( myProject, myBrowser.getSelectedChangeList(), getIncludedChanges(), myActionName, getCommitMessage(), myHandlers, myAllOfDefaultChangeListChangesIncluded, false, myAdditionalData, customResultHandler); if (myIsAlien) { helper.doAlienCommit(myVcs); } else { helper.doCommit(myVcs); } } @Override @Nullable protected JComponent createCenterPanel() { mySplitter = new Splitter(true); mySplitter.setHonorComponentsMinimumSize(true); mySplitter.setFirstComponent(myBrowser); mySplitter.setSecondComponent(myCommitMessageArea); initMainSplitter(); myChangesInfoCalculator = new ChangeInfoCalculator(); myLegend = new CommitLegendPanel(myChangesInfoCalculator); myBrowser.getBottomPanel().add(JBUI.Panels.simplePanel().addToRight(myLegend.getComponent()), BorderLayout.SOUTH); JPanel mainPanel; if (myAdditionalOptionsPanel != null) { JScrollPane optionsPane = ScrollPaneFactory.createScrollPane(myAdditionalOptionsPanel, true); optionsPane.getVerticalScrollBar().setUnitIncrement(10); JPanel infoPanel = JBUI.Panels.simplePanel(optionsPane).withBorder(JBUI.Borders.emptyLeft(10)); mainPanel = new JPanel(new MyOptionsLayout(mySplitter, infoPanel, JBUI.scale(250))); mainPanel.add(mySplitter); mainPanel.add(infoPanel); } else { mainPanel = mySplitter; } myWarningLabel.setBorder(JBUI.Borders.empty(5, 5, 0, 5)); final JPanel panel = new JPanel(new GridBagLayout()); panel.add(myWarningLabel, new GridBag().anchor(GridBagConstraints.NORTHWEST).weightx(1)); JPanel rootPane = JBUI.Panels.simplePanel(mainPanel).addToBottom(panel); // TODO: there are no reason to use such heavy interface for a simple task. myDetailsSplitter = new SplitterWithSecondHideable(true, "Diff", rootPane, new SplitterWithSecondHideable.OnOffListener<Integer>() { @Override public void on(Integer integer) { if (integer == 0) return; myDiffDetails.refresh(); mySplitter.skipNextLayouting(); myDetailsSplitter.getComponent().skipNextLayouting(); final Dimension dialogSize = getSize(); setSize(dialogSize.width, dialogSize.height + integer); repaint(); } @Override public void off(Integer integer) { if (integer == 0) return; myDiffDetails.clear(); // TODO: we may want to keep it in memory mySplitter.skipNextLayouting(); myDetailsSplitter.getComponent().skipNextLayouting(); final Dimension dialogSize = getSize(); setSize(dialogSize.width, dialogSize.height - integer); repaint(); } }) { @Override protected RefreshablePanel createDetails() { final JPanel panel = JBUI.Panels.simplePanel(myDiffDetails.getComponent()); return new RefreshablePanel() { @Override public boolean refreshDataSynch() { return false; } @Override public void dataChanged() { } @Override public void refresh() { } @Override public JPanel getPanel() { return panel; } @Override public void away() { } @Override public boolean isStillValid(Object o) { return false; } @Override public void dispose() { } }; } @Override protected float getSplitterInitialProportion() { float value = PropertiesComponent.getInstance().getFloat(DETAILS_SPLITTER_PROPORTION_OPTION, DETAILS_SPLITTER_PROPORTION_OPTION_DEFAULT); return value <= 0.05 || value >= 0.95 ? DETAILS_SPLITTER_PROPORTION_OPTION_DEFAULT : value; } }; return myDetailsSplitter.getComponent(); } private void initMainSplitter() { mySplitter.setProportion(PropertiesComponent.getInstance().getFloat(SPLITTER_PROPORTION_OPTION, SPLITTER_PROPORTION_OPTION_DEFAULT)); } @NotNull public Set<AbstractVcs> getAffectedVcses() { return myShowVcsCommit ? myBrowser.getAffectedVcses() : Collections.emptySet(); } @NotNull @Override public Collection<VirtualFile> getRoots() { ProjectLevelVcsManager vcsManager = ProjectLevelVcsManager.getInstance(myProject); return ContainerUtil .map2SetNotNull(myBrowser.getCurrentDisplayedChanges(), (change) -> vcsManager.getVcsRootFor(ChangesUtil.getFilePath(change))); } @Override public JComponent getComponent() { return mySplitter; } @Override public boolean hasDiffs() { return !getIncludedChanges().isEmpty() || !myBrowser.getIncludedUnversionedFiles().isEmpty(); } @NotNull @Override public Collection<VirtualFile> getVirtualFiles() { return ContainerUtil.mapNotNull(getIncludedChanges(), (change) -> ChangesUtil.getFilePath(change).getVirtualFile()); } @NotNull @Override public Collection<Change> getSelectedChanges() { return ContainerUtil.newArrayList(getIncludedChanges()); } @NotNull @Override public Collection<File> getFiles() { return ContainerUtil.map(getIncludedChanges(), (change) -> ChangesUtil.getFilePath(change).getIOFile()); } @NotNull @Override public Project getProject() { return myProject; } @Override public boolean vcsIsAffected(String name) { // tod +- performance? if (! ProjectLevelVcsManager.getInstance(myProject).checkVcsIsActive(name)) return false; return ContainerUtil.exists(myBrowser.getAffectedVcses(), (vcs) -> Comparing.equal(vcs.getName(), name)); } @Override public void setCommitMessage(final String currentDescription) { setCommitMessageText(currentDescription); myCommitMessageArea.requestFocusInMessage(); } private void setCommitMessageText(final String currentDescription) { myLastKnownComment = currentDescription; myCommitMessageArea.setText(currentDescription); } @NotNull @Override public String getCommitMessage() { return myCommitMessageArea.getComment(); } @Override public void refresh() { ChangeListManager.getInstance(myProject).invokeAfterUpdate(new Runnable() { @Override public void run() { myBrowser.rebuildList(); for (RefreshableOnComponent component : myAdditionalComponents) { component.refresh(); } } }, InvokeAfterUpdateMode.SILENT, "commit dialog", ModalityState.current()); // title not shown for silently } @Override public void saveState() { for (RefreshableOnComponent component : myAdditionalComponents) { component.saveState(); } } @Override public void restoreState() { for (RefreshableOnComponent component : myAdditionalComponents) { component.restoreState(); } } private void updateButtons() { if (myDisposed || myUpdateDisabled) return; final boolean enabled = hasDiffs(); setOKActionEnabled(enabled); if (myCommitAction != null) { myCommitAction.setEnabled(enabled); } if (myExecutorActions != null) { for (CommitExecutorAction executorAction : myExecutorActions) { executorAction.updateEnabled(enabled); } } myOKButtonUpdateAlarm.cancelAllRequests(); myOKButtonUpdateAlarm.addRequest(myUpdateButtonsRunnable, 300, ModalityState.stateForComponent(myBrowser)); } private void updateLegend() { if (myDisposed || myUpdateDisabled) return; myChangesInfoCalculator.update(myBrowser.getCurrentDisplayedChanges(), getIncludedChanges(), myBrowser.getUnversionedFilesCount(), myBrowser.getIncludedUnversionedFiles().size()); myLegend.update(); } @NotNull private List<Change> getIncludedChanges() { return myBrowser.getCurrentIncludedChanges(); } @Override @NonNls protected String getDimensionServiceKey() { return "CommitChangelistDialog" + LAYOUT_VERSION; } @Override public JComponent getPreferredFocusedComponent() { return myCommitMessageArea.getEditorField(); } @Override public void calcData(DataKey key, DataSink sink) { if (key == Refreshable.PANEL_KEY) { sink.put(Refreshable.PANEL_KEY, this); } else { myBrowser.calcData(key, sink); } } static String trimEllipsis(final String title) { if (title.endsWith("...")) { return title.substring(0, title.length() - 3); } else { return title; } } private void ensureDataIsActual(final Runnable runnable) { ChangeListManager.getInstance(myProject).invokeAfterUpdate(runnable, InvokeAfterUpdateMode.SYNCHRONOUS_CANCELLABLE, "Refreshing changelists...", ModalityState.current()); } private class CommitExecutorAction extends AbstractAction { @NotNull private final CommitExecutor myCommitExecutor; public CommitExecutorAction(@NotNull CommitExecutor commitExecutor, boolean isDefault) { super(commitExecutor.getActionText()); myCommitExecutor = commitExecutor; if (isDefault) { putValue(DEFAULT_ACTION, Boolean.TRUE); } } @Override public void actionPerformed(ActionEvent e) { final Runnable callExecutor = new Runnable() { @Override public void run() { execute(myCommitExecutor); } }; if (myBrowser.isDataIsDirty()) { ensureDataIsActual(callExecutor); } else { callExecutor.run(); } } public void updateEnabled(boolean hasDiffs) { setEnabled(hasDiffs || (myCommitExecutor instanceof CommitExecutorBase) && !((CommitExecutorBase)myCommitExecutor).areChangesRequired()); } } private static class DiffCommitMessageEditor extends CommitMessage implements Disposable { public DiffCommitMessageEditor(final CommitChangeListDialog dialog) { super(dialog.getProject()); getEditorField().setDocument(dialog.myCommitMessageArea.getEditorField().getDocument()); } @Override public Dimension getPreferredSize() { // we don't want to be squeezed to one line return new Dimension(400, 120); } } private void changeDetails() { if (myDetailsSplitter.isOn()) { myDiffDetails.refresh(); } } private class MyChangeProcessor extends CacheChangeProcessor { public MyChangeProcessor(@NotNull Project project) { super(project, DiffPlaces.COMMIT_DIALOG); putContextUserData(DiffUserDataKeysEx.SHOW_READ_ONLY_LOCK, true); } @NotNull @Override protected List<Change> getSelectedChanges() { return myBrowser.getSelectedChanges(); } @NotNull @Override protected List<Change> getAllChanges() { return myBrowser.getAllChanges(); } @Override protected void selectChange(@NotNull Change change) { //noinspection unchecked myBrowser.select((List)Collections.singletonList(change)); } @Override protected void onAfterNavigate() { doCancelAction(); } } private static class MyOptionsLayout extends AbstractLayoutManager { private final JComponent myPanel; private final JComponent myOptions; private final int myMinOptionsWidth; public MyOptionsLayout(@NotNull JComponent panel, @NotNull JComponent options, int minOptionsWidth) { myPanel = panel; myOptions = options; myMinOptionsWidth = minOptionsWidth; } @Override public Dimension preferredLayoutSize(Container parent) { Dimension size1 = myPanel.getPreferredSize(); Dimension size2 = myOptions.getPreferredSize(); return new Dimension(size1.width + size2.width, Math.max(size1.height, size2.height)); } @Override public void layoutContainer(Container parent) { Rectangle bounds = parent.getBounds(); int availableWidth = bounds.width - myPanel.getPreferredSize().width; int preferredWidth = myOptions.getPreferredSize().width; int optionsWidth = Math.max(Math.min(availableWidth, preferredWidth), myMinOptionsWidth); myPanel.setBounds(new Rectangle(0, 0, bounds.width - optionsWidth, bounds.height)); myOptions.setBounds(new Rectangle(bounds.width - optionsWidth, 0, optionsWidth, bounds.height)); } } }