You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
183 lines
6.3 KiB
183 lines
6.3 KiB
|
|
#!/usr/bin/env python3 |
|
|
|
# MIT License |
|
# |
|
# Copyright (c) 2026 Eugenio Parodi <ceccopierangiolieugenio AT googlemail DOT com> |
|
# |
|
# Permission is hereby granted, free of charge, to any person obtaining a copy |
|
# of this software and associated documentation files (the "Software"), to deal |
|
# in the Software without restriction, including without limitation the rights |
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|
# copies of the Software, and to permit persons to whom the Software is |
|
# furnished to do so, subject to the following conditions: |
|
# |
|
# The above copyright notice and this permission notice shall be included in all |
|
# copies or substantial portions of the Software. |
|
# |
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|
# SOFTWARE. |
|
|
|
# Requires: PySide6 (or adapt imports to PyQt5) |
|
import sys |
|
from PySide6.QtCore import Qt, QModelIndex, QPoint |
|
from PySide6.QtGui import QStandardItemModel, QStandardItem |
|
from PySide6.QtWidgets import ( |
|
QApplication, QTreeView, QWidget, QToolButton, |
|
QHBoxLayout, QStyle, QVBoxLayout |
|
) |
|
|
|
class HoverActionBar(QWidget): |
|
"""A small right-aligned bar with action buttons that floats over the view.""" |
|
def __init__(self, parent=None): |
|
super().__init__(parent) |
|
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, False) |
|
self.setWindowFlags(Qt.WindowType.FramelessWindowHint) |
|
self.setAutoFillBackground(True) |
|
self.setObjectName("HoverActionBar") |
|
|
|
# Layout & buttons |
|
layout = QHBoxLayout(self) |
|
layout.setContentsMargins(6, 2, 6, 2) |
|
layout.setSpacing(6) |
|
|
|
self.runBtn = QToolButton(self) |
|
self.runBtn.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_MediaPlay)) |
|
self.runBtn.setToolTip("Run test") |
|
self.runBtn.setCursor(Qt.CursorShape.PointingHandCursor) |
|
|
|
self.debugBtn = QToolButton(self) |
|
self.debugBtn.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_CommandLink)) |
|
self.debugBtn.setToolTip("Debug test") |
|
self.debugBtn.setCursor(Qt.CursorShape.PointingHandCursor) |
|
|
|
layout.addWidget(self.runBtn) |
|
layout.addWidget(self.debugBtn) |
|
|
|
# Optional: light styling |
|
self.setStyleSheet(""" |
|
#HoverActionBar { |
|
background: rgba(240, 240, 240, 0.95); |
|
border: 1px solid rgba(0,0,0,0.08); |
|
border-radius: 6px; |
|
} |
|
QToolButton { border: none; padding: 2px; } |
|
QToolButton::hover { background: rgba(0,0,0,0.06); border-radius: 4px; } |
|
""") |
|
|
|
self._index = QModelIndex() |
|
|
|
def setIndex(self, index: QModelIndex): |
|
self._index = index |
|
|
|
def index(self): |
|
return self._index |
|
|
|
|
|
class HoverTree(QTreeView): |
|
"""QTreeView that shows an action bar aligned to the right side of the hovered row.""" |
|
def __init__(self, parent=None): |
|
self._bar = None |
|
super().__init__(parent) |
|
self.setMouseTracking(True) |
|
self.setUniformRowHeights(True) |
|
self.setHeaderHidden(True) |
|
self._hovered = QModelIndex() |
|
|
|
self._bar = HoverActionBar(self.viewport()) |
|
self._bar.hide() |
|
|
|
# Wire buttons to actions |
|
self._bar.runBtn.clicked.connect(self._onRunClicked) |
|
self._bar.debugBtn.clicked.connect(self._onDebugClicked) |
|
|
|
def leaveEvent(self, event): |
|
super().leaveEvent(event) |
|
self._hovered = QModelIndex() |
|
self._bar.hide() |
|
|
|
def mouseMoveEvent(self, event): |
|
super().mouseMoveEvent(event) |
|
pos = event.position().toPoint() if hasattr(event, 'position') else event.pos() |
|
idx = self.indexAt(pos) |
|
if idx != self._hovered: |
|
self._hovered = idx |
|
self._updateBarGeometry() |
|
|
|
def viewportEvent(self, event): |
|
"""Keep bar positioned if the view scrolls or resizes.""" |
|
res = super().viewportEvent(event) |
|
# Reposition on paint/scroll/resize |
|
if self._bar and self._bar.isVisible() and self._hovered.isValid(): |
|
self._updateBarGeometry() |
|
return res |
|
|
|
def _updateBarGeometry(self): |
|
if not self._hovered.isValid(): |
|
self._bar.hide() |
|
return |
|
|
|
rect = self.visualRect(self._hovered) |
|
if not rect.isValid() or not self.viewport().rect().intersects(rect): |
|
self._bar.hide() |
|
return |
|
|
|
# Size the bar to content |
|
self._bar.adjustSize() |
|
bar_w = self._bar.width() |
|
bar_h = self._bar.height() |
|
|
|
# Right-align inside the row rect, with a small right margin |
|
right_margin = 6 |
|
x = rect.right() - bar_w - right_margin |
|
y = rect.top() + (rect.height() - bar_h) // 2 |
|
|
|
# Ensure it doesn’t overlap the text too aggressively (optional) |
|
min_left = rect.left() + 120 # tweak depending on your content/indentation |
|
x = max(x, min_left) |
|
|
|
self._bar.setIndex(self._hovered) |
|
self._bar.move(QPoint(x, y)) |
|
self._bar.show() |
|
|
|
# Example slots that use the current index |
|
def _onRunClicked(self): |
|
idx = self._bar.index() |
|
if idx.isValid(): |
|
print(f"Run: {idx.data()}") |
|
|
|
def _onDebugClicked(self): |
|
idx = self._bar.index() |
|
if idx.isValid(): |
|
print(f"Debug: {idx.data()}") |
|
|
|
|
|
def build_model(): |
|
model = QStandardItemModel() |
|
root = model.invisibleRootItem() |
|
|
|
for suite in range(3): |
|
suite_item = QStandardItem(f"Suite {suite+1}") |
|
suite_item.setEditable(False) |
|
for case in range(4): |
|
case_item = QStandardItem(f"Test {suite+1}.{case+1}") |
|
case_item.setEditable(False) |
|
suite_item.appendRow(case_item) |
|
root.appendRow(suite_item) |
|
|
|
return model |
|
|
|
|
|
if __name__ == "__main__": |
|
app = QApplication(sys.argv) |
|
view = HoverTree() |
|
view.setModel(build_model()) |
|
view.expandAll() |
|
view.resize(420, 300) |
|
view.show() |
|
sys.exit(app.exec())
|
|
|