Home | History | Annotate | Download | only in ui
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.sdkuilib.internal.repository.ui;
     18 
     19 import com.android.sdkuilib.internal.tasks.ILogUiProvider;
     20 import com.android.sdkuilib.ui.GridDataBuilder;
     21 import com.android.sdkuilib.ui.GridLayoutBuilder;
     22 import com.android.utils.ILogger;
     23 
     24 import org.eclipse.swt.SWT;
     25 import org.eclipse.swt.custom.StyleRange;
     26 import org.eclipse.swt.custom.StyledText;
     27 import org.eclipse.swt.events.SelectionAdapter;
     28 import org.eclipse.swt.events.SelectionEvent;
     29 import org.eclipse.swt.events.ShellAdapter;
     30 import org.eclipse.swt.events.ShellEvent;
     31 import org.eclipse.swt.graphics.Point;
     32 import org.eclipse.swt.graphics.Rectangle;
     33 import org.eclipse.swt.widgets.Button;
     34 import org.eclipse.swt.widgets.Composite;
     35 import org.eclipse.swt.widgets.Display;
     36 import org.eclipse.swt.widgets.Label;
     37 import org.eclipse.swt.widgets.Shell;
     38 import org.eclipse.swt.widgets.Widget;
     39 
     40 
     41 /**
     42  * A floating log window that can be displayed or hidden by the main SDK Manager 2 window.
     43  * It displays a log of the sdk manager operation (listing, install, delete) including
     44  * any errors (e.g. network error or install/delete errors.)
     45  * <p/>
     46  * Since the SDK Manager will direct all log to this window, its purpose is to be
     47  * opened by the main window at startup and left open all the time. When not needed
     48  * the floating window is hidden but not closed. This way it can easily accumulate
     49  * all the log.
     50  */
     51 class LogWindow implements ILogUiProvider {
     52 
     53     private Shell mParentShell;
     54     private Shell mShell;
     55     private Composite mRootComposite;
     56     private StyledText mStyledText;
     57     private Label mLogDescription;
     58     private Button mCloseButton;
     59 
     60     private final ILogger mSecondaryLog;
     61     private boolean mCloseRequested;
     62     private boolean mInitPosition = true;
     63     private String mLastLogMsg = null;
     64 
     65     private enum TextStyle {
     66         DEFAULT,
     67         TITLE,
     68         ERROR
     69     }
     70 
     71     /**
     72      * Creates the floating window. Callers should use {@link #open()} later.
     73      *
     74      * @param parentShell Parent container
     75      * @param secondaryLog An optional logger where messages will <em>also</em> be output.
     76      */
     77     public LogWindow(Shell parentShell, ILogger secondaryLog) {
     78         mParentShell = parentShell;
     79         mSecondaryLog = secondaryLog;
     80     }
     81 
     82     /**
     83      * For testing only. See {@link #open()} and {@link #close()} for normal usage.
     84      * @wbp.parser.entryPoint
     85      */
     86     void openBlocking() {
     87         open();
     88         Display display = Display.getDefault();
     89         while (!mShell.isDisposed()) {
     90             if (!display.readAndDispatch()) {
     91                 display.sleep();
     92             }
     93         }
     94         close();
     95     }
     96 
     97     /**
     98      * Opens the window.
     99      * This call does not block and relies on the fact that the main window is
    100      * already running an SWT event dispatch loop.
    101      * Caller should use {@link #close()} later.
    102      */
    103     public void open() {
    104         createShell();
    105         createContents();
    106         mShell.open();
    107         mShell.layout();
    108         mShell.setVisible(false);
    109     }
    110 
    111     /**
    112      * Closes and <em>destroys</em> the window.
    113      * This must be called just before quitting the app.
    114      * <p/>
    115      * To simply hide/show the window, use {@link #setVisible(boolean)} instead.
    116      */
    117     public void close() {
    118         if (mShell != null && !mShell.isDisposed()) {
    119             mCloseRequested = true;
    120             mShell.close();
    121             mShell = null;
    122         }
    123     }
    124 
    125     /**
    126      * Determines whether the window is currently shown or not.
    127      *
    128      * @return True if the window is shown.
    129      */
    130     public boolean isVisible() {
    131         return mShell != null && !mShell.isDisposed() && mShell.isVisible();
    132     }
    133 
    134     /**
    135      * Toggles the window visibility.
    136      *
    137      * @param visible True to make the window visible, false to hide it.
    138      */
    139     public void setVisible(boolean visible) {
    140         if (mShell != null && !mShell.isDisposed()) {
    141             mShell.setVisible(visible);
    142             if (visible && mInitPosition) {
    143                 mInitPosition = false;
    144                 positionWindow();
    145             }
    146         }
    147     }
    148 
    149     private void createShell() {
    150         mShell = new Shell(mParentShell, SWT.SHELL_TRIM | SWT.TOOL);
    151         mShell.setMinimumSize(new Point(600, 300));
    152         mShell.setSize(450, 300);
    153         mShell.setText("Android SDK Manager Log");
    154         GridLayoutBuilder.create(mShell);
    155 
    156         mShell.addShellListener(new ShellAdapter() {
    157             @Override
    158             public void shellClosed(ShellEvent e) {
    159                 if (!mCloseRequested) {
    160                     e.doit = false;
    161                     setVisible(false);
    162                 }
    163             }
    164         });
    165     }
    166 
    167     /**
    168      * Create contents of the dialog.
    169      */
    170     private void createContents() {
    171         mRootComposite = new Composite(mShell, SWT.NONE);
    172         GridLayoutBuilder.create(mRootComposite).columns(2);
    173         GridDataBuilder.create(mRootComposite).fill().grab();
    174 
    175         mStyledText = new StyledText(mRootComposite,
    176                 SWT.BORDER | SWT.MULTI | SWT.READ_ONLY | SWT.WRAP | SWT.V_SCROLL);
    177         GridDataBuilder.create(mStyledText).hSpan(2).fill().grab();
    178 
    179         mLogDescription = new Label(mRootComposite, SWT.NONE);
    180         GridDataBuilder.create(mLogDescription).hFill().hGrab();
    181 
    182         mCloseButton = new Button(mRootComposite, SWT.NONE);
    183         mCloseButton.setText("Close");
    184         mCloseButton.setToolTipText("Closes the log window");
    185         mCloseButton.addSelectionListener(new SelectionAdapter() {
    186             @Override
    187             public void widgetSelected(SelectionEvent e) {
    188                 setVisible(false);  //$hide$
    189             }
    190         });
    191     }
    192 
    193     // --- Implementation of ILogUiProvider ---
    194 
    195 
    196     /**
    197      * Sets the description in the current task dialog.
    198      * This method can be invoked from a non-UI thread.
    199      */
    200     @Override
    201     public void setDescription(final String description) {
    202         syncExec(mLogDescription, new Runnable() {
    203             @Override
    204             public void run() {
    205                 mLogDescription.setText(description);
    206 
    207                 if (acceptLog(description, true /*isDescription*/)) {
    208                     appendLine(TextStyle.TITLE, description);
    209 
    210                     if (mSecondaryLog != null) {
    211                         mSecondaryLog.info("%1$s", description);  //$NON-NLS-1$
    212                     }
    213                 }
    214             }
    215         });
    216     }
    217 
    218     /**
    219      * Logs a "normal" information line.
    220      * This method can be invoked from a non-UI thread.
    221      */
    222     @Override
    223     public void log(final String log) {
    224         if (acceptLog(log, false /*isDescription*/)) {
    225             syncExec(mLogDescription, new Runnable() {
    226                 @Override
    227                 public void run() {
    228                     appendLine(TextStyle.DEFAULT, log);
    229                 }
    230             });
    231 
    232             if (mSecondaryLog != null) {
    233                 mSecondaryLog.info("  %1$s", log);                //$NON-NLS-1$
    234             }
    235         }
    236     }
    237 
    238     /**
    239      * Logs an "error" information line.
    240      * This method can be invoked from a non-UI thread.
    241      */
    242     @Override
    243     public void logError(final String log) {
    244         if (acceptLog(log, false /*isDescription*/)) {
    245             syncExec(mLogDescription, new Runnable() {
    246                 @Override
    247                 public void run() {
    248                     appendLine(TextStyle.ERROR, log);
    249                 }
    250             });
    251 
    252             if (mSecondaryLog != null) {
    253                 mSecondaryLog.error(null, "%1$s", log);             //$NON-NLS-1$
    254             }
    255         }
    256     }
    257 
    258     /**
    259      * Logs a "verbose" information line, that is extra details which are typically
    260      * not that useful for the end-user and might be hidden until explicitly shown.
    261      * This method can be invoked from a non-UI thread.
    262      */
    263     @Override
    264     public void logVerbose(final String log) {
    265         if (acceptLog(log, false /*isDescription*/)) {
    266             syncExec(mLogDescription, new Runnable() {
    267                 @Override
    268                 public void run() {
    269                     appendLine(TextStyle.DEFAULT, "  " + log);      //$NON-NLS-1$
    270                 }
    271             });
    272 
    273             if (mSecondaryLog != null) {
    274                 mSecondaryLog.info("    %1$s", log);              //$NON-NLS-1$
    275             }
    276         }
    277     }
    278 
    279 
    280     // ----
    281 
    282 
    283     /**
    284      * Centers the dialog in its parent shell.
    285      */
    286     private void positionWindow() {
    287         // Centers the dialog in its parent shell
    288         Shell child = mShell;
    289         if (child != null && mParentShell != null) {
    290             // get the parent client area with a location relative to the display
    291             Rectangle parentArea = mParentShell.getClientArea();
    292             Point parentLoc = mParentShell.getLocation();
    293             int px = parentLoc.x;
    294             int py = parentLoc.y;
    295             int pw = parentArea.width;
    296             int ph = parentArea.height;
    297 
    298             Point childSize = child.getSize();
    299             int cw = Math.max(childSize.x, pw);
    300             int ch = childSize.y;
    301 
    302             int x = 30 + px + (pw - cw) / 2;
    303             if (x < 0) x = 0;
    304 
    305             int y = py + (ph - ch) / 2;
    306             if (y < py) y = py;
    307 
    308             child.setLocation(x, y);
    309             child.setSize(cw, ch);
    310         }
    311     }
    312 
    313     private void appendLine(TextStyle style, String text) {
    314         if (!text.endsWith("\n")) {                                 //$NON-NLS-1$
    315             text += '\n';
    316         }
    317 
    318         int start = mStyledText.getCharCount();
    319 
    320         if (style == TextStyle.DEFAULT) {
    321             mStyledText.append(text);
    322 
    323         } else {
    324             mStyledText.append(text);
    325 
    326             StyleRange sr = new StyleRange();
    327             sr.start = start;
    328             sr.length = text.length();
    329             sr.fontStyle = SWT.BOLD;
    330             if (style == TextStyle.ERROR) {
    331                 sr.foreground = mStyledText.getDisplay().getSystemColor(SWT.COLOR_DARK_RED);
    332             }
    333             sr.underline = false;
    334             mStyledText.setStyleRange(sr);
    335         }
    336 
    337         // Scroll caret if it was already at the end before we added new text.
    338         // Ideally we would scroll if the scrollbar is at the bottom but we don't
    339         // have direct access to the scrollbar without overriding the SWT impl.
    340         if (mStyledText.getCaretOffset() >= start) {
    341             mStyledText.setSelection(mStyledText.getCharCount());
    342         }
    343     }
    344 
    345 
    346     private void syncExec(final Widget widget, final Runnable runnable) {
    347         if (widget != null && !widget.isDisposed()) {
    348             widget.getDisplay().syncExec(runnable);
    349         }
    350     }
    351 
    352     /**
    353      * Filter messages displayed in the log: <br/>
    354      * - Messages with a % are typical part of a progress update and shouldn't be in the log. <br/>
    355      * - Messages that are the same as the same output message should be output a second time.
    356      *
    357      * @param msg The potential log line to print.
    358      * @return True if the log line should be printed, false otherwise.
    359      */
    360     private boolean acceptLog(String msg, boolean isDescription) {
    361         if (msg == null) {
    362             return false;
    363         }
    364 
    365         msg = msg.trim();
    366 
    367         // Descriptions also have the download progress status (0..100%) which we want to avoid
    368         if (isDescription && msg.indexOf('%') != -1) {
    369             return false;
    370         }
    371 
    372         if (msg.equals(mLastLogMsg)) {
    373             return false;
    374         }
    375 
    376         mLastLogMsg = msg;
    377         return true;
    378     }
    379 }
    380