diff --git a/src/ert/gui/suggestor/_suggestor_message.py b/src/ert/gui/suggestor/_suggestor_message.py
index 5037d30f22b..126c58dab5d 100644
--- a/src/ert/gui/suggestor/_suggestor_message.py
+++ b/src/ert/gui/suggestor/_suggestor_message.py
@@ -1,7 +1,5 @@
from __future__ import annotations
-from typing import TYPE_CHECKING
-
from PyQt5 import QtSvg
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QColor
@@ -10,9 +8,10 @@
QHBoxLayout,
QLabel,
QSizePolicy,
+ QVBoxLayout,
QWidget,
)
-from typing_extensions import Self
+from typing_extensions import Any, Self
from ._colors import (
BLUE_BACKGROUND,
@@ -23,9 +22,6 @@
YELLOW_TEXT,
)
-if TYPE_CHECKING:
- from ert.config import ErrorInfo, WarningInfo
-
def _svg_icon(image_name: str) -> QtSvg.QSvgWidget:
widget = QtSvg.QSvgWidget(f"img:{image_name}.svg")
@@ -40,7 +36,8 @@ def __init__(
text_color: str,
bg_color: str,
icon: QWidget,
- info: ErrorInfo,
+ message: str,
+ locations: list[str],
) -> None:
super().__init__()
self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground)
@@ -58,39 +55,111 @@ def __init__(
self.setGraphicsEffect(shadowEffect)
self.setContentsMargins(0, 0, 0, 0)
- self.icon = icon
- info.message = info.message.replace("<", "<").replace(">", ">")
- self.lbl = QLabel(
- '
'
- + f'
'
- + header
- + ""
- + info.message
- + "
"
- + info.location()
- + "
"
- + "
"
- )
+ self._icon = icon
+ self._message = message.replace("<", "<").replace(">", ">")
+ self._locations = locations
+ self._header = header
+ self._text_color = text_color
+
+ self._hbox = QHBoxLayout()
+ self._hbox.setContentsMargins(16, 16, 16, 16)
+ self._hbox.addWidget(self._icon, alignment=Qt.AlignmentFlag.AlignTop)
+ self.setLayout(self._hbox)
+
+ self.lbl = QLabel(self._collapsed_text())
+ self.lbl.setOpenExternalLinks(False)
self.lbl.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.lbl.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
self.lbl.setWordWrap(True)
+ self._expanded = False
+ if len(self._locations) > 1:
+ self._expand_collapse_label = QLabel(self._expand_link())
+ self._expand_collapse_label.setOpenExternalLinks(False)
+ self._expand_collapse_label.linkActivated.connect(self._toggle_expand)
+
+ self._vbox = QWidget()
+ layout = QVBoxLayout()
+ self._vbox.setLayout(layout)
+ layout.addWidget(self.lbl)
+ layout.addWidget(self._expand_collapse_label)
+ self._hbox.addWidget(self._vbox, alignment=Qt.AlignmentFlag.AlignTop)
+ else:
+ self._expand_collapse_label = QLabel()
+ self._hbox.addWidget(self.lbl, alignment=Qt.AlignmentFlag.AlignTop)
+
+ def _toggle_expand(self, _link: Any) -> None:
+ if self._expanded:
+ self.lbl.setText(self._collapsed_text())
+ self._expand_collapse_label.setText(self._expand_link())
+ else:
+ self.lbl.setText(self._expanded_text())
+ self._expand_collapse_label.setText(self._hide_link())
+ self._expanded = not self._expanded
+
+ def _hide_link(self) -> str:
+ return " show less"
+
+ def _expand_link(self) -> str:
+ return f" and {len(self._locations) - 1} more"
+
+ def _collapsed_text(self) -> str:
+ location_paragraph = ""
+ if self._locations:
+ location_paragraph = self._locations[0]
+ location_paragraph = (
+ "" + self._color_bold("location: ") + location_paragraph + "
"
+ )
- self.hbox = QHBoxLayout()
- self.hbox.setContentsMargins(16, 16, 16, 16)
- self.hbox.addWidget(self.icon, alignment=Qt.AlignmentFlag.AlignTop)
- self.hbox.addWidget(self.lbl, alignment=Qt.AlignmentFlag.AlignTop)
- self.setLayout(self.hbox)
+ return self._text(location_paragraph)
+
+ def _expanded_text(self) -> str:
+ location_paragraphs = ""
+ first = True
+ for loc in self._locations:
+ if first:
+ location_paragraphs += f'{self._color_bold("location:")}{loc}
'
+ first = False
+ else:
+ location_paragraphs += f"{loc}
"
+
+ return self._text(location_paragraphs)
+
+ def _text(self, location: str) -> str:
+ return (
+ ''
+ + self._color_bold(self._header)
+ + self._message
+ + location
+ + "
"
+ )
+
+ def _color_bold(self, text: str) -> str:
+ return f'{text}'
@classmethod
- def error_msg(cls, info: ErrorInfo) -> Self:
- return cls("Error: ", RED_TEXT, RED_BACKGROUND, _svg_icon("error"), info)
+ def error_msg(cls, message: str, locations: list[str]) -> Self:
+ return cls(
+ "Error: ", RED_TEXT, RED_BACKGROUND, _svg_icon("error"), message, locations
+ )
@classmethod
- def warning_msg(cls, info: WarningInfo) -> Self:
+ def warning_msg(cls, message: str, locations: list[str]) -> Self:
return cls(
- "Warning: ", YELLOW_TEXT, YELLOW_BACKGROUND, _svg_icon("warning"), info
+ "Warning: ",
+ YELLOW_TEXT,
+ YELLOW_BACKGROUND,
+ _svg_icon("warning"),
+ message,
+ locations,
)
@classmethod
- def deprecation_msg(cls, info: WarningInfo) -> Self:
- return cls("Deprecation: ", BLUE_TEXT, BLUE_BACKGROUND, _svg_icon("bell"), info)
+ def deprecation_msg(cls, message: str, locations: list[str]) -> Self:
+ return cls(
+ "Deprecation: ",
+ BLUE_TEXT,
+ BLUE_BACKGROUND,
+ _svg_icon("bell"),
+ message,
+ locations,
+ )
diff --git a/src/ert/gui/suggestor/suggestor.py b/src/ert/gui/suggestor/suggestor.py
index df62218bcbb..5f8f397f3dc 100644
--- a/src/ert/gui/suggestor/suggestor.py
+++ b/src/ert/gui/suggestor/suggestor.py
@@ -3,7 +3,8 @@
import functools
import logging
import webbrowser
-from typing import TYPE_CHECKING, Callable, Dict, List, Optional
+from collections import defaultdict
+from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Sequence
from PyQt5.QtGui import QCursor
from qtpy.QtCore import Qt
@@ -33,6 +34,13 @@ def _clicked_help_button(menu_label: str, link: str) -> None:
webbrowser.open(link)
+def _combine_messages(infos: Sequence[ErrorInfo]) -> list[tuple[str, list[str]]]:
+ combined = defaultdict(list)
+ for info in infos:
+ combined[info.message].append(info.location())
+ return list(combined.items())
+
+
LIGHT_GREY = "#f7f7f7"
MEDIUM_GREY = "#eaeaea"
HEAVY_GREY = "#dcdcdc"
@@ -232,6 +240,7 @@ def _messages(
NUM_COLUMNS = 2
suggest_msgs = QWidget(parent=self)
+ suggest_msgs.setObjectName("suggestor_messages")
suggest_msgs.setContentsMargins(0, 0, 16, 0)
suggest_layout = QGridLayout()
suggest_layout.setContentsMargins(0, 0, 0, 0)
@@ -241,20 +250,24 @@ def _messages(
column = 0
row = 0
num = 0
- for msg in errors:
- suggest_layout.addWidget(SuggestorMessage.error_msg(msg), row, column)
+ for combined in _combine_messages(errors):
+ suggest_layout.addWidget(SuggestorMessage.error_msg(*combined), row, column)
if column:
row += 1
column = (column + 1) % NUM_COLUMNS
num += 1
- for msg in warnings:
- suggest_layout.addWidget(SuggestorMessage.warning_msg(msg), row, column)
+ for combined in _combine_messages(warnings):
+ suggest_layout.addWidget(
+ SuggestorMessage.warning_msg(*combined), row, column
+ )
if column:
row += 1
column = (column + 1) % NUM_COLUMNS
num += 1
- for msg in deprecations:
- suggest_layout.addWidget(SuggestorMessage.deprecation_msg(msg), row, column)
+ for combined in _combine_messages(deprecations):
+ suggest_layout.addWidget(
+ SuggestorMessage.deprecation_msg(*combined), row, column
+ )
if column:
row += 1
column = (column + 1) % NUM_COLUMNS
diff --git a/tests/unit_tests/gui/test_suggestor.py b/tests/unit_tests/gui/test_suggestor.py
new file mode 100644
index 00000000000..a04366e07cc
--- /dev/null
+++ b/tests/unit_tests/gui/test_suggestor.py
@@ -0,0 +1,23 @@
+import pytest
+from qtpy.QtWidgets import QWidget
+
+from ert.config import ErrorInfo
+from ert.gui.suggestor import Suggestor
+
+
+@pytest.mark.parametrize(
+ "errors, expected_num",
+ [
+ ([ErrorInfo("msg_1")], 1),
+ ([ErrorInfo("msg_1"), ErrorInfo("msg_2")], 2),
+ ([ErrorInfo("msg_1"), ErrorInfo("msg_1"), ErrorInfo("msg_2")], 2),
+ ([ErrorInfo("msg_1"), ErrorInfo("msg_2"), ErrorInfo("msg_3")], 3),
+ ],
+)
+def test_suggestor_combines_errors_with_the_same_message(qtbot, errors, expected_num):
+ suggestor = Suggestor(errors, [], [], lambda: None)
+ msgs = suggestor.findChild(QWidget, name="suggestor_messages")
+ assert msgs is not None
+ msg_layout = msgs.layout()
+ assert msg_layout is not None
+ assert msg_layout.count() == expected_num