/* * Copyright 2018 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ package io.flutter.logging; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.intellij.execution.filters.Filter; import com.intellij.execution.filters.HyperlinkInfo; import com.intellij.execution.process.ProcessHandler; import com.intellij.execution.ui.ConsoleView; import com.intellij.execution.ui.ConsoleViewContentType; import com.intellij.icons.AllIcons; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.actionSystem.ex.CustomComponentAction; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.colors.EditorColorsManager; import com.intellij.openapi.editor.colors.EditorColorsScheme; import com.intellij.openapi.editor.colors.TextAttributesKey; import com.intellij.openapi.editor.markup.EffectType; import com.intellij.openapi.editor.markup.TextAttributes; import com.intellij.openapi.ui.SimpleToolWindowPanel; import com.intellij.openapi.ui.Splitter; import com.intellij.openapi.ui.popup.JBPopupFactory; import com.intellij.ui.ColoredTableCellRenderer; import com.intellij.ui.PopupHandler; import com.intellij.ui.ScrollPaneFactory; import com.intellij.ui.SimpleTextAttributes; import com.intellij.ui.awt.RelativePoint; import com.intellij.ui.components.JBCheckBox; import com.intellij.ui.components.JBLabel; import com.intellij.ui.content.Content; import com.intellij.ui.content.ContentFactory; import com.intellij.util.ui.JBUI; import com.intellij.util.ui.UIUtil; import io.flutter.FlutterUtils; import io.flutter.logging.FlutterLog.Level; import io.flutter.logging.tree.DataPanel; import io.flutter.run.daemon.FlutterApp; import io.flutter.utils.UIUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.util.List; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import static com.intellij.openapi.editor.markup.EffectType.*; import static io.flutter.logging.FlutterLogConstants.LogColumns.*; public class FlutterLogView extends JPanel implements ConsoleView, DataProvider, FlutterLog.Listener { // Toggle to enable experimental logging channel UI. public static final boolean ENABLE_LOGGING_CHANNELS = false; @NotNull private static final Logger LOG = Logger.getInstance(FlutterLogView.class); private static final float DATA_PANEL_SPLITTER_PROPORTION_DEFAULT = 0.60f; @NotNull private static final Map<FlutterLog.Level, TextAttributesKey> LOG_LEVEL_TEXT_ATTRIBUTES_KEY_MAP; @NotNull private static final Map<EffectType, Integer> EFFECT_TYPE_TEXT_STYLE_MAP; @NotNull private static final SimpleTextAttributes REGULAR_ATTRIBUTES = SimpleTextAttributes.GRAY_ATTRIBUTES; static { LOG_LEVEL_TEXT_ATTRIBUTES_KEY_MAP = new HashMap<>(); LOG_LEVEL_TEXT_ATTRIBUTES_KEY_MAP.put(Level.NONE, FlutterLogConstants.NONE_OUTPUT_KEY); LOG_LEVEL_TEXT_ATTRIBUTES_KEY_MAP.put(Level.FINEST, FlutterLogConstants.FINEST_OUTPUT_KEY); LOG_LEVEL_TEXT_ATTRIBUTES_KEY_MAP.put(Level.FINER, FlutterLogConstants.FINER_OUTPUT_KEY); LOG_LEVEL_TEXT_ATTRIBUTES_KEY_MAP.put(Level.FINE, FlutterLogConstants.FINE_OUTPUT_KEY); LOG_LEVEL_TEXT_ATTRIBUTES_KEY_MAP.put(Level.CONFIG, FlutterLogConstants.CONFIG_OUTPUT_KEY); LOG_LEVEL_TEXT_ATTRIBUTES_KEY_MAP.put(Level.INFO, FlutterLogConstants.INFO_OUTPUT_KEY); LOG_LEVEL_TEXT_ATTRIBUTES_KEY_MAP.put(Level.WARNING, FlutterLogConstants.WARNING_OUTPUT_KEY); LOG_LEVEL_TEXT_ATTRIBUTES_KEY_MAP.put(Level.SEVERE, FlutterLogConstants.SEVERE_OUTPUT_KEY); LOG_LEVEL_TEXT_ATTRIBUTES_KEY_MAP.put(Level.SHOUT, FlutterLogConstants.SHOUT_OUTPUT_KEY); EFFECT_TYPE_TEXT_STYLE_MAP = new HashMap<>(); EFFECT_TYPE_TEXT_STYLE_MAP.put(LINE_UNDERSCORE, SimpleTextAttributes.STYLE_UNDERLINE); EFFECT_TYPE_TEXT_STYLE_MAP.put(WAVE_UNDERSCORE, SimpleTextAttributes.STYLE_UNDERLINE | SimpleTextAttributes.STYLE_WAVED); EFFECT_TYPE_TEXT_STYLE_MAP.put(BOLD_LINE_UNDERSCORE, SimpleTextAttributes.STYLE_UNDERLINE | SimpleTextAttributes.STYLE_BOLD); EFFECT_TYPE_TEXT_STYLE_MAP.put(STRIKEOUT, SimpleTextAttributes.STYLE_STRIKEOUT); EFFECT_TYPE_TEXT_STYLE_MAP.put(BOLD_DOTTED_LINE, SimpleTextAttributes.STYLE_BOLD_DOTTED_LINE); EFFECT_TYPE_TEXT_STYLE_MAP.put(SEARCH_MATCH, SimpleTextAttributes.STYLE_SEARCH_MATCH); // TODO(quangson91): Figure out how to map style for these settings. EFFECT_TYPE_TEXT_STYLE_MAP.put(BOXED, null); EFFECT_TYPE_TEXT_STYLE_MAP.put(ROUNDED_BOX, null); } @NotNull private final Map<Level, SimpleTextAttributes> textAttributesByLogLevelCache = new ConcurrentHashMap<>(); private class EntryModel implements FlutterLogTree.EntryModel { boolean showColors; @Override public SimpleTextAttributes style(@Nullable FlutterLogEntry entry, int attributes) { if (showColors && entry != null) { final FlutterLog.Level level = FlutterLog.Level.forValue(entry.getLevel()); return getTextAttributesByLogLevel(level); } return SimpleTextAttributes.REGULAR_ATTRIBUTES; } @NotNull private SimpleTextAttributes getTextAttributesByLogLevel(@NotNull Level level) { if (textAttributesByLogLevelCache.containsKey(level)) { return textAttributesByLogLevelCache.get(level); } return REGULAR_ATTRIBUTES; } } class ConfigureAction extends AnAction implements RightAlignedToolbarAction { @NotNull private final DefaultActionGroup actionGroup; public ConfigureAction() { super("Configure", null, AllIcons.General.GearPlain); actionGroup = createPopupActionGroup(); } @Override public void actionPerformed(@NotNull AnActionEvent e) { getComponentOfActionEvent(e).ifPresent(component -> { final ActionPopupMenu popupMenu = ActionManager.getInstance().createActionPopupMenu(ActionPlaces.UNKNOWN, actionGroup); popupMenu.getComponent().show(component, component.getWidth(), 0); }); } @NotNull private Optional<JComponent> getComponentOfActionEvent(@NotNull AnActionEvent e) { final JComponent component = UIUtils.getComponentOfActionEvent(e); return Optional.ofNullable(component); } @NotNull private DefaultActionGroup createPopupActionGroup() { final DefaultActionGroup actionGroup = new DefaultActionGroup( new ShowTimeStampsAction(), new ShowSequenceNumbersAction(), new ShowLevelAction(), new ShowCategoryAction(), new Separator(), new ClearOnRestartAction(), new ClearOnReloadAction(), new Separator(), new ShowColorsAction() ); if (ENABLE_LOGGING_CHANNELS) { actionGroup.addAll(Arrays.asList(new Separator(), new ConfigureChannelsAction())); } return actionGroup; } } private class ShowTimeStampsAction extends ToggleAction { ShowTimeStampsAction() { super("Show timestamps"); } @Override public boolean isSelected(@NotNull AnActionEvent e) { return flutterLogPreferences.isShowTimestamp(); } @Override public void setSelected(@NotNull AnActionEvent e, boolean state) { flutterLogPreferences.setShowTimestamp(state); logModel.setShowTimestamps(state); logModel.update(); } } private class ShowSequenceNumbersAction extends ToggleAction { ShowSequenceNumbersAction() { super("Show sequence numbers"); } @Override public boolean isSelected(@NotNull AnActionEvent e) { return flutterLogPreferences.isShowSequenceNumbers(); } @Override public void setSelected(@NotNull AnActionEvent e, boolean state) { flutterLogPreferences.setShowSequenceNumbers(state); logModel.setShowSequenceNumbers(state); logModel.update(); } } private class ShowCategoryAction extends ToggleAction { ShowCategoryAction() { super("Show categories"); } @Override public boolean isSelected(@NotNull AnActionEvent e) { return flutterLogPreferences.isShowLogCategory(); } @Override public void setSelected(@NotNull AnActionEvent e, boolean state) { flutterLogPreferences.setShowLogCategory(state); logModel.setShowCategories(state); logModel.update(); } } private class ShowLevelAction extends ToggleAction { ShowLevelAction() { super("Show log levels"); } @Override public boolean isSelected(@NotNull AnActionEvent e) { return flutterLogPreferences.isShowLogLevel(); } @Override public void setSelected(@NotNull AnActionEvent e, boolean state) { flutterLogPreferences.setShowLogLevel(state); logModel.setShowLogLevels(state); logModel.update(); } } private class ClearOnReloadAction extends ToggleAction { ClearOnReloadAction() { super("Clear on reload"); } @Override public boolean isSelected(@NotNull AnActionEvent e) { return flutterLogPreferences.isClearOnReload(); } @Override public void setSelected(@NotNull AnActionEvent e, boolean state) { flutterLogPreferences.setClearOnReload(state); } } private class ClearOnRestartAction extends ToggleAction { ClearOnRestartAction() { super("Clear on restart"); } @Override public boolean isSelected(@NotNull AnActionEvent e) { return flutterLogPreferences.isClearOnRestart(); } @Override public void setSelected(@NotNull AnActionEvent e, boolean state) { flutterLogPreferences.setClearOnRestart(state); } } private class ShowColorsAction extends ToggleAction { ShowColorsAction() { super("Color entries"); } @Override public boolean isSelected(@NotNull AnActionEvent e) { return flutterLogPreferences.isShowColor(); } @Override public void setSelected(@NotNull AnActionEvent e, boolean state) { flutterLogPreferences.setShowColor(state); entryModel.showColors = state; logModel.update(); } } private class ChannelPanel extends JPanel { class LoggerCheckBox extends JBCheckBox implements ActionListener { @NotNull private final LoggingChannel channel; LoggerCheckBox(@NotNull LoggingChannel channel) { super(channel.name); this.channel = channel; setToolTipText(channel.description); setSelected(channel.enabled); addActionListener(this); } @Override public void actionPerformed(ActionEvent e) { app.getFlutterLog().enable(channel, isSelected()); } } ChannelPanel(List<LoggingChannel> channels) { setLayout(new GridLayout(0, 1)); channels.forEach(c -> add(new LoggerCheckBox(c))); } } private class ConfigureChannelsAction extends AnAction { ConfigureChannelsAction() { super("Configure channels..."); } @Override public void actionPerformed(@NotNull AnActionEvent e) { app.getFlutterLog().getLoggingChannels().thenAccept(channels -> ApplicationManager.getApplication().invokeAndWait(() -> { final ChannelPanel panel = new ChannelPanel(channels); final Rectangle visibleRect = logTree.getVisibleRect(); // TODO(pq): make width dynamic based on channel name length final Point topRight = new Point(logTree.getLocationOnScreen().x + visibleRect.width - 150, logTree.getLocationOnScreen().y + visibleRect.y); JBPopupFactory.getInstance() .createComponentPopupBuilder(panel, panel) .setTitle("Logging channels") .setMovable(true) .setRequestFocus(true) .createPopup().show(RelativePoint.fromScreen(topRight)); })).exceptionally(throwable -> { throwable.printStackTrace(); return null; }); } } private class ClearLogAction extends AnAction { ClearLogAction() { super("Clear All", "Clear the log", AllIcons.Actions.GC); } @Override public void actionPerformed(@NotNull AnActionEvent e) { ApplicationManager.getApplication().invokeLater(() -> { logTree.clearEntries(); // Flush state. clear(); }); } @Override public void update(@NotNull AnActionEvent e) { e.getPresentation().setEnabled(logModel.getRoot().getChildCount() > 0); } } private class ScrollToEndAction extends ToggleAction { @NotNull private final AnActionEvent EMPTY_ACTION_EVENT = AnActionEvent.createFromDataContext("empty_action_event", null, DataContext.EMPTY_CONTEXT); ScrollToEndAction() { super("Scroll to the end", "Scroll to the end", AllIcons.RunConfigurations.Scroll_down); } @Override public boolean isSelected(@NotNull AnActionEvent e) { return logModel.autoScrollToEnd; } @Override public void setSelected(@NotNull AnActionEvent e, boolean state) { ApplicationManager.getApplication().invokeLater(() -> { logModel.autoScrollToEnd = state; if (state) { logModel.scrollToEnd(); } }); } public void enableIfNeeded() { if (!isSelected(EMPTY_ACTION_EVENT)) { setSelected(EMPTY_ACTION_EVENT, true); } } public void disableIfNeeded() { if (isSelected(EMPTY_ACTION_EVENT)) { setSelected(EMPTY_ACTION_EVENT, false); } } } private class CopyToClipboardAction extends AnAction { CopyToClipboardAction() { super("Copy as Text", "Copy selected entries to the clipboard", AllIcons.Actions.Copy); } @Override public void actionPerformed(@NotNull AnActionEvent e) { logTree.sendSelectedLogsToClipboard(); } } float lastSplitProportion = DATA_PANEL_SPLITTER_PROPORTION_DEFAULT; private class FilterStatusLabel extends AnAction implements CustomComponentAction, RightAlignedToolbarAction, FlutterLogTree.EventCountListener { JBLabel label; JPanel panel; @Override public void actionPerformed(@NotNull AnActionEvent e) { // None. Just an info display. } @NotNull @Override public JComponent createCustomComponent(@NotNull Presentation presentation) { panel = new JPanel(); label = new JBLabel(); label.setFont(UIUtil.getLabelFont(UIUtil.FontSize.SMALL)); label.setForeground(UIUtil.getInactiveTextColor()); label.setBorder(JBUI.Borders.emptyRight(10)); panel.add(label); logTree.addListener(this, FlutterLogView.this); return panel; } @Override public void updated(int total, int filtered) { if (label != null && label.isVisible()) { final int visibleCount = total - filtered; final StringBuilder sb = new StringBuilder(); sb.append(visibleCount).append(" event"); if (visibleCount != 1) { sb.append("s"); } if (filtered > 0) { sb.append(" (").append(filtered).append(" filtered)"); } label.setText(sb.toString()); SwingUtilities.invokeLater(panel::repaint); } } } private static abstract class LogVerticalScrollChangeListener implements ChangeListener { private volatile int oldScrollValue = 0; @Override public void stateChanged(ChangeEvent e) { if (e.getSource() instanceof BoundedRangeModel) { final BoundedRangeModel model = (BoundedRangeModel)e.getSource(); final int newScrollValue = model.getValue(); final boolean isScrollUp = newScrollValue < oldScrollValue; final boolean isScrollToEnd = newScrollValue + model.getExtent() == model.getMaximum(); if (isScrollUp) { onScrollUp(); } else if (isScrollToEnd) { onScrollToEnd(); } oldScrollValue = newScrollValue; } } protected abstract void onScrollUp(); protected abstract void onScrollToEnd(); } @NotNull private final FlutterApp app; // TODO(pq): make user configurable. private final EntryModel entryModel = new EntryModel(); @NotNull private final SimpleToolWindowPanel toolWindowPanel; @NotNull private final FlutterLogTree.TreeModel logModel; @NotNull private final FlutterLogTree logTree; @NotNull private final FlutterLogFilterPanel filterPanel; @NotNull private final FlutterLogPreferences flutterLogPreferences; @NotNull private final ScrollToEndAction scrollToEndAction = new ScrollToEndAction(); @NotNull private final ClearLogAction clearLogAction = new ClearLogAction(); @NotNull private final DataPanel dataPanel; private JScrollPane dataPane; private Splitter treeSplitter; private final Gson gsonHelper = new GsonBuilder().setPrettyPrinting().create(); boolean isPinned; // Auto-scroll defaults to on. boolean prePinAutoScroll = true; public FlutterLogView(@NotNull FlutterApp app) { this.app = app; flutterLogPreferences = FlutterLogPreferences.getInstance(app.getProject()); filterPanel = new FlutterLogFilterPanel(param -> doFilter()); filterPanel.initFromPreferences(flutterLogPreferences); computeTextAttributesByLogLevelCache(); ApplicationManager.getApplication().getMessageBus().connect(this) .subscribe(EditorColorsManager.TOPIC, scheme -> computeTextAttributesByLogLevelCache()); final FlutterLog flutterLog = app.getFlutterLog(); flutterLog.addListener(this, this); final Content content = ContentFactory.SERVICE.getInstance().createContent(null, null, false); content.setCloseable(false); toolWindowPanel = new SimpleToolWindowPanel(true, true); content.setComponent(toolWindowPanel); final JPanel toolbar = createToolbar(); toolWindowPanel.setToolbar(toolbar); logTree = new FlutterLogTree(app, entryModel, this); logModel = logTree.getLogTreeModel(); logModel.updateFromPreferences(flutterLogPreferences); entryModel.showColors = flutterLogPreferences.isShowColor(); // TODO(pq): add speed search //new TreeTableSpeedSearch(logTree).setComparator(new SpeedSearchComparator(false)); logTree.setTableHeader(null); logTree.setRootVisible(false); logTree.setExpandableItemsEnabled(true); logTree.getTree().setScrollsOnExpand(true); final PopupHandler popupHandler = new PopupHandler() { @Override public void invokePopup(Component comp, int x, int y) { final ActionPopupMenu popupMenu = ActionManager.getInstance().createActionPopupMenu(ActionPlaces.UNKNOWN, getTreePopupActions()); popupMenu.getComponent().show(comp, x, y); } @Override public void mouseMoved(MouseEvent e) { final Cursor cursor = getTagForPosition(e) instanceof HyperlinkInfo ? Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) : Cursor.getDefaultCursor(); logTree.setCursor(cursor); } @Override public void mouseClicked(MouseEvent e) { final Object tag = getTagForPosition(e); if (tag instanceof HyperlinkInfo) { ((HyperlinkInfo)tag).navigate(app.getProject()); } } private Object getTagForPosition(MouseEvent e) { final JTable table = (JTable)e.getSource(); final int row = table.rowAtPoint(e.getPoint()); final int column = table.columnAtPoint(e.getPoint()); if (row == -1 || column == -1) return null; final TableCellRenderer cellRenderer = table.getCellRenderer(row, column); if (cellRenderer instanceof ColoredTableCellRenderer) { final ColoredTableCellRenderer renderer = (ColoredTableCellRenderer)cellRenderer; final Rectangle rc = table.getCellRect(row, column, false); return renderer.getFragmentTagAt(e.getX() - rc.x); } return null; } }; logTree.addMouseListener(popupHandler); logTree.addMouseMotionListener(popupHandler); // Set bounds. // TODO(pq): consider re-sizing dynamically, as needed. fixColumnWidth(logTree.getColumn(TIME), 100); fixColumnWidth(logTree.getColumn(SEQUENCE), 50); fixColumnWidth(logTree.getColumn(LEVEL), 70); fixColumnWidth(logTree.getColumn(CATEGORY), 110); logTree.getColumn(MESSAGE).setMinWidth(100); dataPanel = DataPanel.create(app.getProject()); logTree.addSelectionListener(this::updateDataPanel); setupLogTreeScrollPane(); } private void updateDataPanel() { final List<FlutterLogTree.FlutterEventNode> selectedNodes = logTree.getSelectedNodes(); if (!selectedNodes.isEmpty()) { dataPanel.update(selectedNodes.get(0).entry); } } private void setupLogTreeScrollPane() { final JScrollPane treePane = ScrollPaneFactory.createScrollPane( logTree, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); treePane.getVerticalScrollBar().getModel().addChangeListener(new LogVerticalScrollChangeListener() { @Override protected void onScrollUp() { scrollToEndAction.disableIfNeeded(); } @Override protected void onScrollToEnd() { scrollToEndAction.enableIfNeeded(); } }); logModel.setScrollPane(treePane); dataPane = ScrollPaneFactory.createScrollPane( dataPanel, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); treeSplitter = new Splitter(false); treeSplitter.setProportion(DATA_PANEL_SPLITTER_PROPORTION_DEFAULT); // Data and splitter revealed on demand. dataPane.setVisible(false); treeSplitter.setVisible(false); treeSplitter.setFirstComponent(treePane); treeSplitter.setSecondComponent(dataPane); toolWindowPanel.setContent(treeSplitter); dataPanel.onUpdate(hasContent -> { dataPane.setVisible(hasContent); if (hasContent) { if (treeSplitter.getProportion() != lastSplitProportion) { treeSplitter.setProportion(lastSplitProportion); treeSplitter.revalidate(); treeSplitter.repaint(); } } else { final float proportion = treeSplitter.getProportion(); if (proportion != 1.0f) { lastSplitProportion = proportion; } treeSplitter.setProportion(1.0f); } }); } private void computeTextAttributesByLogLevelCache() { final EditorColorsScheme globalEditorColorsScheme = EditorColorsManager.getInstance().getGlobalScheme(); textAttributesByLogLevelCache.clear(); for (Level level : FlutterLog.Level.values()) { try { final TextAttributesKey key = LOG_LEVEL_TEXT_ATTRIBUTES_KEY_MAP.get(level); final TextAttributes attributes = globalEditorColorsScheme.getAttributes(key); int fontType = attributes.getFontType(); final Color effectColor = attributes.getEffectColor(); final Integer textStyle = EFFECT_TYPE_TEXT_STYLE_MAP.get(attributes.getEffectType()); // TextStyle can exist even when unchecked in settings page. // only effectColor is null when setting effect is unchecked in setting page. // So, we have to check that both effectColor & textStyle are not null. if (effectColor != null && textStyle != null) { fontType = fontType | textStyle; } final SimpleTextAttributes textAttributes = new SimpleTextAttributes( attributes.getBackgroundColor(), attributes.getForegroundColor(), effectColor, fontType ); textAttributesByLogLevelCache.put(level, textAttributes); } catch (Exception e) { // Should never go here. FlutterUtils.warn(LOG, "Error when get text attributes by log level", e); } } } private ActionGroup getTreePopupActions() { return new ActionGroup() { @NotNull @Override public AnAction[] getChildren(@Nullable AnActionEvent e) { return new AnAction[]{ new CopyToClipboardAction(), new Separator(), new ClearLogAction() }; } }; } private void doFilter() { final FlutterLogFilterPanel.FilterParam param = filterPanel.getCurrentFilterParam(); flutterLogPreferences.setToolWindowRegex(param.isRegex()); flutterLogPreferences.setToolWindowMatchCase(param.isMatchCase()); flutterLogPreferences.setToolWindowLogLevel(param.getLogLevel().value); ApplicationManager.getApplication().invokeLater(() -> logTree.setFilter(param)); } @NotNull private FlutterLog getFlutterLog() { return app.getFlutterLog(); } @NotNull private JPanel createToolbar() { final DefaultActionGroup toolbarGroup = new DefaultActionGroup(); toolbarGroup.add(new FilterStatusLabel()); toolbarGroup.add(new ConfigureAction()); final ActionToolbar actionToolbar = ActionManager.getInstance().createActionToolbar("FlutterLogViewToolbar", toolbarGroup, true); actionToolbar.setMiniMode(false); final JPanel toolbar = new JPanel(); toolbar.setLayout(new BorderLayout()); toolbar.add(filterPanel.getRoot(), BorderLayout.WEST); toolbar.add(actionToolbar.getComponent(), BorderLayout.EAST); return toolbar; } @Override public void onEvent(@NotNull FlutterLogEntry entry) { final boolean isError = entry.getKind() == FlutterLogEntry.Kind.FLUTTER_ERROR; logTree.append(entry, isError && !isPinned); if (isError && !isPinned) { prePinAutoScroll = logModel.autoScrollToEnd; scrollToEndAction.disableIfNeeded(); isPinned = true; } } @Override public void onEntryContentChange() { // Called when truncated text values are returned. logModel.uiExec(logModel::update, 10); } @Override public void print(@NotNull String text, @NotNull ConsoleViewContentType contentType) { getFlutterLog().addConsoleEntry(text, contentType); } @Override public void clear() { // Unpin if (isPinned) { if (prePinAutoScroll) { scrollToEndAction.enableIfNeeded(); } else { scrollToEndAction.disableIfNeeded(); } isPinned = false; } dataPane.setVisible(false); } @Override public void scrollTo(int offset) { } @Override public void attachToProcess(ProcessHandler processHandler) { getFlutterLog().listenToProcess(processHandler, this); } @Override public boolean isOutputPaused() { return false; } @Override public void setOutputPaused(boolean value) { } @Override public boolean hasDeferredOutput() { return false; } @Override public void performWhenNoDeferredOutput(@NotNull Runnable runnable) { } @Override public void setHelpId(@NotNull String helpId) { } @Override public void addMessageFilter(@NotNull Filter filter) { } @Override public void printHyperlink(@NotNull String hyperlinkText, @Nullable HyperlinkInfo info) { } @Override public int getContentSize() { return 0; } @Override public boolean canPause() { return false; } @NotNull @Override public AnAction[] createConsoleActions() { return new AnAction[]{ scrollToEndAction, clearLogAction }; } @Override public void allowHeavyFilters() { } @Override public void dispose() { } @Nullable @Override public Object getData(@NotNull String dataId) { return null; } @Override public JComponent getComponent() { return toolWindowPanel; } @Override public JComponent getPreferredFocusableComponent() { return logTree; } private static void fixColumnWidth(@NotNull TableColumn column, int width) { column.setMinWidth(width); column.setMaxWidth(width); column.setPreferredWidth(width); } }