from collections import Counter
import os
import tempfile
import ctypes

from qgis.PyQt.QtCore import Qt, QSize, QSizeF, QRectF, QEvent, QTimer, QPoint, QPointF, QSettings, QByteArray, QUrl, QProcess
from qgis.PyQt.QtWidgets import (
    QWidget,
    QVBoxLayout,
    QLabel,
    QFrame,
    QHBoxLayout,
    QLineEdit,
    QStackedLayout,
    QSizePolicy,
    QMenu,
    QWidgetAction,
    QSlider,
    QFileDialog,
    QApplication,
    QMessageBox,
    QDockWidget,
    QPushButton,
    QScrollArea,
)

from qgis.PyQt.QtGui import (
    QPixmap,
    QImage,
    QPainter,
    QColor,
    QPen,
    QBrush,
    QAction,
    QFont,
    QIcon,
    QDesktopServices,
    QPainterPath
)

from qgis.PyQt.QtSvg import QSvgRenderer

from qgis.PyQt.QtPrintSupport import QPrinter

from qgis.core import (
    Qgis,
    QgsMessageLog,
    QgsApplication,
    QgsProject,
    QgsFeatureRequest,
    QgsRectangle,
    QgsGeometry,
    QgsPointXY,
    QgsDistanceArea,
    QgsCoordinateTransform,
    QgsWkbTypes,
    QgsMapLayerType,
    QgsCategorizedSymbolRenderer,
    QgsGraduatedSymbolRenderer,
    QgsExpression,
    QgsExpressionContext,
    QgsExpressionContextUtils,
    QgsPrintLayout,
    QgsLayoutItemMap,
    QgsLayoutItemPicture,
    QgsLayoutItemScaleBar,
    QgsLayoutExporter,
    QgsLayoutPoint,
    QgsLayoutSize,
    QgsUnitTypes
)

from qgis.utils import plugins

class LivingLegendNorthArrowWidget(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
        self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
        self.setWindowFlags(Qt.WindowType.Widget)
        self.setFixedSize(30, 46)

        self.svg_content = b'''<svg xmlns="http://www.w3.org/2000/svg" height="348" width="226" viewBox="0 0 226 348">
<path d="M5.53,240.17l108.38-113.54,106.91,112.07-106.91-229.3z" stroke="#000000" stroke-width="2" stroke-linejoin="round" fill="#ffffff"/>
<path d="m113.91,9.2162,0,117.41,106.36,111.33z" stroke="#000000" stroke-width="2" stroke-linejoin="round" fill="#000000"/>
<path d="m145.05,321.47-10.375,0-44.125-74.375,0.125,5.625c0.12498,2.1251,0.18748,3.9167,0.1875,5.375v63.375h-8.625v-85.312h13.375l41.25,69.562-0.125-5.9375c-0.0834-2.3333-0.12506-4.3958-0.125-6.1875v-57.438h8.4375z" fill="#000000"/>
</svg>'''

        self.renderer = QSvgRenderer(QByteArray(self.svg_content))

    def paintEvent(self, event):
        painter = QPainter(self)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
        painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform, True)
        self.renderer.render(painter, QRectF(0, 0, self.width(), self.height()))
        painter.end()


class LivingLegendScaleBarWidget(QWidget):
    def __init__(self, owner, parent=None):
        super().__init__(parent)

        self.owner = owner
        self.total_length = 100.0
        self.segment_count = 2
        self.pixel_length = 120

        self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
        self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
        self.setWindowFlags(Qt.WindowType.Widget)

        self.setFixedSize(220, 38)

    def update_values(self):
        try:
            self.total_length = float(self.owner.nice_scalebar_segment_length())
        except Exception:
            self.total_length = 100.0

        try:
            map_units_per_pixel = float(self.owner.canvas.mapUnitsPerPixel())
        except Exception:
            map_units_per_pixel = 1.0

        if map_units_per_pixel <= 0:
            map_units_per_pixel = 1.0

        pixel_length = int(round(self.total_length / map_units_per_pixel))
        pixel_length = max(50, min(pixel_length, 260))

        self.pixel_length = pixel_length

        start_label = "0"
        middle_label = f"{int(round(self.total_length / 2.0))}"
        end_label = f"{int(round(self.total_length))}"
        unit_label = "m"

        try:
            start_width = self.fontMetrics().horizontalAdvance(start_label)
            middle_width = self.fontMetrics().horizontalAdvance(middle_label)
            end_width = self.fontMetrics().horizontalAdvance(end_label)
            unit_width = self.fontMetrics().horizontalAdvance(unit_label)
        except Exception:
            start_width = 10
            middle_width = 30
            end_width = 30
            unit_width = 12

        # The final number is centered over the final tick.
        # The unit is drawn separately to the right, like the export.
        right_padding = int(end_width / 2) + unit_width + 14

        left_margin = self.owner.scale_bar_margin_x_px()

        width = max(
            left_margin + self.pixel_length + right_padding + 12,
            left_margin + self.pixel_length + middle_width + 24,
            90
        )

        self.setFixedSize(width, 38)
        self.update()

    def paintEvent(self, event):
        painter = QPainter(self)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
        painter.setRenderHint(QPainter.RenderHint.TextAntialiasing, True)

        painter.setPen(QPen(QColor(0, 0, 0), 1))
        painter.setBrush(Qt.BrushStyle.NoBrush)

        font = QFont("Arial", 9)
        painter.setFont(font)

        x0 = self.owner.scale_bar_margin_x_px()
        y_line = self.height() - self.owner.scale_bar_margin_y_px()

        x1 = x0 + self.pixel_length
        x_mid = x0 + int(self.pixel_length / 2)

        tick_height = 10
        label_gap = 5

        # Main scale bar line
        painter.drawLine(x0, y_line, x1, y_line)

        # Ticks upward, matching the exported QGIS line-ticks-up feel
        painter.drawLine(x0, y_line, x0, y_line - tick_height)
        painter.drawLine(x_mid, y_line, x_mid, y_line - tick_height)
        painter.drawLine(x1, y_line, x1, y_line - tick_height)

        start_label = "0"
        middle_label = f"{int(round(self.total_length / 2.0))}"
        end_label = f"{int(round(self.total_length))}"
        unit_label = "m"

        label_y = y_line - tick_height - label_gap

        painter.drawText(x0 - 4, label_y, start_label)

        middle_width = painter.fontMetrics().horizontalAdvance(middle_label)
        painter.drawText(
            x_mid - int(middle_width / 2),
            label_y,
            middle_label
        )

        # Center only the final number above the final tick.
        end_width = painter.fontMetrics().horizontalAdvance(end_label)
        painter.drawText(
            x1 - int(end_width / 2),
            label_y,
            end_label
        )

        # Draw the unit separately to the right of the final number.
        unit_offset = 4
        painter.drawText(
            x1 + int(end_width / 2) + unit_offset,
            label_y,
            unit_label
        )

        painter.end()

class EditableLegendLabel(QWidget):

    def __init__(
        self,
        text,
        on_text_changed=None,
        on_editing_started=None,
        on_editing_finished=None,
        parent=None
    ):
        super().__init__(parent)

        self.on_text_changed = on_text_changed
        self.on_editing_started = on_editing_started
        self.on_editing_finished = on_editing_finished

        self.original_text = text
        self.is_editing = False

        self.setSizePolicy(
            QSizePolicy.Policy.Preferred,
            QSizePolicy.Policy.Fixed
        )

        self.stack = QStackedLayout()
        self.stack.setContentsMargins(0, 0, 0, 0)
        self.stack.setSpacing(0)
        self.setLayout(self.stack)

        self.label = QLabel(text)
        self.label.setObjectName("legendItemText")
        self.label.setToolTip("Double click to edit legend label")
        self.label.setSizePolicy(
            QSizePolicy.Policy.Preferred,
            QSizePolicy.Policy.Fixed
        )

        self.edit = QLineEdit(text)
        self.edit.setObjectName("legendItemEditor")
        self.edit.setFrame(False)
        self.edit.setSizePolicy(
            QSizePolicy.Policy.Preferred,
            QSizePolicy.Policy.Fixed
        )

        self.stack.addWidget(self.label)
        self.stack.addWidget(self.edit)

        self.stack.setCurrentWidget(self.label)

        self.label.mouseDoubleClickEvent = self.start_editing
        self.edit.editingFinished.connect(self.finish_editing)

    def start_editing(self, event=None):
        if self.is_editing:
            return

        self.is_editing = True
        self.original_text = self.label.text()

        if self.on_editing_started:
            self.on_editing_started()

        self.edit.setText(self.original_text)
        self.edit.setMinimumWidth(
            max(80, self.label.fontMetrics().horizontalAdvance(self.original_text) + 24)
        )

        self.stack.setCurrentWidget(self.edit)
        self.edit.setFocus()
        self.edit.selectAll()

    def finish_editing(self):
        if not self.is_editing:
            return

        new_text = self.edit.text().strip()

        if new_text == "":
            new_text = self.original_text

        self.label.setText(new_text)
        self.stack.setCurrentWidget(self.label)

        self.is_editing = False

        if self.on_text_changed and new_text != self.original_text:
            self.on_text_changed(new_text)

        if self.on_editing_finished:
            self.on_editing_finished()


