• 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.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