Wayland: Workaround Qt/Wayland bug that prevents popup layout button menu from showing

Sigh. Wayland really is the gift that keeps on giving. For some reason
we cant use QMenu for this so re-implement QMenu like behavior with a
custom widget. Hopefully this doesnt break anything else.

Fixes #2109755 [Layout button doesn't activate options](https://bugs.launchpad.net/calibre/+bug/2109755)
This commit is contained in:
Kovid Goyal 2025-05-01 08:28:24 +05:30
parent bc449963f7
commit 56b9f337f7
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
2 changed files with 121 additions and 30 deletions

View File

@ -546,7 +546,6 @@ class LayoutMixin: # {{{
''')
for button in reversed(self.layout_buttons):
self.status_bar.insertPermanentWidget(2, button)
self.layout_button.setMenu(LayoutMenu(self))
self.layout_button.setVisible(not gprefs['show_layout_buttons'])
def init_layout_mixin(self):
@ -592,12 +591,13 @@ class LayoutMixin: # {{{
self.search_bar_button.toggled.connect(self.toggle_search_bar)
self.layout_button = b = QToolButton(self)
self.layout_button_menu = m = LayoutMenu(self)
b.setAutoRaise(True), b.setCursor(Qt.CursorShape.PointingHandCursor)
b.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
b.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
b.setText(_('Layout')), b.setIcon(QIcon.ic('layout.png'))
b.setToolTip(_(
'Show and hide various parts of the calibre main window'))
b.clicked.connect(m.toggle_visibility)
self.status_bar.addPermanentWidget(b)
# These must be after the layout button because it can be expanded into

View File

@ -2,7 +2,22 @@
# License: GPLv3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
from qt.core import QFontMetrics, QHBoxLayout, QIcon, QMenu, QPushButton, QSize, QSizePolicy, QStyle, QStyleOption, QStylePainter, Qt, QWidget
from qt.core import (
QEvent,
QFontMetrics,
QHBoxLayout,
QIcon,
QKeySequence,
QPainter,
QPushButton,
QSize,
QSizePolicy,
QStyle,
QStyleOption,
QStylePainter,
Qt,
QWidget,
)
class LayoutItem(QWidget):
@ -72,37 +87,47 @@ class LayoutItem(QWidget):
painter.end()
class LayoutMenu(QMenu):
class LayoutMenuInner(QWidget):
def __init__(self, parent=None):
QMenu.__init__(self, parent)
def __init__(self, parent):
super().__init__(parent)
self.l = l = QHBoxLayout(self)
l.setSpacing(20)
self.items = []
if parent is None:
buttons = [
QPushButton(QIcon.ic(i + '.png'), i, self)
for i in 'search tags cover_flow grid book'.split()]
for b in buttons:
b.setVisible(False), b.setCheckable(True), b.setChecked(b.text() in 'tags grid')
b.label = b.text().capitalize()
else:
buttons = parent.layout_buttons
for b in buttons:
self.items.append(LayoutItem(b, self))
l.addWidget(self.items[-1])
self.aboutToShow.connect(self.about_to_show)
self.current_item = None
self.initialized = False
def about_to_show(self):
@property
def gui(self):
return self.parent().parent()
def delayed_init(self):
if not self.initialized:
self.initialized = True
gui = self.gui
if not hasattr(gui, 'layout_buttons'):
buttons = [
QPushButton(QIcon.ic(i + '.png'), i, self)
for i in 'search tags cover_flow grid book'.split()]
for b in buttons:
b.setVisible(False), b.setCheckable(True), b.setChecked(b.text() in 'tags grid')
b.label = b.text().capitalize()
else:
buttons = gui.layout_buttons
l = self.layout()
for b in buttons:
self.items.append(LayoutItem(b, self))
l.addWidget(self.items[-1], alignment=Qt.AlignmentFlag.AlignBottom)
self.current_item = None
for x in self.items:
x.update_tips()
def sizeHint(self):
return QWidget.sizeHint(self)
self.resize(self.sizeHint())
def paintEvent(self, ev):
return QWidget.paintEvent(self, ev)
painter = QPainter(self)
col = self.palette().window().color()
col.setAlphaF(0.9)
painter.fillRect(self.rect(), col)
super().paintEvent(ev)
def item_for_ev(self, ev):
for item in self.items:
@ -113,8 +138,6 @@ class LayoutMenu(QMenu):
if ev.button() != Qt.MouseButton.LeftButton:
ev.ignore()
return
if (ev.pos().isNull() and not ev.screenPos().isNull()) or not self.rect().contains(ev.pos()):
self.hide()
self.current_item = self.item_for_ev(ev)
if self.current_item is not None:
ev.accept()
@ -128,13 +151,81 @@ class LayoutMenu(QMenu):
item = self.item_for_ev(ev)
if item is not None and item is self.current_item:
ev.accept()
self.hide()
self.parent().hide()
item.button.click()
def handle_key_press(self, ev):
q = QKeySequence(ev.keyCombination())
for item in self.items:
sc = item.button.shortcut
if callable(sc):
sc = item.button.shortcut()
else:
sc = QKeySequence.fromString(sc)
if sc.matches(q) == QKeySequence.SequenceMatch.ExactMatch:
self.parent().hide()
item.button.click()
ev.accept()
break
class LayoutMenu(QWidget):
def __init__(self, parent):
super().__init__(parent)
self.setVisible(False)
self.inner = LayoutMenuInner(self)
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
def toggle_visibility(self):
if self.isVisible():
self.hide()
else:
self.show()
def show(self):
self.inner.delayed_init()
parent = self.parent()
self.move(0, 0)
self.resize(parent.rect().size())
r = parent.rect()
y = r.height()
if hasattr(parent, 'layout_button'):
lb = parent.layout_button
y = lb.mapTo(parent, lb.rect().topLeft()).y()
self.inner.move(r.width() - self.inner.size().width(), y - self.inner.size().height())
super().show()
self.raise_()
self.setFocus(Qt.FocusReason.OtherFocusReason)
def event(self, ev):
if ev.type() == QEvent.Type.ShortcutOverride and self.isVisible():
ev.accept()
return super().event(ev)
def keyPressEvent(self, ev):
if ev.matches(QKeySequence.StandardKey.Cancel):
self.hide()
else:
self.inner.handle_key_press(ev)
def mousePressEvent(self, ev):
if ev.button() != Qt.MouseButton.LeftButton:
ev.ignore()
return
if self.inner.rect().contains(ev.pos()):
ev.ignore()
else:
self.hide()
if __name__ == '__main__':
from qt.core import QMainWindow
from calibre.gui2 import Application
app = Application([])
w = LayoutMenu()
w = QMainWindow()
m = LayoutMenu(w)
w.show()
w.exec()
m.show()
app.exec()