File indexing completed on 2024-05-12 05:14:52

0001 /*
0002  *  messagewindow.cpp  -  displays an alarm message in a window
0003  *  Program:  kalarm
0004  *  SPDX-FileCopyrightText: 2001-2024 David Jarvie <djarvie@kde.org>
0005  *
0006  *  SPDX-License-Identifier: GPL-2.0-or-later
0007  */
0008 
0009 #include "messagewindow.h"
0010 #include "messagedisplayhelper.h"
0011 #include "config-kalarm.h"
0012 
0013 #include "deferdlg.h"
0014 #include "editdlg.h"
0015 #include "functions.h"
0016 #include "kalarmapp.h"
0017 #include "mainwindow.h"
0018 #include "preferences.h"
0019 #include "resourcescalendar.h"
0020 #include "lib/config.h"
0021 #include "lib/desktop.h"
0022 #include "lib/file.h"
0023 #include "lib/messagebox.h"
0024 #include "lib/pushbutton.h"
0025 #include "akonadiplugin/akonadiplugin.h"
0026 #include "kalarm_debug.h"
0027 
0028 #include <KAboutData>
0029 #include <KStandardGuiItem>
0030 #include <KLocalizedString>
0031 #include <KConfigGroup>
0032 #include <KTextEdit>
0033 #include <KSqueezedTextLabel>
0034 #include <KWindowSystem>
0035 #if ENABLE_X11
0036 #include <KX11Extras>
0037 #include <KWindowInfo>
0038 #include <netwm.h>
0039 #include <QGuiApplication>
0040 #endif
0041 #include <QTextBrowser>
0042 #include <QScrollBar>
0043 #include <QCheckBox>
0044 #include <QLabel>
0045 #include <QPalette>
0046 #include <QTimer>
0047 #include <QFrame>
0048 #include <QGridLayout>
0049 #include <QVBoxLayout>
0050 #include <QHBoxLayout>
0051 #include <QResizeEvent>
0052 #include <QCloseEvent>
0053 #include <QScreen>
0054 #include <QStyle>
0055 
0056 #include "kmailinterface.h"
0057 
0058 using namespace KAlarmCal;
0059 
0060 namespace
0061 {
0062 
0063 enum FullScreenType { NoFullScreen = 0, FullScreen = 1, FullScreenActive = 2 };
0064 FullScreenType haveFullScreenWindow(int screen);
0065 FullScreenType findFullScreenWindows(const QList<QRect>& screenRects, QList<FullScreenType>& screenTypes);
0066 
0067 const QLatin1String KMAIL_DBUS_SERVICE("org.kde.kmail");
0068 const QLatin1String KMAIL_DBUS_PATH("/KMail");
0069 
0070 // The delay for enabling message window buttons if a zero delay is
0071 // configured, i.e. the windows are placed far from the cursor.
0072 const int proximityButtonDelay = 1000;    // (milliseconds)
0073 const int proximityMultiple = 10;         // multiple of button height distance from cursor for proximity
0074 
0075 // Basic flags for the window
0076 const Qt::WindowFlags     WFLAGS      = Qt::WindowStaysOnTopHint;
0077 const Qt::WindowFlags     WFLAGS2     = Qt::WindowContextHelpButtonHint;
0078 const Qt::WidgetAttribute WidgetFlags = Qt::WA_DeleteOnClose;
0079 
0080 } // namespace
0081 
0082 
0083 // A text label widget which can be scrolled and copied with the mouse.
0084 class MessageText : public KTextEdit
0085 {
0086 public:
0087     MessageText(QWidget* parent = nullptr)
0088         : KTextEdit(parent)
0089     {
0090         setReadOnly(true);
0091         setFrameStyle(NoFrame);
0092         setLineWrapMode(NoWrap);
0093     }
0094     int scrollBarHeight() const     { return horizontalScrollBar()->height(); }
0095     int scrollBarWidth() const      { return verticalScrollBar()->width(); }
0096     void setBackgroundColour(const QColor& c)
0097     {
0098         QPalette pal = viewport()->palette();
0099         pal.setColor(viewport()->backgroundRole(), c);
0100         viewport()->setPalette(pal);
0101     }
0102     QSize sizeHint() const override
0103     {
0104         const QSizeF docsize = document()->size();
0105         return {static_cast<int>(docsize.width() + 0.99) + verticalScrollBar()->width(),
0106                 static_cast<int>(docsize.height() + 0.99) + horizontalScrollBar()->height()};
0107     }
0108 };
0109 
0110 QList<MessageWindow*> MessageWindow::mWindowList;
0111 
0112 /******************************************************************************
0113 * Construct the message window for the specified alarm.
0114 * Other alarms in the supplied event may have been updated by the caller, so
0115 * the whole event needs to be stored for updating the calendar file when it is
0116 * displayed.
0117 */
0118 MessageWindow::MessageWindow(const KAEvent& event, const KAAlarm& alarm, int flags)
0119     : MainWindowBase(nullptr, static_cast<Qt::WindowFlags>(WFLAGS | WFLAGS2 | ((flags & AlwaysHide) || getWorkAreaAndModal() ? Qt::WindowType(0) : Qt::X11BypassWindowManagerHint)))
0120     , MessageDisplay(event, alarm, flags)
0121     , mRestoreHeight(0)
0122 {
0123     qCDebug(KALARM_LOG) << "MessageWindow():" << mEventId();
0124     setAttribute(static_cast<Qt::WidgetAttribute>(WidgetFlags));
0125     setWindowModality(Qt::WindowModal);
0126     setObjectName(QLatin1StringView("MessageWindow"));    // used by LikeBack
0127     if (!(flags & (NoInitView | AlwaysHide)))
0128         MessageWindow::setUpDisplay();   // avoid calling virtual method from constructor
0129 
0130     connect(mHelper, &MessageDisplayHelper::textsChanged, this, &MessageWindow::textsChanged);
0131     connect(mHelper, &MessageDisplayHelper::commandExited, this, &MessageWindow::commandCompleted);
0132 
0133     // Set to save settings automatically, but don't save window size.
0134     // File alarm window size is saved elsewhere.
0135     setAutoSaveSettings(QStringLiteral("MessageWindow"), false);
0136     mWindowList.append(this);
0137     if (mAlwaysHidden())
0138     {
0139         hide();
0140         displayComplete();    // play audio, etc.
0141     }
0142 }
0143 
0144 /******************************************************************************
0145 * Construct the message window for a specified error message.
0146 * If 'dontShowAgain' is non-null, a "Don't show again" option is displayed. Note
0147 * that the option is specific to 'event'.
0148 */
0149 MessageWindow::MessageWindow(const KAEvent& event, const DateTime& alarmDateTime,
0150                        const QStringList& errmsgs, const QString& dontShowAgain)
0151     : MainWindowBase(nullptr, WFLAGS | WFLAGS2)
0152     , MessageDisplay(event, alarmDateTime, errmsgs, dontShowAgain)
0153     , mRestoreHeight(0)
0154 {
0155     qCDebug(KALARM_LOG) << "MessageWindow(errmsg)";
0156     setAttribute(static_cast<Qt::WidgetAttribute>(WidgetFlags));
0157     setWindowModality(Qt::WindowModal);
0158     setObjectName(QLatin1StringView("ErrorWin"));    // used by LikeBack
0159     getWorkAreaAndModal();
0160     MessageWindow::setUpDisplay();   // avoid calling virtual method from constructor
0161 
0162     connect(mHelper, &MessageDisplayHelper::textsChanged, this, &MessageWindow::textsChanged);
0163 
0164     mWindowList.append(this);
0165 }
0166 
0167 /******************************************************************************
0168 * Construct the message window for restoration by session management.
0169 * The window is initialised by readProperties().
0170 */
0171 MessageWindow::MessageWindow()
0172     : MainWindowBase(nullptr, WFLAGS)
0173     , MessageDisplay()
0174 {
0175     qCDebug(KALARM_LOG) << "MessageWindow(): restore";
0176     setAttribute(WidgetFlags);
0177     setWindowModality(Qt::WindowModal);
0178     setObjectName(QLatin1StringView("RestoredMsgWin"));    // used by LikeBack
0179     getWorkAreaAndModal();
0180 
0181     connect(mHelper, &MessageDisplayHelper::textsChanged, this, &MessageWindow::textsChanged);
0182     connect(mHelper, &MessageDisplayHelper::commandExited, this, &MessageWindow::commandCompleted);
0183 
0184     mWindowList.append(this);
0185 }
0186 
0187 /******************************************************************************
0188 * Destructor. Perform any post-alarm actions before tidying up.
0189 */
0190 MessageWindow::~MessageWindow()
0191 {
0192     qCDebug(KALARM_LOG) << "~MessageWindow" << (void*)this << mEventId();
0193     mWindowList.removeAll(this);
0194 }
0195 
0196 /******************************************************************************
0197 * Construct the message window.
0198 */
0199 void MessageWindow::setUpDisplay()
0200 {
0201     mHelper->initTexts();
0202     MessageDisplayHelper::DisplayTexts texts = mHelper->texts();
0203 
0204     const bool reminder = (!mErrorWindow()  &&  (mAlarmType() & KAAlarm::Type::Reminder));
0205     const int leading = fontMetrics().leading();
0206     setCaption(texts.title);
0207     QWidget* topWidget = new QWidget(this);
0208     setCentralWidget(topWidget);
0209     auto topLayout = new QVBoxLayout(topWidget);
0210     const int dcmLeft   = style()->pixelMetric(QStyle::PM_LayoutLeftMargin);
0211     const int dcmTop    = style()->pixelMetric(QStyle::PM_LayoutTopMargin);
0212     const int dcmRight  = style()->pixelMetric(QStyle::PM_LayoutRightMargin);
0213     const int dcmBottom = style()->pixelMetric(QStyle::PM_LayoutBottomMargin);
0214 
0215     QPalette labelPalette = palette();
0216     labelPalette.setColor(backgroundRole(), labelPalette.color(QPalette::Window));
0217 
0218     // Show the alarm date/time, together with a reminder text where appropriate.
0219     // Alarm date/time: display time zone if not local time zone.
0220     mTimeLabel = new QLabel(topWidget);
0221     mTimeLabel->setText(texts.timeFull);
0222     mTimeLabel->setFrameStyle(QFrame::StyledPanel);
0223     mTimeLabel->setPalette(labelPalette);
0224     mTimeLabel->setAutoFillBackground(true);
0225     mTimeLabel->setAlignment(Qt::AlignHCenter);
0226     topLayout->addWidget(mTimeLabel, 0, Qt::AlignHCenter);
0227     mTimeLabel->setWhatsThis(i18nc("@info:whatsthis", "The scheduled date/time for the message (as opposed to the actual time of display)."));
0228     if (texts.timeFull.isEmpty())
0229         mTimeLabel->hide();
0230 
0231     if (!mErrorWindow())
0232     {
0233         // It's a normal alarm message window
0234         switch (mAction())
0235         {
0236             case KAEvent::SubAction::File:
0237             {
0238                 // Display the file name
0239                 auto label = new KSqueezedTextLabel(texts.fileName, topWidget);
0240                 label->setFrameStyle(QFrame::StyledPanel);
0241                 label->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
0242                 label->setPalette(labelPalette);
0243                 label->setAutoFillBackground(true);
0244                 label->setWhatsThis(i18nc("@info:whatsthis", "The file whose contents are displayed below"));
0245                 topLayout->addWidget(label, 0, Qt::AlignHCenter);
0246 
0247                 if (mErrorMsgs().isEmpty())
0248                 {
0249                     // Display contents of file
0250                     auto view = new QTextBrowser(topWidget);
0251                     view->setFrameStyle(QFrame::NoFrame);
0252                     view->setWordWrapMode(QTextOption::NoWrap);
0253                     QPalette pal = view->viewport()->palette();
0254                     pal.setColor(view->viewport()->backgroundRole(), mBgColour());
0255                     view->viewport()->setPalette(pal);
0256                     view->setTextColor(mFgColour());
0257                     view->setCurrentFont(mFont());
0258 
0259                     switch (texts.fileType)
0260                     {
0261                         case File::Type::Image:
0262                         case File::Type::TextFormatted:
0263                             view->setHtml(texts.message);
0264                             break;
0265                         default:
0266                             view->setPlainText(texts.message);
0267                             break;
0268                     }
0269                     view->setMinimumSize(view->sizeHint());
0270                     topLayout->addWidget(view);
0271 
0272                     // Set the default size to 20 lines square.
0273                     // Note that after the first file has been displayed, this size
0274                     // is overridden by the user-set default stored in the config file.
0275                     // So there is no need to calculate an accurate size.
0276                     int h = 20*view->fontMetrics().lineSpacing() + 2*view->frameWidth();
0277                     view->resize(QSize(h, h).expandedTo(view->sizeHint()));
0278                     view->setWhatsThis(i18nc("@info:whatsthis", "The contents of the file to be displayed"));
0279                 }
0280                 break;
0281             }
0282             case KAEvent::SubAction::Message:
0283             {
0284                 // Message label
0285                 // Using MessageText instead of QLabel allows scrolling and mouse copying
0286                 auto text = new MessageText(topWidget);
0287                 text->setAutoFillBackground(true);
0288                 text->setBackgroundColour(mBgColour());
0289                 text->setTextColor(mFgColour());
0290                 text->setCurrentFont(mFont());
0291                 text->insertPlainText(texts.message);
0292                 const int lineSpacing = text->fontMetrics().lineSpacing();
0293                 const QSize s = text->sizeHint();
0294                 const int h = s.height();
0295                 text->setMaximumHeight(h + text->scrollBarHeight());
0296                 text->setMinimumHeight(qMin(h, lineSpacing*4));
0297                 text->setMaximumWidth(s.width() + text->scrollBarWidth());
0298                 text->setWhatsThis(i18nc("@info:whatsthis", "The alarm message"));
0299                 const int vspace = lineSpacing/2;
0300                 const int hspace = lineSpacing - (dcmLeft + dcmRight)/2;
0301                 topLayout->addSpacing(vspace);
0302                 topLayout->addStretch();
0303                 // Don't include any horizontal margins if message is 2/3 screen width
0304                 if (text->sizeHint().width() >= Desktop::workArea(mScreenNumber).width()*2/3)
0305                     topLayout->addWidget(text, 1, Qt::AlignHCenter);
0306                 else
0307                 {
0308                     auto layout = new QHBoxLayout();
0309                     layout->addSpacing(hspace);
0310                     layout->addWidget(text, 1, Qt::AlignHCenter);
0311                     layout->addSpacing(hspace);
0312                     topLayout->addLayout(layout);
0313                 }
0314                 if (!reminder)
0315                     topLayout->addStretch();
0316                 break;
0317             }
0318             case KAEvent::SubAction::Command:
0319             {
0320                 mCommandText = new MessageText(topWidget);
0321                 mCommandText->setBackgroundColour(mBgColour());
0322                 mCommandText->setTextColor(mFgColour());
0323                 mCommandText->setCurrentFont(mFont());
0324                 topLayout->addWidget(mCommandText);
0325                 mCommandText->setWhatsThis(i18nc("@info:whatsthis", "The output of the alarm's command"));
0326                 mCommandText->setPlainText(texts.message);
0327                 break;
0328             }
0329             case KAEvent::SubAction::Email:
0330             default:
0331                 break;
0332         }
0333 
0334         if (!texts.remainingTime.isEmpty())
0335         {
0336             // Advance reminder: show remaining time until the actual alarm
0337             mRemainingText = new QLabel(topWidget);
0338             mRemainingText->setFrameStyle(QFrame::Box | QFrame::Raised);
0339             mRemainingText->setContentsMargins(leading, leading, leading, leading);
0340             mRemainingText->setPalette(labelPalette);
0341             mRemainingText->setAutoFillBackground(true);
0342             mRemainingText->setText(texts.remainingTime);
0343             topLayout->addWidget(mRemainingText, 0, Qt::AlignHCenter);
0344             topLayout->addSpacing(style()->pixelMetric(QStyle::PM_LayoutVerticalSpacing));
0345             topLayout->addStretch();
0346         }
0347     }
0348     else
0349     {
0350         // It's an error message
0351         switch (mAction())
0352         {
0353             case KAEvent::SubAction::Email:
0354             {
0355                 // Display the email addresses and subject.
0356                 QFrame* frame = new QFrame(topWidget);
0357                 frame->setFrameStyle(QFrame::Box | QFrame::Raised);
0358                 frame->setWhatsThis(i18nc("@info:whatsthis", "The email to send"));
0359                 topLayout->addWidget(frame, 0, Qt::AlignHCenter);
0360                 auto grid = new QGridLayout(frame);
0361 
0362                 QLabel* label = new QLabel(texts.errorEmail[0], frame);
0363                 grid->addWidget(label, 0, 0, Qt::AlignLeft);
0364                 label = new QLabel(texts.errorEmail[1], frame);
0365                 grid->addWidget(label, 0, 1, Qt::AlignLeft);
0366 
0367                 label = new QLabel(texts.errorEmail[2], frame);
0368                 grid->addWidget(label, 1, 0, Qt::AlignLeft);
0369                 label = new QLabel(texts.errorEmail[3], frame);
0370                 grid->addWidget(label, 1, 1, Qt::AlignLeft);
0371                 break;
0372             }
0373             case KAEvent::SubAction::Command:
0374             case KAEvent::SubAction::File:
0375             case KAEvent::SubAction::Message:
0376             default:
0377                 // Just display the error message strings
0378                 break;
0379         }
0380     }
0381 
0382     if (mErrorMsgs().isEmpty())
0383     {
0384         topWidget->setAutoFillBackground(true);
0385         QPalette palette = topWidget->palette();
0386         palette.setColor(topWidget->backgroundRole(), mBgColour());
0387         topWidget->setPalette(palette);
0388     }
0389     else
0390     {
0391         auto layout = new QHBoxLayout();
0392         layout->setContentsMargins(2 * dcmLeft, 2 * dcmTop, 2 * dcmRight, 2 * dcmBottom);
0393         layout->addStretch();
0394         topLayout->addLayout(layout);
0395         QLabel* label = new QLabel(topWidget);
0396         label->setPixmap(QIcon::fromTheme(QStringLiteral("dialog-error")).pixmap(style()->pixelMetric(QStyle::PM_MessageBoxIconSize)));
0397         layout->addWidget(label, 0, Qt::AlignRight);
0398         auto vlayout = new QVBoxLayout();
0399         layout->addLayout(vlayout);
0400         for (const QString& msg : mErrorMsgs())
0401         {
0402             label = new QLabel(msg, topWidget);
0403             vlayout->addWidget(label, 0, Qt::AlignLeft);
0404         }
0405         layout->addStretch();
0406         if (!mDontShowAgain().isEmpty())
0407         {
0408             mDontShowAgainCheck = new QCheckBox(i18nc("@option:check", "Do not display this error message again for this alarm"), topWidget);
0409             topLayout->addWidget(mDontShowAgainCheck, 0, Qt::AlignLeft);
0410         }
0411     }
0412 
0413     auto grid = new QGridLayout();
0414     grid->setColumnStretch(0, 1);     // keep the buttons right-adjusted in the window
0415     topLayout->addLayout(grid);
0416     int gridIndex = 1;
0417 
0418     // Close button
0419     mOkButton = new PushButton(KStandardGuiItem::close(), topWidget);
0420     // Prevent accidental acknowledgement of the message if the user is typing when the window appears
0421     mOkButton->clearFocus();
0422     mOkButton->setFocusPolicy(Qt::ClickFocus);    // don't allow keyboard selection
0423     connect(mOkButton, &QAbstractButton::clicked, this, &MessageWindow::slotOk);
0424     grid->addWidget(mOkButton, 0, gridIndex++, Qt::AlignHCenter);
0425     mOkButton->setWhatsThis(i18nc("@info:whatsthis", "Acknowledge the alarm"));
0426 
0427     if (mShowEdit())
0428     {
0429         // Edit button
0430         mEditButton = new PushButton(i18nc("@action:button", "Edit..."), topWidget);
0431         mEditButton->setFocusPolicy(Qt::ClickFocus);    // don't allow keyboard selection
0432         connect(mEditButton, &QAbstractButton::clicked, this, &MessageWindow::slotEdit);
0433         grid->addWidget(mEditButton, 0, gridIndex++, Qt::AlignHCenter);
0434         mEditButton->setToolTip(i18nc("@info:tooltip", "Edit the alarm"));
0435         mEditButton->setWhatsThis(i18nc("@info:whatsthis", "Edit the alarm."));
0436     }
0437 
0438     // Defer button
0439     mDeferButton = new PushButton(i18nc("@action:button", "Defer..."), topWidget);
0440     mDeferButton->setFocusPolicy(Qt::ClickFocus);    // don't allow keyboard selection
0441     connect(mDeferButton, &QAbstractButton::clicked, this, &MessageWindow::slotDefer);
0442     grid->addWidget(mDeferButton, 0, gridIndex++, Qt::AlignHCenter);
0443     mDeferButton->setToolTip(i18nc("@info:tooltip", "Defer the alarm until later"));
0444     mDeferButton->setWhatsThis(xi18nc("@info:whatsthis", "<para>Defer the alarm until later.</para>"
0445                                     "<para>You will be prompted to specify when the alarm should be redisplayed.</para>"));
0446 
0447     if (mNoDefer())
0448         mDeferButton->hide();
0449     else
0450         mHelper->setDeferralLimit(mEvent());    // ensure that button is disabled when alarm can't be deferred any more
0451 
0452     if (!mAudioFile().isEmpty()  &&  (mVolume() || mFadeVolume() > 0))
0453     {
0454         // Silence button to stop sound repetition
0455         mSilenceButton = new PushButton(topWidget);
0456         mSilenceButton->setIcon(QIcon::fromTheme(QStringLiteral("media-playback-stop")));
0457         grid->addWidget(mSilenceButton, 0, gridIndex++, Qt::AlignHCenter);
0458         mSilenceButton->setToolTip(i18nc("@info:tooltip", "Stop sound"));
0459         mSilenceButton->setWhatsThis(i18nc("@info:whatsthis", "Stop playing the sound"));
0460         // To avoid getting in a mess, disable the button until sound playing has been set up
0461         mSilenceButton->setEnabled(false);
0462 
0463         mHelper->setSilenceButton(mSilenceButton);
0464     }
0465 
0466     if (mEmailId() >= 0  &&  Preferences::useAkonadi())
0467     {
0468         // KMail button
0469         mKMailButton = new PushButton(topWidget);
0470         mKMailButton->setIcon(QIcon::fromTheme(QStringLiteral("internet-mail")));
0471         connect(mKMailButton, &QAbstractButton::clicked, this, &MessageWindow::slotShowKMailMessage);
0472         grid->addWidget(mKMailButton, 0, gridIndex++, Qt::AlignHCenter);
0473         mKMailButton->setToolTip(xi18nc("@info:tooltip Locate this email in KMail", "Locate in <application>KMail</application>"));
0474         mKMailButton->setWhatsThis(xi18nc("@info:whatsthis", "Locate and highlight this email in <application>KMail</application>"));
0475     }
0476 
0477     // KAlarm button
0478     mKAlarmButton = new PushButton(topWidget);
0479     mKAlarmButton->setIcon(QIcon::fromTheme(KAboutData::applicationData().componentName()));
0480     connect(mKAlarmButton, &QAbstractButton::clicked, this, &MessageWindow::displayMainWindow);
0481     grid->addWidget(mKAlarmButton, 0, gridIndex++, Qt::AlignHCenter);
0482     mKAlarmButton->setToolTip(xi18nc("@info:tooltip", "Activate <application>KAlarm</application>"));
0483     mKAlarmButton->setWhatsThis(xi18nc("@info:whatsthis", "Activate <application>KAlarm</application>"));
0484 
0485     int butsize = mKAlarmButton->sizeHint().height();
0486     if (mSilenceButton)
0487         butsize = qMax(butsize, mSilenceButton->sizeHint().height());
0488     if (mKMailButton)
0489         butsize = qMax(butsize, mKMailButton->sizeHint().height());
0490     mKAlarmButton->setFixedSize(butsize, butsize);
0491     if (mSilenceButton)
0492         mSilenceButton->setFixedSize(butsize, butsize);
0493     if (mKMailButton)
0494         mKMailButton->setFixedSize(butsize, butsize);
0495 
0496     // Disable all buttons initially, to prevent accidental clicking on if they happen to be
0497     // under the mouse just as the window appears.
0498     mOkButton->setEnabled(false);
0499     if (mDeferButton->isVisible())
0500         mDeferButton->setEnabled(false);
0501     if (mEditButton)
0502         mEditButton->setEnabled(false);
0503     if (mKMailButton)
0504         mKMailButton->setEnabled(false);
0505     mKAlarmButton->setEnabled(false);
0506 
0507     topLayout->activate();
0508     setMinimumSize(QSize(grid->sizeHint().width() + dcmLeft + dcmRight,
0509                          sizeHint().height()));
0510 #if ENABLE_X11
0511     if (KWindowSystem::isPlatformX11())
0512     {
0513         const bool modal = !(windowFlags() & Qt::X11BypassWindowManagerHint);
0514         NET::States wstate = NET::Sticky | NET::KeepAbove;
0515         if (modal)
0516             wstate |= NET::Modal;
0517         WId winid = winId();
0518         KX11Extras::setState(winid, wstate);
0519         KX11Extras::setOnAllDesktops(winid, true);   // show on all virtual desktops
0520     }
0521 #endif
0522 
0523     mInitialised = true;   // the window's widgets have been created
0524 }
0525 
0526 /******************************************************************************
0527 * Return the number of message windows, optionally excluding always-hidden ones.
0528 */
0529 int MessageWindow::windowCount(bool excludeAlwaysHidden)
0530 {
0531     int count = mWindowList.count();
0532     if (excludeAlwaysHidden)
0533     {
0534         for (MessageWindow* win : std::as_const(mWindowList))
0535         {
0536             if (win->mAlwaysHidden())
0537                 --count;
0538         }
0539     }
0540     return count;
0541 }
0542 
0543 /******************************************************************************
0544 * Returns the widget to act as parent for error messages, etc.
0545 */
0546 QWidget* MessageWindow::displayParent()
0547 {
0548     return this;
0549 }
0550 
0551 void MessageWindow::closeDisplay()
0552 {
0553     close();
0554 }
0555 
0556 void MessageWindow::showDisplay()
0557 {
0558     if (!mAlwaysHidden())
0559         display();
0560 }
0561 
0562 void MessageWindow::raiseDisplay()
0563 {
0564     raise();
0565 }
0566 
0567 /******************************************************************************
0568 * Raise the alarm window, re-output any required audio notification, and
0569 * reschedule the alarm in the calendar file.
0570 */
0571 void MessageWindow::repeat(const KAAlarm& alarm)
0572 {
0573     if (!mInitialised)
0574         return;
0575     if (mDeferData)
0576     {
0577         // Cancel any deferral dialog so that the user notices something's going on,
0578         // and also because the deferral time limit will have changed.
0579         delete mDeferData;
0580         mDeferData = nullptr;
0581     }
0582     if (mEventId().isEmpty())
0583         return;
0584     KAEvent event = ResourcesCalendar::event(mEventId());
0585     if (event.isValid())
0586     {
0587         mAlarmType() = alarm.type();    // store new alarm type for use if it is later deferred
0588         if (mAlwaysHidden())
0589             playAudio();
0590         else
0591         {
0592             if (!mDeferData  ||  Preferences::modalMessages())
0593             {
0594                 raise();
0595                 playAudio();
0596             }
0597             if (mDeferButton->isVisible())
0598             {
0599                 mDeferButton->setEnabled(true);
0600                 mHelper->setDeferralLimit(event);    // ensure that button is disabled when alarm can't be deferred any more
0601             }
0602         }
0603         if (mHelper->alarmShowing(event))
0604             ResourcesCalendar::updateEvent(event);
0605     }
0606 }
0607 
0608 bool MessageWindow::hasDefer() const
0609 {
0610     return mDeferButton && mDeferButton->isVisible();
0611 }
0612 
0613 /******************************************************************************
0614 * Show the Defer button when it was previously hidden.
0615 */
0616 void MessageWindow::showDefer()
0617 {
0618     if (mDeferButton)
0619     {
0620         mNoDefer() = false;
0621         mDeferButton->show();
0622         mHelper->setDeferralLimit(mEvent());    // ensure that button is disabled when alarm can't be deferred any more
0623         resize(sizeHint());
0624     }
0625 }
0626 
0627 /******************************************************************************
0628 * Convert a reminder window into a normal alarm window.
0629 */
0630 void MessageWindow::cancelReminder(const KAEvent& event, const KAAlarm& alarm)
0631 {
0632     if (mHelper->cancelReminder(event, alarm))
0633     {
0634         const MessageDisplayHelper::DisplayTexts& texts = mHelper->texts();
0635         setCaption(texts.title);
0636         mTimeLabel->setText(texts.timeFull);
0637         if (mRemainingText)
0638             mRemainingText->hide();
0639         setMinimumHeight(0);
0640         centralWidget()->layout()->activate();
0641         setMinimumHeight(sizeHint().height());
0642         resize(sizeHint());
0643     }
0644 }
0645 
0646 /******************************************************************************
0647 * Update and show the alarm's trigger time.
0648 * This is assumed to have previously been hidden.
0649 */
0650 void MessageWindow::showDateTime(const KAEvent& event, const KAAlarm& alarm)
0651 {
0652     if (mTimeLabel  &&  mHelper->updateDateTime(event, alarm))
0653     {
0654         mTimeLabel->setText(mHelper->texts().timeFull);
0655         mTimeLabel->show();
0656     }
0657 }
0658 
0659 /******************************************************************************
0660 * Called when the texts to display have changed.
0661 */
0662 void MessageWindow::textsChanged(MessageDisplayHelper::DisplayTexts::TextIds ids, const QString& change)
0663 {
0664     const MessageDisplayHelper::DisplayTexts& texts = mHelper->texts();
0665 
0666     if (ids & MessageDisplayHelper::DisplayTexts::Title)
0667         setCaption(texts.title);
0668 
0669     if (ids & MessageDisplayHelper::DisplayTexts::TimeFull)
0670         mTimeLabel->setText(texts.timeFull);
0671 
0672     if (ids & MessageDisplayHelper::DisplayTexts::RemainingTime)
0673     {
0674         if (mRemainingText)
0675         {
0676             if (texts.remainingTime.isEmpty())
0677                 mRemainingText->hide();
0678             else
0679                 mRemainingText->setText(texts.remainingTime);
0680         }
0681     }
0682 
0683     if (ids & MessageDisplayHelper::DisplayTexts::MessageAppend)
0684     {
0685         // More output is available from the command which is providing the text
0686         // for this window. Add the output and resize the window to show it.
0687         mCommandText->insertPlainText(change);
0688         resize(sizeHint());
0689     }
0690 }
0691 
0692 /******************************************************************************
0693 * Called when the command providing the alarm message text has exited.
0694 * 'success' is true if the command did not fail completely.
0695 */
0696 void MessageWindow::commandCompleted(bool success)
0697 {
0698     if (!success)
0699     {
0700         // The command failed completely. KAlarmApp will output an error
0701         // message, so delete the empty window.
0702         close();
0703     }
0704 }
0705 
0706 /******************************************************************************
0707 * Save settings to the session managed config file, for restoration
0708 * when the program is restored.
0709 */
0710 void MessageWindow::saveProperties(KConfigGroup& config)
0711 {
0712     if (mShown  &&  mHelper->saveProperties(config))
0713         config.writeEntry("Height", height());
0714 }
0715 
0716 /******************************************************************************
0717 * Read settings from the session managed config file.
0718 * This function is automatically called whenever the app is being restored.
0719 * Read in whatever was saved in saveProperties().
0720 */
0721 void MessageWindow::readProperties(const KConfigGroup& config)
0722 {
0723     mRestoreHeight = config.readEntry("Height", 0);
0724 
0725     if (mHelper->readProperties(config))
0726     {
0727         // The retrieved alarm was shown by this class, and we need to initialise
0728         // its display.
0729         setUpDisplay();
0730     }
0731 }
0732 
0733 /******************************************************************************
0734 * Spread alarm windows over the screen so that they are all visible, or pile
0735 * them on top of each other again.
0736 * Reply = true if windows are now scattered, false if piled up.
0737 */
0738 bool MessageWindow::spread(bool scatter)
0739 {
0740     if (KWindowSystem::isPlatformWayland())
0741         return false;    // Wayland doesn't allow positioning of windows
0742 
0743     if (windowCount(true) <= 1)    // ignore always-hidden windows
0744         return false;
0745 
0746     const QRect desk = Desktop::workArea();   // get the usable area of the desktop
0747     if (scatter == isSpread(desk.topLeft()))
0748         return scatter;
0749 
0750     if (scatter)
0751     {
0752         // Usually there won't be many windows, so a crude
0753         // scattering algorithm should suffice.
0754         int x = desk.left();
0755         int y = desk.top();
0756         int ynext = y;
0757         for (int errmsgs = 0;  errmsgs < 2;  ++errmsgs)
0758         {
0759             // Display alarm messages first, then error messages, since most
0760             // error messages tend to be the same height.
0761             for (MessageWindow* w : std::as_const(mWindowList))
0762             {
0763                 if ((!errmsgs && w->mErrorWindow())
0764                 ||  (errmsgs && !w->mErrorWindow()))
0765                     continue;
0766                 const QSize sz = w->frameGeometry().size();
0767                 if (x + sz.width() > desk.right())
0768                 {
0769                     x = desk.left();
0770                     y = ynext;
0771                 }
0772                 int ytmp = y;
0773                 if (y + sz.height() > desk.bottom())
0774                 {
0775                     ytmp = desk.bottom() - sz.height();
0776                     if (ytmp < desk.top())
0777                         ytmp = desk.top();
0778                 }
0779                 w->move(x, ytmp);
0780                 x += sz.width();
0781                 if (ytmp + sz.height() > ynext)
0782                     ynext = ytmp + sz.height();
0783             }
0784         }
0785     }
0786     else
0787     {
0788         // Move all windows to the top left corner
0789         for (MessageWindow* w : std::as_const(mWindowList))
0790             w->move(desk.topLeft());
0791     }
0792     return scatter;
0793 }
0794 
0795 /******************************************************************************
0796 * Check whether message windows are all piled up, or are spread out.
0797 * Reply = true if windows are currently spread, false if piled up.
0798 */
0799 bool MessageWindow::isSpread(const QPoint& topLeft)
0800 {
0801     for (MessageWindow* w : std::as_const(mWindowList))
0802     {
0803         if (w->pos() != topLeft)
0804             return true;
0805     }
0806     return false;
0807 }
0808 
0809 /******************************************************************************
0810 * Display the window, if it should not already be auto-closed.
0811 * If windows are being positioned away from the mouse cursor, it is initially
0812 * positioned at the top left to slightly reduce the number of times the
0813 * windows need to be moved in showEvent().
0814 */
0815 void MessageWindow::display()
0816 {
0817     connect(mHelper, &MessageDisplayHelper::autoCloseNow, this, &QWidget::close);
0818     if (mHelper->activateAutoClose())
0819     {
0820         if (Preferences::messageButtonDelay() == 0)
0821             move(0, 0);
0822         MainWindowBase::show();
0823         // Ensure that the screen wakes from sleep, in case the window manager
0824         // doesn't do this when the window is displayed.
0825         mHelper->wakeScreen();
0826     }
0827 }
0828 
0829 /******************************************************************************
0830 * Returns the window's recommended size exclusive of its frame.
0831 */
0832 QSize MessageWindow::sizeHint() const
0833 {
0834     QSize desired;
0835     switch (mAction())
0836     {
0837         case KAEvent::SubAction::Message:
0838             desired = MainWindowBase::sizeHint();
0839             break;
0840         case KAEvent::SubAction::Command:
0841             if (mShown)
0842             {
0843                 // For command output, expand the window to accommodate the text
0844                 const QSize texthint = mCommandText->sizeHint();
0845                 int w = texthint.width() + style()->pixelMetric(QStyle::PM_LayoutLeftMargin)
0846                                          + style()->pixelMetric(QStyle::PM_LayoutRightMargin);
0847                 if (w < width())
0848                     w = width();
0849                 const int ypadding = height() - mCommandText->height();
0850                 desired = QSize(w, texthint.height() + ypadding);
0851                 break;
0852             }
0853             // fall through to default
0854             [[fallthrough]];
0855         default:
0856             return MainWindowBase::sizeHint();
0857     }
0858 
0859     // Limit the size to fit inside the working area of the desktop
0860     const QSize desktop = Desktop::workArea(mScreenNumber).size();
0861     const QSize frameThickness = frameGeometry().size() - geometry().size();  // title bar & window frame
0862     return desired.boundedTo(desktop - frameThickness);
0863 }
0864 
0865 /******************************************************************************
0866 * Called when the window is shown.
0867 * The first time, output any required audio notification, and reschedule or
0868 * delete the event from the calendar file.
0869 */
0870 void MessageWindow::showEvent(QShowEvent* se)
0871 {
0872     MainWindowBase::showEvent(se);
0873     if (mShown  ||  !mInitialised)
0874         return;
0875     if (mErrorWindow()  ||  mAlarmType() == KAAlarm::Type::Invalid)
0876     {
0877         // Don't bother repositioning error messages,
0878         // and invalid alarms should be deleted anyway.
0879         enableButtons();
0880     }
0881     else
0882     {
0883         /* Set the window size.
0884          * Note that the frame thickness is not yet known when this
0885          * method is called, so for large windows the size needs to be
0886          * set again later.
0887          */
0888         bool execComplete = true;
0889         QSize s = sizeHint();     // fit the window round the message
0890         if (mAction() == KAEvent::SubAction::File  &&  mErrorMsgs().isEmpty())
0891             Config::readWindowSize("FileMessage", s);
0892         resize(s);
0893 
0894         const QRect desk = Desktop::workArea(mScreenNumber);
0895         const QRect frame = frameGeometry();
0896 
0897         mButtonDelay = Preferences::messageButtonDelay() * 1000;
0898         if (mButtonDelay)
0899         {
0900             // Position the window in the middle of the screen, and
0901             // delay enabling the buttons.
0902             mPositioning = true;
0903             move((desk.width() - frame.width())/2, (desk.height() - frame.height())/2);
0904             execComplete = false;
0905         }
0906         else
0907         {
0908             /* Try to ensure that the window can't accidentally be acknowledged
0909              * by the user clicking the mouse just as it appears.
0910              * To achieve this, move the window so that the OK button is as far away
0911              * from the cursor as possible. If the buttons are still too close to the
0912              * cursor, disable the buttons for a short time.
0913              * N.B. This can't be done in show(), since the geometry of the window
0914              *      is not known until it is displayed. Unfortunately by moving the
0915              *      window in showEvent(), a flicker is unavoidable.
0916              *      See the Qt documentation on window geometry for more details.
0917              */
0918             // PROBLEM: The frame size is not known yet!
0919             const QPoint cursor = QCursor::pos();
0920             const QRect rect  = geometry();
0921             // Find the offsets from the outside of the frame to the edges of the OK button
0922             const QRect button(mOkButton->mapToParent(QPoint(0, 0)), mOkButton->mapToParent(mOkButton->rect().bottomRight()));
0923             const int buttonLeft   = button.left() + rect.left() - frame.left();
0924             const int buttonRight  = width() - button.right() + frame.right() - rect.right();
0925             const int buttonTop    = button.top() + rect.top() - frame.top();
0926             const int buttonBottom = height() - button.bottom() + frame.bottom() - rect.bottom();
0927 
0928             const int centrex = (desk.width() + buttonLeft - buttonRight) / 2;
0929             const int centrey = (desk.height() + buttonTop - buttonBottom) / 2;
0930             const int x = (cursor.x() < centrex) ? desk.right() - frame.width() : desk.left();
0931             const int y = (cursor.y() < centrey) ? desk.bottom() - frame.height() : desk.top();
0932 
0933             // Find the enclosing rectangle for the new button positions
0934             // and check if the cursor is too near
0935             QRect buttons = mOkButton->geometry().united(mKAlarmButton->geometry());
0936             buttons.translate(rect.left() + x - frame.left(), rect.top() + y - frame.top());
0937             const int minDistance = proximityMultiple * mOkButton->height();
0938             if ((abs(cursor.x() - buttons.left()) < minDistance
0939               || abs(cursor.x() - buttons.right()) < minDistance)
0940             &&  (abs(cursor.y() - buttons.top()) < minDistance
0941               || abs(cursor.y() - buttons.bottom()) < minDistance))
0942                 mButtonDelay = proximityButtonDelay;    // too near - disable buttons initially
0943 
0944             if (x != frame.left()  ||  y != frame.top())
0945             {
0946                 mPositioning = true;
0947                 move(x, y);
0948                 execComplete = false;
0949             }
0950         }
0951         if (execComplete)
0952             displayComplete();    // play audio, etc.
0953     }
0954 
0955     // Set the window size etc. once the frame size is known
0956     QTimer::singleShot(0, this, &MessageWindow::frameDrawn);   //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
0957 
0958     mShown = true;
0959 }
0960 
0961 /******************************************************************************
0962 * Called when the window has been moved.
0963 */
0964 void MessageWindow::moveEvent(QMoveEvent* e)
0965 {
0966     MainWindowBase::moveEvent(e);
0967     if (!KWindowSystem::isPlatformWayland())  // Wayland doesn't allow positioning of windows
0968         theApp()->setSpreadWindowsState(isSpread(Desktop::workArea(mScreenNumber).topLeft()));
0969     if (mPositioning)
0970     {
0971         // The window has just been initially positioned
0972         mPositioning = false;
0973         displayComplete();    // play audio, etc.
0974     }
0975 }
0976 
0977 /******************************************************************************
0978 * Called after (hopefully) the window frame size is known.
0979 * Reset the initial window size if it exceeds the working area of the desktop.
0980 * Set the 'spread windows' menu item status.
0981 */
0982 void MessageWindow::frameDrawn()
0983 {
0984     if (!mErrorWindow()  &&  mAction() == KAEvent::SubAction::Message)
0985     {
0986         const QSize s = sizeHint();
0987         if (width() > s.width()  ||  height() > s.height())
0988             resize(s);
0989     }
0990     if (!KWindowSystem::isPlatformWayland())  // Wayland doesn't allow positioning of windows
0991         theApp()->setSpreadWindowsState(isSpread(Desktop::workArea(mScreenNumber).topLeft()));
0992 }
0993 
0994 /******************************************************************************
0995 * Called when the window has been displayed properly (in its correct position),
0996 * to play sounds and reschedule the event.
0997 */
0998 void MessageWindow::displayComplete()
0999 {
1000     mHelper->displayComplete(true);
1001 
1002     if (!mAlwaysHidden())
1003     {
1004         // Enable the window's buttons either now or after the configured delay
1005         if (mButtonDelay > 0)
1006             QTimer::singleShot(mButtonDelay, this, &MessageWindow::enableButtons);   //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
1007         else
1008             enableButtons();
1009     }
1010 }
1011 
1012 /******************************************************************************
1013 * Enable the window's buttons.
1014 */
1015 void MessageWindow::enableButtons()
1016 {
1017     mOkButton->setEnabled(true);
1018     mKAlarmButton->setEnabled(true);
1019     if (mDeferButton->isVisible()  &&  !mDisableDeferral())
1020         mDeferButton->setEnabled(true);
1021     if (mEditButton)
1022         mEditButton->setEnabled(true);
1023     if (mKMailButton)
1024         mKMailButton->setEnabled(true);
1025 }
1026 
1027 /******************************************************************************
1028 * Called when the window's size has changed (before it is painted).
1029 */
1030 void MessageWindow::resizeEvent(QResizeEvent* re)
1031 {
1032     if (mRestoreHeight)
1033     {
1034         // Restore the window height on session restoration
1035         if (mRestoreHeight != re->size().height())
1036         {
1037             QSize size = re->size();
1038             size.setHeight(mRestoreHeight);
1039             resize(size);
1040         }
1041         else if (isVisible())
1042             mRestoreHeight = 0;
1043     }
1044     else
1045     {
1046         if (mShown  &&  mAction() == KAEvent::SubAction::File  &&  mErrorMsgs().isEmpty())
1047             Config::writeWindowSize("FileMessage", re->size());
1048         MainWindowBase::resizeEvent(re);
1049     }
1050 }
1051 
1052 /******************************************************************************
1053 * Called when a close event is received.
1054 * Only quits the application if there is no system tray icon displayed.
1055 */
1056 void MessageWindow::closeEvent(QCloseEvent* ce)
1057 {
1058     if (!mHelper->closeEvent())
1059     {
1060         ce->ignore();
1061         return;
1062     }
1063     MainWindowBase::closeEvent(ce);
1064 }
1065 
1066 /******************************************************************************
1067 * Called by MessageDisplayHelper to confirm that the alarm message should be
1068 * acknowledged (closed).
1069 */
1070 bool MessageWindow::confirmAcknowledgement()
1071 {
1072     if (!mNoCloseConfirm())
1073     {
1074         // Ask for confirmation of acknowledgement. Use warningYesNo() because its default is No.
1075         if (KAMessageBox::warningYesNo(this, i18nc("@info", "Do you really want to acknowledge this alarm?"),
1076                                              i18nc("@action:button", "Acknowledge Alarm"), KGuiItem(i18nc("@action:button", "Acknowledge")), KStandardGuiItem::cancel())
1077             != KMessageBox::ButtonCode::PrimaryAction)
1078         {
1079             return false;
1080         }
1081     }
1082     return true;
1083 }
1084 
1085 /******************************************************************************
1086 * Called when the OK button is clicked.
1087 */
1088 void MessageWindow::slotOk()
1089 {
1090     if (mDontShowAgainCheck  &&  mDontShowAgainCheck->isChecked())
1091         KAlarm::setDontShowErrors(mEventId(), mDontShowAgain());
1092     close();
1093 }
1094 
1095 /******************************************************************************
1096 * Called when the KMail button is clicked.
1097 * Tells KMail to display the email message displayed in this message window.
1098 */
1099 void MessageWindow::slotShowKMailMessage()
1100 {
1101     AkonadiPlugin* akonadiPlugin = Preferences::akonadiPlugin();
1102     if (!akonadiPlugin)
1103         return;
1104     qCDebug(KALARM_LOG) << "MessageWindow::slotShowKMailMessage";
1105     if (mEmailId() < 0)
1106         return;
1107     org::kde::kmail::kmail kmail(KMAIL_DBUS_SERVICE, KMAIL_DBUS_PATH, QDBusConnection::sessionBus());
1108     // Display the message contents
1109     QDBusReply<bool> reply = kmail.showMail(mEmailId());
1110     bool failed1 = true;
1111     bool failed2 = true;
1112     if (!reply.isValid())
1113         qCCritical(KALARM_LOG) << "kmail 'showMail' D-Bus call failed:" << reply.error().message();
1114     else if (reply.value())
1115         failed1 = false;
1116 
1117     // Select the mail folder containing the message
1118     qint64 colId = akonadiPlugin->getCollectionId(mEmailId());
1119     if (colId < 0)
1120         qCWarning(KALARM_LOG) << "MessageWindow::slotShowKMailMessage: No parent found for item" << mEmailId();
1121     else
1122     {
1123         reply = kmail.selectFolder(QString::number(colId));
1124         if (!reply.isValid())
1125             qCCritical(KALARM_LOG) << "kmail 'selectFolder' D-Bus call failed:" << reply.error().message();
1126         else if (reply.value())
1127             failed2 = false;
1128     }
1129 
1130     if (failed1 || failed2)
1131         KAMessageBox::error(this, xi18nc("@info", "Unable to locate this email in <application>KMail</application>"));
1132 }
1133 
1134 /******************************************************************************
1135 * Called when the Edit... button is clicked.
1136 * Displays the alarm edit dialog.
1137 *
1138 * NOTE: The alarm edit dialog is made a child of the main window, not this
1139 *       window, so that if this window closes before the dialog (e.g. on
1140 *       auto-close), KAlarm doesn't crash. The dialog is set non-modal so that
1141 *       the main window is unaffected, but modal mode is simulated so that
1142 *       this window is inactive while the dialog is open.
1143 */
1144 void MessageWindow::slotEdit()
1145 {
1146     qCDebug(KALARM_LOG) << "MessageWindow::slotEdit";
1147     EditAlarmDlg* dlg = mHelper->createEdit();
1148     if (!dlg)
1149         return;
1150     KWindowSystem::setMainWindow(dlg->windowHandle(), winId());
1151 #if ENABLE_X11
1152     if (KWindowSystem::isPlatformX11())
1153         KX11Extras::setOnAllDesktops(dlg->winId(), false);   // don't show on all virtual desktops
1154 #endif
1155     setButtonsReadOnly(true);
1156 #if ENABLE_X11
1157     if (KWindowSystem::isPlatformX11())
1158         connect(KX11Extras::self(), &KX11Extras::activeWindowChanged, this, &MessageWindow::activeWindowChanged);
1159 #endif
1160     mHelper->executeEdit();
1161 }
1162 
1163 /******************************************************************************
1164 * Called when Cancel is clicked in the alarm edit dialog invoked by the Edit
1165 * button, or when the dialog is deleted.
1166 */
1167 void MessageWindow::editDlgCancelled()
1168 {
1169     setButtonsReadOnly(false);
1170 }
1171 
1172 /******************************************************************************
1173 * Called when the active window has changed. If this window has become the
1174 * active window and there is an alarm edit dialog, simulate a modal dialog by
1175 * making the alarm edit dialog the active window instead.
1176 */
1177 void MessageWindow::activeWindowChanged(WId win)
1178 {
1179 #if ENABLE_X11
1180     // Note that the alarm edit dialog is not a QWindow, so we can't call
1181     // QWindow::requestActivate().
1182     if (mEditDlg()  &&  win == winId())
1183         KX11Extras::activateWindow(mEditDlg()->winId());
1184 #endif
1185 }
1186 
1187 /******************************************************************************
1188 * Called when the Defer... button is clicked.
1189 * Displays the defer message dialog.
1190 */
1191 void MessageWindow::slotDefer()
1192 {
1193     mDeferData = createDeferDlg(this, false);
1194     if (windowFlags() & Qt::X11BypassWindowManagerHint)
1195         mDeferData->dlg->setWindowFlags(mDeferData->dlg->windowFlags() | Qt::X11BypassWindowManagerHint);
1196     if (!Preferences::modalMessages())
1197         lower();
1198     executeDeferDlg(mDeferData);
1199     mDeferData = nullptr;   // it was deleted by executeDeferDlg()
1200 }
1201 
1202 /******************************************************************************
1203 * Set or clear the read-only state of the dialog buttons.
1204 */
1205 void MessageWindow::setButtonsReadOnly(bool ro)
1206 {
1207     mOkButton->setReadOnly(ro, true);
1208     mDeferButton->setReadOnly(ro, true);
1209     if (mEditButton)
1210         mEditButton->setReadOnly(ro, true);
1211     if (mSilenceButton)
1212         mSilenceButton->setReadOnly(ro, true);
1213     if (mKMailButton)
1214         mKMailButton->setReadOnly(ro, true);
1215     mKAlarmButton->setReadOnly(ro, true);
1216 }
1217 
1218 bool MessageWindow::isDeferButtonEnabled() const
1219 {
1220     return mDeferButton->isEnabled()  &&  mDeferButton->isVisible();
1221 }
1222 
1223 void MessageWindow::enableDeferButton(bool enable)
1224 {
1225     mDeferButton->setEnabled(enable);
1226 }
1227 
1228 void MessageWindow::enableEditButton(bool enable)
1229 {
1230     if (mEditButton)
1231         mEditButton->setEnabled(enable);
1232 }
1233 
1234 /******************************************************************************
1235 * Called when the KAlarm icon button in the message window is clicked.
1236 * Displays the main window, with the appropriate alarm selected.
1237 */
1238 void MessageWindow::displayMainWindow()
1239 {
1240     MessageDisplay::displayMainWindow();
1241 }
1242 
1243 /******************************************************************************
1244 * Check whether the message window should be modal, i.e. with title bar etc.
1245 * Normally this follows the Preferences setting, but if there is a full screen
1246 * window displayed, on X11 the message window has to bypass the window manager
1247 * in order to display on top of it (which has the side effect that it will have
1248 * no window decoration).
1249 *
1250 * Also find the usable area of the desktop (excluding panel etc.), on the
1251 * appropriate screen if there are multiple screens.
1252 */
1253 bool MessageWindow::getWorkAreaAndModal()
1254 {
1255     mScreenNumber = -1;
1256     const bool modal = Preferences::modalMessages();
1257     const QList<QScreen*> screens = QGuiApplication::screens();
1258     const int numScreens = screens.count();
1259     if (numScreens > 1)
1260     {
1261         // There are multiple screens.
1262         // Check for any full screen windows, even if they are not the active
1263         // window, and try not to show the alarm message their screens.
1264         mScreenNumber = screens.indexOf(MainWindow::mainMainWindow()->screen());  // default = KAlarm's screen
1265         if (QGuiApplication::primaryScreen()->virtualSiblings().size() > 1)
1266         {
1267             // The screens form a single virtual desktop.
1268             // Xinerama, for example, uses this scheme.
1269             QList<FullScreenType> screenTypes(numScreens);
1270             QList<QRect> screenRects(numScreens);
1271             for (int s = 0;  s < numScreens;  ++s)
1272                 screenRects[s] = screens[s]->geometry();
1273             const FullScreenType full = findFullScreenWindows(screenRects, screenTypes);
1274             if (full == NoFullScreen  ||  screenTypes[mScreenNumber] == NoFullScreen)
1275                 return modal;
1276             for (int s = 0;  s < numScreens;  ++s)
1277             {
1278                 if (screenTypes[s] == NoFullScreen)
1279 
1280                 {
1281                     // There is no full screen window on this screen
1282                     mScreenNumber = s;
1283                     return modal;
1284                 }
1285             }
1286             // All screens contain a full screen window: use one without
1287             // an active full screen window.
1288             for (int s = 0;  s < numScreens;  ++s)
1289             {
1290                 if (screenTypes[s] == FullScreen)
1291                 {
1292                     mScreenNumber = s;
1293                     return modal;
1294                 }
1295             }
1296         }
1297         else
1298         {
1299             // The screens are completely separate from each other.
1300             int inactiveScreen = -1;
1301             FullScreenType full = haveFullScreenWindow(mScreenNumber);
1302 //qCDebug(KALARM_LOG)<<"full="<<full<<", screen="<<mScreenNumber;
1303             if (full == NoFullScreen)
1304                 return modal;   // KAlarm's screen doesn't contain a full screen window
1305             if (full == FullScreen)
1306                 inactiveScreen = mScreenNumber;
1307             for (int s = 0;  s < numScreens;  ++s)
1308             {
1309                 if (s != mScreenNumber)
1310                 {
1311                     full = haveFullScreenWindow(s);
1312                     if (full == NoFullScreen)
1313                     {
1314                         // There is no full screen window on this screen
1315                         mScreenNumber = s;
1316                         return modal;
1317                     }
1318                     if (full == FullScreen  &&  inactiveScreen < 0)
1319                         inactiveScreen = s;
1320                 }
1321             }
1322             if (inactiveScreen >= 0)
1323             {
1324                 // All screens contain a full screen window: use one without
1325                 // an active full screen window.
1326                 mScreenNumber = inactiveScreen;
1327                 return modal;
1328             }
1329         }
1330         return false;  // can't logically get here, since there can only be one active window...
1331     }
1332     if (modal)
1333     {
1334 #if ENABLE_X11
1335         if (KWindowSystem::isPlatformX11())
1336         {
1337             const WId activeId = KX11Extras::activeWindow();
1338             const KWindowInfo wi = KWindowInfo(activeId, NET::WMState);
1339             if (wi.valid()  &&  wi.hasState(NET::FullScreen))
1340                 return false;    // the active window is full screen.
1341         }
1342 #endif
1343     }
1344     return modal;
1345 }
1346 
1347 namespace
1348 {
1349 
1350 /******************************************************************************
1351 * In a multi-screen setup (not a single virtual desktop), find whether the
1352 * specified screen has a full screen window on it.
1353 */
1354 FullScreenType haveFullScreenWindow(int screen)
1355 {
1356     FullScreenType type = NoFullScreen;
1357 //TODO: implement on Wayland
1358 #if ENABLE_X11
1359     if (KWindowSystem::isPlatformX11())
1360     {
1361         using namespace QNativeInterface;
1362         auto* x11App = qGuiApp->nativeInterface<QX11Application>();
1363         if (!x11App)
1364             return type;
1365         xcb_connection_t* connection = x11App->connection();
1366         const NETRootInfo rootInfo(connection, NET::ClientList | NET::ActiveWindow, NET::Properties2(), screen);
1367         const xcb_window_t rootWindow   = rootInfo.rootWindow();
1368         const xcb_window_t activeWindow = rootInfo.activeWindow();
1369         const xcb_window_t* windows     = rootInfo.clientList();
1370         const int windowCount           = rootInfo.clientListCount();
1371         for (int w = 0;  w < windowCount;  ++w)
1372         {
1373             NETWinInfo winInfo(connection, windows[w], rootWindow, NET::WMState|NET::WMGeometry, NET::Properties2());
1374             if (winInfo.state() & NET::FullScreen)
1375             {
1376 //qCDebug(KALARM_LOG)<<"Found FULL SCREEN: " << windows[w];
1377                 type = FullScreen;
1378                 if (windows[w] == activeWindow)
1379                     return FullScreenActive;
1380             }
1381         }
1382     }
1383 #endif // ENABLE_X11
1384     return type;
1385 }
1386 
1387 /******************************************************************************
1388 * In a multi-screen setup (single virtual desktop, e.g. Xinerama), find which
1389 * screens have full screen windows on them.
1390 */
1391 FullScreenType findFullScreenWindows(const QList<QRect>& screenRects, QList<FullScreenType>& screenTypes)
1392 {
1393     FullScreenType result = NoFullScreen;
1394     screenTypes.fill(NoFullScreen);
1395 //TODO: implement on Wayland
1396 #if ENABLE_X11
1397     if (KWindowSystem::isPlatformX11())
1398     {
1399         using namespace QNativeInterface;
1400         auto* x11App = qGuiApp->nativeInterface<QX11Application>();
1401         if (!x11App)
1402             return result;
1403         xcb_connection_t* connection = x11App->connection();
1404         const NETRootInfo rootInfo(connection, NET::ClientList | NET::ActiveWindow, NET::Properties2());
1405         const xcb_window_t rootWindow   = rootInfo.rootWindow();
1406         const xcb_window_t activeWindow = rootInfo.activeWindow();
1407         const xcb_window_t* windows     = rootInfo.clientList();
1408         const int windowCount           = rootInfo.clientListCount();
1409 //qCDebug(KALARM_LOG)<<"Virtual desktops: Window count="<<windowCount<<", active="<<activeWindow<<", geom="<<QApplication::desktop()->screenGeometry(0);
1410         NETRect netgeom;
1411         NETRect netframe;
1412         for (int w = 0;  w < windowCount;  ++w)
1413         {
1414             NETWinInfo winInfo(connection, windows[w], rootWindow, NET::WMState | NET::WMGeometry, NET::Properties2());
1415             if (winInfo.state() & NET::FullScreen)
1416             {
1417                 // Found a full screen window - find which screen it's on
1418                 const bool active = (windows[w] == activeWindow);
1419                 winInfo.kdeGeometry(netframe, netgeom);
1420                 const QRect winRect(netgeom.pos.x, netgeom.pos.y, netgeom.size.width, netgeom.size.height);
1421 //qCDebug(KALARM_LOG)<<"Found FULL SCREEN: "<<windows[w]<<", geom="<<winRect;
1422                 for (int s = 0, count = screenRects.count();  s < count;  ++s)
1423                 {
1424                     if (screenRects[s].contains(winRect))
1425                     {
1426 //qCDebug(KALARM_LOG)<<"FULL SCREEN on screen"<<s<<", active="<<active;
1427                         if (active)
1428                             screenTypes[s] = result = FullScreenActive;
1429                         else
1430                         {
1431                             if (screenTypes[s] == NoFullScreen)
1432                                 screenTypes[s] = FullScreen;
1433                             if (result == NoFullScreen)
1434                                 result = FullScreen;
1435                         }
1436                         break;
1437                     }
1438                 }
1439             }
1440         }
1441     }
1442 #endif // ENABLE_X11
1443     return result;
1444 }
1445 
1446 } // namespace
1447 
1448 #include "moc_messagewindow.cpp"
1449 
1450 // vim: et sw=4: