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.ddmuilib.logcat; 18 19 import com.android.ddmlib.DdmConstants; 20 import com.android.ddmlib.IDevice; 21 import com.android.ddmlib.Log.LogLevel; 22 import com.android.ddmuilib.ITableFocusListener; 23 import com.android.ddmuilib.ITableFocusListener.IFocusedTableActivator; 24 import com.android.ddmuilib.ImageLoader; 25 import com.android.ddmuilib.SelectionDependentPanel; 26 import com.android.ddmuilib.TableHelper; 27 28 import org.eclipse.jface.dialogs.MessageDialog; 29 import org.eclipse.jface.preference.IPreferenceStore; 30 import org.eclipse.jface.preference.PreferenceConverter; 31 import org.eclipse.jface.util.IPropertyChangeListener; 32 import org.eclipse.jface.util.PropertyChangeEvent; 33 import org.eclipse.jface.viewers.ColumnViewer; 34 import org.eclipse.jface.viewers.ColumnViewerToolTipSupport; 35 import org.eclipse.jface.viewers.TableViewer; 36 import org.eclipse.jface.viewers.TableViewerColumn; 37 import org.eclipse.jface.viewers.ViewerCell; 38 import org.eclipse.jface.window.ToolTip; 39 import org.eclipse.jface.window.Window; 40 import org.eclipse.swt.SWT; 41 import org.eclipse.swt.custom.SashForm; 42 import org.eclipse.swt.dnd.Clipboard; 43 import org.eclipse.swt.dnd.TextTransfer; 44 import org.eclipse.swt.dnd.Transfer; 45 import org.eclipse.swt.events.ControlAdapter; 46 import org.eclipse.swt.events.ControlEvent; 47 import org.eclipse.swt.events.FocusEvent; 48 import org.eclipse.swt.events.FocusListener; 49 import org.eclipse.swt.events.ModifyEvent; 50 import org.eclipse.swt.events.ModifyListener; 51 import org.eclipse.swt.events.SelectionAdapter; 52 import org.eclipse.swt.events.SelectionEvent; 53 import org.eclipse.swt.graphics.Font; 54 import org.eclipse.swt.graphics.FontData; 55 import org.eclipse.swt.graphics.GC; 56 import org.eclipse.swt.layout.GridData; 57 import org.eclipse.swt.layout.GridLayout; 58 import org.eclipse.swt.widgets.Combo; 59 import org.eclipse.swt.widgets.Composite; 60 import org.eclipse.swt.widgets.Control; 61 import org.eclipse.swt.widgets.Display; 62 import org.eclipse.swt.widgets.Event; 63 import org.eclipse.swt.widgets.FileDialog; 64 import org.eclipse.swt.widgets.Label; 65 import org.eclipse.swt.widgets.Listener; 66 import org.eclipse.swt.widgets.ScrollBar; 67 import org.eclipse.swt.widgets.Table; 68 import org.eclipse.swt.widgets.TableColumn; 69 import org.eclipse.swt.widgets.TableItem; 70 import org.eclipse.swt.widgets.Text; 71 import org.eclipse.swt.widgets.ToolBar; 72 import org.eclipse.swt.widgets.ToolItem; 73 74 import java.io.BufferedWriter; 75 import java.io.FileWriter; 76 import java.io.IOException; 77 import java.util.ArrayList; 78 import java.util.Arrays; 79 import java.util.Collections; 80 import java.util.List; 81 82 /** 83 * LogCatPanel displays a table listing the logcat messages. 84 */ 85 public final class LogCatPanel extends SelectionDependentPanel 86 implements ILogCatMessageEventListener { 87 /** Preference key to use for storing list of logcat filters. */ 88 public static final String LOGCAT_FILTERS_LIST = "logcat.view.filters.list"; 89 90 /** Preference key to use for storing font settings. */ 91 public static final String LOGCAT_VIEW_FONT_PREFKEY = "logcat.view.font"; 92 93 // Use a monospace font family 94 private static final String FONT_FAMILY = 95 DdmConstants.CURRENT_PLATFORM == DdmConstants.PLATFORM_DARWIN ? "Monaco":"Courier New"; 96 97 // Use the default system font size 98 private static final FontData DEFAULT_LOGCAT_FONTDATA; 99 static { 100 int h = Display.getDefault().getSystemFont().getFontData()[0].getHeight(); 101 DEFAULT_LOGCAT_FONTDATA = new FontData(FONT_FAMILY, h, SWT.NORMAL); 102 } 103 104 private static final String LOGCAT_VIEW_COLSIZE_PREFKEY_PREFIX = "logcat.view.colsize."; 105 private static final String DISPLAY_FILTERS_COLUMN_PREFKEY = "logcat.view.display.filters"; 106 107 /** Default message to show in the message search field. */ 108 private static final String DEFAULT_SEARCH_MESSAGE = 109 "Search for messages. Accepts Java regexes. " 110 + "Prefix with pid:, app:, tag: or text: to limit scope."; 111 112 /** Tooltip to show in the message search field. */ 113 private static final String DEFAULT_SEARCH_TOOLTIP = 114 "Example search patterns:\n" 115 + " sqlite (search for sqlite in text field)\n" 116 + " app:browser (search for messages generated by the browser application)"; 117 118 private static final String IMAGE_ADD_FILTER = "add.png"; //$NON-NLS-1$ 119 private static final String IMAGE_DELETE_FILTER = "delete.png"; //$NON-NLS-1$ 120 private static final String IMAGE_EDIT_FILTER = "edit.png"; //$NON-NLS-1$ 121 private static final String IMAGE_SAVE_LOG_TO_FILE = "save.png"; //$NON-NLS-1$ 122 private static final String IMAGE_CLEAR_LOG = "clear.png"; //$NON-NLS-1$ 123 private static final String IMAGE_DISPLAY_FILTERS = "displayfilters.png"; //$NON-NLS-1$ 124 private static final String IMAGE_PAUSE_LOGCAT = "pause_logcat.png"; //$NON-NLS-1$ 125 126 private static final int[] WEIGHTS_SHOW_FILTERS = new int[] {15, 85}; 127 private static final int[] WEIGHTS_LOGCAT_ONLY = new int[] {0, 100}; 128 129 private LogCatReceiver mReceiver; 130 private IPreferenceStore mPrefStore; 131 132 private List<LogCatFilter> mLogCatFilters; 133 private int mCurrentSelectedFilterIndex; 134 135 private int mRemovedEntriesCount = 0; 136 private int mPreviousRemainingCapacity = 0; 137 138 private ToolItem mNewFilterToolItem; 139 private ToolItem mDeleteFilterToolItem; 140 private ToolItem mEditFilterToolItem; 141 private TableViewer mFiltersTableViewer; 142 143 private Combo mLiveFilterLevelCombo; 144 private Text mLiveFilterText; 145 146 private TableViewer mViewer; 147 148 private boolean mShouldScrollToLatestLog = true; 149 private ToolItem mPauseLogcatCheckBox; 150 private boolean mLastItemPainted = false; 151 152 private String mLogFileExportFolder; 153 private LogCatMessageLabelProvider mLogCatMessageLabelProvider; 154 155 private SashForm mSash; 156 157 /** 158 * Construct a logcat panel. 159 * @param prefStore preference store where UI preferences will be saved 160 */ LogCatPanel(IPreferenceStore prefStore)161 public LogCatPanel(IPreferenceStore prefStore) { 162 mPrefStore = prefStore; 163 164 initializeFilters(); 165 166 setupDefaultPreferences(); 167 initializePreferenceUpdateListeners(); 168 } 169 initializeFilters()170 private void initializeFilters() { 171 mLogCatFilters = new ArrayList<LogCatFilter>(); 172 173 /* add default filter matching all messages */ 174 String tag = ""; 175 String text = ""; 176 String pid = ""; 177 String app = ""; 178 mLogCatFilters.add(new LogCatFilter("All messages (no filters)", 179 tag, text, pid, app, LogLevel.VERBOSE)); 180 181 /* restore saved filters from prefStore */ 182 List<LogCatFilter> savedFilters = getSavedFilters(); 183 mLogCatFilters.addAll(savedFilters); 184 } 185 setupDefaultPreferences()186 private void setupDefaultPreferences() { 187 PreferenceConverter.setDefault(mPrefStore, LogCatPanel.LOGCAT_VIEW_FONT_PREFKEY, 188 DEFAULT_LOGCAT_FONTDATA); 189 mPrefStore.setDefault(LogCatMessageList.MAX_MESSAGES_PREFKEY, 190 LogCatMessageList.MAX_MESSAGES_DEFAULT); 191 mPrefStore.setDefault(DISPLAY_FILTERS_COLUMN_PREFKEY, true); 192 } 193 initializePreferenceUpdateListeners()194 private void initializePreferenceUpdateListeners() { 195 mPrefStore.addPropertyChangeListener(new IPropertyChangeListener() { 196 @Override 197 public void propertyChange(PropertyChangeEvent event) { 198 String changedProperty = event.getProperty(); 199 200 if (changedProperty.equals(LogCatPanel.LOGCAT_VIEW_FONT_PREFKEY)) { 201 mLogCatMessageLabelProvider.setFont(getFontFromPrefStore()); 202 refreshLogCatTable(); 203 } else if (changedProperty.equals( 204 LogCatMessageList.MAX_MESSAGES_PREFKEY)) { 205 mReceiver.resizeFifo(mPrefStore.getInt( 206 LogCatMessageList.MAX_MESSAGES_PREFKEY)); 207 refreshLogCatTable(); 208 } 209 } 210 }); 211 } 212 saveFilterPreferences()213 private void saveFilterPreferences() { 214 LogCatFilterSettingsSerializer serializer = new LogCatFilterSettingsSerializer(); 215 216 /* save all filter settings except the first one which is the default */ 217 String e = serializer.encodeToPreferenceString( 218 mLogCatFilters.subList(1, mLogCatFilters.size())); 219 mPrefStore.setValue(LOGCAT_FILTERS_LIST, e); 220 } 221 getSavedFilters()222 private List<LogCatFilter> getSavedFilters() { 223 LogCatFilterSettingsSerializer serializer = new LogCatFilterSettingsSerializer(); 224 String e = mPrefStore.getString(LOGCAT_FILTERS_LIST); 225 return serializer.decodeFromPreferenceString(e); 226 } 227 228 @Override deviceSelected()229 public void deviceSelected() { 230 IDevice device = getCurrentDevice(); 231 if (device == null) { 232 // If the device is not working properly, getCurrentDevice() could return null. 233 // In such a case, we don't launch logcat, nor switch the display. 234 return; 235 } 236 237 if (mReceiver != null) { 238 // Don't need to listen to new logcat messages from previous device anymore. 239 mReceiver.removeMessageReceivedEventListener(this); 240 241 // When switching between devices, existing filter match count should be reset. 242 for (LogCatFilter f : mLogCatFilters) { 243 f.resetUnreadCount(); 244 } 245 } 246 247 mReceiver = LogCatReceiverFactory.INSTANCE.newReceiver(device, mPrefStore); 248 mReceiver.addMessageReceivedEventListener(this); 249 mViewer.setInput(mReceiver.getMessages()); 250 251 // Always scroll to last line whenever the selected device changes. 252 // Run this in a separate async thread to give the table some time to update after the 253 // setInput above. 254 Display.getDefault().asyncExec(new Runnable() { 255 @Override 256 public void run() { 257 scrollToLatestLog(); 258 } 259 }); 260 } 261 262 @Override clientSelected()263 public void clientSelected() { 264 } 265 266 @Override postCreation()267 protected void postCreation() { 268 } 269 270 @Override createControl(Composite parent)271 protected Control createControl(Composite parent) { 272 GridLayout layout = new GridLayout(1, false); 273 parent.setLayout(layout); 274 275 createViews(parent); 276 setupDefaults(); 277 278 return null; 279 } 280 createViews(Composite parent)281 private void createViews(Composite parent) { 282 mSash = createSash(parent); 283 284 createListOfFilters(mSash); 285 createLogTableView(mSash); 286 287 boolean showFilters = mPrefStore.getBoolean(DISPLAY_FILTERS_COLUMN_PREFKEY); 288 updateFiltersColumn(showFilters); 289 } 290 createSash(Composite parent)291 private SashForm createSash(Composite parent) { 292 SashForm sash = new SashForm(parent, SWT.HORIZONTAL); 293 sash.setLayoutData(new GridData(GridData.FILL_BOTH)); 294 return sash; 295 } 296 createListOfFilters(SashForm sash)297 private void createListOfFilters(SashForm sash) { 298 Composite c = new Composite(sash, SWT.BORDER); 299 GridLayout layout = new GridLayout(2, false); 300 c.setLayout(layout); 301 c.setLayoutData(new GridData(GridData.FILL_BOTH)); 302 303 createFiltersToolbar(c); 304 createFiltersTable(c); 305 } 306 createFiltersToolbar(Composite parent)307 private void createFiltersToolbar(Composite parent) { 308 Label l = new Label(parent, SWT.NONE); 309 l.setText("Saved Filters"); 310 GridData gd = new GridData(); 311 gd.horizontalAlignment = SWT.LEFT; 312 l.setLayoutData(gd); 313 314 ToolBar t = new ToolBar(parent, SWT.FLAT); 315 gd = new GridData(); 316 gd.horizontalAlignment = SWT.RIGHT; 317 t.setLayoutData(gd); 318 319 /* new filter */ 320 mNewFilterToolItem = new ToolItem(t, SWT.PUSH); 321 mNewFilterToolItem.setImage( 322 ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_ADD_FILTER, t.getDisplay())); 323 mNewFilterToolItem.setToolTipText("Add a new logcat filter"); 324 mNewFilterToolItem.addSelectionListener(new SelectionAdapter() { 325 @Override 326 public void widgetSelected(SelectionEvent arg0) { 327 addNewFilter(); 328 } 329 }); 330 331 /* delete filter */ 332 mDeleteFilterToolItem = new ToolItem(t, SWT.PUSH); 333 mDeleteFilterToolItem.setImage( 334 ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_DELETE_FILTER, t.getDisplay())); 335 mDeleteFilterToolItem.setToolTipText("Delete selected logcat filter"); 336 mDeleteFilterToolItem.addSelectionListener(new SelectionAdapter() { 337 @Override 338 public void widgetSelected(SelectionEvent arg0) { 339 deleteSelectedFilter(); 340 } 341 }); 342 343 /* edit filter */ 344 mEditFilterToolItem = new ToolItem(t, SWT.PUSH); 345 mEditFilterToolItem.setImage( 346 ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_EDIT_FILTER, t.getDisplay())); 347 mEditFilterToolItem.setToolTipText("Edit selected logcat filter"); 348 mEditFilterToolItem.addSelectionListener(new SelectionAdapter() { 349 @Override 350 public void widgetSelected(SelectionEvent arg0) { 351 editSelectedFilter(); 352 } 353 }); 354 } 355 addNewFilter()356 private void addNewFilter() { 357 LogCatFilterSettingsDialog d = new LogCatFilterSettingsDialog( 358 Display.getCurrent().getActiveShell()); 359 if (d.open() != Window.OK) { 360 return; 361 } 362 363 LogCatFilter f = new LogCatFilter(d.getFilterName().trim(), 364 d.getTag().trim(), 365 d.getText().trim(), 366 d.getPid().trim(), 367 d.getAppName().trim(), 368 LogLevel.getByString(d.getLogLevel())); 369 370 mLogCatFilters.add(f); 371 mFiltersTableViewer.refresh(); 372 373 /* select the newly added entry */ 374 int idx = mLogCatFilters.size() - 1; 375 mFiltersTableViewer.getTable().setSelection(idx); 376 377 filterSelectionChanged(); 378 saveFilterPreferences(); 379 } 380 deleteSelectedFilter()381 private void deleteSelectedFilter() { 382 int selectedIndex = mFiltersTableViewer.getTable().getSelectionIndex(); 383 if (selectedIndex <= 0) { 384 /* return if no selected filter, or the default filter was selected (0th). */ 385 return; 386 } 387 388 mLogCatFilters.remove(selectedIndex); 389 mFiltersTableViewer.refresh(); 390 mFiltersTableViewer.getTable().setSelection(selectedIndex - 1); 391 392 filterSelectionChanged(); 393 saveFilterPreferences(); 394 } 395 editSelectedFilter()396 private void editSelectedFilter() { 397 int selectedIndex = mFiltersTableViewer.getTable().getSelectionIndex(); 398 if (selectedIndex < 0) { 399 return; 400 } 401 402 LogCatFilter curFilter = mLogCatFilters.get(selectedIndex); 403 404 LogCatFilterSettingsDialog dialog = new LogCatFilterSettingsDialog( 405 Display.getCurrent().getActiveShell()); 406 dialog.setDefaults(curFilter.getName(), curFilter.getTag(), curFilter.getText(), 407 curFilter.getPid(), curFilter.getAppName(), curFilter.getLogLevel()); 408 if (dialog.open() != Window.OK) { 409 return; 410 } 411 412 LogCatFilter f = new LogCatFilter(dialog.getFilterName(), 413 dialog.getTag(), 414 dialog.getText(), 415 dialog.getPid(), 416 dialog.getAppName(), 417 LogLevel.getByString(dialog.getLogLevel())); 418 mLogCatFilters.set(selectedIndex, f); 419 mFiltersTableViewer.refresh(); 420 421 mFiltersTableViewer.getTable().setSelection(selectedIndex); 422 filterSelectionChanged(); 423 saveFilterPreferences(); 424 } 425 426 /** 427 * Select the transient filter for the specified application. If no such filter 428 * exists, then create one and then select that. This method should be called from 429 * the UI thread. 430 * @param appName application name to filter by 431 */ selectTransientAppFilter(String appName)432 public void selectTransientAppFilter(String appName) { 433 assert mViewer.getTable().getDisplay().getThread() == Thread.currentThread(); 434 435 LogCatFilter f = findTransientAppFilter(appName); 436 if (f == null) { 437 f = createTransientAppFilter(appName); 438 mLogCatFilters.add(f); 439 } 440 441 selectFilterAt(mLogCatFilters.indexOf(f)); 442 } 443 findTransientAppFilter(String appName)444 private LogCatFilter findTransientAppFilter(String appName) { 445 for (LogCatFilter f : mLogCatFilters) { 446 if (f.isTransient() && f.getAppName().equals(appName)) { 447 return f; 448 } 449 } 450 return null; 451 } 452 createTransientAppFilter(String appName)453 private LogCatFilter createTransientAppFilter(String appName) { 454 LogCatFilter f = new LogCatFilter(appName + " (Session Filter)", 455 "", 456 "", 457 "", 458 appName, 459 LogLevel.VERBOSE); 460 f.setTransient(); 461 return f; 462 } 463 selectFilterAt(final int index)464 private void selectFilterAt(final int index) { 465 mFiltersTableViewer.refresh(); 466 mFiltersTableViewer.getTable().setSelection(index); 467 filterSelectionChanged(); 468 } 469 createFiltersTable(Composite parent)470 private void createFiltersTable(Composite parent) { 471 final Table table = new Table(parent, SWT.FULL_SELECTION); 472 473 GridData gd = new GridData(GridData.FILL_BOTH); 474 gd.horizontalSpan = 2; 475 table.setLayoutData(gd); 476 477 mFiltersTableViewer = new TableViewer(table); 478 mFiltersTableViewer.setContentProvider(new LogCatFilterContentProvider()); 479 mFiltersTableViewer.setLabelProvider(new LogCatFilterLabelProvider()); 480 mFiltersTableViewer.setInput(mLogCatFilters); 481 482 mFiltersTableViewer.getTable().addSelectionListener(new SelectionAdapter() { 483 @Override 484 public void widgetSelected(SelectionEvent event) { 485 filterSelectionChanged(); 486 } 487 488 @Override 489 public void widgetDefaultSelected(SelectionEvent arg0) { 490 editSelectedFilter(); 491 } 492 }); 493 } 494 createLogTableView(SashForm sash)495 private void createLogTableView(SashForm sash) { 496 Composite c = new Composite(sash, SWT.NONE); 497 c.setLayout(new GridLayout()); 498 c.setLayoutData(new GridData(GridData.FILL_BOTH)); 499 500 createLiveFilters(c); 501 createLogcatViewTable(c); 502 } 503 504 /** 505 * Create the search bar at the top of the logcat messages table. 506 * FIXME: Currently, this feature is incomplete: The UI elements are created, but they 507 * are all set to disabled state. 508 */ createLiveFilters(Composite parent)509 private void createLiveFilters(Composite parent) { 510 Composite c = new Composite(parent, SWT.NONE); 511 c.setLayout(new GridLayout(3, false)); 512 c.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 513 514 mLiveFilterText = new Text(c, SWT.BORDER | SWT.SEARCH); 515 mLiveFilterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 516 mLiveFilterText.setMessage(DEFAULT_SEARCH_MESSAGE); 517 mLiveFilterText.setToolTipText(DEFAULT_SEARCH_TOOLTIP); 518 mLiveFilterText.addModifyListener(new ModifyListener() { 519 @Override 520 public void modifyText(ModifyEvent arg0) { 521 updateAppliedFilters(); 522 } 523 }); 524 525 mLiveFilterLevelCombo = new Combo(c, SWT.READ_ONLY | SWT.DROP_DOWN); 526 mLiveFilterLevelCombo.setItems( 527 LogCatFilterSettingsDialog.getLogLevels().toArray(new String[0])); 528 mLiveFilterLevelCombo.select(0); 529 mLiveFilterLevelCombo.addSelectionListener(new SelectionAdapter() { 530 @Override 531 public void widgetSelected(SelectionEvent arg0) { 532 updateAppliedFilters(); 533 } 534 }); 535 536 ToolBar toolBar = new ToolBar(c, SWT.FLAT); 537 538 ToolItem saveToLog = new ToolItem(toolBar, SWT.PUSH); 539 saveToLog.setImage(ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_SAVE_LOG_TO_FILE, 540 toolBar.getDisplay())); 541 saveToLog.setToolTipText("Export Selected Items To Text File.."); 542 saveToLog.addSelectionListener(new SelectionAdapter() { 543 @Override 544 public void widgetSelected(SelectionEvent arg0) { 545 saveLogToFile(); 546 } 547 }); 548 549 ToolItem clearLog = new ToolItem(toolBar, SWT.PUSH); 550 clearLog.setImage( 551 ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_CLEAR_LOG, toolBar.getDisplay())); 552 clearLog.setToolTipText("Clear Log"); 553 clearLog.addSelectionListener(new SelectionAdapter() { 554 @Override 555 public void widgetSelected(SelectionEvent arg0) { 556 if (mReceiver != null) { 557 mReceiver.clearMessages(); 558 refreshLogCatTable(); 559 560 mRemovedEntriesCount = 0; 561 562 // the filters view is not cleared unless the filters are re-applied. 563 updateAppliedFilters(); 564 } 565 } 566 }); 567 568 final ToolItem showFiltersColumn = new ToolItem(toolBar, SWT.CHECK); 569 showFiltersColumn.setImage( 570 ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_DISPLAY_FILTERS, 571 toolBar.getDisplay())); 572 showFiltersColumn.setSelection(mPrefStore.getBoolean(DISPLAY_FILTERS_COLUMN_PREFKEY)); 573 showFiltersColumn.setToolTipText("Display Saved Filters View"); 574 showFiltersColumn.addSelectionListener(new SelectionAdapter() { 575 @Override 576 public void widgetSelected(SelectionEvent event) { 577 boolean showFilters = showFiltersColumn.getSelection(); 578 mPrefStore.setValue(DISPLAY_FILTERS_COLUMN_PREFKEY, showFilters); 579 updateFiltersColumn(showFilters); 580 } 581 }); 582 583 mPauseLogcatCheckBox = new ToolItem(toolBar, SWT.CHECK); 584 mPauseLogcatCheckBox.setImage( 585 ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_PAUSE_LOGCAT, 586 toolBar.getDisplay())); 587 mPauseLogcatCheckBox.setSelection(false); 588 mPauseLogcatCheckBox.setToolTipText("Pause receiving new logcat messages."); 589 mPauseLogcatCheckBox.addSelectionListener(new SelectionAdapter() { 590 @Override 591 public void widgetSelected(SelectionEvent event) { 592 boolean pauseLogcat = mPauseLogcatCheckBox.getSelection(); 593 setScrollToLatestLog(!pauseLogcat, false); 594 } 595 }); 596 } 597 updateFiltersColumn(boolean showFilters)598 private void updateFiltersColumn(boolean showFilters) { 599 if (showFilters) { 600 mSash.setWeights(WEIGHTS_SHOW_FILTERS); 601 } else { 602 mSash.setWeights(WEIGHTS_LOGCAT_ONLY); 603 } 604 } 605 606 /** 607 * Save logcat messages selected in the table to a file. 608 */ saveLogToFile()609 private void saveLogToFile() { 610 /* show dialog box and get target file name */ 611 final String fName = getLogFileTargetLocation(); 612 if (fName == null) { 613 return; 614 } 615 616 /* obtain list of selected messages */ 617 final List<LogCatMessage> selectedMessages = getSelectedLogCatMessages(); 618 619 /* save messages to file in a different (non UI) thread */ 620 Thread t = new Thread(new Runnable() { 621 @Override 622 public void run() { 623 try { 624 BufferedWriter w = new BufferedWriter(new FileWriter(fName)); 625 for (LogCatMessage m : selectedMessages) { 626 w.append(m.toString()); 627 w.newLine(); 628 } 629 w.close(); 630 } catch (final IOException e) { 631 Display.getDefault().asyncExec(new Runnable() { 632 @Override 633 public void run() { 634 MessageDialog.openError(Display.getCurrent().getActiveShell(), 635 "Unable to export selection to file.", 636 "Unexpected error while saving selected messages to file: " 637 + e.getMessage()); 638 } 639 }); 640 } 641 } 642 }); 643 t.setName("Saving selected items to logfile.."); 644 t.start(); 645 } 646 647 /** 648 * Display a {@link FileDialog} to the user and obtain the location for the log file. 649 * @return path to target file, null if user canceled the dialog 650 */ getLogFileTargetLocation()651 private String getLogFileTargetLocation() { 652 FileDialog fd = new FileDialog(Display.getCurrent().getActiveShell(), SWT.SAVE); 653 654 fd.setText("Save Log.."); 655 fd.setFileName("log.txt"); 656 657 if (mLogFileExportFolder == null) { 658 mLogFileExportFolder = System.getProperty("user.home"); 659 } 660 fd.setFilterPath(mLogFileExportFolder); 661 662 fd.setFilterNames(new String[] { 663 "Text Files (*.txt)" 664 }); 665 fd.setFilterExtensions(new String[] { 666 "*.txt" 667 }); 668 669 String fName = fd.open(); 670 if (fName != null) { 671 mLogFileExportFolder = fd.getFilterPath(); /* save path to restore on future calls */ 672 } 673 674 return fName; 675 } 676 getSelectedLogCatMessages()677 private List<LogCatMessage> getSelectedLogCatMessages() { 678 Table table = mViewer.getTable(); 679 int[] indices = table.getSelectionIndices(); 680 Arrays.sort(indices); /* Table.getSelectionIndices() does not specify an order */ 681 682 // Get items from the table's input as opposed to getting each table item's data. 683 // Retrieving table item's data can return NULL in case of a virtual table if the item 684 // has not been displayed yet. 685 Object input = mViewer.getInput(); 686 if (!(input instanceof LogCatMessageList)) { 687 return Collections.emptyList(); 688 } 689 690 List<LogCatMessage> filteredItems = applyCurrentFilters((LogCatMessageList) input); 691 List<LogCatMessage> selectedMessages = new ArrayList<LogCatMessage>(indices.length); 692 for (int i : indices) { 693 // consider removed logcat message entries 694 i -= mRemovedEntriesCount; 695 if (i >= 0 && i < filteredItems.size()) { 696 LogCatMessage m = filteredItems.get(i); 697 selectedMessages.add(m); 698 } 699 } 700 701 return selectedMessages; 702 } 703 applyCurrentFilters(LogCatMessageList msgList)704 private List<LogCatMessage> applyCurrentFilters(LogCatMessageList msgList) { 705 Object[] items = msgList.toArray(); 706 List<LogCatMessage> filteredItems = new ArrayList<LogCatMessage>(items.length); 707 List<LogCatViewerFilter> filters = getFiltersToApply(); 708 709 for (Object item : items) { 710 if (!(item instanceof LogCatMessage)) { 711 continue; 712 } 713 714 LogCatMessage msg = (LogCatMessage) item; 715 if (!isMessageFiltered(msg, filters)) { 716 filteredItems.add(msg); 717 } 718 } 719 720 return filteredItems; 721 } 722 isMessageFiltered(LogCatMessage msg, List<LogCatViewerFilter> filters)723 private boolean isMessageFiltered(LogCatMessage msg, List<LogCatViewerFilter> filters) { 724 for (LogCatViewerFilter f : filters) { 725 if (!f.select(null, null, msg)) { 726 // message does not make it through this filter 727 return true; 728 } 729 } 730 731 return false; 732 } 733 createLogcatViewTable(Composite parent)734 private void createLogcatViewTable(Composite parent) { 735 // The SWT.VIRTUAL bit causes the table to be rendered faster. However it makes all rows 736 // to be of the same height, thereby clipping any rows with multiple lines of text. 737 // In such a case, users can view the full text by hovering over the item and looking at 738 // the tooltip. 739 final Table table = new Table(parent, SWT.FULL_SELECTION | SWT.MULTI | SWT.VIRTUAL); 740 mViewer = new TableViewer(table); 741 742 table.setLayoutData(new GridData(GridData.FILL_BOTH)); 743 table.getHorizontalBar().setVisible(true); 744 745 /** Columns to show in the table. */ 746 String[] properties = { 747 "Level", 748 "Time", 749 "PID", 750 "Application", 751 "Tag", 752 "Text", 753 }; 754 755 /** The sampleText for each column is used to determine the default widths 756 * for each column. The contents do not matter, only their lengths are needed. */ 757 String[] sampleText = { 758 " ", 759 " 00-00 00:00:00.0000 ", 760 " 0000", 761 " com.android.launcher", 762 " SampleTagText", 763 " Log Message field should be pretty long by default. As long as possible for correct display on Mac.", 764 }; 765 766 mLogCatMessageLabelProvider = new LogCatMessageLabelProvider(getFontFromPrefStore()); 767 for (int i = 0; i < properties.length; i++) { 768 TableColumn tc = TableHelper.createTableColumn(mViewer.getTable(), 769 properties[i], /* Column title */ 770 SWT.LEFT, /* Column Style */ 771 sampleText[i], /* String to compute default col width */ 772 getColPreferenceKey(properties[i]), /* Preference Store key for this column */ 773 mPrefStore); 774 TableViewerColumn tvc = new TableViewerColumn(mViewer, tc); 775 tvc.setLabelProvider(mLogCatMessageLabelProvider); 776 } 777 778 mViewer.getTable().setLinesVisible(true); /* zebra stripe the table */ 779 mViewer.getTable().setHeaderVisible(true); 780 mViewer.setContentProvider(new LogCatMessageContentProvider()); 781 WrappingToolTipSupport.enableFor(mViewer, ToolTip.NO_RECREATE); 782 783 // Set the row height to be sufficient enough to display the current font. 784 // This is not strictly necessary, except that on WinXP, the rows showed up clipped. So 785 // we explicitly set it to be sure. 786 mViewer.getTable().addListener(SWT.MeasureItem, new Listener() { 787 @Override 788 public void handleEvent(Event event) { 789 event.height = event.gc.getFontMetrics().getHeight(); 790 } 791 }); 792 793 // Update the label provider whenever the text column's width changes 794 TableColumn textColumn = mViewer.getTable().getColumn(properties.length - 1); 795 textColumn.addControlListener(new ControlAdapter() { 796 @Override 797 public void controlResized(ControlEvent event) { 798 TableColumn tc = (TableColumn) event.getSource(); 799 int width = tc.getWidth(); 800 GC gc = new GC(tc.getParent()); 801 int avgCharWidth = gc.getFontMetrics().getAverageCharWidth(); 802 gc.dispose(); 803 804 if (mLogCatMessageLabelProvider != null) { 805 mLogCatMessageLabelProvider.setMinimumLengthForToolTips(width/avgCharWidth); 806 } 807 } 808 }); 809 810 setupAutoScrollLockBehavior(); 811 initDoubleClickListener(); 812 } 813 814 /** 815 * Setup to automatically enable or disable scroll lock. From a user's perspective, 816 * the logcat window will: <ul> 817 * <li> Automatically scroll and reveal new entries if the scrollbar is at the bottom. </li> 818 * <li> Not scroll even when new messages are received if the scrollbar is not at the bottom. 819 * </li> 820 * </ul> 821 * This requires that we are able to detect where the scrollbar is and what direction 822 * it is moving. Unfortunately, that proves to be very platform dependent. Here's the behavior 823 * of the scroll events on different platforms: <ul> 824 * <li> On Windows, scroll bar events specify which direction the scrollbar is moving, but 825 * it is not possible to determine if the scrollbar is right at the end. </li> 826 * <li> On Mac/Cocoa, scroll bar events do not specify the direction of movement (it is always 827 * set to SWT.DRAG), and it is not possible to identify where the scrollbar is since 828 * small movements of the scrollbar are not reflected in sb.getSelection(). </li> 829 * <li> On Linux/gtk, we don't get the direction, but we can accurately locate the 830 * scrollbar location using getSelection(), getThumb() and getMaximum(). 831 * </ul> 832 */ setupAutoScrollLockBehavior()833 private void setupAutoScrollLockBehavior() { 834 if (DdmConstants.CURRENT_PLATFORM == DdmConstants.PLATFORM_WINDOWS) { 835 // On Windows, it is not possible to detect whether the scrollbar is at the 836 // bottom using the values of ScrollBar.getThumb, getSelection and getMaximum. 837 // Instead we resort to the following workaround: attach to the paint listener 838 // and see if the last item has been painted since the previous scroll event. 839 // If the last item has been painted, then we assume that we are at the bottom. 840 mViewer.getTable().addListener(SWT.PaintItem, new Listener() { 841 @Override 842 public void handleEvent(Event event) { 843 TableItem item = (TableItem) event.item; 844 TableItem[] items = mViewer.getTable().getItems(); 845 if (items.length > 0 && items[items.length - 1] == item) { 846 mLastItemPainted = true; 847 } 848 } 849 }); 850 mViewer.getTable().getVerticalBar().addSelectionListener(new SelectionAdapter() { 851 @Override 852 public void widgetSelected(SelectionEvent event) { 853 boolean scrollToLast; 854 if (event.detail == SWT.ARROW_UP || event.detail == SWT.PAGE_UP 855 || event.detail == SWT.HOME) { 856 // if we know that we are moving up, then do not scroll down 857 scrollToLast = false; 858 } else { 859 // otherwise, enable scrollToLast only if the last item was displayed 860 scrollToLast = mLastItemPainted; 861 } 862 863 setScrollToLatestLog(scrollToLast, true); 864 mLastItemPainted = false; 865 } 866 }); 867 } else if (DdmConstants.CURRENT_PLATFORM == DdmConstants.PLATFORM_LINUX) { 868 // On Linux/gtk, we do not get any details regarding the scroll event (up/down/etc). 869 // So we completely rely on whether the scrollbar is at the bottom or not. 870 mViewer.getTable().getVerticalBar().addSelectionListener(new SelectionAdapter() { 871 @Override 872 public void widgetSelected(SelectionEvent event) { 873 ScrollBar sb = (ScrollBar) event.getSource(); 874 boolean scrollToLast = sb.getSelection() + sb.getThumb() == sb.getMaximum(); 875 setScrollToLatestLog(scrollToLast, true); 876 } 877 }); 878 } else { 879 // On Mac, we do not get any details regarding the (trackball) scroll event, 880 // nor can we rely on getSelection() changing for small movements. As a result, we 881 // do not setup any auto scroll lock behavior. Mac users have to manually pause and 882 // unpause if they are looking at a particular item in a high volume stream of events. 883 } 884 } 885 setScrollToLatestLog(boolean scroll, boolean updateCheckbox)886 private void setScrollToLatestLog(boolean scroll, boolean updateCheckbox) { 887 mShouldScrollToLatestLog = scroll; 888 889 if (updateCheckbox) { 890 mPauseLogcatCheckBox.setSelection(!scroll); 891 } 892 893 if (scroll) { 894 mViewer.refresh(); 895 scrollToLatestLog(); 896 } 897 } 898 899 private static class WrappingToolTipSupport extends ColumnViewerToolTipSupport { WrappingToolTipSupport(ColumnViewer viewer, int style, boolean manualActivation)900 protected WrappingToolTipSupport(ColumnViewer viewer, int style, 901 boolean manualActivation) { 902 super(viewer, style, manualActivation); 903 } 904 905 @Override createViewerToolTipContentArea(Event event, ViewerCell cell, Composite parent)906 protected Composite createViewerToolTipContentArea(Event event, ViewerCell cell, 907 Composite parent) { 908 Composite comp = new Composite(parent, SWT.NONE); 909 GridLayout l = new GridLayout(1, false); 910 l.horizontalSpacing = 0; 911 l.marginWidth = 0; 912 l.marginHeight = 0; 913 l.verticalSpacing = 0; 914 comp.setLayout(l); 915 916 Text text = new Text(comp, SWT.BORDER | SWT.V_SCROLL | SWT.WRAP); 917 text.setEditable(false); 918 text.setText(cell.getElement().toString()); 919 text.setLayoutData(new GridData(500, 150)); 920 921 return comp; 922 } 923 924 @Override isHideOnMouseDown()925 public boolean isHideOnMouseDown() { 926 return false; 927 } 928 enableFor(ColumnViewer viewer, int style)929 public static final void enableFor(ColumnViewer viewer, int style) { 930 new WrappingToolTipSupport(viewer, style, false); 931 } 932 } 933 getColPreferenceKey(String field)934 private String getColPreferenceKey(String field) { 935 return LOGCAT_VIEW_COLSIZE_PREFKEY_PREFIX + field; 936 } 937 getFontFromPrefStore()938 private Font getFontFromPrefStore() { 939 FontData fd = PreferenceConverter.getFontData(mPrefStore, 940 LogCatPanel.LOGCAT_VIEW_FONT_PREFKEY); 941 return new Font(Display.getDefault(), fd); 942 } 943 setupDefaults()944 private void setupDefaults() { 945 int defaultFilterIndex = 0; 946 mFiltersTableViewer.getTable().setSelection(defaultFilterIndex); 947 948 filterSelectionChanged(); 949 } 950 951 /** 952 * Perform all necessary updates whenever a filter is selected (by user or programmatically). 953 */ filterSelectionChanged()954 private void filterSelectionChanged() { 955 int idx = getSelectedSavedFilterIndex(); 956 if (idx == -1) { 957 /* One of the filters should always be selected. 958 * On Linux, there is no way to deselect an item. 959 * On Mac, clicking inside the list view, but not an any item will result 960 * in all items being deselected. In such a case, we simply reselect the 961 * first entry. */ 962 idx = 0; 963 mFiltersTableViewer.getTable().setSelection(idx); 964 } 965 966 mCurrentSelectedFilterIndex = idx; 967 968 resetUnreadCountForSelectedFilter(); 969 updateFiltersToolBar(); 970 updateAppliedFilters(); 971 } 972 resetUnreadCountForSelectedFilter()973 private void resetUnreadCountForSelectedFilter() { 974 int index = getSelectedSavedFilterIndex(); 975 mLogCatFilters.get(index).resetUnreadCount(); 976 977 refreshFiltersTable(); 978 } 979 getSelectedSavedFilterIndex()980 private int getSelectedSavedFilterIndex() { 981 return mFiltersTableViewer.getTable().getSelectionIndex(); 982 } 983 updateFiltersToolBar()984 private void updateFiltersToolBar() { 985 /* The default filter at index 0 can neither be edited, nor removed. */ 986 boolean en = getSelectedSavedFilterIndex() != 0; 987 mEditFilterToolItem.setEnabled(en); 988 mDeleteFilterToolItem.setEnabled(en); 989 } 990 updateAppliedFilters()991 private void updateAppliedFilters() { 992 List<LogCatViewerFilter> filters = getFiltersToApply(); 993 mViewer.setFilters(filters.toArray(new LogCatViewerFilter[filters.size()])); 994 995 /* whenever filters are changed, the number of displayed logs changes 996 * drastically. Display the latest log in such a situation. */ 997 scrollToLatestLog(); 998 } 999 getFiltersToApply()1000 private List<LogCatViewerFilter> getFiltersToApply() { 1001 /* list of filters to apply = saved filter + live filters */ 1002 List<LogCatViewerFilter> filters = new ArrayList<LogCatViewerFilter>(); 1003 filters.add(getSelectedSavedFilter()); 1004 filters.addAll(getCurrentLiveFilters()); 1005 return filters; 1006 } 1007 getCurrentLiveFilters()1008 private List<LogCatViewerFilter> getCurrentLiveFilters() { 1009 List<LogCatViewerFilter> liveFilters = new ArrayList<LogCatViewerFilter>(); 1010 1011 List<LogCatFilter> liveFilterSettings = LogCatFilter.fromString( 1012 mLiveFilterText.getText(), /* current query */ 1013 LogLevel.getByString(mLiveFilterLevelCombo.getText())); /* current log level */ 1014 for (LogCatFilter s : liveFilterSettings) { 1015 liveFilters.add(new LogCatViewerFilter(s)); 1016 } 1017 1018 return liveFilters; 1019 } 1020 getSelectedSavedFilter()1021 private LogCatViewerFilter getSelectedSavedFilter() { 1022 int index = getSelectedSavedFilterIndex(); 1023 return new LogCatViewerFilter(mLogCatFilters.get(index)); 1024 } 1025 1026 1027 @Override setFocus()1028 public void setFocus() { 1029 } 1030 1031 /** 1032 * Update view whenever a message is received. 1033 * @param receivedMessages list of messages from logcat 1034 * Implements {@link ILogCatMessageEventListener#messageReceived()}. 1035 */ 1036 @Override messageReceived(List<LogCatMessage> receivedMessages)1037 public void messageReceived(List<LogCatMessage> receivedMessages) { 1038 refreshLogCatTable(); 1039 1040 if (mShouldScrollToLatestLog) { 1041 updateUnreadCount(receivedMessages); 1042 refreshFiltersTable(); 1043 } else { 1044 LogCatMessageList messageList = mReceiver.getMessages(); 1045 int remainingCapacity = messageList.remainingCapacity(); 1046 if (remainingCapacity == 0) { 1047 mRemovedEntriesCount += 1048 receivedMessages.size() - mPreviousRemainingCapacity; 1049 } 1050 mPreviousRemainingCapacity = remainingCapacity; 1051 } 1052 } 1053 1054 /** 1055 * When new messages are received, and they match a saved filter, update 1056 * the unread count associated with that filter. 1057 * @param receivedMessages list of new messages received 1058 */ updateUnreadCount(List<LogCatMessage> receivedMessages)1059 private void updateUnreadCount(List<LogCatMessage> receivedMessages) { 1060 for (int i = 0; i < mLogCatFilters.size(); i++) { 1061 if (i == mCurrentSelectedFilterIndex) { 1062 /* no need to update unread count for currently selected filter */ 1063 continue; 1064 } 1065 mLogCatFilters.get(i).updateUnreadCount(receivedMessages); 1066 } 1067 } 1068 refreshFiltersTable()1069 private void refreshFiltersTable() { 1070 Display.getDefault().asyncExec(new Runnable() { 1071 @Override 1072 public void run() { 1073 if (mFiltersTableViewer.getTable().isDisposed()) { 1074 return; 1075 } 1076 mFiltersTableViewer.refresh(); 1077 } 1078 }); 1079 } 1080 1081 /** Task currently submitted to {@link Display#asyncExec} to be run in UI thread. */ 1082 private LogCatTableRefresherTask mCurrentRefresher; 1083 1084 /** 1085 * Refresh the logcat table asynchronously from the UI thread. 1086 * This method adds a new async refresh only if there are no pending refreshes for the table. 1087 * Doing so eliminates redundant refresh threads from being queued up to be run on the 1088 * display thread. 1089 */ refreshLogCatTable()1090 private void refreshLogCatTable() { 1091 synchronized (this) { 1092 if (mCurrentRefresher == null && mShouldScrollToLatestLog) { 1093 mCurrentRefresher = new LogCatTableRefresherTask(); 1094 Display.getDefault().asyncExec(mCurrentRefresher); 1095 } 1096 } 1097 } 1098 1099 private class LogCatTableRefresherTask implements Runnable { 1100 @Override run()1101 public void run() { 1102 if (mViewer.getTable().isDisposed()) { 1103 return; 1104 } 1105 synchronized (LogCatPanel.this) { 1106 mCurrentRefresher = null; 1107 } 1108 1109 if (mShouldScrollToLatestLog) { 1110 mViewer.refresh(); 1111 scrollToLatestLog(); 1112 } 1113 } 1114 } 1115 1116 /** Scroll to the last line. */ scrollToLatestLog()1117 private void scrollToLatestLog() { 1118 mRemovedEntriesCount = 0; 1119 mViewer.getTable().setTopIndex(mViewer.getTable().getItemCount() - 1); 1120 } 1121 1122 private List<ILogCatMessageSelectionListener> mMessageSelectionListeners; 1123 initDoubleClickListener()1124 private void initDoubleClickListener() { 1125 mMessageSelectionListeners = new ArrayList<ILogCatMessageSelectionListener>(1); 1126 1127 mViewer.getTable().addSelectionListener(new SelectionAdapter() { 1128 @Override 1129 public void widgetDefaultSelected(SelectionEvent arg0) { 1130 List<LogCatMessage> selectedMessages = getSelectedLogCatMessages(); 1131 if (selectedMessages.size() == 0) { 1132 return; 1133 } 1134 1135 for (ILogCatMessageSelectionListener l : mMessageSelectionListeners) { 1136 l.messageDoubleClicked(selectedMessages.get(0)); 1137 } 1138 } 1139 }); 1140 } 1141 addLogCatMessageSelectionListener(ILogCatMessageSelectionListener l)1142 public void addLogCatMessageSelectionListener(ILogCatMessageSelectionListener l) { 1143 mMessageSelectionListeners.add(l); 1144 } 1145 1146 private ITableFocusListener mTableFocusListener; 1147 1148 /** 1149 * Specify the listener to be called when the logcat view gets focus. This interface is 1150 * required by DDMS to hook up the menu items for Copy and Select All. 1151 * @param listener listener to be notified when logcat view is in focus 1152 */ setTableFocusListener(ITableFocusListener listener)1153 public void setTableFocusListener(ITableFocusListener listener) { 1154 mTableFocusListener = listener; 1155 1156 final Table table = mViewer.getTable(); 1157 final IFocusedTableActivator activator = new IFocusedTableActivator() { 1158 @Override 1159 public void copy(Clipboard clipboard) { 1160 copySelectionToClipboard(clipboard); 1161 } 1162 1163 @Override 1164 public void selectAll() { 1165 table.selectAll(); 1166 } 1167 }; 1168 1169 table.addFocusListener(new FocusListener() { 1170 @Override 1171 public void focusGained(FocusEvent e) { 1172 mTableFocusListener.focusGained(activator); 1173 } 1174 1175 @Override 1176 public void focusLost(FocusEvent e) { 1177 mTableFocusListener.focusLost(activator); 1178 } 1179 }); 1180 } 1181 1182 /** Copy all selected messages to clipboard. */ copySelectionToClipboard(Clipboard clipboard)1183 public void copySelectionToClipboard(Clipboard clipboard) { 1184 StringBuilder sb = new StringBuilder(); 1185 1186 for (LogCatMessage m : getSelectedLogCatMessages()) { 1187 sb.append(m.toString()); 1188 sb.append('\n'); 1189 } 1190 1191 if (sb.length() > 0) { 1192 clipboard.setContents( 1193 new Object[] {sb.toString()}, 1194 new Transfer[] {TextTransfer.getInstance()} 1195 ); 1196 } 1197 } 1198 1199 /** Select all items in the logcat table. */ selectAll()1200 public void selectAll() { 1201 mViewer.getTable().selectAll(); 1202 } 1203 } 1204