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