public static @NotNull Balloon createBalloon(@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(JBUI.scale(ExperimentalUI.isNewUI() ? 2 : 0), 0));
    content.setBorder(JBUI.Borders.empty(JBUI.insets("Notification.borderInsets",
                                                     ExperimentalUI.isNewUI() ? JBUI.insets(4, 4, 6, 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;
  }
public static @NotNull Balloon createBalloon(@Nullable JComponent windowComponent,
                                               @NotNull Notification notification,
                                               boolean showCallout,
                                               boolean hideOnClickOutside,
                                               @NotNull Ref<BalloonLayoutData> layoutDataRef,
                                               @NotNull Disposable parentDisposable)