blob: c72e0670464b8298ddb643c28b96c71cf997ac95 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.netbeans.modules.progress.ui;
import java.awt.AWTEvent;
import java.awt.Component;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.event.AWTEventListener;
import java.awt.event.ActionEvent;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowStateListener;
import java.util.HashMap;
import java.util.Map;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JSeparator;
import javax.swing.JWindow;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import org.netbeans.modules.progress.spi.InternalHandle;
import org.netbeans.modules.progress.spi.ProgressEvent;
import org.netbeans.modules.progress.spi.ProgressUIWorkerWithModel;
import org.netbeans.modules.progress.spi.TaskModel;
import org.openide.DialogDisplayer;
import org.openide.NotifyDescriptor;
import org.openide.util.ImageUtilities;
import org.openide.util.NbBundle;
import org.openide.util.Utilities;
import org.openide.windows.WindowManager;
/**
*
* @author Milos Kleint (mkleint@netbeans.org)
*/
public class StatusLineComponent extends JPanel implements ProgressUIWorkerWithModel {
private NbProgressBar bar;
private JLabel label;
private JSeparator separator;
private InternalHandle handle;
private boolean showingPopup = false;
private TaskModel model;
private MouseListener mouseListener;
private HideAWTListener hideListener;
private JWindow popupWindow;
private PopupPane pane;
private Map<InternalHandle, ListComponent> handleComponentMap;
private final int preferredHeight;
private JButton closeButton;
private JLabel extraLabel;
public StatusLineComponent() {
handleComponentMap = new HashMap<InternalHandle, ListComponent>();
FlowLayout flay = new FlowLayout();
flay.setVgap(1);
flay.setHgap(5);
setLayout(flay);
mouseListener = new MListener();
addMouseListener(mouseListener);
hideListener = new HideAWTListener();
createLabel();
createBar();
// tricks to figure out correct height.
bar.setStringPainted(true);
bar.setString("@@@"); // NOI18N
label.setText("@@@"); // NOI18N
preferredHeight = Math.max(label.getPreferredSize().height, bar.getPreferredSize().height) + 3;
setOpaque(false);
discardLabel();
discardBar();
pane = new PopupPane();
pane.getActionMap().put("HidePopup", new AbstractAction() {
public @Override void actionPerformed(ActionEvent actionEvent) {
// System.out.println("escape pressed - hiding");
hidePopup();
}
});
pane.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "HidePopup");
pane.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "HidePopup");
}
private void createLabel() {
discardLabel();
label = new JLabel();
label.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
label.addMouseListener(mouseListener);
}
private void discardLabel() {
if (label != null) {
label.removeMouseListener(mouseListener);
label = null;
}
}
private void createExtraLabel() {
discardExtraLabel();
extraLabel = new JLabel();
extraLabel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
extraLabel.addMouseListener(mouseListener);
}
private void discardExtraLabel() {
if (extraLabel != null) {
extraLabel.removeMouseListener(mouseListener);
extraLabel = null;
}
}
private void createBar() {
discardBar();
bar = new NbProgressBar();
bar.setUseInStatusBar(true);
bar.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
// setBorder(BorderFactory.createLineBorder(Color.BLUE, 1));
// HACK - put smaller font inside the progress bar to keep
// the height of the progressbar constant for determinate and indeterminate bars
// Font fnt = UIManager.getFont("ProgressBar.font");
// bar.setFont(fnt.deriveFont(fnt.getStyle(), fnt.getSize() - 3));
bar.addMouseListener(mouseListener);
}
private void discardBar() {
if (bar != null) {
bar.removeMouseListener(mouseListener);
//EMI: On OSX, an animation thread is started when the progressbar is created, even if not displayed (or added to a container).
// The following line is needed to kill the animation thread otherwise this tends to be alive for the rest of the JVM execution
// pumping a lot of repaing events in the event queue (in my tests, about 50% of the CPU while idle).
// The culprit apple.laf.CUIAquaProgressBar$Animator was discovered with the normal Profiler, while Tim Boudreau told me about a similar
// problem with OSX and the pulsating button (that also has a thread for that animation).
// Finally, btrace and this IDEA bug report (http://www.jetbrains.net/jira/browse/IDEADEV-25376) connected the dots.
bar.getUI().uninstallUI(bar);
bar = null;
}
}
private void createCloseButton() {
discardCloseButton();
closeButton = new JButton();
closeButton.setBorderPainted(false);
closeButton.setBorder(BorderFactory.createEmptyBorder());
closeButton.setOpaque(false);
closeButton.setContentAreaFilled(false);
Image img = (Image)UIManager.get("nb.progress.cancel.icon");
if( null != img ) {
closeButton.setIcon( new ImageIcon( img ) );
}
img = (Image)UIManager.get("nb.progress.cancel.icon.mouseover");
if( null != img ) {
closeButton.setRolloverEnabled(true);
closeButton.setRolloverIcon( new ImageIcon( img ) );
}
img = (Image)UIManager.get("nb.progress.cancel.icon.pressed");
if( null != img ) {
closeButton.setPressedIcon( new ImageIcon( img ) );
}
}
private void setCloseButtonNameAndTooltip() {
closeButton.setName(NbBundle.getMessage(ListComponent.class, "ListComponent.btnClose.name"));
closeButton.setToolTipText(NbBundle.getMessage(ListComponent.class, "ListComponent.btnClose.tooltip"));
}
private void discardCloseButton() {
closeButton = null;
}
private void createSeparator() {
discardSeparator();
separator = new JSeparator(JSeparator.VERTICAL);
// separator.setPreferredSize(new Dimension(5, prefferedHeight));
separator.setBorder(BorderFactory.createEmptyBorder(1, 0, 2, 0));
}
private void discardSeparator() {
separator = null;
}
@Override
public Dimension getPreferredSize() {
Dimension retValue;
retValue = super.getPreferredSize();
retValue.height = preferredHeight;
return retValue;
}
@Override
public Dimension getMinimumSize() {
Dimension retValue;
retValue = super.getMinimumSize();
retValue.height = preferredHeight;
return retValue;
}
@Override
public Dimension getMaximumSize() {
Dimension retValue;
retValue = super.getMaximumSize();
retValue.height = preferredHeight;
return retValue;
}
public @Override void setModel(TaskModel mod) {
model = mod;
model.addListDataListener(new Listener());
model.addListSelectionListener(new ListSelectionListener() {
public @Override void valueChanged(ListSelectionEvent e) {
pane.updateBoldFont(model.getSelectedHandle());
}
});
}
private void setTooltipForAll() {
int size = model.getSize();
String key = "NbProgressBar.tooltip1"; //NOI18N
if (size == 1) {
key = "NbProgressBar.tooltip2"; //NOI18N
}
String text = NbBundle.getMessage(StatusLineComponent.class, key, size);
setToolTipText(text);
if (label != null) {
label.setToolTipText(text);
}
if (bar != null) {
bar.setToolTipText(text);
}
}
public @Override void processProgressEvent(ProgressEvent event) {
if (event.getType() == ProgressEvent.TYPE_START) {
createListItem(event.getSource());
} else if (event.getType() == ProgressEvent.TYPE_PROGRESS ||
event.getType() == ProgressEvent.TYPE_SWITCH ||
event.getType() == ProgressEvent.TYPE_SILENT) {
ListComponent comp = handleComponentMap.get(event.getSource());
if (comp == null) {
createListItem(event.getSource());
comp = handleComponentMap.get(event.getSource());
}
comp.processProgressEvent(event);
} else if (event.getType() == ProgressEvent.TYPE_FINISH) {
removeListItem(event.getSource());
if (model.getSelectedHandle() != null && handle != model.getSelectedHandle()) {
ProgressEvent snap = model.getSelectedHandle().requestStateSnapshot();
initializeComponent(snap);
if (snap.getSource().isInSleepMode()) {
bar.setString(snap.getMessage());
}
}
//release handle reference if it is the last one
if (model.getSize() == 0 && event.getSource().equals(handle)) {
handle = null;
}
}
}
public @Override void processSelectedProgressEvent(ProgressEvent event) {
if (event.getType() == ProgressEvent.TYPE_START) {
initializeComponent(event);
return;
} else if (event.getType() == ProgressEvent.TYPE_FINISH) {
//happens only when there's no more handles.
hidePopup();
removeAll();
discardSeparator();
discardCloseButton();
discardBar();
discardLabel();
discardExtraLabel();
//#63393, 61940 fix - removeAll() just invalidates. seems to work without revalidate/repaint on some platforms, fail on others.
revalidate();
repaint();
return;
} else {
if (event.getSource() != handle || event.isSwitched() ||
event.getType() == ProgressEvent.TYPE_SILENT || // the following condition re-initiates the bar when going from/to sleep mode..
(event.getSource().isInSleepMode() != (bar.getClientProperty(NbProgressBar.SLEEPY) != null))) { //NIO18N
initializeComponent(event);
}
if (bar != null) {
if (event.getWorkunitsDone() > 0) {
bar.setValue(event.getWorkunitsDone());
}
bar.setString(getBarString(event.getPercentageDone(), event.getEstimatedCompletion()));
if (event.getDisplayName() != null) {
label.setText(event.getDisplayName());
}
if (event.getSource().isInSleepMode()) {
bar.setString(event.getMessage());
}
}
}
}
static String formatEstimate(long estimate) {
long minutes = estimate / 60;
long seconds = estimate - (minutes * 60);
return "" + minutes + (seconds < 10 ? ":0" : ":") + seconds;
}
static String getBarString(double percentage, long estimatedCompletion) {
if (estimatedCompletion != -1) {
return formatEstimate(estimatedCompletion);
}
if (percentage != -1) {
int rounded = (int) Math.round(percentage);
if (rounded > 100) {
rounded = 100;
}
return "" + rounded + "%"; //NOI18N
}
return "";
}
private void updateExtraLabel() {
if (extraLabel != null) {
if (handleComponentMap.size() > 1) {
extraLabel.setText(NbBundle.getMessage(StatusLineComponent.class, "StatusLineComponent.extra", handleComponentMap.size() - 1));
} else {
extraLabel.setText(null);
}
}
}
private void initializeComponent(ProgressEvent event) {
handle = event.getSource();
boolean toShow = false;
if (label == null) {
createLabel();
add(label);
toShow = true;
label.setToolTipText(getToolTipText());
}
label.setText(handle.getDisplayName());
if (bar == null) {
createBar();
add(bar);
toShow = true;
bar.setToolTipText(getToolTipText());
}
NbProgressBar.setupBar(event.getSource(), bar);
if (closeButton == null) {
createCloseButton();
add(closeButton);
toShow = true;
}
if (extraLabel == null) {
createExtraLabel();
updateExtraLabel();
add(extraLabel);
toShow = true;
}
if (separator == null) {
createSeparator();
add(separator);
toShow = true;
}
if (handle.isAllowCancel()) {
closeButton.setAction(new CancelAction(false));
} else {
closeButton.setAction(new EmptyCancelAction());
}
// #200126: tooltip property must be set following the action or it will be overwritten
setCloseButtonNameAndTooltip();
if (toShow) {
revalidate();
repaint();
}
}
private class Listener implements ListDataListener {
public @Override void intervalAdded(ListDataEvent e) {
setTooltipForAll();
}
public @Override void intervalRemoved(ListDataEvent e) {
setTooltipForAll();
}
public @Override void contentsChanged(ListDataEvent e) {
setTooltipForAll();
}
}
public void hidePopup() {
if (GraphicsEnvironment.isHeadless()) {
return;
}
if (popupWindow != null) {
// popupWindow.getContentPane().removeAll();
popupWindow.setVisible(false);
}
Toolkit.getDefaultToolkit().removeAWTEventListener(hideListener);
WindowManager.getDefault().getMainWindow().removeWindowStateListener(hideListener);
WindowManager.getDefault().getMainWindow().removeComponentListener(hideListener);
showingPopup = false;
}
private void createListItem(InternalHandle handle) {
ListComponent comp;
if (handleComponentMap.containsKey(handle)) {
// happens when we click to display on popup and there is a
// new handle waiting in the queue.
comp = handleComponentMap.get(handle);
} else {
comp = new ListComponent(handle);
handleComponentMap.put(handle, comp);
}
pane.addListComponent(comp);
pane.updateBoldFont(model.getSelectedHandle());
if (showingPopup) {
resizePopup();
}
updateExtraLabel();
}
private void removeListItem(InternalHandle handle) {
ListComponent c = handleComponentMap.remove(handle);
pane.removeListComponent(handle);
pane.updateBoldFont(model.getSelectedHandle());
if (showingPopup) {
resizePopup();
}
if (c != null) {
c.clearProgressBarOSX();
}
updateExtraLabel();
}
public @Override void showPopup() {
if (GraphicsEnvironment.isHeadless()) {
return;
}
if (showingPopup) {
return;
}
InternalHandle[] handles = model.getHandles();
if (handles.length == 0) {
// just in case..
return;
}
showingPopup = true;
// NOT using PopupFactory
// 1. on linux, creates mediumweight popup taht doesn't refresh behind visible glasspane
// 2. on mac, needs an owner frame otherwise hiding tooltip also hides the popup. (linux requires no owner frame to force heavyweight)
// 3. the created window is not focusable window
if (popupWindow == null) {
popupWindow = new JWindow(WindowManager.getDefault().getMainWindow());
popupWindow.getContentPane().add(pane);
}
Toolkit.getDefaultToolkit().addAWTEventListener(hideListener, AWTEvent.MOUSE_EVENT_MASK);
WindowManager.getDefault().getMainWindow().addWindowStateListener(hideListener);
WindowManager.getDefault().getMainWindow().addComponentListener(hideListener);
resizePopup();
popupWindow.setVisible(true);
pane.requestFocus();
pane.repaint();
// System.out.println(" window focusable=" + popupWindow.isFocusableWindow());
}
private void resizePopup() {
popupWindow.pack();
Point point = new Point(0,0);
SwingUtilities.convertPointToScreen(point, this);
Dimension dim = popupWindow.getSize();
//#63265
Rectangle usableRect = Utilities.getUsableScreenBounds();
int sepShift = separator != null ? separator.getSize().width : 0;
Point loc = new Point(point.x + this.getSize().width - dim.width - sepShift - 5 * 2 , point.y - dim.height - 5);
// -5 in x coordinate is becuase of the hgap between the separator and button and separator and edge
//#JDEV #17370036
if( loc.x < usableRect.x )
loc.x = Math.max( loc.x, usableRect.x );
if( loc.x+dim.width > usableRect.x+usableRect.width )
loc.x = usableRect.x + usableRect.width - dim.width;
if (! usableRect.contains(loc)) {
loc = new Point(loc.x, point.y + 5 + this.getSize().height);
}
// +4 here because of the width of the close button in popup, we
// want the progress bars to align visually.. but there's separator in status now..
popupWindow.setLocation(loc);
// System.out.println("count=" + count);
// System.out.println("offset =" + offset);
}
private class HideAWTListener extends ComponentAdapter implements AWTEventListener, WindowStateListener {
public @Override void eventDispatched(java.awt.AWTEvent aWTEvent) {
if (aWTEvent instanceof MouseEvent) {
MouseEvent mv = (MouseEvent)aWTEvent;
if (mv.getClickCount() > 0) {
//#118828
if (! (aWTEvent.getSource() instanceof Component)) {
return;
}
Component comp = (Component)aWTEvent.getSource();
Container par = SwingUtilities.getAncestorNamed("progresspopup", comp); //NOI18N
Container barpar = SwingUtilities.getAncestorOfClass(StatusLineComponent.class, comp);
if (par == null && barpar == null) {
hidePopup();
}
}
}
}
public @Override void windowStateChanged(WindowEvent windowEvent) {
if (showingPopup) {
int oldState = windowEvent.getOldState();
int newState = windowEvent.getNewState();
if (((oldState & Frame.ICONIFIED) == 0) &&
((newState & Frame.ICONIFIED) == Frame.ICONIFIED)) {
hidePopup();
// } else if (((oldState & Frame.ICONIFIED) == Frame.ICONIFIED) &&
// ((newState & Frame.ICONIFIED) == 0 )) {
// //TODO remember we showed before and show again? I guess not worth the efford, not part of spec.
}
}
}
@Override
public void componentResized(ComponentEvent evt) {
if (showingPopup) {
resizePopup();
}
}
@Override
public void componentMoved(ComponentEvent evt) {
if (showingPopup) {
resizePopup();
}
}
}
private class MListener extends MouseAdapter {
@Override
public void mouseClicked(java.awt.event.MouseEvent e) {
if (e.getButton() != MouseEvent.BUTTON1) {
showMenu(e);
} else {
if (showingPopup) {
hidePopup();
} else {
showPopup();
}
}
}
}
private void showMenu(MouseEvent e) {
JPopupMenu popup = new JPopupMenu();
popup.add(new ProgressListAction(NbBundle.getMessage(StatusLineComponent.class, "StatusLineComponent.ShowProcessList")));
popup.add(new ViewAction());
popup.add(new CancelAction(true));
popup.show((Component)e.getSource(), e.getX(), e.getY());
}
private class CancelAction extends AbstractAction {
public CancelAction(boolean text) {
if (text) {
putValue(Action.NAME, NbBundle.getMessage(StatusLineComponent.class, "StatusLineComponent.Cancel"));
} else {
Image icon = (Image)UIManager.get("nb.progress.cancel.icon");
if (icon == null) {
// for custom L&F?
putValue(Action.SMALL_ICON, ImageUtilities.loadImageIcon("org/netbeans/progress/module/resources/buton.png", true));
} else {
putValue(Action.SMALL_ICON, new ImageIcon(icon));
}
}
setEnabled(handle == null ? false : handle.isAllowCancel());
}
public @Override void actionPerformed(ActionEvent actionEvent) {
InternalHandle hndl = handle;
if (hndl !=null && hndl.getState() == InternalHandle.STATE_RUNNING) {
String message = NbBundle.getMessage(StatusLineComponent.class, "Cancel_Question", handle.getDisplayName());
String title = NbBundle.getMessage(StatusLineComponent.class, "Cancel_Question_Title");
NotifyDescriptor dd = new NotifyDescriptor(message, title,
NotifyDescriptor.YES_NO_OPTION,
NotifyDescriptor.QUESTION_MESSAGE, null, null);
Object retType = DialogDisplayer.getDefault().notify(dd);
if (retType == NotifyDescriptor.YES_OPTION && hndl.getState() == InternalHandle.STATE_RUNNING) {
hndl.requestCancel();
}
}
}
}
private class ViewAction extends AbstractAction {
public ViewAction() {
putValue(Action.NAME, NbBundle.getMessage(StatusLineComponent.class, "StatusLineComponent.View"));
setEnabled(handle == null ? false : handle.isAllowView());
}
public @Override void actionPerformed(ActionEvent actionEvent) {
if (handle != null) {
handle.requestView();
}
}
}
private class EmptyCancelAction extends AbstractAction {
public EmptyCancelAction() {
setEnabled(false);
putValue(Action.SMALL_ICON, new Icon() {
public @Override int getIconHeight() {
return 12;
}
public @Override int getIconWidth() {
return 12;
}
public @Override void paintIcon(Component c, Graphics g, int x, int y) {
}
});
putValue(Action.NAME, "");
}
public @Override void actionPerformed(ActionEvent e) {
}
}
}