/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.ddmuilib.logcat; import com.android.ddmlib.DdmConstants; import com.android.ddmlib.IDevice; import com.android.ddmlib.Log.LogLevel; import com.android.ddmuilib.ITableFocusListener; import com.android.ddmuilib.ITableFocusListener.IFocusedTableActivator; import com.android.ddmuilib.ImageLoader; import com.android.ddmuilib.SelectionDependentPanel; import com.android.ddmuilib.TableHelper; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.preference.PreferenceConverter; import org.eclipse.jface.util.IPropertyChangeListener; import org.eclipse.jface.util.PropertyChangeEvent; import org.eclipse.jface.viewers.ColumnViewer; import org.eclipse.jface.viewers.ColumnViewerToolTipSupport; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.viewers.TableViewerColumn; import org.eclipse.jface.viewers.ViewerCell; import org.eclipse.jface.window.ToolTip; import org.eclipse.jface.window.Window; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.SashForm; import org.eclipse.swt.dnd.Clipboard; import org.eclipse.swt.dnd.TextTransfer; import org.eclipse.swt.dnd.Transfer; import org.eclipse.swt.events.ControlAdapter; import org.eclipse.swt.events.ControlEvent; import org.eclipse.swt.events.FocusEvent; import org.eclipse.swt.events.FocusListener; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.FontData; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Combo; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.FileDialog; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.ScrollBar; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableColumn; import org.eclipse.swt.widgets.TableItem; import org.eclipse.swt.widgets.Text; import org.eclipse.swt.widgets.ToolBar; import org.eclipse.swt.widgets.ToolItem; import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; /** * LogCatPanel displays a table listing the logcat messages. */ public final class LogCatPanel extends SelectionDependentPanel implements ILogCatMessageEventListener { /** Preference key to use for storing list of logcat filters. */ public static final String LOGCAT_FILTERS_LIST = "logcat.view.filters.list"; /** Preference key to use for storing font settings. */ public static final String LOGCAT_VIEW_FONT_PREFKEY = "logcat.view.font"; // Use a monospace font family private static final String FONT_FAMILY = DdmConstants.CURRENT_PLATFORM == DdmConstants.PLATFORM_DARWIN ? "Monaco":"Courier New"; // Use the default system font size private static final FontData DEFAULT_LOGCAT_FONTDATA; static { int h = Display.getDefault().getSystemFont().getFontData()[0].getHeight(); DEFAULT_LOGCAT_FONTDATA = new FontData(FONT_FAMILY, h, SWT.NORMAL); } private static final String LOGCAT_VIEW_COLSIZE_PREFKEY_PREFIX = "logcat.view.colsize."; private static final String DISPLAY_FILTERS_COLUMN_PREFKEY = "logcat.view.display.filters"; /** Default message to show in the message search field. */ private static final String DEFAULT_SEARCH_MESSAGE = "Search for messages. Accepts Java regexes. " + "Prefix with pid:, app:, tag: or text: to limit scope."; /** Tooltip to show in the message search field. */ private static final String DEFAULT_SEARCH_TOOLTIP = "Example search patterns:\n" + " sqlite (search for sqlite in text field)\n" + " app:browser (search for messages generated by the browser application)"; private static final String IMAGE_ADD_FILTER = "add.png"; //$NON-NLS-1$ private static final String IMAGE_DELETE_FILTER = "delete.png"; //$NON-NLS-1$ private static final String IMAGE_EDIT_FILTER = "edit.png"; //$NON-NLS-1$ private static final String IMAGE_SAVE_LOG_TO_FILE = "save.png"; //$NON-NLS-1$ private static final String IMAGE_CLEAR_LOG = "clear.png"; //$NON-NLS-1$ private static final String IMAGE_DISPLAY_FILTERS = "displayfilters.png"; //$NON-NLS-1$ private static final String IMAGE_PAUSE_LOGCAT = "pause_logcat.png"; //$NON-NLS-1$ private static final int[] WEIGHTS_SHOW_FILTERS = new int[] {15, 85}; private static final int[] WEIGHTS_LOGCAT_ONLY = new int[] {0, 100}; private LogCatReceiver mReceiver; private IPreferenceStore mPrefStore; private List<LogCatFilter> mLogCatFilters; private int mCurrentSelectedFilterIndex; private int mRemovedEntriesCount = 0; private int mPreviousRemainingCapacity = 0; private ToolItem mNewFilterToolItem; private ToolItem mDeleteFilterToolItem; private ToolItem mEditFilterToolItem; private TableViewer mFiltersTableViewer; private Combo mLiveFilterLevelCombo; private Text mLiveFilterText; private TableViewer mViewer; private boolean mShouldScrollToLatestLog = true; private ToolItem mPauseLogcatCheckBox; private boolean mLastItemPainted = false; private String mLogFileExportFolder; private LogCatMessageLabelProvider mLogCatMessageLabelProvider; private SashForm mSash; /** * Construct a logcat panel. * @param prefStore preference store where UI preferences will be saved */ public LogCatPanel(IPreferenceStore prefStore) { mPrefStore = prefStore; initializeFilters(); setupDefaultPreferences(); initializePreferenceUpdateListeners(); } private void initializeFilters() { mLogCatFilters = new ArrayList<LogCatFilter>(); /* add default filter matching all messages */ String tag = ""; String text = ""; String pid = ""; String app = ""; mLogCatFilters.add(new LogCatFilter("All messages (no filters)", tag, text, pid, app, LogLevel.VERBOSE)); /* restore saved filters from prefStore */ List<LogCatFilter> savedFilters = getSavedFilters(); mLogCatFilters.addAll(savedFilters); } private void setupDefaultPreferences() { PreferenceConverter.setDefault(mPrefStore, LogCatPanel.LOGCAT_VIEW_FONT_PREFKEY, DEFAULT_LOGCAT_FONTDATA); mPrefStore.setDefault(LogCatMessageList.MAX_MESSAGES_PREFKEY, LogCatMessageList.MAX_MESSAGES_DEFAULT); mPrefStore.setDefault(DISPLAY_FILTERS_COLUMN_PREFKEY, true); } private void initializePreferenceUpdateListeners() { mPrefStore.addPropertyChangeListener(new IPropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent event) { String changedProperty = event.getProperty(); if (changedProperty.equals(LogCatPanel.LOGCAT_VIEW_FONT_PREFKEY)) { mLogCatMessageLabelProvider.setFont(getFontFromPrefStore()); refreshLogCatTable(); } else if (changedProperty.equals( LogCatMessageList.MAX_MESSAGES_PREFKEY)) { mReceiver.resizeFifo(mPrefStore.getInt( LogCatMessageList.MAX_MESSAGES_PREFKEY)); refreshLogCatTable(); } } }); } private void saveFilterPreferences() { LogCatFilterSettingsSerializer serializer = new LogCatFilterSettingsSerializer(); /* save all filter settings except the first one which is the default */ String e = serializer.encodeToPreferenceString( mLogCatFilters.subList(1, mLogCatFilters.size())); mPrefStore.setValue(LOGCAT_FILTERS_LIST, e); } private List<LogCatFilter> getSavedFilters() { LogCatFilterSettingsSerializer serializer = new LogCatFilterSettingsSerializer(); String e = mPrefStore.getString(LOGCAT_FILTERS_LIST); return serializer.decodeFromPreferenceString(e); } @Override public void deviceSelected() { IDevice device = getCurrentDevice(); if (device == null) { // If the device is not working properly, getCurrentDevice() could return null. // In such a case, we don't launch logcat, nor switch the display. return; } if (mReceiver != null) { // Don't need to listen to new logcat messages from previous device anymore. mReceiver.removeMessageReceivedEventListener(this); // When switching between devices, existing filter match count should be reset. for (LogCatFilter f : mLogCatFilters) { f.resetUnreadCount(); } } mReceiver = LogCatReceiverFactory.INSTANCE.newReceiver(device, mPrefStore); mReceiver.addMessageReceivedEventListener(this); mViewer.setInput(mReceiver.getMessages()); // Always scroll to last line whenever the selected device changes. // Run this in a separate async thread to give the table some time to update after the // setInput above. Display.getDefault().asyncExec(new Runnable() { @Override public void run() { scrollToLatestLog(); } }); } @Override public void clientSelected() { } @Override protected void postCreation() { } @Override protected Control createControl(Composite parent) { GridLayout layout = new GridLayout(1, false); parent.setLayout(layout); createViews(parent); setupDefaults(); return null; } private void createViews(Composite parent) { mSash = createSash(parent); createListOfFilters(mSash); createLogTableView(mSash); boolean showFilters = mPrefStore.getBoolean(DISPLAY_FILTERS_COLUMN_PREFKEY); updateFiltersColumn(showFilters); } private SashForm createSash(Composite parent) { SashForm sash = new SashForm(parent, SWT.HORIZONTAL); sash.setLayoutData(new GridData(GridData.FILL_BOTH)); return sash; } private void createListOfFilters(SashForm sash) { Composite c = new Composite(sash, SWT.BORDER); GridLayout layout = new GridLayout(2, false); c.setLayout(layout); c.setLayoutData(new GridData(GridData.FILL_BOTH)); createFiltersToolbar(c); createFiltersTable(c); } private void createFiltersToolbar(Composite parent) { Label l = new Label(parent, SWT.NONE); l.setText("Saved Filters"); GridData gd = new GridData(); gd.horizontalAlignment = SWT.LEFT; l.setLayoutData(gd); ToolBar t = new ToolBar(parent, SWT.FLAT); gd = new GridData(); gd.horizontalAlignment = SWT.RIGHT; t.setLayoutData(gd); /* new filter */ mNewFilterToolItem = new ToolItem(t, SWT.PUSH); mNewFilterToolItem.setImage( ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_ADD_FILTER, t.getDisplay())); mNewFilterToolItem.setToolTipText("Add a new logcat filter"); mNewFilterToolItem.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent arg0) { addNewFilter(); } }); /* delete filter */ mDeleteFilterToolItem = new ToolItem(t, SWT.PUSH); mDeleteFilterToolItem.setImage( ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_DELETE_FILTER, t.getDisplay())); mDeleteFilterToolItem.setToolTipText("Delete selected logcat filter"); mDeleteFilterToolItem.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent arg0) { deleteSelectedFilter(); } }); /* edit filter */ mEditFilterToolItem = new ToolItem(t, SWT.PUSH); mEditFilterToolItem.setImage( ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_EDIT_FILTER, t.getDisplay())); mEditFilterToolItem.setToolTipText("Edit selected logcat filter"); mEditFilterToolItem.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent arg0) { editSelectedFilter(); } }); } private void addNewFilter() { LogCatFilterSettingsDialog d = new LogCatFilterSettingsDialog( Display.getCurrent().getActiveShell()); if (d.open() != Window.OK) { return; } LogCatFilter f = new LogCatFilter(d.getFilterName().trim(), d.getTag().trim(), d.getText().trim(), d.getPid().trim(), d.getAppName().trim(), LogLevel.getByString(d.getLogLevel())); mLogCatFilters.add(f); mFiltersTableViewer.refresh(); /* select the newly added entry */ int idx = mLogCatFilters.size() - 1; mFiltersTableViewer.getTable().setSelection(idx); filterSelectionChanged(); saveFilterPreferences(); } private void deleteSelectedFilter() { int selectedIndex = mFiltersTableViewer.getTable().getSelectionIndex(); if (selectedIndex <= 0) { /* return if no selected filter, or the default filter was selected (0th). */ return; } mLogCatFilters.remove(selectedIndex); mFiltersTableViewer.refresh(); mFiltersTableViewer.getTable().setSelection(selectedIndex - 1); filterSelectionChanged(); saveFilterPreferences(); } private void editSelectedFilter() { int selectedIndex = mFiltersTableViewer.getTable().getSelectionIndex(); if (selectedIndex < 0) { return; } LogCatFilter curFilter = mLogCatFilters.get(selectedIndex); LogCatFilterSettingsDialog dialog = new LogCatFilterSettingsDialog( Display.getCurrent().getActiveShell()); dialog.setDefaults(curFilter.getName(), curFilter.getTag(), curFilter.getText(), curFilter.getPid(), curFilter.getAppName(), curFilter.getLogLevel()); if (dialog.open() != Window.OK) { return; } LogCatFilter f = new LogCatFilter(dialog.getFilterName(), dialog.getTag(), dialog.getText(), dialog.getPid(), dialog.getAppName(), LogLevel.getByString(dialog.getLogLevel())); mLogCatFilters.set(selectedIndex, f); mFiltersTableViewer.refresh(); mFiltersTableViewer.getTable().setSelection(selectedIndex); filterSelectionChanged(); saveFilterPreferences(); } /** * Select the transient filter for the specified application. If no such filter * exists, then create one and then select that. This method should be called from * the UI thread. * @param appName application name to filter by */ public void selectTransientAppFilter(String appName) { assert mViewer.getTable().getDisplay().getThread() == Thread.currentThread(); LogCatFilter f = findTransientAppFilter(appName); if (f == null) { f = createTransientAppFilter(appName); mLogCatFilters.add(f); } selectFilterAt(mLogCatFilters.indexOf(f)); } private LogCatFilter findTransientAppFilter(String appName) { for (LogCatFilter f : mLogCatFilters) { if (f.isTransient() && f.getAppName().equals(appName)) { return f; } } return null; } private LogCatFilter createTransientAppFilter(String appName) { LogCatFilter f = new LogCatFilter(appName + " (Session Filter)", "", "", "", appName, LogLevel.VERBOSE); f.setTransient(); return f; } private void selectFilterAt(final int index) { mFiltersTableViewer.refresh(); mFiltersTableViewer.getTable().setSelection(index); filterSelectionChanged(); } private void createFiltersTable(Composite parent) { final Table table = new Table(parent, SWT.FULL_SELECTION); GridData gd = new GridData(GridData.FILL_BOTH); gd.horizontalSpan = 2; table.setLayoutData(gd); mFiltersTableViewer = new TableViewer(table); mFiltersTableViewer.setContentProvider(new LogCatFilterContentProvider()); mFiltersTableViewer.setLabelProvider(new LogCatFilterLabelProvider()); mFiltersTableViewer.setInput(mLogCatFilters); mFiltersTableViewer.getTable().addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent event) { filterSelectionChanged(); } @Override public void widgetDefaultSelected(SelectionEvent arg0) { editSelectedFilter(); } }); } private void createLogTableView(SashForm sash) { Composite c = new Composite(sash, SWT.NONE); c.setLayout(new GridLayout()); c.setLayoutData(new GridData(GridData.FILL_BOTH)); createLiveFilters(c); createLogcatViewTable(c); } /** * Create the search bar at the top of the logcat messages table. * FIXME: Currently, this feature is incomplete: The UI elements are created, but they * are all set to disabled state. */ private void createLiveFilters(Composite parent) { Composite c = new Composite(parent, SWT.NONE); c.setLayout(new GridLayout(3, false)); c.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mLiveFilterText = new Text(c, SWT.BORDER | SWT.SEARCH); mLiveFilterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mLiveFilterText.setMessage(DEFAULT_SEARCH_MESSAGE); mLiveFilterText.setToolTipText(DEFAULT_SEARCH_TOOLTIP); mLiveFilterText.addModifyListener(new ModifyListener() { @Override public void modifyText(ModifyEvent arg0) { updateAppliedFilters(); } }); mLiveFilterLevelCombo = new Combo(c, SWT.READ_ONLY | SWT.DROP_DOWN); mLiveFilterLevelCombo.setItems( LogCatFilterSettingsDialog.getLogLevels().toArray(new String[0])); mLiveFilterLevelCombo.select(0); mLiveFilterLevelCombo.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent arg0) { updateAppliedFilters(); } }); ToolBar toolBar = new ToolBar(c, SWT.FLAT); ToolItem saveToLog = new ToolItem(toolBar, SWT.PUSH); saveToLog.setImage(ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_SAVE_LOG_TO_FILE, toolBar.getDisplay())); saveToLog.setToolTipText("Export Selected Items To Text File.."); saveToLog.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent arg0) { saveLogToFile(); } }); ToolItem clearLog = new ToolItem(toolBar, SWT.PUSH); clearLog.setImage( ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_CLEAR_LOG, toolBar.getDisplay())); clearLog.setToolTipText("Clear Log"); clearLog.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent arg0) { if (mReceiver != null) { mReceiver.clearMessages(); refreshLogCatTable(); mRemovedEntriesCount = 0; // the filters view is not cleared unless the filters are re-applied. updateAppliedFilters(); } } }); final ToolItem showFiltersColumn = new ToolItem(toolBar, SWT.CHECK); showFiltersColumn.setImage( ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_DISPLAY_FILTERS, toolBar.getDisplay())); showFiltersColumn.setSelection(mPrefStore.getBoolean(DISPLAY_FILTERS_COLUMN_PREFKEY)); showFiltersColumn.setToolTipText("Display Saved Filters View"); showFiltersColumn.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent event) { boolean showFilters = showFiltersColumn.getSelection(); mPrefStore.setValue(DISPLAY_FILTERS_COLUMN_PREFKEY, showFilters); updateFiltersColumn(showFilters); } }); mPauseLogcatCheckBox = new ToolItem(toolBar, SWT.CHECK); mPauseLogcatCheckBox.setImage( ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_PAUSE_LOGCAT, toolBar.getDisplay())); mPauseLogcatCheckBox.setSelection(false); mPauseLogcatCheckBox.setToolTipText("Pause receiving new logcat messages."); mPauseLogcatCheckBox.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent event) { boolean pauseLogcat = mPauseLogcatCheckBox.getSelection(); setScrollToLatestLog(!pauseLogcat, false); } }); } private void updateFiltersColumn(boolean showFilters) { if (showFilters) { mSash.setWeights(WEIGHTS_SHOW_FILTERS); } else { mSash.setWeights(WEIGHTS_LOGCAT_ONLY); } } /** * Save logcat messages selected in the table to a file. */ private void saveLogToFile() { /* show dialog box and get target file name */ final String fName = getLogFileTargetLocation(); if (fName == null) { return; } /* obtain list of selected messages */ final List<LogCatMessage> selectedMessages = getSelectedLogCatMessages(); /* save messages to file in a different (non UI) thread */ Thread t = new Thread(new Runnable() { @Override public void run() { try { BufferedWriter w = new BufferedWriter(new FileWriter(fName)); for (LogCatMessage m : selectedMessages) { w.append(m.toString()); w.newLine(); } w.close(); } catch (final IOException e) { Display.getDefault().asyncExec(new Runnable() { @Override public void run() { MessageDialog.openError(Display.getCurrent().getActiveShell(), "Unable to export selection to file.", "Unexpected error while saving selected messages to file: " + e.getMessage()); } }); } } }); t.setName("Saving selected items to logfile.."); t.start(); } /** * Display a {@link FileDialog} to the user and obtain the location for the log file. * @return path to target file, null if user canceled the dialog */ private String getLogFileTargetLocation() { FileDialog fd = new FileDialog(Display.getCurrent().getActiveShell(), SWT.SAVE); fd.setText("Save Log.."); fd.setFileName("log.txt"); if (mLogFileExportFolder == null) { mLogFileExportFolder = System.getProperty("user.home"); } fd.setFilterPath(mLogFileExportFolder); fd.setFilterNames(new String[] { "Text Files (*.txt)" }); fd.setFilterExtensions(new String[] { "*.txt" }); String fName = fd.open(); if (fName != null) { mLogFileExportFolder = fd.getFilterPath(); /* save path to restore on future calls */ } return fName; } private List<LogCatMessage> getSelectedLogCatMessages() { Table table = mViewer.getTable(); int[] indices = table.getSelectionIndices(); Arrays.sort(indices); /* Table.getSelectionIndices() does not specify an order */ // Get items from the table's input as opposed to getting each table item's data. // Retrieving table item's data can return NULL in case of a virtual table if the item // has not been displayed yet. Object input = mViewer.getInput(); if (!(input instanceof LogCatMessageList)) { return Collections.emptyList(); } List<LogCatMessage> filteredItems = applyCurrentFilters((LogCatMessageList) input); List<LogCatMessage> selectedMessages = new ArrayList<LogCatMessage>(indices.length); for (int i : indices) { // consider removed logcat message entries i -= mRemovedEntriesCount; if (i >= 0 && i < filteredItems.size()) { LogCatMessage m = filteredItems.get(i); selectedMessages.add(m); } } return selectedMessages; } private List<LogCatMessage> applyCurrentFilters(LogCatMessageList msgList) { Object[] items = msgList.toArray(); List<LogCatMessage> filteredItems = new ArrayList<LogCatMessage>(items.length); List<LogCatViewerFilter> filters = getFiltersToApply(); for (Object item : items) { if (!(item instanceof LogCatMessage)) { continue; } LogCatMessage msg = (LogCatMessage) item; if (!isMessageFiltered(msg, filters)) { filteredItems.add(msg); } } return filteredItems; } private boolean isMessageFiltered(LogCatMessage msg, List<LogCatViewerFilter> filters) { for (LogCatViewerFilter f : filters) { if (!f.select(null, null, msg)) { // message does not make it through this filter return true; } } return false; } private void createLogcatViewTable(Composite parent) { // The SWT.VIRTUAL bit causes the table to be rendered faster. However it makes all rows // to be of the same height, thereby clipping any rows with multiple lines of text. // In such a case, users can view the full text by hovering over the item and looking at // the tooltip. final Table table = new Table(parent, SWT.FULL_SELECTION | SWT.MULTI | SWT.VIRTUAL); mViewer = new TableViewer(table); table.setLayoutData(new GridData(GridData.FILL_BOTH)); table.getHorizontalBar().setVisible(true); /** Columns to show in the table. */ String[] properties = { "Level", "Time", "PID", "Application", "Tag", "Text", }; /** The sampleText for each column is used to determine the default widths * for each column. The contents do not matter, only their lengths are needed. */ String[] sampleText = { " ", " 00-00 00:00:00.0000 ", " 0000", " com.android.launcher", " SampleTagText", " Log Message field should be pretty long by default. As long as possible for correct display on Mac.", }; mLogCatMessageLabelProvider = new LogCatMessageLabelProvider(getFontFromPrefStore()); for (int i = 0; i < properties.length; i++) { TableColumn tc = TableHelper.createTableColumn(mViewer.getTable(), properties[i], /* Column title */ SWT.LEFT, /* Column Style */ sampleText[i], /* String to compute default col width */ getColPreferenceKey(properties[i]), /* Preference Store key for this column */ mPrefStore); TableViewerColumn tvc = new TableViewerColumn(mViewer, tc); tvc.setLabelProvider(mLogCatMessageLabelProvider); } mViewer.getTable().setLinesVisible(true); /* zebra stripe the table */ mViewer.getTable().setHeaderVisible(true); mViewer.setContentProvider(new LogCatMessageContentProvider()); WrappingToolTipSupport.enableFor(mViewer, ToolTip.NO_RECREATE); // Set the row height to be sufficient enough to display the current font. // This is not strictly necessary, except that on WinXP, the rows showed up clipped. So // we explicitly set it to be sure. mViewer.getTable().addListener(SWT.MeasureItem, new Listener() { @Override public void handleEvent(Event event) { event.height = event.gc.getFontMetrics().getHeight(); } }); // Update the label provider whenever the text column's width changes TableColumn textColumn = mViewer.getTable().getColumn(properties.length - 1); textColumn.addControlListener(new ControlAdapter() { @Override public void controlResized(ControlEvent event) { TableColumn tc = (TableColumn) event.getSource(); int width = tc.getWidth(); GC gc = new GC(tc.getParent()); int avgCharWidth = gc.getFontMetrics().getAverageCharWidth(); gc.dispose(); if (mLogCatMessageLabelProvider != null) { mLogCatMessageLabelProvider.setMinimumLengthForToolTips(width/avgCharWidth); } } }); setupAutoScrollLockBehavior(); initDoubleClickListener(); } /** * Setup to automatically enable or disable scroll lock. From a user's perspective, * the logcat window will: <ul> * <li> Automatically scroll and reveal new entries if the scrollbar is at the bottom. </li> * <li> Not scroll even when new messages are received if the scrollbar is not at the bottom. * </li> * </ul> * This requires that we are able to detect where the scrollbar is and what direction * it is moving. Unfortunately, that proves to be very platform dependent. Here's the behavior * of the scroll events on different platforms: <ul> * <li> On Windows, scroll bar events specify which direction the scrollbar is moving, but * it is not possible to determine if the scrollbar is right at the end. </li> * <li> On Mac/Cocoa, scroll bar events do not specify the direction of movement (it is always * set to SWT.DRAG), and it is not possible to identify where the scrollbar is since * small movements of the scrollbar are not reflected in sb.getSelection(). </li> * <li> On Linux/gtk, we don't get the direction, but we can accurately locate the * scrollbar location using getSelection(), getThumb() and getMaximum(). * </ul> */ private void setupAutoScrollLockBehavior() { if (DdmConstants.CURRENT_PLATFORM == DdmConstants.PLATFORM_WINDOWS) { // On Windows, it is not possible to detect whether the scrollbar is at the // bottom using the values of ScrollBar.getThumb, getSelection and getMaximum. // Instead we resort to the following workaround: attach to the paint listener // and see if the last item has been painted since the previous scroll event. // If the last item has been painted, then we assume that we are at the bottom. mViewer.getTable().addListener(SWT.PaintItem, new Listener() { @Override public void handleEvent(Event event) { TableItem item = (TableItem) event.item; TableItem[] items = mViewer.getTable().getItems(); if (items.length > 0 && items[items.length - 1] == item) { mLastItemPainted = true; } } }); mViewer.getTable().getVerticalBar().addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent event) { boolean scrollToLast; if (event.detail == SWT.ARROW_UP || event.detail == SWT.PAGE_UP || event.detail == SWT.HOME) { // if we know that we are moving up, then do not scroll down scrollToLast = false; } else { // otherwise, enable scrollToLast only if the last item was displayed scrollToLast = mLastItemPainted; } setScrollToLatestLog(scrollToLast, true); mLastItemPainted = false; } }); } else if (DdmConstants.CURRENT_PLATFORM == DdmConstants.PLATFORM_LINUX) { // On Linux/gtk, we do not get any details regarding the scroll event (up/down/etc). // So we completely rely on whether the scrollbar is at the bottom or not. mViewer.getTable().getVerticalBar().addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent event) { ScrollBar sb = (ScrollBar) event.getSource(); boolean scrollToLast = sb.getSelection() + sb.getThumb() == sb.getMaximum(); setScrollToLatestLog(scrollToLast, true); } }); } else { // On Mac, we do not get any details regarding the (trackball) scroll event, // nor can we rely on getSelection() changing for small movements. As a result, we // do not setup any auto scroll lock behavior. Mac users have to manually pause and // unpause if they are looking at a particular item in a high volume stream of events. } } private void setScrollToLatestLog(boolean scroll, boolean updateCheckbox) { mShouldScrollToLatestLog = scroll; if (updateCheckbox) { mPauseLogcatCheckBox.setSelection(!scroll); } if (scroll) { mViewer.refresh(); scrollToLatestLog(); } } private static class WrappingToolTipSupport extends ColumnViewerToolTipSupport { protected WrappingToolTipSupport(ColumnViewer viewer, int style, boolean manualActivation) { super(viewer, style, manualActivation); } @Override protected Composite createViewerToolTipContentArea(Event event, ViewerCell cell, Composite parent) { Composite comp = new Composite(parent, SWT.NONE); GridLayout l = new GridLayout(1, false); l.horizontalSpacing = 0; l.marginWidth = 0; l.marginHeight = 0; l.verticalSpacing = 0; comp.setLayout(l); Text text = new Text(comp, SWT.BORDER | SWT.V_SCROLL | SWT.WRAP); text.setEditable(false); text.setText(cell.getElement().toString()); text.setLayoutData(new GridData(500, 150)); return comp; } @Override public boolean isHideOnMouseDown() { return false; } public static final void enableFor(ColumnViewer viewer, int style) { new WrappingToolTipSupport(viewer, style, false); } } private String getColPreferenceKey(String field) { return LOGCAT_VIEW_COLSIZE_PREFKEY_PREFIX + field; } private Font getFontFromPrefStore() { FontData fd = PreferenceConverter.getFontData(mPrefStore, LogCatPanel.LOGCAT_VIEW_FONT_PREFKEY); return new Font(Display.getDefault(), fd); } private void setupDefaults() { int defaultFilterIndex = 0; mFiltersTableViewer.getTable().setSelection(defaultFilterIndex); filterSelectionChanged(); } /** * Perform all necessary updates whenever a filter is selected (by user or programmatically). */ private void filterSelectionChanged() { int idx = getSelectedSavedFilterIndex(); if (idx == -1) { /* One of the filters should always be selected. * On Linux, there is no way to deselect an item. * On Mac, clicking inside the list view, but not an any item will result * in all items being deselected. In such a case, we simply reselect the * first entry. */ idx = 0; mFiltersTableViewer.getTable().setSelection(idx); } mCurrentSelectedFilterIndex = idx; resetUnreadCountForSelectedFilter(); updateFiltersToolBar(); updateAppliedFilters(); } private void resetUnreadCountForSelectedFilter() { int index = getSelectedSavedFilterIndex(); mLogCatFilters.get(index).resetUnreadCount(); refreshFiltersTable(); } private int getSelectedSavedFilterIndex() { return mFiltersTableViewer.getTable().getSelectionIndex(); } private void updateFiltersToolBar() { /* The default filter at index 0 can neither be edited, nor removed. */ boolean en = getSelectedSavedFilterIndex() != 0; mEditFilterToolItem.setEnabled(en); mDeleteFilterToolItem.setEnabled(en); } private void updateAppliedFilters() { List<LogCatViewerFilter> filters = getFiltersToApply(); mViewer.setFilters(filters.toArray(new LogCatViewerFilter[filters.size()])); /* whenever filters are changed, the number of displayed logs changes * drastically. Display the latest log in such a situation. */ scrollToLatestLog(); } private List<LogCatViewerFilter> getFiltersToApply() { /* list of filters to apply = saved filter + live filters */ List<LogCatViewerFilter> filters = new ArrayList<LogCatViewerFilter>(); filters.add(getSelectedSavedFilter()); filters.addAll(getCurrentLiveFilters()); return filters; } private List<LogCatViewerFilter> getCurrentLiveFilters() { List<LogCatViewerFilter> liveFilters = new ArrayList<LogCatViewerFilter>(); List<LogCatFilter> liveFilterSettings = LogCatFilter.fromString( mLiveFilterText.getText(), /* current query */ LogLevel.getByString(mLiveFilterLevelCombo.getText())); /* current log level */ for (LogCatFilter s : liveFilterSettings) { liveFilters.add(new LogCatViewerFilter(s)); } return liveFilters; } private LogCatViewerFilter getSelectedSavedFilter() { int index = getSelectedSavedFilterIndex(); return new LogCatViewerFilter(mLogCatFilters.get(index)); } @Override public void setFocus() { } /** * Update view whenever a message is received. * @param receivedMessages list of messages from logcat * Implements {@link ILogCatMessageEventListener#messageReceived()}. */ @Override public void messageReceived(List<LogCatMessage> receivedMessages) { refreshLogCatTable(); if (mShouldScrollToLatestLog) { updateUnreadCount(receivedMessages); refreshFiltersTable(); } else { LogCatMessageList messageList = mReceiver.getMessages(); int remainingCapacity = messageList.remainingCapacity(); if (remainingCapacity == 0) { mRemovedEntriesCount += receivedMessages.size() - mPreviousRemainingCapacity; } mPreviousRemainingCapacity = remainingCapacity; } } /** * When new messages are received, and they match a saved filter, update * the unread count associated with that filter. * @param receivedMessages list of new messages received */ private void updateUnreadCount(List<LogCatMessage> receivedMessages) { for (int i = 0; i < mLogCatFilters.size(); i++) { if (i == mCurrentSelectedFilterIndex) { /* no need to update unread count for currently selected filter */ continue; } mLogCatFilters.get(i).updateUnreadCount(receivedMessages); } } private void refreshFiltersTable() { Display.getDefault().asyncExec(new Runnable() { @Override public void run() { if (mFiltersTableViewer.getTable().isDisposed()) { return; } mFiltersTableViewer.refresh(); } }); } /** Task currently submitted to {@link Display#asyncExec} to be run in UI thread. */ private LogCatTableRefresherTask mCurrentRefresher; /** * Refresh the logcat table asynchronously from the UI thread. * This method adds a new async refresh only if there are no pending refreshes for the table. * Doing so eliminates redundant refresh threads from being queued up to be run on the * display thread. */ private void refreshLogCatTable() { synchronized (this) { if (mCurrentRefresher == null && mShouldScrollToLatestLog) { mCurrentRefresher = new LogCatTableRefresherTask(); Display.getDefault().asyncExec(mCurrentRefresher); } } } private class LogCatTableRefresherTask implements Runnable { @Override public void run() { if (mViewer.getTable().isDisposed()) { return; } synchronized (LogCatPanel.this) { mCurrentRefresher = null; } if (mShouldScrollToLatestLog) { mViewer.refresh(); scrollToLatestLog(); } } } /** Scroll to the last line. */ private void scrollToLatestLog() { mRemovedEntriesCount = 0; mViewer.getTable().setTopIndex(mViewer.getTable().getItemCount() - 1); } private List<ILogCatMessageSelectionListener> mMessageSelectionListeners; private void initDoubleClickListener() { mMessageSelectionListeners = new ArrayList<ILogCatMessageSelectionListener>(1); mViewer.getTable().addSelectionListener(new SelectionAdapter() { @Override public void widgetDefaultSelected(SelectionEvent arg0) { List<LogCatMessage> selectedMessages = getSelectedLogCatMessages(); if (selectedMessages.size() == 0) { return; } for (ILogCatMessageSelectionListener l : mMessageSelectionListeners) { l.messageDoubleClicked(selectedMessages.get(0)); } } }); } public void addLogCatMessageSelectionListener(ILogCatMessageSelectionListener l) { mMessageSelectionListeners.add(l); } private ITableFocusListener mTableFocusListener; /** * Specify the listener to be called when the logcat view gets focus. This interface is * required by DDMS to hook up the menu items for Copy and Select All. * @param listener listener to be notified when logcat view is in focus */ public void setTableFocusListener(ITableFocusListener listener) { mTableFocusListener = listener; final Table table = mViewer.getTable(); final IFocusedTableActivator activator = new IFocusedTableActivator() { @Override public void copy(Clipboard clipboard) { copySelectionToClipboard(clipboard); } @Override public void selectAll() { table.selectAll(); } }; table.addFocusListener(new FocusListener() { @Override public void focusGained(FocusEvent e) { mTableFocusListener.focusGained(activator); } @Override public void focusLost(FocusEvent e) { mTableFocusListener.focusLost(activator); } }); } /** Copy all selected messages to clipboard. */ public void copySelectionToClipboard(Clipboard clipboard) { StringBuilder sb = new StringBuilder(); for (LogCatMessage m : getSelectedLogCatMessages()) { sb.append(m.toString()); sb.append('\n'); } if (sb.length() > 0) { clipboard.setContents( new Object[] {sb.toString()}, new Transfer[] {TextTransfer.getInstance()} ); } } /** Select all items in the logcat table. */ public void selectAll() { mViewer.getTable().selectAll(); } }