1 /* This file is part of the KDE libraries
2     Copyright (C) 1997, 1998, 1999, 2000  Sven Radej (radej@kde.org)
3     Copyright (C) 1997, 1998, 1999, 2000 Matthias Ettrich (ettrich@kde.org)
4     Copyright (C) 1999, 2000 Daniel "Mosfet" Duley (mosfet@kde.org)
5 
6     This library is free software; you can redistribute it and/or
7     modify it under the terms of the GNU Library General Public
8     License as published by the Free Software Foundation; either
9     version 2 of the License, or (at your option) any later version.
10 
11     This library is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14     Library General Public License for more details.
15 
16     You should have received a copy of the GNU Library General Public License
17     along with this library; see the file COPYING.LIB.  If not, write to
18     the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
19     Boston, MA 02110-1301, USA.
20     */
21 
22 #include "kmenubar.h"
23 
24 #include <config-kdelibs4support.h>
25 
26 #include <stdio.h>
27 
28 #include <QObject>
29 #include <QTimer>
30 #include <QActionEvent>
31 #include <QDesktopWidget>
32 #include <QPainter>
33 #include <QStyle>
34 #include <QStyleOptionMenuItem>
35 
36 #include <kconfig.h>
37 #include <kglobalsettings.h>
38 #include <qapplication.h>
39 #include <kdebug.h>
40 #include <kconfiggroup.h>
41 #include <kwindowsystem.h>
42 
43 #if HAVE_X11
44 #include <kmanagerselection.h>
45 #include <qx11info_x11.h>
46 
47 #include <X11/Xlib.h>
48 #include <X11/Xutil.h>
49 #include <X11/Xatom.h>
50 #endif
51 
52 /*
53 
54  Toplevel menubar (not for the fallback size handling done by itself):
55  - should not alter position or set strut
56  - every toplevel must have at most one matching topmenu
57  - embedder won't allow shrinking below a certain size
58  - must have WM_TRANSIENT_FOR pointing the its mainwindow
59      - the exception is desktop's menubar, which can be transient for root window
60        because of using root window as the desktop window
61  - Fitts' Law
62 
63 */
64 
65 static int block_resize = 0;
66 
67 class Q_DECL_HIDDEN KMenuBar::KMenuBarPrivate
68 {
69 public:
KMenuBarPrivate()70     KMenuBarPrivate()
71         :   forcedTopLevel(false),
72             topLevel(false),
73             wasTopLevel(false),
74 #if HAVE_X11
75             selection(nullptr),
76             isX11(QX11Info::isPlatformX11()),
77 #endif
78             min_size(0, 0)
79     {
80     }
~KMenuBarPrivate()81     ~KMenuBarPrivate()
82     {
83 #if HAVE_X11
84         delete selection;
85 #endif
86     }
87     int frameStyle; // only valid in toplevel mode
88     int lineWidth;  // dtto
89     int margin;     // dtto
90     bool fallback_mode : 1; // dtto
91 
92     bool forcedTopLevel : 1;
93     bool topLevel : 1;
94     bool wasTopLevel : 1; // when TLW is fullscreen, remember state
95 
96 #if HAVE_X11
97     KSelectionWatcher *selection;
98     bool isX11;
99 #endif
100     QTimer selection_timer;
101     QSize min_size;
102 #if HAVE_X11
103     static Atom makeSelectionAtom();
104 #endif
105 };
106 
107 #if HAVE_X11
108 static Atom selection_atom = None;
109 static Atom msg_type_atom = None;
110 
111 static
initAtoms()112 void initAtoms()
113 {
114     char nm[ 100 ];
115     sprintf(nm, "_KDE_TOPMENU_OWNER_S%d", DefaultScreen(QX11Info::display()));
116     char nm2[] = "_KDE_TOPMENU_MINSIZE";
117     char *names[ 2 ] = { nm, nm2 };
118     Atom atoms[ 2 ];
119     XInternAtoms(QX11Info::display(), names, 2, False, atoms);
120     selection_atom = atoms[ 0 ];
121     msg_type_atom = atoms[ 1 ];
122 }
123 
makeSelectionAtom()124 Atom KMenuBar::KMenuBarPrivate::makeSelectionAtom()
125 {
126     if (!QX11Info::isPlatformX11()) {
127         return 0;
128     }
129     if (selection_atom == None) {
130         initAtoms();
131     }
132     return selection_atom;
133 }
134 #endif
135 
KMenuBar(QWidget * parent)136 KMenuBar::KMenuBar(QWidget *parent)
137     : QMenuBar(parent), d(new KMenuBarPrivate)
138 {
139     connect(&d->selection_timer, SIGNAL(timeout()),
140             this, SLOT(selectionTimeout()));
141 
142     connect(qApp->desktop(), SIGNAL(resized(int)), SLOT(updateFallbackSize()));
143 
144     // toolbarAppearanceChanged(int) is sent when changing macstyle
145     connect(KGlobalSettings::self(), SIGNAL(toolbarAppearanceChanged(int)),
146             this, SLOT(slotReadConfig()));
147 
148     slotReadConfig();
149 }
150 
~KMenuBar()151 KMenuBar::~KMenuBar()
152 {
153     delete d;
154 }
155 
setTopLevelMenu(bool top_level)156 void KMenuBar::setTopLevelMenu(bool top_level)
157 {
158     d->forcedTopLevel = top_level;
159     setTopLevelMenuInternal(top_level);
160 }
161 
setTopLevelMenuInternal(bool top_level)162 void KMenuBar::setTopLevelMenuInternal(bool top_level)
163 {
164     if (d->forcedTopLevel) {
165         top_level = true;
166     }
167 
168     d->wasTopLevel = top_level;
169     if (parentWidget()
170             && parentWidget()->topLevelWidget()->isFullScreen()) {
171         top_level = false;
172     }
173 
174     if (isTopLevelMenu() == top_level) {
175         return;
176     }
177     d->topLevel = top_level;
178     if (isTopLevelMenu()) {
179 #if HAVE_X11
180         if (d->isX11) {
181             d->selection = new KSelectionWatcher(KMenuBarPrivate::makeSelectionAtom(),
182                                                 DefaultScreen(QX11Info::display()));
183             connect(d->selection, SIGNAL(newOwner(Window)),
184                     this, SLOT(updateFallbackSize()));
185             connect(d->selection, SIGNAL(lostOwner()),
186                     this, SLOT(updateFallbackSize()));
187         }
188 #endif
189         d->frameStyle = 0; //frameStyle();
190         d->lineWidth = 0; //lineWidth();
191         d->margin = 0; //margin();
192         d->fallback_mode = false;
193         bool wasShown = !isHidden();
194         setParent(parentWidget(), Qt::Window | Qt::Tool | Qt::FramelessWindowHint);
195         setGeometry(0, 0, width(), height());
196 #if HAVE_X11
197         KWindowSystem::setType(winId(), NET::TopMenu);
198 #endif
199 
200         if (parentWidget()) {
201             setAttribute(Qt::WA_NativeWindow, true);
202             KWindowSystem::setMainWindow(windowHandle(), parentWidget()->topLevelWidget()->winId());
203         }
204         //QMenuBar::setFrameStyle( NoFrame );
205         //QMenuBar::setLineWidth( 0 );
206         //QMenuBar::setMargin( 0 );
207         updateFallbackSize();
208         d->min_size = QSize(0, 0);
209         if (parentWidget() && !parentWidget()->isTopLevel()) {
210             setVisible(parentWidget()->isVisible());
211         } else if (wasShown) {
212             show();
213         }
214     } else {
215 #if HAVE_X11
216         delete d->selection;
217         d->selection = nullptr;
218 #endif
219         setAttribute(Qt::WA_NoSystemBackground, false);
220         setBackgroundRole(QPalette::Button);
221         setFrameStyle(d->frameStyle);
222         setLineWidth(d->lineWidth);
223         setMargin(d->margin);
224         setMinimumSize(0, 0);
225         setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX);
226         updateMenuBarSize();
227         if (parentWidget()) {
228             setParent(parentWidget());
229         }
230     }
231 }
232 
isTopLevelMenu() const233 bool KMenuBar::isTopLevelMenu() const
234 {
235     return d->topLevel;
236 }
237 
slotReadConfig()238 void KMenuBar::slotReadConfig()
239 {
240     KConfigGroup cg(KSharedConfig::openConfig(), "KDE");
241     setTopLevelMenuInternal(cg.readEntry("macStyle", false));
242 }
243 
eventFilter(QObject * obj,QEvent * ev)244 bool KMenuBar::eventFilter(QObject *obj, QEvent *ev)
245 {
246     if (d->topLevel) {
247         if (parentWidget() && obj == parentWidget()->topLevelWidget()) {
248             if (ev->type() == QEvent::Resize) {
249                 return false;    // ignore resizing of parent, QMenuBar would try to adjust size
250             }
251 #ifdef QT3_SUPPORT
252             if (ev->type() == QEvent::Accel || ev->type() == QEvent::AccelAvailable) {
253                 if (QApplication::sendEvent(topLevelWidget(), ev)) {
254                     return true;
255                 }
256             }
257 #endif
258             /* FIXME QEvent::ShowFullScreen is no more
259             if(ev->type() == QEvent::ShowFullScreen )
260                 // will update the state properly
261                 setTopLevelMenuInternal( d->topLevel );
262             */
263         }
264         if (parentWidget() && obj == parentWidget() && ev->type() == QEvent::ParentChange) {
265             setAttribute(Qt::WA_NativeWindow, true);
266             KWindowSystem::setMainWindow(windowHandle(), parentWidget()->topLevelWidget()->winId());
267             setVisible(parentWidget()->isTopLevel() || parentWidget()->isVisible());
268         }
269         if (parentWidget() && !parentWidget()->isTopLevel() && obj == parentWidget()) {
270             // if the parent is not toplevel, KMenuBar needs to match its visibility status
271             if (ev->type() == QEvent::Show) {
272                 setAttribute(Qt::WA_NativeWindow, true);
273                 KWindowSystem::setMainWindow(windowHandle(), parentWidget()->topLevelWidget()->winId());
274                 show();
275             }
276             if (ev->type() == QEvent::Hide) {
277                 hide();
278             }
279         }
280     } else {
281         if (parentWidget() && obj == parentWidget()->topLevelWidget()) {
282             if (ev->type() == QEvent::WindowStateChange
283                     && !parentWidget()->topLevelWidget()->isFullScreen()) {
284                 setTopLevelMenuInternal(d->wasTopLevel);
285             }
286         }
287     }
288     return QMenuBar::eventFilter(obj, ev);
289 }
290 
updateFallbackSize()291 void KMenuBar::updateFallbackSize()
292 {
293     if (!d->topLevel) {
294         return;
295     }
296 #if HAVE_X11
297     if (d->selection && d->selection->owner() != None)
298 #endif
299     {
300         // somebody is managing us, don't mess anything, undo changes
301         // done in fallback mode if needed
302         d->selection_timer.stop();
303         if (d->fallback_mode) {
304             d->fallback_mode = false;
305             KWindowSystem::setStrut(winId(), 0, 0, 0, 0);
306             setMinimumSize(0, 0);
307             setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX);
308             updateMenuBarSize();
309         }
310         return;
311     }
312     if (d->selection_timer.isActive()) {
313         return;
314     }
315     d->selection_timer.setInterval(100);
316     d->selection_timer.setSingleShot(true);
317     d->selection_timer.start();
318 }
319 
selectionTimeout()320 void KMenuBar::selectionTimeout()
321 {
322     // nobody is managing us, handle resizing
323     if (d->topLevel) {
324         d->fallback_mode = true; // KMenuBar is handling its position itself
325         KConfigGroup xineramaConfig(KSharedConfig::openConfig(), "Xinerama");
326         int screen = xineramaConfig.readEntry("MenubarScreen",
327                                               QApplication::desktop()->screenNumber(QPoint(0, 0)));
328         QRect area = QApplication::desktop()->screenGeometry(screen);
329         int margin = 0;
330         move(area.left() - margin, area.top() - margin);
331         setFixedSize(area.width() + 2 * margin, heightForWidth(area.width() + 2 * margin));
332 #if HAVE_X11
333         int strut_height = height() - margin;
334         if (strut_height < 0) {
335             strut_height = 0;
336         }
337         KWindowSystem::setStrut(winId(), 0, 0, strut_height, 0);
338 #endif
339     }
340 }
341 
resizeEvent(QResizeEvent * e)342 void KMenuBar::resizeEvent(QResizeEvent *e)
343 {
344     if (e->spontaneous() && d->topLevel && !d->fallback_mode) {
345         ++block_resize; // do not respond with configure request to ConfigureNotify event
346         QMenuBar::resizeEvent(e); // to avoid possible infinite loop
347         --block_resize;
348     } else {
349         QMenuBar::resizeEvent(e);
350     }
351 }
352 
setGeometry(const QRect & r)353 void KMenuBar::setGeometry(const QRect &r)
354 {
355     setGeometry(r.x(), r.y(), r.width(), r.height());
356 }
357 
setGeometry(int x,int y,int w,int h)358 void KMenuBar::setGeometry(int x, int y, int w, int h)
359 {
360     if (block_resize > 0) {
361         move(x, y);
362         return;
363     }
364     checkSize(w, h);
365     if (geometry() != QRect(x, y, w, h)) {
366         QMenuBar::setGeometry(x, y, w, h);
367     }
368 }
369 
resize(int w,int h)370 void KMenuBar::resize(int w, int h)
371 {
372     if (block_resize > 0) {
373         return;
374     }
375     checkSize(w, h);
376     if (size() != QSize(w, h)) {
377         QMenuBar::resize(w, h);
378     }
379 //    kDebug() << "RS:" << w << ":" << h << ":" << width() << ":" << height() << ":" << minimumWidth() << ":" << minimumHeight();
380 }
381 
resize(const QSize & s)382 void KMenuBar::resize(const QSize &s)
383 {
384     QMenuBar::resize(s);
385 }
386 
checkSize(int & w,int & h)387 void KMenuBar::checkSize(int &w, int &h)
388 {
389     if (!d->topLevel || d->fallback_mode) {
390         return;
391     }
392     QSize s = sizeHint();
393     w = s.width();
394     h = s.height();
395     // This is not done as setMinimumSize(), because that would set the minimum
396     // size in WM_NORMAL_HINTS, and KWin would not allow changing to smaller size
397     // anymore
398     w = qMax(w, d->min_size.width());
399     h = qMax(h, d->min_size.height());
400 }
401 
402 // QMenuBar's sizeHint() gives wrong size (insufficient width), which causes wrapping in the kicker applet
sizeHint() const403 QSize KMenuBar::sizeHint() const
404 {
405     if (!d->topLevel || block_resize > 0) {
406         return QMenuBar::sizeHint();
407     }
408     // Since QMenuBar::sizeHint() may indirectly call resize(),
409     // avoid infinite recursion.
410     ++block_resize;
411     // find the minimum useful height, and enlarge the width until the menu fits in that height (one row)
412     int h = heightForWidth(1000000);
413     int w = QMenuBar::sizeHint().width();
414     // optimization - don't call heightForWidth() too many times
415     while (heightForWidth(w + 12) > h) {
416         w += 12;
417     }
418     while (heightForWidth(w + 4) > h) {
419         w += 4;
420     }
421     while (heightForWidth(w) > h) {
422         ++w;
423     }
424     --block_resize;
425     return QSize(w, h);
426 }
427 
428 #pragma message("Port to Qt5 native filter")
429 #if 0
430 bool KMenuBar::x11Event(XEvent *ev)
431 {
432     if (ev->type == ClientMessage && ev->xclient.message_type == msg_type_atom
433             && ev->xclient.window == winId()) {
434         // QMenuBar is trying really hard to keep the size it deems right.
435         // Forcing minimum size and blocking resizing to match parent size
436         // in checkResizingToParent() seem to be the only way to make
437         // KMenuBar keep the size it wants
438         d->min_size = QSize(ev->xclient.data.l[ 1 ], ev->xclient.data.l[ 2 ]);
439 //        kDebug() << "MINSIZE:" << d->min_size;
440         updateMenuBarSize();
441         return true;
442     }
443     return QMenuBar::x11Event(ev);
444 }
445 #endif
446 
updateMenuBarSize()447 void KMenuBar::updateMenuBarSize()
448 {
449     //menuContentsChanged(); // trigger invalidating calculated size
450     resize(sizeHint());    // and resize to preferred size
451 }
452 
setFrameStyle(int style)453 void KMenuBar::setFrameStyle(int style)
454 {
455     if (d->topLevel) {
456         d->frameStyle = style;
457     }
458 //     else
459 //  QMenuBar::setFrameStyle( style );
460 }
461 
setLineWidth(int width)462 void KMenuBar::setLineWidth(int width)
463 {
464     if (d->topLevel) {
465         d->lineWidth = width;
466     }
467 //     else
468 //  QMenuBar::setLineWidth( width );
469 }
470 
setMargin(int margin)471 void KMenuBar::setMargin(int margin)
472 {
473     if (d->topLevel) {
474         d->margin = margin;
475     }
476 //     else
477 //  QMenuBar::setMargin( margin );
478 }
479 
closeEvent(QCloseEvent * e)480 void KMenuBar::closeEvent(QCloseEvent *e)
481 {
482     if (d->topLevel) {
483         e->ignore();    // mainly for the fallback mode
484     } else {
485         QMenuBar::closeEvent(e);
486     }
487 }
488 
paintEvent(QPaintEvent * pe)489 void KMenuBar::paintEvent(QPaintEvent *pe)
490 {
491     // Closes the BR77113
492     // We need to overload this method to paint only the menu items
493     // This way when the KMenuBar is embedded in the menu applet it
494     // integrates correctly.
495     //
496     // Background mode and origin are set so late because of styles
497     // using the polish() method to modify these settings.
498     //
499     // Of course this hack can safely be removed when real transparency
500     // will be available
501 
502 //    if( !d->topLevel )
503     {
504         QMenuBar::paintEvent(pe);
505     }
506 #if 0
507     else {
508         QPainter p(this);
509         bool up_enabled = isUpdatesEnabled();
510         Qt::BackgroundMode bg_mode = backgroundMode();
511         BackgroundOrigin bg_origin = backgroundOrigin();
512 
513         setUpdatesEnabled(false);
514         setBackgroundMode(Qt::X11ParentRelative);
515         setBackgroundOrigin(WindowOrigin);
516 
517         p.eraseRect(rect());
518         erase();
519 
520         QColorGroup g = colorGroup();
521         bool e;
522 
523         for (int i = 0; i < (int)count(); i++) {
524             QMenuItem *mi = findItem(idAt(i));
525 
526             if (!mi->text().isEmpty() || !mi->icon().isNull()) {
527                 QRect r = itemRect(i);
528                 if (r.isEmpty() || !mi->isVisible()) {
529                     continue;
530                 }
531 
532                 e = mi->isEnabled() && mi->isVisible();
533                 if (e)
534                     g = isEnabled() ? (isActiveWindow() ? palette().active() :
535                                        palette().inactive()) : palette().disabled();
536                 else {
537                     g = palette().disabled();
538                 }
539 
540                 bool item_active = (activeAction() ==  mi);
541 
542                 p.setClipRect(r);
543 
544                 if (item_active) {
545                     QStyleOptionMenuItem miOpt;
546                     miOpt.init(this);
547                     miOpt.rect = r;
548                     miOpt.text = mi->text();
549                     miOpt.icon = mi->icon();
550                     miOpt.palette = g;
551 
552                     QStyle::State flags = QStyle::State_None;
553                     if (isEnabled() && e) {
554                         flags |= QStyle::State_Enabled;
555                     }
556                     if (item_active) {
557                         flags |= QStyle::State_Active;
558                     }
559                     if (item_active && actItemDown) {
560                         flags |= QStyle::State_Down;
561                     }
562                     flags |= QStyle::State_HasFocus;
563 
564                     mi->state = flags;
565 
566                     style()->drawControl(QStyle::CE_MenuBarItem, &miOpt, &p, this);
567                 } else {
568                     style()->drawItem(p, r, Qt::AlignCenter | Qt::AlignVCenter | Qt::TextShowMnemonic,
569                                       g, e, mi->pixmap(), mi->text());
570                 }
571             }
572         }
573 
574         setBackgroundOrigin(bg_origin);
575         setBackgroundMode(bg_mode);
576         setUpdatesEnabled(up_enabled);
577     }
578 #endif
579 }
580 
581