194 lines
7.8 KiB
Python

#! python3 # noqa: E265
# standard library
import logging
from functools import partial
from typing import Callable, Literal, Optional, Union
# PyQGIS
from qgis.core import Qgis, QgsMessageLog, QgsMessageOutput
from qgis.gui import QgsMessageBar
from qgis.PyQt.QtWidgets import QPushButton, QWidget
from qgis.utils import iface
# project package
import gn_tools.toolbelt.preferences as plg_prefs_hdlr
from gn_tools.__about__ import __title__
# ############################################################################
# ########## Classes ###############
# ##################################
class PlgLogger(logging.Handler):
"""Python logging handler supercharged with QGIS useful methods."""
@staticmethod
def log(
message: str,
application: str = __title__,
log_level: Union[
Qgis.MessageLevel, Literal[0, 1, 2, 3, 4]
] = Qgis.MessageLevel.Info,
push: bool = False,
duration: Optional[int] = None,
# widget
button: bool = False,
button_text: Optional[str] = None,
button_more_text: Optional[str] = None,
button_connect: Optional[Callable] = None,
# parent
parent_location: Optional[QWidget] = None,
):
"""Send messages to QGIS messages windows and to the user as a message bar. \
Plugin name is used as title. If debug mode is disabled, only warnings (1) and \
errors (2) or with push are sent.
:param message: message to display
:type message: str
:param application: name of the application sending the message. \
Defaults to __about__.__title__
:type application: str, optional
:param log_level: message level. Possible values: any values of enum \
`Qgis.MessageLevel`. For legacy purposes, it's also possible to pass \
corresponding integers but it's not recommended anymore. Legacy values: \
0 (info), 1 (warning), 2 (critical), 3 (success), 4 (none - grey). Defaults \
to Qgis.MessageLevel(0) (info)
:type log_level: Union[Qgis.MessageLevel, Literal[0, 1, 2, 3, 4]], optional
:param push: also display the message in the QGIS message bar in addition to \
the log, defaults to False
:type push: bool, optional
:param duration: duration of the message in seconds. If not set, the \
duration is calculated from the log level: `(log_level + 1) * 3`. seconds. \
If set to 0, then the message must be manually dismissed by the user. \
Defaults to None.
:type duration: int, optional
:param button: display a button in the message bar. Defaults to False.
:type button: bool, optional
:param button_text: text label of the button. Defaults to None.
:type button_text: str, optional
:param button_more_text: text to display within the QgsMessageOutput
:type button_more_text: str, optional
:param button_connect: function to be called when the button is pressed. \
If not set, a simple dialog (QgsMessageOutput) is used to dislay the message. \
Defaults to None.
:type button_connect: Callable, optional
:param parent_location: parent location widget. \
If not set, QGIS canvas message bar is used to push message, \
otherwise if a QgsMessageBar is available in parent_location it is used instead. \
Defaults to None.
:type parent_location: Widget, optional
:Example:
.. code-block:: python
# using enums from Qgis:
# Qgis.Info, Qgis.MessageLevel.Warning, Qgis.MessageLevel.Critical, Qgis.MessageLevel.Success, Qgis.MessageLevel.NoLevel
from qgis.core import Qgis
log(message="Plugin loaded - INFO", log_level=Qgis.MessageLevel.Info, push=False)
log(
message="Something went wrong but it's not blocking",
log_level=Qgis.MessageLevel.Warning
)
log(
message="Plugin failed to load - CRITICAL",
log_level=Qgis.MessageLevel(2),
push=True
)
# LEGACY - using integers:
log(message="Plugin loaded - INFO", log_level=Qgis.MessageLevel.Info, push=False)
log(message="Plugin loaded - WARNING", log_level=Qgis.MessageLevel.Warning, push=1, duration=5)
log(message="Plugin loaded - ERROR", log_level=Qgis.MessageLevel.Critical, push=1, duration=0)
log(
message="Plugin loaded - SUCCESS",
log_level=Qgis.MessageLevel.Success,
push=1,
duration=10,
button=True
)
log(
message="Plugin loaded",
log_level=Qgis.MessageLevel.Critical,
push=1,
duration=0
button=True,
button_label=self.tr("See details"),
button_more_text=detailed_error_message
)
log(message="Plugin loaded - TEST", log_level=Qgis.MessageLevel.NoLevel, push=0)
"""
# if not debug mode and not push, let's ignore INFO, SUCCESS and TEST
debug_mode = plg_prefs_hdlr.PlgOptionsManager.get_plg_settings().debug_mode
if not debug_mode and not push and (log_level < 1 or log_level > 2):
return
# if log_level is an int, convert it to Qgis.MessageLevel
if isinstance(log_level, int):
log_level = Qgis.MessageLevel(log_level)
# ensure message is a string
if not isinstance(message, str):
try:
message = str(message)
except Exception as err:
err_msg = "Log message must be a string, not: {}. Trace: {}".format(
type(message), err
)
logging.error(err_msg)
message = err_msg
# send it to QGIS messages panel
QgsMessageLog.logMessage(
message=message, tag=application, notifyUser=push, level=log_level
)
# optionally, display message on QGIS Message bar (above the map canvas)
if push and iface is not None:
msg_bar = None
# QGIS or custom dialog
if parent_location and isinstance(parent_location, QWidget):
msg_bar = parent_location.findChild(QgsMessageBar)
if not msg_bar:
msg_bar = iface.messageBar()
# calc duration
if duration is None:
duration = (log_level + 1) * 3
# create message with/out a widget
if button:
# create output message
notification = iface.messageBar().createMessage(
title=application, text=message
)
widget_button = QPushButton(button_text or "More...")
if button_connect:
widget_button.clicked.connect(button_connect)
else:
mini_dlg: QgsMessageOutput = QgsMessageOutput.createMessageOutput()
mini_dlg.setTitle(application)
mini_dlg.setMessage(
f"{message}\n{button_more_text}",
QgsMessageOutput.MessageType.MessageText,
)
widget_button.clicked.connect(partial(mini_dlg.showMessage, False))
notification.layout().addWidget(widget_button)
msg_bar.pushWidget(
widget=notification, level=log_level, duration=duration
)
else:
# send simple message
msg_bar.pushMessage(
title=application,
text=message,
level=log_level,
duration=duration,
)