diff --git a/grid/core-impl/resources/messages/DataGridBundle.properties b/grid/core-impl/resources/messages/DataGridBundle.properties index 5c1ca0c81cee..74201fcdb192 100644 --- a/grid/core-impl/resources/messages/DataGridBundle.properties +++ b/grid/core-impl/resources/messages/DataGridBundle.properties @@ -20,6 +20,7 @@ action.full.message.text=Full Message action.close.text=Close tooltip.close.esc=Close (Esc) dialog.title.query.error=Query Error +dialog.button.copy.and.close=Copy and close notification.content.unknown.problem.occurred.see.details=An unknown problem occurred (see Details) Console.TableResult.n.bytes.of.m.bytes.loaded={0} of {1} action.Console.TableResult.ClearCell.text=Clear Field diff --git a/grid/core-impl/src/connection/throwable/info/ErrorInfo.java b/grid/core-impl/src/connection/throwable/info/ErrorInfo.java index 7d38a64d8f4b..a2b558753855 100644 --- a/grid/core-impl/src/connection/throwable/info/ErrorInfo.java +++ b/grid/core-impl/src/connection/throwable/info/ErrorInfo.java @@ -14,6 +14,7 @@ public interface ErrorInfo extends ThrowableInfo { interface Fix { @Nls String getName(); + default Integer getMnemonic() { return null; } default boolean isSilent() { return true; } diff --git a/grid/impl/src/run/ui/ErrorNotificationPanel.kt b/grid/impl/src/run/ui/ErrorNotificationPanel.kt index c4b28557c694..39058dbfb9d4 100644 --- a/grid/impl/src/run/ui/ErrorNotificationPanel.kt +++ b/grid/impl/src/run/ui/ErrorNotificationPanel.kt @@ -2,227 +2,200 @@ package com.intellij.database.run.ui import com.intellij.CommonBundle import com.intellij.database.DataGridBundle -import com.intellij.database.datagrid.GridUtil import com.intellij.icons.AllIcons import com.intellij.ide.CopyProvider -import com.intellij.ide.IdeTooltipManager.Companion.getInstance -import com.intellij.ide.IdeTooltipManager.Companion.initPane import com.intellij.ide.TextCopyProvider -import com.intellij.openapi.Disposable +import com.intellij.openapi.MnemonicHelper import com.intellij.openapi.actionSystem.* -import com.intellij.openapi.actionSystem.UiDataProvider.Companion.wrapComponent +import com.intellij.openapi.ide.CopyPasteManager import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.ui.MessageType import com.intellij.openapi.ui.Messages -import com.intellij.openapi.util.* -import com.intellij.openapi.util.registry.Registry.Companion.`is` +import com.intellij.openapi.util.NlsActions +import com.intellij.openapi.util.NlsContexts import com.intellij.openapi.util.text.StringUtil -import com.intellij.ui.HintHint -import com.intellij.ui.HyperlinkAdapter -import com.intellij.util.Consumer -import com.intellij.util.ObjectUtils -import com.intellij.util.containers.ContainerUtil -import com.intellij.util.ui.JBInsets +import com.intellij.util.ui.JBDimension import com.intellij.util.ui.JBUI -import com.intellij.util.ui.UIUtil -import org.jetbrains.annotations.NonNls -import java.awt.BorderLayout -import java.awt.Color -import java.awt.Dimension -import java.awt.event.HierarchyEvent -import java.awt.event.HierarchyListener -import java.awt.event.InputEvent +import org.jetbrains.annotations.Nls +import java.awt.* import java.awt.event.KeyEvent +import java.awt.event.MouseEvent +import java.awt.event.MouseListener +import java.awt.font.TextAttribute import javax.swing.* -import javax.swing.event.HyperlinkEvent + +private const val HORIZONTAL_LAYOUT_THRESHOLD = 2 +private const val VERTICAL_MARGINS = 10 +private const val HEIGHT_BETWEEN_TEXT_AND_BUTTONS = 6 +private const val HORIZONTAL_MARGINS = 16 class ErrorNotificationPanel private constructor( - htmlErrorMessage: @NlsContexts.NotificationContent String, - private val myActions: MutableMap, - private val myType: MessageType -) : JPanel(BorderLayout()) { - private val myMessagePane: JEditorPane - private val myCopyProvider: CopyProvider + htmlErrorMessage: String?, + items: List, + private val hideErrorAction: Runnable?, + private val messageType: MessageType = MessageType.ERROR, +) : JPanel(BorderLayout()), UiDataProvider { + private var copyProvider: CopyProvider? = null + private val textPane: JTextArea? init { - setBorder(JBUI.Borders.empty(0, 4)) + background = messageType.popupBackground + isFocusable = true + isFocusCycleRoot = true + border = JBUI.Borders.empty(VERTICAL_MARGINS, 0) + isRequestFocusEnabled = true - myMessagePane = initPane(htmlErrorMessage, HintHint() - .setAwtTooltip(false) - .setTextFg(getForeground()) - .setTextBg(getBackground()) - .setBorderColor(getBackground()) - .setBorderInsets(JBInsets.emptyInsets()), null) - myMessagePane.setBorder(null) - myMessagePane.addHyperlinkListener(object : HyperlinkAdapter() { - override fun hyperlinkActivated(e: HyperlinkEvent) { - performAction(e.description) - } - }) - myCopyProvider = object : TextCopyProvider() { - override fun getActionUpdateThread(): ActionUpdateThread { - return ActionUpdateThread.EDT - } + val horizontalLayout = items.size <= HORIZONTAL_LAYOUT_THRESHOLD + textPane = htmlErrorMessage?.let { createTextPanel(htmlErrorMessage, horizontalLayout) } + initComponent(items, horizontalLayout) - override fun getTextLinesToCopy(): MutableCollection? { - val text = myMessagePane.getSelectedText() - return if (StringUtil.isEmpty(text)) null else mutableSetOf(text) - } + SwingUtilities.invokeLater { requestFocusInWindow() } + } + + private fun initComponent(items: List, horizontalLayout: Boolean) { + val buttonsGravity = if (horizontalLayout) { BorderLayout.EAST } else { BorderLayout.NORTH } + val buttonPanel = JPanel(FlowLayout(FlowLayout.RIGHT, HORIZONTAL_MARGINS, 0)).apply { + items.forEach { add(it.buildComponent()) } + isOpaque = false + border = JBUI.Borders.empty() } - add(wrapComponent(myMessagePane, UiDataProvider { sink: DataSink? -> - sink!!.set(PlatformDataKeys.COPY_PROVIDER, this.myCopyProvider) - }), BorderLayout.CENTER) + add(buttonPanel, buttonsGravity) + textPane?.let { add(it, BorderLayout.CENTER) } + addSubscribers(textPane) + + addMouseListener(RequestFocusMouseListener(this)) + textPane?.addMouseListener(RequestFocusMouseListener(this)) + } + + private fun createTextPanel(htmlText: String, horizontalLayout: Boolean) = JTextArea().apply { + isOpaque = false + isEditable = false + isFocusable = true + text = htmlText.trimIndent() + font = UIManager.getFont("ToolTip.font") + lineWrap = true + border = if (horizontalLayout) { + JBUI.Borders.empty(0, VERTICAL_MARGINS, 0, 0) + } else { + JBUI.Borders.empty(HEIGHT_BETWEEN_TEXT_AND_BUTTONS, HORIZONTAL_MARGINS, 0, HORIZONTAL_MARGINS) + } + wrapStyleWord = true + foreground = messageType.titleForeground + caret = object : javax.swing.text.DefaultCaret() { + override fun paint(g: Graphics?) { /* hide cursor */ } + } + } + + override fun addMouseListener(listener: MouseListener) { + super.addMouseListener(listener) + textPane?.addMouseListener(listener) + } + + private fun addSubscribers(textPane: JTextArea?) { object : DumbAwareAction(DataGridBundle.message("action.close.text")) { override fun actionPerformed(e: AnActionEvent) { - performAction(DataGridBundle.message("action.close.text")) + hideErrorAction?.run() } }.registerCustomShortcutSet(CustomShortcutSet(KeyEvent.VK_ESCAPE), this) + + copyProvider = object : TextCopyProvider() { + override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT + override fun getTextLinesToCopy(): MutableCollection? { + textPane ?: return null + textPane.selectedText?.let { return mutableSetOf(it) } + return mutableSetOf(textPane.text) + } + } } - private fun performAction(actionName: String?) { - myActions[actionName]?.run() + override fun getMinimumSize(): Dimension = JBUI.emptySize() + + override fun uiDataSnapshot(sink: DataSink) { + sink[PlatformDataKeys.COPY_PROVIDER] = copyProvider } - override fun getBackground(): Color { - return type.popupBackground - } - - override fun getForeground(): Color { - return type.titleForeground - } - - private val type: MessageType - get() = ObjectUtils.chooseNotNull(myType, MessageType.ERROR) - - override fun getMinimumSize(): Dimension { - return JBUI.emptySize() - } - - val content: JComponent - get() = myMessagePane - class Builder internal constructor( - private val myMessage: @NlsContexts.NotificationContent String?, - private val myError: Throwable?, - private val myBaseComponent: JComponent + private val message: @NlsContexts.NotificationContent String?, + private val error: Throwable?, ) { - private val myLongMessage: Boolean - - private val myActions: MutableMap = LinkedHashMap() - private val myShowHideHandlers: MutableList> = ArrayList>() - private val myHtmlBuilder = StringBuilder() + private var errorMessage: String? = null + private val items = mutableListOf() + private var type: MessageType = MessageType.ERROR + private var hideErrorAction: Runnable? = null private var isChoppedMessage = false - private var myType: MessageType = MessageType.ERROR - init { - var errorMessage = if (myMessage == null) if (myError == null) null else getNormalizedMessage(myError) - else getNormalized( - myMessage) - val font = getInstance().getTextFont(true) - val fm = myBaseComponent.getFontMetrics(font) - myLongMessage = SwingUtilities.computeStringWidth(fm, errorMessage) > myBaseComponent.getWidth() * 3 / 4 - if (errorMessage != null) { - errorMessage = StringUtil.escapeXmlEntities(errorMessage).replace("\n", "
") + errorMessage = when { + message != null -> getNormalized(message) + error != null -> getNormalizedMessage(error) + else -> null } - - myHtmlBuilder.append("") - myHtmlBuilder.append("") } fun messageType(type: MessageType): Builder { - myType = type + this.type = type return this } - fun addIconLink(command: String?, tooltipText: @NlsContexts.Tooltip String?, realIcon: Icon, action: Runnable?): Builder { - val iconPath = GridUtil.getIconPath(realIcon) - - startActionColumn() - myHtmlBuilder.append("") - endActionColumn() - - if (action != null) { - myActions.put(command, action) - } - + fun addIconLink(tooltipText: @NlsContexts.Tooltip String?, realIcon: Icon, action: Runnable?): Builder { + items.add(IconLink(realIcon, { action?.run() }, tooltipText)) return this } fun addSpace(): Builder { - startActionColumn() - endActionColumn() + items.add(EmptySpace()) return this } - fun addLink(command: @NonNls String, linkHtml: @NlsActions.ActionText String, action: Runnable): Builder { - startActionColumn() - val mnemonicIndex = UIUtil.getDisplayMnemonicIndex(command) - val fixedCommand: @NlsSafe String = if (mnemonicIndex < 0) command else command.substring(0, mnemonicIndex) + command.substring(mnemonicIndex + 1) - ContainerUtil.addIfNotNull?>(myShowHideHandlers, - createMnemonicActionIfNeeded(fixedCommand, mnemonicIndex, action, myBaseComponent)) - myHtmlBuilder.append("").append(linkHtml).append("") - endActionColumn() - myActions.put(fixedCommand, action) + fun addLink(link: @NlsActions.ActionText String, mnemonicCode: Int? = null, action: Runnable): Builder { + items.add(TextLink(link, { action.run() }, mnemonicCode)) return this } fun addDetailsButton(): Builder { - val message: String = (if (myError == null) myMessage - else if (myError.stackTrace.size > 0) com.intellij.util.ExceptionUtil.getThrowableText(myError, "com.intellij.") - else myError.message)!! - if (StringUtil.contains(myHtmlBuilder, message)) return this - return addLink("details", DataGridBundle.message("action.details.text"), Runnable { - Messages.showIdeaMessageDialog(null, message, - DataGridBundle.message("dialog.title.query.error"), - arrayOf(CommonBundle.getOkButtonText()), 0, Messages.getErrorIcon(), null) - }) + val message: String? = when { + error == null -> message + error.stackTrace.size > 0 -> com.intellij.util.ExceptionUtil.getThrowableText(error, "com.intellij.") + else -> error.message + } + + return addLink(DataGridBundle.message("action.details.text")) { + Messages.showIdeaMessageDialog( + null, message, DataGridBundle.message("dialog.title.query.error"), + arrayOf(CommonBundle.getOkButtonText()), 0, Messages.getErrorIcon(), null) + } } fun addFullMessageButtonIfNeeded(): Builder { if (!isChoppedMessage) return this - return addLink("details", DataGridBundle.message("action.full.message.text"), Runnable { - Messages.showIdeaMessageDialog(null, myMessage, - DataGridBundle.message("dialog.title.query.error"), - arrayOf(CommonBundle.getOkButtonText()), 0, Messages.getErrorIcon(), null + return addLink(DataGridBundle.message("action.full.message.text"), KeyEvent.VK_F, Runnable { + val result = Messages.showIdeaMessageDialog( + null, message, DataGridBundle.message("dialog.title.query.error"), + arrayOf(CommonBundle.getCancelButtonText(), DataGridBundle.message("dialog.button.copy.and.close")), 0, Messages.getErrorIcon(), null ) + if (result == 1) { + message?.let { CopyPasteManager.copyTextToClipboard(message) } + } }) } fun addCloseButton(action: Runnable?): Builder { - return addIconLink(DataGridBundle.message("action.close.text"), DataGridBundle.message("tooltip.close.esc"), - AllIcons.Actions.Close, action) + hideErrorAction = action + return addIconLink(DataGridBundle.message("tooltip.close.esc"), AllIcons.Actions.Close, action) } fun build(): ErrorNotificationPanel { - myHtmlBuilder.append("
") - myHtmlBuilder.append(errorMessage) - myHtmlBuilder.append("
") - val result = ErrorNotificationPanel(myHtmlBuilder.toString(), myActions, myType) //NON-NLS - registerShowHideHandlers(result) + val result = ErrorNotificationPanel(errorMessage, items, hideErrorAction, this.type) return result } - private fun startActionColumn() { - myHtmlBuilder.append("
") - } - - private fun endActionColumn() { - myHtmlBuilder.append("
") - } - + @Suppress("HardCodedStringLiteral") private fun getNormalizedMessage(error: Throwable): @NlsContexts.NotificationContent String { - var sourceMessage = StringUtil.notNullize(error.message, - DataGridBundle.message( - "notification.content.unknown.problem.occurred.see.details")) + "kgmsdkgmksdfgnmksndfgknskdfgnksndgkndfkgnsdkfgnskdngkndsfgksnkgnfksdngksdnfgksndkgnsdkfgnksdnfgkndkfgnskngfksnkgfnksdnfgksdnfgknsdkgnslgnskldfnglksnfgksnfgksnkfgnksdfngksdnfgksdngksndfgknsdkf" - // In some cases source message contains stacktrace inside. Let's chop it + var sourceMessage = StringUtil.notNullize(error.message, DataGridBundle.message( + "notification.content.unknown.problem.occurred.see.details")) + // In some cases a source message contains a stacktrace inside. Let's chop it val divPos = sourceMessage.indexOf("\n\tat ") if (divPos != -1) { sourceMessage = sourceMessage.substring(0, divPos) @@ -238,59 +211,97 @@ class ErrorNotificationPanel private constructor( if (sourceMessage.length > limit) isChoppedMessage = true return StringUtil.trimLog(sourceMessage, limit + 1) } + } - private fun registerShowHideHandlers(component: JComponent) { - if (myShowHideHandlers.isEmpty()) return + private interface PanelItem { + fun buildComponent(): JComponent + } - component.addHierarchyListener(object : HierarchyListener { - private var myShownDisposable: Disposable? = null - override fun hierarchyChanged(e: HierarchyEvent) { - val c = e.component - if (c == null || (e.getChangeFlags() and HierarchyEvent.SHOWING_CHANGED.toLong()) <= 0) return + private class TextLink( + @Nls private val linkText: String, + private val onClickAction: () -> Unit, + private val mnemonicCode: Int? = null, + ) : PanelItem { + override fun buildComponent(): JComponent { + return JButton().apply { + foreground = JBUI.CurrentTheme.Link.Foreground.ENABLED + text = linkText + font = UIManager.getFont("ToolTip.font") - if (c.isShowing()) { - myShownDisposable = Disposer.newDisposable() - for (handler in myShowHideHandlers) { - handler.consume(myShownDisposable) - } - return - } + isOpaque = false + isContentAreaFilled = false + isBorderPainted = false + val fontMetrics = getFontMetrics(font) + preferredSize = JBDimension(fontMetrics.stringWidth(linkText), fontMetrics.height) + border = JBUI.Borders.empty() - if (myShownDisposable != null) Disposer.dispose(myShownDisposable!!) - myShownDisposable = null + isFocusable = true + if (mnemonicCode != null) { + mnemonic = mnemonicCode + addActionListener { onClickAction() } + MnemonicHelper.registerMnemonicAction(this, mnemonicCode) } - }) - } - - companion object { - private fun createMnemonicActionIfNeeded( - command: @NlsActions.ActionText String, - mnemonicIndex: Int, - runnable: Runnable, - component: JComponent? - ): Consumer? { - if (mnemonicIndex < 0) return null - return { parentDisposable: Disposable? -> - val a: DumbAwareAction = object : DumbAwareAction(command) { - override fun actionPerformed(e: AnActionEvent) { - runnable.run() - } - } - val modifiers = if (SystemInfo.isMac && !`is`( - "ide.mac.alt.mnemonic.without.ctrl") - ) InputEvent.ALT_MASK or InputEvent.CTRL_MASK - else InputEvent.ALT_MASK - val keyStroke = KeyStroke.getKeyStroke(command.get(mnemonicIndex).uppercaseChar().code, modifiers) - a.registerCustomShortcutSet(CustomShortcutSet(keyStroke), component, parentDisposable) - } as? Consumer + addMouseListener(MouseClickedListener(onClickAction, this)) + cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) } } } + private class IconLink( + private val icon: Icon, + private val onClickAction: () -> Unit, + @Nls private val tooltipText: String? = null, + ) : PanelItem { + override fun buildComponent(): JComponent { + return JLabel(icon).apply { + toolTipText = tooltipText + + addMouseListener(MouseClickedListener(onClickAction, this)) + cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) + } + } + } + + private class EmptySpace() : PanelItem { + override fun buildComponent(): JComponent { + return Box.createRigidArea(JBDimension(1, 20)) as JComponent + } + } + + private class MouseClickedListener( + private val onClickAction: () -> Unit, + private val component: JComponent, + ) : MouseListener { + private val originalFont = component.font + private val underlinedFont = originalFont.deriveFont(mapOf(TextAttribute.UNDERLINE to TextAttribute.UNDERLINE_ON)) + override fun mouseClicked(e: MouseEvent?) = onClickAction() + override fun mousePressed(e: MouseEvent?) = Unit + override fun mouseReleased(e: MouseEvent?) = Unit + override fun mouseEntered(e: MouseEvent?) { + component.font = underlinedFont + } + + override fun mouseExited(e: MouseEvent?) { + component.font = originalFont + } + } + + private class RequestFocusMouseListener( + private val component: JComponent, + ) : MouseListener { + override fun mouseClicked(e: MouseEvent?) { + component.requestFocusInWindow() + } + override fun mousePressed(e: MouseEvent?) = Unit + override fun mouseReleased(e: MouseEvent?) = Unit + override fun mouseEntered(e: MouseEvent?) = Unit + override fun mouseExited(e: MouseEvent?) = Unit + } + companion object { @JvmStatic - fun create(message: @NlsContexts.NotificationContent String?, error: Throwable?, baseComponent: JComponent): Builder { - return Builder(message, error, baseComponent) + fun create(message: @NlsContexts.NotificationContent String?, error: Throwable?): Builder { + return Builder(message, error) } } } diff --git a/grid/impl/src/run/ui/TableResultPanel.java b/grid/impl/src/run/ui/TableResultPanel.java index eb6b576be9fd..152c640613d6 100644 --- a/grid/impl/src/run/ui/TableResultPanel.java +++ b/grid/impl/src/run/ui/TableResultPanel.java @@ -67,10 +67,7 @@ import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; +import java.awt.event.*; import java.util.*; import java.util.List; import java.util.function.BiConsumer; @@ -825,11 +822,11 @@ public class TableResultPanel extends UserDataHolderBase void showError(@NotNull ErrorInfo errorInfo, final @Nullable DataGridRequestPlace source) { hideErrorPanel(); ErrorNotificationPanel.Builder builder = - ErrorNotificationPanel.create(errorInfo.getMessage(), errorInfo.getOriginalThrowable(), myMainPanel); + ErrorNotificationPanel.create(errorInfo.getMessage(), errorInfo.getOriginalThrowable()); List fixes = errorInfo.getFixes(); if (!fixes.isEmpty()) { for (ErrorInfo.Fix fix : fixes) { - builder.addLink(fix.getName(), fix.getName(), () -> GridHelper.get(this).applyFix(myProject, fix, null)); + builder.addLink(fix.getName(), null, () -> GridHelper.get(this).applyFix(myProject, fix, null)); } builder.addSpace(); } @@ -845,7 +842,7 @@ public class TableResultPanel extends UserDataHolderBase int c = viewColumnIdx.asInteger() + 1; //noinspection DialogTitleCapitalization String title = DataGridBundle.message("action.row.choice.col.text", r, c, c < 1 ? 0 : 1); - builder.addLink("navigate", title, () -> { + builder.addLink(title, KeyEvent.VK_N, () -> { if (viewRowIdx.isValid(this) && viewColumnIdx.isValid(this)) { scrollToLocally(this, viewRowIdx, viewColumnIdx); }