Balloon (@Nullable JComponent windowComponent, @NotNull Notification notification, boolean showCallout, boolean hideOnClickOutside, @NotNull Ref<BalloonLayoutData> layoutDataRef, @NotNull Disposable parentDisposable) { BalloonLayoutData layoutData = layoutDataRef.isNull() ? new BalloonLayoutData() : layoutDataRef.get(); if (layoutData.groupId == null) { layoutData.groupId = notification.getGroupId(); } else { layoutData.groupId = null; layoutData.mergeData = null; } layoutData.id = notification.id; layoutData.displayId = notification.getDisplayId(); layoutDataRef.set(layoutData); if (layoutData.textColor == null) { layoutData.textColor = JBColor.namedColor("Notification.foreground", DEFAULT_TEXT_COLOR); } if (layoutData.fillColor == null) { layoutData.fillColor = FILL_COLOR; } if (layoutData.borderColor == null) { layoutData.borderColor = BORDER_COLOR; } if (notification.isSuggestionType()) { layoutData.collapseType = notification.isImportantSuggestion() ? BalloonLayoutData.Type.ImportantSuggestion : BalloonLayoutData.Type.Suggestion; } else { layoutData.collapseType = BalloonLayoutData.Type.Timeline; } boolean actions = !notification.getActions().isEmpty() || notification.getContextHelpAction() != null; boolean showFullContent = layoutData.showFullContent || notification instanceof NotificationFullContent; JEditorPane text = new JEditorPane() { @Override protected void paintComponent(Graphics g) { super.paintComponent(g); if (layoutData.showMinSize) { Point location = getCollapsedTextEndLocation(this, layoutData); if (location != null) { if (g instanceof Graphics2D) { ((Graphics2D)g).setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); } g.setColor(getForeground()); g.drawString("...", location.x, location.y + g.getFontMetrics().getAscent()); } } } }; NotificationsUtil.configureHtmlEditorKit(text, true); text.setForeground(layoutData.textColor); HyperlinkListener listener = NotificationsUtil.wrapListener(notification); if (listener != null) { text.addHyperlinkListener(listener); } Supplier<@Nls String> textBuilder = () -> { String fontStyle = NotificationsUtil.getFontStyle(); int prefSize = new JLabel(NotificationsUtil.buildHtml(notification, null, true, null, fontStyle)).getPreferredSize().width; String style; if (layoutData.showFullContent) { style = prefSize > BalloonLayoutConfiguration.MaxFullContentWidth() ? BalloonLayoutConfiguration.MaxFullContentWidthStyle() : null; } else { style = prefSize > BalloonLayoutConfiguration.MaxWidth() ? BalloonLayoutConfiguration.MaxWidthStyle() : null; } return NotificationsUtil.buildHtml(notification, style, true, null, fontStyle); }; String textContent = textBuilder.get(); text.setText(textContent); setTextAccessibleName(text, textContent); text.setEditable(false); text.setOpaque(false); text.setBorder(null); JPanel content = new NonOpaquePanel(new BorderLayout()); content.setBorder(JBUI.Borders.empty(JBUI.insets("Notification.borderInsets", ExperimentalUI.isNewUI() ? JBUI.insets(4, 4, 4, 0) : JBInsets.emptyInsets()))); if (text.getCaret() != null) { text.setCaretPosition(0); } JScrollPane pane = createBalloonScrollPane(text, false); pane.getVerticalScrollBar().addAdjustmentListener(new AdjustmentListener() { @Override public void adjustmentValueChanged(AdjustmentEvent e) { JScrollBar scrollBar = pane.getVerticalScrollBar(); if (layoutData.showMinSize && scrollBar.getValue() > 0) { scrollBar.removeAdjustmentListener(this); scrollBar.setValue(0); scrollBar.addAdjustmentListener(this); } } }); LinkLabel<Void> expandAction = null; int lines = 4; if (notification.hasTitle()) { lines--; } if (actions) { lines--; } layoutData.fullHeight = text.getPreferredSize().height; layoutData.twoLineHeight = calculateContentHeight(lines); layoutData.maxScrollHeight = Math.min(layoutData.fullHeight, calculateContentHeight(10)); layoutData.configuration = BalloonLayoutConfiguration.create(notification, layoutData, actions); if (layoutData.welcomeScreen) { layoutData.maxScrollHeight = layoutData.fullHeight; } else if (!showFullContent && layoutData.maxScrollHeight != layoutData.fullHeight) { pane.setViewport(new GradientViewport(text, JBInsets.create(10, 0), true) { @Override protected @Nullable Color getViewColor() { return layoutData.fillColor; } @Override protected void paintGradient(Graphics g) { if (!layoutData.showMinSize) { super.paintGradient(g); } } }); } configureBalloonScrollPane(pane, layoutData.fillColor); if (showFullContent) { if (windowComponent == null) { pane.setPreferredSize(text.getPreferredSize()); } else { pane.setPreferredSize( new Dimension(text.getPreferredSize().width, (int)Math.min(layoutData.fullHeight, windowComponent.getHeight() * 0.75))); } } else if (layoutData.twoLineHeight < layoutData.fullHeight) { text.setPreferredSize(null); Dimension size = text.getPreferredSize(); size.height = layoutData.twoLineHeight; text.setPreferredSize(size); text.setSize(size); layoutData.showMinSize = true; pane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); pane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER); pane.setPreferredSize(size); text.setCaret(new TextCaret(layoutData)); expandAction = new LinkLabel<>(null, AllIcons.Ide.Notification.Expand, (link, ignored) -> { layoutData.showMinSize = !layoutData.showMinSize; text.setPreferredSize(null); Dimension _size = text.getPreferredSize(); if (layoutData.showMinSize) { _size.height = layoutData.twoLineHeight; pane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER); link.setIcon(AllIcons.Ide.Notification.Expand); link.setHoveringIcon(AllIcons.Ide.Notification.ExpandHover); NotificationCollector.getInstance().logNotificationBalloonCollapsed(layoutData.project, notification); } else { text.select(0, 0); _size.height = layoutData.fullHeight; pane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); link.setIcon(AllIcons.Ide.Notification.Collapse); link.setHoveringIcon(AllIcons.Ide.Notification.CollapseHover); NotificationCollector.getInstance().logNotificationBalloonExpanded(layoutData.project, notification); } text.setPreferredSize(_size); text.setSize(_size); if (!layoutData.showMinSize) { _size = new Dimension(_size.width, layoutData.maxScrollHeight); } pane.setPreferredSize(_size); content.doLayout(); layoutData.doLayout.run(); }); expandAction.setHoveringIcon(AllIcons.Ide.Notification.ExpandHover); layoutData.isExpandable = true; } NotificationCenterPanel centerPanel = new NotificationCenterPanel(text, layoutData); content.add(centerPanel, BorderLayout.CENTER); if (notification.hasTitle()) { String titleStyle = StringUtil.defaultIfEmpty(NotificationsUtil.getFontStyle(), "") + "white-space:nowrap;"; JLabel title = new JLabel(); String titleContent = NotificationsUtil.buildHtml(notification, titleStyle, false, null, null); title.setText(titleContent); setTextAccessibleName(title, titleContent); title.setOpaque(false); title.setForeground(layoutData.textColor); centerPanel.addTitle(title); } if (expandAction != null) { centerPanel.addExpandAction(expandAction); } if (notification.hasContent()) { centerPanel.addContent(layoutData.welcomeScreen ? text : pane); } Icon icon = NotificationsUtil.getIcon(notification); JComponent iconComponent = new JComponent() { @Override protected void paintComponent(Graphics g) { super.paintComponent(g); icon.paintIcon(this, g, layoutData.configuration.iconOffset.width, layoutData.configuration.iconOffset.height); } }; iconComponent.setOpaque(false); Runnable iconSizeRunnable = () -> iconComponent.setPreferredSize( new Dimension(Math.max(layoutData.configuration.iconPanelWidth, icon.getIconWidth() + layoutData.configuration.iconOffset.width + JBUIScale.scale(5)), 2 * layoutData.configuration.iconOffset.height + icon.getIconHeight())); iconSizeRunnable.run(); content.add(iconComponent, BorderLayout.WEST); HoverAdapter hoverAdapter = new HoverAdapter(); hoverAdapter.addSource(content); hoverAdapter.addSource(centerPanel); hoverAdapter.addSource(text); hoverAdapter.addSource(pane); if (actions) { createActionPanel(notification, centerPanel, layoutData.configuration.actionGap, hoverAdapter); } if (expandAction != null) { hoverAdapter.addComponent(expandAction, component -> { Rectangle bounds; Point location = SwingUtilities.convertPoint(content.getParent(), content.getLocation(), component.getParent()); if (layoutData.showMinSize) { Component centerComponent = layoutData.welcomeScreen ? text : pane; Point centerLocation = SwingUtilities.convertPoint(centerComponent.getParent(), centerComponent.getLocation(), component.getParent()); bounds = new Rectangle(location.x, centerLocation.y, content.getWidth(), centerComponent.getHeight()); } else { bounds = new Rectangle(location.x, component.getY(), content.getWidth(), component.getHeight()); JBInsets.addTo(bounds, JBUI.insets(5, 0, 7, 0)); } return bounds; }); } hoverAdapter.initListeners(); if (layoutData.mergeData != null) { createMergeAction(layoutData, content); } text.setSize(text.getPreferredSize()); Runnable paneSizeRunnable = () -> { Dimension paneSize = new Dimension(text.getPreferredSize()); int maxWidth = JBUIScale.scale(600); if (windowComponent != null) { maxWidth = Math.min(maxWidth, windowComponent.getWidth() - 20); } if (paneSize.width > maxWidth) { pane.setPreferredSize(new Dimension(maxWidth, paneSize.height + UIUtil.getScrollBarWidth())); } }; paneSizeRunnable.run(); content.putClientProperty(NOTIFICATION_BALLOON_FLAG, new Object()); BalloonBuilder builder = JBPopupFactory.getInstance().createBalloonBuilder(content); builder.setFillColor(layoutData.fillColor) .setCloseButtonEnabled(true) .setShowCallout(showCallout) .setShadow(false) .setAnimationCycle(200) .setHideOnClickOutside(hideOnClickOutside) .setHideOnAction(hideOnClickOutside) .setHideOnKeyOutside(hideOnClickOutside) .setHideOnFrameResize(false) .setBorderColor(layoutData.borderColor) .setBorderInsets(JBInsets.emptyInsets()); Balloon balloon = builder.createBalloon(); if (balloon instanceof BalloonImpl balloonImpl) { balloonImpl.getContent().addMouseListener(new MouseAdapter() { }); balloon.setAnimationEnabled(false); balloonImpl.setShadowBorderProvider(new NotificationBalloonRoundShadowBorderProvider(layoutData.fillColor, layoutData.borderColor)); if (!layoutData.welcomeScreen) { balloonImpl.setActionProvider(new NotificationBalloonActionProvider(balloonImpl, centerPanel.getTitle(), layoutData, notification)); } if (layoutData.fadeoutTime != 0) { ((BalloonImpl)balloon).startSmartFadeoutTimer((int)layoutData.fadeoutTime); } } notification.setBalloon(balloon); int _lines = lines; Runnable lafCallback = () -> { NotificationsUtil.configureHtmlEditorKit(text, true); text.setText(textBuilder.get()); text.setPreferredSize(null); Dimension size = text.getPreferredSize(); layoutData.fullHeight = size.height; layoutData.twoLineHeight = calculateContentHeight(_lines); layoutData.maxScrollHeight = Math.min(layoutData.fullHeight, calculateContentHeight(10)); layoutData.configuration = BalloonLayoutConfiguration.create(notification, layoutData, actions); if (layoutData.welcomeScreen) { layoutData.maxScrollHeight = layoutData.fullHeight; } if (showFullContent) { if (windowComponent == null) { pane.setPreferredSize(size); } else { pane.setPreferredSize(new Dimension(size.width, (int)Math.min(layoutData.fullHeight, windowComponent.getHeight() * 0.75))); } } else if (layoutData.twoLineHeight < layoutData.fullHeight) { size.height = layoutData.twoLineHeight; text.setPreferredSize(size); text.setSize(size); layoutData.showMinSize = true; pane.setPreferredSize(size); } text.revalidate(); text.repaint(); iconSizeRunnable.run(); paneSizeRunnable.run(); content.doLayout(); Runnable doLayout = layoutData.doLayout; if (doLayout != null) { doLayout.run(); } Container parent = content.getParent(); if (parent == null) { parent = content; } parent.doLayout(); parent.revalidate(); parent.repaint(); }; MessageBusConnection connection = ApplicationManager.getApplication().getMessageBus().connect(balloon); connection.subscribe(LafManagerListener.TOPIC, source -> { // We need to call this callback in the next iteration, // because otherwise some components inside the balloon may update their borders after all sizes are calculated inside `lafCallback` SwingUtilities.invokeLater(lafCallback); }); connection.subscribe(UISettingsListener.TOPIC, uiSettings -> lafCallback.run()); Disposer.register(parentDisposable, balloon); return balloon; }