I have a script for a radiant menu that is intended to appear when a user presses the right-click mouse button. While holding the button down, the user can choose a "Tool." The radiant menu is designed to close when the user releases the mouse button.
However, I've encountered an issue with my script. When I initially press the right-click button for the first time, none of the events associated with the radiant button seem to run. In an attempt to troubleshoot, I decided to remove the closing function on button release. Surprisingly, I found that my events only fail to execute when I initially press the right-click button. Strangely, everything starts working correctly when I release the button. I suspect this behavior is related to how Qt handles events.
I've attempted to resolve this issue by implementing an eventFilter, but it doesn't seem to have helped. Could someone please explain how I can achieve the desired functionality? I want the radiant menu to appear while the right-click button is pressed, allowing the user to choose a "Tool," and then automatically close when the button is released.
Here is an example of the script which I wrote so far:
import math
import sys
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
class RadialMenu(QWidget):
WINDOW_WIDTH = 320
WINDOW_HEIGHT = 200
OUTER_CIRCLE_RADIUS = 50
OUTER_CIRCLE_DIAMETER = 2 * OUTER_CIRCLE_RADIUS
INNER_CIRCLE_RADIUS = 10
INNER_CIRCLE_DIAMETER = 2 * INNER_CIRCLE_RADIUS
TEXT_WIDTH = 80
TEXT_HEIGHT = 24
TEXT_RADIUS = 0.5 * TEXT_HEIGHT
NUM_ZONES = 8
ZONE_SPAN_ANGLE = 360 / NUM_ZONES
def __init__(self, hotkey, parent=None):
super(RadialMenu, self).__init__(parent)
self.setFixedSize(self.WINDOW_WIDTH, self.WINDOW_HEIGHT)
self.setWindowFlags(Qt.FramelessWindowHint)
self.setAttribute(Qt.WA_TranslucentBackground, True)
self.setMouseTracking(True)
self.hotkey = hotkey
self.tools = [None] * self.NUM_ZONES
self.center_point = QPoint(int(0.5 * self.width()), int(0.5 * self.height()))
self.outer_circle_rect = QRect(self.center_point.x() - self.OUTER_CIRCLE_RADIUS,
self.center_point.y() - self.OUTER_CIRCLE_RADIUS,
self.OUTER_CIRCLE_DIAMETER, self.OUTER_CIRCLE_DIAMETER)
self.inner_circle_rect = QRect(self.center_point.x() - self.INNER_CIRCLE_RADIUS,
self.center_point.y() - self.INNER_CIRCLE_RADIUS,
self.INNER_CIRCLE_DIAMETER, self.INNER_CIRCLE_DIAMETER)
self.selected_section = -1
self.active_zone = -1
self.highlight_bounds = QRect(int(self.center_point.x() - 0.5 * self.height()), 0, int(self.height()),
int(self.height()))
self.highlight_start_angle = 0
self.hotkey_down = False
self.mouse_moved = False
def set_tool(self, index, label, func):
if index < 0 or index >= self.NUM_ZONES:
return
self.tools[index] = (label, func)
def distance_from_center(self, x, y):
return math.hypot(x - self.center_point.x(), y - self.center_point.y())
def point_from_center(self, zone, distance):
degrees = zone * self.ZONE_SPAN_ANGLE
radians = math.radians(degrees)
x1 = self.center_point.x() + (distance * math.cos(radians))
y1 = self.center_point.y() + (distance * math.sin(radians))
return x1, y1
def label_point(self, zone):
distance = self.OUTER_CIRCLE_RADIUS + self.TEXT_RADIUS
x, y = self.point_from_center(zone, distance)
if zone in [0, 1, 7]:
x += 2
elif zone in [2, 6]:
x -= 0.5 * self.TEXT_WIDTH
elif zone in [3, 4, 5]:
x -= 2 + self.TEXT_WIDTH
if zone in [0, 4]:
y -= 0.5 * self.TEXT_HEIGHT
elif zone in [1, 3]:
y -= 0.3 * self.TEXT_HEIGHT
elif zone in [5, 7]:
y -= 0.7 * self.TEXT_HEIGHT
elif zone == 2:
y += 2
elif zone == 6:
y -= 2 + self.TEXT_HEIGHT
return x, y
def update_active_zone(self, pos):
if self.distance_from_center(pos.x(), pos.y()) <= self.INNER_CIRCLE_RADIUS:
self.clear_active_zone()
return
point = pos - self.center_point
degrees = math.degrees(math.atan2(point.y(), point.x())) % 360
start_angle_offset = -0.5 * self.ZONE_SPAN_ANGLE
degrees = (degrees + start_angle_offset) % 360
temp_section = math.floor(degrees / self.ZONE_SPAN_ANGLE)
if self.selected_section != temp_section:
self.selected_section = temp_section
self.highlight_start_angle = (self.selected_section * self.ZONE_SPAN_ANGLE) - start_angle_offset
self.active_zone = (self.selected_section + 1) % 8
self.update()
def clear_active_zone(self):
self.active_zone = -1
self.selected_section = -1
self.update()
def activate_selection(self):
if self.active_zone >= 0 and self.tools[self.active_zone]:
self.tools[self.active_zone][1]()
self.hide()
def show(self):
pop_up_pos = QCursor.pos() - self.center_point
self.move(pop_up_pos)
super(RadialMenu, self).show()
def showEvent(self, show_event):
self.setFocus(Qt.PopupFocusReason)
self.mouse_moved = False
self.hotkey_down = True
def keyPressEvent(self, key_event):
key = key_event.key()
if key == self.hotkey and not key_event.isAutoRepeat():
self.hide()
elif key == Qt.Key_Escape:
self.hide()
def keyReleaseEvent(self, key_event):
if key_event.key() == self.hotkey and not key_event.isAutoRepeat():
self.hotkey_down = False
if self.mouse_moved:
self.activate_selection()
def mousePressEvent(self, mouse_event):
if self.mouse_moved or not self.hotkey_down:
self.activate_selection()
def mouseReleaseEvent(self, mouse_event):
if self.mouse_moved:
self.activate_selection()
def mouseMoveEvent(self, mouse_event):
print('check')
self.update_active_zone(mouse_event.pos())
if (mouse_event.pos() - self.center_point).manhattanLength() > self.INNER_CIRCLE_RADIUS:
self.mouse_moved = True
def leaveEvent(self, leave_event):
self.clear_active_zone()
def paintEvent(self, paint_event):
painter = QPainter(self)
painter.setRenderHints(QPainter.Antialiasing)
# Clickable area
painter.setPen(Qt.transparent)
painter.setBrush(QColor(0, 0, 0, 1))
painter.drawRect(self.rect())
# Outer Circle
radial_grad = QRadialGradient(self.center_point, 50)
radial_grad.setColorAt(.1, QColor(0, 0, 0, 255))
radial_grad.setColorAt(1, QColor(0, 0, 0, 1))
painter.setBrush(radial_grad)
circle_pen = QPen(Qt.cyan, 2)
painter.setPen(circle_pen)
painter.drawEllipse(self.outer_circle_rect)
# Zone Highlight
if self.active_zone >= 0:
radial_grad2 = QRadialGradient(self.center_point, 80)
radial_grad2.setColorAt(0, QColor(255, 255, 255, 255))
radial_grad2.setColorAt(0.9, QColor(0, 0, 0, 1))
painter.setBrush(radial_grad2)
painter.setPen(Qt.transparent)
painter.drawPie(self.highlight_bounds, int(-self.highlight_start_angle * 16),
int(-self.ZONE_SPAN_ANGLE * 16))
# Inner Circle
painter.setBrush(Qt.black)
painter.setPen(circle_pen)
painter.drawEllipse(self.inner_circle_rect)
for i in range(self.NUM_ZONES):
if self.tools[i]:
if i == self.active_zone:
painter.setBrush(QColor(255, 255, 0, 127))
else:
painter.setBrush(QColor(0, 0, 0, 127))
label_x, label_y = self.label_point(i)
painter.setPen(Qt.transparent)
painter.drawRoundedRect(int(label_x), int(label_y), int(self.TEXT_WIDTH), int(self.TEXT_HEIGHT),
int(self.TEXT_RADIUS), int(self.TEXT_RADIUS))
painter.setPen(Qt.white)
painter.drawText(int(label_x), int(label_y), int(self.TEXT_WIDTH), int(self.TEXT_HEIGHT),
Qt.AlignCenter, self.tools[i][0])
def focusOutEvent(self, focus_event):
self.hide()
class TestWindow(QMainWindow):
def __init__(self, parent=None):
super(TestWindow, self).__init__(parent=None)
self.setWindowTitle("Radial Menu Example")
self.setFixedSize(960, 540)
self.setStyleSheet("background-color: #2c2f33;")
self.radial_menus = []
radial_menu = RadialMenu(Qt.RightButton)
for i in range(0, RadialMenu.NUM_ZONES):
radial_menu.set_tool(i, "Tool {0}".format(i), lambda index=i: self.activate_tool(index))
self.radial_menus.append(radial_menu)
def mousePressEvent(self, event):
if event.type() == QEvent.MouseButtonPress:
if event.button() == Qt.RightButton:
for radial_menu in self.radial_menus:
if event.button() == radial_menu.hotkey:
radial_menu.show()
def mouseReleaseEvent(self, event):
if event.type() == QEvent.MouseButtonRelease:
if event.button() == Qt.RightButton:
for radial_menu in self.radial_menus:
if event.button() == radial_menu.hotkey:
pass
#radial_menu.close()
def activate_tool(self, tool_index):
print("Activate tool for index: {0}".format(tool_index))
if __name__ == "__main__":
app = QApplication(sys.argv)
window = TestWindow()
window.show()
app.exec_()
A widget can only get mouse move events only if it becomes the "mouse grabber", which can only happen as long as it received the mouse event itself or grabMouse()
was called on it.
Since synthesizing a further mouse press and posting it to the event queue does not guarantee that the widget will receive it after it has been actually shown, one solution could be to call grabMouse()
in the show()
override (or the showEvent()
one).
Note that you must also call releaseMouse()
as soon as the widget is hidden.
def show(self):
...
self.grabMouse()
def hideEvent(self, event):
self.releaseMouse()
A more appropriate approach, though, may be to use the Qt.Popup
window flag, which indirectly grabs the mouse on the window, similarly to what happens to the QComboBox popup. Note that it might be required to explicitly call self.update()
within show()
.
class RadialMenu(QWidget):
...
def __init__(self, hotkey, parent=None):
...
self.setWindowFlags(Qt.FramelessWindowHint | Qt.Popup)
def show(self):
...
self.update()