File indexing completed on 2025-02-02 04:26:11

0001 /* SPDX-FileCopyrightText: 2022 Noah Davis <noahadvs@gmail.com>
0002  * SPDX-License-Identifier: LGPL-2.0-or-later
0003  */
0004 
0005 #include "OptionsMenu.h"
0006 
0007 #include "CaptureModeModel.h"
0008 #include "Gui/SettingsDialog/SettingsDialog.h"
0009 #include "SpectacleCore.h"
0010 #include "WidgetWindowUtils.h"
0011 #include "settings.h"
0012 
0013 #include <KLocalizedString>
0014 #include <KStandardAction>
0015 
0016 #include <QStyle>
0017 
0018 using namespace Qt::StringLiterals;
0019 
0020 static QPointer<OptionsMenu> s_instance = nullptr;
0021 
0022 OptionsMenu::OptionsMenu(QWidget *parent)
0023     : SpectacleMenu(parent)
0024     , captureModeSection(new QAction(this))
0025     , captureModeGroup(new QActionGroup(this)) // exclusive by default)
0026     , captureSettingsSection(new QAction(this))
0027     , includeMousePointerAction(new QAction(this))
0028     , includeWindowDecorationsAction(new QAction(this))
0029     , includeWindowShadowAction(new QAction(this))
0030     , onlyCapturePopupAction(new QAction(this))
0031     , quitAfterSaveAction(new QAction(this))
0032     , captureOnClickAction(new QAction(this))
0033     , delayAction(new QWidgetAction(this))
0034     , delayWidget(new QWidget(this))
0035     , delayLayout(new QHBoxLayout(delayWidget.get()))
0036     , delayLabel(new QLabel(delayWidget.get()))
0037     , delaySpinBox(new SmartSpinBox(delayWidget.get()))
0038 {
0039     addAction(KStandardAction::preferences(this, &OptionsMenu::showPreferencesDialog, this));
0040 
0041     // QMenu::addSection just adds an action with text and separator mode enabled
0042     captureModeSection->setText(i18n("Capture Mode"));
0043     captureModeSection->setSeparator(true);
0044     addAction(captureModeSection.get());
0045 
0046     // Add capture mode actions.
0047     // This cannot be done in the constructor because captureModeModel will be null at this time.
0048     connect(this, &OptionsMenu::aboutToShow,
0049             this, &OptionsMenu::updateCaptureModes);
0050 
0051     // make capture mode actions do things
0052     connect(captureModeGroup.get(), &QActionGroup::triggered, this, [](QAction *action){
0053         int mode = action->data().toInt();
0054         Settings::setCaptureMode(mode);
0055     });
0056     connect(Settings::self(), &Settings::captureModeChanged, this, [this](){
0057         int mode = Settings::captureMode();
0058         if (captureModeGroup->checkedAction() && mode == captureModeGroup->checkedAction()->data().toInt()) {
0059             return;
0060         }
0061         for (auto action : std::as_const(captureModeActions)) {
0062             if (mode == action->data().toInt()) {
0063                 action->setChecked(true);
0064             }
0065         }
0066     });
0067 
0068     captureSettingsSection->setText(i18n("Capture Settings"));
0069     captureSettingsSection->setSeparator(true);
0070     addAction(captureSettingsSection.get());
0071 
0072     includeMousePointerAction->setText(i18n("Include mouse pointer"));
0073     includeMousePointerAction->setToolTip(i18n("Show the mouse cursor in the screenshot image"));
0074     includeMousePointerAction->setCheckable(true);
0075     includeMousePointerAction->setChecked(Settings::includePointer());
0076     connect(includeMousePointerAction.get(), &QAction::toggled, this, [](bool checked){
0077         Settings::setIncludePointer(checked);
0078     });
0079     connect(Settings::self(), &Settings::includePointerChanged, this, [this](){
0080         includeMousePointerAction->setChecked(Settings::includePointer());
0081     });
0082     addAction(includeMousePointerAction.get());
0083 
0084     includeWindowDecorationsAction->setText(i18n("Include window titlebar and borders"));
0085     includeWindowDecorationsAction->setToolTip(i18n("Show the window title bar, the minimize/maximize/close buttons, and the window border"));
0086     includeWindowDecorationsAction->setCheckable(true);
0087     includeWindowDecorationsAction->setChecked(Settings::includeDecorations());
0088     connect(includeWindowDecorationsAction.get(), &QAction::toggled, this, [](bool checked){
0089         Settings::setIncludeDecorations(checked);
0090     });
0091     connect(Settings::self(), &Settings::includeDecorationsChanged, this, [this](){
0092         includeWindowDecorationsAction->setChecked(Settings::includeDecorations());
0093     });
0094     addAction(includeWindowDecorationsAction.get());
0095 
0096     includeWindowShadowAction->setText(i18n("Include window shadow"));
0097     includeWindowShadowAction->setToolTip(i18n("Show the window shadow"));
0098     includeWindowShadowAction->setCheckable(true);
0099     includeWindowShadowAction->setChecked(Settings::includeShadow());
0100     connect(includeWindowShadowAction.get(), &QAction::toggled, this, [](bool checked) {
0101         Settings::setIncludeShadow(checked);
0102     });
0103     connect(Settings::self(), &Settings::includeShadowChanged, this, [this]() {
0104         includeWindowShadowAction->setChecked(Settings::includeShadow());
0105     });
0106     addAction(includeWindowShadowAction.get());
0107 
0108     onlyCapturePopupAction->setText(i18n("Capture the current pop-up only"));
0109     onlyCapturePopupAction->setToolTip(
0110         i18n("Capture only the current pop-up window (like a menu, tooltip etc).\n"
0111              "If disabled, the pop-up is captured along with the parent window"));
0112     onlyCapturePopupAction->setCheckable(true);
0113     onlyCapturePopupAction->setChecked(Settings::transientOnly());
0114     connect(onlyCapturePopupAction.get(), &QAction::toggled, this, [](bool checked){
0115         Settings::setTransientOnly(checked);
0116     });
0117     connect(Settings::self(), &Settings::transientOnlyChanged, this, [this](){
0118         onlyCapturePopupAction->setChecked(Settings::transientOnly());
0119     });
0120     addAction(onlyCapturePopupAction.get());
0121 
0122     quitAfterSaveAction->setText(i18n("Quit after manual Save or Copy"));
0123     quitAfterSaveAction->setToolTip(i18n("Quit Spectacle after manually saving or copying the image"));
0124     quitAfterSaveAction->setCheckable(true);
0125     quitAfterSaveAction->setChecked(Settings::quitAfterSaveCopyExport());
0126     connect(quitAfterSaveAction.get(), &QAction::toggled, this, [](bool checked){
0127         Settings::setQuitAfterSaveCopyExport(checked);
0128     });
0129     connect(Settings::self(), &Settings::quitAfterSaveCopyExportChanged, this, [this](){
0130         quitAfterSaveAction->setChecked(Settings::quitAfterSaveCopyExport());
0131     });
0132     addAction(quitAfterSaveAction.get());
0133 
0134     addSeparator();
0135 
0136     // add capture on click
0137     captureOnClickAction->setText(i18n("Capture On Click"));
0138     captureOnClickAction->setCheckable(true);
0139     captureOnClickAction->setChecked(Settings::captureOnClick());
0140     connect(captureOnClickAction.get(), &QAction::toggled, this, [this](bool checked){
0141         Settings::setCaptureOnClick(checked);
0142         delayAction->setEnabled(!checked);
0143     });
0144     connect(Settings::self(), &Settings::captureOnClickChanged, this, [this](){
0145         captureOnClickAction->setChecked(Settings::captureOnClick());
0146     });
0147     addAction(captureOnClickAction.get());
0148 
0149     // set up delay widget
0150     auto spinbox = delaySpinBox.get();
0151     auto label = delayLabel.get();
0152     label->setText(i18n("Delay:"));
0153     spinbox->setDecimals(1);
0154     spinbox->setSingleStep(1.0);
0155     spinbox->setMinimum(0.0);
0156     spinbox->setMaximum(999);
0157     spinbox->setSpecialValueText(i18n("No Delay"));
0158     delayActionLayoutUpdate();
0159     connect(spinbox, qOverload<double>(&SmartSpinBox::valueChanged), this, [this](){
0160         if (updatingDelayActionLayout) {
0161             return;
0162         }
0163         Settings::setCaptureDelay(delaySpinBox->value());
0164     });
0165     connect(Settings::self(), &Settings::captureDelayChanged, this, [this](){
0166         delaySpinBox->setValue(Settings::captureDelay());
0167     });
0168     delayWidget->setLayout(delayLayout.get());
0169     delayLayout->addWidget(label);
0170     delayLayout->addWidget(spinbox);
0171     delayLayout->setAlignment(Qt::AlignLeft);
0172     delayAction->setDefaultWidget(delayWidget.get());
0173     delayAction->setEnabled(!captureOnClickAction->isChecked());
0174     addAction(delayAction.get());
0175 }
0176 
0177 OptionsMenu *OptionsMenu::instance()
0178 {
0179     // We need to create it here instead of using Q_GLOBAL_STATIC like the other menus
0180     // to prevent a segfault.
0181     if (!s_instance) {
0182         s_instance = new OptionsMenu;
0183     }
0184     return s_instance;
0185 }
0186 
0187 void OptionsMenu::showPreferencesDialog()
0188 {
0189     KConfigDialog *dialog = KConfigDialog::exists(u"settings"_s);
0190     if (!dialog) {
0191         dialog = new SettingsDialog;
0192         dialog->setAttribute(Qt::WA_DeleteOnClose);
0193     }
0194 
0195     // properly set the transientparent chain
0196     setWidgetTransientParent(dialog, getWidgetTransientParent(this));
0197 
0198     // HACK: how to make it appear on top of the fullscreen window? dialog->setWindowFlags(Qt::Popup); is an ugly way
0199 
0200     dialog->show();
0201 }
0202 
0203 void OptionsMenu::setCaptureModeOptionsEnabled(bool enabled)
0204 {
0205     captureModeOptionsEnabled = enabled;
0206 }
0207 
0208 void OptionsMenu::changeEvent(QEvent *event)
0209 {
0210     switch(event->type()) {
0211     case QEvent::FontChange:
0212     case QEvent::LayoutDirectionChange:
0213     case QEvent::StyleChange:
0214         delayActionLayoutUpdate();
0215         break;
0216     default: break;
0217     }
0218     QWidget::changeEvent(event);
0219 }
0220 
0221 void OptionsMenu::delayActionLayoutUpdate()
0222 {
0223     // We can't block signals while doing this to prevent unnecessary
0224     // processing because the spinbox has internal connections that need
0225     // to work in order to get the correct size.
0226     // We use our own guarding variable instead.
0227     updatingDelayActionLayout = true;
0228     delaySpinBox->setValue(delaySpinBox->maximum());
0229     delaySpinBox->setMinimumWidth(delaySpinBox->sizeHint().width());
0230     delaySpinBox->setValue(Settings::captureDelay());
0231     updatingDelayActionLayout = false;
0232 
0233     int menuHMargin = style()->pixelMetric(QStyle::PM_MenuHMargin);
0234     int menuVMargin = style()->pixelMetric(QStyle::PM_MenuVMargin);
0235     if (layoutDirection() == Qt::RightToLeft) {
0236         delayLabel->setContentsMargins(0, 0, menuHMargin + delayLabel->fontMetrics().descent(), 0);
0237     } else {
0238         delayLabel->setContentsMargins(menuHMargin + delayLabel->fontMetrics().descent(), 0, 0, 0);
0239     }
0240     delayLayout->setContentsMargins(0, menuVMargin, 0, 0);
0241 }
0242 
0243 void OptionsMenu::updateCaptureModes()
0244 {
0245     captureModeSection->setVisible(captureModeOptionsEnabled);
0246     if (!captureModeOptionsEnabled) {
0247         for (auto action : std::as_const(captureModeActions)) {
0248             captureModeGroup->removeAction(action);
0249             removeAction(action);
0250             action->deleteLater();
0251         }
0252         captureModeActions.clear();
0253         return;
0254     }
0255     auto captureModeModel = SpectacleCore::instance()->captureModeModel();
0256     if (captureModeModel == nullptr) {
0257         qWarning() << Q_FUNC_INFO << "captureModeModel is null, not updating actions";
0258         return;
0259     }
0260     // Only make this conneciton once.
0261     // Can't be done in the constructor because captureModeModel is null at that time.
0262     if (!captureModesInitialized) {
0263         connect(captureModeModel, &CaptureModeModel::captureModesChanged, this, [this](){
0264             shouldUpdateCaptureModes = true;
0265         });
0266         captureModesInitialized = true;
0267     }
0268     // avoid unnecessarily resetting actions
0269     if (!shouldUpdateCaptureModes) {
0270         return;
0271     }
0272     shouldUpdateCaptureModes = false;
0273     for (auto action : std::as_const(captureModeActions)) {
0274         captureModeGroup->removeAction(action);
0275         removeAction(action);
0276         action->deleteLater();
0277     }
0278     captureModeActions.clear();
0279     for (int i = 0; i < captureModeModel->rowCount(); ++i) {
0280         auto index = captureModeModel->index(i);
0281         auto action = new QAction(this);
0282         captureModeActions.append(action);
0283         action->setText(captureModeModel->data(index, Qt::DisplayRole).toString());
0284         action->setData(captureModeModel->data(index, CaptureModeModel::CaptureModeRole));
0285         action->setCheckable(true);
0286         captureModeGroup->addAction(action);
0287         if (action->data().toInt() == Settings::captureMode()) {
0288             action->setChecked(true);
0289         }
0290         insertAction(captureSettingsSection.get(), action);
0291     }
0292 }
0293 
0294 #include "moc_OptionsMenu.cpp"