/* * 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.wm.impl.status; import com.intellij.icons.AllIcons; import com.intellij.ide.PowerSaveMode; import com.intellij.idea.ActionsBundle; import com.intellij.notification.EventLog; import com.intellij.openapi.Disposable; import com.intellij.openapi.actionSystem.ActionGroup; import com.intellij.openapi.actionSystem.ActionManager; import com.intellij.openapi.actionSystem.ActionPlaces; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx; import com.intellij.openapi.fileEditor.impl.EditorsSplitters; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.TaskInfo; import com.intellij.openapi.progress.impl.ProgressSuspender; import com.intellij.openapi.progress.util.AbstractProgressIndicatorExBase; import com.intellij.openapi.project.ProjectUtil; import com.intellij.openapi.ui.MessageType; import com.intellij.openapi.ui.popup.*; import com.intellij.openapi.util.*; import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.wm.CustomStatusBarWidget; import com.intellij.openapi.wm.IdeFrame; import com.intellij.openapi.wm.StatusBar; import com.intellij.openapi.wm.StatusBarWidget; import com.intellij.openapi.wm.ex.ProgressIndicatorEx; import com.intellij.openapi.wm.impl.ToolWindowsPane; import com.intellij.ui.BalloonLayoutImpl; import com.intellij.ui.Gray; import com.intellij.ui.InplaceButton; import com.intellij.ui.TabbedPaneWrapper; import com.intellij.ui.awt.RelativePoint; import com.intellij.ui.components.labels.LinkLabel; import com.intellij.ui.components.panels.Wrapper; import com.intellij.util.Alarm; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.JBIterable; import com.intellij.util.ui.*; 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.event.HyperlinkListener; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.*; import java.util.List; public class InfoAndProgressPanel extends JPanel implements CustomStatusBarWidget { private final ProcessPopup myPopup; private final StatusPanel myInfoPanel = new StatusPanel(); private final JPanel myRefreshAndInfoPanel = new JPanel(); private final AnimatedIcon myProgressIcon; private final List<ProgressIndicatorEx> myOriginals = new ArrayList<>(); private final List<TaskInfo> myInfos = new ArrayList<>(); private final Map<InlineProgressIndicator, ProgressIndicatorEx> myInline2Original = new HashMap<>(); private final MultiValuesMap<ProgressIndicatorEx, InlineProgressIndicator> myOriginal2Inlines = new MultiValuesMap<>(); private final MergingUpdateQueue myUpdateQueue; private final Alarm myQueryAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD); private boolean myShouldClosePopupAndOnProcessFinish; private final Alarm myRefreshAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD); private final AnimatedIcon myRefreshIcon; private String myCurrentRequestor; private boolean myDisposed; private final Set<InlineProgressIndicator> myDirtyIndicators = ContainerUtil.newIdentityTroveSet(); private final Update myUpdateIndicators = new Update("UpdateIndicators", false, 1) { @Override public void run() { List<InlineProgressIndicator> indicators; synchronized (myDirtyIndicators) { indicators = ContainerUtil.newArrayList(myDirtyIndicators); myDirtyIndicators.clear(); } for (InlineProgressIndicator indicator : indicators) { indicator.updateAndRepaint(); } } }; InfoAndProgressPanel() { setOpaque(false); myRefreshIcon = new RefreshFileSystemIcon(); myRefreshIcon.setPaintPassiveIcon(false); myRefreshAndInfoPanel.setLayout(new BorderLayout()); myRefreshAndInfoPanel.setOpaque(false); myRefreshAndInfoPanel.add(myRefreshIcon, BorderLayout.WEST); myRefreshAndInfoPanel.add(myInfoPanel, BorderLayout.CENTER); myProgressIcon = new AsyncProcessIcon("Background process"); myProgressIcon.setOpaque(false); myProgressIcon.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { handle(e); } @Override public void mouseReleased(MouseEvent e) { handle(e); } }); myProgressIcon.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); myProgressIcon.setBorder(StatusBarWidget.WidgetBorder.INSTANCE); myProgressIcon.setToolTipText(ActionsBundle.message("action.ShowProcessWindow.double.click")); myUpdateQueue = new MergingUpdateQueue("Progress indicator", 50, true, MergingUpdateQueue.ANY_COMPONENT); myPopup = new ProcessPopup(this); setRefreshVisible(false); restoreEmptyStatus(); runOnPowerSaveChange(this::updateProgressIcon, this); } private void runOnPowerSaveChange(@NotNull Runnable runnable, Disposable parentDisposable) { synchronized (myOriginals) { if (!myDisposed) { ApplicationManager.getApplication().getMessageBus().connect(parentDisposable) .subscribe(PowerSaveMode.TOPIC, () -> UIUtil.invokeLaterIfNeeded(runnable)); } } } private void handle(MouseEvent e) { if (UIUtil.isActionClick(e, MouseEvent.MOUSE_PRESSED)) { if (!myPopup.isShowing()) { openProcessPopup(true); } else { hideProcessPopup(); } } else if (e.isPopupTrigger()) { ActionGroup group = (ActionGroup)ActionManager.getInstance().getAction("BackgroundTasks"); ActionManager.getInstance().createActionPopupMenu(ActionPlaces.UNKNOWN, group).getComponent().show(e.getComponent(), e.getX(), e.getY()); } } @Override @NotNull public String ID() { return "InfoAndProgress"; } @Override public WidgetPresentation getPresentation(@NotNull PlatformType type) { return null; } @Override public void install(@NotNull StatusBar statusBar) { } @Override public void dispose() { setRefreshVisible(false); synchronized (myOriginals) { restoreEmptyStatus(); for (InlineProgressIndicator indicator : myInline2Original.keySet()) { Disposer.dispose(indicator); } myInline2Original.clear(); myOriginal2Inlines.clear(); myDisposed = true; } } @Override public JComponent getComponent() { return this; } @NotNull List<Pair<TaskInfo, ProgressIndicator>> getBackgroundProcesses() { synchronized (myOriginals) { if (myOriginals.isEmpty()) return Collections.emptyList(); List<Pair<TaskInfo, ProgressIndicator>> result = new ArrayList<>(myOriginals.size()); for (int i = 0; i < myOriginals.size(); i++) { result.add(Pair.create(myInfos.get(i), myOriginals.get(i))); } return Collections.unmodifiableList(result); } } void addProgress(@NotNull ProgressIndicatorEx original, @NotNull TaskInfo info) { synchronized (myOriginals) { final boolean veryFirst = !hasProgressIndicators(); myOriginals.add(original); myInfos.add(info); final InlineProgressIndicator expanded = createInlineDelegate(info, original, false); final InlineProgressIndicator compact = createInlineDelegate(info, original, true); myPopup.addIndicator(expanded); updateProgressIcon(); if (veryFirst && !myPopup.isShowing()) { buildInInlineIndicator(compact); } else { buildInProcessCount(); if (myInfos.size() > 1 && Registry.is("ide.windowSystem.autoShowProcessPopup")) { openProcessPopup(false); } } runQuery(); } } private boolean hasProgressIndicators() { synchronized (myOriginals) { return !myOriginals.isEmpty(); } } private void removeProgress(@NotNull InlineProgressIndicator progress) { synchronized (myOriginals) { if (!myInline2Original.containsKey(progress)) return; // already disposed final boolean last = myOriginals.size() == 1; final boolean beforeLast = myOriginals.size() == 2; myPopup.removeIndicator(progress); final ProgressIndicatorEx original = removeFromMaps(progress); if (myOriginals.contains(original)) { Disposer.dispose(progress); return; } if (last) { restoreEmptyStatus(); if (myShouldClosePopupAndOnProcessFinish) { hideProcessPopup(); } } else { if (myPopup.isShowing() || myOriginals.size() > 1) { buildInProcessCount(); } else if (beforeLast) { buildInInlineIndicator(createInlineDelegate(myInfos.get(0), myOriginals.get(0), true)); } else { restoreEmptyStatus(); } } runQuery(); } Disposer.dispose(progress); } private ProgressIndicatorEx removeFromMaps(@NotNull InlineProgressIndicator progress) { final ProgressIndicatorEx original = myInline2Original.get(progress); myInline2Original.remove(progress); synchronized (myDirtyIndicators) { myDirtyIndicators.remove(progress); } myOriginal2Inlines.remove(original, progress); if (myOriginal2Inlines.get(original) == null) { final int originalIndex = myOriginals.indexOf(original); myOriginals.remove(originalIndex); myInfos.remove(originalIndex); } return original; } private void openProcessPopup(boolean requestFocus) { synchronized (myOriginals) { if (myPopup.isShowing()) return; if (hasProgressIndicators()) { myShouldClosePopupAndOnProcessFinish = true; buildInProcessCount(); } else { myShouldClosePopupAndOnProcessFinish = false; restoreEmptyStatus(); } myPopup.show(requestFocus); } } void hideProcessPopup() { synchronized (myOriginals) { if (!myPopup.isShowing()) return; if (myOriginals.size() == 1) { buildInInlineIndicator(createInlineDelegate(myInfos.get(0), myOriginals.get(0), true)); } else if (!hasProgressIndicators()) { restoreEmptyStatus(); } else { buildInProcessCount(); } myPopup.hide(); } } private void buildInProcessCount() { removeAll(); setLayout(new BorderLayout()); final JPanel progressCountPanel = new JPanel(new BorderLayout(0, 0)); progressCountPanel.setOpaque(false); String processWord = myOriginals.size() == 1 ? " process" : " processes"; final LinkLabel<Object> label = new LinkLabel<>(myOriginals.size() + processWord + " running...", null, (aSource, aLinkData) -> triggerPopupShowing()); if (SystemInfo.isMac) label.setFont(JBUI.Fonts.label(11)); label.setOpaque(false); final Wrapper labelComp = new Wrapper(label); labelComp.setOpaque(false); progressCountPanel.add(labelComp, BorderLayout.CENTER); //myProgressIcon.setBorder(new IdeStatusBarImpl.MacStatusBarWidgetBorder()); progressCountPanel.add(myProgressIcon, BorderLayout.WEST); add(myRefreshAndInfoPanel, BorderLayout.CENTER); progressCountPanel.setBorder(JBUI.Borders.emptyRight(4)); add(progressCountPanel, BorderLayout.EAST); revalidate(); repaint(); } private void buildInInlineIndicator(@NotNull final InlineProgressIndicator inline) { removeAll(); setLayout(new InlineLayout()); final JRootPane pane = getRootPane(); if (pane == null) return; // e.g. project frame is closed add(myRefreshAndInfoPanel); final JPanel inlinePanel = new JPanel(new BorderLayout()); inline.getComponent().setBorder(JBUI.Borders.empty(1, 0, 0, 2)); final JComponent inlineComponent = inline.getComponent(); inlineComponent.setOpaque(false); inlinePanel.add(inlineComponent, BorderLayout.CENTER); //myProgressIcon.setBorder(new IdeStatusBarImpl.MacStatusBarWidgetBorder()); inlinePanel.add(myProgressIcon, BorderLayout.WEST); inline.updateProgressNow(); inlinePanel.setOpaque(false); add(inlinePanel); myRefreshAndInfoPanel.revalidate(); myRefreshAndInfoPanel.repaint(); final PresentationModeProgressPanel panel = new PresentationModeProgressPanel(inline); MyInlineProgressIndicator delegate = new MyInlineProgressIndicator(true, inline.getInfo(), inline) { @Override protected void updateProgress() { super.updateProgress(); panel.update(); } }; Disposer.register(inline, delegate); Component anchor = getAnchor(pane); final BalloonLayoutImpl balloonLayout = getBalloonLayout(pane); final Balloon balloon = JBPopupFactory.getInstance().createBalloonBuilder(panel.getProgressPanel()) .setFadeoutTime(0) .setFillColor(Gray.TRANSPARENT) .setShowCallout(false) .setBorderColor(Gray.TRANSPARENT) .setBorderInsets(JBUI.emptyInsets()) .setAnimationCycle(0) .setCloseButtonEnabled(false) .setHideOnClickOutside(false) .setDisposable(inline) .setHideOnFrameResize(false) .setHideOnKeyOutside(false) .setBlockClicksThroughBalloon(true) .setHideOnAction(false) .createBalloon(); if (balloonLayout != null) { class MyListener implements JBPopupListener, Runnable { @Override public void beforeShown(LightweightWindowEvent event) { balloonLayout.addListener(this); } @Override public void onClosed(LightweightWindowEvent event) { balloonLayout.removeListener(this); } @Override public void run() { if (!balloon.isDisposed()) { balloon.revalidate(); } } } balloon.addListener(new MyListener()); } balloon.show(new PositionTracker<Balloon>(anchor) { @Override public RelativePoint recalculateLocation(Balloon object) { Component c = getAnchor(pane); int y = c.getHeight() - 45; if (balloonLayout != null && !isBottomSideToolWindowsVisible(pane)) { Component component = balloonLayout.getTopBalloonComponent(); if (component != null) { y = SwingUtilities.convertPoint(component, 0, -45, c).y; } } return new RelativePoint(c, new Point(c.getWidth() - 150, y)); } }, Balloon.Position.above); } @Nullable private static BalloonLayoutImpl getBalloonLayout(@NotNull JRootPane pane) { Component parent = UIUtil.findUltimateParent(pane); if (parent instanceof IdeFrame) { return (BalloonLayoutImpl)((IdeFrame)parent).getBalloonLayout(); } return null; } @NotNull private static Component getAnchor(@NotNull JRootPane pane) { Component tabWrapper = UIUtil.findComponentOfType(pane, TabbedPaneWrapper.TabWrapper.class); if (tabWrapper != null) return tabWrapper; Component splitters = UIUtil.findComponentOfType(pane, EditorsSplitters.class); if (splitters != null) return splitters; FileEditorManagerEx ex = FileEditorManagerEx.getInstanceEx(ProjectUtil.guessCurrentProject(pane)); return ex == null ? pane : ex.getSplitters(); } private static boolean isBottomSideToolWindowsVisible(@NotNull JRootPane parent) { ToolWindowsPane pane = UIUtil.findComponentOfType(parent, ToolWindowsPane.class); return pane != null && pane.isBottomSideToolWindowsVisible(); } public Couple<String> setText(@Nullable final String text, @Nullable final String requestor) { if (StringUtil.isEmpty(text) && !Comparing.equal(requestor, myCurrentRequestor) && !EventLog.LOG_REQUESTOR.equals(requestor)) { return Couple.of(myInfoPanel.getText(), myCurrentRequestor); } boolean logMode = myInfoPanel.updateText(EventLog.LOG_REQUESTOR.equals(requestor) ? "" : text); myCurrentRequestor = logMode ? EventLog.LOG_REQUESTOR : requestor; return Couple.of(text, requestor); } void setRefreshVisible(final boolean visible) { UIUtil.invokeLaterIfNeeded(() -> { myRefreshAlarm.cancelAllRequests(); myRefreshAlarm.addRequest(() -> { if (visible) { myRefreshIcon.resume(); } else { myRefreshIcon.suspend(); } myRefreshIcon.revalidate(); myRefreshIcon.repaint(); }, visible ? 100 : 300); }); } void setRefreshToolTipText(final String tooltip) { myRefreshIcon.setToolTipText(tooltip); } public BalloonHandler notifyByBalloon(MessageType type, String htmlBody, @Nullable Icon icon, @Nullable HyperlinkListener listener) { final Balloon balloon = JBPopupFactory.getInstance().createHtmlTextBalloonBuilder( htmlBody.replace("\n", "<br>"), icon != null ? icon : type.getDefaultIcon(), type.getPopupBackground(), listener).createBalloon(); SwingUtilities.invokeLater(() -> { Component comp = this; if (comp.isShowing()) { int offset = comp.getHeight() / 2; Point point = new Point(comp.getWidth() - offset, comp.getHeight() - offset); balloon.show(new RelativePoint(comp, point), Balloon.Position.above); } else { final JRootPane rootPane = SwingUtilities.getRootPane(comp); if (rootPane != null && rootPane.isShowing()) { final Container contentPane = rootPane.getContentPane(); final Rectangle bounds = contentPane.getBounds(); final Point target = UIUtil.getCenterPoint(bounds, JBUI.size(1, 1)); target.y = bounds.height - 3; balloon.show(new RelativePoint(contentPane, target), Balloon.Position.above); } } }); return () -> SwingUtilities.invokeLater(balloon::hide); } private static class InlineLayout extends AbstractLayoutManager { private int myProgressWidth; @Override public Dimension preferredLayoutSize(final Container parent) { Dimension result = new Dimension(); for (int i = 0; i < parent.getComponentCount(); i++) { final Dimension prefSize = parent.getComponent(i).getPreferredSize(); result.width += prefSize.width; result.height = Math.max(prefSize.height, result.height); } return result; } @Override public void layoutContainer(final Container parent) { assert parent.getComponentCount() == 2; // 1. info; 2. progress Component infoPanel = parent.getComponent(0); Component progressPanel = parent.getComponent(1); int progressPrefWidth = progressPanel.getPreferredSize().width; final Dimension size = parent.getSize(); int maxProgressWidth = (int) (size.width * 0.8); int minProgressWidth = (int) (size.width * 0.5); if (progressPrefWidth > myProgressWidth) { myProgressWidth = progressPrefWidth; } if (myProgressWidth > maxProgressWidth) { myProgressWidth = maxProgressWidth; } if (myProgressWidth < minProgressWidth) { myProgressWidth = minProgressWidth; } infoPanel.setBounds(0, 0, size.width - myProgressWidth, size.height); progressPanel.setBounds(size.width - myProgressWidth, 0, myProgressWidth, size.height); } } @NotNull private InlineProgressIndicator createInlineDelegate(@NotNull TaskInfo info, @NotNull ProgressIndicatorEx original, final boolean compact) { final Collection<InlineProgressIndicator> inlines = myOriginal2Inlines.get(original); if (inlines != null) { for (InlineProgressIndicator eachInline : inlines) { if (eachInline.isCompact() == compact) return eachInline; } } final InlineProgressIndicator inline = new MyInlineProgressIndicator(compact, info, original); myInline2Original.put(inline, original); myOriginal2Inlines.put(original, inline); if (compact) { inline.getComponent().addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { handle(e); } @Override public void mouseReleased(MouseEvent e) { handle(e); } }); } return inline; } private void triggerPopupShowing() { if (myPopup.isShowing()) { hideProcessPopup(); } else { openProcessPopup(true); } } private void restoreEmptyStatusInner() { removeAll(); updateProgressIcon(); Container iconParent = myProgressIcon.getParent(); if (iconParent != null) { iconParent.remove(myProgressIcon); // to prevent leaks to this removed parent via progress icon } } private void updateProgressIcon() { if (myOriginals.isEmpty() || PowerSaveMode.isEnabled() || myOriginals.stream().map(ProgressSuspender::getSuspender).filter(Objects::nonNull).anyMatch(ProgressSuspender::isSuspended)) { myProgressIcon.suspend(); } else { myProgressIcon.resume(); } } private void restoreEmptyStatus() { restoreEmptyStatusInner(); setLayout(new BorderLayout()); add(myRefreshAndInfoPanel, BorderLayout.CENTER); myRefreshAndInfoPanel.revalidate(); myRefreshAndInfoPanel.repaint(); } //private String formatTime(long t) { // if (t < 1000) return "< 1 sec"; // if (t < 60 * 1000) return (t / 1000) + " sec"; // return "~" + (int)Math.ceil(t / (60 * 1000f)) + " min"; //} boolean isProcessWindowOpen() { return myPopup.isShowing(); } void setProcessWindowOpen(final boolean open) { if (open) { openProcessPopup(true); } else { hideProcessPopup(); } } private class MyInlineProgressIndicator extends InlineProgressIndicator { private ProgressIndicatorEx myOriginal; MyInlineProgressIndicator(final boolean compact, @NotNull TaskInfo task, @NotNull ProgressIndicatorEx original) { super(compact, task); myOriginal = original; original.addStateDelegate(this); addStateDelegate(new AbstractProgressIndicatorExBase(){ @Override public void cancel() { super.cancel(); updateProgress(); } }); Runnable updatePowerSaveStatus = () -> myProgress.setVisible(!PowerSaveMode.isEnabled()); runOnPowerSaveChange(updatePowerSaveStatus, this); updatePowerSaveStatus.run(); } @Override protected JBIterable<ProgressButton> createEastButtons() { return JBIterable.of(createSuspendButton()).append(super.createEastButtons()); } private ProgressButton createSuspendButton() { InplaceButton suspendButton = new InplaceButton( new IconButton("Pause", AllIcons.Actions.Pause, AllIcons.Actions.Pause, AllIcons.Actions.Pause), new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { ProgressSuspender suspender = Objects.requireNonNull(getSuspender()); suspender.setSuspended(!suspender.isSuspended()); updateProgressNow(); } }).setFillBg(false); suspendButton.setVisible(false); return new ProgressButton(suspendButton, () -> { ProgressSuspender suspender = getSuspender(); suspendButton.setVisible(suspender != null); if (suspender != null) { String toolTipText = suspender.isSuspended() ? "Resume" : "Pause"; if (!toolTipText.equals(suspendButton.getToolTipText())) { updateProgressIcon(); } suspendButton.setIcon(suspender.isSuspended() ? AllIcons.Actions.Resume : AllIcons.Actions.Pause); suspendButton.setToolTipText(toolTipText); } }); } @Nullable private ProgressSuspender getSuspender() { ProgressIndicatorEx original = myOriginal; return original == null ? null : ProgressSuspender.getSuspender(original); } @Override public void stop() { super.stop(); updateProgress(); } @Override protected boolean isFinished() { TaskInfo info = getInfo(); return info == null || isFinished(info); } @Override public void finish(@NotNull final TaskInfo task) { super.finish(task); queueRunningUpdate(() -> removeProgress(this)); } @Override public void dispose() { super.dispose(); myOriginal = null; } @Override protected void cancelRequest() { myOriginal.cancel(); } @Override protected void queueProgressUpdate() { synchronized (myDirtyIndicators) { myDirtyIndicators.add(this); } myUpdateQueue.queue(myUpdateIndicators); } @Override protected void queueRunningUpdate(@NotNull final Runnable update) { myUpdateQueue.queue(new Update(new Object(), false, 0) { @Override public void run() { ApplicationManager.getApplication().invokeLater(update); } }); } } private void runQuery() { if (getRootPane() == null) return; Set<InlineProgressIndicator> indicators = getCurrentInlineIndicators(); if (indicators.isEmpty()) return; for (InlineProgressIndicator each : indicators) { each.updateProgress(); } myQueryAlarm.cancelAllRequests(); myQueryAlarm.addRequest(this::runQuery, 2000); } @NotNull private Set<InlineProgressIndicator> getCurrentInlineIndicators() { synchronized (myOriginals) { return myInline2Original.keySet(); } } }