OSDN Git Service

original
[gb-231r1-is01/Gingerbread_2.3.3_r1_IS01.git] / sdk / ddms / libs / ddmuilib / src / com / android / ddmuilib / logcat / LogPanel.java
1 /*
2  * Copyright (C) 2007 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.IDevice;
20 import com.android.ddmlib.Log;
21 import com.android.ddmlib.MultiLineReceiver;
22 import com.android.ddmlib.Log.LogLevel;
23 import com.android.ddmuilib.DdmUiPreferences;
24 import com.android.ddmuilib.ITableFocusListener;
25 import com.android.ddmuilib.SelectionDependentPanel;
26 import com.android.ddmuilib.TableHelper;
27 import com.android.ddmuilib.ITableFocusListener.IFocusedTableActivator;
28 import com.android.ddmuilib.actions.ICommonAction;
29
30 import org.eclipse.jface.preference.IPreferenceStore;
31 import org.eclipse.swt.SWT;
32 import org.eclipse.swt.SWTException;
33 import org.eclipse.swt.dnd.Clipboard;
34 import org.eclipse.swt.dnd.TextTransfer;
35 import org.eclipse.swt.dnd.Transfer;
36 import org.eclipse.swt.events.ControlEvent;
37 import org.eclipse.swt.events.ControlListener;
38 import org.eclipse.swt.events.FocusEvent;
39 import org.eclipse.swt.events.FocusListener;
40 import org.eclipse.swt.events.ModifyEvent;
41 import org.eclipse.swt.events.ModifyListener;
42 import org.eclipse.swt.events.SelectionAdapter;
43 import org.eclipse.swt.events.SelectionEvent;
44 import org.eclipse.swt.graphics.Font;
45 import org.eclipse.swt.graphics.Rectangle;
46 import org.eclipse.swt.layout.FillLayout;
47 import org.eclipse.swt.layout.GridData;
48 import org.eclipse.swt.layout.GridLayout;
49 import org.eclipse.swt.widgets.Composite;
50 import org.eclipse.swt.widgets.Control;
51 import org.eclipse.swt.widgets.Display;
52 import org.eclipse.swt.widgets.FileDialog;
53 import org.eclipse.swt.widgets.Label;
54 import org.eclipse.swt.widgets.TabFolder;
55 import org.eclipse.swt.widgets.TabItem;
56 import org.eclipse.swt.widgets.Table;
57 import org.eclipse.swt.widgets.TableColumn;
58 import org.eclipse.swt.widgets.TableItem;
59 import org.eclipse.swt.widgets.Text;
60
61 import java.io.FileWriter;
62 import java.io.IOException;
63 import java.util.ArrayList;
64 import java.util.Arrays;
65 import java.util.regex.Matcher;
66 import java.util.regex.Pattern;
67
68 public class LogPanel extends SelectionDependentPanel {
69
70     private static final int STRING_BUFFER_LENGTH = 10000;
71
72     /** no filtering. Only one tab with everything. */
73     public static final int FILTER_NONE = 0;
74     /** manual mode for filter. all filters are manually created. */
75     public static final int FILTER_MANUAL = 1;
76     /** automatic mode for filter (pid mode).
77      * All filters are automatically created. */
78     public static final int FILTER_AUTO_PID = 2;
79     /** automatic mode for filter (tag mode).
80      * All filters are automatically created. */
81     public static final int FILTER_AUTO_TAG = 3;
82     /** Manual filtering mode + new filter for debug app, if needed */
83     public static final int FILTER_DEBUG = 4;
84
85     public static final int COLUMN_MODE_MANUAL = 0;
86     public static final int COLUMN_MODE_AUTO = 1;
87
88     public static String PREFS_TIME;
89     public static String PREFS_LEVEL;
90     public static String PREFS_PID;
91     public static String PREFS_TAG;
92     public static String PREFS_MESSAGE;
93
94     /**
95      * This pattern is meant to parse the first line of a log message with the option
96      * 'logcat -v long'. The first line represents the date, tag, severity, etc.. while the
97      * following lines are the message (can be several line).<br>
98      * This first line looks something like<br>
99      * <code>"[ 00-00 00:00:00.000 &lt;pid&gt;:0x&lt;???&gt; &lt;severity&gt;/&lt;tag&gt;]"</code>
100      * <br>
101      * Note: severity is one of V, D, I, W, or EM<br>
102      * Note: the fraction of second value can have any number of digit.
103      * Note the tag should be trim as it may have spaces at the end.
104      */
105     private static Pattern sLogPattern = Pattern.compile(
106             "^\\[\\s(\\d\\d-\\d\\d\\s\\d\\d:\\d\\d:\\d\\d\\.\\d+)" + //$NON-NLS-1$
107             "\\s+(\\d*):(0x[0-9a-fA-F]+)\\s([VDIWE])/(.*)\\]$"); //$NON-NLS-1$
108
109     /**
110      * Interface for Storage Filter manager. Implementation of this interface
111      * provide a custom way to archive an reload filters.
112      */
113     public interface ILogFilterStorageManager {
114
115         public LogFilter[] getFilterFromStore();
116
117         public void saveFilters(LogFilter[] filters);
118
119         public boolean requiresDefaultFilter();
120     }
121
122     private Composite mParent;
123     private IPreferenceStore mStore;
124
125     /** top object in the view */
126     private TabFolder mFolders;
127
128     private LogColors mColors;
129
130     private ILogFilterStorageManager mFilterStorage;
131
132     private LogCatOuputReceiver mCurrentLogCat;
133
134     /**
135      * Circular buffer containing the logcat output. This is unfiltered.
136      * The valid content goes from <code>mBufferStart</code> to
137      * <code>mBufferEnd - 1</code>. Therefore its number of item is
138      * <code>mBufferEnd - mBufferStart</code>.
139      */
140     private LogMessage[] mBuffer = new LogMessage[STRING_BUFFER_LENGTH];
141
142     /** Represents the oldest message in the buffer */
143     private int mBufferStart = -1;
144
145     /**
146      * Represents the next usable item in the buffer to receive new message.
147      * This can be equal to mBufferStart, but when used mBufferStart will be
148      * incremented as well.
149      */
150     private int mBufferEnd = -1;
151
152     /** Filter list */
153     private LogFilter[] mFilters;
154
155     /** Default filter */
156     private LogFilter mDefaultFilter;
157
158     /** Current filter being displayed */
159     private LogFilter mCurrentFilter;
160
161     /** Filtering mode */
162     private int mFilterMode = FILTER_NONE;
163
164     /** Device currently running logcat */
165     private IDevice mCurrentLoggedDevice = null;
166
167     private ICommonAction mDeleteFilterAction;
168     private ICommonAction mEditFilterAction;
169
170     private ICommonAction[] mLogLevelActions;
171
172     /** message data, separated from content for multi line messages */
173     protected static class LogMessageInfo {
174         public LogLevel logLevel;
175         public int pid;
176         public String pidString;
177         public String tag;
178         public String time;
179     }
180
181     /** pointer to the latest LogMessageInfo. this is used for multi line
182      * log message, to reuse the info regarding level, pid, etc...
183      */
184     private LogMessageInfo mLastMessageInfo = null;
185
186     private boolean mPendingAsyncRefresh = false;
187
188     private String mDefaultLogSave;
189
190     private int mColumnMode = COLUMN_MODE_MANUAL;
191     private Font mDisplayFont;
192
193     private ITableFocusListener mGlobalListener;
194
195     /** message data, separated from content for multi line messages */
196     protected static class LogMessage {
197         public LogMessageInfo data;
198         public String msg;
199
200         @Override
201         public String toString() {
202             return data.time + ": " //$NON-NLS-1$
203                 + data.logLevel + "/" //$NON-NLS-1$
204                 + data.tag + "(" //$NON-NLS-1$
205                 + data.pidString + "): " //$NON-NLS-1$
206                 + msg;
207         }
208     }
209
210     /**
211      * objects able to receive the output of a remote shell command,
212      * specifically a logcat command in this case
213      */
214     private final class LogCatOuputReceiver extends MultiLineReceiver {
215
216         public boolean isCancelled = false;
217
218         public LogCatOuputReceiver() {
219             super();
220
221             setTrimLine(false);
222         }
223
224         @Override
225         public void processNewLines(String[] lines) {
226             if (isCancelled == false) {
227                 processLogLines(lines);
228             }
229         }
230
231         public boolean isCancelled() {
232             return isCancelled;
233         }
234     }
235
236     /**
237      * Parser class for the output of a "ps" shell command executed on a device.
238      * This class looks for a specific pid to find the process name from it.
239      * Once found, the name is used to update a filter and a tab object
240      *
241      */
242     private class PsOutputReceiver extends MultiLineReceiver {
243
244         private LogFilter mFilter;
245
246         private TabItem mTabItem;
247
248         private int mPid;
249
250         /** set to true when we've found the pid we're looking for */
251         private boolean mDone = false;
252
253         PsOutputReceiver(int pid, LogFilter filter, TabItem tabItem) {
254             mPid = pid;
255             mFilter = filter;
256             mTabItem = tabItem;
257         }
258
259         public boolean isCancelled() {
260             return mDone;
261         }
262
263         @Override
264         public void processNewLines(String[] lines) {
265             for (String line : lines) {
266                 if (line.startsWith("USER")) { //$NON-NLS-1$
267                     continue;
268                 }
269                 // get the pid.
270                 int index = line.indexOf(' ');
271                 if (index == -1) {
272                     continue;
273                 }
274                 // look for the next non blank char
275                 index++;
276                 while (line.charAt(index) == ' ') {
277                     index++;
278                 }
279
280                 // this is the start of the pid.
281                 // look for the end.
282                 int index2 = line.indexOf(' ', index);
283
284                 // get the line
285                 String pidStr = line.substring(index, index2);
286                 int pid = Integer.parseInt(pidStr);
287                 if (pid != mPid) {
288                     continue;
289                 } else {
290                     // get the process name
291                     index = line.lastIndexOf(' ');
292                     final String name = line.substring(index + 1);
293
294                     mFilter.setName(name);
295
296                     // update the tab
297                     Display d = mFolders.getDisplay();
298                     d.asyncExec(new Runnable() {
299                        public void run() {
300                            mTabItem.setText(name);
301                        }
302                     });
303
304                     // we're done with this ps.
305                     mDone = true;
306                     return;
307                 }
308             }
309         }
310
311     }
312
313
314     /**
315      * Create the log view with some default parameters
316      * @param colors The display color object
317      * @param filterStorage the storage for user defined filters.
318      * @param mode The filtering mode
319      */
320     public LogPanel(LogColors colors,
321             ILogFilterStorageManager filterStorage, int mode) {
322         mColors = colors;
323         mFilterMode = mode;
324         mFilterStorage = filterStorage;
325         mStore = DdmUiPreferences.getStore();
326     }
327
328     public void setActions(ICommonAction deleteAction, ICommonAction editAction,
329             ICommonAction[] logLevelActions) {
330         mDeleteFilterAction = deleteAction;
331         mEditFilterAction = editAction;
332         mLogLevelActions = logLevelActions;
333     }
334
335     /**
336      * Sets the column mode. Must be called before creatUI
337      * @param mode the column mode. Valid values are COLUMN_MOD_MANUAL and
338      *  COLUMN_MODE_AUTO
339      */
340     public void setColumnMode(int mode) {
341         mColumnMode  = mode;
342     }
343
344     /**
345      * Sets the display font.
346      * @param font The display font.
347      */
348     public void setFont(Font font) {
349         mDisplayFont = font;
350
351         if (mFilters != null) {
352             for (LogFilter f : mFilters) {
353                 Table table = f.getTable();
354                 if (table != null) {
355                     table.setFont(font);
356                 }
357             }
358         }
359
360         if (mDefaultFilter != null) {
361             Table table = mDefaultFilter.getTable();
362             if (table != null) {
363                 table.setFont(font);
364             }
365         }
366     }
367
368     /**
369      * Sent when a new device is selected. The new device can be accessed
370      * with {@link #getCurrentDevice()}.
371      */
372     @Override
373     public void deviceSelected() {
374         startLogCat(getCurrentDevice());
375     }
376
377     /**
378      * Sent when a new client is selected. The new client can be accessed
379      * with {@link #getCurrentClient()}.
380      */
381     @Override
382     public void clientSelected() {
383         // pass
384     }
385
386
387     /**
388      * Creates a control capable of displaying some information.  This is
389      * called once, when the application is initializing, from the UI thread.
390      */
391     @Override
392     protected Control createControl(Composite parent) {
393         mParent = parent;
394
395         Composite top = new Composite(parent, SWT.NONE);
396         top.setLayoutData(new GridData(GridData.FILL_BOTH));
397         top.setLayout(new GridLayout(1, false));
398
399         // create the tab folder
400         mFolders = new TabFolder(top, SWT.NONE);
401         mFolders.setLayoutData(new GridData(GridData.FILL_BOTH));
402         mFolders.addSelectionListener(new SelectionAdapter() {
403             @Override
404             public void widgetSelected(SelectionEvent e) {
405                 if (mCurrentFilter != null) {
406                     mCurrentFilter.setSelectedState(false);
407                 }
408                 mCurrentFilter = getCurrentFilter();
409                 mCurrentFilter.setSelectedState(true);
410                 updateColumns(mCurrentFilter.getTable());
411                 if (mCurrentFilter.getTempFilterStatus()) {
412                     initFilter(mCurrentFilter);
413                 }
414                 selectionChanged(mCurrentFilter);
415             }
416         });
417
418
419         Composite bottom = new Composite(top, SWT.NONE);
420         bottom.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
421         bottom.setLayout(new GridLayout(3, false));
422
423         Label label = new Label(bottom, SWT.NONE);
424         label.setText("Filter:");
425
426         final Text filterText = new Text(bottom, SWT.SINGLE | SWT.BORDER);
427         filterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
428         filterText.addModifyListener(new ModifyListener() {
429             public void modifyText(ModifyEvent e) {
430                 updateFilteringWith(filterText.getText());
431             }
432         });
433
434         /*
435         Button addFilterBtn = new Button(bottom, SWT.NONE);
436         addFilterBtn.setImage(mImageLoader.loadImage("add.png", //$NON-NLS-1$
437                 addFilterBtn.getDisplay()));
438         */
439
440         // get the filters
441         createFilters();
442
443         // for each filter, create a tab.
444         int index = 0;
445
446         if (mDefaultFilter != null) {
447             createTab(mDefaultFilter, index++, false);
448         }
449
450         if (mFilters != null) {
451             for (LogFilter f : mFilters) {
452                 createTab(f, index++, false);
453             }
454         }
455
456         return top;
457     }
458
459     @Override
460     protected void postCreation() {
461         // pass
462     }
463
464     /**
465      * Sets the focus to the proper object.
466      */
467     @Override
468     public void setFocus() {
469         mFolders.setFocus();
470     }
471
472
473     /**
474      * Starts a new logcat and set mCurrentLogCat as the current receiver.
475      * @param device the device to connect logcat to.
476      */
477     public void startLogCat(final IDevice device) {
478         if (device == mCurrentLoggedDevice) {
479             return;
480         }
481
482         // if we have a logcat already running
483         if (mCurrentLoggedDevice != null) {
484             stopLogCat(false);
485             mCurrentLoggedDevice = null;
486         }
487
488         resetUI(false);
489
490         if (device != null) {
491             // create a new output receiver
492             mCurrentLogCat = new LogCatOuputReceiver();
493
494             // start the logcat in a different thread
495             new Thread("Logcat")  { //$NON-NLS-1$
496                 @Override
497                 public void run() {
498
499                     while (device.isOnline() == false &&
500                             mCurrentLogCat != null &&
501                             mCurrentLogCat.isCancelled == false) {
502                         try {
503                             sleep(2000);
504                         } catch (InterruptedException e) {
505                             return;
506                         }
507                     }
508
509                     if (mCurrentLogCat == null || mCurrentLogCat.isCancelled) {
510                         // logcat was stopped/cancelled before the device became ready.
511                         return;
512                     }
513
514                     try {
515                         mCurrentLoggedDevice = device;
516                         device.executeShellCommand("logcat -v long", mCurrentLogCat, 0 /*timeout*/); //$NON-NLS-1$
517                     } catch (Exception e) {
518                         Log.e("Logcat", e);
519                     } finally {
520                         // at this point the command is terminated.
521                         mCurrentLogCat = null;
522                         mCurrentLoggedDevice = null;
523                     }
524                 }
525             }.start();
526         }
527     }
528
529     /** Stop the current logcat */
530     public void stopLogCat(boolean inUiThread) {
531         if (mCurrentLogCat != null) {
532             mCurrentLogCat.isCancelled = true;
533
534             // when the thread finishes, no one will reference that object
535             // and it'll be destroyed
536             mCurrentLogCat = null;
537
538             // reset the content buffer
539             for (int i = 0 ; i < STRING_BUFFER_LENGTH; i++) {
540                 mBuffer[i] = null;
541             }
542
543             // because it's a circular buffer, it's hard to know if
544             // the array is empty with both start/end at 0 or if it's full
545             // with both start/end at 0 as well. So to mean empty, we use -1
546             mBufferStart = -1;
547             mBufferEnd = -1;
548
549             resetFilters();
550             resetUI(inUiThread);
551         }
552     }
553
554     /**
555      * Adds a new Filter. This methods displays the UI to create the filter
556      * and set up its parameters.<br>
557      * <b>MUST</b> be called from the ui thread.
558      *
559      */
560     public void addFilter() {
561         EditFilterDialog dlg = new EditFilterDialog(mFolders.getShell());
562         if (dlg.open()) {
563             synchronized (mBuffer) {
564                 // get the new filter in the array
565                 LogFilter filter = dlg.getFilter();
566                 addFilterToArray(filter);
567
568                 int index = mFilters.length - 1;
569                 if (mDefaultFilter != null) {
570                     index++;
571                 }
572
573                 if (false) {
574
575                     for (LogFilter f : mFilters) {
576                         if (f.uiReady()) {
577                             f.dispose();
578                         }
579                     }
580                     if (mDefaultFilter != null && mDefaultFilter.uiReady()) {
581                         mDefaultFilter.dispose();
582                     }
583
584                     // for each filter, create a tab.
585                     int i = 0;
586                     if (mFilters != null) {
587                         for (LogFilter f : mFilters) {
588                             createTab(f, i++, true);
589                         }
590                     }
591                     if (mDefaultFilter != null) {
592                         createTab(mDefaultFilter, i++, true);
593                     }
594                 } else {
595
596                     // create ui for the filter.
597                     createTab(filter, index, true);
598
599                     // reset the default as it shouldn't contain the content of
600                     // this new filter.
601                     if (mDefaultFilter != null) {
602                         initDefaultFilter();
603                     }
604                 }
605
606                 // select the new filter
607                 if (mCurrentFilter != null) {
608                     mCurrentFilter.setSelectedState(false);
609                 }
610                 mFolders.setSelection(index);
611                 filter.setSelectedState(true);
612                 mCurrentFilter = filter;
613
614                 selectionChanged(filter);
615
616                 // finally we update the filtering mode if needed
617                 if (mFilterMode == FILTER_NONE) {
618                     mFilterMode = FILTER_MANUAL;
619                 }
620
621                 mFilterStorage.saveFilters(mFilters);
622
623             }
624         }
625     }
626
627     /**
628      * Edits the current filter. The method displays the UI to edit the filter.
629      */
630     public void editFilter() {
631         if (mCurrentFilter != null && mCurrentFilter != mDefaultFilter) {
632             EditFilterDialog dlg = new EditFilterDialog(
633                     mFolders.getShell(), mCurrentFilter);
634             if (dlg.open()) {
635                 synchronized (mBuffer) {
636                     // at this point the filter has been updated.
637                     // so we update its content
638                     initFilter(mCurrentFilter);
639
640                     // and the content of the "other" filter as well.
641                     if (mDefaultFilter != null) {
642                         initDefaultFilter();
643                     }
644
645                     mFilterStorage.saveFilters(mFilters);
646                 }
647             }
648         }
649     }
650
651     /**
652      * Deletes the current filter.
653      */
654     public void deleteFilter() {
655         synchronized (mBuffer) {
656             if (mCurrentFilter != null && mCurrentFilter != mDefaultFilter) {
657                 // remove the filter from the list
658                 removeFilterFromArray(mCurrentFilter);
659                 mCurrentFilter.dispose();
660
661                 // select the new filter
662                 mFolders.setSelection(0);
663                 if (mFilters.length > 0) {
664                     mCurrentFilter = mFilters[0];
665                 } else {
666                     mCurrentFilter = mDefaultFilter;
667                 }
668
669                 selectionChanged(mCurrentFilter);
670
671                 // update the content of the "other" filter to include what was filtered out
672                 // by the deleted filter.
673                 if (mDefaultFilter != null) {
674                     initDefaultFilter();
675                 }
676
677                 mFilterStorage.saveFilters(mFilters);
678             }
679         }
680     }
681
682     /**
683      * saves the current selection in a text file.
684      * @return false if the saving failed.
685      */
686     public boolean save() {
687         synchronized (mBuffer) {
688             FileDialog dlg = new FileDialog(mParent.getShell(), SWT.SAVE);
689             String fileName;
690
691             dlg.setText("Save log...");
692             dlg.setFileName("log.txt");
693             String defaultPath = mDefaultLogSave;
694             if (defaultPath == null) {
695                 defaultPath = System.getProperty("user.home"); //$NON-NLS-1$
696             }
697             dlg.setFilterPath(defaultPath);
698             dlg.setFilterNames(new String[] {
699                 "Text Files (*.txt)"
700             });
701             dlg.setFilterExtensions(new String[] {
702                 "*.txt"
703             });
704
705             fileName = dlg.open();
706             if (fileName != null) {
707                 mDefaultLogSave = dlg.getFilterPath();
708
709                 // get the current table and its selection
710                 Table currentTable = mCurrentFilter.getTable();
711
712                 int[] selection = currentTable.getSelectionIndices();
713
714                 // we need to sort the items to be sure.
715                 Arrays.sort(selection);
716
717                 // loop on the selection and output the file.
718                 try {
719                     FileWriter writer = new FileWriter(fileName);
720
721                     for (int i : selection) {
722                         TableItem item = currentTable.getItem(i);
723                         LogMessage msg = (LogMessage)item.getData();
724                         String line = msg.toString();
725                         writer.write(line);
726                         writer.write('\n');
727                     }
728                     writer.flush();
729
730                 } catch (IOException e) {
731                     return false;
732                 }
733             }
734         }
735
736         return true;
737     }
738
739     /**
740      * Empty the current circular buffer.
741      */
742     public void clear() {
743         synchronized (mBuffer) {
744             for (int i = 0 ; i < STRING_BUFFER_LENGTH; i++) {
745                 mBuffer[i] = null;
746             }
747
748             mBufferStart = -1;
749             mBufferEnd = -1;
750
751             // now we clear the existing filters
752             for (LogFilter filter : mFilters) {
753                 filter.clear();
754             }
755
756             // and the default one
757             if (mDefaultFilter != null) {
758                 mDefaultFilter.clear();
759             }
760         }
761     }
762
763     /**
764      * Copies the current selection of the current filter as multiline text.
765      *
766      * @param clipboard The clipboard to place the copied content.
767      */
768     public void copy(Clipboard clipboard) {
769         // get the current table and its selection
770         Table currentTable = mCurrentFilter.getTable();
771
772         copyTable(clipboard, currentTable);
773     }
774
775     /**
776      * Selects all lines.
777      */
778     public void selectAll() {
779         Table currentTable = mCurrentFilter.getTable();
780         currentTable.selectAll();
781     }
782
783     /**
784      * Sets a TableFocusListener which will be notified when one of the tables
785      * gets or loses focus.
786      *
787      * @param listener
788      */
789     public void setTableFocusListener(ITableFocusListener listener) {
790         // record the global listener, to make sure table created after
791         // this call will still be setup.
792         mGlobalListener = listener;
793
794         // now we setup the existing filters
795         for (LogFilter filter : mFilters) {
796             Table table = filter.getTable();
797
798             addTableToFocusListener(table);
799         }
800
801         // and the default one
802         if (mDefaultFilter != null) {
803             addTableToFocusListener(mDefaultFilter.getTable());
804         }
805     }
806
807     /**
808      * Sets up a Table object to notify the global Table Focus listener when it
809      * gets or loses the focus.
810      *
811      * @param table the Table object.
812      */
813     private void addTableToFocusListener(final Table table) {
814         // create the activator for this table
815         final IFocusedTableActivator activator = new IFocusedTableActivator() {
816             public void copy(Clipboard clipboard) {
817                 copyTable(clipboard, table);
818             }
819
820             public void selectAll() {
821                 table.selectAll();
822             }
823         };
824
825         // add the focus listener on the table to notify the global listener
826         table.addFocusListener(new FocusListener() {
827             public void focusGained(FocusEvent e) {
828                 mGlobalListener.focusGained(activator);
829             }
830
831             public void focusLost(FocusEvent e) {
832                 mGlobalListener.focusLost(activator);
833             }
834         });
835     }
836
837     /**
838      * Copies the current selection of a Table into the provided Clipboard, as
839      * multi-line text.
840      *
841      * @param clipboard The clipboard to place the copied content.
842      * @param table The table to copy from.
843      */
844     private static void copyTable(Clipboard clipboard, Table table) {
845         int[] selection = table.getSelectionIndices();
846
847         // we need to sort the items to be sure.
848         Arrays.sort(selection);
849
850         // all lines must be concatenated.
851         StringBuilder sb = new StringBuilder();
852
853         // loop on the selection and output the file.
854         for (int i : selection) {
855             TableItem item = table.getItem(i);
856             LogMessage msg = (LogMessage)item.getData();
857             String line = msg.toString();
858             sb.append(line);
859             sb.append('\n');
860         }
861
862         // now add that to the clipboard
863         clipboard.setContents(new Object[] {
864             sb.toString()
865         }, new Transfer[] {
866             TextTransfer.getInstance()
867         });
868     }
869
870     /**
871      * Sets the log level for the current filter, but does not save it.
872      * @param i
873      */
874     public void setCurrentFilterLogLevel(int i) {
875         LogFilter filter = getCurrentFilter();
876
877         filter.setLogLevel(i);
878
879         initFilter(filter);
880     }
881
882     /**
883      * Creates a new tab in the folderTab item. Must be called from the ui
884      *      thread.
885      * @param filter The filter associated with the tab.
886      * @param index the index of the tab. if -1, the tab will be added at the
887      *          end.
888      * @param fillTable If true the table is filled with the current content of
889      *          the buffer.
890      * @return The TabItem object that was created.
891      */
892     private TabItem createTab(LogFilter filter, int index, boolean fillTable) {
893         synchronized (mBuffer) {
894             TabItem item = null;
895             if (index != -1) {
896                 item = new TabItem(mFolders, SWT.NONE, index);
897             } else {
898                 item = new TabItem(mFolders, SWT.NONE);
899             }
900             item.setText(filter.getName());
901
902             // set the control (the parent is the TabFolder item, always)
903             Composite top = new Composite(mFolders, SWT.NONE);
904             item.setControl(top);
905
906             top.setLayout(new FillLayout());
907
908             // create the ui, first the table
909             final Table t = new Table(top, SWT.MULTI | SWT.FULL_SELECTION);
910
911             if (mDisplayFont != null) {
912                 t.setFont(mDisplayFont);
913             }
914
915             // give the ui objects to the filters.
916             filter.setWidgets(item, t);
917
918             t.setHeaderVisible(true);
919             t.setLinesVisible(false);
920
921             if (mGlobalListener != null) {
922                 addTableToFocusListener(t);
923             }
924
925             // create a controllistener that will handle the resizing of all the
926             // columns (except the last) and of the table itself.
927             ControlListener listener = null;
928             if (mColumnMode == COLUMN_MODE_AUTO) {
929                 listener = new ControlListener() {
930                     public void controlMoved(ControlEvent e) {
931                     }
932
933                     public void controlResized(ControlEvent e) {
934                         Rectangle r = t.getClientArea();
935
936                         // get the size of all but the last column
937                         int total = t.getColumn(0).getWidth();
938                         total += t.getColumn(1).getWidth();
939                         total += t.getColumn(2).getWidth();
940                         total += t.getColumn(3).getWidth();
941
942                         if (r.width > total) {
943                             t.getColumn(4).setWidth(r.width-total);
944                         }
945                     }
946                 };
947
948                 t.addControlListener(listener);
949             }
950
951             // then its column
952             TableColumn col = TableHelper.createTableColumn(t, "Time", SWT.LEFT,
953                     "00-00 00:00:00", //$NON-NLS-1$
954                     PREFS_TIME, mStore);
955             if (mColumnMode == COLUMN_MODE_AUTO) {
956                 col.addControlListener(listener);
957             }
958
959             col = TableHelper.createTableColumn(t, "", SWT.CENTER,
960                     "D", //$NON-NLS-1$
961                     PREFS_LEVEL, mStore);
962             if (mColumnMode == COLUMN_MODE_AUTO) {
963                 col.addControlListener(listener);
964             }
965
966             col = TableHelper.createTableColumn(t, "pid", SWT.LEFT,
967                     "9999", //$NON-NLS-1$
968                     PREFS_PID, mStore);
969             if (mColumnMode == COLUMN_MODE_AUTO) {
970                 col.addControlListener(listener);
971             }
972
973             col = TableHelper.createTableColumn(t, "tag", SWT.LEFT,
974                     "abcdefgh",  //$NON-NLS-1$
975                     PREFS_TAG, mStore);
976             if (mColumnMode == COLUMN_MODE_AUTO) {
977                 col.addControlListener(listener);
978             }
979
980             col = TableHelper.createTableColumn(t, "Message", SWT.LEFT,
981                     "abcdefghijklmnopqrstuvwxyz0123456789",  //$NON-NLS-1$
982                     PREFS_MESSAGE, mStore);
983             if (mColumnMode == COLUMN_MODE_AUTO) {
984                 // instead of listening on resize for the last column, we make
985                 // it non resizable.
986                 col.setResizable(false);
987             }
988
989             if (fillTable) {
990                 initFilter(filter);
991             }
992             return item;
993         }
994     }
995
996     protected void updateColumns(Table table) {
997         if (table != null) {
998             int index = 0;
999             TableColumn col;
1000
1001             col = table.getColumn(index++);
1002             col.setWidth(mStore.getInt(PREFS_TIME));
1003
1004             col = table.getColumn(index++);
1005             col.setWidth(mStore.getInt(PREFS_LEVEL));
1006
1007             col = table.getColumn(index++);
1008             col.setWidth(mStore.getInt(PREFS_PID));
1009
1010             col = table.getColumn(index++);
1011             col.setWidth(mStore.getInt(PREFS_TAG));
1012
1013             col = table.getColumn(index++);
1014             col.setWidth(mStore.getInt(PREFS_MESSAGE));
1015         }
1016     }
1017
1018     public void resetUI(boolean inUiThread) {
1019         if (mFilterMode == FILTER_AUTO_PID || mFilterMode == FILTER_AUTO_TAG) {
1020             if (inUiThread) {
1021                 mFolders.dispose();
1022                 mParent.pack(true);
1023                 createControl(mParent);
1024             } else {
1025                 Display d = mFolders.getDisplay();
1026
1027                 // run sync as we need to update right now.
1028                 d.syncExec(new Runnable() {
1029                     public void run() {
1030                         mFolders.dispose();
1031                         mParent.pack(true);
1032                         createControl(mParent);
1033                     }
1034                 });
1035             }
1036         } else  {
1037             // the ui is static we just empty it.
1038             if (mFolders.isDisposed() == false) {
1039                 if (inUiThread) {
1040                     emptyTables();
1041                 } else {
1042                     Display d = mFolders.getDisplay();
1043
1044                     // run sync as we need to update right now.
1045                     d.syncExec(new Runnable() {
1046                         public void run() {
1047                             if (mFolders.isDisposed() == false) {
1048                                 emptyTables();
1049                             }
1050                         }
1051                     });
1052                 }
1053             }
1054         }
1055     }
1056
1057     /**
1058      * Process new Log lines coming from {@link LogCatOuputReceiver}.
1059      * @param lines the new lines
1060      */
1061     protected void processLogLines(String[] lines) {
1062         // WARNING: this will not work if the string contains more line than
1063         // the buffer holds.
1064
1065         if (lines.length > STRING_BUFFER_LENGTH) {
1066             Log.e("LogCat", "Receiving more lines than STRING_BUFFER_LENGTH");
1067         }
1068
1069         // parse the lines and create LogMessage that are stored in a temporary list
1070         final ArrayList<LogMessage> newMessages = new ArrayList<LogMessage>();
1071
1072         synchronized (mBuffer) {
1073             for (String line : lines) {
1074                 // ignore empty lines.
1075                 if (line.length() > 0) {
1076                     // check for header lines.
1077                     Matcher matcher = sLogPattern.matcher(line);
1078                     if (matcher.matches()) {
1079                         // this is a header line, parse the header and keep it around.
1080                         mLastMessageInfo = new LogMessageInfo();
1081
1082                         mLastMessageInfo.time = matcher.group(1);
1083                         mLastMessageInfo.pidString = matcher.group(2);
1084                         mLastMessageInfo.pid = Integer.valueOf(mLastMessageInfo.pidString);
1085                         mLastMessageInfo.logLevel = LogLevel.getByLetterString(matcher.group(4));
1086                         mLastMessageInfo.tag = matcher.group(5).trim();
1087                     } else {
1088                         // This is not a header line.
1089                         // Create a new LogMessage and process it.
1090                         LogMessage mc = new LogMessage();
1091
1092                         if (mLastMessageInfo == null) {
1093                             // The first line of output wasn't preceded
1094                             // by a header line; make something up so
1095                             // that users of mc.data don't NPE.
1096                             mLastMessageInfo = new LogMessageInfo();
1097                             mLastMessageInfo.time = "??-?? ??:??:??.???"; //$NON-NLS1$
1098                             mLastMessageInfo.pidString = "<unknown>"; //$NON-NLS1$
1099                             mLastMessageInfo.pid = 0;
1100                             mLastMessageInfo.logLevel = LogLevel.INFO;
1101                             mLastMessageInfo.tag = "<unknown>"; //$NON-NLS1$
1102                         }
1103
1104                         // If someone printed a log message with
1105                         // embedded '\n' characters, there will
1106                         // one header line followed by multiple text lines.
1107                         // Use the last header that we saw.
1108                         mc.data = mLastMessageInfo;
1109
1110                         // tabs seem to display as only 1 tab so we replace the leading tabs
1111                         // by 4 spaces.
1112                         mc.msg = line.replaceAll("\t", "    "); //$NON-NLS-1$ //$NON-NLS-2$
1113
1114                         // process the new LogMessage.
1115                         processNewMessage(mc);
1116
1117                         // store the new LogMessage
1118                         newMessages.add(mc);
1119                     }
1120                 }
1121             }
1122
1123             // if we don't have a pending Runnable that will do the refresh, we ask the Display
1124             // to run one in the UI thread.
1125             if (mPendingAsyncRefresh == false) {
1126                 mPendingAsyncRefresh = true;
1127
1128                 try {
1129                     Display display = mFolders.getDisplay();
1130
1131                     // run in sync because this will update the buffer start/end indices
1132                     display.asyncExec(new Runnable() {
1133                         public void run() {
1134                             asyncRefresh();
1135                         }
1136                     });
1137                 } catch (SWTException e) {
1138                     // display is disposed, we're probably quitting. Let's stop.
1139                     stopLogCat(false);
1140                 }
1141             }
1142         }
1143     }
1144
1145     /**
1146      * Refreshes the UI with new messages.
1147      */
1148     private void asyncRefresh() {
1149         if (mFolders.isDisposed() == false) {
1150             synchronized (mBuffer) {
1151                 try {
1152                     // the circular buffer has been updated, let have the filter flush their
1153                     // display with the new messages.
1154                     if (mFilters != null) {
1155                         for (LogFilter f : mFilters) {
1156                             f.flush();
1157                         }
1158                     }
1159
1160                     if (mDefaultFilter != null) {
1161                         mDefaultFilter.flush();
1162                     }
1163                 } finally {
1164                     // the pending refresh is done.
1165                     mPendingAsyncRefresh = false;
1166                 }
1167             }
1168         } else {
1169             stopLogCat(true);
1170         }
1171     }
1172
1173     /**
1174      * Processes a new Message.
1175      * <p/>This adds the new message to the buffer, and gives it to the existing filters.
1176      * @param newMessage
1177      */
1178     private void processNewMessage(LogMessage newMessage) {
1179         // if we are in auto filtering mode, make sure we have
1180         // a filter for this
1181         if (mFilterMode == FILTER_AUTO_PID ||
1182                 mFilterMode == FILTER_AUTO_TAG) {
1183            checkFilter(newMessage.data);
1184         }
1185
1186         // compute the index where the message goes.
1187         // was the buffer empty?
1188         int messageIndex = -1;
1189         if (mBufferStart == -1) {
1190             messageIndex = mBufferStart = 0;
1191             mBufferEnd = 1;
1192         } else {
1193             messageIndex = mBufferEnd;
1194
1195             // check we aren't overwriting start
1196             if (mBufferEnd == mBufferStart) {
1197                 mBufferStart = (mBufferStart + 1) % STRING_BUFFER_LENGTH;
1198             }
1199
1200             // increment the next usable slot index
1201             mBufferEnd = (mBufferEnd + 1) % STRING_BUFFER_LENGTH;
1202         }
1203
1204         LogMessage oldMessage = null;
1205
1206         // record the message that was there before
1207         if (mBuffer[messageIndex] != null) {
1208             oldMessage = mBuffer[messageIndex];
1209         }
1210
1211         // then add the new one
1212         mBuffer[messageIndex] = newMessage;
1213
1214         // give the new message to every filters.
1215         boolean filtered = false;
1216         if (mFilters != null) {
1217             for (LogFilter f : mFilters) {
1218                 filtered |= f.addMessage(newMessage, oldMessage);
1219             }
1220         }
1221         if (filtered == false && mDefaultFilter != null) {
1222             mDefaultFilter.addMessage(newMessage, oldMessage);
1223         }
1224     }
1225
1226     private void createFilters() {
1227         if (mFilterMode == FILTER_DEBUG || mFilterMode == FILTER_MANUAL) {
1228             // unarchive the filters.
1229             mFilters = mFilterStorage.getFilterFromStore();
1230
1231             // set the colors
1232             if (mFilters != null) {
1233                 for (LogFilter f : mFilters) {
1234                     f.setColors(mColors);
1235                 }
1236             }
1237
1238             if (mFilterStorage.requiresDefaultFilter()) {
1239                 mDefaultFilter = new LogFilter("Log");
1240                 mDefaultFilter.setColors(mColors);
1241                 mDefaultFilter.setSupportsDelete(false);
1242                 mDefaultFilter.setSupportsEdit(false);
1243             }
1244         } else if (mFilterMode == FILTER_NONE) {
1245             // if the filtering mode is "none", we create a single filter that
1246             // will receive all
1247             mDefaultFilter = new LogFilter("Log");
1248             mDefaultFilter.setColors(mColors);
1249             mDefaultFilter.setSupportsDelete(false);
1250             mDefaultFilter.setSupportsEdit(false);
1251         }
1252     }
1253
1254     /** Checks if there's an automatic filter for this md and if not
1255      * adds the filter and the ui.
1256      * This must be called from the UI!
1257      * @param md
1258      * @return true if the filter existed already
1259      */
1260     private boolean checkFilter(final LogMessageInfo md) {
1261         if (true)
1262             return true;
1263         // look for a filter that matches the pid
1264         if (mFilterMode == FILTER_AUTO_PID) {
1265             for (LogFilter f : mFilters) {
1266                 if (f.getPidFilter() == md.pid) {
1267                     return true;
1268                 }
1269             }
1270         } else if (mFilterMode == FILTER_AUTO_TAG) {
1271             for (LogFilter f : mFilters) {
1272                 if (f.getTagFilter().equals(md.tag)) {
1273                     return true;
1274                 }
1275             }
1276         }
1277
1278         // if we reach this point, no filter was found.
1279         // create a filter with a temporary name of the pid
1280         final LogFilter newFilter = new LogFilter(md.pidString);
1281         String name = null;
1282         if (mFilterMode == FILTER_AUTO_PID) {
1283             newFilter.setPidMode(md.pid);
1284
1285             // ask the monitor thread if it knows the pid.
1286             name = mCurrentLoggedDevice.getClientName(md.pid);
1287         } else {
1288             newFilter.setTagMode(md.tag);
1289             name = md.tag;
1290         }
1291         addFilterToArray(newFilter);
1292
1293         final String fname = name;
1294
1295         // create the tabitem
1296         final TabItem newTabItem = createTab(newFilter, -1, true);
1297
1298         // if the name is unknown
1299         if (fname == null) {
1300             // we need to find the process running under that pid.
1301             // launch a thread do a ps on the device
1302             new Thread("remote PS") { //$NON-NLS-1$
1303                 @Override
1304                 public void run() {
1305                     // create the receiver
1306                     PsOutputReceiver psor = new PsOutputReceiver(md.pid,
1307                             newFilter, newTabItem);
1308
1309                     // execute ps
1310                     try {
1311                         mCurrentLoggedDevice.executeShellCommand("ps", psor); //$NON-NLS-1$
1312                     } catch (IOException e) {
1313                         // hmm...
1314                     }
1315                 }
1316             }.start();
1317         }
1318
1319         return false;
1320     }
1321
1322     /**
1323      * Adds a new filter to the current filter array, and set its colors
1324      * @param newFilter The filter to add
1325      */
1326     private void addFilterToArray(LogFilter newFilter) {
1327         // set the colors
1328         newFilter.setColors(mColors);
1329
1330         // add it to the array.
1331         if (mFilters != null && mFilters.length > 0) {
1332             LogFilter[] newFilters = new LogFilter[mFilters.length+1];
1333             System.arraycopy(mFilters, 0, newFilters, 0, mFilters.length);
1334             newFilters[mFilters.length] = newFilter;
1335             mFilters = newFilters;
1336         } else {
1337             mFilters = new LogFilter[1];
1338             mFilters[0] = newFilter;
1339         }
1340     }
1341
1342     private void removeFilterFromArray(LogFilter oldFilter) {
1343         // look for the index
1344         int index = -1;
1345         for (int i = 0 ; i < mFilters.length ; i++) {
1346             if (mFilters[i] == oldFilter) {
1347                 index = i;
1348                 break;
1349             }
1350         }
1351
1352         if (index != -1) {
1353             LogFilter[] newFilters = new LogFilter[mFilters.length-1];
1354             System.arraycopy(mFilters, 0, newFilters, 0, index);
1355             System.arraycopy(mFilters, index + 1, newFilters, index,
1356                     newFilters.length-index);
1357             mFilters = newFilters;
1358         }
1359     }
1360
1361     /**
1362      * Initialize the filter with already existing buffer.
1363      * @param filter
1364      */
1365     private void initFilter(LogFilter filter) {
1366         // is it empty
1367         if (filter.uiReady() == false) {
1368             return;
1369         }
1370
1371         if (filter == mDefaultFilter) {
1372             initDefaultFilter();
1373             return;
1374         }
1375
1376         filter.clear();
1377
1378         if (mBufferStart != -1) {
1379             int max = mBufferEnd;
1380             if (mBufferEnd < mBufferStart) {
1381                 max += STRING_BUFFER_LENGTH;
1382             }
1383
1384             for (int i = mBufferStart; i < max; i++) {
1385                 int realItemIndex = i % STRING_BUFFER_LENGTH;
1386
1387                 filter.addMessage(mBuffer[realItemIndex], null /* old message */);
1388             }
1389         }
1390
1391         filter.flush();
1392         filter.resetTempFilteringStatus();
1393     }
1394
1395     /**
1396      * Refill the default filter. Not to be called directly.
1397      * @see initFilter()
1398      */
1399     private void initDefaultFilter() {
1400         mDefaultFilter.clear();
1401
1402         if (mBufferStart != -1) {
1403             int max = mBufferEnd;
1404             if (mBufferEnd < mBufferStart) {
1405                 max += STRING_BUFFER_LENGTH;
1406             }
1407
1408             for (int i = mBufferStart; i < max; i++) {
1409                 int realItemIndex = i % STRING_BUFFER_LENGTH;
1410                 LogMessage msg = mBuffer[realItemIndex];
1411
1412                 // first we check that the other filters don't take this message
1413                 boolean filtered = false;
1414                 for (LogFilter f : mFilters) {
1415                     filtered |= f.accept(msg);
1416                 }
1417
1418                 if (filtered == false) {
1419                     mDefaultFilter.addMessage(msg, null /* old message */);
1420                 }
1421             }
1422         }
1423
1424         mDefaultFilter.flush();
1425         mDefaultFilter.resetTempFilteringStatus();
1426     }
1427
1428     /**
1429      * Reset the filters, to handle change in device in automatic filter mode
1430      */
1431     private void resetFilters() {
1432         // if we are in automatic mode, then we need to rmove the current
1433         // filter.
1434         if (mFilterMode == FILTER_AUTO_PID || mFilterMode == FILTER_AUTO_TAG) {
1435             mFilters = null;
1436
1437             // recreate the filters.
1438             createFilters();
1439         }
1440     }
1441
1442
1443     private LogFilter getCurrentFilter() {
1444         int index = mFolders.getSelectionIndex();
1445
1446         // if mFilters is null or index is invalid, we return the default
1447         // filter. It doesn't matter if that one is null as well, since we
1448         // would return null anyway.
1449         if (index == 0 || mFilters == null) {
1450             return mDefaultFilter;
1451         }
1452
1453         return mFilters[index-1];
1454     }
1455
1456
1457     private void emptyTables() {
1458         for (LogFilter f : mFilters) {
1459             f.getTable().removeAll();
1460         }
1461
1462         if (mDefaultFilter != null) {
1463             mDefaultFilter.getTable().removeAll();
1464         }
1465     }
1466
1467     protected void updateFilteringWith(String text) {
1468         synchronized (mBuffer) {
1469             // reset the temp filtering for all the filters
1470             for (LogFilter f : mFilters) {
1471                 f.resetTempFiltering();
1472             }
1473             if (mDefaultFilter != null) {
1474                 mDefaultFilter.resetTempFiltering();
1475             }
1476
1477             // now we need to figure out the new temp filtering
1478             // split each word
1479             String[] segments = text.split(" "); //$NON-NLS-1$
1480
1481             ArrayList<String> keywords = new ArrayList<String>(segments.length);
1482
1483             // loop and look for temp id/tag
1484             int tempPid = -1;
1485             String tempTag = null;
1486             for (int i = 0 ; i < segments.length; i++) {
1487                 String s = segments[i];
1488                 if (tempPid == -1 && s.startsWith("pid:")) { //$NON-NLS-1$
1489                     // get the pid
1490                     String[] seg = s.split(":"); //$NON-NLS-1$
1491                     if (seg.length == 2) {
1492                         if (seg[1].matches("^[0-9]*$")) { //$NON-NLS-1$
1493                             tempPid = Integer.valueOf(seg[1]);
1494                         }
1495                     }
1496                 } else if (tempTag == null && s.startsWith("tag:")) { //$NON-NLS-1$
1497                     String seg[] = segments[i].split(":"); //$NON-NLS-1$
1498                     if (seg.length == 2) {
1499                         tempTag = seg[1];
1500                     }
1501                 } else {
1502                     keywords.add(s);
1503                 }
1504             }
1505
1506             // set the temp filtering in the filters
1507             if (tempPid != -1 || tempTag != null || keywords.size() > 0) {
1508                 String[] keywordsArray = keywords.toArray(
1509                         new String[keywords.size()]);
1510
1511                 for (LogFilter f : mFilters) {
1512                     if (tempPid != -1) {
1513                         f.setTempPidFiltering(tempPid);
1514                     }
1515                     if (tempTag != null) {
1516                         f.setTempTagFiltering(tempTag);
1517                     }
1518                     f.setTempKeywordFiltering(keywordsArray);
1519                 }
1520
1521                 if (mDefaultFilter != null) {
1522                     if (tempPid != -1) {
1523                         mDefaultFilter.setTempPidFiltering(tempPid);
1524                     }
1525                     if (tempTag != null) {
1526                         mDefaultFilter.setTempTagFiltering(tempTag);
1527                     }
1528                     mDefaultFilter.setTempKeywordFiltering(keywordsArray);
1529
1530                 }
1531             }
1532
1533             initFilter(mCurrentFilter);
1534         }
1535     }
1536
1537     /**
1538      * Called when the current filter selection changes.
1539      * @param selectedFilter
1540      */
1541     private void selectionChanged(LogFilter selectedFilter) {
1542         if (mLogLevelActions != null) {
1543             // get the log level
1544             int level = selectedFilter.getLogLevel();
1545             for (int i = 0 ; i < mLogLevelActions.length; i++) {
1546                 ICommonAction a = mLogLevelActions[i];
1547                 if (i == level - 2) {
1548                     a.setChecked(true);
1549                 } else {
1550                     a.setChecked(false);
1551                 }
1552             }
1553         }
1554
1555         if (mDeleteFilterAction != null) {
1556             mDeleteFilterAction.setEnabled(selectedFilter.supportsDelete());
1557         }
1558         if (mEditFilterAction != null) {
1559             mEditFilterAction.setEnabled(selectedFilter.supportsEdit());
1560         }
1561     }
1562
1563     public String getSelectedErrorLineMessage() {
1564         Table table = mCurrentFilter.getTable();
1565         int[] selection = table.getSelectionIndices();
1566
1567         if (selection.length == 1) {
1568             TableItem item = table.getItem(selection[0]);
1569             LogMessage msg = (LogMessage)item.getData();
1570             if (msg.data.logLevel == LogLevel.ERROR || msg.data.logLevel == LogLevel.WARN)
1571                 return msg.msg;
1572         }
1573         return null;
1574     }
1575 }