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