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