class LivingLegendWidget(QWidget):

    def __init__(self, iface):
        self.iface = iface
        self.canvas = iface.mapCanvas()

        try:
            self.parent_widget = self.canvas.viewport()
        except Exception:
            self.parent_widget = self.canvas

        super().__init__(self.parent_widget)

        self.connected_layer_ids = set()

        self.settings = QSettings("LivingLegend", "LivingLegend")

        self.show_feature_counts = self.read_bool_setting(
            "show_feature_counts",
            False
        )

        self.show_feature_area = self.read_bool_setting(
            "show_feature_area",
            False
        )

        self.sync_label_changes_with_project = self.read_bool_setting(
            "sync_label_changes_with_project",
            True
        )
        self.show_layer_name = self.read_bool_setting(
            "show_layer_name",
            True
        )

        self.show_legend_title = self.read_bool_setting(
            "show_legend_title",
            False
        )

        self.legend_title_text = str(
            self.settings.value("legend_title_text", "Legend")
        )

        if self.legend_title_text.strip() == "":
            self.legend_title_text = "Legend"

        self.stick_to_corner = self.read_bool_setting(
            "stick_to_corner",
            True
        )

        self.sticky_corner = str(
            self.settings.value("sticky_corner", "top_right")
        )

        if self.sticky_corner not in [
            "top_left",
            "top_center",
            "top_right",
            "right_center",
            "bottom_right",
            "bottom_center",
            "bottom_left",
            "left_center"
        ]:
            self.sticky_corner = "top_right"

        self.scale_bar_enabled = self.read_bool_setting(
            "scale_bar_enabled",
            True
        )

        self.north_arrow_enabled = self.read_bool_setting(
            "north_arrow_enabled",
            True
        )

        self.scale_bar_position = str(
            self.settings.value("scale_bar_position", "bottom_left")
        )

        self.north_arrow_position = str(
            self.settings.value("north_arrow_position", "top_left")
        )

        if self.scale_bar_position not in [
            "top_left",
            "top_center",
            "top_right",
            "right_center",
            "bottom_right",
            "bottom_center",
            "bottom_left",
            "left_center"
        ]:
            self.scale_bar_position = "bottom_left"

        if self.north_arrow_position not in [
            "top_left",
            "top_center",
            "top_right",
            "right_center",
            "bottom_right",
            "bottom_center",
            "bottom_left",
            "left_center"
        ]:
            self.north_arrow_position = "top_left"

        self.scale_bar_widget = None
        self.north_arrow_widget = None

        try:
            self.legend_opacity = float(
                self.settings.value("legend_opacity", 0.82)
            )
        except Exception:
            self.legend_opacity = 0.82

        self.legend_opacity = max(0.35, min(1.0, self.legend_opacity))

        # Used when sync is disabled.
        # These labels are only local to the living legend session/project view.
        self.local_label_overrides = {}

        # Used for items hidden only inside the Living Legend.
        # This does not change QGIS layer visibility or renderer item visibility.
        self.hidden_legend_items = {}

        self.default_margin = 8
        self.position_has_been_initialized = False
        self.position_restore_attempts = 0

        self.dragging = False
        self.drag_offset = QPoint(0, 0)
        self.drag_start_global_pos = QPoint(0, 0)
        self.drag_has_moved = False
        self.drag_move_threshold_px = 4

        self.legend_update_pending = False
        self.legend_update_active = False
        self.editing_active = False

        self.resize_update_pending = False
        
        self.legend_hidden_due_to_no_items = False

        self.update_timer = QTimer(self)
        self.update_timer.setSingleShot(True)
        self.update_timer.timeout.connect(self.updateLegend)

        self.resize_timer = QTimer(self)
        self.resize_timer.setSingleShot(True)
        self.resize_timer.timeout.connect(self.adjust_size_to_content)

        self.setObjectName("LivingLegendOverlay")

        self.setWindowFlags(Qt.WindowType.Widget)
        self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
        self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
        self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, False)

        self.setMouseTracking(True)

        try:
            self.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)
        except Exception:
            try:
                self.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)
            except Exception as exception:
                self.log_ignored_exception(
                    "Could not set context menu policy on legend overlay",
                    exception
                )

        self.setSizePolicy(
            QSizePolicy.Policy.Fixed,
            QSizePolicy.Policy.Fixed
        )

        self.main_layout = QVBoxLayout()
        self.main_layout.setContentsMargins(0, 0, 0, 0)
        self.main_layout.setSpacing(0)
        self.setLayout(self.main_layout)

        self.panel = QFrame()
        self.panel.setObjectName("legendPanel")
        self.panel.setMouseTracking(True)
        self.panel.setAttribute(
            Qt.WidgetAttribute.WA_StyledBackground,
            True
        )
        self.panel.setAutoFillBackground(False)

        try:
            self.panel.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)
        except Exception:
            try:
                self.panel.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)
            except Exception as exception:
                self.log_ignored_exception(
                    "Could not set context menu policy on legend panel",
                    exception
                )

        self.panel_layout = QVBoxLayout()
        self.panel_layout.setContentsMargins(0, 0, 0, 0)
        self.panel_layout.setSpacing(0)
        self.panel.setLayout(self.panel_layout)

        self.scroll_area = QScrollArea(self.panel)
        self.scroll_area.setWidgetResizable(True)
        self.scroll_area.setFrameShape(QFrame.Shape.NoFrame)
        self.scroll_area.setHorizontalScrollBarPolicy(
            Qt.ScrollBarPolicy.ScrollBarAlwaysOff
        )
        self.scroll_area.setVerticalScrollBarPolicy(
            Qt.ScrollBarPolicy.ScrollBarAsNeeded
        )
        self.scroll_area.setAttribute(
            Qt.WidgetAttribute.WA_TranslucentBackground,
            True
        )
        self.scroll_area.setAutoFillBackground(False)

        try:
            self.scroll_area.viewport().setAttribute(
                Qt.WidgetAttribute.WA_TranslucentBackground,
                True
            )
            self.scroll_area.viewport().setAutoFillBackground(False)
        except Exception as exception:
            self.log_ignored_exception(
                "Could not make legend scroll viewport transparent",
                exception
            )

        self.scroll_content = QWidget()
        self.scroll_content.setObjectName("legendScrollContent")
        self.scroll_content.setAttribute(
            Qt.WidgetAttribute.WA_TranslucentBackground,
            True
        )
        self.scroll_content.setAutoFillBackground(False)
        self.content_layout = QVBoxLayout()
        self.content_layout.setContentsMargins(7, 5, 7, 6)
        self.content_layout.setSpacing(3)
        self.scroll_content.setLayout(self.content_layout)

        self.scroll_area.setWidget(self.scroll_content)
        self.panel_layout.addWidget(self.scroll_area)
        self.main_layout.addWidget(self.panel)

        self.register_drag_filter(self.scroll_area)
        self.register_drag_filter(self.scroll_area.viewport())
        self.register_drag_filter(self.scroll_content)

        self.apply_style()

        # Temporary position until the legend knows its final content size.
        # The final default position is top right and is set in adjust_size_to_content().
        self.move(0, 0)

        self.installEventFilter(self)
        self.panel.installEventFilter(self)
        self.parent_widget.installEventFilter(self)

        self.canvas.extentsChanged.connect(self.requestLegendUpdate)

        QgsProject.instance().layersAdded.connect(self.on_layers_added)
        QgsProject.instance().layersRemoved.connect(self.requestLegendUpdate)

        root = QgsProject.instance().layerTreeRoot()
        root.visibilityChanged.connect(self.requestLegendUpdate)

        try:
            root.layerOrderChanged.connect(self.requestLegendUpdate)
        except Exception as exception:
            self.log_ignored_exception(
                "Could not connect layer order change signal",
                exception
            )

        self.connect_existing_layer_signals()
        self.updateLegend()

        self.create_decoration_widgets()
        self.update_decoration_widgets()

        try:
            self.canvas.extentsChanged.connect(self.update_decoration_widgets)
        except Exception as exception:
            self.log_ignored_exception(
                "Could not connect canvas extent signal for decoration updates",
                exception
            )


    def read_bool_setting(self, key, default_value):
        value = self.settings.value(key, default_value)

        if isinstance(value, bool):
            return value

        if isinstance(value, str):
            return value.lower() in ["true", "1", "yes", "on"]

        return bool(value)

    def float_or_none(self, value):
        try:
            return float(value)
        except (TypeError, ValueError) as exception:
            self.log_ignored_exception(
                "Could not convert value to float",
                exception
            )
            return None

    def geometry_bounding_box_or_none(self, geometry):
        try:
            return geometry.boundingBox()
        except Exception as exception:
            self.log_ignored_exception(
                "Could not calculate feature geometry bounding box",
                exception
            )
            return None

    def log_ignored_exception(self, message, exception):
        QgsMessageLog.logMessage(
            f"{message}: {exception}",
            "Living Legend",
            Qgis.MessageLevel.Info
        )

    def apply_style(self):
        panel_alpha = int(255 * self.legend_opacity)
        panel_alpha = max(70, min(255, panel_alpha))

        self.setStyleSheet(f"""
            QWidget#LivingLegendOverlay {{
                background: transparent;
            }}

            QFrame#legendPanel {{
                background-color: rgba(255, 255, 255, {panel_alpha});
                border: 1px solid rgba(20, 20, 20, 170);
                border-radius: 3px;
            }}

            QLabel#layerTitle {{
                color: rgb(20, 20, 20);
                font-weight: 700;
                font-size: 9pt;
                padding: 0px;
                margin: 0px;
            }}

            QLineEdit#layerTitleEditor {{
                color: rgb(20, 20, 20);
                background-color: rgba(255, 255, 255, 250);
                border: 1px solid rgba(70, 120, 170, 190);
                border-radius: 2px;
                padding: 0px 2px 0px 2px;
                font-weight: 700;
                font-size: 9pt;
            }}

            QLabel#legendTitle {{
                color: rgb(20, 20, 20);
                font-weight: 700;
                font-size: 11pt;
                padding: 0px 0px 3px 0px;
                margin: 0px;
            }}

            QLabel#legendItemText {{
                color: rgb(45, 45, 45);
                font-size: 9pt;
                padding: 0px;
                margin: 0px;
            }}

            QLabel#legendCountText {{
                color: rgb(70, 70, 70);
                font-size: 9pt;
                padding: 0px;
                margin: 0px;
            }}

            QLabel#emptyLegendText {{
                color: rgb(70, 70, 70);
                font-size: 9pt;
                padding: 2px;
            }}

            QLineEdit#legendItemEditor {{
                color: rgb(20, 20, 20);
                background-color: rgba(255, 255, 255, 250);
                border: 1px solid rgba(70, 120, 170, 190);
                border-radius: 2px;
                padding: 0px 2px 0px 2px;
                font-size: 9pt;
            }}

            QScrollArea {{
                background: transparent;
                border: none;
            }}

            QScrollArea::viewport {{
                background: transparent;
                border: none;
            }}

            QScrollArea > QWidget > QWidget {{
                background: transparent;
            }}

            QWidget#legendScrollContent {{
                background: transparent;
            }}

            QScrollBar:vertical {{
                background: rgba(255, 255, 255, 40);
                width: 10px;
                margin: 2px 1px 2px 1px;
                border-radius: 4px;
            }}

            QScrollBar::handle:vertical {{
                background: rgba(70, 70, 70, 110);
                min-height: 24px;
                border-radius: 4px;
            }}

            QScrollBar::handle:vertical:hover {{
                background: rgba(70, 70, 70, 160);
            }}

            QScrollBar::add-line:vertical,
            QScrollBar::sub-line:vertical {{
                height: 0px;
                border: none;
                background: transparent;
            }}

            QScrollBar::add-page:vertical,
            QScrollBar::sub-page:vertical {{
                background: transparent;
            }}

        """)

    def available_overlay_positions(self):
        return [
            "top_left",
            "top_center",
            "top_right",
            "right_center",
            "bottom_right",
            "bottom_center",
            "bottom_left",
            "left_center"
        ]

    def overlay_position_display_name(self, position):
        names = {
            "top_left": "Top left",
            "top_center": "Top center",
            "top_right": "Top right",
            "right_center": "Right center",
            "bottom_right": "Bottom right",
            "bottom_center": "Bottom center",
            "bottom_left": "Bottom left",
            "left_center": "Left center"
        }

        return names.get(position, "Top right")

    def position_widget_on_canvas(self, widget, position, margin=None):
        if widget is None:
            return

        if margin is None:
            margin = self.default_margin

        parent_width = self.parent_widget.width()
        parent_height = self.parent_widget.height()

        if parent_width < 100 or parent_height < 100:
            return

        widget_width = widget.width()
        widget_height = widget.height()

        if position == "top_left":
            x = margin
            y = margin

        elif position == "top_center":
            x = int((parent_width - widget_width) / 2)
            y = margin

        elif position == "top_right":
            x = parent_width - widget_width - margin
            y = margin

        elif position == "right_center":
            x = parent_width - widget_width - margin
            y = int((parent_height - widget_height) / 2)

        elif position == "bottom_right":
            x = parent_width - widget_width - margin
            y = parent_height - widget_height - margin

        elif position == "bottom_center":
            x = int((parent_width - widget_width) / 2)
            y = parent_height - widget_height - margin

        elif position == "bottom_left":
            x = margin
            y = parent_height - widget_height - margin

        elif position == "left_center":
            x = margin
            y = int((parent_height - widget_height) / 2)

        else:
            x = parent_width - widget_width - margin
            y = margin

        x = max(margin, min(x, parent_width - widget_width - margin))
        y = max(margin, min(y, parent_height - widget_height - margin))

        widget.move(x, y)

    def legend_edge_margin_px(self):
        return 8

    def maximum_legend_height_px(self):
        try:
            parent_height = int(self.parent_widget.height())
        except Exception:
            parent_height = 0

        if parent_height < 100:
            return 600

        return max(
            80,
            parent_height - (2 * self.legend_edge_margin_px())
        )

    def legend_scrollbar_width_px(self):
        try:
            return self.scroll_area.verticalScrollBar().sizeHint().width()
        except Exception as exception:
            self.log_ignored_exception(
                "Could not read vertical scrollbar width",
                exception
            )

        return 12


    def sticky_corner_target_position(self, corner):
        parent_width = self.parent_widget.width()
        parent_height = self.parent_widget.height()

        if parent_width < 100 or parent_height < 100:
            return None

        margin = self.legend_edge_margin_px()

        if corner == "top_left":
            return QPoint(
                margin,
                margin
            )

        if corner == "top_right":
            return QPoint(
                parent_width - self.width() - margin,
                margin
            )

        if corner == "bottom_left":
            return QPoint(
                margin,
                parent_height - self.height() - margin
            )

        if corner == "bottom_right":
            return QPoint(
                parent_width - self.width() - margin,
                parent_height - self.height() - margin
            )

        return None

    def nearest_auto_sticky_corner(self, tolerance_px=24):
        corners = [
            "top_left",
            "top_right",
            "bottom_left",
            "bottom_right"
        ]

        current_pos = self.pos()
        nearest_corner = None
        nearest_distance_squared = None

        for corner in corners:
            target_pos = self.sticky_corner_target_position(corner)

            if target_pos is None:
                continue

            dx = current_pos.x() - target_pos.x()
            dy = current_pos.y() - target_pos.y()
            distance_squared = dx * dx + dy * dy

            if (
                nearest_distance_squared is None or
                distance_squared < nearest_distance_squared
            ):
                nearest_distance_squared = distance_squared
                nearest_corner = corner

        if nearest_corner is None:
            return None

        if nearest_distance_squared <= tolerance_px * tolerance_px:
            return nearest_corner

        return None

    def activate_auto_sticky_corner_if_close(self):
        corner = self.nearest_auto_sticky_corner()

        if corner is None:
            return False

        self.sticky_corner = corner
        self.stick_to_corner = True

        self.settings.setValue("sticky_corner", self.sticky_corner)
        self.settings.setValue("stick_to_corner", True)
        self.settings.sync()

        self.apply_sticky_corner_position()

        return True

    def set_stick_to_corner(self, checked):
        self.stick_to_corner = bool(checked)
        self.settings.setValue("stick_to_corner", self.stick_to_corner)
        self.settings.sync()

        if self.stick_to_corner:
            self.apply_sticky_corner_position()

    def set_sticky_corner(self, corner):
        if corner not in self.available_overlay_positions():
            return

        self.sticky_corner = corner
        self.stick_to_corner = True

        self.settings.setValue("sticky_corner", self.sticky_corner)
        self.settings.setValue("stick_to_corner", True)
        self.settings.sync()

        self.apply_sticky_corner_position()

    def sticky_corner_display_name(self, corner):
        return self.overlay_position_display_name(corner)

    def move_to_sticky_corner(self):
        parent_width = self.parent_widget.width()
        parent_height = self.parent_widget.height()

        if parent_width < 100 or parent_height < 100:
            return False

        if self.sticky_corner in [
            "top_left",
            "top_right",
            "bottom_left",
            "bottom_right"
        ]:
            target_pos = self.sticky_corner_target_position(
                self.sticky_corner
            )

            if target_pos is None:
                return False

            self.move(target_pos)
            return True

        self.position_widget_on_canvas(
            self,
            self.sticky_corner,
            self.legend_edge_margin_px()
        )

        return True

    def apply_sticky_corner_position(self):
        if not self.stick_to_corner:
            return False

        moved = self.move_to_sticky_corner()

        if moved:
            self.position_has_been_initialized = True
            self.save_position()

        return moved

    def create_decoration_widgets(self):
        if self.scale_bar_widget is None:
            self.scale_bar_widget = LivingLegendScaleBarWidget(
                self,
                self.parent_widget
            )

        if self.north_arrow_widget is None:
            self.north_arrow_widget = LivingLegendNorthArrowWidget(
                self.parent_widget
            )

        self.update_decoration_widgets()

    def decoration_margin_px(self):
        # The export uses a small 2 mm layout margin.
        # The live scale bar has some internal padding, so a smaller widget
        # margin makes the visible result match the export more closely.
        return 0

    def scale_bar_margin_x_px(self):
        # Visible horizontal margin of the live scale bar inside its transparent widget.
        # For bottom-left position, this is the visible distance from the map's left edge
        # when decoration_margin_px() returns 0.
        return 9

    def scale_bar_margin_y_px(self):
        # Visible vertical margin of the live scale bar inside its transparent widget.
        # For bottom-left position, this is the visible distance from the map's bottom edge
        # when decoration_margin_px() returns 0.
        return 9

    def hide_decoration_widgets(self):
        try:
            if self.scale_bar_widget is not None:
                self.scale_bar_widget.hide()
        except Exception as exception:
            self.log_ignored_exception(
                "Could not hide scale bar widget",
                exception
            )

        try:
            if self.north_arrow_widget is not None:
                self.north_arrow_widget.hide()
        except Exception as exception:
            self.log_ignored_exception(
                "Could not hide north arrow widget",
                exception
            )

    def update_decoration_widgets(self):
        if not self.isVisible():
            self.hide_decoration_widgets()
            return

        try:
            if self.scale_bar_widget is not None:
                if self.scale_bar_enabled:
                    self.scale_bar_widget.update_values()
                    self.scale_bar_widget.show()
                    self.position_widget_on_canvas(
                        self.scale_bar_widget,
                        self.scale_bar_position,
                        self.decoration_margin_px()
                    )
                    self.scale_bar_widget.raise_()
                else:
                    self.scale_bar_widget.hide()
        except Exception as exception:
            self.log_ignored_exception(
                "Could not update scale bar widget visibility",
                exception
            )

        try:
            if self.north_arrow_widget is not None:
                if self.north_arrow_enabled:
                    self.north_arrow_widget.show()
                    self.position_widget_on_canvas(
                        self.north_arrow_widget,
                        self.north_arrow_position,
                        self.default_margin
                    )
                    self.north_arrow_widget.raise_()
                else:
                    self.north_arrow_widget.hide()
        except Exception as exception:
            self.log_ignored_exception(
                "Could not update north arrow widget visibility",
                exception
            )

        try:
            self.raise_()
        except Exception as exception:
            self.log_ignored_exception(
                "Could not raise legend overlay",
                exception
            )

    def set_scale_bar_enabled(self, checked):
        self.scale_bar_enabled = bool(checked)
        self.settings.setValue("scale_bar_enabled", self.scale_bar_enabled)
        self.settings.sync()
        self.update_decoration_widgets()

    def set_north_arrow_enabled(self, checked):
        self.north_arrow_enabled = bool(checked)
        self.settings.setValue("north_arrow_enabled", self.north_arrow_enabled)
        self.settings.sync()
        self.update_decoration_widgets()

    def set_scale_bar_position(self, position):
        if position not in self.available_overlay_positions():
            return

        self.scale_bar_position = position
        self.settings.setValue("scale_bar_position", self.scale_bar_position)
        self.settings.sync()
        self.update_decoration_widgets()

    def set_north_arrow_position(self, position):
        if position not in self.available_overlay_positions():
            return

        self.north_arrow_position = position
        self.settings.setValue("north_arrow_position", self.north_arrow_position)
        self.settings.sync()
        self.update_decoration_widgets()

    def settings_contains_position(self):
        try:
            return (
                self.settings.contains("legend_pos_x") and
                self.settings.contains("legend_pos_y")
            )
        except Exception:
            return False

    def read_saved_position(self):
        try:
            x = int(self.settings.value("legend_pos_x", 0))
            y = int(self.settings.value("legend_pos_y", 0))
            return QPoint(x, y)
        except Exception:
            return None

    def save_position(self):
        try:
            # Only save after the startup/default/saved position has been applied.
            if not self.position_has_been_initialized:
                return

            # Do not save while the QGIS canvas is still initializing.
            # This prevents accidental saving of temporary top-left coordinates.
            if self.parent_widget.width() < 100 or self.parent_widget.height() < 100:
                return

            self.settings.setValue("legend_pos_x", int(self.x()))
            self.settings.setValue("legend_pos_y", int(self.y()))
            self.settings.sync()

        except Exception as exception:
            self.log_ignored_exception(
                "Could not save legend position",
                exception
            )

    def move_to_default_top_right(self):
        old_corner = self.sticky_corner
        old_stick_state = self.stick_to_corner

        self.sticky_corner = "top_right"
        self.stick_to_corner = True

        self.move_to_sticky_corner()

        self.sticky_corner = old_corner
        self.stick_to_corner = old_stick_state

    def apply_saved_or_default_position(self):
        parent_width = self.parent_widget.width()
        parent_height = self.parent_widget.height()

        if parent_width < 100 or parent_height < 100:
            self.position_restore_attempts += 1

            if self.position_restore_attempts < 40:
                self.resize_timer.start(150)

            return

        if self.stick_to_corner:
            if self.apply_sticky_corner_position():
                return

        saved_pos = None

        if self.settings_contains_position():
            saved_pos = self.read_saved_position()

        if saved_pos is not None:
            self.move(saved_pos)
            self.keep_inside_map()
        else:
            self.move_to_default_top_right()

        self.position_has_been_initialized = True

    def set_show_layer_name(self, checked):
        self.show_layer_name = bool(checked)
        self.settings.setValue("show_layer_name", self.show_layer_name)
        self.requestLegendUpdate()

    def reset_all_settings_to_default(self):
        try:
            self.settings.clear()
        except Exception as exception:
            self.log_ignored_exception(
                "Could not clear Living Legend settings",
                exception
            )                

        self.show_feature_counts = False
        self.show_feature_area = False
        self.sync_label_changes_with_project = True
        self.show_layer_name = True
        self.show_legend_title = False
        self.legend_title_text = "Legend"
        self.stick_to_corner = True
        self.sticky_corner = "top_right"

        self.scale_bar_enabled = True
        self.scale_bar_position = "bottom_left"

        self.north_arrow_enabled = True
        self.north_arrow_position = "top_left"

        self.legend_opacity = 0.82
        self.local_label_overrides = {}
        self.hidden_legend_items = {}
        self.position_has_been_initialized = False

        try:
            self.settings.setValue("show_feature_counts", False)
            self.settings.setValue("show_feature_area", False)
            self.settings.setValue("sync_label_changes_with_project", True)
            self.settings.setValue("show_layer_name", True)
            self.settings.setValue("show_legend_title", False)
            self.settings.setValue("legend_title_text", "Legend")
            self.settings.setValue("stick_to_corner", True)
            self.settings.setValue("sticky_corner", "top_right")

            self.settings.setValue("scale_bar_enabled", True)
            self.settings.setValue("scale_bar_position", "bottom_left")

            self.settings.setValue("north_arrow_enabled", True)
            self.settings.setValue("north_arrow_position", "top_left")

            self.settings.setValue("legend_opacity", self.legend_opacity)
            self.settings.sync()
        except Exception as exception:
            self.log_ignored_exception(
                "Could not write reset defaults to settings",
                exception
            )

        self.apply_style()
        self.update_decoration_widgets()

        try:
            self.update_timer.stop()
        except Exception as exception:
            self.log_ignored_exception(
                "Could not stop pending legend update timer",
                exception
            )

        self.updateLegend()
        self.resize_timer.start(0)

    def set_show_feature_counts(self, checked):
        self.show_feature_counts = bool(checked)
        self.settings.setValue("show_feature_counts", self.show_feature_counts)
        self.requestLegendUpdate()

    def set_show_feature_area(self, checked):
        self.show_feature_area = bool(checked)
        self.settings.setValue("show_feature_area", self.show_feature_area)
        self.settings.sync()
        self.requestLegendUpdate()

    def set_show_legend_title(self, checked):
        self.show_legend_title = bool(checked)
        self.settings.setValue("show_legend_title", self.show_legend_title)
        self.settings.sync()
        self.requestLegendUpdate()

    def set_legend_title_text(self, new_title):
        new_title = new_title.strip()

        if new_title == "":
            new_title = "Legend"

        self.legend_title_text = new_title
        self.settings.setValue("legend_title_text", self.legend_title_text)
        self.settings.sync()
        self.requestResizeUpdate()

    def set_sync_label_changes_with_project(self, checked):
        self.sync_label_changes_with_project = bool(checked)

        self.settings.setValue(
            "sync_label_changes_with_project",
            self.sync_label_changes_with_project
        )
        self.settings.sync()

        if self.sync_label_changes_with_project:
            self.local_label_overrides = {}
            self.requestLegendUpdate()
        else:
            self.requestResizeUpdate()

    def set_legend_opacity(self, value):
        self.legend_opacity = max(0.35, min(1.0, float(value)))
        self.settings.setValue("legend_opacity", self.legend_opacity)
        self.apply_style()

    def make_label_key(self, layer, item_type, item_identifier):
        return f"{layer.id()}::{item_type}::{item_identifier}"

    def get_display_label(self, layer, item_type, item_identifier, default_label):
        key = self.make_label_key(layer, item_type, item_identifier)

        if (
            not self.sync_label_changes_with_project and
            key in self.local_label_overrides
        ):
            return self.local_label_overrides[key]

        return default_label

    def set_local_label_override(self, layer, item_type, item_identifier, new_label):
        key = self.make_label_key(layer, item_type, item_identifier)
        self.local_label_overrides[key] = new_label

    def eventFilter(self, watched, event):
        event_type = event.type()

        # The parent_widget is the QGIS map canvas viewport.
        # We only listen to its resize event.
        # Mouse and context menu events from the map canvas must never be handled here,
        # otherwise normal QGIS map interaction would be blocked.
        if watched == self.parent_widget:
            if event_type == QEvent.Type.Resize:
                self.requestResizeUpdate()

                if self.stick_to_corner:
                    self.apply_sticky_corner_position()
                else:
                    self.keep_inside_map()

                self.update_decoration_widgets()

            return False

        # Right-click on the living legend or on any child widget of the legend.
        # We handle this on MouseButtonPress already, because waiting for ContextMenu
        # can let the event pass through to the QGIS map canvas on some systems.
        if event_type == QEvent.Type.MouseButtonPress:
            if self.is_right_mouse_button(event):
                self.show_context_menu(event, watched)
                return True

            return self.handle_mouse_press(event)

        if event_type == QEvent.Type.ContextMenu:
            self.show_context_menu(event, watched)
            return True

        if event_type == QEvent.Type.MouseMove:
            return self.handle_mouse_move(event)

        if event_type == QEvent.Type.MouseButtonRelease:
            return self.handle_mouse_release(event)

        if event_type == QEvent.Type.MouseButtonDblClick:
            return False

        if event_type == QEvent.Type.Wheel:
            return self.handle_legend_wheel_event(event)

        return super().eventFilter(watched, event)

    def is_right_mouse_button(self, event):
        try:
            return event.button() == Qt.MouseButton.RightButton
        except Exception:
            try:
                return event.button() == Qt.MouseButton.RightButton
            except Exception:
                return False

    def get_event_global_pos(self, event):
        try:
            return event.globalPosition().toPoint()
        except Exception:
            return event.globalPos()

    def prepare_for_capture(self):
        self.updateLegend()
        self.adjust_size_to_content()
        self.raise_()

        try:
            QApplication.processEvents()
        except Exception as exception:
            self.log_ignored_exception(
                "Could not process pending Qt events before capture",
                exception
            )

    def copy_legend_to_clipboard(self):
        pixmap = self.grab_legend_pixmap()

        if pixmap.isNull():
            return

        QApplication.clipboard().setPixmap(pixmap)

        self.show_temporary_message(
            "Living Legend",
            "Legend copied to clipboard.",
            3
        )

    def copy_map_extent_to_clipboard(self):
        image = self.render_current_map_layout_to_clipboard_image()

        if image.isNull():
            self.show_export_warning(
                "Living Legend export",
                "The map could not be rendered to the clipboard."
            )
            return

        QApplication.clipboard().setPixmap(
            QPixmap.fromImage(image)
        )

        self.show_temporary_message(
            "Living Legend",
            "Map copied to clipboard.",
            3
        )

    def map_export_dpi(self):
        return 300.0

    def screen_dpi(self):
        # Use a fixed screen baseline.
        # This makes 300 DPI export approximately 300 / 96 = 3.125x
        # the visible map canvas pixel dimensions, similar to a high-resolution
        # QGIS export of the current view.
        return 96.0

    def capture_scale_factor(self):
        # Converts the visible canvas size to an output size matching 300 DPI.
        # Example: 300 / 96 = 3.125x pixel dimensions.
        scale_factor = max(1.0, self.map_export_dpi() / self.screen_dpi())

        # Safety cap:
        # Very large canvases at 300 DPI can create huge images and make QGIS
        # appear frozen or run out of memory. This keeps exports practical.
        max_output_dimension = 9000.0

        parent_width = max(1.0, float(self.parent_widget.width()))
        parent_height = max(1.0, float(self.parent_widget.height()))

        scale_factor = min(
            scale_factor,
            max_output_dimension / parent_width,
            max_output_dimension / parent_height
        )

        return max(1.0, scale_factor)

    def image_dots_per_meter(self):
        return int(self.map_export_dpi() / 25.4 * 1000.0)

    def render_widget_to_pixmap(self, widget, scale_factor=None, transparent=True):
        if scale_factor is None:
            scale_factor = self.capture_scale_factor()

        width = max(1, widget.width())
        height = max(1, widget.height())

        pixmap = QPixmap(
            int(width * scale_factor),
            int(height * scale_factor)
        )

        if transparent:
            pixmap.fill(Qt.GlobalColor.transparent)
        else:
            pixmap.fill(Qt.GlobalColor.white)

        painter = QPainter(pixmap)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
        painter.setRenderHint(QPainter.RenderHint.TextAntialiasing, True)
        painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform, True)

        painter.scale(scale_factor, scale_factor)

        try:
            widget.render(painter)
        except Exception as exception:
            self.log_ignored_exception(
                "Could not render widget to pixmap",
                exception
            )

        painter.end()

        return pixmap

    def grab_legend_pixmap(self):
        self.prepare_for_capture()

        # Render the legend itself at high resolution instead of using self.grab().
        return self.render_widget_to_pixmap(
            self,
            scale_factor=self.capture_scale_factor(),
            transparent=True
        )

    def show_export_warning(self, title, message):
        try:
            QMessageBox.warning(
                self,
                title,
                message
            )
        except Exception as exception:
            self.log_ignored_exception(
                "Could not show export warning message",
                exception
            )

    def show_temporary_message(self, title, message, duration=2):
        try:
            self.iface.messageBar().pushMessage(
                title,
                message,
                level=Qgis.MessageLevel.Success,
                duration=duration
            )
        except Exception as exception:
            self.log_ignored_exception(
                "Could not show temporary QGIS message bar notification",
                exception
            )

    def reveal_file_in_file_manager(self, file_path):
        try:
            normalized_file_path = os.path.abspath(file_path)

            if os.name == "nt":
                native_file_path = os.path.normpath(
                    normalized_file_path
                )
                folder_path = os.path.dirname(
                    native_file_path
                )

                if os.path.exists(native_file_path):
                    parameters = f'/select,"{native_file_path}"'

                    result_code = ctypes.windll.shell32.ShellExecuteW(
                        None,
                        "open",
                        "explorer.exe",
                        parameters,
                        None,
                        1
                    )

                    if result_code <= 32:
                        self.log_ignored_exception(
                            "Could not reveal exported file using ShellExecuteW",
                            RuntimeError(
                                f"ShellExecuteW result code: {result_code}"
                            )
                        )

                        if os.path.isdir(folder_path):
                            QDesktopServices.openUrl(
                                QUrl.fromLocalFile(folder_path)
                            )

                    return

                if os.path.isdir(folder_path):
                    opened = QDesktopServices.openUrl(
                        QUrl.fromLocalFile(folder_path)
                    )

                    if not opened:
                        self.log_ignored_exception(
                            "Could not open exported file folder",
                            RuntimeError(folder_path)
                        )

                    return

                self.log_ignored_exception(
                    "Exported file and parent folder do not exist",
                    RuntimeError(native_file_path)
                )
                return

            folder_path = os.path.dirname(
                normalized_file_path
            )

            if not os.path.isdir(folder_path):
                self.log_ignored_exception(
                    "Exported file parent folder does not exist",
                    RuntimeError(folder_path)
                )
                return

            opened = QDesktopServices.openUrl(
                QUrl.fromLocalFile(folder_path)
            )

            if not opened:
                self.log_ignored_exception(
                    "Could not open exported file folder",
                    RuntimeError(folder_path)
                )

        except Exception as exception:
            self.log_ignored_exception(
                "Could not reveal exported file in file manager",
                exception
            )

    def show_export_success_message(self, title, message, file_path, duration=8):
        try:
            folder_path = os.path.dirname(os.path.abspath(file_path))
            file_name = os.path.basename(file_path)

            message_widget = QWidget()
            message_layout = QHBoxLayout()
            message_layout.setContentsMargins(0, 0, 0, 0)
            message_layout.setSpacing(4)
            message_widget.setLayout(message_layout)

            text_label = QLabel(f"{message} ")

            link_button = QPushButton(
                f'Show "{file_name}" in folder'
            )
            link_button.setFlat(True)
            link_button.setCursor(Qt.CursorShape.PointingHandCursor)
            link_button.setToolTip(folder_path)
            link_button.setStyleSheet("""
                QPushButton {
                    color: rgb(0, 70, 160);
                    background: transparent;
                    border: none;
                    text-decoration: underline;
                    padding: 0px;
                    margin: 0px;
                }

                QPushButton:hover {
                    color: rgb(0, 100, 220);
                }
            """)

            link_button.clicked.connect(
                lambda checked=False, exported_file_path=file_path: self.reveal_file_in_file_manager(
                    exported_file_path
                )
            )

            message_layout.addWidget(text_label)
            message_layout.addWidget(link_button)
            message_layout.addStretch()

            self.iface.messageBar().pushWidget(
                message_widget,
                Qgis.MessageLevel.Success,
                duration
            )

        except Exception as exception:
            self.log_ignored_exception(
                "Could not show export success message with file location link",
                exception
            )

            self.show_temporary_message(
                title,
                message,
                3
            )

    def layout_export_result_message(self, result):
        try:
            if result == QgsLayoutExporter.ExportResult.Success:
                return "Export succeeded."
            if result == QgsLayoutExporter.ExportResult.Canceled:
                return "Export was canceled."
            if result == QgsLayoutExporter.ExportResult.MemoryError:
                return "Export failed because QGIS ran out of memory."
            if result == QgsLayoutExporter.ExportResult.FileError:
                return "Export failed because the output file could not be written."
            if result == QgsLayoutExporter.ExportResult.PrintError:
                return "Export failed because the print device could not be initialized."
            if result == QgsLayoutExporter.ExportResult.SvgLayerError:
                return "Export failed because layered SVG export failed."
            if result == QgsLayoutExporter.ExportResult.IteratorError:
                return "Export failed because of an iterator or atlas error."
        except Exception as exception:
            self.log_ignored_exception(
                "Could not read QgsLayoutExporter scoped export result",
                exception
            )

        try:
            if result == QgsLayoutExporter.ExportResult.Success:
                return "Export succeeded."
            if result == QgsLayoutExporter.ExportResult.Canceled:
                return "Export was canceled."
            if result == QgsLayoutExporter.ExportResult.MemoryError:
                return "Export failed because QGIS ran out of memory."
            if result == QgsLayoutExporter.ExportResult.FileError:
                return "Export failed because the output file could not be written."
            if result == QgsLayoutExporter.ExportResult.PrintError:
                return "Export failed because the print device could not be initialized."
            if result == QgsLayoutExporter.ExportResult.SvgLayerError:
                return "Export failed because layered SVG export failed."
            if result == QgsLayoutExporter.ExportResult.IteratorError:
                return "Export failed because of an iterator or atlas error."
        except Exception as exception:
            self.log_ignored_exception(
                "Could not read QgsLayoutExporter legacy export result",
                exception
            )

        return f"Export failed with result code: {result}"

    def current_map_layout_size_mm(self):
        # The layout page must represent the physical size of the visible
        # canvas at screen DPI, not at export DPI.
        #
        # If we divide by 300 here, the exported image has about the same
        # pixel dimensions as the screen view, but QGIS still renders
        # millimeter-based symbol widths at print DPI. This makes outlines
        # look much too thick.
        #
        # Dividing by screen DPI instead gives QGIS enough output pixels at
        # 300 DPI while keeping the visual stroke weight close to the live map.
        screen_dpi = self.screen_dpi()

        width_px = max(1, self.parent_widget.width())
        height_px = max(1, self.parent_widget.height())

        width_mm = width_px / screen_dpi * 25.4
        height_mm = height_px / screen_dpi * 25.4

        return width_mm, height_mm

    def nice_scalebar_segment_length(self):
        try:
            extent_width = float(self.canvas.extent().width())
        except Exception:
            return 100.0

        if extent_width <= 0:
            return 100.0

        target = extent_width * 0.25

        power = 1.0

        while power * 10.0 <= target:
            power *= 10.0

        while power > target:
            power /= 10.0

        candidates = [
            1.0 * power,
            2.0 * power,
            5.0 * power,
            10.0 * power
        ]

        best = candidates[0]

        for candidate in candidates:
            if candidate <= target:
                best = candidate

        return best

    def create_north_arrow_svg(self):
        temporary_directory = tempfile.gettempdir()

        north_arrow_path = os.path.join(
            temporary_directory,
            "living_legend_export_north_arrow.svg"
        )

        svg_content = '''<svg xmlns="http://www.w3.org/2000/svg" height="348" width="226" viewBox="0 0 226 348">
<path d="M5.53,240.17l108.38-113.54,106.91,112.07-106.91-229.3z" stroke="#000000" stroke-width="2" stroke-linejoin="round" fill="#ffffff"/>
<path d="m113.91,9.2162,0,117.41,106.36,111.33z" stroke="#000000" stroke-width="2" stroke-linejoin="round" fill="#000000"/>
<path d="m145.05,321.47-10.375,0-44.125-74.375,0.125,5.625c0.12498,2.1251,0.18748,3.9167,0.1875,5.375v63.375h-8.625v-85.312h13.375l41.25,69.562-0.125-5.9375c-0.0834-2.3333-0.12506-4.3958-0.125-6.1875v-57.438h8.4375z" fill="#000000"/>
</svg>'''

        try:
            with open(north_arrow_path, "w", encoding="utf-8") as file:
                file.write(svg_content)

            return north_arrow_path

        except Exception:
            return None

    def layout_position_xy(
        self,
        position,
        item_width_mm,
        item_height_mm,
        page_width_mm,
        page_height_mm,
        margin_mm=2.0
    ):
        if position == "top_left":
            x = margin_mm
            y = margin_mm

        elif position == "top_center":
            x = (page_width_mm - item_width_mm) / 2.0
            y = margin_mm

        elif position == "top_right":
            x = page_width_mm - item_width_mm - margin_mm
            y = margin_mm

        elif position == "right_center":
            x = page_width_mm - item_width_mm - margin_mm
            y = (page_height_mm - item_height_mm) / 2.0

        elif position == "bottom_right":
            x = page_width_mm - item_width_mm - margin_mm
            y = page_height_mm - item_height_mm - margin_mm

        elif position == "bottom_center":
            x = (page_width_mm - item_width_mm) / 2.0
            y = page_height_mm - item_height_mm - margin_mm

        elif position == "bottom_left":
            x = margin_mm
            y = page_height_mm - item_height_mm - margin_mm

        elif position == "left_center":
            x = margin_mm
            y = (page_height_mm - item_height_mm) / 2.0

        else:
            x = page_width_mm - item_width_mm - margin_mm
            y = margin_mm

        x = max(margin_mm, min(x, page_width_mm - item_width_mm - margin_mm))
        y = max(margin_mm, min(y, page_height_mm - item_height_mm - margin_mm))

        return x, y

    def add_red_layer_overlay_to_layout(
        self,
        layout,
        page_width_mm,
        page_height_mm,
        temporary_paths
    ):
        overlay_image = self.create_red_layer_export_overlay()

        if overlay_image is None:
            return

        overlay_path = os.path.join(
            tempfile.gettempdir(),
            "living_legend_red_layer_overlay.png"
        )

        try:
            saved = overlay_image.save(
                overlay_path,
                "PNG"
            )

            if not saved:
                self.log_ignored_exception(
                    "Could not save Red Layer export overlay image",
                    RuntimeError(overlay_path)
                )
                return

            temporary_paths.append(overlay_path)

            overlay_item = QgsLayoutItemPicture(layout)

            try:
                overlay_item.setPicturePath(overlay_path)
            except Exception as exception:
                self.log_ignored_exception(
                    "Could not set Red Layer overlay picture path",
                    exception
                )

                try:
                    overlay_item.setSource(overlay_path)
                except Exception as source_exception:
                    self.log_ignored_exception(
                        "Could not set Red Layer overlay picture source",
                        source_exception
                    )
                    return

            overlay_item.attemptMove(
                QgsLayoutPoint(
                    0.0,
                    0.0,
                    QgsUnitTypes.LayoutUnit.LayoutMillimeters
                )
            )
            overlay_item.attemptResize(
                QgsLayoutSize(
                    page_width_mm,
                    page_height_mm,
                    QgsUnitTypes.LayoutUnit.LayoutMillimeters
                )
            )

            layout.addLayoutItem(overlay_item)

        except Exception as exception:
            self.log_ignored_exception(
                "Could not add Red Layer overlay to export layout",
                exception
            )

    def add_basic_decorations_to_layout(
        self,
        layout,
        map_item,
        page_width_mm,
        page_height_mm,
        temporary_paths
    ):
        if self.scale_bar_enabled:
            try:
                scale_bar = QgsLayoutItemScaleBar(layout)
                scale_bar.setLinkedMap(map_item)

                try:
                    scale_bar.setStyle("Line Ticks Up")
                except Exception as exception:
                    self.log_ignored_exception(
                        "Could not set scale bar style",
                        exception
                    )

                try:
                    total_length = float(self.nice_scalebar_segment_length())
                    scale_bar.setNumberOfSegments(2)
                    scale_bar.setNumberOfSegmentsLeft(0)
                    scale_bar.setUnitsPerSegment(total_length / 2.0)
                except Exception as exception:
                    self.log_ignored_exception(
                        "Could not configure scale bar segment length",
                        exception
                    )

                try:
                    scale_bar.setUnitLabel("m")
                except Exception as exception:
                    self.log_ignored_exception(
                        "Could not set scale bar unit label",
                        exception
                    )

                try:
                    scale_bar.setHeight(3)
                except Exception as exception:
                    self.log_ignored_exception(
                        "Could not set scale bar height",
                        exception
                    )

                scale_bar_width_mm = 70.0
                scale_bar_height_mm = 12.0

                scale_bar_x_mm, scale_bar_y_mm = self.layout_position_xy(
                    self.scale_bar_position,
                    scale_bar_width_mm,
                    scale_bar_height_mm,
                    page_width_mm,
                    page_height_mm,
                    2.0
                )

                try:
                    scale_bar.attemptMove(
                        QgsLayoutPoint(
                            scale_bar_x_mm,
                            scale_bar_y_mm,
                            QgsUnitTypes.LayoutUnit.LayoutMillimeters
                        )
                    )
                except Exception:
                    scale_bar.attemptMove(
                        QgsLayoutPoint(
                            scale_bar_x_mm,
                            scale_bar_y_mm,
                            QgsUnitTypes.LayoutUnit.LayoutMillimeters
                        )
                    )

                layout.addLayoutItem(scale_bar)

            except Exception as exception:
                self.log_ignored_exception(
                    "Could not add scale bar to export layout",
                    exception
                )

        if self.north_arrow_enabled:
            try:
                north_arrow_path = self.create_north_arrow_svg()

                if north_arrow_path:
                    temporary_paths.append(north_arrow_path)

                    north_arrow = QgsLayoutItemPicture(layout)

                    try:
                        north_arrow.setPicturePath(north_arrow_path)
                    except Exception:
                        try:
                            north_arrow.setSource(north_arrow_path)
                        except Exception as exception:
                            self.log_ignored_exception(
                                "Could not set north arrow picture source",
                                exception
                            )

                    arrow_width_mm = 9.0
                    arrow_height_mm = 16.0

                    arrow_x_mm, arrow_y_mm = self.layout_position_xy(
                        self.north_arrow_position,
                        arrow_width_mm,
                        arrow_height_mm,
                        page_width_mm,
                        page_height_mm,
                        2.0
                    )

                    try:
                        north_arrow.attemptMove(
                            QgsLayoutPoint(
                                arrow_x_mm,
                                arrow_y_mm,
                                QgsUnitTypes.LayoutUnit.LayoutMillimeters
                            )
                        )
                        north_arrow.attemptResize(
                            QgsLayoutSize(
                                arrow_width_mm,
                                arrow_height_mm,
                                QgsUnitTypes.LayoutUnit.LayoutMillimeters
                            )
                        )
                    except Exception:
                        north_arrow.attemptMove(
                            QgsLayoutPoint(
                                arrow_x_mm,
                                arrow_y_mm,
                                QgsUnitTypes.LayoutUnit.LayoutMillimeters
                            )
                        )
                        north_arrow.attemptResize(
                            QgsLayoutSize(
                                arrow_width_mm,
                                arrow_height_mm,
                                QgsUnitTypes.LayoutUnit.LayoutMillimeters
                            )
                        )

                    layout.addLayoutItem(north_arrow)

            except Exception as exception:
                self.log_ignored_exception(
                    "Could not add north arrow to export layout",
                    exception
                )

    def create_temporary_layout_with_map_and_legend(self):
        self.prepare_for_capture()

        project = QgsProject.instance()

        temporary_paths = []

        layout = QgsPrintLayout(project)
        layout.initializeDefaults()
        layout.setName("LivingLegendTemporaryExportLayout")

        page_width_mm, page_height_mm = self.current_map_layout_size_mm()

        try:
            page = layout.pageCollection().page(0)
            page.setPageSize(
                QgsLayoutSize(
                    page_width_mm,
                    page_height_mm,
                    QgsUnitTypes.LayoutUnit.LayoutMillimeters
                )
            )
        except Exception:
            try:
                page = layout.pageCollection().page(0)
                page.setPageSize(
                    QgsLayoutSize(
                        page_width_mm,
                        page_height_mm,
                        QgsUnitTypes.LayoutUnit.LayoutMillimeters
                    )
                )
            except Exception as exception:
                self.log_ignored_exception(
                    "Could not set temporary export page size",
                    exception
                )

        map_item = QgsLayoutItemMap(layout)
        map_item.setRect(0, 0, page_width_mm, page_height_mm)

        try:
            map_item.setExtent(self.canvas.extent())
        except Exception as exception:
            self.log_ignored_exception(
                "Could not set export map extent",
                exception
            )

        try:
            map_item.setLayers(self.canvas.layers())
        except Exception as exception:
            self.log_ignored_exception(
                "Could not set export map layer list",
                exception
            )

        try:
            map_item.attemptMove(
                QgsLayoutPoint(
                    0,
                    0,
                    QgsUnitTypes.LayoutUnit.LayoutMillimeters
                )
            )
            map_item.attemptResize(
                QgsLayoutSize(
                    page_width_mm,
                    page_height_mm,
                    QgsUnitTypes.LayoutUnit.LayoutMillimeters
                )
            )
        except Exception:
            map_item.attemptMove(
                QgsLayoutPoint(
                    0,
                    0,
                    QgsUnitTypes.LayoutUnit.LayoutMillimeters
                )
            )
            map_item.attemptResize(
                QgsLayoutSize(
                    page_width_mm,
                    page_height_mm,
                    QgsUnitTypes.LayoutUnit.LayoutMillimeters
                )
            )

        layout.addLayoutItem(map_item)

        # Red Layer uses canvas-only rubber bands and annotation items.
        # Reproduce them as a transparent overlay above the exported map.
        self.add_red_layer_overlay_to_layout(
            layout,
            page_width_mm,
            page_height_mm,
            temporary_paths
        )

        # Add layout equivalents for common QGIS map decorations.
        # This keeps the PDF vector-friendly while restoring the visible
        # scale bar and north arrow from the canvas.
        self.add_basic_decorations_to_layout(
            layout,
            map_item,
            page_width_mm,
            page_height_mm,
            temporary_paths
        )

        legend_pixmap = self.grab_legend_pixmap()

        if not legend_pixmap.isNull():
            try:
                temporary_directory = tempfile.gettempdir()

                legend_temp_path = os.path.join(
                    temporary_directory,
                    "living_legend_export_overlay.png"
                )

                temporary_paths.append(legend_temp_path)

                legend_pixmap.save(legend_temp_path, "PNG")

                parent_width = max(1.0, float(self.parent_widget.width()))
                parent_height = max(1.0, float(self.parent_widget.height()))

                legend_x_mm = float(self.x()) / parent_width * page_width_mm
                legend_y_mm = float(self.y()) / parent_height * page_height_mm

                legend_width_mm = float(self.width()) / parent_width * page_width_mm
                legend_height_mm = float(self.height()) / parent_height * page_height_mm

                legend_item = QgsLayoutItemPicture(layout)

                try:
                    legend_item.setPicturePath(legend_temp_path)
                except Exception:
                    try:
                        legend_item.setSource(legend_temp_path)
                    except Exception as exception:
                        self.log_ignored_exception(
                            "Could not set legend overlay picture source",
                            exception
                        )

                try:
                    legend_item.attemptMove(
                        QgsLayoutPoint(
                            legend_x_mm,
                            legend_y_mm,
                            QgsUnitTypes.LayoutUnit.LayoutMillimeters
                        )
                    )
                    legend_item.attemptResize(
                        QgsLayoutSize(
                            legend_width_mm,
                            legend_height_mm,
                            QgsUnitTypes.LayoutUnit.LayoutMillimeters
                        )
                    )
                except Exception:
                    legend_item.attemptMove(
                        QgsLayoutPoint(
                            legend_x_mm,
                            legend_y_mm,
                            QgsUnitTypes.LayoutUnit.LayoutMillimeters
                        )
                    )
                    legend_item.attemptResize(
                        QgsLayoutSize(
                            legend_width_mm,
                            legend_height_mm,
                            QgsUnitTypes.LayoutUnit.LayoutMillimeters
                        )
                    )

                layout.addLayoutItem(legend_item)

            except Exception as exception:
                self.show_export_warning(
                    "Living Legend export",
                    f"The legend overlay could not be added to the map export:\n{exception}"
                )

        return layout, temporary_paths

    def red_layer_plugin_instance(self):
        try:
            for plugin_name, plugin_instance in plugins.items():
                if plugin_instance is None:
                    continue

                if not hasattr(plugin_instance, "geoSketches"):
                    continue

                if not hasattr(plugin_instance, "canvas"):
                    continue

                if plugin_instance.canvas != self.canvas:
                    continue

                return plugin_instance

        except Exception as exception:
            self.log_ignored_exception(
                "Could not inspect installed plugins for Red Layer",
                exception
            )

        return None

    def red_layer_annotation_text(self, sketch_entry):
        try:
            stored_text = str(sketch_entry[4]).strip()

            if stored_text:
                return stored_text

        except Exception as exception:
            self.log_ignored_exception(
                "Could not read stored Red Layer annotation text",
                exception
            )

        try:
            annotation_item = sketch_entry[3]

            if annotation_item is None:
                return ""

            if hasattr(annotation_item, "annotation"):
                annotation = annotation_item.annotation()

                if annotation is not None and hasattr(annotation, "document"):
                    document = annotation.document()

                    if document is not None:
                        return document.toPlainText().strip()

            if hasattr(annotation_item, "document"):
                document = annotation_item.document()

                if document is not None:
                    return document.toPlainText().strip()

        except Exception as exception:
            self.log_ignored_exception(
                "Could not read live Red Layer annotation text",
                exception
            )

        return ""

    def red_layer_annotation_anchor_point(self, geometry):
        if geometry is None:
            return None

        try:
            if geometry.isEmpty():
                return None
        except Exception as exception:
            self.log_ignored_exception(
                "Could not check Red Layer annotation geometry",
                exception
            )
            return None

        try:
            point_geometry = geometry.interpolate(
                geometry.length() / 2.0
            )

            if (
                point_geometry is not None and
                not point_geometry.isEmpty()
            ):
                return point_geometry.asPoint()

        except Exception as exception:
            self.log_ignored_exception(
                "Could not interpolate Red Layer annotation position",
                exception
            )

        try:
            bounding_box = geometry.boundingBox()
            return bounding_box.center()

        except Exception as exception:
            self.log_ignored_exception(
                "Could not calculate fallback Red Layer annotation position",
                exception
            )

        return None

    def red_layer_map_point_to_overlay_point(
        self,
        map_point,
        scale_factor
    ):
        if map_point is None:
            return None

        try:
            map_to_pixel = self.canvas.mapSettings().mapToPixel()

            canvas_point = map_to_pixel.transform(
                QgsPointXY(
                    float(map_point.x()),
                    float(map_point.y())
                )
            )

            return QPointF(
                float(canvas_point.x()) * scale_factor,
                float(canvas_point.y()) * scale_factor
            )

        except Exception as exception:
            self.log_ignored_exception(
                "Could not convert Red Layer map point to export pixel coordinates",
                exception
            )

        return None

    def draw_red_layer_geometry(
        self,
        painter,
        geometry,
        color,
        width_pixels,
        scale_factor
    ):
        if geometry is None:
            return False

        try:
            if geometry.isEmpty():
                return False
        except Exception as exception:
            self.log_ignored_exception(
                "Could not check Red Layer geometry before export",
                exception
            )
            return False

        painter_path = QPainterPath()
        previous_vertex = None
        vertex_count = 0

        try:
            for vertex in geometry.vertices():
                overlay_point = self.red_layer_map_point_to_overlay_point(
                    vertex,
                    scale_factor
                )

                if overlay_point is None:
                    continue

                if previous_vertex is None:
                    painter_path.moveTo(overlay_point)
                else:
                    painter_path.lineTo(overlay_point)

                previous_vertex = overlay_point
                vertex_count += 1

        except Exception as exception:
            self.log_ignored_exception(
                "Could not read Red Layer geometry vertices",
                exception
            )
            return False

        if vertex_count < 2:
            return False

        try:
            pen = QPen(color)
            pen.setWidthF(
                max(
                    1.0,
                    float(width_pixels) * scale_factor
                )
            )
            pen.setCapStyle(Qt.PenCapStyle.RoundCap)
            pen.setJoinStyle(Qt.PenJoinStyle.RoundJoin)

            painter.setPen(pen)
            painter.setBrush(Qt.BrushStyle.NoBrush)
            painter.drawPath(painter_path)

            return True

        except Exception as exception:
            self.log_ignored_exception(
                "Could not paint Red Layer geometry onto export overlay",
                exception
            )

        return False

    def draw_red_layer_annotation(
        self,
        painter,
        geometry,
        annotation_text,
        scale_factor
    ):
        annotation_text = str(annotation_text).strip()

        if not annotation_text:
            return

        anchor_point = self.red_layer_annotation_anchor_point(
            geometry
        )

        overlay_anchor = self.red_layer_map_point_to_overlay_point(
            anchor_point,
            scale_factor
        )

        if overlay_anchor is None:
            return

        try:
            font = QFont("Arial")
            font.setPointSizeF(9.0 * scale_factor)

            painter.setFont(font)

            metrics = painter.fontMetrics()
            text_lines = annotation_text.splitlines()

            if not text_lines:
                return

            horizontal_padding = 6.0 * scale_factor
            vertical_padding = 4.0 * scale_factor
            line_spacing = metrics.height()

            text_width = max(
                metrics.horizontalAdvance(line)
                for line in text_lines
            )

            box_width = (
                float(text_width) +
                (2.0 * horizontal_padding)
            )
            box_height = (
                float(line_spacing * len(text_lines)) +
                (2.0 * vertical_padding)
            )

            box_left = overlay_anchor.x() + (8.0 * scale_factor)
            box_top = overlay_anchor.y() - box_height - (8.0 * scale_factor)

            box_rect = QRectF(
                box_left,
                box_top,
                box_width,
                box_height
            )

            painter.setPen(
                QPen(
                    QColor(70, 70, 70, 220),
                    max(1.0, scale_factor)
                )
            )
            painter.setBrush(
                QBrush(
                    QColor(255, 255, 225, 235)
                )
            )
            painter.drawRoundedRect(
                box_rect,
                3.0 * scale_factor,
                3.0 * scale_factor
            )

            painter.setPen(
                QPen(QColor(25, 25, 25, 255))
            )

            text_y = (
                box_top +
                vertical_padding +
                metrics.ascent()
            )

            for line in text_lines:
                painter.drawText(
                    QPointF(
                        box_left + horizontal_padding,
                        text_y
                    ),
                    line
                )
                text_y += line_spacing

        except Exception as exception:
            self.log_ignored_exception(
                "Could not paint Red Layer annotation onto export overlay",
                exception
            )

    def create_red_layer_export_overlay(self):
        red_layer_plugin = self.red_layer_plugin_instance()

        if red_layer_plugin is None:
            return None

        try:
            sketch_entries = list(
                red_layer_plugin.geoSketches
            )
        except Exception as exception:
            self.log_ignored_exception(
                "Could not read Red Layer sketches",
                exception
            )
            return None

        if not sketch_entries:
            return None

        scale_factor = self.capture_scale_factor()

        output_width = max(
            1,
            int(round(
                self.parent_widget.width() * scale_factor
            ))
        )
        output_height = max(
            1,
            int(round(
                self.parent_widget.height() * scale_factor
            ))
        )

        overlay_image = QImage(
            output_width,
            output_height,
            QImage.Format.Format_ARGB32_Premultiplied
        )
        overlay_image.fill(Qt.GlobalColor.transparent)

        painter = QPainter(overlay_image)
        painter.setRenderHint(
            QPainter.RenderHint.Antialiasing,
            True
        )
        painter.setRenderHint(
            QPainter.RenderHint.TextAntialiasing,
            True
        )
        painter.setRenderHint(
            QPainter.RenderHint.SmoothPixmapTransform,
            True
        )

        drawn_sketch_count = 0
        annotation_entries = []

        try:
            for sketch_entry in sketch_entries:
                try:
                    rubber_band = sketch_entry[2]

                    if rubber_band is None:
                        continue

                    geometry = rubber_band.asGeometry()

                    if geometry is None:
                        continue

                    if geometry.isEmpty():
                        continue

                    color = QColor(str(sketch_entry[0]))

                    if not color.isValid():
                        color = QColor("#aa0000")

                    try:
                        width_pixels = float(sketch_entry[1])
                    except (TypeError, ValueError) as exception:
                        self.log_ignored_exception(
                            "Could not read Red Layer sketch width",
                            exception
                        )
                        width_pixels = 5.0

                    if self.draw_red_layer_geometry(
                        painter,
                        geometry,
                        color,
                        width_pixels,
                        scale_factor
                    ):
                        drawn_sketch_count += 1

                    annotation_text = self.red_layer_annotation_text(
                        sketch_entry
                    )

                    if annotation_text:
                        annotation_entries.append(
                            (
                                QgsGeometry(geometry),
                                annotation_text
                            )
                        )

                except Exception as exception:
                    self.log_ignored_exception(
                        "Could not add Red Layer sketch to export overlay",
                        exception
                    )

            for geometry, annotation_text in annotation_entries:
                self.draw_red_layer_annotation(
                    painter,
                    geometry,
                    annotation_text,
                    scale_factor
                )

        finally:
            painter.end()

        if drawn_sketch_count == 0:
            return None

        overlay_image.setDotsPerMeterX(
            self.image_dots_per_meter()
        )
        overlay_image.setDotsPerMeterY(
            self.image_dots_per_meter()
        )

        return overlay_image
        

    def export_current_map_layout_to_image(self, file_path):
        layout, temporary_paths = self.create_temporary_layout_with_map_and_legend()

        exporter = QgsLayoutExporter(layout)

        settings = QgsLayoutExporter.ImageExportSettings()
        settings.dpi = self.map_export_dpi()

        result = exporter.exportToImage(
            file_path,
            settings
        )

        try:
            for temporary_path in temporary_paths:
                if temporary_path and os.path.exists(temporary_path):
                    os.remove(temporary_path)
        except Exception as exception:
            self.log_ignored_exception(
                "Could not remove temporary image export files",
                exception
            )

        try:
            success_value = QgsLayoutExporter.ExportResult.Success
        except Exception:
            success_value = QgsLayoutExporter.ExportResult.Success

        if result != success_value:
            self.show_export_warning(
                "Living Legend export",
                self.layout_export_result_message(result)
            )
            return False

        return True

    def export_current_map_layout_to_pdf(self, file_path):
        layout, temporary_paths = self.create_temporary_layout_with_map_and_legend()

        exporter = QgsLayoutExporter(layout)

        settings = QgsLayoutExporter.PdfExportSettings()
        settings.dpi = self.map_export_dpi()

        try:
            settings.rasterizeWholeImage = False
        except Exception as exception:
            self.log_ignored_exception(
                "Could not configure PDF rasterization setting",
            )

        try:
            settings.forceVectorOutput = True
        except Exception as exception:
            self.log_ignored_exception(
                "Could not configure PDF vector output setting",
                exception
            )

        result = exporter.exportToPdf(
            file_path,
            settings
        )

        try:
            for temporary_path in temporary_paths:
                if temporary_path and os.path.exists(temporary_path):
                    os.remove(temporary_path)
        except Exception as exception:
            self.log_ignored_exception(
                "Could not remove temporary PDF export files",
                exception
            )


        try:
            success_value = QgsLayoutExporter.ExportResult.Success
        except Exception:
            success_value = QgsLayoutExporter.ExportResult.Success

        if result != success_value:
            self.show_export_warning(
                "Living Legend export",
                self.layout_export_result_message(result)
            )
            return False

        return True

    def render_current_map_layout_to_clipboard_image(self):
        layout, temporary_paths = self.create_temporary_layout_with_map_and_legend()

        exporter = QgsLayoutExporter(layout)

        try:
            image = exporter.renderPageToImage(
                0,
                QSize(),
                self.map_export_dpi()
            )
        except Exception:
            try:
                image = exporter.renderPageToImage(0)
            except Exception:
                image = QImage()

        try:
            for temporary_path in temporary_paths:
                if temporary_path and os.path.exists(temporary_path):
                    os.remove(temporary_path)
        except Exception as exception:
            self.log_ignored_exception(
                "Could not remove temporary clipboard export files",
                exception
            )

        if not image.isNull():
            image.setDotsPerMeterX(self.image_dots_per_meter())
            image.setDotsPerMeterY(self.image_dots_per_meter())

        return image

    def save_pixmap_as_image(self, pixmap, title, default_name):
        if pixmap.isNull():
            return None

        file_path, selected_filter = QFileDialog.getSaveFileName(
            self,
            title,
            default_name,
            "PNG Image (*.png);;JPEG Image (*.jpg *.jpeg)"
        )

        if not file_path:
            return None

        lower_path = file_path.lower()

        if not (
            lower_path.endswith(".png") or
            lower_path.endswith(".jpg") or
            lower_path.endswith(".jpeg")
        ):
            file_path += ".png"
            lower_path = file_path.lower()

        image = pixmap.toImage()
        image.setDotsPerMeterX(self.image_dots_per_meter())
        image.setDotsPerMeterY(self.image_dots_per_meter())

        try:
            if lower_path.endswith(".jpg") or lower_path.endswith(".jpeg"):
                saved = bool(image.save(file_path, "JPG", 95))
            else:
                saved = bool(image.save(file_path, "PNG"))

            if saved:
                return file_path

        except Exception as exception:
            self.log_ignored_exception(
                "Could not save legend pixmap as image",
                exception
            )

        return None
    
    def write_pixmap_to_exact_size_pdf(self, pixmap, file_path):
        if pixmap.isNull():
            return

        dpi = self.map_export_dpi()

        width_mm = pixmap.width() / dpi * 25.4
        height_mm = pixmap.height() / dpi * 25.4

        try:
            printer = QPrinter(QPrinter.PrinterMode.HighResolution)
        except Exception:
            printer = QPrinter(QPrinter.PrinterMode.HighResolution)

        try:
            printer.setResolution(int(dpi))
        except Exception as exception:
            self.log_ignored_exception(
                "Could not set PDF printer resolution",
                exception
            )

        try:
            printer.setOutputFormat(QPrinter.OutputFormat.PdfFormat)
        except Exception:
            printer.setOutputFormat(QPrinter.OutputFormat.PdfFormat)

        printer.setOutputFileName(file_path)
        printer.setFullPage(True)

        try:
            printer.setPageSizeMM(QSizeF(width_mm, height_mm))
        except Exception:
            try:
                printer.setPaperSize(
                    QSizeF(width_mm, height_mm),
                    QPrinter.Unit.Millimeter
                )
            except Exception as exception:
                self.log_ignored_exception(
                    "Could not set PDF page size",
                    exception
                )

        painter = QPainter(printer)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
        painter.setRenderHint(QPainter.RenderHint.TextAntialiasing, True)
        painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform, True)

        page_rect = painter.viewport()

        target_rect = QRectF(
            page_rect.x(),
            page_rect.y(),
            page_rect.width(),
            page_rect.height()
        )

        source_rect = QRectF(
            0,
            0,
            pixmap.width(),
            pixmap.height()
        )

        painter.drawPixmap(target_rect, pixmap, source_rect)
        painter.end()

    def save_pixmap_as_pdf(self, pixmap, title, default_name):
        if pixmap.isNull():
            return None

        file_path, selected_filter = QFileDialog.getSaveFileName(
            self,
            title,
            default_name,
            "PDF File (*.pdf)"
        )

        if not file_path:
            return None

        if not file_path.lower().endswith(".pdf"):
            file_path += ".pdf"

        try:
            self.write_pixmap_to_exact_size_pdf(
                pixmap,
                file_path
            )
            return file_path

        except Exception as exception:
            self.log_ignored_exception(
                "Could not save legend pixmap as PDF",
                exception
            )

        return None

    def export_map_extent_as_image(self):
        file_path, selected_filter = QFileDialog.getSaveFileName(
            self,
            "Export Current Map Extent with Living Legend as Image",
            "map_with_living_legend.png",
            "PNG Image (*.png);;JPEG Image (*.jpg *.jpeg)"
        )

        if not file_path:
            return

        lower_path = file_path.lower()

        if not (
            lower_path.endswith(".png") or
            lower_path.endswith(".jpg") or
            lower_path.endswith(".jpeg")
        ):
            file_path += ".png"

        if self.export_current_map_layout_to_image(file_path):
            self.show_export_success_message(
                "Living Legend",
                "Map exported as image.",
                file_path
            )

    def export_map_extent_as_pdf(self):
        file_path, selected_filter = QFileDialog.getSaveFileName(
            self,
            "Export Current Map Extent with Living Legend as PDF",
            "map_with_living_legend.pdf",
            "PDF File (*.pdf)"
        )

        if not file_path:
            return

        if not file_path.lower().endswith(".pdf"):
            file_path += ".pdf"

        if self.export_current_map_layout_to_pdf(file_path):
            self.show_export_success_message(
                "Living Legend",
                "Map exported as PDF.",
                file_path
            )

    def export_legend_as_image(self):
        file_path = self.save_pixmap_as_image(
            self.grab_legend_pixmap(),
            "Export Living Legend as Image",
            "living_legend.png"
        )

        if file_path:
            self.show_export_success_message(
                "Living Legend",
                "Legend exported as image.",
                file_path
            )

    def export_legend_as_pdf(self):
        file_path = self.save_pixmap_as_pdf(
            self.grab_legend_pixmap(),
            "Export Living Legend as PDF",
            "living_legend.pdf"
        )

        if file_path:
            self.show_export_success_message(
                "Living Legend",
                "Legend exported as PDF.",
                file_path
            )


    def set_widget_legend_context(self, widget, context):
        if widget is None:
            return

        try:
            widget.legend_context = context
        except Exception as exception:
            self.log_ignored_exception(
                "Could not assign legend context to widget",
                exception
            )

        # EditableLegendLabel has inner label/edit widgets.
        # Add the same context to them, so right-clicking the visible text works.
        for child_name in ["label", "edit"]:
            try:
                child = getattr(widget, child_name)
                child.legend_context = context
            except Exception as exception:
                self.log_ignored_exception(
                    "Could not assign legend context to child widget",
                    exception
                )

    def legend_context_from_widget(self, widget):
        current_widget = widget

        while current_widget is not None:
            try:
                context = getattr(current_widget, "legend_context", None)
                if context:
                    return context
            except Exception as exception:
                self.log_ignored_exception(
                    "Could not read legend context from widget",
                    exception
                )

            if current_widget == self:
                break

            try:
                current_widget = current_widget.parentWidget()
            except Exception:
                break

        return None

    def layer_from_legend_context(self, context):
        if not context:
            return None

        layer_id = context.get("layer_id")

        if not layer_id:
            return None

        try:
            return QgsProject.instance().mapLayer(layer_id)
        except Exception:
            return None

    def feature_ids_for_legend_context(self, context):
        layer = self.layer_from_legend_context(context)

        if layer is None:
            return []

        target_type = context.get("target_type")

        if target_type in ["layer", "single"]:
            try:
                return [
                    feature.id()
                    for feature in self.get_features_in_current_extent(layer)
                ]
            except Exception:
                return []

        renderer = layer.renderer()

        if isinstance(renderer, QgsCategorizedSymbolRenderer):
            if target_type != "category":
                return []

            category_value = str(context.get("category_value", ""))

            try:
                class_attribute = renderer.classAttribute()
                ids = []

                for feature in self.get_features_in_current_extent(layer):
                    value = self.get_attribute_or_expression_value(
                        layer,
                        feature,
                        class_attribute
                    )

                    if str(value) == category_value:
                        ids.append(feature.id())

                return ids

            except Exception:
                return []

        if isinstance(renderer, QgsGraduatedSymbolRenderer):
            if target_type != "range":
                return []

            try:
                range_index = int(context.get("range_index", -1))
                ranges = renderer.ranges()

                if range_index < 0 or range_index >= len(ranges):
                    return []

                range_item = ranges[range_index]
                lower_value = range_item.lowerValue()
                upper_value = range_item.upperValue()
                class_attribute = renderer.classAttribute()

                ids = []

                for feature in self.get_features_in_current_extent(layer):
                    value = self.get_attribute_or_expression_value(
                        layer,
                        feature,
                        class_attribute
                    )

                    numeric_value = self.float_or_none(value)

                    if numeric_value is None:
                        continue

                    if lower_value <= numeric_value <= upper_value:
                        ids.append(feature.id())

                return ids

            except Exception:
                return []

        return []

    def feature_ids_for_zoom_legend_context(self, context):
        layer = self.layer_from_legend_context(context)

        if layer is None:
            return []

        target_type = context.get("target_type")

        if target_type in ["layer", "single"]:
            try:
                return [
                    feature.id()
                    for feature in layer.getFeatures()
                ]
            except Exception:
                return []

        renderer = layer.renderer()

        if isinstance(renderer, QgsCategorizedSymbolRenderer):
            if target_type != "category":
                return []

            category_value = str(context.get("category_value", ""))

            try:
                class_attribute = renderer.classAttribute()
                ids = []

                for feature in layer.getFeatures():
                    value = self.get_attribute_or_expression_value(
                        layer,
                        feature,
                        class_attribute
                    )

                    if str(value) == category_value:
                        ids.append(feature.id())

                return ids

            except Exception:
                return []

        if isinstance(renderer, QgsGraduatedSymbolRenderer):
            if target_type != "range":
                return []

            try:
                range_index = int(context.get("range_index", -1))
                ranges = renderer.ranges()

                if range_index < 0 or range_index >= len(ranges):
                    return []

                range_item = ranges[range_index]
                lower_value = range_item.lowerValue()
                upper_value = range_item.upperValue()
                class_attribute = renderer.classAttribute()

                ids = []

                for feature in layer.getFeatures():
                    value = self.get_attribute_or_expression_value(
                        layer,
                        feature,
                        class_attribute
                    )

                    numeric_value = self.float_or_none(value)

                    if numeric_value is None:
                        continue

                    if lower_value <= numeric_value <= upper_value:
                        ids.append(feature.id())

                return ids

            except Exception:
                return []

        return []

    def extent_for_feature_ids(self, layer, feature_ids):
        if layer is None:
            return None

        if not feature_ids:
            return None

        combined_extent = None

        try:
            request = QgsFeatureRequest()
            request.setFilterFids(feature_ids)

            for feature in layer.getFeatures(request):
                geometry = feature.geometry()

                if geometry is None:
                    continue

                try:
                    if geometry.isEmpty():
                        continue
                except Exception as exception:
                    self.log_ignored_exception(
                        "Could not check whether feature geometry is empty",
                        exception
                    )

                feature_extent = self.geometry_bounding_box_or_none(geometry)

                if feature_extent is None:
                    continue

                if combined_extent is None:
                    combined_extent = QgsRectangle(feature_extent)
                else:
                    combined_extent.combineExtentWith(feature_extent)

        except Exception:
            return None

        if combined_extent is None:
            return None

        try:
            source_crs = layer.crs()
            destination_crs = self.canvas.mapSettings().destinationCrs()

            if (
                source_crs.isValid() and
                destination_crs.isValid() and
                source_crs != destination_crs
            ):
                coordinate_transform = QgsCoordinateTransform(
                    source_crs,
                    destination_crs,
                    QgsProject.instance()
                )

                combined_extent = coordinate_transform.transformBoundingBox(
                    combined_extent
                )

        except Exception as exception:
            self.log_ignored_exception(
                "Could not transform feature extent to canvas CRS",
                exception
            )

        return combined_extent

    def padded_zoom_extent(self, extent):
        if extent is None:
            return None

        try:
            current_extent = self.canvas.extent()
        except Exception:
            current_extent = None

        try:
            width = float(extent.width())
            height = float(extent.height())
            center_x = float(extent.center().x())
            center_y = float(extent.center().y())
        except Exception:
            return None

        if current_extent is not None:
            try:
                current_width = max(float(current_extent.width()), 1.0)
                current_height = max(float(current_extent.height()), 1.0)
            except Exception:
                current_width = 1.0
                current_height = 1.0
        else:
            current_width = 1.0
            current_height = 1.0

        # Single points or perfectly vertical/horizontal point groups can have
        # zero width or height. Give them a sane minimum extent so QGIS really
        # changes the map extent instead of only panning.
        minimum_width = current_width * 0.10
        minimum_height = current_height * 0.10

        if width <= 0:
            width = minimum_width

        if height <= 0:
            height = minimum_height

        # Add padding around the actual feature extent.
        width = width * 1.20
        height = height * 1.20

        # Preserve the current canvas aspect ratio. Without this, QGIS may adjust
        # the requested extent in a way that looks unexpected, especially for
        # narrow point distributions.
        try:
            canvas_width = max(float(self.parent_widget.width()), 1.0)
            canvas_height = max(float(self.parent_widget.height()), 1.0)
            canvas_aspect = canvas_width / canvas_height
        except Exception:
            canvas_aspect = 1.0

        if canvas_aspect > 0:
            extent_aspect = width / height

            if extent_aspect > canvas_aspect:
                height = width / canvas_aspect
            else:
                width = height * canvas_aspect

        return QgsRectangle(
            center_x - (width / 2.0),
            center_y - (height / 2.0),
            center_x + (width / 2.0),
            center_y + (height / 2.0)
        )

    def zoom_to_legend_context(self, context):
        layer = self.layer_from_legend_context(context)

        if layer is None:
            return

        feature_ids = self.feature_ids_for_zoom_legend_context(context)

        if not feature_ids:
            self.show_export_warning(
                "Living Legend",
                "No matching feature could be found for this legend item."
            )
            return

        try:
            self.iface.setActiveLayer(layer)
        except Exception as exception:
            self.log_ignored_exception(
                "Could not set active layer before zooming",
                exception
            )

        feature_extent = self.extent_for_feature_ids(
            layer,
            feature_ids
        )

        zoom_extent = self.padded_zoom_extent(feature_extent)

        if zoom_extent is None:
            self.show_export_warning(
                "Living Legend",
                "The matching feature extent could not be calculated."
            )
            return

        try:
            self.canvas.setExtent(zoom_extent)
            self.canvas.refresh()
        except Exception as exception:
            self.log_ignored_exception(
                "Could not apply zoom extent to canvas",
                exception
            )

    def open_attribute_table_for_legend_context(self, context):
        layer = self.layer_from_legend_context(context)

        if layer is None:
            return

        try:
            self.iface.setActiveLayer(layer)
        except Exception as exception:
            self.log_ignored_exception(
                "Could not set active layer before opening attribute table",
                exception
            )

        try:
            self.iface.showAttributeTable(layer)
            return
        except Exception as exception:
            self.log_ignored_exception(
                "Could not open attribute table through iface.showAttributeTable",
                exception
            )

        try:
            self.iface.actionOpenTable().trigger()
        except Exception as exception:
            self.log_ignored_exception(
                "Could not open attribute table through fallback action",
                exception
            )

    def show_layer_in_layer_panel_for_legend_context(self, context):
        layer = self.layer_from_legend_context(context)

        if layer is None:
            return

        try:
            self.iface.setActiveLayer(layer)
        except Exception as exception:
            self.log_ignored_exception(
                "Could not set active layer before showing layer in Layers panel",
                exception
            )

        try:
            layer_tree_view = self.iface.layerTreeView()
        except Exception:
            return

        try:
            layer_tree_view.setCurrentLayer(layer)
        except Exception as exception:
            self.log_ignored_exception(
                "Could not select layer in Layers panel",
                exception
            )

        try:
            current_index = layer_tree_view.currentIndex()

            if current_index.isValid():
                layer_tree_view.scrollTo(current_index)
        except Exception as exception:
            self.log_ignored_exception(
                "Could not scroll Layers panel to selected layer",
                exception
            )

        try:
            layer_tree_view.setFocus()
        except Exception as exception:
            self.log_ignored_exception(
                "Could not focus Layers panel",
                exception
            )

        try:
            parent_widget = layer_tree_view.parentWidget()

            while parent_widget is not None:
                if isinstance(parent_widget, QDockWidget):
                    parent_widget.show()
                    parent_widget.raise_()
                    break

                parent_widget = parent_widget.parentWidget()
        except Exception as exception:
            self.log_ignored_exception(
                "Could not raise Layers panel dock widget",
                exception
            )

    def disable_layer_node_in_layer_panel(self, layer):
        layer_disabled = False

        try:
            root = QgsProject.instance().layerTreeRoot()

            for node in root.findLayers():
                try:
                    node_layer = node.layer()

                    if node_layer is None:
                        continue

                    if node_layer.id() != layer.id():
                        continue

                    node.setItemVisibilityChecked(False)
                    layer_disabled = True

                except Exception as exception:
                    self.log_ignored_exception(
                        "Could not disable layer tree node visibility",
                        exception
                    )

        except Exception as exception:
            self.log_ignored_exception(
                "Could not access layer tree while disabling layer visibility",
                exception
            )

        return layer_disabled

    def disable_categorized_legend_item(self, layer, renderer, context):
        category_value = str(context.get("category_value", ""))

        try:
            categories = renderer.categories()

            for category_index, category in enumerate(categories):
                if str(category.value()) != category_value:
                    continue

                renderer.updateCategoryRenderState(
                    category_index,
                    False
                )
                return True

        except Exception as exception:
            self.log_ignored_exception(
                "Could not disable categorized renderer legend item",
                exception
            )

        return False

    def disable_graduated_legend_item(self, layer, renderer, context):
        try:
            range_index = int(context.get("range_index", -1))

            if range_index < 0:
                return False

            ranges = renderer.ranges()

            if range_index >= len(ranges):
                return False

            renderer.updateRangeRenderState(
                range_index,
                False
            )
            return True

        except Exception as exception:
            self.log_ignored_exception(
                "Could not disable graduated renderer legend item",
                exception
            )

        return False

    def disable_legend_context_in_layer_panel(self, context):
        layer = self.layer_from_legend_context(context)

        if layer is None:
            return

        try:
            self.iface.setActiveLayer(layer)
        except Exception as exception:
            self.log_ignored_exception(
                "Could not set active layer before disabling legend context",
                exception
            )

        target_type = context.get("target_type", "layer")
        disabled = False

        if target_type == "layer":
            disabled = self.disable_layer_node_in_layer_panel(layer)

        else:
            renderer = None

            try:
                renderer = layer.renderer()
            except Exception as exception:
                self.log_ignored_exception(
                    "Could not read renderer before disabling legend item",
                    exception
                )

            if isinstance(renderer, QgsCategorizedSymbolRenderer):
                disabled = self.disable_categorized_legend_item(
                    layer,
                    renderer,
                    context
                )

            elif isinstance(renderer, QgsGraduatedSymbolRenderer):
                disabled = self.disable_graduated_legend_item(
                    layer,
                    renderer,
                    context
                )

            else:
                disabled = self.disable_layer_node_in_layer_panel(layer)

        if not disabled:
            self.log_ignored_exception(
                "No matching layer or legend item found while disabling context",
                RuntimeError(str(context))
            )
            return

        try:
            layer.triggerRepaint()
        except Exception as exception:
            self.log_ignored_exception(
                "Could not trigger layer repaint after disabling legend context",
                exception
            )

        try:
            self.iface.layerTreeView().refreshLayerSymbology(layer.id())
        except Exception as exception:
            self.log_ignored_exception(
                "Could not refresh layer symbology after disabling legend context",
                exception
            )

        try:
            QgsProject.instance().setDirty(True)
        except Exception as exception:
            self.log_ignored_exception(
                "Could not mark project dirty after disabling legend context",
                exception
            )

        try:
            self.canvas.refresh()
        except Exception as exception:
            self.log_ignored_exception(
                "Could not refresh canvas after disabling legend context",
                exception
            )

        self.requestLegendUpdate()

    def layer_hidden_legend_key(self, layer_id):
        return f"hidden::{layer_id}::layer::layer"

    def hidden_legend_key_from_context(self, context):
        layer_id = str(context.get("layer_id", ""))
        target_type = str(context.get("target_type", "layer"))

        if target_type == "category":
            identifier = str(context.get("category_value", ""))
        elif target_type == "range":
            identifier = str(context.get("range_index", -1))
        elif target_type == "single":
            identifier = "layer_name"
        else:
            identifier = "layer"

        return f"hidden::{layer_id}::{target_type}::{identifier}"

    def is_layer_hidden_in_living_legend(self, layer):
        if layer is None:
            return False

        try:
            return self.layer_hidden_legend_key(layer.id()) in self.hidden_legend_items
        except Exception as exception:
            self.log_ignored_exception(
                "Could not check whether layer is hidden in Living Legend",
                exception
            )

        return False

    def is_legend_context_hidden_in_living_legend(self, context):
        if not context:
            return False

        try:
            layer_id = str(context.get("layer_id", ""))
            target_type = str(context.get("target_type", "layer"))

            if target_type != "layer":
                layer_key = self.layer_hidden_legend_key(layer_id)

                if layer_key in self.hidden_legend_items:
                    return True

            context_key = self.hidden_legend_key_from_context(context)
            return context_key in self.hidden_legend_items

        except Exception as exception:
            self.log_ignored_exception(
                "Could not check whether legend context is hidden in Living Legend",
                exception
            )

        return False

    def hidden_legend_context_display_label(self, context):
        layer = self.layer_from_legend_context(context)
        layer_name = "Unknown layer"

        if layer is not None:
            try:
                layer_name = layer.name()
            except Exception as exception:
                self.log_ignored_exception(
                    "Could not read layer name for hidden legend item label",
                    exception
                )

        target_type = str(context.get("target_type", "layer"))
        display_label = str(context.get("display_label", "")).strip()

        if target_type == "layer":
            return layer_name

        if display_label:
            return f"{layer_name}: {display_label}"

        if target_type == "category":
            return f'{layer_name}: {context.get("category_value", "")}'

        if target_type == "range":
            return f'{layer_name}: range {context.get("range_index", -1)}'

        return layer_name

    def hide_legend_context_in_living_legend(self, context):
        if not context:
            return

        try:
            context_key = self.hidden_legend_key_from_context(context)
            hidden_context = dict(context)

            if not str(hidden_context.get("display_label", "")).strip():
                hidden_context["display_label"] = self.hidden_legend_context_display_label(
                    hidden_context
                )

            self.hidden_legend_items[context_key] = hidden_context

        except Exception as exception:
            self.log_ignored_exception(
                "Could not hide legend context in Living Legend",
                exception
            )
            return

        self.requestLegendUpdate()

    def show_hidden_legend_context_in_living_legend(self, context):
        if not context:
            return

        try:
            context_key = self.hidden_legend_key_from_context(context)

            if context_key in self.hidden_legend_items:
                del self.hidden_legend_items[context_key]

        except Exception as exception:
            self.log_ignored_exception(
                "Could not show hidden legend context in Living Legend",
                exception
            )
            return

        self.requestLegendUpdate()

    def show_hidden_legend_contexts_in_living_legend(self, contexts):
        try:
            for context in contexts:
                context_key = self.hidden_legend_key_from_context(context)

                if context_key in self.hidden_legend_items:
                    del self.hidden_legend_items[context_key]

        except Exception as exception:
            self.log_ignored_exception(
                "Could not show multiple hidden legend contexts in Living Legend",
                exception
            )
            return

        self.requestLegendUpdate()

    def layer_node_is_visible_for_layer(self, layer):
        if layer is None:
            return False

        try:
            root = QgsProject.instance().layerTreeRoot()

            for node in root.findLayers():
                try:
                    node_layer = node.layer()

                    if node_layer is None:
                        continue

                    if node_layer.id() != layer.id():
                        continue

                    return bool(node.isVisible())

                except Exception as exception:
                    self.log_ignored_exception(
                        "Could not check layer tree node visibility",
                        exception
                    )

        except Exception as exception:
            self.log_ignored_exception(
                "Could not access layer tree while checking hidden item visibility",
                exception
            )

        return False

    def hidden_category_is_in_current_extent(self, layer, renderer, context):
        category_value = str(context.get("category_value", ""))

        try:
            matching_category = None

            for category in renderer.categories():
                if str(category.value()) == category_value:
                    matching_category = category
                    break

            if matching_category is None:
                return False

            if not self.legend_item_is_enabled(renderer, matching_category):
                return False

            visible_value_counts = self.collect_visible_categorized_values(
                layer,
                renderer
            )

            return category_value in visible_value_counts

        except Exception as exception:
            self.log_ignored_exception(
                "Could not check whether hidden categorized item is in current extent",
                exception
            )

        return False

    def hidden_range_is_in_current_extent(self, layer, renderer, context):
        try:
            range_index = int(context.get("range_index", -1))

            if range_index < 0:
                return False

            ranges = renderer.ranges()

            if range_index >= len(ranges):
                return False

            range_item = ranges[range_index]

            if not self.legend_item_is_enabled(renderer, range_item):
                return False

            visible_range_counts = self.collect_visible_graduated_ranges(
                layer,
                renderer
            )

            return range_index in visible_range_counts

        except Exception as exception:
            self.log_ignored_exception(
                "Could not check whether hidden graduated item is in current extent",
                exception
            )

        return False

    def hidden_context_is_in_current_map_extent(self, context):
        layer = self.layer_from_legend_context(context)

        if layer is None:
            return False

        if not self.layer_node_is_visible_for_layer(layer):
            return False

        try:
            if layer.type() != QgsMapLayerType.VectorLayer:
                return False
        except Exception as exception:
            self.log_ignored_exception(
                "Could not check hidden legend item layer type",
                exception
            )
            return False

        try:
            if not layer.isInScaleRange(self.canvas.scale()):
                return False
        except Exception as exception:
            self.log_ignored_exception(
                "Could not check hidden legend item scale range",
                exception
            )

        target_type = str(context.get("target_type", "layer"))

        if target_type in ["layer", "single"]:
            return self.layer_has_features_in_current_extent(layer)

        renderer = None

        try:
            renderer = layer.renderer()
        except Exception as exception:
            self.log_ignored_exception(
                "Could not read renderer while checking hidden legend item extent",
                exception
            )

        if isinstance(renderer, QgsCategorizedSymbolRenderer):
            return self.hidden_category_is_in_current_extent(
                layer,
                renderer,
                context
            )

        if isinstance(renderer, QgsGraduatedSymbolRenderer):
            return self.hidden_range_is_in_current_extent(
                layer,
                renderer,
                context
            )

        return False

    def hidden_legend_contexts_in_current_map_extent(self):
        hidden_contexts = []

        try:
            for hidden_context in self.hidden_legend_items.values():
                if self.hidden_context_is_in_current_map_extent(hidden_context):
                    hidden_contexts.append(hidden_context)

        except Exception as exception:
            self.log_ignored_exception(
                "Could not collect hidden legend items in current map extent",
                exception
            )

        hidden_contexts.sort(
            key=lambda context: self.hidden_legend_context_display_label(context).lower()
        )

        return hidden_contexts

    def apply_context_menu_style(self, menu):
        try:
            menu.setSeparatorsCollapsible(False)
        except Exception as exception:
            self.log_ignored_exception(
                "Could not configure context menu separator behavior",
                exception
            )

        menu.setStyleSheet("""
            QMenu::separator {
                height: 0px;
                border: none;
                border-top: 1px solid rgba(120, 120, 120, 140);
                margin-left: 8px;
                margin-right: 8px;
                margin-top: 5px;
                margin-bottom: 5px;
                background: transparent;
            }
        """)

    def zoom_to_layer_menu_icon(self, menu):
        try:
            layer_tree_view = self.iface.layerTreeView()
            default_actions = layer_tree_view.defaultActions()

            for method_name in [
                "actionZoomToLayers",
                "actionZoomToLayer"
            ]:
                if not hasattr(default_actions, method_name):
                    continue

                method = getattr(default_actions, method_name)
                action = method(self.canvas, menu)

                if action is None:
                    continue

                icon = action.icon()

                if icon is not None and not icon.isNull():
                    return icon

        except Exception as exception:
            self.log_ignored_exception(
                "Could not read Zoom to Layer icon from QGIS layer tree actions",
                exception
            )

        try:
            icon = QgsApplication.getThemeIcon(
                "/mActionZoomToLayer.svg"
            )

            if icon is not None and not icon.isNull():
                return icon

        except Exception as exception:
            self.log_ignored_exception(
                "Could not read Zoom to Layer icon from QGIS theme",
                exception
            )

        return None

    def attribute_table_menu_icon(self):
        try:
            action = self.iface.actionOpenTable()

            if action is not None:
                icon = action.icon()

                if icon is not None and not icon.isNull():
                    return icon

        except Exception as exception:
            self.log_ignored_exception(
                "Could not read Open Attribute Table icon from QGIS interface action",
                exception
            )

        try:
            icon = QgsApplication.getThemeIcon(
                "/mActionOpenTable.svg"
            )

            if icon is not None and not icon.isNull():
                return icon

        except Exception as exception:
            self.log_ignored_exception(
                "Could not read Open Attribute Table icon from QGIS theme",
                exception
            )

        return None

    def create_menu_action_with_optional_icon(self, text, menu, icon=None):
        if icon is None:
            return QAction(text, menu)

        try:
            if not icon.isNull():
                return QAction(icon, text, menu)

        except Exception as exception:
            self.log_ignored_exception(
                "Could not apply icon to context menu action",
                exception
            )

        return QAction(text, menu)

    def add_hidden_legend_items_menu(self, menu):
        hidden_contexts = self.hidden_legend_contexts_in_current_map_extent()
        hidden_count = len(hidden_contexts)

        hidden_menu = menu.addMenu(
            f"Hidden legend items [{hidden_count}]"
        )

        if not hidden_contexts:
            no_hidden_action = QAction(
                "No hidden items in current map extent",
                hidden_menu
            )
            no_hidden_action.setEnabled(False)
            hidden_menu.addAction(no_hidden_action)
            return

        show_all_action = QAction(
            "Show all",
            hidden_menu
        )
        show_all_action.triggered.connect(
            lambda checked=False, contexts=[
                dict(context) for context in hidden_contexts
            ]: self.show_hidden_legend_contexts_in_living_legend(
                contexts
            )
        )
        hidden_menu.addAction(show_all_action)
        hidden_menu.addSeparator()

        for hidden_context in hidden_contexts:
            item_menu = hidden_menu.addMenu(
                self.hidden_legend_context_display_label(hidden_context)
            )

            show_action = QAction(
                "Show", # former Show in Living Legend
                item_menu
            )
            show_action.triggered.connect(
                lambda checked=False, context=dict(hidden_context): self.show_hidden_legend_context_in_living_legend(
                    context
                )
            )
            item_menu.addAction(show_action)

    def show_context_menu(self, event, clicked_widget=None):
        # Make sure any dragging state is cancelled before opening the menu.
        self.dragging = False

        try:
            self.releaseMouse()
        except Exception as exception:
            self.log_ignored_exception(
                "Could not release mouse before showing context menu",
                exception
            )

        menu = QMenu(self)
        self.apply_context_menu_style(menu)

        legend_context = self.legend_context_from_widget(clicked_widget)

        if legend_context:
            target_type = legend_context.get("target_type", "layer")

            if target_type == "layer":
                zoom_text = "&Zoom to Layer"
                table_text = "&Open Attribute Table for layer"
            else:
                zoom_text = "&Zoom to Item"
                table_text = "&Open Attribute Table for layer"

            zoom_icon = self.zoom_to_layer_menu_icon(menu)

            zoom_action = self.create_menu_action_with_optional_icon(
                zoom_text,
                menu,
                zoom_icon
            )

            zoom_action.triggered.connect(
                lambda checked=False, context=legend_context: self.zoom_to_legend_context(
                    context
                )
            )
            menu.addAction(zoom_action)

            table_icon = self.attribute_table_menu_icon()

            table_action = self.create_menu_action_with_optional_icon(
                table_text,
                menu,
                table_icon
            )

            table_action.triggered.connect(
                lambda checked=False, context=legend_context: self.open_attribute_table_for_legend_context(
                    context
                )
            )
            menu.addAction(table_action)

            jump_to_layer_panel_icon_path = os.path.join(
                os.path.dirname(__file__),
                "icons",
                "JumpToLayerPanel.svg"
            )

            show_layer_action = QAction(
                QIcon(jump_to_layer_panel_icon_path),
                "&Jump to Layer in layers panel",
                menu
            )
            show_layer_action.triggered.connect(
                lambda checked=False, context=legend_context: self.show_layer_in_layer_panel_for_legend_context(
                    context
                )
            )
            menu.addAction(show_layer_action)

            disable_icon = os.path.join(os.path.dirname(__file__), "icons", "DisableFeature.svg")

            if target_type == "layer":
                disable_text = "&Disable Layer in layers panel"
                disable_layer_action = QAction(
                    QIcon(disable_icon),
                    disable_text,
                    menu
                )
            else:
                disable_text = "&Disable Item in layers panel"
                disable_layer_action = QAction(
                    QIcon(disable_icon),
                    disable_text,
                    menu
                )

            disable_layer_action.triggered.connect(
                lambda checked=False, context=legend_context: self.disable_legend_context_in_layer_panel(
                    context
                )
            )
            menu.addAction(disable_layer_action)

            hide_icon = os.path.join(os.path.dirname(__file__), "icons", "HideFeature.svg")

            if target_type == "layer":
                hide_text = "&Hide Layer in living legend"
                hide_legend_item_action = QAction(
                    QIcon(hide_icon),
                    hide_text,
                    menu
                )
            else:
                hide_text = "&Hide Item in living legend"
                hide_legend_item_action = QAction(
                    QIcon(hide_icon),
                    hide_text,
                    menu
                )               

            hide_legend_item_action.triggered.connect(
                lambda checked=False, context=legend_context: self.hide_legend_context_in_living_legend(
                    context
                )
            )
            menu.addAction(hide_legend_item_action)

            menu.addSeparator()

        copy_map_icon_path = os.path.join(
            os.path.dirname(__file__),
            "icons",
            "CopyMapIcon.svg"
        )

        copy_map_action = QAction(
            QIcon(copy_map_icon_path),
            "Copy &Map to clipboard",
            menu
        )
        copy_map_action.triggered.connect(self.copy_map_extent_to_clipboard)
        menu.addAction(copy_map_action)

        copy_legend_icon_path = os.path.join(
            os.path.dirname(__file__),
            "icons",
            "CopyLegendIcon.svg"
        )

        copy_legend_action = QAction(
            QIcon(copy_legend_icon_path),
            "Copy &Legend to clipboard",
            menu
        )
        copy_legend_action.triggered.connect(self.copy_legend_to_clipboard)
        menu.addAction(copy_legend_action)

        export_menu = menu.addMenu("&Export")
        export_menu.setIcon(
            QIcon(":images/themes/default/mActionSharingExport.svg")
        )

        export_map_menu = export_menu.addMenu("&Map")

        export_map_image_action = QAction("As &image...", export_map_menu)
        export_map_image_action.triggered.connect(self.export_map_extent_as_image)
        export_map_menu.addAction(export_map_image_action)

        export_map_pdf_action = QAction("As &PDF...", export_map_menu)
        export_map_pdf_action.triggered.connect(self.export_map_extent_as_pdf)
        export_map_menu.addAction(export_map_pdf_action)

        export_legend_menu = export_menu.addMenu("&Legend")

        export_legend_image_action = QAction("As &image...", export_legend_menu)
        export_legend_image_action.triggered.connect(self.export_legend_as_image)
        export_legend_menu.addAction(export_legend_image_action)

        export_legend_pdf_action = QAction("As &PDF...", export_legend_menu)
        export_legend_pdf_action.triggered.connect(self.export_legend_as_pdf)
        export_legend_menu.addAction(export_legend_pdf_action)

        menu.addSeparator()

        layer_name_action = QAction("Show Layer &Name", menu)
        layer_name_action.setCheckable(True)
        layer_name_action.setChecked(self.show_layer_name)
        layer_name_action.toggled.connect(self.set_show_layer_name)
        menu.addAction(layer_name_action)

        area_action = QAction("Show &Item Dimensions", menu)
        area_action.setCheckable(True)
        area_action.setChecked(self.show_feature_area)
        area_action.toggled.connect(self.set_show_feature_area)
        menu.addAction(area_action)

        count_action = QAction("Show Feature &Count", menu)
        count_action.setCheckable(True)
        count_action.setChecked(self.show_feature_counts)
        count_action.toggled.connect(self.set_show_feature_counts)
        menu.addAction(count_action)

        legend_title_action = QAction("Show Legend &Title", menu)
        legend_title_action.setCheckable(True)
        legend_title_action.setChecked(self.show_legend_title)
        legend_title_action.toggled.connect(self.set_show_legend_title)
        menu.addAction(legend_title_action)

        sync_action = QAction("S&ync Label Changes with QGIS Legend", menu)
        sync_action.setCheckable(True)
        sync_action.setChecked(self.sync_label_changes_with_project)
        sync_action.toggled.connect(self.set_sync_label_changes_with_project)
        menu.addAction(sync_action)

        self.add_hidden_legend_items_menu(menu)

        menu.addSeparator()

        stick_action = QAction("Stick to Corner", menu)
        stick_action.setCheckable(True)
        stick_action.setChecked(self.stick_to_corner)
        stick_action.toggled.connect(self.set_stick_to_corner)
        menu.addAction(stick_action)

        corner_menu = menu.addMenu("Legend Position")
        corner_actions = []

        for corner in self.available_overlay_positions():
            corner_action = QAction(
                self.sticky_corner_display_name(corner),
                corner_menu
            )
            corner_action.setCheckable(True)
            corner_action.setChecked(self.sticky_corner == corner)
            corner_action.triggered.connect(
                lambda checked=False, selected_corner=corner: self.set_sticky_corner(
                    selected_corner
                )
            )

            corner_menu.addAction(corner_action)
            corner_actions.append(corner_action)

        menu.addSeparator()

        decorations_menu = menu.addMenu("Decorations")

        scale_bar_action = QAction("Show scale bar", decorations_menu)
        scale_bar_action.setCheckable(True)
        scale_bar_action.setChecked(self.scale_bar_enabled)
        scale_bar_action.toggled.connect(self.set_scale_bar_enabled)
        decorations_menu.addAction(scale_bar_action)

        scale_bar_position_menu = decorations_menu.addMenu("Scale bar position")

        for position in self.available_overlay_positions():
            position_action = QAction(
                self.overlay_position_display_name(position),
                scale_bar_position_menu
            )
            position_action.setCheckable(True)
            position_action.setChecked(self.scale_bar_position == position)
            position_action.triggered.connect(
                lambda checked=False, selected_position=position: self.set_scale_bar_position(
                    selected_position
                )
            )
            scale_bar_position_menu.addAction(position_action)

        decorations_menu.addSeparator()

        north_arrow_action = QAction("Show north arrow", decorations_menu)
        north_arrow_action.setCheckable(True)
        north_arrow_action.setChecked(self.north_arrow_enabled)
        north_arrow_action.toggled.connect(self.set_north_arrow_enabled)
        decorations_menu.addAction(north_arrow_action)

        north_arrow_position_menu = decorations_menu.addMenu("North arrow position")

        for position in self.available_overlay_positions():
            position_action = QAction(
                self.overlay_position_display_name(position),
                north_arrow_position_menu
            )
            position_action.setCheckable(True)
            position_action.setChecked(self.north_arrow_position == position)
            position_action.triggered.connect(
                lambda checked=False, selected_position=position: self.set_north_arrow_position(
                    selected_position
                )
            )
            north_arrow_position_menu.addAction(position_action)

        menu.addSeparator()

        transparency_widget = QWidget(menu)

        transparency_layout = QVBoxLayout()
        transparency_layout.setContentsMargins(8, 6, 8, 6)
        transparency_layout.setSpacing(4)
        transparency_widget.setLayout(transparency_layout)

        transparency_label = QLabel(
            f"Opacity: {int(self.legend_opacity * 100)}%"
        )

        transparency_slider = QSlider(Qt.Orientation.Horizontal)
        transparency_slider.setMinimum(35)
        transparency_slider.setMaximum(100)
        transparency_slider.setValue(int(self.legend_opacity * 100))
        transparency_slider.setFixedWidth(180)

        def update_transparency(value):
            transparency_label.setText(f"Opacity: {value}%")
            self.set_legend_opacity(value / 100.0)

        transparency_slider.valueChanged.connect(update_transparency)

        transparency_layout.addWidget(transparency_label)
        transparency_layout.addWidget(transparency_slider)

        transparency_action = QWidgetAction(menu)
        transparency_action.setDefaultWidget(transparency_widget)

        menu.addAction(transparency_action)

        menu.addSeparator()

        reset_action = QAction("Reset all settings to default", menu)
        reset_action.triggered.connect(self.reset_all_settings_to_default)
        menu.addAction(reset_action)

        try:
            global_pos = self.get_event_global_pos(event)
        except Exception:
            try:
                global_pos = event.globalPos()
            except Exception:
                global_pos = self.mapToGlobal(QPoint(0, 0))

        try:
            menu.exec(global_pos)
        except Exception:
            menu.exec(global_pos)

    def handle_legend_wheel_event(self, event):
        try:
            scroll_bar = self.scroll_area.verticalScrollBar()

            if scroll_bar is not None:
                current_value = scroll_bar.value()
                new_value = current_value

                try:
                    pixel_delta = event.pixelDelta()

                    if not pixel_delta.isNull():
                        new_value = current_value - pixel_delta.y()
                    else:
                        angle_delta = event.angleDelta().y()

                        if angle_delta != 0:
                            step = max(scroll_bar.singleStep(), 20)
                            new_value = current_value - int(angle_delta / 120.0 * step * 3)

                except Exception:
                    try:
                        delta = event.delta()
                        step = max(scroll_bar.singleStep(), 20)
                        new_value = current_value - int(delta / 120.0 * step * 3)
                    except Exception:
                        new_value = current_value

                new_value = max(
                    scroll_bar.minimum(),
                    min(scroll_bar.maximum(), new_value)
                )

                scroll_bar.setValue(new_value)

        except Exception as exception:
            self.log_ignored_exception(
                "Could not handle legend wheel event",
                exception
            )

        try:
            event.accept()
        except Exception as exception:
            self.log_ignored_exception(
                "Could not accept legend wheel event",
                exception
            )

        return True

    def handle_mouse_press(self, event):
        try:
            if event.button() != Qt.MouseButton.LeftButton:
                return False
        except Exception:
            try:
                if event.button() != Qt.MouseButton.LeftButton:
                    return False
            except Exception:
                return False

        if self.editing_active:
            return False

        self.dragging = True
        self.drag_has_moved = False
        self.drag_start_global_pos = self.get_event_global_pos(event)
        self.drag_offset = self.drag_start_global_pos - self.mapToGlobal(
            QPoint(0, 0)
        )

        try:
            self.grabMouse()
        except Exception as exception:
            self.log_ignored_exception(
                "Could not grab mouse for legend dragging",
                exception
            )

        # Consume the event because this click belongs to the legend,
        # not to the map canvas behind it.
        return True

    def handle_mouse_move(self, event):
        if not self.dragging:
            return False

        if self.editing_active:
            return False

        current_global_pos = self.get_event_global_pos(event)

        dx = current_global_pos.x() - self.drag_start_global_pos.x()
        dy = current_global_pos.y() - self.drag_start_global_pos.y()

        if not self.drag_has_moved:
            distance_squared = dx * dx + dy * dy

            if distance_squared < (
                self.drag_move_threshold_px *
                self.drag_move_threshold_px
            ):
                return True

            self.drag_has_moved = True

        new_pos = current_global_pos - self.drag_offset
        parent_global = self.parent_widget.mapToGlobal(QPoint(0, 0))

        x = new_pos.x() - parent_global.x()
        y = new_pos.y() - parent_global.y()

        margin = self.legend_edge_margin_px()

        x = max(
            margin,
            min(
                x,
                self.parent_widget.width() - self.width() - margin
            )
        )

        y = max(
            margin,
            min(
                y,
                self.parent_widget.height() - self.height() - margin
            )
        )

        self.move(x, y)

        return True

    def handle_mouse_release(self, event):
        if self.dragging:
            self.dragging = False

            try:
                self.releaseMouse()
            except Exception as exception:
                self.log_ignored_exception(
                    "Could not release mouse after legend dragging",
                    exception
                )

            if not self.drag_has_moved:
                self.drag_has_moved = False
                return True

            self.drag_has_moved = False

            if self.activate_auto_sticky_corner_if_close():
                return True

            if self.stick_to_corner:
                self.stick_to_corner = False
                self.settings.setValue("stick_to_corner", False)
                self.settings.sync()

            self.save_position()

            return True

        return False

    def register_drag_filter(self, widget):
        try:
            widget.installEventFilter(self)
            widget.setMouseTracking(True)

            try:
                widget.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)
            except Exception:
                try:
                    widget.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)
                except Exception as exception:
                    self.log_ignored_exception(
                        "Could not set context menu policy on child widget",
                        exception
                    )

        except Exception as exception:
            self.log_ignored_exception(
                "Could not register drag filter on widget",
                exception
            )

    def requestLegendUpdate(self):
        if self.editing_active:
            self.legend_update_pending = True
            return

        if self.legend_update_active:
            self.legend_update_pending = True
            return

        self.update_timer.start(120)

    def on_editing_started(self):
        self.editing_active = True
        self.update_timer.stop()

    def on_editing_finished(self):
        self.editing_active = False

        if self.legend_update_pending:
            self.legend_update_pending = False
            self.requestLegendUpdate()
        else:
            self.requestResizeUpdate()

    def requestResizeUpdate(self):
        self.resize_timer.start(0)

    def connect_existing_layer_signals(self):
        layers = list(QgsProject.instance().mapLayers().values())
        self.connect_layer_signals(layers)

    def on_layers_added(self, layers):
        self.connect_layer_signals(layers)
        self.requestLegendUpdate()

    def connect_layer_signals(self, layers):
        for layer in layers:
            if layer is None:
                continue

            layer_id = layer.id()

            if layer_id in self.connected_layer_ids:
                continue

            self.connected_layer_ids.add(layer_id)

            for signal_name in [
                "styleChanged",
                "rendererChanged",
                "legendChanged",
                "repaintRequested",
                "opacityChanged"
            ]:
                try:
                    signal = getattr(layer, signal_name)
                    signal.connect(self.requestLegendUpdate)
                except Exception as exception:
                    self.log_ignored_exception(
                        "Could not connect layer signal to legend update",
                        exception
                    )

    def clear_layout(self):
        while self.content_layout.count():
            item = self.content_layout.takeAt(0)

            widget = item.widget()
            if widget:
                widget.setParent(None)
                widget.deleteLater()

            child_layout = item.layout()
            if child_layout:
                self.clear_child_layout(child_layout)

    def clear_child_layout(self, layout):
        while layout.count():
            item = layout.takeAt(0)

            widget = item.widget()
            if widget:
                widget.setParent(None)
                widget.deleteLater()

            child_layout = item.layout()
            if child_layout:
                self.clear_child_layout(child_layout)

    def create_symbol_label(self, symbol):
        symbol_label = QLabel()
        symbol_label.setFixedSize(20, 20)
        symbol_label.setAlignment(Qt.AlignmentFlag.AlignCenter)

        preview_size = QSize(18, 18)

        pixmap = QPixmap(preview_size)
        pixmap.fill(Qt.GlobalColor.transparent)

        if symbol is not None:
            cloned_symbol = None

            try:
                # Important:
                # Use a clone for the preview. This avoids holding or manipulating
                # renderer-owned symbol objects directly and allows QGIS to render
                # the complete symbol stack, including hatch fills, transparent fills,
                # outlines, multi-layer symbols, line styles, etc.
                cloned_symbol = symbol.clone()

                painter = QPainter(pixmap)
                painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)

                cloned_symbol.drawPreviewIcon(
                    painter,
                    preview_size
                )

                painter.end()

            except Exception:
                # Safe fallback:
                # Do not call symbol.color() here, because that only returns the
                # generic symbol color and loses hatch/outline/transparent styling.
                # It also previously caused unstable behavior with some symbols.
                pixmap.fill(Qt.GlobalColor.transparent)

                painter = QPainter(pixmap)
                painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
                painter.setPen(QPen(QColor(80, 80, 80, 180), 1))
                painter.setBrush(Qt.BrushStyle.NoBrush)
                painter.drawRect(2, 2, 13, 13)
                painter.end()

            finally:
                cloned_symbol = None

        else:
            painter = QPainter(pixmap)
            painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
            painter.setPen(QPen(QColor(80, 80, 80, 180), 1))
            painter.setBrush(Qt.BrushStyle.NoBrush)
            painter.drawRect(2, 2, 13, 13)
            painter.end()

        symbol_label.setPixmap(pixmap)

        self.register_drag_filter(symbol_label)

        return symbol_label

    def current_extent_geometry_in_layer_crs(self, layer):
        try:
            canvas_extent = self.canvas.extent()
            extent_geometry = QgsGeometry.fromRect(canvas_extent)

            source_crs = self.canvas.mapSettings().destinationCrs()
            target_crs = layer.crs()

            if (
                source_crs.isValid() and
                target_crs.isValid() and
                source_crs != target_crs
            ):
                transform = QgsCoordinateTransform(
                    source_crs,
                    target_crs,
                    QgsProject.instance()
                )
                extent_geometry.transform(transform)

            return extent_geometry

        except Exception as exception:
            self.log_ignored_exception(
                "Could not create current extent geometry in layer CRS",
                exception
            )

        try:
            return QgsGeometry.fromRect(
                self.get_layer_extent_in_layer_crs(layer)
            )
        except Exception as exception:
            self.log_ignored_exception(
                "Could not create fallback current extent geometry in layer CRS",
                exception
            )

        return None

    def get_layer_extent_in_layer_crs(self, layer):
        canvas_extent = self.canvas.extent()

        try:
            return self.canvas.mapSettings().mapToLayerCoordinates(
                layer,
                canvas_extent
            )
        except Exception:
            return canvas_extent

    def get_attribute_or_expression_value(self, layer, feature, attribute_or_expression):
        field_index = layer.fields().indexFromName(attribute_or_expression)

        if field_index >= 0:
            return feature[field_index]

        expression = QgsExpression(attribute_or_expression)

        context = QgsExpressionContext()
        context.appendScopes(
            QgsExpressionContextUtils.globalProjectLayerScopes(layer)
        )
        context.setFeature(feature)

        return expression.evaluate(context)

    def get_features_in_current_extent(self, layer):
        layer_extent = self.get_layer_extent_in_layer_crs(layer)

        request = QgsFeatureRequest()
        request.setFilterRect(layer_extent)

        return layer.getFeatures(request)

    def format_area_value(self, area_square_meters):
        try:
            area_square_meters = float(area_square_meters)
        except (TypeError, ValueError) as exception:
            self.log_ignored_exception(
                "Could not convert area value to float",
                exception
            )
            return None

        if area_square_meters <= 0:
            return None

        if area_square_meters < 0.01:
            value = area_square_meters * 10000.0
            unit = "cm²"
        elif area_square_meters < 1.0:
            value = area_square_meters * 100.0
            unit = "dm²"
        elif area_square_meters < 10000.0:
            value = area_square_meters
            unit = "m²"
        elif area_square_meters < 1000000.0:
            value = area_square_meters / 10000.0
            unit = "ha"
        else:
            value = area_square_meters / 1000000.0
            unit = "km²"

        if value < 10:
            formatted_value = f"{value:.2f}"
        elif value < 100:
            formatted_value = f"{value:.1f}"
        else:
            formatted_value = f"{value:.0f}"

        return f"{formatted_value} {unit}"

    def format_length_value(self, length_meters):
        try:
            length_meters = float(length_meters)
        except (TypeError, ValueError) as exception:
            self.log_ignored_exception(
                "Could not convert length value to float",
                exception
            )
            return None

        if length_meters <= 0:
            return None

        if length_meters < 0.1:
            value = length_meters * 100.0
            unit = "cm"
        elif length_meters < 1.0:
            value = length_meters * 10.0
            unit = "dm"
        elif length_meters < 1000.0:
            value = length_meters
            unit = "m"
        else:
            value = length_meters / 1000.0
            unit = "km"

        if value < 10:
            formatted_value = f"{value:.2f}"
        elif value < 100:
            formatted_value = f"{value:.1f}"
        else:
            formatted_value = f"{value:.0f}"

        return f"{formatted_value} {unit}"

    def format_dimension_value(self, dimension_value, dimension_type):
        if dimension_type == "area":
            return self.format_area_value(dimension_value)

        if dimension_type == "length":
            return self.format_length_value(dimension_value)

        return 

    def legend_detail_text(
        self,
        feature_count=None,
        dimension_value=None,
        dimension_type=None
    ):
        detail_parts = []

        if self.show_feature_counts and feature_count is not None:
            detail_parts.append(f"[{feature_count}]")

        if self.show_feature_area:
            formatted_dimension = self.format_dimension_value(
                dimension_value,
                dimension_type
            )

            if formatted_dimension:
                detail_parts.append(formatted_dimension)

        if not detail_parts:
            return None

        return " · ".join(detail_parts)

    def distance_area_for_layer(self, layer):
        distance_area = QgsDistanceArea()

        try:
            distance_area.setSourceCrs(
                layer.crs(),
                QgsProject.instance().transformContext()
            )
        except Exception as exception:
            self.log_ignored_exception(
                "Could not set source CRS for area measurement",
                exception
            )

        try:
            ellipsoid = QgsProject.instance().ellipsoid()

            if ellipsoid:
                distance_area.setEllipsoid(ellipsoid)

        except Exception as exception:
            self.log_ignored_exception(
                "Could not set ellipsoid for area measurement",
                exception
            )

        return distance_area

    def dimension_type_for_layer(self, layer):
        try:
            geometry_type = QgsWkbTypes.geometryType(layer.wkbType())

            if geometry_type == QgsWkbTypes.GeometryType.PolygonGeometry:
                return "area"

            if geometry_type == QgsWkbTypes.GeometryType.LineGeometry:
                return "length"

        except Exception as exception:
            self.log_ignored_exception(
                "Could not determine layer dimension type",
                exception
            )

        return None

    def visible_feature_dimension_value(
        self,
        feature,
        extent_geometry,
        distance_area,
        dimension_type
    ):
        try:
            geometry = feature.geometry()

            if geometry is None:
                return 0.0

            if geometry.isEmpty():
                return 0.0

            if extent_geometry is None:
                return 0.0

            if dimension_type not in ["area", "length"]:
                return 0.0

            working_geometry = QgsGeometry(geometry)

            try:
                if not working_geometry.isGeosValid():
                    working_geometry = working_geometry.makeValid()
            except Exception as exception:
                self.log_ignored_exception(
                    "Could not validate geometry before visible dimension calculation",
                    exception
                )

            clipped_geometry = working_geometry.intersection(extent_geometry)

            if clipped_geometry is None:
                return 0.0

            if clipped_geometry.isEmpty():
                return 0.0

            try:
                if not clipped_geometry.isGeosValid():
                    clipped_geometry = clipped_geometry.makeValid()
            except Exception as exception:
                self.log_ignored_exception(
                    "Could not validate clipped geometry before dimension measurement",
                    exception
                )

            if dimension_type == "area":
                return float(distance_area.measureArea(clipped_geometry))

            if dimension_type == "length":
                return float(distance_area.measureLength(clipped_geometry))

        except Exception as exception:
            self.log_ignored_exception(
                "Could not calculate visible feature dimension",
                exception
            )

        return 0.0

    def collect_visible_layer_dimension(self, layer):
        total_dimension = 0.0
        dimension_type = self.dimension_type_for_layer(layer)

        if dimension_type is None:
            return total_dimension, None

        try:
            extent_geometry = self.current_extent_geometry_in_layer_crs(layer)

            if extent_geometry is None:
                return total_dimension, dimension_type

            request = QgsFeatureRequest()
            request.setFilterRect(extent_geometry.boundingBox())

            distance_area = self.distance_area_for_layer(layer)

            for feature in layer.getFeatures(request):
                total_dimension += self.visible_feature_dimension_value(
                    feature,
                    extent_geometry,
                    distance_area,
                    dimension_type
                )

        except Exception as exception:
            self.log_ignored_exception(
                "Could not collect visible layer dimension",
                exception
            )

        return total_dimension, dimension_type

    def collect_visible_categorized_dimensions(self, layer, renderer):
        class_attribute = renderer.classAttribute()
        visible_value_dimensions = Counter()
        dimension_type = self.dimension_type_for_layer(layer)

        if dimension_type is None:
            return visible_value_dimensions, None

        try:
            extent_geometry = self.current_extent_geometry_in_layer_crs(layer)

            if extent_geometry is None:
                return visible_value_dimensions, dimension_type

            request = QgsFeatureRequest()
            request.setFilterRect(extent_geometry.boundingBox())

            distance_area = self.distance_area_for_layer(layer)

            for feature in layer.getFeatures(request):
                value = self.get_attribute_or_expression_value(
                    layer,
                    feature,
                    class_attribute
                )

                dimension_value = self.visible_feature_dimension_value(
                    feature,
                    extent_geometry,
                    distance_area,
                    dimension_type
                )

                visible_value_dimensions[str(value)] += dimension_value

        except Exception as exception:
            self.log_ignored_exception(
                "Could not collect visible categorized feature dimensions",
                exception
            )

        return visible_value_dimensions, dimension_type

    def collect_visible_graduated_dimensions(self, layer, renderer):
        class_attribute = renderer.classAttribute()
        visible_range_dimensions = Counter()
        dimension_type = self.dimension_type_for_layer(layer)

        if dimension_type is None:
            return visible_range_dimensions, None

        try:
            extent_geometry = self.current_extent_geometry_in_layer_crs(layer)

            if extent_geometry is None:
                return visible_range_dimensions, dimension_type

            request = QgsFeatureRequest()
            request.setFilterRect(extent_geometry.boundingBox())

            distance_area = self.distance_area_for_layer(layer)
            ranges = renderer.ranges()

            for feature in layer.getFeatures(request):
                value = self.get_attribute_or_expression_value(
                    layer,
                    feature,
                    class_attribute
                )

                numeric_value = self.float_or_none(value)

                if numeric_value is None:
                    continue

                dimension_value = self.visible_feature_dimension_value(
                    feature,
                    extent_geometry,
                    distance_area,
                    dimension_type
                )

                for range_index, range_item in enumerate(ranges):
                    if not self.legend_item_is_enabled(renderer, range_item):
                        continue

                    lower_value = range_item.lowerValue()
                    upper_value = range_item.upperValue()

                    if lower_value <= numeric_value <= upper_value:
                        visible_range_dimensions[range_index] += dimension_value
                        break

        except Exception as exception:
            self.log_ignored_exception(
                "Could not collect visible graduated feature dimensions",
                exception
            )

        return visible_range_dimensions, dimension_type

    def layer_has_features_in_current_extent(self, layer):
        try:
            for _ in self.get_features_in_current_extent(layer):
                return True
        except Exception:
            return False

        return False

    def legend_item_is_enabled(self, renderer, legend_item):
        try:
            if hasattr(legend_item, "renderState"):
                if not bool(legend_item.renderState()):
                    return False
        except Exception as exception:
            self.log_ignored_exception(
                "Could not read legend item render state",
                exception
            )

        possible_keys = []

        for method_name in ["uuid", "ruleKey"]:
            try:
                method = getattr(legend_item, method_name)
                possible_key = method()

                if possible_key:
                    possible_keys.append(possible_key)
            except Exception as exception:
                self.log_ignored_exception(
                    "Could not read legend item key",
                    exception
                )

        for key in possible_keys:
            try:
                if not bool(renderer.legendSymbolItemChecked(key)):
                    return False
            except Exception as exception:
                self.log_ignored_exception(
                    "Could not read legend symbol item checked state",
                    exception
                )

        return True

    def add_legend_title(self):
        title_widget = EditableLegendLabel(
            self.legend_title_text,
            on_text_changed=self.set_legend_title_text,
            on_editing_started=self.on_editing_started,
            on_editing_finished=self.on_editing_finished
        )

        title_widget.label.setObjectName("legendTitle")
        title_widget.label.setToolTip("Double click to edit legend title")

        self.register_drag_filter(title_widget)
        self.register_drag_filter(title_widget.label)
        self.register_drag_filter(title_widget.edit)

        self.content_layout.addWidget(title_widget)

    def add_layer_title(self, layer):
        display_name = self.get_display_label(
            layer,
            "layer",
            "layer_name",
            layer.name()
        )

        title_widget = EditableLegendLabel(
            display_name,
            on_text_changed=lambda new_text, lyr=layer: self.update_layer_title(
                lyr,
                new_text
            ),
            on_editing_started=self.on_editing_started,
            on_editing_finished=self.on_editing_finished
        )

        title_widget.label.setObjectName("layerTitle")
        title_widget.edit.setObjectName("layerTitleEditor")
        title_widget.label.setToolTip(
            "Double click to edit layer name"
        )

        layer_context = {
            "layer_id": layer.id(),
            "target_type": "layer",
            "display_label": display_name
        }

        self.set_widget_legend_context(
            title_widget,
            layer_context
        )

        self.register_drag_filter(title_widget)
        self.register_drag_filter(title_widget.label)
        self.register_drag_filter(title_widget.edit)

        self.content_layout.addWidget(title_widget)

    def add_legend_row(
        self,
        symbol,
        label_text,
        count_text=None,
        on_label_changed=None,
        legend_context=None
    ):
        row = QHBoxLayout()
        row.setContentsMargins(0, 0, 0, 0)
        row.setSpacing(4)

        symbol_label = self.create_symbol_label(symbol)

        if legend_context:
            self.set_widget_legend_context(symbol_label, legend_context)

        if on_label_changed:
            text_widget = EditableLegendLabel(
                label_text,
                on_text_changed=on_label_changed,
                on_editing_started=self.on_editing_started,
                on_editing_finished=self.on_editing_finished
            )

            self.register_drag_filter(text_widget)
            self.register_drag_filter(text_widget.label)

        else:
            text_widget = QLabel(label_text)
            text_widget.setObjectName("legendItemText")
            text_widget.setSizePolicy(
                QSizePolicy.Policy.Preferred,
                QSizePolicy.Policy.Fixed
            )

            self.register_drag_filter(text_widget)

        if legend_context:
            self.set_widget_legend_context(text_widget, legend_context)

        row.addWidget(symbol_label)
        row.addWidget(text_widget)

        if count_text is not None:
            count_label = QLabel(count_text)
            count_label.setObjectName("legendCountText")
            count_label.setAlignment(
                Qt.AlignmentFlag.AlignRight |
                Qt.AlignmentFlag.AlignVCenter
            )
            count_label.setMinimumWidth(28)
            count_label.setSizePolicy(
                QSizePolicy.Policy.Fixed,
                QSizePolicy.Policy.Fixed
            )

            self.register_drag_filter(count_label)

            row.addWidget(count_label)

        row.addStretch()

        self.content_layout.addLayout(row)

    def update_layer_title(self, layer, new_label):
        self.set_local_label_override(
            layer,
            "layer",
            "layer_name",
            new_label
        )

        if not self.sync_label_changes_with_project:
            self.requestResizeUpdate()
            return

        try:
            layer.setName(new_label)
        except Exception as exception:
            self.log_ignored_exception(
                "Could not update layer name",
                exception
            )
            return

        try:
            self.iface.layerTreeView().refreshLayerSymbology(layer.id())
        except Exception as exception:
            self.log_ignored_exception(
                "Could not refresh layer tree after layer name update",
                exception
            )

        try:
            QgsProject.instance().setDirty(True)
        except Exception as exception:
            self.log_ignored_exception(
                "Could not mark project dirty after layer name update",
                exception
            )

        self.requestLegendUpdate()

    def update_category_label(self, layer, category_index, category_identifier, new_label):
        self.set_local_label_override(
            layer,
            "category",
            category_identifier,
            new_label
        )

        if not self.sync_label_changes_with_project:
            self.requestResizeUpdate()
            return

        renderer = layer.renderer()

        if not isinstance(renderer, QgsCategorizedSymbolRenderer):
            return

        updated = False

        try:
            renderer.updateCategoryLabel(category_index, new_label)
            updated = True
        except Exception as exception:
            self.log_ignored_exception(
                "Could not update categorized legend label directly",
                exception
            )

        if not updated:
            try:
                categories = renderer.categories()
                category = categories[category_index]
                category.setLabel(new_label)
                renderer.updateCategoryLabel(category_index, new_label)
                updated = True
            except Exception as exception:
                self.log_ignored_exception(
                    "Could not update categorized legend label through category object",
                    exception
                )

        try:
            layer.triggerRepaint()
        except Exception as exception:
            self.log_ignored_exception(
                "Could not trigger layer repaint after category label update",
                exception
            )

        try:
            self.iface.layerTreeView().refreshLayerSymbology(layer.id())
        except Exception as exception:
            self.log_ignored_exception(
                "Could not refresh layer symbology after category label update",
                exception
            )

        try:
            QgsProject.instance().setDirty(True)
        except Exception as exception:
            self.log_ignored_exception(
                "Could not mark project dirty after category label update",
                exception
            )

        self.requestLegendUpdate()

    def update_range_label(self, layer, range_index, range_identifier, new_label):
        self.set_local_label_override(
            layer,
            "range",
            range_identifier,
            new_label
        )

        if not self.sync_label_changes_with_project:
            self.requestResizeUpdate()
            return

        renderer = layer.renderer()

        if not isinstance(renderer, QgsGraduatedSymbolRenderer):
            return

        updated = False

        try:
            renderer.updateRangeLabel(range_index, new_label)
            updated = True
        except Exception as exception:
            self.log_ignored_exception(
                "Could not update graduated range label directly",
                exception
            )

        if not updated:
            try:
                ranges = renderer.ranges()
                range_item = ranges[range_index]
                range_item.setLabel(new_label)
                renderer.updateRangeLabel(range_index, new_label)
                updated = True
            except Exception as exception:
                self.log_ignored_exception(
                    "Could not update graduated range label through range object",
                    exception
                )

        try:
            layer.triggerRepaint()
        except Exception as exception:
            self.log_ignored_exception(
                "Could not trigger layer repaint after range label update",
                exception
            )

        try:
            self.iface.layerTreeView().refreshLayerSymbology(layer.id())
        except Exception as exception:
            self.log_ignored_exception(
                "Could not refresh layer symbology after range label update",
                exception
            )

        try:
            QgsProject.instance().setDirty(True)
        except Exception as exception:
            self.log_ignored_exception(
                "Could not mark project dirty after range label update",
                exception
            )

        self.requestLegendUpdate()

    def update_single_layer_label(self, layer, new_label):
        self.set_local_label_override(
            layer,
            "single",
            "layer_name",
            new_label
        )

        if not self.sync_label_changes_with_project:
            self.requestResizeUpdate()
            return

        try:
            layer.setName(new_label)
        except Exception:
            return

        try:
            self.iface.layerTreeView().refreshLayerSymbology(layer.id())
        except Exception as exception:
            self.log_ignored_exception(
                "Could not refresh layer symbology after single layer label update",
                exception
            )

        try:
            QgsProject.instance().setDirty(True)
        except Exception as exception:
            self.log_ignored_exception(
                "Could not mark project dirty after single layer label update",
                exception
            )

        self.requestLegendUpdate()

    def collect_visible_categorized_values(self, layer, renderer):
        class_attribute = renderer.classAttribute()
        visible_value_counts = Counter()

        try:
            for feature in self.get_features_in_current_extent(layer):
                value = self.get_attribute_or_expression_value(
                    layer,
                    feature,
                    class_attribute
                )

                visible_value_counts[str(value)] += 1
        except Exception:
            return Counter()

        return visible_value_counts

    def collect_visible_graduated_ranges(self, layer, renderer):
        class_attribute = renderer.classAttribute()
        visible_range_counts = Counter()

        try:
            ranges = renderer.ranges()

            for feature in self.get_features_in_current_extent(layer):
                value = self.get_attribute_or_expression_value(
                    layer,
                    feature,
                    class_attribute
                )

                numeric_value = self.float_or_none(value)
                
                if numeric_value is None:
                    continue

                for range_index, range_item in enumerate(ranges):
                    if not self.legend_item_is_enabled(renderer, range_item):
                        continue

                    lower_value = range_item.lowerValue()
                    upper_value = range_item.upperValue()

                    if lower_value <= numeric_value <= upper_value:
                        visible_range_counts[range_index] += 1
                        break

        except Exception:
            return Counter()

        return visible_range_counts

    def hide_legend_panel_due_to_no_items(self):
        self.legend_hidden_due_to_no_items = True

        try:
            self.panel.hide()
        except Exception as exception:
            self.log_ignored_exception(
                "Could not hide empty legend panel",
                exception
            )

        try:
            self.setMinimumSize(0, 0)
            self.setMaximumSize(0, 0)
            self.resize(0, 0)
        except Exception as exception:
            self.log_ignored_exception(
                "Could not collapse empty legend widget",
                exception
            )

    def show_legend_panel_with_items(self):
        self.legend_hidden_due_to_no_items = False

        try:
            self.panel.show()
        except Exception as exception:
            self.log_ignored_exception(
                "Could not show legend panel with visible items",
                exception
            )

    def updateLegend(self):
        if self.editing_active:
            self.legend_update_pending = True
            return

        if self.legend_update_active:
            self.legend_update_pending = True
            return

        self.legend_update_active = True
        old_pos = self.pos()
        self.clear_layout()

        if self.show_legend_title:
            self.add_legend_title()

        self.connect_existing_layer_signals()

        root = QgsProject.instance().layerTreeRoot()

        visible_layers = []

        for node in root.findLayers():
            if not node.isVisible():
                continue

            layer = node.layer()

            if layer is None:
                continue

            if layer.type() != QgsMapLayerType.VectorLayer:
                continue

            if self.is_layer_hidden_in_living_legend(layer):
                continue

            try:
                if not layer.isInScaleRange(self.canvas.scale()):
                    continue
            except Exception as exception:
                self.log_ignored_exception(
                    "Could not evaluate visible graduated symbol range",
                    exception
                )

            if self.layer_has_features_in_current_extent(layer):
                visible_layers.append(layer)

        total_legend_items = 0

        for layer in visible_layers:

            renderer = layer.renderer()

            if renderer is None:
                continue

            layer_items_added = 0

            if isinstance(renderer, QgsCategorizedSymbolRenderer):

                visible_value_counts = self.collect_visible_categorized_values(
                    layer,
                    renderer
                )

                visible_value_dimensions, dimension_type = self.collect_visible_categorized_dimensions(
                    layer,
                    renderer
                )

                categories = renderer.categories()

                for category_index, category in enumerate(categories):

                    if not self.legend_item_is_enabled(renderer, category):
                        continue

                    category_value = str(category.value())

                    if category_value not in visible_value_counts:
                        continue

                    count = visible_value_counts[category_value]

                    label_text = category.label()

                    if label_text is None or label_text == "":
                        label_text = category_value

                    label_text = self.get_display_label(
                        layer,
                        "category",
                        category_value,
                        label_text
                    )

                    legend_context = {
                        "layer_id": layer.id(),
                        "target_type": "category",
                        "category_value": category_value,
                        "display_label": label_text
                    }

                    if self.is_legend_context_hidden_in_living_legend(legend_context):
                        continue

                    if layer_items_added == 0:
                        if total_legend_items > 0:
                            spacer = QWidget()
                            spacer.setFixedHeight(3)
                            self.content_layout.addWidget(spacer)

                        if self.show_layer_name:
                            self.add_layer_title(layer)

                    detail_text = self.legend_detail_text(
                        count,
                        visible_value_dimensions[category_value],
                        dimension_type
                    )

                    self.add_legend_row(
                        category.symbol(),
                        label_text,
                        detail_text,
                        lambda new_text, lyr=layer, idx=category_index, ident=category_value: self.update_category_label(
                            lyr,
                            idx,
                            ident,
                            new_text
                        ),
                        legend_context
                    )

                    layer_items_added += 1
                    total_legend_items += 1

            elif isinstance(renderer, QgsGraduatedSymbolRenderer):

                visible_range_counts = self.collect_visible_graduated_ranges(
                    layer,
                    renderer
                )

                visible_range_dimensions, dimension_type = self.collect_visible_graduated_dimensions(
                    layer,
                    renderer
                )

                ranges = renderer.ranges()

                for range_index, range_item in enumerate(ranges):

                    if not self.legend_item_is_enabled(renderer, range_item):
                        continue

                    if range_index not in visible_range_counts:
                        continue

                    count = visible_range_counts[range_index]

                    label_text = range_item.label()

                    if label_text is None or label_text == "":
                        label_text = f"{range_item.lowerValue()} - {range_item.upperValue()}"

                    range_identifier = f"{range_item.lowerValue()}::{range_item.upperValue()}::{range_index}"

                    label_text = self.get_display_label(
                        layer,
                        "range",
                        range_identifier,
                        label_text
                    )

                    legend_context = {
                        "layer_id": layer.id(),
                        "target_type": "range",
                        "range_index": range_index,
                        "display_label": label_text
                    }

                    if self.is_legend_context_hidden_in_living_legend(legend_context):
                        continue

                    if layer_items_added == 0:
                        if total_legend_items > 0:
                            spacer = QWidget()
                            spacer.setFixedHeight(3)
                            self.content_layout.addWidget(spacer)

                        if self.show_layer_name:
                            self.add_layer_title(layer)

                    detail_text = self.legend_detail_text(
                        count,
                        visible_range_dimensions[range_index],
                        dimension_type
                    )

                    self.add_legend_row(
                        range_item.symbol(),
                        label_text,
                        detail_text,
                        lambda new_text, lyr=layer, idx=range_index, ident=range_identifier: self.update_range_label(
                            lyr,
                            idx,
                            ident,
                            new_text
                        ),
                        legend_context
                    )

                    layer_items_added += 1
                    total_legend_items += 1

            else:
                feature_count = 0
                try:
                    for _ in self.get_features_in_current_extent(layer):
                        feature_count += 1
                except Exception as exception:
                    self.log_ignored_exception(
                        "Could not count visible single-symbol features",
                        exception
                    )
                    feature_count = 0

                visible_layer_dimension, dimension_type = self.collect_visible_layer_dimension(layer)

                if feature_count > 0:
                    try:
                        symbol = renderer.symbol()
                    except Exception as exception:
                        self.log_ignored_exception(
                            "Could not read renderer symbol for single legend item",
                            exception
                        )
                        symbol = None

                    label_text = self.get_display_label(
                        layer,
                        "single",
                        "layer_name",
                        layer.name()
                    )

                    legend_context = {
                        "layer_id": layer.id(),
                        "target_type": "single",
                        "display_label": label_text
                    }

                    if self.is_legend_context_hidden_in_living_legend(legend_context):
                        continue

                    if layer_items_added == 0:
                        if total_legend_items > 0:
                            spacer = QWidget()
                            spacer.setFixedHeight(3)
                            self.content_layout.addWidget(spacer)

                    detail_text = self.legend_detail_text(
                        feature_count,
                        visible_layer_dimension,
                        dimension_type
                    )

                    self.add_legend_row(
                        symbol,
                        label_text,
                        detail_text,
                        lambda new_text, lyr=layer: self.update_single_layer_label(
                            lyr,
                            new_text
                        ),
                        legend_context
                    )

                    layer_items_added += 1
                    total_legend_items += 1

        if total_legend_items == 0:
            self.clear_layout()
            self.hide_legend_panel_due_to_no_items()

            self.legend_update_active = False

            if self.legend_update_pending:
                self.legend_update_pending = False
                self.requestLegendUpdate()

            return

        self.show_legend_panel_with_items()

        if self.position_has_been_initialized:
            self.move(old_pos)

        self.legend_update_active = False
        self.requestResizeUpdate()

        if self.legend_update_pending:
            self.legend_update_pending = False
            self.requestLegendUpdate()

    def showEvent(self, event):
        super().showEvent(event)

        # Run immediately and then again shortly after.
        # The second pass catches QGIS startup cases where the canvas becomes
        # valid slightly after the widget is shown.
        self.adjust_size_to_content()
        self.update_decoration_widgets()
        self.resize_timer.start(150)

    def hideEvent(self, event):
        self.save_position()
        self.hide_decoration_widgets()
        super().hideEvent(event)

    def closeEvent(self, event):
        self.save_position()
        self.hide_decoration_widgets()
        super().closeEvent(event)

    def adjust_size_to_content(self):
        if self.legend_hidden_due_to_no_items:
            try:
                self.setMinimumSize(0, 0)
                self.setMaximumSize(0, 0)
                self.resize(0, 0)
            except Exception as exception:
                self.log_ignored_exception(
                    "Could not keep empty legend widget collapsed",
                    exception
                )

            return

        self.content_layout.activate()
        self.scroll_content.layout().activate()
        self.scroll_area.updateGeometry()
        self.panel.layout().activate()
        self.main_layout.activate()

        content_hint = self.scroll_content.sizeHint()

        content_width = max(120, content_hint.width())
        content_height = max(24, content_hint.height())

        max_height = self.maximum_legend_height_px()
        needs_vertical_scrollbar = content_height > max_height

        scroll_height = min(content_height, max_height)
        scroll_width = content_width

        if needs_vertical_scrollbar:
            scroll_width += self.legend_scrollbar_width_px() + 2

        border_padding = 2

        final_width = scroll_width + border_padding
        final_height = scroll_height + border_padding

        try:
            self.scroll_area.setMinimumSize(scroll_width, scroll_height)
            self.scroll_area.setMaximumSize(scroll_width, scroll_height)
            self.scroll_area.resize(scroll_width, scroll_height)
        except Exception as exception:
            self.log_ignored_exception(
                "Could not resize legend scroll area",
                exception
            )

        self.setMinimumSize(final_width, final_height)
        self.setMaximumSize(final_width, final_height)
        self.resize(final_width, final_height)

        if self.stick_to_corner:
            self.apply_sticky_corner_position()
        elif not self.position_has_been_initialized:
            self.apply_saved_or_default_position()
        else:
            self.keep_inside_map()

        self.apply_style()
        self.raise_()

    def keep_inside_map(self):
        margin = self.legend_edge_margin_px()

        parent_width = self.parent_widget.width()
        parent_height = self.parent_widget.height()

        # Important startup guard:
        # If the canvas is not ready, do not clamp the legend position.
        # Clamping against a 0px or tiny canvas is what causes the top-left bug.
        if parent_width < 100 or parent_height < 100:
            return

        x = self.x()
        y = self.y()

        if x + self.width() > parent_width - margin:
            x = max(margin, parent_width - self.width() - margin)

        if y + self.height() > parent_height - margin:
            y = max(margin, parent_height - self.height() - margin)

        if x < margin:
            x = margin

        if y < margin:
            y = margin

        self.move(x, y)