# Copyright (c) 2025-2026 macro-qgis-plugin contributors.
#
#
# This file is part of macro-qgis-plugin.
#
# macro-qgis-plugin is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# macro-qgis-plugin is distributed in the hope that it will be
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with macro-qgis-plugin. If not, see <https://www.gnu.org/licenses/>.
"""Utility functions for widget lookup, event position handling, and Qt compat."""
from collections.abc import Iterator
from typing import (
TYPE_CHECKING,
cast,
)
from qgis.PyQt.QtCore import QObject, QPoint
from qgis.PyQt.QtGui import QMouseEvent, QWheelEvent
from qgis.PyQt.QtWidgets import QAbstractButton, QWidget
from qgis.utils import iface as iface_
if TYPE_CHECKING:
from qgis.gui import QgisInterface
iface = cast("QgisInterface", iface_)
[docs]
def is_object_map_canvas(obj: QObject) -> bool:
"""Return True if *obj* is the map canvas viewport widget."""
return obj == iface.mapCanvas().viewport()
[docs]
def get_widget_text(widget: QWidget) -> str:
"""Return the display text of *widget*, falling back to its object name."""
text = ""
if isinstance(widget, QAbstractButton):
text = widget.text()
return text or widget.objectName()
[docs]
def get_sibling_index(widget: QWidget, parent: QWidget) -> int:
"""Get the index of a widget among its same-class siblings in the parent."""
widget_class = widget.__class__.__name__
same_class_siblings = [
child
for child in parent.findChildren(QWidget)
if child.__class__.__name__ == widget_class and child.parentWidget() is parent
]
for i, sibling in enumerate(same_class_siblings):
if sibling is widget:
return i
return 0
[docs]
def find_nearest_visible_children_of_type(
target_point: QPoint, parent_widget: QWidget, widget_class: str
) -> Iterator[QWidget]:
"""Yield visible children of *widget_class* sorted by distance to *target_point*."""
widgets = find_nearest_visible_children_with_threshold(target_point, parent_widget)
return (widget for widget in widgets if widget.__class__.__name__ == widget_class)
[docs]
def find_nearest_visible_children_with_threshold(
target_point: QPoint, parent_widget: QWidget
) -> Iterator[QWidget]:
"""Yield all visible descendants of *parent_widget* sorted by distance."""
nearest_visible_children = set()
def distance_to_widget(widget: QWidget) -> int:
widget_center = widget.geometry().center()
# Calculate the Euclidean distance (squared)
return (target_point.x() - widget_center.x()) ** 2 + (
target_point.y() - widget_center.y()
) ** 2
def find_recursive(widget: QWidget) -> None:
for child in widget.findChildren(QWidget):
if child.isVisible():
nearest_visible_children.add((child, (distance_to_widget(child))))
find_recursive(child)
# Start the recursive search from the parent widget
find_recursive(parent_widget)
# Sort the results by distance
return (child[0] for child in sorted(nearest_visible_children, key=lambda x: x[1]))
[docs]
def enum_value(enum_or_flag: object) -> int:
"""Convert a Qt enum/flag to int, compatible with both PyQt5 and PyQt6."""
if hasattr(enum_or_flag, "value"):
return enum_or_flag.value # type: ignore[union-attr]
return int(enum_or_flag) # type: ignore[arg-type,call-overload]
[docs]
def event_pos(event: QMouseEvent | QWheelEvent) -> QPoint:
"""Get local position from a mouse/wheel event (PyQt5/6 compatible)."""
if hasattr(event, "position"):
return event.position().toPoint()
return event.pos()
[docs]
def event_global_pos(event: QMouseEvent | QWheelEvent) -> QPoint:
"""Get global position from a mouse/wheel event (PyQt5/6 compatible)."""
if hasattr(event, "globalPosition"):
return event.globalPosition().toPoint()
return event.globalPos()