/* * Copyright (C) 2011 The Android Open Source Project * * 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.android.sdkuilib.internal.repository.ui; import com.android.sdkuilib.internal.tasks.ILogUiProvider; import com.android.sdkuilib.ui.GridDataBuilder; import com.android.sdkuilib.ui.GridLayoutBuilder; import com.android.utils.ILogger; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.StyleRange; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.ShellAdapter; import org.eclipse.swt.events.ShellEvent; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Widget; /** * A floating log window that can be displayed or hidden by the main SDK Manager 2 window. * It displays a log of the sdk manager operation (listing, install, delete) including * any errors (e.g. network error or install/delete errors.) * <p/> * Since the SDK Manager will direct all log to this window, its purpose is to be * opened by the main window at startup and left open all the time. When not needed * the floating window is hidden but not closed. This way it can easily accumulate * all the log. */ class LogWindow implements ILogUiProvider { private Shell mParentShell; private Shell mShell; private Composite mRootComposite; private StyledText mStyledText; private Label mLogDescription; private Button mCloseButton; private final ILogger mSecondaryLog; private boolean mCloseRequested; private boolean mInitPosition = true; private String mLastLogMsg = null; private enum TextStyle { DEFAULT, TITLE, ERROR } /** * Creates the floating window. Callers should use {@link #open()} later. * * @param parentShell Parent container * @param secondaryLog An optional logger where messages will <em>also</em> be output. */ public LogWindow(Shell parentShell, ILogger secondaryLog) { mParentShell = parentShell; mSecondaryLog = secondaryLog; } /** * For testing only. See {@link #open()} and {@link #close()} for normal usage. * @wbp.parser.entryPoint */ void openBlocking() { open(); Display display = Display.getDefault(); while (!mShell.isDisposed()) { if (!display.readAndDispatch()) { display.sleep(); } } close(); } /** * Opens the window. * This call does not block and relies on the fact that the main window is * already running an SWT event dispatch loop. * Caller should use {@link #close()} later. */ public void open() { createShell(); createContents(); mShell.open(); mShell.layout(); mShell.setVisible(false); } /** * Closes and <em>destroys</em> the window. * This must be called just before quitting the app. * <p/> * To simply hide/show the window, use {@link #setVisible(boolean)} instead. */ public void close() { if (mShell != null && !mShell.isDisposed()) { mCloseRequested = true; mShell.close(); mShell = null; } } /** * Determines whether the window is currently shown or not. * * @return True if the window is shown. */ public boolean isVisible() { return mShell != null && !mShell.isDisposed() && mShell.isVisible(); } /** * Toggles the window visibility. * * @param visible True to make the window visible, false to hide it. */ public void setVisible(boolean visible) { if (mShell != null && !mShell.isDisposed()) { mShell.setVisible(visible); if (visible && mInitPosition) { mInitPosition = false; positionWindow(); } } } private void createShell() { mShell = new Shell(mParentShell, SWT.SHELL_TRIM | SWT.TOOL); mShell.setMinimumSize(new Point(600, 300)); mShell.setSize(450, 300); mShell.setText("Android SDK Manager Log"); GridLayoutBuilder.create(mShell); mShell.addShellListener(new ShellAdapter() { @Override public void shellClosed(ShellEvent e) { if (!mCloseRequested) { e.doit = false; setVisible(false); } } }); } /** * Create contents of the dialog. */ private void createContents() { mRootComposite = new Composite(mShell, SWT.NONE); GridLayoutBuilder.create(mRootComposite).columns(2); GridDataBuilder.create(mRootComposite).fill().grab(); mStyledText = new StyledText(mRootComposite, SWT.BORDER | SWT.MULTI | SWT.READ_ONLY | SWT.WRAP | SWT.V_SCROLL); GridDataBuilder.create(mStyledText).hSpan(2).fill().grab(); mLogDescription = new Label(mRootComposite, SWT.NONE); GridDataBuilder.create(mLogDescription).hFill().hGrab(); mCloseButton = new Button(mRootComposite, SWT.NONE); mCloseButton.setText("Close"); mCloseButton.setToolTipText("Closes the log window"); mCloseButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { setVisible(false); //$hide$ } }); } // --- Implementation of ILogUiProvider --- /** * Sets the description in the current task dialog. * This method can be invoked from a non-UI thread. */ @Override public void setDescription(final String description) { syncExec(mLogDescription, new Runnable() { @Override public void run() { mLogDescription.setText(description); if (acceptLog(description, true /*isDescription*/)) { appendLine(TextStyle.TITLE, description); if (mSecondaryLog != null) { mSecondaryLog.info("%1$s", description); //$NON-NLS-1$ } } } }); } /** * Logs a "normal" information line. * This method can be invoked from a non-UI thread. */ @Override public void log(final String log) { if (acceptLog(log, false /*isDescription*/)) { syncExec(mLogDescription, new Runnable() { @Override public void run() { appendLine(TextStyle.DEFAULT, log); } }); if (mSecondaryLog != null) { mSecondaryLog.info(" %1$s", log); //$NON-NLS-1$ } } } /** * Logs an "error" information line. * This method can be invoked from a non-UI thread. */ @Override public void logError(final String log) { if (acceptLog(log, false /*isDescription*/)) { syncExec(mLogDescription, new Runnable() { @Override public void run() { appendLine(TextStyle.ERROR, log); } }); if (mSecondaryLog != null) { mSecondaryLog.error(null, "%1$s", log); //$NON-NLS-1$ } } } /** * Logs a "verbose" information line, that is extra details which are typically * not that useful for the end-user and might be hidden until explicitly shown. * This method can be invoked from a non-UI thread. */ @Override public void logVerbose(final String log) { if (acceptLog(log, false /*isDescription*/)) { syncExec(mLogDescription, new Runnable() { @Override public void run() { appendLine(TextStyle.DEFAULT, " " + log); //$NON-NLS-1$ } }); if (mSecondaryLog != null) { mSecondaryLog.info(" %1$s", log); //$NON-NLS-1$ } } } // ---- /** * Centers the dialog in its parent shell. */ private void positionWindow() { // Centers the dialog in its parent shell Shell child = mShell; if (child != null && mParentShell != null) { // get the parent client area with a location relative to the display Rectangle parentArea = mParentShell.getClientArea(); Point parentLoc = mParentShell.getLocation(); int px = parentLoc.x; int py = parentLoc.y; int pw = parentArea.width; int ph = parentArea.height; Point childSize = child.getSize(); int cw = Math.max(childSize.x, pw); int ch = childSize.y; int x = 30 + px + (pw - cw) / 2; if (x < 0) x = 0; int y = py + (ph - ch) / 2; if (y < py) y = py; child.setLocation(x, y); child.setSize(cw, ch); } } private void appendLine(TextStyle style, String text) { if (!text.endsWith("\n")) { //$NON-NLS-1$ text += '\n'; } int start = mStyledText.getCharCount(); if (style == TextStyle.DEFAULT) { mStyledText.append(text); } else { mStyledText.append(text); StyleRange sr = new StyleRange(); sr.start = start; sr.length = text.length(); sr.fontStyle = SWT.BOLD; if (style == TextStyle.ERROR) { sr.foreground = mStyledText.getDisplay().getSystemColor(SWT.COLOR_DARK_RED); } sr.underline = false; mStyledText.setStyleRange(sr); } // Scroll caret if it was already at the end before we added new text. // Ideally we would scroll if the scrollbar is at the bottom but we don't // have direct access to the scrollbar without overriding the SWT impl. if (mStyledText.getCaretOffset() >= start) { mStyledText.setSelection(mStyledText.getCharCount()); } } private void syncExec(final Widget widget, final Runnable runnable) { if (widget != null && !widget.isDisposed()) { widget.getDisplay().syncExec(runnable); } } /** * Filter messages displayed in the log: <br/> * - Messages with a % are typical part of a progress update and shouldn't be in the log. <br/> * - Messages that are the same as the same output message should be output a second time. * * @param msg The potential log line to print. * @return True if the log line should be printed, false otherwise. */ private boolean acceptLog(String msg, boolean isDescription) { if (msg == null) { return false; } msg = msg.trim(); // Descriptions also have the download progress status (0..100%) which we want to avoid if (isDescription && msg.indexOf('%') != -1) { return false; } if (msg.equals(mLastLogMsg)) { return false; } mLastLogMsg = msg; return true; } }