Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Combine error and warning boxes that have the same message #8154

Merged
merged 7 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 100 additions & 31 deletions src/ert/gui/suggestor/_suggestor_message.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
Expand All @@ -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")
Expand All @@ -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)
Expand All @@ -58,39 +55,111 @@ def __init__(
self.setGraphicsEffect(shadowEffect)
self.setContentsMargins(0, 0, 0, 0)

self.icon = icon
info.message = info.message.replace("<", "&lt;").replace(">", "&gt;")
self.lbl = QLabel(
'<div style="font-size: 16px; line-height: 24px;">'
+ f'<b style="color: {text_color}">'
+ header
+ "</b>"
+ info.message
+ "<p>"
+ info.location()
+ "</p>"
+ "</div>"
)
self._icon = icon
self._message = message.replace("<", "&lt;").replace(">", "&gt;")
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 " <a href=#morelocations>show less</a>"

def _expand_link(self) -> str:
return f" <a href=#morelocations>and {len(self._locations) - 1} more</a>"

def _collapsed_text(self) -> str:
location_paragraph = ""
if self._locations:
location_paragraph = self._locations[0]
location_paragraph = (
"<p>" + self._color_bold("location: ") + location_paragraph + "</p>"
)

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'<p>{self._color_bold("location:")}{loc}</p>'
first = False
else:
location_paragraphs += f"<p>{loc}</p>"

return self._text(location_paragraphs)

def _text(self, location: str) -> str:
return (
'<div style="font-size: 16px; line-height: 24px;">'
+ self._color_bold(self._header)
+ self._message
+ location
+ "</div>"
)

def _color_bold(self, text: str) -> str:
return f'<b style="color: {self._text_color}">{text}</b>'

@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,
)
27 changes: 20 additions & 7 deletions src/ert/gui/suggestor/suggestor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
23 changes: 23 additions & 0 deletions tests/unit_tests/gui/test_suggestor.py
Original file line number Diff line number Diff line change
@@ -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
Loading