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