Home | History | Annotate | Download | only in sdkman2
      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.sdkman2;
     18 
     19 import com.android.sdklib.ISdkLog;
     20 import com.android.sdkuilib.internal.tasks.ILogUiProvider;
     21 import com.android.sdkuilib.ui.GridDataBuilder;
     22 import com.android.sdkuilib.ui.GridLayoutBuilder;
     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 ISdkLog 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, ISdkLog 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     public void setDescription(final String description) {
    201         syncExec(mLogDescription, new Runnable() {
    202             public void run() {
    203                 mLogDescription.setText(description);
    204 
    205                 if (acceptLog(description, true /*isDescription*/)) {
    206                     appendLine(TextStyle.TITLE, description);
    207 
    208                     if (mSecondaryLog != null) {
    209                         mSecondaryLog.printf("%1$s", description);  //$NON-NLS-1$
    210                     }
    211                 }
    212             }
    213         });
    214     }
    215 
    216     /**
    217      * Logs a "normal" information line.
    218      * This method can be invoked from a non-UI thread.
    219      */
    220     public void log(final String log) {
    221         if (acceptLog(log, false /*isDescription*/)) {
    222             syncExec(mLogDescription, new Runnable() {
    223                 public void run() {
    224                     appendLine(TextStyle.DEFAULT, log);
    225                 }
    226             });
    227 
    228             if (mSecondaryLog != null) {
    229                 mSecondaryLog.printf("  %1$s", log);                //$NON-NLS-1$
    230             }
    231         }
    232     }
    233 
    234     /**
    235      * Logs an "error" information line.
    236      * This method can be invoked from a non-UI thread.
    237      */
    238     public void logError(final String log) {
    239         if (acceptLog(log, false /*isDescription*/)) {
    240             syncExec(mLogDescription, new Runnable() {
    241                 public void run() {
    242                     appendLine(TextStyle.ERROR, log);
    243                 }
    244             });
    245 
    246             if (mSecondaryLog != null) {
    247                 mSecondaryLog.printf("ERROR: %1$s", log);           //$NON-NLS-1$
    248             }
    249         }
    250     }
    251 
    252     /**
    253      * Logs a "verbose" information line, that is extra details which are typically
    254      * not that useful for the end-user and might be hidden until explicitly shown.
    255      * This method can be invoked from a non-UI thread.
    256      */
    257     public void logVerbose(final String log) {
    258         if (acceptLog(log, false /*isDescription*/)) {
    259             syncExec(mLogDescription, new Runnable() {
    260                 public void run() {
    261                     appendLine(TextStyle.DEFAULT, "  " + log);      //$NON-NLS-1$
    262                 }
    263             });
    264 
    265             if (mSecondaryLog != null) {
    266                 mSecondaryLog.printf("    %1$s", log);              //$NON-NLS-1$
    267             }
    268         }
    269     }
    270 
    271 
    272     // ----
    273 
    274 
    275     /**
    276      * Centers the dialog in its parent shell.
    277      */
    278     private void positionWindow() {
    279         // Centers the dialog in its parent shell
    280         Shell child = mShell;
    281         if (child != null && mParentShell != null) {
    282             // get the parent client area with a location relative to the display
    283             Rectangle parentArea = mParentShell.getClientArea();
    284             Point parentLoc = mParentShell.getLocation();
    285             int px = parentLoc.x;
    286             int py = parentLoc.y;
    287             int pw = parentArea.width;
    288             int ph = parentArea.height;
    289 
    290             Point childSize = child.getSize();
    291             int cw = Math.max(childSize.x, pw);
    292             int ch = childSize.y;
    293 
    294             int x = 30 + px + (pw - cw) / 2;
    295             if (x < 0) x = 0;
    296 
    297             int y = py + (ph - ch) / 2;
    298             if (y < py) y = py;
    299 
    300             child.setLocation(x, y);
    301             child.setSize(cw, ch);
    302         }
    303     }
    304 
    305     private void appendLine(TextStyle style, String text) {
    306         if (!text.endsWith("\n")) {                                 //$NON-NLS-1$
    307             text += '\n';
    308         }
    309 
    310         int start = mStyledText.getCharCount();
    311 
    312         if (style == TextStyle.DEFAULT) {
    313             mStyledText.append(text);
    314 
    315         } else {
    316             mStyledText.append(text);
    317 
    318             StyleRange sr = new StyleRange();
    319             sr.start = start;
    320             sr.length = text.length();
    321             sr.fontStyle = SWT.BOLD;
    322             if (style == TextStyle.ERROR) {
    323                 sr.foreground = mStyledText.getDisplay().getSystemColor(SWT.COLOR_DARK_RED);
    324             }
    325             sr.underline = false;
    326             mStyledText.setStyleRange(sr);
    327         }
    328 
    329         // Scroll caret if it was already at the end before we added new text.
    330         // Ideally we would scroll if the scrollbar is at the bottom but we don't
    331         // have direct access to the scrollbar without overriding the SWT impl.
    332         if (mStyledText.getCaretOffset() >= start) {
    333             mStyledText.setSelection(mStyledText.getCharCount());
    334         }
    335     }
    336 
    337 
    338     private void syncExec(final Widget widget, final Runnable runnable) {
    339         if (widget != null && !widget.isDisposed()) {
    340             widget.getDisplay().syncExec(runnable);
    341         }
    342     }
    343 
    344     /**
    345      * Filter messages displayed in the log: <br/>
    346      * - Messages with a % are typical part of a progress update and shouldn't be in the log. <br/>
    347      * - Messages that are the same as the same output message should be output a second time.
    348      *
    349      * @param msg The potential log line to print.
    350      * @return True if the log line should be printed, false otherwise.
    351      */
    352     private boolean acceptLog(String msg, boolean isDescription) {
    353         if (msg == null) {
    354             return false;
    355         }
    356 
    357         msg = msg.trim();
    358 
    359         // Descriptions also have the download progress status (0..100%) which we want to avoid
    360         if (isDescription && msg.indexOf('%') != -1) {
    361             return false;
    362         }
    363 
    364         if (msg.equals(mLastLogMsg)) {
    365             return false;
    366         }
    367 
    368         mLastLogMsg = msg;
    369         return true;
    370     }
    371 }
    372