package org.gudy.azureus2.ui.swt.progress; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.StyleRange; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.events.MouseAdapter; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.*; import org.gudy.azureus2.core3.internat.MessageText; import org.gudy.azureus2.core3.util.Constants; import org.gudy.azureus2.ui.swt.*; import org.gudy.azureus2.ui.swt.mainwindow.Colors; import org.gudy.azureus2.ui.swt.twistie.ITwistieListener; import org.gudy.azureus2.ui.swt.twistie.TwistieLabel; import org.gudy.azureus2.ui.swt.twistie.TwistieSection; import com.aelitis.azureus.ui.swt.imageloader.ImageLoader; /** * * @author knguyen * */ public class ProgressReporterPanel extends Composite implements IProgressReportConstants, IProgressReporterListener { private Color normalColor = null; private Color errorColor = null; public IProgressReporter pReporter = null; private Label imageLabel = null; private Label nameLabel = null; private Label statusLabel = null; private StyledText detailListWidget = null; private GridData detailSectionData = null; private AZProgressBar pBar = null; private Composite progressPanel = null; private TwistieSection detailSection = null; private int style; private Label actionLabel_cancel = null; private Label actionLabel_remove = null; private Label actionLabel_retry = null; /** * The height of the detail panel when the window first appears. * This only takes effect when there is some detail messages to display and when that * list of messages is too long; this value limits the height of the panel so that the window * does not grow to take up too much of the screen in such instances. */ private int maxPreferredDetailPanelHeight = 200; /** * The preferred maximum height for the detail section on initialization */ private int maxPreferredDetailPanelHeight_Standalone = 600; /** * The preferred maximum width for the panel. When the longest line of the detail messages * of the width of the title is too wide we use this limit to prevent the panel from taking * up too much width; the user is still free to manually make the panel wider of narrower as desired */ private int maxPreferredWidth = 900; /** * we persist the last error to avoid it being overwritten by subsequent non-error messages */ private String lastStatusError = null; /** * Create a panel for the given reporter. * <code>style</code> could be one or more of these: * <ul> * <li><code>IProgressReportConstants.NONE</code> -- the default</li> * <li><code>IProgressReportConstants.AUTO_CLOSE</code> -- automatically disposes this panel when the given reporter is done</li> * <li><code>IProgressReportConstants.STANDALONE</code> -- this panel will be hosted by itself in a window; the detail section of this panel will be given more height</li> * <li><code>IProgressReportConstants.BORDER</code> -- this panel will be hosted by itself in a window; the detail section of this panel will be given more height</li> * </ul> * @param parent the <code>Composite</code> hosting the panel * @param reporter the <code>IProgressReporter</code> to host * @param style one of the style bits listed above */ public ProgressReporterPanel(Composite parent, IProgressReporter reporter, int style) { super(parent, ((style & BORDER) != 0 ? SWT.BORDER : SWT.NONE)); if (null == reporter) { throw new NullPointerException("IProgressReporter can not be null");//KN: should use resource? } this.pReporter = reporter; this.style = style; normalColor = Colors.blue; errorColor = Colors.colorError; /* * Give this Composite some margin and spacing */ setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); GridLayout gLayout = new GridLayout(2, false); gLayout.marginWidth = 25; gLayout.marginTop =15; gLayout.marginBottom=10; setLayout(gLayout); /* * Creates the rest of the controls */ createControls(pReporter.getProgressReport()); /* * Resize content when this panel changes size */ addListener(SWT.Resize, new Listener() { public void handleEvent(Event e) { resizeContent(); } }); /* * Update the UI when ever the reporter send a report */ pReporter.addListener(this); } /** * Call-back method from <code>IProgressReporterListener</code>; this method is called when ever the reporter * dispatches an event */ public int report(IProgressReport pReport) { return handleEvents(pReport); } /** * Creates all the controls for the panel * @param pReport */ private void createControls(IProgressReport pReport) { imageLabel = new Label(this, SWT.NONE); imageLabel.setLayoutData(new GridData(SWT.BEGINNING, SWT.TOP, false, false,1,3)); /* * Creates the main panel */ progressPanel = new Composite(this, SWT.NONE); progressPanel.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false)); GridLayout rightLayout = new GridLayout(4, false); rightLayout.marginHeight = 0; rightLayout.marginWidth = 0; progressPanel.setLayout(rightLayout); /* * Creates all the controls */ nameLabel = new Label(progressPanel, SWT.WRAP); nameLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 4, 1)); pBar = new AZProgressBar(progressPanel, pReport.isIndeterminate()); pBar.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); actionLabel_cancel = new Label(progressPanel, SWT.NONE); actionLabel_cancel.setLayoutData(new GridData(SWT.END, SWT.CENTER, false, false)); actionLabel_remove = new Label(progressPanel, SWT.NONE); actionLabel_remove.setLayoutData(new GridData(SWT.END, SWT.CENTER, false, false)); actionLabel_retry = new Label(progressPanel, SWT.NONE); actionLabel_retry.setLayoutData(new GridData(SWT.END, SWT.CENTER, false, false)); statusLabel = new Label(progressPanel, SWT.NONE); statusLabel.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, true, false, 2, 1)); /* * Creates the detail section */ createDetailSection(pReport); /* * Initialize controls from information in the given report */ initControls(pReport); /* * Listener to 'cancel' label */ actionLabel_cancel.addMouseListener(new MouseAdapter() { public void mouseDown(MouseEvent e) { pReporter.cancel(); } }); /* * Listener to 'retry' label */ actionLabel_retry.addMouseListener(new MouseAdapter() { public void mouseDown(MouseEvent e) { pReporter.retry(); } }); /* * Listener to 'remove' label */ actionLabel_remove.addMouseListener(new MouseAdapter() { public void mouseDown(MouseEvent e) { /* * Removes the current reporter from the history stack of the reporting manager */ ProgressReportingManager.getInstance().remove(pReporter); /* * Then perform general clean-ups */ dispose(); } }); } /** * Initialize the controls with information from the given <code>IProgressReport</code> * @param pReport */ private void initControls(IProgressReport pReport) { /* * Image */ if (null != pReport.getImage()) { imageLabel.setImage(pReport.getImage()); } else { imageLabel.setImage(getDisplay().getSystemImage(SWT.ICON_INFORMATION)); } /* * Name label */ nameLabel.setText(formatForDisplay(pReport.getName())); /* * Action labels */ ImageLoader imageLoader = ImageLoader.getInstance(); imageLoader.setLabelImage(actionLabel_cancel, "progress_cancel"); imageLoader.setLabelImage(actionLabel_remove, "progress_remove"); imageLoader.setLabelImage(actionLabel_retry, "progress_retry"); actionLabel_cancel.setToolTipText(MessageText.getString("Progress.reporting.action.label.cancel.tooltip")); actionLabel_remove.setToolTipText(MessageText.getString("Progress.reporting.action.label.remove.tooltip")); actionLabel_retry.setToolTipText(MessageText.getString("Progress.reporting.action.label.retry.tooltip")); /* ======================================== * Catch up on any messages we might have missed */ { if (true == pReport.isInErrorState()) { updateStatusLabel( MessageText.getString("Progress.reporting.default.error"), true); }else if (true == pReport.isDone()) { updateStatusLabel( MessageText.getString("Progress.reporting.status.finished"), false); } else if (true == pReport.isCanceled()) { updateStatusLabel( MessageText.getString("Progress.reporting.status.canceled"), false); } else if (true == pReport.isIndeterminate()) { updateStatusLabel(Constants.INFINITY_STRING, false); } else { updateStatusLabel(pReport.getPercentage() + "%", false); } } /* * Synch the progress bar */ synchProgressBar(pReport); /* * Synch the action label according to existing properties of the ProgressReport */ synchActionLabels(pReport); } /** * Below the panel, taking up the entire width of the window, is the detail section */ private void createDetailSection(IProgressReport pReport) { Label separator = new Label(this, SWT.SEPARATOR | SWT.HORIZONTAL); separator.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); detailSection = new TwistieSection(this, TwistieLabel.NONE); detailSection.setTitle(MessageText.getString("Progress.reporting.action.label.detail")); Composite sectionContent = detailSection.getContent(); detailSectionData = new GridData(SWT.FILL, SWT.FILL, true, true); detailSection.setLayoutData(detailSectionData); GridLayout sectionLayout = new GridLayout(); sectionLayout.marginHeight = 0; sectionLayout.marginWidth = 0; sectionContent.setLayout(sectionLayout); detailSection.setEnabled(false); detailListWidget = new StyledText(sectionContent, SWT.BORDER | SWT.V_SCROLL | SWT.WRAP); detailListWidget.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); /* * Add a default message instead of an empty box if there is no history; * remove this later when a real detail message arrive */ IMessage[] messages = pReporter.getMessageHistory(); /* * Show error messages in red; otherwise use default color */ for (int i = 0; i < messages.length; i++) { if (messages[i].getType() == MSG_TYPE_ERROR) { appendToDetail(formatForDisplay(messages[i].getValue()), true); } else { appendToDetail(formatForDisplay(messages[i].getValue()), false); } } resizeDetailSection(); /* * Force a layout when ever the section is collapsed or expanded */ detailSection.addTwistieListener(new ITwistieListener() { public void isCollapsed(boolean value) { resizeDetailSection(); layout(true, true); } }); } /** * Ensure that the detail does not take up too much vertical space */ private void resizeDetailSection() { /* * Since detail panel automatically grows to show the entire list of detail messages * we want to at least place a limit on its height just in case the list is too long. * A vertical scrollbar will appear so the user can scroll through the rest of the list */ Point computedSize = detailSection.computeSize(SWT.DEFAULT, SWT.DEFAULT); detailSectionData.heightHint = computedSize.y; if ((STANDALONE & style) != 0) { if (computedSize.y > maxPreferredDetailPanelHeight_Standalone) { detailSectionData.heightHint = maxPreferredDetailPanelHeight_Standalone; } } else if (computedSize.y > maxPreferredDetailPanelHeight) { detailSectionData.heightHint = maxPreferredDetailPanelHeight; } if (computedSize.x > maxPreferredWidth) { detailSectionData.widthHint = maxPreferredWidth; } } public Point computeSize(int hint, int hint2, boolean changed) { Point newSize = super.computeSize(hint, hint2, changed); if (newSize.x > maxPreferredWidth) { newSize.x = maxPreferredWidth; } return newSize; } /** * Process the event from the given <code>ProgressReport</code> * * @param pReport */ private int handleEvents(final IProgressReport pReport) { if (null == pReport || true == isDisposed() || null == getDisplay()) { return RETVAL_OK; } /* Note each 'case' statement of the 'switch' block performs its UI update encapsulated in an .asyncExec() * so that the UI does not freeze or flicker. It may be tempting to encapsulate the whole 'switch' block * within one .asyncExec() but then a separate loop would be required to wait for the Runnable to finish * before attaining the correct return code. The alternative would be to encapsulate the 'switch' block * in a .syncExec() but that would cause freezing and flickering */ switch (pReport.getReportType()) { case REPORT_TYPE_PROPERTY_CHANGED: getDisplay().asyncExec(new Runnable() { public void run() { if (null != nameLabel && false == nameLabel.isDisposed()) { nameLabel.setText(formatForDisplay(pReport.getName())); } if (true == pReport.isIndeterminate()) { updateStatusLabel(Constants.INFINITY_STRING, false); } else { updateStatusLabel(pReport.getPercentage() + "%", false); } appendToDetail(pReport.getMessage(), false); appendToDetail(pReport.getDetailMessage(), false); synchProgressBar(pReport); synchActionLabels(pReport); resizeContent(); } }); break; case REPORT_TYPE_CANCEL: getDisplay().asyncExec(new Runnable() { public void run() { synchProgressBar(pReport); updateStatusLabel( MessageText.getString("Progress.reporting.status.canceled"), false); appendToDetail(pReport.getMessage(), false); synchActionLabels(pReport); resizeContent(); } }); break; case REPORT_TYPE_DONE: getDisplay().asyncExec(new Runnable() { public void run() { if (((style & AUTO_CLOSE) != 0)) { dispose(); } else { synchProgressBar(pReport); updateStatusLabel( MessageText.getString("Progress.reporting.status.finished"), false); appendToDetail( MessageText.getString("Progress.reporting.status.finished"), false); synchActionLabels(pReport); resizeContent(); } } }); /* * Since the reporter is done we don't need this listener anymore */ return RETVAL_OK_TO_DISPOSE; case REPORT_TYPE_MODE_CHANGE: getDisplay().asyncExec(new Runnable() { public void run() { if (null != pBar && false == pBar.isDisposed()) { pBar.setIndeterminate(pReport.isIndeterminate()); } } }); break; case REPORT_TYPE_ERROR: getDisplay().asyncExec(new Runnable() { public void run() { updateStatusLabel( MessageText.getString("Progress.reporting.default.error"), true); appendToDetail(pReport.getErrorMessage(), true); synchActionLabels(pReport); synchProgressBar(pReport); resizeContent(); } }); break; case REPORT_TYPE_RETRY: getDisplay().asyncExec(new Runnable() { public void run() { lastStatusError = null; updateStatusLabel(pReport.getMessage(), false); appendToDetail( MessageText.getString("Progress.reporting.status.retrying"), false); synchActionLabels(pReport); synchProgressBar(pReport); resizeContent(); } }); break; default: break; } return RETVAL_OK; } /** * Synchronize the progress bar with the given <code>IProgressReport</code> * @param pReport */ private void synchProgressBar(IProgressReport pReport) { if (null == pBar || pBar.isDisposed() || null == pReport) { return; } if (true == pReport.isInErrorState()) { pBar.setIndeterminate(false); pBar.setSelection(pReport.getMinimum()); } else { pBar.setIndeterminate(pReport.isIndeterminate()); if (false == pReport.isIndeterminate()) { pBar.setMinimum(pReport.getMinimum()); pBar.setMaximum(pReport.getMaximum()); } pBar.setSelection(pReport.getSelection()); } } /** * Sets the defined color to the given <code>label</code> * @param label * @param text * @param showAsError <code>true</code> to show as error; <code>false</code> otherwise */ private void updateStatusLabel(String text, boolean showAsError) { if (null == statusLabel || statusLabel.isDisposed()) { return; } // we persist any error reports as these need to be sticky and not // overritten by subsequent messages if ( showAsError ){ lastStatusError = text; } if ( lastStatusError != null ){ showAsError = true; text = lastStatusError; } statusLabel.setText(formatForDisplay(text)); if (false == showAsError) { statusLabel.setForeground(normalColor); } else { statusLabel.setForeground(errorColor); } statusLabel.update(); } /** * Display the appropriate text for the action labels * based on what action can be taken */ private void synchActionLabels(IProgressReport pReport) { if (null == actionLabel_remove || null == actionLabel_cancel || null == actionLabel_retry || true == actionLabel_remove.isDisposed() || true == actionLabel_cancel.isDisposed() || true == actionLabel_retry.isDisposed()) { return; } /* * There are 3 labels that can be clicked on; base on the state of the reporter itself. * The basic rules are these: * * If it's done * then show just "remove" * * else if it's in error * and retry is allowed * then show "retry" and "remove" * else just show "remove" * * else if it's been canceled * and retry is allowed * then show "retry" and "remove" * else just show "remove" * * else if it's none of the above * then show just the "cancel" label * enable the label if cancel is allowed * else disable the label * */ showActionLabel(actionLabel_cancel, false); showActionLabel(actionLabel_remove, false); showActionLabel(actionLabel_retry, false); if (true == pReport.isDone()) { showActionLabel(actionLabel_remove, true); } else if (true == pReport.isInErrorState()) { if (true == pReport.isRetryAllowed()) { showActionLabel(actionLabel_retry, true); showActionLabel(actionLabel_remove, true); } else { showActionLabel(actionLabel_remove, true); } } else if (true == pReport.isCanceled()) { if (true == pReport.isRetryAllowed()) { showActionLabel(actionLabel_retry, true); showActionLabel(actionLabel_remove, true); } else { showActionLabel(actionLabel_remove, true); } } else { showActionLabel(actionLabel_cancel, true); actionLabel_cancel.setEnabled(pReport.isCancelAllowed()); } } /** * Convenience method for showing or hiding a label by setting its <code>GridData.widthHint</code> * @param label * @param showIt */ private void showActionLabel(Label label, boolean showIt) { ((GridData) label.getLayoutData()).widthHint = (true == showIt) ? 16 : 0; } /** * Resizes the content of this panel to fit within the shell and to layout children control appropriately */ public void resizeContent() { if (false == isDisposed()) { layout(true, true); } } /** * Formats the string so it displays properly in an SWT text control * @param string * @return */ private String formatForDisplay(String string) { /* * SWT text controls do not allow a null argument as a value and will throw an exception if a <code>null</code> is encountered</p> */ string = null == string ? "" : string; /* * Escaping the '&' character so it won't be shown as an underscore */ return string.replaceAll("&", "&&"); } public void addTwistieListener(ITwistieListener listener) { detailSection.addTwistieListener(listener); } public void removeTwistieListener(ITwistieListener listener) { detailSection.removeTwistieListener(listener); } /** * Appends the given message to the detail panel; render the message in error color if specified * @param value * @param isError if <code>true</code> then render the message in the system error color; otherwise render in default color */ private void appendToDetail(String value, boolean isError) { if (null == value || value.length() < 1) { return; } if (null == detailListWidget || detailListWidget.isDisposed()) { return; } int charCount = detailListWidget.getCharCount(); detailListWidget.append(value + "\n"); if (true == isError) { StyleRange style2 = new StyleRange(); style2.start = charCount; style2.length = value.length(); style2.foreground = errorColor; detailListWidget.setStyleRange(style2); } detailSection.setEnabled(true); if ( isError ){ detailSection.setCollapsed( false ); detailListWidget.setSelection( detailListWidget.getCharCount(), detailListWidget.getCharCount()); } } /** * Returns the attached reporter * @return */ public IProgressReporter getProgressReporter(){ return pReporter; } public int getStyle(){ return style; } }