/* * 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.openapi.wm.impl.status; import com.intellij.ide.ui.UISettings; import com.intellij.idea.ActionsBundle; import com.intellij.notification.EventLog; 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.editor.impl.EditorComponentImpl; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.TaskInfo; import com.intellij.openapi.ui.MessageType; import com.intellij.openapi.ui.popup.Balloon; import com.intellij.openapi.ui.popup.BalloonHandler; import com.intellij.openapi.ui.popup.JBPopupFactory; 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.StatusBar; import com.intellij.openapi.wm.StatusBarWidget; import com.intellij.openapi.wm.ex.ProgressIndicatorEx; import com.intellij.ui.Gray; import com.intellij.ui.awt.RelativePoint; import com.intellij.ui.components.labels.LinkLabel; import com.intellij.ui.components.labels.LinkListener; import com.intellij.ui.components.panels.Wrapper; import com.intellij.util.Alarm; 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.border.EmptyBorder; import javax.swing.event.HyperlinkListener; import java.awt.*; 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 ArrayList myOriginals = new ArrayList(); private final ArrayList myInfos = new ArrayList(); private final Map myInline2Original = new HashMap(); private final MultiValuesMap 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; public InfoAndProgressPanel() { setOpaque(false); myRefreshIcon = new RefreshFileSystemIcon(); // new AsyncProcessIcon("Refreshing filesystem") { // protected Icon getPassiveIcon() { // return myEmptyRefreshIcon; // } // // @Override // public Dimension getPreferredSize() { // if (!isRunning()) return new Dimension(0, 0); // return super.getPreferredSize(); // } // // @Override // public void paint(Graphics g) { // g.translate(0, -1); // super.paint(g); // g.translate(0, 1); // } //}; 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(); } 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); InlineProgressIndicator[] indicators = getCurrentInlineIndicators().toArray(new InlineProgressIndicator[0]); for (InlineProgressIndicator indicator : indicators) { Disposer.dispose(indicator); } myInline2Original.clear(); myOriginal2Inlines.clear(); } @Override public JComponent getComponent() { return this; } @NotNull public List> getBackgroundProcesses() { synchronized (myOriginals) { if (myOriginals.isEmpty()) return Collections.emptyList(); List> 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); } } public 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); myProgressIcon.resume(); 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; final boolean last = myOriginals.size() == 1; final boolean beforeLast = myOriginals.size() == 2; myPopup.removeIndicator(progress); final ProgressIndicatorEx original = removeFromMaps(progress); if (myOriginals.contains(original)) 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(); } } private ProgressIndicatorEx removeFromMaps(@NotNull InlineProgressIndicator progress) { final ProgressIndicatorEx original = myInline2Original.get(progress); myInline2Original.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 label = new LinkLabel(myOriginals.size() + processWord + " running...", null, new LinkListener() { @Override public void linkSelected(final LinkLabel aSource, final Object aLinkData) { triggerPopupShowing(); } }); if (SystemInfo.isMac) label.setFont(UIUtil.getLabelFont().deriveFont(11.0f)); 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(new EmptyBorder(0, 0, 0, 4)); add(progressCountPanel, BorderLayout.EAST); revalidate(); repaint(); } private void buildInInlineIndicator(@NotNull final InlineProgressIndicator inline) { removeAll(); setLayout(new InlineLayout()); add(myRefreshAndInfoPanel); final JPanel inlinePanel = new JPanel(new BorderLayout()); inline.getComponent().setBorder(new EmptyBorder(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(); UISettings uiSettings = UISettings.getInstance(); if (uiSettings.PRESENTATION_MODE || !uiSettings.SHOW_STATUS_BAR && Registry.is("ide.show.progress.without.status.bar")) { final JRootPane pane = myInfoPanel.getRootPane(); final RelativePoint point = new RelativePoint(pane, new Point(pane.getWidth() - 250, 60)); final PresentationModeProgressPanel panel = new PresentationModeProgressPanel(inline); final MyInlineProgressIndicator delegate = new MyInlineProgressIndicator(true, inline.getInfo(), inline) { @Override protected void updateProgress() { super.updateProgress(); panel.update(); } }; Disposer.register(inline, delegate); JBPopupFactory.getInstance().createBalloonBuilder(panel.getRootPanel()) .setFadeoutTime(0) .setFillColor(Gray.TRANSPARENT) .setShowCallout(false) .setBorderColor(Gray.TRANSPARENT) .setBorderInsets(new Insets(0, 0, 0, 0)) .setAnimationCycle(0) .setCloseButtonEnabled(false) .setHideOnClickOutside(false) .setDisposable(inline) .setHideOnFrameResize(false) .setHideOnKeyOutside(false) .setBlockClicksThroughBalloon(true) .setHideOnAction(false) .createBalloon().show(new PositionTracker(pane) { @Override public RelativePoint recalculateLocation(Balloon object) { final EditorComponentImpl editorComponent = UIUtil.findComponentOfType(pane, EditorComponentImpl.class); if (editorComponent != null) { return new RelativePoint(editorComponent.getParent().getParent(), new Point(editorComponent.getParent().getParent().getWidth() - 150, editorComponent.getParent().getParent().getHeight() - 70)); } return point; } }, Balloon.Position.above); } } public Couple 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); } public void setRefreshVisible(final boolean visible) { UIUtil.invokeLaterIfNeeded(new Runnable() { @Override public void run() { myRefreshAlarm.cancelAllRequests(); myRefreshAlarm.addRequest(new Runnable() { @Override public void run() { if (visible) { myRefreshIcon.resume(); } else { myRefreshIcon.suspend(); } myRefreshIcon.revalidate(); myRefreshIcon.repaint(); } }, visible ? 100 : 300); } }); } public 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", "
"), icon != null ? icon : type.getDefaultIcon(), type.getPopupBackground(), listener).createBalloon(); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { Component comp = InfoAndProgressPanel.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, new Dimension(1, 1)); target.y = bounds.height - 3; balloon.show(new RelativePoint(contentPane, target), Balloon.Position.above); } } } }); return new BalloonHandler() { @Override public void hide() { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { 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 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 restoreEmptyStatus() { removeAll(); setLayout(new BorderLayout()); add(myRefreshAndInfoPanel, BorderLayout.CENTER); myProgressIcon.suspend(); 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"; //} public boolean isProcessWindowOpen() { return myPopup.isShowing(); } public void setProcessWindowOpen(final boolean open) { if (open) { openProcessPopup(true); } else { hideProcessPopup(); } } private class MyInlineProgressIndicator extends InlineProgressIndicator { private ProgressIndicatorEx myOriginal; public MyInlineProgressIndicator(final boolean compact, @NotNull TaskInfo task, @NotNull ProgressIndicatorEx original) { super(compact, task); myOriginal = original; original.addStateDelegate(this); } @Override public void cancel() { super.cancel(); updateProgress(); } @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(new Runnable() { @Override public void run() { removeProgress(MyInlineProgressIndicator.this); Disposer.dispose(MyInlineProgressIndicator.this); } }); } @Override public void dispose() { super.dispose(); myOriginal = null; } @Override protected void cancelRequest() { myOriginal.cancel(); } @Override protected void queueProgressUpdate(final Runnable update) { myUpdateQueue.queue(new Update(MyInlineProgressIndicator.this, false, 1) { @Override public void run() { update.run(); } }); } @Override protected void queueRunningUpdate(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 indicators = getCurrentInlineIndicators(); if (indicators.isEmpty()) return; for (InlineProgressIndicator each : indicators) { each.updateProgress(); } myQueryAlarm.cancelAllRequests(); myQueryAlarm.addRequest(new Runnable() { @Override public void run() { runQuery(); } }, 2000); } @NotNull private Set getCurrentInlineIndicators() { synchronized (myOriginals) { return myInline2Original.keySet(); } } }