• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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