blob: e7dcec9553dc808c499136cf40b1708613a958c7 [file] [log] [blame]
/*
* 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.FindDialog;
import com.android.ddmuilib.ImageLoader;
import com.android.ddmuilib.SelectionDependentPanel;
import com.android.ddmuilib.TableHelper;
import com.android.ddmuilib.AbstractBufferFindTarget;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.MenuManager;
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.TableViewer;
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.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
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.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.graphics.Rectangle;
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.Menu;
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;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
/**
* LogCatPanel displays a table listing the logcat messages.
*/
public final class LogCatPanel extends SelectionDependentPanel
implements ILogCatBufferChangeListener {
/** 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";
// Preference keys for message colors based on severity level
private static final String MSG_COLOR_PREFKEY_PREFIX = "logcat.msg.color.";
public static final String VERBOSE_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "verbose"; //$NON-NLS-1$
public static final String DEBUG_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "debug"; //$NON-NLS-1$
public static final String INFO_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "info"; //$NON-NLS-1$
public static final String WARN_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "warn"; //$NON-NLS-1$
public static final String ERROR_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "error"; //$NON-NLS-1$
public static final String ASSERT_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "assert"; //$NON-NLS-1$
// 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_SCROLL_LOCK = "scroll_lock.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};
/** Index of the default filter in the saved filters column. */
private static final int DEFAULT_FILTER_INDEX = 0;
/* Text colors for the filter box */
private static final Color VALID_FILTER_REGEX_COLOR =
Display.getDefault().getSystemColor(SWT.COLOR_BLACK);
private static final Color INVALID_FILTER_REGEX_COLOR =
Display.getDefault().getSystemColor(SWT.COLOR_RED);
private LogCatReceiver mReceiver;
private IPreferenceStore mPrefStore;
private List<LogCatFilter> mLogCatFilters;
private int mCurrentSelectedFilterIndex;
private ToolItem mNewFilterToolItem;
private ToolItem mDeleteFilterToolItem;
private ToolItem mEditFilterToolItem;
private TableViewer mFiltersTableViewer;
private Combo mLiveFilterLevelCombo;
private Text mLiveFilterText;
private List<LogCatFilter> mCurrentFilters = Collections.emptyList();
private Table mTable;
private boolean mShouldScrollToLatestLog = true;
private ToolItem mScrollLockCheckBox;
private String mLogFileExportFolder;
private Font mFont;
private int mWrapWidthInChars;
private Color mVerboseColor;
private Color mDebugColor;
private Color mInfoColor;
private Color mWarnColor;
private Color mErrorColor;
private Color mAssertColor;
private SashForm mSash;
// messages added since last refresh, synchronized on mLogBuffer
private List<LogCatMessage> mLogBuffer;
// # of messages deleted since last refresh, synchronized on mLogBuffer
private int mDeletedLogCount;
/**
* Construct a logcat panel.
* @param prefStore preference store where UI preferences will be saved
*/
public LogCatPanel(IPreferenceStore prefStore) {
mPrefStore = prefStore;
mLogBuffer = new ArrayList<LogCatMessage>(LogCatMessageList.MAX_MESSAGES_DEFAULT);
initializeFilters();
setupDefaultPreferences();
initializePreferenceUpdateListeners();
mFont = getFontFromPrefStore();
loadMessageColorPreferences();
}
private void loadMessageColorPreferences() {
if (mVerboseColor != null) {
disposeMessageColors();
}
mVerboseColor = getColorFromPrefStore(VERBOSE_COLOR_PREFKEY);
mDebugColor = getColorFromPrefStore(DEBUG_COLOR_PREFKEY);
mInfoColor = getColorFromPrefStore(INFO_COLOR_PREFKEY);
mWarnColor = getColorFromPrefStore(WARN_COLOR_PREFKEY);
mErrorColor = getColorFromPrefStore(ERROR_COLOR_PREFKEY);
mAssertColor = getColorFromPrefStore(ASSERT_COLOR_PREFKEY);
}
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);
/* Default Colors for different log levels. */
PreferenceConverter.setDefault(mPrefStore, LogCatPanel.VERBOSE_COLOR_PREFKEY,
new RGB(0, 0, 0));
PreferenceConverter.setDefault(mPrefStore, LogCatPanel.DEBUG_COLOR_PREFKEY,
new RGB(0, 0, 127));
PreferenceConverter.setDefault(mPrefStore, LogCatPanel.INFO_COLOR_PREFKEY,
new RGB(0, 127, 0));
PreferenceConverter.setDefault(mPrefStore, LogCatPanel.WARN_COLOR_PREFKEY,
new RGB(255, 127, 0));
PreferenceConverter.setDefault(mPrefStore, LogCatPanel.ERROR_COLOR_PREFKEY,
new RGB(255, 0, 0));
PreferenceConverter.setDefault(mPrefStore, LogCatPanel.ASSERT_COLOR_PREFKEY,
new RGB(255, 0, 0));
}
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)) {
if (mFont != null) {
mFont.dispose();
}
mFont = getFontFromPrefStore();
recomputeWrapWidth();
Display.getDefault().syncExec(new Runnable() {
@Override
public void run() {
for (TableItem it: mTable.getItems()) {
it.setFont(mFont);
}
}
});
} else if (changedProperty.startsWith(MSG_COLOR_PREFKEY_PREFIX)) {
loadMessageColorPreferences();
Display.getDefault().syncExec(new Runnable() {
@Override
public void run() {
Color c = mVerboseColor;
for (TableItem it: mTable.getItems()) {
Object data = it.getData();
if (data instanceof LogCatMessage) {
c = getForegroundColor((LogCatMessage) data);
}
it.setForeground(c);
}
}
});
} else if (changedProperty.equals(LogCatMessageList.MAX_MESSAGES_PREFKEY)) {
mReceiver.resizeFifo(mPrefStore.getInt(
LogCatMessageList.MAX_MESSAGES_PREFKEY));
reloadLogBuffer();
}
}
});
}
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);
reloadLogBuffer();
// 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(String defaultTag, String defaultText, String defaultPid,
String defaultAppName, LogLevel defaultLevel) {
LogCatFilterSettingsDialog d = new LogCatFilterSettingsDialog(
Display.getCurrent().getActiveShell());
d.setDefaults("", defaultTag, defaultText, defaultPid, defaultAppName, defaultLevel);
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 addNewFilter() {
addNewFilter("", "", "",
"", LogLevel.VERBOSE);
}
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 mTable.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();
if (index != mFiltersTableViewer.getTable().getSelectionIndex()) {
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. */
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) {
updateFilterTextColor();
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();
// 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);
}
});
mScrollLockCheckBox = new ToolItem(toolBar, SWT.CHECK);
mScrollLockCheckBox.setImage(
ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_SCROLL_LOCK,
toolBar.getDisplay()));
mScrollLockCheckBox.setSelection(false);
mScrollLockCheckBox.setToolTipText("Scroll Lock");
mScrollLockCheckBox.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent event) {
boolean scrollLock = mScrollLockCheckBox.getSelection();
setScrollToLatestLog(!scrollLock);
}
});
}
/** Sets the foreground color of filter text based on whether the regex is valid. */
private void updateFilterTextColor() {
String text = mLiveFilterText.getText();
Color c;
try {
Pattern.compile(text.trim());
c = VALID_FILTER_REGEX_COLOR;
} catch (PatternSyntaxException e) {
c = INVALID_FILTER_REGEX_COLOR;
}
mLiveFilterText.setForeground(c);
}
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() {
BufferedWriter w = null;
try {
w = new BufferedWriter(new FileWriter(fName));
for (LogCatMessage m : selectedMessages) {
w.append(m.toString());
w.newLine();
}
} 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());
}
});
} finally {
if (w != null) {
try {
w.close();
} catch (IOException e) {
// ignore
}
}
}
}
});
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() {
int[] indices = mTable.getSelectionIndices();
Arrays.sort(indices); /* Table.getSelectionIndices() does not specify an order */
List<LogCatMessage> selectedMessages = new ArrayList<LogCatMessage>(indices.length);
for (int i : indices) {
Object data = mTable.getItem(i).getData();
if (data instanceof LogCatMessage) {
selectedMessages.add((LogCatMessage) data);
}
}
return selectedMessages;
}
private List<LogCatMessage> applyCurrentFilters(List<LogCatMessage> msgList) {
List<LogCatMessage> filteredItems = new ArrayList<LogCatMessage>(msgList.size());
for (LogCatMessage msg: msgList) {
if (isMessageAccepted(msg, mCurrentFilters)) {
filteredItems.add(msg);
}
}
return filteredItems;
}
private boolean isMessageAccepted(LogCatMessage msg, List<LogCatFilter> filters) {
for (LogCatFilter f : filters) {
if (!f.matches(msg)) {
// not accepted by this filter
return false;
}
}
// accepted by all filters
return true;
}
private void createLogcatViewTable(Composite parent) {
mTable = new Table(parent, SWT.FULL_SELECTION | SWT.MULTI);
mTable.setLayoutData(new GridData(GridData.FILL_BOTH));
mTable.getHorizontalBar().setVisible(true);
/** Columns to show in the table. */
String[] properties = {
"Level",
"Time",
"PID",
"TID",
"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",
" 0000",
" com.android.launcher",
" SampleTagText",
" Log Message field should be pretty long by default. As long as possible for correct display on Mac.",
};
for (int i = 0; i < properties.length; i++) {
TableHelper.createTableColumn(mTable,
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);
}
// don't zebra stripe the table: When the buffer is full, and scroll lock is on, having
// zebra striping means that the background could keep changing depending on the number
// of new messages added to the bottom of the log.
mTable.setLinesVisible(false);
mTable.setHeaderVisible(true);
// 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.
mTable.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 = mTable.getColumn(properties.length - 1);
textColumn.addControlListener(new ControlAdapter() {
@Override
public void controlResized(ControlEvent event) {
recomputeWrapWidth();
}
});
addRightClickMenu(mTable);
initDoubleClickListener();
recomputeWrapWidth();
mTable.addDisposeListener(new DisposeListener() {
@Override
public void widgetDisposed(DisposeEvent arg0) {
dispose();
}
});
}
/** Setup menu to be displayed when right clicking a log message. */
private void addRightClickMenu(final Table table) {
// This action will pop up a create filter dialog pre-populated with current selection
final Action filterAction = new Action("Filter similar messages...") {
@Override
public void run() {
List<LogCatMessage> selectedMessages = getSelectedLogCatMessages();
if (selectedMessages.size() == 0) {
addNewFilter();
} else {
LogCatMessage m = selectedMessages.get(0);
addNewFilter(m.getTag(), m.getMessage(), m.getPid(), m.getAppName(),
m.getLogLevel());
}
}
};
final Action findAction = new Action("Find...") {
@Override
public void run() {
showFindDialog();
};
};
final MenuManager mgr = new MenuManager();
mgr.add(filterAction);
mgr.add(findAction);
final Menu menu = mgr.createContextMenu(table);
table.addListener(SWT.MenuDetect, new Listener() {
@Override
public void handleEvent(Event event) {
Point pt = table.getDisplay().map(null, table, new Point(event.x, event.y));
Rectangle clientArea = table.getClientArea();
// The click location is in the header if it is between
// clientArea.y and clientArea.y + header height
boolean header = pt.y > clientArea.y
&& pt.y < (clientArea.y + table.getHeaderHeight());
// Show the menu only if it is not inside the header
table.setMenu(header ? null : menu);
}
});
}
public void recomputeWrapWidth() {
if (mTable == null || mTable.isDisposed()) {
return;
}
// get width of the last column (log message)
TableColumn tc = mTable.getColumn(mTable.getColumnCount() - 1);
int colWidth = tc.getWidth();
// get font width
GC gc = new GC(tc.getParent());
gc.setFont(mFont);
int avgCharWidth = gc.getFontMetrics().getAverageCharWidth();
gc.dispose();
int MIN_CHARS_PER_LINE = 50; // show atleast these many chars per line
mWrapWidthInChars = Math.max(colWidth/avgCharWidth, MIN_CHARS_PER_LINE);
int OFFSET_AT_END_OF_LINE = 10; // leave some space at the end of the line
mWrapWidthInChars -= OFFSET_AT_END_OF_LINE;
}
private void setScrollToLatestLog(boolean scroll) {
mShouldScrollToLatestLog = scroll;
if (scroll) {
scrollToLatestLog();
}
}
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 Color getColorFromPrefStore(String key) {
RGB rgb = PreferenceConverter.getColor(mPrefStore, key);
return new Color(Display.getDefault(), rgb);
}
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 = mFiltersTableViewer.getTable().getSelectionIndex();
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() {
mLogCatFilters.get(mCurrentSelectedFilterIndex).resetUnreadCount();
refreshFiltersTable();
}
private void updateFiltersToolBar() {
/* The default filter at index 0 can neither be edited, nor removed. */
boolean en = mCurrentSelectedFilterIndex != DEFAULT_FILTER_INDEX;
mEditFilterToolItem.setEnabled(en);
mDeleteFilterToolItem.setEnabled(en);
}
private void updateAppliedFilters() {
mCurrentFilters = getFiltersToApply();
reloadLogBuffer();
}
private List<LogCatFilter> getFiltersToApply() {
/* list of filters to apply = saved filter + live filters */
List<LogCatFilter> filters = new ArrayList<LogCatFilter>();
if (mCurrentSelectedFilterIndex != DEFAULT_FILTER_INDEX) {
filters.add(getSelectedSavedFilter());
}
filters.addAll(getCurrentLiveFilters());
return filters;
}
private List<LogCatFilter> getCurrentLiveFilters() {
return LogCatFilter.fromString(
mLiveFilterText.getText(), /* current query */
LogLevel.getByString(mLiveFilterLevelCombo.getText())); /* current log level */
}
private LogCatFilter getSelectedSavedFilter() {
return mLogCatFilters.get(mCurrentSelectedFilterIndex);
}
@Override
public void setFocus() {
}
@Override
public void bufferChanged(List<LogCatMessage> addedMessages,
List<LogCatMessage> deletedMessages) {
synchronized (mLogBuffer) {
addedMessages = applyCurrentFilters(addedMessages);
deletedMessages = applyCurrentFilters(deletedMessages);
mLogBuffer.addAll(addedMessages);
mDeletedLogCount += deletedMessages.size();
}
refreshLogCatTable();
updateUnreadCount(addedMessages);
refreshFiltersTable();
}
private void reloadLogBuffer() {
mTable.removeAll();
synchronized (mLogBuffer) {
mLogBuffer.clear();
mDeletedLogCount = 0;
}
if (mReceiver == null || mReceiver.getMessages() == null) {
return;
}
List<LogCatMessage> addedMessages = mReceiver.getMessages().getAllMessages();
List<LogCatMessage> deletedMessages = Collections.emptyList();
bufferChanged(addedMessages, deletedMessages);
}
/**
* 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) {
mCurrentRefresher = new LogCatTableRefresherTask();
Display.getDefault().asyncExec(mCurrentRefresher);
}
}
}
/**
* The {@link LogCatTableRefresherTask} takes care of refreshing the table with the
* new log messages that have been received. Since the log behaves like a circular buffer,
* the first step is to remove items from the top of the table (if necessary). This step
* is complicated by the fact that a single log message may span multiple rows if the message
* was wrapped. Once the deleted items are removed, the new messages are added to the bottom
* of the table. If scroll lock is enabled, the item that was original visible is made visible
* again, if not, the last item is made visible.
*/
private class LogCatTableRefresherTask implements Runnable {
@Override
public void run() {
if (mTable.isDisposed()) {
return;
}
synchronized (LogCatPanel.this) {
mCurrentRefresher = null;
}
// Current topIndex so that it can be restored if scroll locked.
int topIndex = mTable.getTopIndex();
mTable.setRedraw(false);
// Obtain the list of new messages, and the number of deleted messages.
List<LogCatMessage> newMessages;
int deletedMessageCount;
synchronized (mLogBuffer) {
newMessages = new ArrayList<LogCatMessage>(mLogBuffer);
mLogBuffer.clear();
deletedMessageCount = mDeletedLogCount;
mDeletedLogCount = 0;
mFindTarget.scrollBy(deletedMessageCount);
}
int originalItemCount = mTable.getItemCount();
// Remove entries from the start of the table if they were removed in the log buffer
// This is complicated by the fact that a single message may span multiple TableItems
// if it was word-wrapped.
deletedMessageCount -= removeFromTable(mTable, deletedMessageCount);
// Compute number of table items that were deleted from the table.
int deletedItemCount = originalItemCount - mTable.getItemCount();
// If there are more messages to delete (after deleting messages from the table),
// then delete them from the start of the newly added messages list
if (deletedMessageCount > 0) {
assert deletedMessageCount < newMessages.size();
for (int i = 0; i < deletedMessageCount; i++) {
newMessages.remove(0);
}
}
// Add the remaining messages to the table.
for (LogCatMessage m: newMessages) {
List<String> wrappedMessageList = wrapMessage(m.getMessage(), mWrapWidthInChars);
Color c = getForegroundColor(m);
for (int i = 0; i < wrappedMessageList.size(); i++) {
TableItem item = new TableItem(mTable, SWT.NONE);
if (i == 0) {
// Only set the message data in the first item. This allows code that
// examines the table item data (such as copy selection) to distinguish
// between real messages versus lines that are really just wrapped
// content from the previous message.
item.setData(m);
item.setText(new String[] {
Character.toString(m.getLogLevel().getPriorityLetter()),
m.getTime(),
m.getPid(),
m.getTid(),
m.getAppName(),
m.getTag(),
wrappedMessageList.get(i)
});
} else {
item.setText(new String[] {
"", "", "", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
"", "", "", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
wrappedMessageList.get(i)
});
}
item.setForeground(c);
item.setFont(mFont);
}
}
if (mShouldScrollToLatestLog) {
scrollToLatestLog();
} else {
// If scroll locked, show the same item that was original visible in the table.
int index = Math.max(topIndex - deletedItemCount, 0);
mTable.setTopIndex(index);
}
mTable.setRedraw(true);
}
/**
* Removes given number of messages from the table, starting at the top of the table.
* Note that the number of messages deleted is not equal to the number of rows
* deleted since a single message could span multiple rows. This method first calculates
* the number of rows that correspond to the number of messages to delete, and then
* removes all those rows.
* @param table table from which messages should be removed
* @param msgCount number of messages to be removed
* @return number of messages that were actually removed
*/
private int removeFromTable(Table table, int msgCount) {
int deletedMessageCount = 0; // # of messages that have been deleted
int lastItemToDelete = 0; // index of the last item that should be deleted
while (deletedMessageCount < msgCount && lastItemToDelete < table.getItemCount()) {
// only rows that begin a message have their item data set
TableItem item = table.getItem(lastItemToDelete);
if (item.getData() != null) {
deletedMessageCount++;
}
lastItemToDelete++;
}
// If there are any table items left over at the end that are wrapped over from the
// previous message, mark them for deletion as well.
if (lastItemToDelete < table.getItemCount()
&& table.getItem(lastItemToDelete).getData() == null) {
lastItemToDelete++;
}
table.remove(0, lastItemToDelete - 1);
return deletedMessageCount;
}
}
/** Scroll to the last line. */
private void scrollToLatestLog() {
mTable.setTopIndex(mTable.getItemCount() - 1);
}
/**
* Splits the message into multiple lines if the message length exceeds given width.
* If the message was split, then a wrap character \u23ce is appended to the end of all
* lines but the last one.
*/
private List<String> wrapMessage(String msg, int wrapWidth) {
if (msg.length() < wrapWidth) {
return Collections.singletonList(msg);
}
List<String> wrappedMessages = new ArrayList<String>();
int offset = 0;
int len = msg.length();
while (len > 0) {
int copylen = Math.min(wrapWidth, len);
String s = msg.substring(offset, offset + copylen);
offset += copylen;
len -= copylen;
if (len > 0) { // if there are more lines following, then append a wrap marker
s += " \u23ce"; //$NON-NLS-1$
}
wrappedMessages.add(s);
}
return wrappedMessages;
}
private Color getForegroundColor(LogCatMessage m) {
LogLevel l = m.getLogLevel();
if (l.equals(LogLevel.VERBOSE)) {
return mVerboseColor;
} else if (l.equals(LogLevel.INFO)) {
return mInfoColor;
} else if (l.equals(LogLevel.DEBUG)) {
return mDebugColor;
} else if (l.equals(LogLevel.ERROR)) {
return mErrorColor;
} else if (l.equals(LogLevel.WARN)) {
return mWarnColor;
} else if (l.equals(LogLevel.ASSERT)) {
return mAssertColor;
}
return mVerboseColor;
}
private List<ILogCatMessageSelectionListener> mMessageSelectionListeners;
private void initDoubleClickListener() {
mMessageSelectionListeners = new ArrayList<ILogCatMessageSelectionListener>(1);
mTable.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 IFocusedTableActivator activator = new IFocusedTableActivator() {
@Override
public void copy(Clipboard clipboard) {
copySelectionToClipboard(clipboard);
}
@Override
public void selectAll() {
mTable.selectAll();
}
};
mTable.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() {
mTable.selectAll();
}
private void dispose() {
if (mFont != null && !mFont.isDisposed()) {
mFont.dispose();
}
if (mVerboseColor != null && !mVerboseColor.isDisposed()) {
disposeMessageColors();
}
}
private void disposeMessageColors() {
mVerboseColor.dispose();
mDebugColor.dispose();
mInfoColor.dispose();
mWarnColor.dispose();
mErrorColor.dispose();
mAssertColor.dispose();
}
private class LogcatFindTarget extends AbstractBufferFindTarget {
@Override
public void selectAndReveal(int index) {
mTable.deselectAll();
mTable.select(index);
mTable.showSelection();
}
@Override
public int getItemCount() {
return mTable.getItemCount();
}
@Override
public String getItem(int index) {
Object data = mTable.getItem(index).getData();
if (data != null) {
return data.toString();
}
return null;
}
@Override
public int getStartingIndex() {
// start searches from current selection if present, otherwise from the tail end
// of the buffer
int s = mTable.getSelectionIndex();
if (s != -1) {
return s;
} else {
return getItemCount() - 1;
}
};
};
private FindDialog mFindDialog;
private LogcatFindTarget mFindTarget = new LogcatFindTarget();
public void showFindDialog() {
if (mFindDialog != null) {
// if the dialog is already displayed
return;
}
mFindDialog = new FindDialog(Display.getDefault().getActiveShell(), mFindTarget);
mFindDialog.open(); // blocks until find dialog is closed
mFindDialog = null;
}
}