1 /*
2   SPDX-FileCopyrightText: 2008-2014 Eike Hein <hein@kde.org>
3   SPDX-FileCopyrightText: 2009 Juan Carlos Torres <carlosdgtorres@gmail.com>
4   SPDX-FileCopyrightText: 2020 Ryan McCoskrie <work@ryanmccoskrie.me>
5 
6   SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
7 */
8 
9 #include "mainwindow.h"
10 #include "config/appearancesettings.h"
11 #include "config/windowsettings.h"
12 #include "firstrundialog.h"
13 #include "sessionstack.h"
14 #include "settings.h"
15 #include "skin.h"
16 #include "tabbar.h"
17 #include "terminal.h"
18 #include "titlebar.h"
19 #include "ui_behaviorsettings.h"
20 
21 #include <KAboutData>
22 #include <KActionCollection>
23 #include <KConfigDialog>
24 #include <KGlobalAccel>
25 #include <KHelpMenu>
26 #include <KLocalizedString>
27 #include <KMessageBox>
28 #include <KNotification>
29 #include <KNotifyConfigWidget>
30 #include <KShortcutsDialog>
31 #include <KStandardAction>
32 #include <KStatusNotifierItem>
33 #include <KToggleFullScreenAction>
34 #include <KWindowEffects>
35 #include <KWindowSystem>
36 
37 #include <QApplication>
38 #include <QDBusConnection>
39 #include <QDBusInterface>
40 #include <QDBusPendingReply>
41 #include <QDesktopWidget>
42 #include <QMenu>
43 #include <QPainter>
44 #include <QPlatformSurfaceEvent>
45 #include <QScreen>
46 #include <QWhatsThis>
47 #include <QWindow>
48 
49 #if HAVE_X11
50 #include <QX11Info>
51 
52 #include <X11/Xlib.h>
53 #include <fixx11h.h>
54 #endif
55 
56 #if HAVE_KWAYLAND
57 #include <KWayland/Client/connection_thread.h>
58 #include <KWayland/Client/plasmashell.h>
59 #include <KWayland/Client/registry.h>
60 #include <KWayland/Client/surface.h>
61 #endif
62 
MainWindow(QWidget * parent)63 MainWindow::MainWindow(QWidget *parent)
64     : KMainWindow(parent, Qt::CustomizeWindowHint | Qt::FramelessWindowHint | Qt::Tool)
65 {
66     QDBusConnection::sessionBus().registerObject(QStringLiteral("/yakuake/window"), this, QDBusConnection::ExportScriptableSlots);
67 
68     setAttribute(Qt::WA_TranslucentBackground, true);
69     setAttribute(Qt::WA_DeleteOnClose, false);
70     setAttribute(Qt::WA_QuitOnClose, true);
71 
72     m_skin = new Skin();
73     m_menu = new QMenu(this);
74     m_helpMenu = new KHelpMenu(this, KAboutData::applicationData());
75     m_sessionStack = new SessionStack(this);
76     m_titleBar = new TitleBar(this);
77     m_tabBar = new TabBar(this);
78     m_notifierItem = nullptr;
79 
80     m_firstRunDialog = nullptr;
81     m_isFullscreen = false;
82 
83 #if HAVE_X11
84     m_kwinAssistPropSet = false;
85     m_isX11 = KWindowSystem::isPlatformX11();
86 #else
87     m_isX11 = false;
88 #endif
89     m_isWayland = KWindowSystem::isPlatformWayland();
90 #if HAVE_KWAYLAND
91     m_plasmaShell = nullptr;
92     m_plasmaShellSurface = nullptr;
93     initWayland();
94 #endif
95 
96     m_toggleLock = false;
97 
98     setupActions();
99     setupMenu();
100 
101     connect(m_tabBar, SIGNAL(newTabRequested()), m_sessionStack, SLOT(addSession()));
102     connect(m_tabBar, SIGNAL(lastTabClosed()), m_tabBar, SIGNAL(newTabRequested()));
103     connect(m_tabBar, SIGNAL(lastTabClosed()), this, SLOT(handleLastTabClosed()));
104     connect(m_tabBar, SIGNAL(tabSelected(int)), m_sessionStack, SLOT(raiseSession(int)));
105     connect(m_tabBar, SIGNAL(tabClosed(int)), m_sessionStack, SLOT(removeSession(int)));
106     connect(m_tabBar, &TabBar::tabTitleEdited, m_sessionStack, [&](int, QString) {
107         m_sessionStack->raiseSession(m_sessionStack->activeSessionId());
108     });
109     connect(m_tabBar, SIGNAL(requestTerminalHighlight(int)), m_sessionStack, SLOT(handleTerminalHighlightRequest(int)));
110     connect(m_tabBar, SIGNAL(requestRemoveTerminalHighlight()), m_sessionStack, SIGNAL(removeTerminalHighlight()));
111     connect(m_tabBar, SIGNAL(tabContextMenuClosed()), m_sessionStack, SIGNAL(removeTerminalHighlight()));
112 
113     connect(m_sessionStack, SIGNAL(sessionAdded(int, QString)), m_tabBar, SLOT(addTab(int, QString)));
114     connect(m_sessionStack, SIGNAL(sessionRaised(int)), m_tabBar, SLOT(selectTab(int)));
115     connect(m_sessionStack, SIGNAL(sessionRemoved(int)), m_tabBar, SLOT(removeTab(int)));
116     connect(m_sessionStack, SIGNAL(activeTitleChanged(QString)), m_titleBar, SLOT(setTitle(QString)));
117     connect(m_sessionStack, SIGNAL(activeTitleChanged(QString)), this, SLOT(setWindowTitle(QString)));
118 
119     connect(&m_mousePoller, SIGNAL(timeout()), this, SLOT(pollMouse()));
120 
121     connect(KWindowSystem::self(), SIGNAL(workAreaChanged()), this, SLOT(applyWindowGeometry()));
122     connect(QApplication::desktop(), SIGNAL(screenCountChanged(int)), this, SLOT(updateScreenMenu()));
123 
124     applySettings();
125 
126     m_sessionStack->addSession();
127 
128     if (Settings::firstRun()) {
129         QMetaObject::invokeMethod(this, "toggleWindowState", Qt::QueuedConnection);
130         QMetaObject::invokeMethod(this, "showFirstRunDialog", Qt::QueuedConnection);
131     } else {
132         if (Settings::pollMouse())
133             toggleMousePoll(true);
134     }
135 
136     if (Settings::openAfterStart())
137         QMetaObject::invokeMethod(this, "toggleWindowState", Qt::QueuedConnection);
138 }
139 
~MainWindow()140 MainWindow::~MainWindow()
141 {
142     Settings::self()->save();
143 
144     delete m_skin;
145 }
146 
147 #if HAVE_KWAYLAND
initWayland()148 void MainWindow::initWayland()
149 {
150     if (!m_isWayland) {
151         return;
152     }
153 
154     using namespace KWayland::Client;
155     auto connection = ConnectionThread::fromApplication(this);
156     if (!connection) {
157         return;
158     }
159     Registry *registry = new Registry(this);
160     registry->create(connection);
161     QObject::connect(registry, &Registry::interfacesAnnounced, this, [registry, this] {
162         const auto interface = registry->interface(Registry::Interface::PlasmaShell);
163         if (interface.name != 0) {
164             m_plasmaShell = registry->createPlasmaShell(interface.name, interface.version, this);
165         }
166     });
167 
168     registry->setup();
169     connection->roundtrip();
170 }
171 
initWaylandSurface()172 void MainWindow::initWaylandSurface()
173 {
174     if (m_plasmaShellSurface) {
175         m_plasmaShellSurface->setPosition(pos());
176         return;
177     }
178     if (!m_plasmaShell) {
179         return;
180     }
181     if (auto surface = KWayland::Client::Surface::fromWindow(windowHandle())) {
182         m_plasmaShellSurface = m_plasmaShell->createSurface(surface, this);
183         m_plasmaShellSurface->setPosition(pos());
184     }
185 }
186 
187 #endif
188 
queryClose()189 bool MainWindow::queryClose()
190 {
191     bool confirmQuit = Settings::confirmQuit();
192     bool hasUnclosableSessions = m_sessionStack->hasUnclosableSessions();
193 
194     QString closeQuestion = xi18nc("@info", "Are you sure you want to quit?");
195     QString warningMessage;
196 
197     if ((confirmQuit && m_sessionStack->count() > 1) || hasUnclosableSessions) {
198         if (confirmQuit && m_sessionStack->count() > 1) {
199             if (hasUnclosableSessions)
200                 warningMessage = xi18nc("@info",
201                                         "<warning>There are multiple open sessions, <emphasis>some of which you have locked to prevent closing them "
202                                         "accidentally.</emphasis> These will be killed if you continue.</warning>");
203             else
204                 warningMessage = xi18nc("@info", "<warning>There are multiple open sessions. These will be killed if you continue.</warning>");
205         } else if (hasUnclosableSessions) {
206             warningMessage = xi18nc("@info",
207                                     "<warning>There are one or more open sessions that you have locked to prevent closing them accidentally. These will be "
208                                     "killed if you continue.</warning>");
209         }
210 
211         int result = KMessageBox::warningContinueCancel(this,
212                                                         warningMessage + QStringLiteral("<br /><br />") + closeQuestion,
213                                                         xi18nc("@title:window", "Really Quit?"),
214                                                         KStandardGuiItem::quit(),
215                                                         KStandardGuiItem::cancel());
216 
217         return result != KMessageBox::Cancel;
218     }
219 
220     return true;
221 }
222 
setupActions()223 void MainWindow::setupActions()
224 {
225     m_actionCollection = new KActionCollection(this);
226 
227     KToggleFullScreenAction *fullScreenAction = new KToggleFullScreenAction(this);
228     fullScreenAction->setWindow(this);
229     actionCollection()->setDefaultShortcut(fullScreenAction, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_F11));
230     m_actionCollection->addAction(QStringLiteral("view-full-screen"), fullScreenAction);
231     connect(fullScreenAction, SIGNAL(toggled(bool)), this, SLOT(setFullScreen(bool)));
232 
233     QAction *action = KStandardAction::quit(this, SLOT(close()), actionCollection());
234     actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_Q));
235     action = KStandardAction::aboutApp(m_helpMenu, SLOT(aboutApplication()), actionCollection());
236     action = KStandardAction::reportBug(m_helpMenu, SLOT(reportBug()), actionCollection());
237     action = KStandardAction::aboutKDE(m_helpMenu, SLOT(aboutKDE()), actionCollection());
238     action = KStandardAction::keyBindings(this, SLOT(configureKeys()), actionCollection());
239     action = KStandardAction::configureNotifications(this, SLOT(configureNotifications()), actionCollection());
240     action = KStandardAction::preferences(this, SLOT(configureApp()), actionCollection());
241 
242     action = KStandardAction::whatsThis(this, SLOT(whatsThis()), actionCollection());
243 
244     action = actionCollection()->addAction(QStringLiteral("toggle-window-state"));
245     action->setText(xi18nc("@action", "Open/Retract Yakuake"));
246     action->setIcon(QIcon::fromTheme(QStringLiteral("yakuake")));
247 #ifndef Q_OS_WIN /* PORT */
248     KGlobalAccel::self()->setGlobalShortcut(action, QList<QKeySequence>() << QKeySequence(Qt::Key_F12));
249 #else
250     KGlobalAccel::self()->setGlobalShortcut(action, QList<QKeySequence>() << QKeySequence(Qt::Key_F11));
251 #endif
252     connect(action, SIGNAL(triggered()), this, SLOT(toggleWindowState()));
253     connect(action, SIGNAL(changed()), this, SLOT(updateTrayTooltip()));
254     connect(KGlobalAccel::self(), SIGNAL(globalShortcutChanged(QAction *, const QKeySequence &)), this, SLOT(updateTrayTooltip()));
255     updateTrayTooltip();
256 
257     action = actionCollection()->addAction(QStringLiteral("keep-open"));
258     action->setText(xi18nc("@action", "Keep window open when it loses focus"));
259     action->setCheckable(true);
260     connect(action, SIGNAL(toggled(bool)), this, SLOT(setKeepOpen(bool)));
261 
262     action = actionCollection()->addAction(QStringLiteral("manage-profiles"));
263     action->setText(xi18nc("@action", "Manage Profiles..."));
264     action->setIcon(QIcon::fromTheme(QStringLiteral("configure")));
265     connect(action, SIGNAL(triggered()), m_sessionStack, SIGNAL(manageProfiles()));
266 
267     action = actionCollection()->addAction(QStringLiteral("edit-profile"));
268     action->setText(xi18nc("@action", "Edit Current Profile..."));
269     action->setIcon(QIcon::fromTheme(QStringLiteral("document-properties")));
270     connect(action, SIGNAL(triggered()), this, SLOT(handleContextDependentAction()));
271     m_contextDependentActions << action;
272 
273     action = actionCollection()->addAction(QStringLiteral("increase-window-width"));
274     action->setText(xi18nc("@action", "Increase Window Width"));
275     actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::ALT + Qt::SHIFT + Qt::Key_Right));
276     connect(action, SIGNAL(triggered()), this, SLOT(increaseWindowWidth()));
277 
278     action = actionCollection()->addAction(QStringLiteral("decrease-window-width"));
279     action->setText(xi18nc("@action", "Decrease Window Width"));
280     actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::ALT + Qt::SHIFT + Qt::Key_Left));
281     connect(action, SIGNAL(triggered()), this, SLOT(decreaseWindowWidth()));
282 
283     action = actionCollection()->addAction(QStringLiteral("increase-window-height"));
284     action->setText(xi18nc("@action", "Increase Window Height"));
285     actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::ALT + Qt::SHIFT + Qt::Key_Down));
286     connect(action, SIGNAL(triggered()), this, SLOT(increaseWindowHeight()));
287 
288     action = actionCollection()->addAction(QStringLiteral("decrease-window-height"));
289     action->setText(xi18nc("@action", "Decrease Window Height"));
290     actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::ALT + Qt::SHIFT + Qt::Key_Up));
291     connect(action, SIGNAL(triggered()), this, SLOT(decreaseWindowHeight()));
292 
293     action = actionCollection()->addAction(QStringLiteral("new-session"));
294     action->setText(xi18nc("@action", "New Session"));
295     action->setIcon(QIcon::fromTheme(QStringLiteral("tab-new")));
296     actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_T));
297     connect(action, SIGNAL(triggered()), m_sessionStack, SLOT(addSession()));
298 
299     action = actionCollection()->addAction(QStringLiteral("new-session-two-horizontal"));
300     action->setText(xi18nc("@action", "Two Terminals, Horizontally"));
301     action->setIcon(QIcon::fromTheme(QStringLiteral("tab-new")));
302     connect(action, SIGNAL(triggered()), m_sessionStack, SLOT(addSessionTwoHorizontal()));
303 
304     action = actionCollection()->addAction(QStringLiteral("new-session-two-vertical"));
305     action->setText(xi18nc("@action", "Two Terminals, Vertically"));
306     action->setIcon(QIcon::fromTheme(QStringLiteral("tab-new")));
307     connect(action, SIGNAL(triggered()), m_sessionStack, SLOT(addSessionTwoVertical()));
308 
309     action = actionCollection()->addAction(QStringLiteral("new-session-quad"));
310     action->setText(xi18nc("@action", "Four Terminals, Grid"));
311     action->setIcon(QIcon::fromTheme(QStringLiteral("tab-new")));
312     connect(action, SIGNAL(triggered()), m_sessionStack, SLOT(addSessionQuad()));
313 
314     action = actionCollection()->addAction(QStringLiteral("close-session"));
315     action->setText(xi18nc("@action", "Close Session"));
316     action->setIcon(QIcon::fromTheme(QStringLiteral("tab-close")));
317     actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_W));
318     connect(action, SIGNAL(triggered()), this, SLOT(handleContextDependentAction()));
319     m_contextDependentActions << action;
320 
321     action = actionCollection()->addAction(QStringLiteral("previous-session"));
322     action->setText(xi18nc("@action", "Previous Session"));
323     action->setIcon(QIcon::fromTheme(QStringLiteral("go-previous")));
324     actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::SHIFT + Qt::Key_Left));
325     connect(action, SIGNAL(triggered()), m_tabBar, SLOT(selectPreviousTab()));
326 
327     action = actionCollection()->addAction(QStringLiteral("next-session"));
328     action->setText(xi18nc("@action", "Next Session"));
329     action->setIcon(QIcon::fromTheme(QStringLiteral("go-next")));
330     actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::SHIFT + Qt::Key_Right));
331     connect(action, SIGNAL(triggered()), m_tabBar, SLOT(selectNextTab()));
332 
333     action = actionCollection()->addAction(QStringLiteral("move-session-left"));
334     action->setText(xi18nc("@action", "Move Session Left"));
335     action->setIcon(QIcon::fromTheme(QStringLiteral("arrow-left")));
336     actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_Left));
337     connect(action, SIGNAL(triggered()), this, SLOT(handleContextDependentAction()));
338     m_contextDependentActions << action;
339 
340     action = actionCollection()->addAction(QStringLiteral("move-session-right"));
341     action->setText(xi18nc("@action", "Move Session Right"));
342     action->setIcon(QIcon::fromTheme(QStringLiteral("arrow-right")));
343     actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_Right));
344     connect(action, SIGNAL(triggered()), this, SLOT(handleContextDependentAction()));
345     m_contextDependentActions << action;
346 
347     action = actionCollection()->addAction(QStringLiteral("grow-terminal-right"));
348     action->setText(xi18nc("@action", "Grow Terminal to the Right"));
349     action->setIcon(QIcon::fromTheme(QStringLiteral("arrow-right")));
350     actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_Right));
351     connect(action, SIGNAL(triggered()), this, SLOT(handleContextDependentAction()));
352     m_contextDependentActions << action;
353 
354     action = actionCollection()->addAction(QStringLiteral("grow-terminal-left"));
355     action->setText(xi18nc("@action", "Grow Terminal to the Left"));
356     action->setIcon(QIcon::fromTheme(QStringLiteral("arrow-left")));
357     actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_Left));
358     connect(action, SIGNAL(triggered()), this, SLOT(handleContextDependentAction()));
359     m_contextDependentActions << action;
360 
361     action = actionCollection()->addAction(QStringLiteral("grow-terminal-top"));
362     action->setText(xi18nc("@action", "Grow Terminal to the Top"));
363     action->setIcon(QIcon::fromTheme(QStringLiteral("arrow-up")));
364     actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_Up));
365     connect(action, SIGNAL(triggered()), this, SLOT(handleContextDependentAction()));
366     m_contextDependentActions << action;
367 
368     action = actionCollection()->addAction(QStringLiteral("grow-terminal-bottom"));
369     action->setText(xi18nc("@action", "Grow Terminal to the Bottom"));
370     action->setIcon(QIcon::fromTheme(QStringLiteral("arrow-down")));
371     actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_Down));
372     connect(action, SIGNAL(triggered()), this, SLOT(handleContextDependentAction()));
373     m_contextDependentActions << action;
374 
375     action = actionCollection()->addAction(QStringLiteral("rename-session"));
376     action->setText(xi18nc("@action", "Rename Session..."));
377     action->setIcon(QIcon::fromTheme(QStringLiteral("edit-rename")));
378     actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_S));
379     connect(action, SIGNAL(triggered()), this, SLOT(handleContextDependentAction()));
380     m_contextDependentActions << action;
381 
382     action = actionCollection()->addAction(QStringLiteral("previous-terminal"));
383     action->setText(xi18nc("@action", "Previous Terminal"));
384     action->setIcon(QIcon::fromTheme(QStringLiteral("go-previous")));
385     actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_Tab));
386     connect(action, SIGNAL(triggered()), m_sessionStack, SIGNAL(previousTerminal()));
387 
388     action = actionCollection()->addAction(QStringLiteral("next-terminal"));
389     action->setText(xi18nc("@action", "Next Terminal"));
390     action->setIcon(QIcon::fromTheme(QStringLiteral("go-next")));
391     actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL | Qt::Key_Tab));
392     connect(action, SIGNAL(triggered()), m_sessionStack, SIGNAL(nextTerminal()));
393 
394     action = actionCollection()->addAction(QStringLiteral("close-active-terminal"));
395     action->setText(xi18nc("@action", "Close Active Terminal"));
396     action->setIcon(QIcon::fromTheme(QStringLiteral("view-close")));
397     actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_R));
398     connect(action, SIGNAL(triggered()), this, SLOT(handleContextDependentAction()));
399     m_contextDependentActions << action;
400 
401     action = actionCollection()->addAction(QStringLiteral("split-left-right"));
402     action->setText(xi18nc("@action", "Split Left/Right"));
403     action->setIcon(QIcon::fromTheme(QStringLiteral("view-split-left-right")));
404     actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_ParenLeft));
405     connect(action, SIGNAL(triggered()), this, SLOT(handleContextDependentAction()));
406     m_contextDependentActions << action;
407 
408     action = actionCollection()->addAction(QStringLiteral("split-top-bottom"));
409     action->setText(xi18nc("@action", "Split Top/Bottom"));
410     action->setIcon(QIcon::fromTheme(QStringLiteral("view-split-top-bottom")));
411     actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_ParenRight));
412     connect(action, SIGNAL(triggered()), this, SLOT(handleContextDependentAction()));
413     m_contextDependentActions << action;
414 
415     action = actionCollection()->addAction(QStringLiteral("toggle-session-prevent-closing"));
416     action->setText(xi18nc("@action", "Prevent Closing"));
417     action->setCheckable(true);
418     connect(action, SIGNAL(triggered(bool)), this, SLOT(handleContextDependentToggleAction(bool)));
419     m_contextDependentActions << action;
420 
421     action = actionCollection()->addAction(QStringLiteral("toggle-session-keyboard-input"));
422     action->setText(xi18nc("@action", "Disable Keyboard Input"));
423     action->setCheckable(true);
424     connect(action, SIGNAL(triggered(bool)), this, SLOT(handleContextDependentToggleAction(bool)));
425     m_contextDependentActions << action;
426 
427     action = actionCollection()->addAction(QStringLiteral("toggle-session-monitor-activity"));
428     action->setText(xi18nc("@action", "Monitor for Activity"));
429     actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_A));
430     action->setCheckable(true);
431     connect(action, SIGNAL(triggered(bool)), this, SLOT(handleContextDependentToggleAction(bool)));
432     m_contextDependentActions << action;
433 
434     action = actionCollection()->addAction(QStringLiteral("toggle-session-monitor-silence"));
435     action->setText(xi18nc("@action", "Monitor for Silence"));
436     actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_I));
437     action->setCheckable(true);
438     connect(action, SIGNAL(triggered(bool)), this, SLOT(handleContextDependentToggleAction(bool)));
439     m_contextDependentActions << action;
440 
441     action = actionCollection()->addAction(QStringLiteral("toggle-titlebar"));
442     action->setText(xi18nc("@action", "Toggle Titlebar"));
443     actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_M));
444     action->setCheckable(true);
445     connect(action, SIGNAL(triggered()), this, SLOT(handleToggleTitlebar()));
446 
447     for (uint i = 1; i <= 10; ++i) {
448         action = actionCollection()->addAction(QStringLiteral("switch-to-session-%1").arg(i));
449         action->setText(xi18nc("@action", "Switch to Session %1", i));
450         action->setData(i - 1);
451         connect(action, SIGNAL(triggered()), this, SLOT(handleSwitchToAction()));
452 
453         if (i < 10) {
454             // add default shortcut bindings for the first 9 sessions
455             actionCollection()->setDefaultShortcut(action, QStringLiteral("Alt+%1").arg(i));
456         } else {
457             // add default shortcut bindings for the 10th session
458             actionCollection()->setDefaultShortcut(action, Qt::ALT + Qt::Key_0);
459         }
460     }
461 
462     m_actionCollection->associateWidget(this);
463     m_actionCollection->readSettings();
464 }
465 
handleContextDependentAction(QAction * action,int sessionId)466 void MainWindow::handleContextDependentAction(QAction *action, int sessionId)
467 {
468     if (sessionId == -1)
469         sessionId = m_sessionStack->activeSessionId();
470     if (sessionId == -1)
471         return;
472 
473     if (!action)
474         action = qobject_cast<QAction *>(QObject::sender());
475 
476     if (action == actionCollection()->action(QStringLiteral("edit-profile")))
477         m_sessionStack->editProfile(sessionId);
478 
479     if (action == actionCollection()->action(QStringLiteral("close-session")))
480         m_sessionStack->removeSession(sessionId);
481 
482     if (action == actionCollection()->action(QStringLiteral("move-session-left")))
483         m_tabBar->moveTabLeft(sessionId);
484 
485     if (action == actionCollection()->action(QStringLiteral("move-session-right")))
486         m_tabBar->moveTabRight(sessionId);
487 
488     if (action == actionCollection()->action(QStringLiteral("rename-session")))
489         m_tabBar->interactiveRename(sessionId);
490 
491     if (action == actionCollection()->action(QStringLiteral("close-active-terminal")))
492         m_sessionStack->closeActiveTerminal(sessionId);
493 
494     if (action == actionCollection()->action(QStringLiteral("split-left-right")))
495         m_sessionStack->splitSessionLeftRight(sessionId);
496 
497     if (action == actionCollection()->action(QStringLiteral("split-top-bottom")))
498         m_sessionStack->splitSessionTopBottom(sessionId);
499 
500     if (action == actionCollection()->action(QStringLiteral("grow-terminal-right")))
501         m_sessionStack->tryGrowTerminalRight(m_sessionStack->activeTerminalId());
502 
503     if (action == actionCollection()->action(QStringLiteral("grow-terminal-left")))
504         m_sessionStack->tryGrowTerminalLeft(m_sessionStack->activeTerminalId());
505 
506     if (action == actionCollection()->action(QStringLiteral("grow-terminal-top")))
507         m_sessionStack->tryGrowTerminalTop(m_sessionStack->activeTerminalId());
508 
509     if (action == actionCollection()->action(QStringLiteral("grow-terminal-bottom")))
510         m_sessionStack->tryGrowTerminalBottom(m_sessionStack->activeTerminalId());
511 }
512 
handleContextDependentToggleAction(bool checked,QAction * action,int sessionId)513 void MainWindow::handleContextDependentToggleAction(bool checked, QAction *action, int sessionId)
514 {
515     if (sessionId == -1)
516         sessionId = m_sessionStack->activeSessionId();
517     if (sessionId == -1)
518         return;
519 
520     if (!action)
521         action = qobject_cast<QAction *>(QObject::sender());
522 
523     if (action == actionCollection()->action(QStringLiteral("toggle-session-prevent-closing"))) {
524         m_sessionStack->setSessionClosable(sessionId, !checked);
525 
526         // Repaint the tab bar when the Prevent Closing action is toggled
527         // so the lock icon is added to or removed from the tab label.
528         m_tabBar->repaint();
529     }
530 
531     if (action == actionCollection()->action(QStringLiteral("toggle-session-keyboard-input")))
532         m_sessionStack->setSessionKeyboardInputEnabled(sessionId, !checked);
533 
534     if (action == actionCollection()->action(QStringLiteral("toggle-session-monitor-activity")))
535         m_sessionStack->setSessionMonitorActivityEnabled(sessionId, checked);
536 
537     if (action == actionCollection()->action(QStringLiteral("toggle-session-monitor-silence")))
538         m_sessionStack->setSessionMonitorSilenceEnabled(sessionId, checked);
539 }
540 
setContextDependentActionsQuiet(bool quiet)541 void MainWindow::setContextDependentActionsQuiet(bool quiet)
542 {
543     QListIterator<QAction *> i(m_contextDependentActions);
544 
545     while (i.hasNext())
546         i.next()->blockSignals(quiet);
547 }
548 
handleToggleTerminalKeyboardInput(bool checked)549 void MainWindow::handleToggleTerminalKeyboardInput(bool checked)
550 {
551     QAction *action = qobject_cast<QAction *>(QObject::sender());
552 
553     if (!action || action->data().isNull())
554         return;
555 
556     bool ok = false;
557     int terminalId = action->data().toInt(&ok);
558     if (!ok)
559         return;
560 
561     m_sessionStack->setTerminalKeyboardInputEnabled(terminalId, !checked);
562 }
563 
handleToggleTerminalMonitorActivity(bool checked)564 void MainWindow::handleToggleTerminalMonitorActivity(bool checked)
565 {
566     QAction *action = qobject_cast<QAction *>(QObject::sender());
567 
568     if (!action || action->data().isNull())
569         return;
570 
571     bool ok = false;
572     int terminalId = action->data().toInt(&ok);
573     if (!ok)
574         return;
575 
576     m_sessionStack->setTerminalMonitorActivityEnabled(terminalId, checked);
577 }
578 
handleToggleTerminalMonitorSilence(bool checked)579 void MainWindow::handleToggleTerminalMonitorSilence(bool checked)
580 {
581     QAction *action = qobject_cast<QAction *>(QObject::sender());
582 
583     if (!action || action->data().isNull())
584         return;
585 
586     bool ok = false;
587     int terminalId = action->data().toInt(&ok);
588     if (!ok)
589         return;
590 
591     m_sessionStack->setTerminalMonitorSilenceEnabled(terminalId, checked);
592 }
593 
handleTerminalActivity(Terminal * terminal)594 void MainWindow::handleTerminalActivity(Terminal *terminal)
595 {
596     Session *session = qobject_cast<Session *>(sender());
597 
598     if (session) {
599         disconnect(terminal, SIGNAL(activityDetected(Terminal *)), session, SIGNAL(activityDetected(Terminal *)));
600 
601         QString message(xi18nc("@info", "Activity detected in monitored terminal in session \"%1\".", m_tabBar->tabTitle(session->id())));
602 
603         KNotification::event(QLatin1String("activity"), message, QPixmap(), terminal->partWidget(), KNotification::CloseWhenWidgetActivated);
604     }
605 }
606 
handleTerminalSilence(Terminal * terminal)607 void MainWindow::handleTerminalSilence(Terminal *terminal)
608 {
609     Session *session = qobject_cast<Session *>(sender());
610 
611     if (session) {
612         QString message(xi18nc("@info", "Silence detected in monitored terminal in session \"%1\".", m_tabBar->tabTitle(session->id())));
613 
614         KNotification::event(QLatin1String("silence"), message, QPixmap(), terminal->partWidget(), KNotification::CloseWhenWidgetActivated);
615     }
616 }
617 
handleLastTabClosed()618 void MainWindow::handleLastTabClosed()
619 {
620     if (isVisible() && !Settings::keepOpenAfterLastSessionCloses())
621         toggleWindowState();
622 }
623 
handleSwitchToAction()624 void MainWindow::handleSwitchToAction()
625 {
626     QAction *action = qobject_cast<QAction *>(QObject::sender());
627 
628     if (action && !action->data().isNull())
629         m_sessionStack->raiseSession(m_tabBar->sessionAtTab(action->data().toInt()));
630 }
631 
handleToggleTitlebar()632 void MainWindow::handleToggleTitlebar()
633 {
634     auto toggleFunc = [this]() {
635         bool showTitleBar = !Settings::showTitleBar();
636         m_titleBar->setVisible(showTitleBar);
637         Settings::setShowTitleBar(showTitleBar);
638         Settings::self()->save();
639         applyWindowGeometry();
640     };
641 
642     if (Settings::showTitleBar()) { // If the title bar is hidden don't ask if toggling is ok
643 
644         const char *message =
645             "You are about to hide the title bar. This will keep you "
646             "from accessing the settings menu via the mouse. To show "
647             "the title bar again press the keyboard shortcut (default "
648             "Ctrl+Shift+m) or access the settings menu via keyborad "
649             "shortcut (defult: Ctrl+Shift+,).";
650 
651         const int result = KMessageBox::warningContinueCancel(this,
652                                                               xi18nc("@info", message),
653                                                               xi18nc("@title:window", "Hiding Title Bar"),
654                                                               KStandardGuiItem::cont(),
655                                                               KStandardGuiItem::cancel(),
656                                                               QStringLiteral("hinding_title_bar"));
657 
658         if (result == KMessageBox::ButtonCode::Continue) {
659             toggleFunc();
660         }
661     } else {
662         toggleFunc();
663     }
664 }
665 
setupMenu()666 void MainWindow::setupMenu()
667 {
668     m_menu->insertSection(nullptr, xi18nc("@title:menu", "Help"));
669     m_menu->addAction(actionCollection()->action(QLatin1String(KStandardAction::name(KStandardAction::WhatsThis))));
670     m_menu->addAction(actionCollection()->action(QLatin1String(KStandardAction::name(KStandardAction::ReportBug))));
671     m_menu->addAction(actionCollection()->action(QLatin1String(KStandardAction::name(KStandardAction::AboutApp))));
672     m_menu->addAction(actionCollection()->action(QLatin1String(KStandardAction::name(KStandardAction::AboutKDE))));
673 
674     m_menu->insertSection(nullptr, xi18nc("@title:menu", "Quick Options"));
675     m_menu->addAction(actionCollection()->action(QStringLiteral("view-full-screen")));
676     m_menu->addAction(actionCollection()->action(QStringLiteral("keep-open")));
677 
678     m_screenMenu = new QMenu(this);
679     connect(m_screenMenu, SIGNAL(triggered(QAction *)), this, SLOT(setScreen(QAction *)));
680     m_screenMenu->setTitle(xi18nc("@title:menu", "Screen"));
681     m_menu->addMenu(m_screenMenu);
682 
683     m_windowWidthMenu = new QMenu(this);
684     connect(m_windowWidthMenu, SIGNAL(triggered(QAction *)), this, SLOT(setWindowWidth(QAction *)));
685     m_windowWidthMenu->setTitle(xi18nc("@title:menu", "Width"));
686     m_menu->addMenu(m_windowWidthMenu);
687 
688     m_windowHeightMenu = new QMenu(this);
689     connect(m_windowHeightMenu, SIGNAL(triggered(QAction *)), this, SLOT(setWindowHeight(QAction *)));
690     m_windowHeightMenu->setTitle(xi18nc("@title:menu", "Height"));
691     m_menu->addMenu(m_windowHeightMenu);
692 
693     m_menu->insertSection(nullptr, xi18nc("@title:menu", "Settings"));
694     m_menu->addAction(actionCollection()->action(QStringLiteral("manage-profiles")));
695     m_menu->addAction(actionCollection()->action(QLatin1String(KStandardAction::name(KStandardAction::KeyBindings))));
696     m_menu->addAction(actionCollection()->action(QLatin1String(KStandardAction::name(KStandardAction::ConfigureNotifications))));
697     m_menu->addAction(actionCollection()->action(QLatin1String(KStandardAction::name(KStandardAction::Preferences))));
698 
699     m_menu->addSeparator();
700     m_menu->addAction(actionCollection()->action(QLatin1String(KStandardAction::name(KStandardAction::Quit))));
701 }
702 
updateScreenMenu()703 void MainWindow::updateScreenMenu()
704 {
705     QAction *action;
706 
707     m_screenMenu->clear();
708 
709     action = m_screenMenu->addAction(xi18nc("@item:inmenu", "At mouse location"));
710     action->setCheckable(true);
711     action->setData(0);
712     action->setChecked(Settings::screen() == 0);
713 
714     for (int i = 1; i <= QGuiApplication::screens().count(); i++) {
715         action = m_screenMenu->addAction(xi18nc("@item:inmenu", "Screen %1", i));
716         action->setCheckable(true);
717         action->setData(i);
718         action->setChecked(i == Settings::screen());
719     }
720 
721     action = m_screenMenu->menuAction();
722     action->setVisible(QGuiApplication::screens().count() > 1);
723 }
724 
updateWindowSizeMenus()725 void MainWindow::updateWindowSizeMenus()
726 {
727     updateWindowWidthMenu();
728     updateWindowHeightMenu();
729 }
730 
updateWindowWidthMenu()731 void MainWindow::updateWindowWidthMenu()
732 {
733     QAction *action = nullptr;
734 
735     if (m_windowWidthMenu->isEmpty()) {
736         for (int i = 10; i <= 100; i += 10) {
737             action = m_windowWidthMenu->addAction(QString::number(i) + QStringLiteral("%"));
738             action->setCheckable(true);
739             action->setData(i);
740             action->setChecked(i == Settings::width());
741         }
742     } else {
743         QListIterator<QAction *> i(m_windowWidthMenu->actions());
744 
745         while (i.hasNext()) {
746             action = i.next();
747 
748             action->setChecked(action->data().toInt() == Settings::width());
749         }
750     }
751 }
752 
updateWindowHeightMenu()753 void MainWindow::updateWindowHeightMenu()
754 {
755     QAction *action = nullptr;
756 
757     if (m_windowHeightMenu->isEmpty()) {
758         for (int i = 10; i <= 100; i += 10) {
759             action = m_windowHeightMenu->addAction(QString::number(i) + QStringLiteral("%"));
760             action->setCheckable(true);
761             action->setData(i);
762             action->setChecked(i == Settings::height());
763         }
764     } else {
765         QListIterator<QAction *> i(m_windowHeightMenu->actions());
766 
767         while (i.hasNext()) {
768             action = i.next();
769 
770             action->setChecked(action->data().toInt() == Settings::height());
771         }
772     }
773 }
774 
configureKeys()775 void MainWindow::configureKeys()
776 {
777     KShortcutsDialog dialog(KShortcutsEditor::AllActions, KShortcutsEditor::LetterShortcutsAllowed, this);
778     dialog.addCollection(actionCollection());
779 
780     const auto collections = m_sessionStack->getPartActionCollections();
781 
782     if (collections.size() >= 1) {
783         dialog.addCollection(collections.at(0), QStringLiteral("Konsolepart"));
784     }
785 
786     if (!dialog.configure()) {
787         return;
788     }
789 
790     if (collections.size() >= 1) {
791         // We need to update all the other collections
792         // rootCollection is the collection which got updatet by the dialog
793         const auto rootCollection = collections.at(0);
794 
795         // For all the other collections
796         for (auto i = 1; i < collections.size(); ++i) {
797             // Update all the action they share with rootCollection
798             const auto rootActions = rootCollection->actions();
799             for (const auto *action : rootActions) {
800                 if (auto *destaction = collections.at(i)->action(action->objectName())) {
801                     destaction->setShortcuts(action->shortcuts());
802                 }
803             }
804         }
805     }
806 }
807 
configureNotifications()808 void MainWindow::configureNotifications()
809 {
810     KNotifyConfigWidget::configure(this);
811 }
812 
configureApp()813 void MainWindow::configureApp()
814 {
815     if (KConfigDialog::showDialog(QStringLiteral("settings")))
816         return;
817 
818     KConfigDialog *settingsDialog = new KConfigDialog(this, QStringLiteral("settings"), Settings::self());
819     settingsDialog->setMinimumHeight(560);
820     settingsDialog->setFaceType(KPageDialog::List);
821     connect(settingsDialog, &KConfigDialog::settingsChanged, this, &MainWindow::applySettings);
822 
823     WindowSettings *windowSettings = new WindowSettings(settingsDialog);
824     settingsDialog->addPage(windowSettings, xi18nc("@title Preferences page name", "Window"), QStringLiteral("preferences-system-windows-move"));
825     connect(windowSettings, SIGNAL(updateWindowGeometry(int, int, int)), this, SLOT(setWindowGeometry(int, int, int)));
826 
827     QWidget *behaviorSettings = new QWidget(settingsDialog);
828     Ui::BehaviorSettings behaviorSettingsUi;
829     behaviorSettingsUi.setupUi(behaviorSettings);
830     settingsDialog->addPage(behaviorSettings, xi18nc("@title Preferences page name", "Behavior"), QStringLiteral("preferences-system-windows-actions"));
831 
832     AppearanceSettings *appearanceSettings = new AppearanceSettings(settingsDialog);
833     settingsDialog->addPage(appearanceSettings, xi18nc("@title Preferences page name", "Appearance"), QStringLiteral("preferences-desktop-theme"));
834     connect(settingsDialog, &QDialog::rejected, appearanceSettings, &AppearanceSettings::resetSelection);
835 
836     settingsDialog->button(QDialogButtonBox::Help)->hide();
837     settingsDialog->button(QDialogButtonBox::Cancel)->setFocus();
838 
839     connect(settingsDialog, &QDialog::finished, [=]() {
840         m_toggleLock = true;
841         KWindowSystem::activateWindow(winId());
842         KWindowSystem::forceActiveWindow(winId());
843     });
844 
845     settingsDialog->show();
846 }
847 
applySettings()848 void MainWindow::applySettings()
849 {
850     if (Settings::dynamicTabTitles()) {
851         connect(m_sessionStack, SIGNAL(titleChanged(int, QString)), m_tabBar, SLOT(setTabTitleAutomated(int, QString)));
852 
853         m_sessionStack->emitTitles();
854     } else {
855         disconnect(m_sessionStack, SIGNAL(titleChanged(int, QString)), m_tabBar, SLOT(setTabTitleAutomated(int, QString)));
856     }
857 
858     m_animationTimer.setInterval(Settings::frames() ? 10 : 0);
859 
860     m_tabBar->setVisible(Settings::showTabBar());
861     m_titleBar->setVisible(Settings::showTitleBar());
862 
863     if (!Settings::showSystrayIcon() && m_notifierItem) {
864         delete m_notifierItem;
865         m_notifierItem = nullptr;
866 
867         // Removing the notifier item deletes the menu
868         // add a new one
869         m_menu = new QMenu(this);
870         setupMenu();
871         m_titleBar->updateMenu();
872     } else if (Settings::showSystrayIcon() && !m_notifierItem) {
873         m_notifierItem = new KStatusNotifierItem(this);
874         m_notifierItem->setStandardActionsEnabled(false);
875         m_notifierItem->setIconByName(QStringLiteral("yakuake"));
876         m_notifierItem->setStatus(KStatusNotifierItem::Active);
877         m_notifierItem->setContextMenu(m_menu);
878 
879         // Prevent the default implementation of showing
880         // and instead run toggleWindowState
881         m_notifierItem->setAssociatedWidget(nullptr);
882         connect(m_notifierItem, &KStatusNotifierItem::activateRequested, this, &MainWindow::toggleWindowState);
883         updateTrayTooltip();
884     }
885 
886     repaint(); // used to repaint skin borders if Settings::hideSkinBorders has been changed
887 
888     setKeepOpen(Settings::keepOpen());
889 
890     updateScreenMenu();
891     updateWindowSizeMenus();
892 
893     updateUseTranslucency();
894 
895     applySkin();
896     applyWindowGeometry();
897     applyWindowProperties();
898 }
899 
applySkin()900 void MainWindow::applySkin()
901 {
902     bool gotSkin = m_skin->load(Settings::skin(), Settings::skinInstalledWithKns());
903 
904     if (!gotSkin) {
905         Settings::setSkin(QStringLiteral("default"));
906         gotSkin = m_skin->load(Settings::skin());
907     }
908 
909     if (!gotSkin) {
910         KMessageBox::error(parentWidget(),
911                            xi18nc("@info",
912                                   "<application>Yakuake</application> was unable to load a skin. It is likely that it was installed incorrectly.<nl/><nl/>"
913                                   "The application will now quit."),
914                            xi18nc("@title:window", "Cannot Load Skin"));
915 
916         QMetaObject::invokeMethod(qApp, "quit", Qt::QueuedConnection);
917     }
918 
919     m_titleBar->applySkin();
920     m_tabBar->applySkin();
921 }
922 
applyWindowProperties()923 void MainWindow::applyWindowProperties()
924 {
925     if (Settings::keepOpen() && !Settings::keepAbove()) {
926         KWindowSystem::clearState(winId(), NET::KeepAbove);
927         KWindowSystem::setState(winId(), NET::Sticky | NET::SkipTaskbar | NET::SkipPager);
928     } else
929         KWindowSystem::setState(winId(), NET::KeepAbove | NET::Sticky | NET::SkipTaskbar | NET::SkipPager);
930 
931     KWindowSystem::setOnAllDesktops(winId(), Settings::showOnAllDesktops());
932     KWindowEffects::enableBlurBehind(windowHandle(), Settings::blur());
933 }
934 
applyWindowGeometry()935 void MainWindow::applyWindowGeometry()
936 {
937     int width, height;
938 
939     QAction *action = actionCollection()->action(QStringLiteral("view-full-screen"));
940 
941     if (action->isChecked()) {
942         width = 100;
943         height = 100;
944     } else {
945         width = Settings::width();
946         height = Settings::height();
947     }
948 
949     setWindowGeometry(width, height, Settings::position());
950 }
951 
setWindowGeometry(int newWidth,int newHeight,int newPosition)952 void MainWindow::setWindowGeometry(int newWidth, int newHeight, int newPosition)
953 {
954     QRect workArea = getDesktopGeometry();
955 
956     int maxHeight = workArea.height() * newHeight / 100;
957 
958     int targetWidth = workArea.width() * newWidth / 100;
959 
960     setGeometry(workArea.x() + workArea.width() * newPosition * (100 - newWidth) / 10000, workArea.y(), targetWidth, maxHeight);
961 #if HAVE_KWAYLAND
962     initWaylandSurface();
963 #endif
964 
965     maxHeight -= m_titleBar->height();
966     m_titleBar->setGeometry(0, maxHeight, targetWidth, m_titleBar->height());
967     if (!isVisible())
968         m_titleBar->updateMask();
969 
970     if (Settings::frames() > 0)
971         m_animationStepSize = maxHeight / Settings::frames();
972     else
973         m_animationStepSize = maxHeight;
974 
975     auto borderWidth = Settings::hideSkinBorders() ? 0 : m_skin->borderWidth();
976 
977     if (Settings::showTabBar()) {
978         if (m_skin->tabBarCompact()) {
979             m_tabBar->setGeometry(m_skin->tabBarLeft(), maxHeight, width() - m_skin->tabBarLeft() - m_skin->tabBarRight(), m_tabBar->height());
980         } else {
981             maxHeight -= m_tabBar->height();
982             m_tabBar->setGeometry(borderWidth, maxHeight - borderWidth, width() - 2 * borderWidth, m_tabBar->height());
983         }
984     }
985 
986     m_sessionStack->setGeometry(borderWidth, 0, width() - 2 * borderWidth, maxHeight - borderWidth);
987 
988     updateMask();
989 }
990 
setScreen(QAction * action)991 void MainWindow::setScreen(QAction *action)
992 {
993     Settings::setScreen(action->data().toInt());
994     Settings::self()->save();
995 
996     applyWindowGeometry();
997 
998     updateScreenMenu();
999 }
1000 
setWindowWidth(int width)1001 void MainWindow::setWindowWidth(int width)
1002 {
1003     Settings::setWidth(width);
1004     Settings::self()->save();
1005 
1006     applyWindowGeometry();
1007 
1008     updateWindowWidthMenu();
1009 }
1010 
setWindowHeight(int height)1011 void MainWindow::setWindowHeight(int height)
1012 {
1013     Settings::setHeight(height);
1014     Settings::self()->save();
1015 
1016     applyWindowGeometry();
1017 
1018     updateWindowHeightMenu();
1019 }
1020 
setWindowWidth(QAction * action)1021 void MainWindow::setWindowWidth(QAction *action)
1022 {
1023     setWindowWidth(action->data().toInt());
1024 }
1025 
setWindowHeight(QAction * action)1026 void MainWindow::setWindowHeight(QAction *action)
1027 {
1028     setWindowHeight(action->data().toInt());
1029 }
1030 
increaseWindowWidth()1031 void MainWindow::increaseWindowWidth()
1032 {
1033     if (Settings::width() <= 90)
1034         setWindowWidth(Settings::width() + 10);
1035 }
1036 
decreaseWindowWidth()1037 void MainWindow::decreaseWindowWidth()
1038 {
1039     if (Settings::width() >= 20)
1040         setWindowWidth(Settings::width() - 10);
1041 }
1042 
increaseWindowHeight()1043 void MainWindow::increaseWindowHeight()
1044 {
1045     if (Settings::height() <= 90)
1046         setWindowHeight(Settings::height() + 10);
1047 }
1048 
decreaseWindowHeight()1049 void MainWindow::decreaseWindowHeight()
1050 {
1051     if (Settings::height() >= 20)
1052         setWindowHeight(Settings::height() - 10);
1053 }
1054 
updateMask()1055 void MainWindow::updateMask()
1056 {
1057     QRegion region = m_titleBar->mask();
1058 
1059     region.translate(0, m_titleBar->y());
1060 
1061     region += QRegion(0, 0, width(), m_titleBar->y());
1062 
1063     setMask(region);
1064 }
1065 
paintEvent(QPaintEvent * event)1066 void MainWindow::paintEvent(QPaintEvent *event)
1067 {
1068     QPainter painter(this);
1069 
1070     if (useTranslucency()) {
1071         painter.setOpacity(qreal(Settings::backgroundColorOpacity()) / 100);
1072         painter.fillRect(rect(), Settings::backgroundColor());
1073         painter.setOpacity(1.0);
1074     } else
1075         painter.fillRect(rect(), Settings::backgroundColor());
1076 
1077     if (!Settings::hideSkinBorders()) {
1078         const QRect leftBorder(0, 0, m_skin->borderWidth(), height() - m_titleBar->height());
1079         painter.fillRect(leftBorder, m_skin->borderColor());
1080 
1081         const QRect rightBorder(width() - m_skin->borderWidth(), 0, m_skin->borderWidth(), height() - m_titleBar->height());
1082         painter.fillRect(rightBorder, m_skin->borderColor());
1083 
1084         const QRect bottomBorder(0, height() - m_skin->borderWidth() - m_titleBar->height(), width(), m_skin->borderWidth());
1085         painter.fillRect(bottomBorder, m_skin->borderColor());
1086     }
1087 
1088     KMainWindow::paintEvent(event);
1089 }
1090 
moveEvent(QMoveEvent * event)1091 void MainWindow::moveEvent(QMoveEvent *event)
1092 {
1093     if (Settings::screen() && QApplication::desktop()->screenNumber(this) != getScreen()) {
1094         Settings::setScreen(QApplication::desktop()->screenNumber(this) + 1);
1095 
1096         updateScreenMenu();
1097 
1098         applyWindowGeometry();
1099     }
1100 
1101     KMainWindow::moveEvent(event);
1102 }
1103 
closeEvent(QCloseEvent * event)1104 void MainWindow::closeEvent(QCloseEvent *event)
1105 {
1106     KMainWindow::closeEvent(event);
1107     if (event->isAccepted()) {
1108         QApplication::quit();
1109     }
1110 }
1111 
wmActiveWindowChanged()1112 void MainWindow::wmActiveWindowChanged()
1113 {
1114     if (m_toggleLock) {
1115         m_toggleLock = false;
1116         return;
1117     }
1118 
1119     KWindowInfo info(KWindowSystem::activeWindow(), {}, NET::WM2TransientFor);
1120 
1121     if (info.valid() && info.transientFor() == winId()) {
1122         return;
1123     }
1124 
1125     if (!Settings::keepOpen() && isVisible() && !isActiveWindow()) {
1126         toggleWindowState();
1127     }
1128 }
1129 
changeEvent(QEvent * event)1130 void MainWindow::changeEvent(QEvent *event)
1131 {
1132     if (event->type() == QEvent::WindowStateChange && !m_isFullscreen) {
1133         if (windowState().testFlag(Qt::WindowMaximized)) {
1134             // Don't alter settings to new size so unmaximizing restores previous geometry.
1135             setWindowGeometry(100, 100, Settings::position());
1136             setWindowState(Qt::WindowMaximized);
1137         } else {
1138             setWindowGeometry(Settings::width(), Settings::height(), Settings::position());
1139         }
1140     }
1141 
1142     KMainWindow::changeEvent(event);
1143 }
1144 
focusNextPrevChild(bool)1145 bool MainWindow::focusNextPrevChild(bool)
1146 {
1147     return false;
1148 }
1149 
toggleWindowState()1150 void MainWindow::toggleWindowState()
1151 {
1152     if (m_isWayland) {
1153         auto message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.plasmashell"),
1154                                                       QStringLiteral("/StrutManager"),
1155                                                       QStringLiteral("org.kde.PlasmaShell.StrutManager"),
1156                                                       QStringLiteral("availableScreenRect"));
1157         message.setArguments({QGuiApplication::screens().at(getScreen())->name()});
1158         QDBusPendingCall call = QDBusConnection::sessionBus().asyncCall(message);
1159         QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call, this);
1160 
1161         QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, [=]() {
1162             QDBusPendingReply<QRect> reply = *watcher;
1163             m_availableScreenRect = reply.isValid() ? reply.value() : QRect();
1164             setWindowGeometry(Settings::width(), Settings::height(), Settings::position());
1165             watcher->deleteLater();
1166         });
1167 
1168         _toggleWindowState();
1169     } else {
1170         _toggleWindowState();
1171     }
1172 }
1173 
_toggleWindowState()1174 void MainWindow::_toggleWindowState()
1175 {
1176     bool visible = isVisible();
1177 
1178     if (visible && KWindowSystem::activeWindow() != winId() && Settings::keepOpen()) {
1179         // Window is open but doesn't have focus; it's set to stay open
1180         // regardless of focus loss.
1181 
1182         if (Settings::toggleToFocus()) {
1183             // The open/retract action is set to focus the window when it's
1184             // open but lacks focus. The following will cause it to receive
1185             // focus, and in an environment with multiple virtual desktops
1186             // will also cause the window manager to switch to the virtual
1187             // desktop the window resides on.
1188 
1189             KWindowSystem::activateWindow(winId());
1190             KWindowSystem::forceActiveWindow(winId());
1191 
1192             return;
1193         } else if (!Settings::showOnAllDesktops() && KWindowInfo(winId(), NET::WMDesktop).desktop() != KWindowSystem::currentDesktop()) {
1194             // The open/restrict action isn't set to focus the window, but
1195             // the window is currently on another virtual desktop (the option
1196             // to show it on all of them is disabled), so closing it doesn't
1197             // make sense and we're opting to show it instead to avoid
1198             // requiring the user to invoke the action twice to get to see
1199             // Yakuake. Just forcing focus would cause the window manager to
1200             // switch to the virtual desktop the window currently resides on,
1201             // so move the window to the current desktop before doing so.
1202 
1203             KWindowSystem::setOnDesktop(winId(), KWindowSystem::currentDesktop());
1204 
1205             KWindowSystem::activateWindow(winId());
1206             KWindowSystem::forceActiveWindow(winId());
1207 
1208             return;
1209         }
1210     }
1211 
1212 #if HAVE_X11
1213     if (!Settings::useWMAssist() && m_kwinAssistPropSet)
1214         kwinAssistPropCleanup();
1215 
1216     if (m_isX11 && Settings::useWMAssist() && KWindowSystem::compositingActive())
1217         kwinAssistToggleWindowState(visible);
1218     else
1219 #endif
1220         if (!m_isWayland) {
1221         xshapeToggleWindowState(visible);
1222     } else {
1223         if (visible) {
1224             sharedPreHideWindow();
1225 
1226             hide();
1227 
1228             sharedAfterHideWindow();
1229         } else {
1230             sharedPreOpenWindow();
1231             if (KWindowEffects::isEffectAvailable(KWindowEffects::Slide)) {
1232                 KWindowEffects::slideWindow(windowHandle(), KWindowEffects::TopEdge);
1233             }
1234 
1235             show();
1236 
1237             sharedAfterOpenWindow();
1238         }
1239     }
1240 }
1241 
1242 #if HAVE_X11
kwinAssistToggleWindowState(bool visible)1243 void MainWindow::kwinAssistToggleWindowState(bool visible)
1244 {
1245     bool gotEffect = false;
1246 
1247     Display *display = QX11Info::display();
1248     Atom atom = XInternAtom(display, "_KDE_SLIDE", false);
1249     int count;
1250     Atom *list = XListProperties(display, DefaultRootWindow(display), &count);
1251 
1252     if (list != nullptr) {
1253         gotEffect = (std::find(list, list + count, atom) != list + count);
1254 
1255         XFree(list);
1256     }
1257 
1258     if (gotEffect) {
1259         Atom atom = XInternAtom(display, "_KDE_SLIDE", false);
1260 
1261         if (Settings::frames() > 0) {
1262             QVarLengthArray<long, 1024> data(4);
1263 
1264             data[0] = 0;
1265             data[1] = 1;
1266             data[2] = Settings::frames() * 10;
1267             data[3] = Settings::frames() * 10;
1268 
1269             XChangeProperty(display, winId(), atom, atom, 32, PropModeReplace, reinterpret_cast<unsigned char *>(data.data()), data.size());
1270 
1271             m_kwinAssistPropSet = true;
1272         } else
1273             XDeleteProperty(display, winId(), atom);
1274 
1275         if (visible) {
1276             sharedPreHideWindow();
1277 
1278             hide();
1279 
1280             sharedAfterHideWindow();
1281         } else {
1282             sharedPreOpenWindow();
1283 
1284             show();
1285 
1286             sharedAfterOpenWindow();
1287         }
1288 
1289         return;
1290     }
1291 
1292     // Fall back to legacy animation strategy if kwin doesn't have the
1293     // effect loaded.
1294     xshapeToggleWindowState(visible);
1295 }
1296 
kwinAssistPropCleanup()1297 void MainWindow::kwinAssistPropCleanup()
1298 {
1299     if (!QX11Info::isPlatformX11())
1300         return;
1301 
1302     Display *display = QX11Info::display();
1303     Atom atom = XInternAtom(display, "_KDE_SLIDE", false);
1304 
1305     XDeleteProperty(display, winId(), atom);
1306 
1307     m_kwinAssistPropSet = false;
1308 }
1309 #endif
1310 
xshapeToggleWindowState(bool visible)1311 void MainWindow::xshapeToggleWindowState(bool visible)
1312 {
1313     if (m_animationTimer.isActive())
1314         return;
1315 
1316     if (visible) {
1317         sharedPreHideWindow();
1318 
1319         m_animationFrame = Settings::frames();
1320 
1321         connect(&m_animationTimer, SIGNAL(timeout()), this, SLOT(xshapeRetractWindow()));
1322         m_animationTimer.start();
1323     } else {
1324         m_animationFrame = 0;
1325 
1326         connect(&m_animationTimer, SIGNAL(timeout()), this, SLOT(xshapeOpenWindow()));
1327         m_animationTimer.start();
1328     }
1329 }
1330 
xshapeOpenWindow()1331 void MainWindow::xshapeOpenWindow()
1332 {
1333     if (m_animationFrame == 0) {
1334         sharedPreOpenWindow();
1335 
1336         show();
1337 
1338         sharedAfterOpenWindow();
1339     }
1340 
1341     if (m_animationFrame == Settings::frames()) {
1342         m_animationTimer.stop();
1343         m_animationTimer.disconnect();
1344 
1345         m_titleBar->move(0, height() - m_titleBar->height());
1346         updateMask();
1347     } else {
1348         int maskHeight = m_animationStepSize * m_animationFrame;
1349 
1350         QRegion newMask = m_titleBar->mask();
1351         newMask.translate(0, maskHeight);
1352         newMask += QRegion(0, 0, width(), maskHeight);
1353 
1354         m_titleBar->move(0, maskHeight);
1355         setMask(newMask);
1356 
1357         m_animationFrame++;
1358     }
1359 }
1360 
xshapeRetractWindow()1361 void MainWindow::xshapeRetractWindow()
1362 {
1363     if (m_animationFrame == 0) {
1364         m_animationTimer.stop();
1365         m_animationTimer.disconnect();
1366 
1367         hide();
1368 
1369         sharedAfterHideWindow();
1370     } else {
1371         m_titleBar->move(0, m_titleBar->y() - m_animationStepSize);
1372         setMask(QRegion(mask()).translated(0, -m_animationStepSize));
1373 
1374         --m_animationFrame;
1375     }
1376 }
1377 
sharedPreOpenWindow()1378 void MainWindow::sharedPreOpenWindow()
1379 {
1380     applyWindowGeometry();
1381 
1382     updateUseTranslucency();
1383 
1384     if (Settings::pollMouse())
1385         toggleMousePoll(false);
1386     if (Settings::rememberFullscreen())
1387         setFullScreen(m_isFullscreen);
1388 }
1389 
sharedAfterOpenWindow()1390 void MainWindow::sharedAfterOpenWindow()
1391 {
1392     if (!Settings::firstRun())
1393         KWindowSystem::forceActiveWindow(winId());
1394 
1395     connect(qGuiApp, &QGuiApplication::focusWindowChanged, this, &MainWindow::wmActiveWindowChanged);
1396 
1397     applyWindowProperties();
1398 
1399 #if HAVE_KWAYLAND
1400     initWaylandSurface();
1401 #endif
1402 
1403     Q_EMIT windowOpened();
1404 }
1405 
sharedPreHideWindow()1406 void MainWindow::sharedPreHideWindow()
1407 {
1408     disconnect(qGuiApp, &QGuiApplication::focusWindowChanged, this, &MainWindow::wmActiveWindowChanged);
1409 }
1410 
sharedAfterHideWindow()1411 void MainWindow::sharedAfterHideWindow()
1412 {
1413     if (Settings::pollMouse())
1414         toggleMousePoll(true);
1415 
1416 #if HAVE_KWAYLAND
1417     delete m_plasmaShellSurface;
1418     m_plasmaShellSurface = nullptr;
1419 #endif
1420 
1421     Q_EMIT windowClosed();
1422 }
1423 
activate()1424 void MainWindow::activate()
1425 {
1426     KWindowSystem::activateWindow(winId());
1427 }
1428 
toggleMousePoll(bool poll)1429 void MainWindow::toggleMousePoll(bool poll)
1430 {
1431     if (poll)
1432         m_mousePoller.start(Settings::pollInterval());
1433     else
1434         m_mousePoller.stop();
1435 }
1436 
pollMouse()1437 void MainWindow::pollMouse()
1438 {
1439     QPoint pos = QCursor::pos();
1440     QRect workArea = getDesktopGeometry();
1441 
1442     int windowX = workArea.x() + workArea.width() * Settings::position() * (100 - Settings::width()) / 10000;
1443     int windowWidth = workArea.width() * Settings::width() / 100;
1444 
1445     if (pos.y() == 0 && pos.x() >= windowX && pos.x() <= (windowX + windowWidth))
1446         toggleWindowState();
1447 }
1448 
setKeepOpen(bool keepOpen)1449 void MainWindow::setKeepOpen(bool keepOpen)
1450 {
1451     if (Settings::keepOpen() != keepOpen) {
1452         Settings::setKeepOpen(keepOpen);
1453         Settings::self()->save();
1454 
1455         applyWindowProperties();
1456     }
1457 
1458     actionCollection()->action(QStringLiteral("keep-open"))->setChecked(keepOpen);
1459     m_titleBar->setFocusButtonState(keepOpen);
1460 }
1461 
setFullScreen(bool state)1462 void MainWindow::setFullScreen(bool state)
1463 {
1464     if (isVisible())
1465         m_isFullscreen = state;
1466     if (state) {
1467         setWindowState(windowState() | Qt::WindowFullScreen);
1468         setWindowGeometry(100, 100, Settings::position());
1469     } else {
1470         setWindowState(windowState() & ~Qt::WindowFullScreen);
1471         setWindowGeometry(Settings::width(), Settings::height(), Settings::position());
1472     }
1473 }
1474 
getScreen()1475 int MainWindow::getScreen()
1476 {
1477     if (!Settings::screen() || Settings::screen() > QGuiApplication::screens().length()) {
1478         // Right after unplugging an external monitor and the Yakuake window was on
1479         // that monitor, QGuiApplication::screenAt() can return nullptr so we fallback on
1480         // the first monitor.
1481         QScreen *screen = QGuiApplication::screenAt(QCursor::pos());
1482         return screen ? QGuiApplication::screens().indexOf(screen) : 0;
1483     } else {
1484         return Settings::screen() - 1;
1485     }
1486 }
1487 
getScreenGeometry()1488 QRect MainWindow::getScreenGeometry()
1489 {
1490     QScreen *screen = QGuiApplication::screens().at(getScreen());
1491     QRect screenGeometry = screen->geometry();
1492     screenGeometry.moveTo(screenGeometry.topLeft() / screen->devicePixelRatio());
1493     return screenGeometry;
1494 }
1495 
getDesktopGeometry()1496 QRect MainWindow::getDesktopGeometry()
1497 {
1498     QRect screenGeometry = getScreenGeometry();
1499 
1500     QAction *action = actionCollection()->action(QStringLiteral("view-full-screen"));
1501 
1502     if (action->isChecked())
1503         return screenGeometry;
1504 
1505     if (m_isWayland) {
1506         // on Wayland it's not possible to get the work area from KWindowSystem
1507         // but plasmashell provides this through dbus
1508         return m_availableScreenRect.isValid() ? m_availableScreenRect : screenGeometry;
1509     }
1510 
1511     if (QGuiApplication::screens().count() > 1) {
1512         const QList<WId> allWindows = KWindowSystem::windows();
1513         QList<WId> offScreenWindows;
1514 
1515         QListIterator<WId> i(allWindows);
1516 
1517         while (i.hasNext()) {
1518             WId windowId = i.next();
1519 
1520             if (KWindowSystem::hasWId(windowId)) {
1521                 KWindowInfo windowInfo = KWindowInfo(windowId, NET::WMDesktop | NET::WMGeometry, NET::WM2ExtendedStrut);
1522 
1523                 // If windowInfo is valid and the window is located at the same (current)
1524                 // desktop with the yakuake window...
1525                 if (windowInfo.valid() && windowInfo.isOnCurrentDesktop()) {
1526                     NETExtendedStrut strut = windowInfo.extendedStrut();
1527 
1528                     // Get the area covered by each strut.
1529                     QRect topStrut(strut.top_start, 0, strut.top_end - strut.top_start, strut.top_width);
1530                     QRect bottomStrut(strut.bottom_start,
1531                                       screenGeometry.bottom() - strut.bottom_width,
1532                                       strut.bottom_end - strut.bottom_start,
1533                                       strut.bottom_width);
1534                     QRect leftStrut(0, strut.left_start, strut.left_width, strut.left_end - strut.left_start);
1535                     QRect rightStrut(screenGeometry.right() - strut.right_width, strut.right_start, strut.right_width, strut.right_end - strut.right_start);
1536 
1537                     // If the window has no strut, no need to bother further.
1538                     if (topStrut.isEmpty() && bottomStrut.isEmpty() && leftStrut.isEmpty() && rightStrut.isEmpty())
1539                         continue;
1540 
1541                     // If any of the strut and the window itself intersects with our screen geometry,
1542                     // it will be correctly handled by workArea(). If the window doesn't intersect
1543                     // with our screen geometry it's most likely a plasma panel and can/should be
1544                     // ignored
1545                     if ((topStrut.intersects(screenGeometry) || bottomStrut.intersects(screenGeometry) || leftStrut.intersects(screenGeometry)
1546                          || rightStrut.intersects(screenGeometry))
1547                         && windowInfo.geometry().intersects(screenGeometry)) {
1548                         continue;
1549                     }
1550 
1551                     // This window has a strut on the same desktop as us but which does not cover our screen
1552                     // geometry. It should be ignored, otherwise the returned work area will wrongly include
1553                     // the strut.
1554                     offScreenWindows << windowId;
1555                 }
1556             }
1557         }
1558 
1559         return KWindowSystem::workArea(offScreenWindows).intersected(screenGeometry);
1560     }
1561 
1562     return KWindowSystem::workArea();
1563 }
1564 
whatsThis()1565 void MainWindow::whatsThis()
1566 {
1567     QWhatsThis::enterWhatsThisMode();
1568 }
1569 
showFirstRunDialog()1570 void MainWindow::showFirstRunDialog()
1571 {
1572     if (!m_firstRunDialog) {
1573         m_firstRunDialog = new FirstRunDialog(this);
1574 
1575         connect(m_firstRunDialog, &QDialog::finished, this, &MainWindow::firstRunDialogFinished);
1576         connect(m_firstRunDialog, &QDialog::accepted, this, &MainWindow::firstRunDialogOk);
1577     }
1578 
1579     m_firstRunDialog->show();
1580 }
1581 
firstRunDialogFinished()1582 void MainWindow::firstRunDialogFinished()
1583 {
1584     Settings::setFirstRun(false);
1585     Settings::self()->save();
1586 
1587     m_firstRunDialog->deleteLater();
1588 
1589     KWindowSystem::forceActiveWindow(winId());
1590 }
1591 
firstRunDialogOk()1592 void MainWindow::firstRunDialogOk()
1593 {
1594     QAction *action = static_cast<QAction *>(actionCollection()->action(QStringLiteral("toggle-window-state")));
1595 
1596     KGlobalAccel::self()->setShortcut(action, QList<QKeySequence>() << m_firstRunDialog->keySequence(), KGlobalAccel::NoAutoloading);
1597 
1598     actionCollection()->writeSettings();
1599 }
1600 
updateUseTranslucency()1601 void MainWindow::updateUseTranslucency()
1602 {
1603     m_useTranslucency = (Settings::translucency() && KWindowSystem::compositingActive());
1604 }
1605 
updateTrayTooltip()1606 void MainWindow::updateTrayTooltip()
1607 {
1608     if (!m_notifierItem) {
1609         return;
1610     }
1611 
1612     auto *action = actionCollection()->action(QStringLiteral("toggle-window-state"));
1613     const QList<QKeySequence> &shortcuts = KGlobalAccel::self()->shortcut(action);
1614     if (!shortcuts.isEmpty()) {
1615         const QString shortcut(shortcuts.first().toString(QKeySequence::NativeText));
1616         m_notifierItem->setToolTip(QStringLiteral("yakuake"), QStringLiteral("Yakuake"), xi18nc("@info", "Press <shortcut>%1</shortcut> to open", shortcut));
1617     }
1618 }
1619