1 /* This file is part of the KDE project
2    SPDX-FileCopyrightText: 2001 Christoph Cullmann <cullmann@kde.org>
3    SPDX-FileCopyrightText: 2002 Joseph Wenninger <jowenn@kde.org>
4    SPDX-FileCopyrightText: 2007 Mirko Stocker <me@misto.ch>
5 
6    SPDX-License-Identifier: LGPL-2.0-only
7 */
8 
9 #include "kateconfigdialog.h"
10 
11 #include "kateapp.h"
12 #include "kateconfigplugindialogpage.h"
13 #include "katedebug.h"
14 #include "katedocmanager.h"
15 #include "katemainwindow.h"
16 #include "katepluginmanager.h"
17 #include "katequickopenmodel.h"
18 #include "katesessionmanager.h"
19 #include "kateviewmanager.h"
20 
21 #include <KConfigGroup>
22 #include <KLocalizedString>
23 #include <KMessageBox>
24 #include <KPluralHandlingSpinBox>
25 #include <KSharedConfig>
26 #include <KStandardAction>
27 
28 #include <QCheckBox>
29 #include <QComboBox>
30 #include <QDesktopServices>
31 #include <QDialogButtonBox>
32 #include <QFrame>
33 #include <QGroupBox>
34 #include <QLabel>
35 #include <QVBoxLayout>
36 
KateConfigDialog(KateMainWindow * parent)37 KateConfigDialog::KateConfigDialog(KateMainWindow *parent)
38     : KPageDialog(parent)
39     , m_mainWindow(parent)
40 {
41     setWindowTitle(i18n("Configure"));
42     setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Apply | QDialogButtonBox::Cancel | QDialogButtonBox::Help);
43 
44     // first: add the KTextEditor config pages
45     // rational: most people want to alter e.g. the fonts, the colors or some other editor stuff first
46     addEditorPages();
47 
48     // second: add out own config pages
49     // this includes all plugin config pages, added to the bottom
50     addBehaviorPage();
51     addSessionPage();
52     addFeedbackPage();
53     addPluginsPage();
54     addPluginPages();
55 
56     // ensure no stray signals already set this!
57     m_dataChanged = false;
58     buttonBox()->button(QDialogButtonBox::Apply)->setEnabled(false);
59 
60     // handle dialog actions
61     connect(this, &KateConfigDialog::accepted, this, &KateConfigDialog::slotApply);
62     connect(buttonBox()->button(QDialogButtonBox::Apply), &QPushButton::clicked, this, &KateConfigDialog::slotApply);
63     connect(buttonBox()->button(QDialogButtonBox::Help), &QPushButton::clicked, this, &KateConfigDialog::slotHelp);
64 }
65 
addBehaviorPage()66 void KateConfigDialog::addBehaviorPage()
67 {
68     KSharedConfig::Ptr config = KSharedConfig::openConfig();
69     KConfigGroup cgGeneral = KConfigGroup(config, "General");
70 
71     QFrame *generalFrame = new QFrame;
72     KPageWidgetItem *item = addPage(generalFrame, i18n("Behavior"));
73     item->setHeader(i18n("Behavior Options"));
74     item->setIcon(QIcon::fromTheme(QStringLiteral("preferences-system-windows-behavior")));
75 
76     QVBoxLayout *layout = new QVBoxLayout(generalFrame);
77     layout->setContentsMargins(0, 0, 0, 0);
78 
79     // GROUP with the one below: "Behavior"
80     QGroupBox *buttonGroup = new QGroupBox(i18n("&Behavior"), generalFrame);
81     QVBoxLayout *vbox = new QVBoxLayout;
82     layout->addWidget(buttonGroup);
83 
84     auto hlayout = new QHBoxLayout;
85     auto label = new QLabel(i18n("&Switch to output view upon message type:"), buttonGroup);
86     hlayout->addWidget(label);
87     m_messageTypes = new QComboBox(buttonGroup);
88     hlayout->addWidget(m_messageTypes);
89     label->setBuddy(m_messageTypes);
90     m_messageTypes->addItems({i18n("Never"), i18n("Error"), i18n("Warning"), i18n("Info"), i18n("Log")});
91     m_messageTypes->setCurrentIndex(cgGeneral.readEntry("Show output view for message type", 1));
92     connect(m_messageTypes, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &KateConfigDialog::slotChanged);
93     vbox->addLayout(hlayout);
94 
95     // modified files notification
96     m_modNotifications = new QCheckBox(i18n("Use a separate &dialog for handling externally modified files"), buttonGroup);
97     m_modNotifications->setChecked(m_mainWindow->modNotificationEnabled());
98     m_modNotifications->setWhatsThis(
99         i18n("If enabled, a modal dialog will be used to show all of the modified files. "
100              "If not enabled, you will be individually asked what to do for each modified file "
101              "only when that file's view receives focus."));
102     connect(m_modNotifications, &QCheckBox::toggled, this, &KateConfigDialog::slotChanged);
103 
104     vbox->addWidget(m_modNotifications);
105 
106     buttonGroup->setLayout(vbox);
107 
108     // tabbar => we allow to configure some limit on number of tabs to show
109     buttonGroup = new QGroupBox(i18n("&Tabs"), generalFrame);
110     vbox = new QVBoxLayout;
111     buttonGroup->setLayout(vbox);
112     hlayout = new QHBoxLayout;
113     label = new QLabel(i18n("&Limit number of tabs:"), buttonGroup);
114     hlayout->addWidget(label);
115     m_tabLimit = new QSpinBox(buttonGroup);
116     hlayout->addWidget(m_tabLimit);
117     label->setBuddy(m_tabLimit);
118     m_tabLimit->setRange(0, 256);
119     m_tabLimit->setSpecialValueText(i18n("Unlimited"));
120     m_tabLimit->setValue(cgGeneral.readEntry("Tabbar Tab Limit", 0));
121     connect(m_tabLimit, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &KateConfigDialog::slotChanged);
122     vbox->addLayout(hlayout);
123 
124     m_showTabCloseButton = new QCheckBox(i18n("&Show close button"), buttonGroup);
125     m_showTabCloseButton->setChecked(cgGeneral.readEntry("Show Tabs Close Button", true));
126     m_showTabCloseButton->setToolTip(i18n("When checked each tab will display a close button."));
127     connect(m_showTabCloseButton, &QCheckBox::toggled, this, &KateConfigDialog::slotChanged);
128     vbox->addWidget(m_showTabCloseButton);
129 
130     m_expandTabs = new QCheckBox(i18n("&Expand tabs"), buttonGroup);
131     m_expandTabs->setChecked(cgGeneral.readEntry("Expand Tabs", true));
132     m_expandTabs->setToolTip(i18n("When checked tabs take as much size as possible."));
133     connect(m_expandTabs, &QCheckBox::toggled, this, &KateConfigDialog::slotChanged);
134     vbox->addWidget(m_expandTabs);
135 
136     m_tabDoubleClickNewDocument = new QCheckBox(i18n("&Double click opens a new document"), buttonGroup);
137     m_tabDoubleClickNewDocument->setChecked(cgGeneral.readEntry("Tab Double Click New Document", true));
138     m_tabDoubleClickNewDocument->setToolTip(i18n("When checked double click opens a new document."));
139     connect(m_tabDoubleClickNewDocument, &QCheckBox::toggled, this, &KateConfigDialog::slotChanged);
140     vbox->addWidget(m_tabDoubleClickNewDocument);
141 
142     m_tabMiddleClickCloseDocument = new QCheckBox(i18n("&Middle click closes a document"), buttonGroup);
143     m_tabMiddleClickCloseDocument->setChecked(cgGeneral.readEntry("Tab Middle Click Close Document", true));
144     m_tabMiddleClickCloseDocument->setToolTip(i18n("When checked middle click closes a document."));
145     connect(m_tabMiddleClickCloseDocument, &QCheckBox::toggled, this, &KateConfigDialog::slotChanged);
146     vbox->addWidget(m_tabMiddleClickCloseDocument);
147 
148     layout->addWidget(buttonGroup);
149 
150     layout->addStretch(1); // :-] works correct without autoadd
151 }
152 
addSessionPage()153 void KateConfigDialog::addSessionPage()
154 {
155     KSharedConfig::Ptr config = KSharedConfig::openConfig();
156     KConfigGroup cgGeneral = KConfigGroup(config, "General");
157 
158     QWidget *sessionsPage = new QWidget();
159     auto item = addPage(sessionsPage, i18n("Session"));
160     item->setHeader(i18n("Session Management"));
161     item->setIcon(QIcon::fromTheme(QStringLiteral("view-history")));
162 
163     sessionConfigUi.setupUi(sessionsPage);
164 
165     // save meta infos
166     sessionConfigUi.saveMetaInfos->setChecked(KateApp::self()->documentManager()->getSaveMetaInfos());
167     connect(sessionConfigUi.saveMetaInfos, &QGroupBox::toggled, this, &KateConfigDialog::slotChanged);
168 
169     // meta infos days
170     sessionConfigUi.daysMetaInfos->setMaximum(180);
171     sessionConfigUi.daysMetaInfos->setSpecialValueText(i18nc("The special case of 'Delete unused meta-information after'", "(never)"));
172     sessionConfigUi.daysMetaInfos->setSuffix(ki18ncp("The suffix of 'Delete unused meta-information after'", " day", " days"));
173     sessionConfigUi.daysMetaInfos->setValue(KateApp::self()->documentManager()->getDaysMetaInfos());
174     connect(sessionConfigUi.daysMetaInfos,
175             static_cast<void (KPluralHandlingSpinBox::*)(int)>(&KPluralHandlingSpinBox::valueChanged),
176             this,
177             &KateConfigDialog::slotChanged);
178 
179     // restore view  config
180     sessionConfigUi.restoreVC->setChecked(cgGeneral.readEntry("Restore Window Configuration", true));
181     connect(sessionConfigUi.restoreVC, &QCheckBox::toggled, this, &KateConfigDialog::slotChanged);
182 
183     sessionConfigUi.spinBoxRecentFilesCount->setValue(recentFilesMaxCount());
184     connect(sessionConfigUi.spinBoxRecentFilesCount, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &KateConfigDialog::slotChanged);
185 
186     QString sesStart(cgGeneral.readEntry("Startup Session", "manual"));
187     if (sesStart == QLatin1String("new")) {
188         sessionConfigUi.startNewSessionRadioButton->setChecked(true);
189     } else if (sesStart == QLatin1String("last")) {
190         sessionConfigUi.loadLastUserSessionRadioButton->setChecked(true);
191     } else {
192         sessionConfigUi.manuallyChooseSessionRadioButton->setChecked(true);
193     }
194 
195     connect(sessionConfigUi.startNewSessionRadioButton, &QRadioButton::toggled, this, &KateConfigDialog::slotChanged);
196     connect(sessionConfigUi.loadLastUserSessionRadioButton, &QRadioButton::toggled, this, &KateConfigDialog::slotChanged);
197     connect(sessionConfigUi.manuallyChooseSessionRadioButton, &QRadioButton::toggled, this, &KateConfigDialog::slotChanged);
198 
199     // Closing last file closes Kate
200     sessionConfigUi.modCloseAfterLast->setChecked(m_mainWindow->modCloseAfterLast());
201     connect(sessionConfigUi.modCloseAfterLast, &QCheckBox::toggled, this, &KateConfigDialog::slotChanged);
202 
203     // stash unsave changes
204     sessionConfigUi.stashNewUnsavedFiles->setChecked(KateApp::self()->stashManager()->stashNewUnsavedFiles());
205     sessionConfigUi.stashUnsavedFilesChanges->setChecked(KateApp::self()->stashManager()->stashUnsavedChanges());
206     connect(sessionConfigUi.stashNewUnsavedFiles, &QRadioButton::toggled, this, &KateConfigDialog::slotChanged);
207     connect(sessionConfigUi.stashUnsavedFilesChanges, &QRadioButton::toggled, this, &KateConfigDialog::slotChanged);
208 }
209 
addPluginsPage()210 void KateConfigDialog::addPluginsPage()
211 {
212     QFrame *page = new QFrame(this);
213     QVBoxLayout *vlayout = new QVBoxLayout(page);
214     vlayout->setContentsMargins(0, 0, 0, 0);
215     vlayout->setSpacing(0);
216 
217     KateConfigPluginPage *configPluginPage = new KateConfigPluginPage(page, this);
218     vlayout->addWidget(configPluginPage);
219     connect(configPluginPage, &KateConfigPluginPage::changed, this, &KateConfigDialog::slotChanged);
220 
221     auto item = addPage(page, i18n("Plugins"));
222     item->setHeader(i18n("Plugin Manager"));
223     item->setIcon(QIcon::fromTheme(QStringLiteral("preferences-plugin")));
224 }
225 
addFeedbackPage()226 void KateConfigDialog::addFeedbackPage()
227 {
228 #ifdef WITH_KUSERFEEDBACK
229     // KUserFeedback Config
230     auto page = new QFrame(this);
231     auto vlayout = new QVBoxLayout(page);
232     vlayout->setContentsMargins(0, 0, 0, 0);
233     vlayout->setSpacing(0);
234 
235     m_userFeedbackWidget = new KUserFeedback::FeedbackConfigWidget(page);
236     m_userFeedbackWidget->setFeedbackProvider(&KateApp::self()->userFeedbackProvider());
237     connect(m_userFeedbackWidget, &KUserFeedback::FeedbackConfigWidget::configurationChanged, this, &KateConfigDialog::slotChanged);
238     vlayout->addWidget(m_userFeedbackWidget);
239 
240     auto item = addPage(page, i18n("User Feedback"));
241     item->setHeader(i18n("User Feedback"));
242     item->setIcon(QIcon::fromTheme(QStringLiteral("preferences-desktop-locale")));
243 #endif
244 }
245 
addPluginPages()246 void KateConfigDialog::addPluginPages()
247 {
248     const KatePluginList &pluginList(KateApp::self()->pluginManager()->pluginList());
249     for (const KatePluginInfo &plugin : pluginList) {
250         if (plugin.load) {
251             addPluginPage(plugin.plugin);
252         }
253     }
254 }
255 
addEditorPages()256 void KateConfigDialog::addEditorPages()
257 {
258     for (int i = 0; i < KTextEditor::Editor::instance()->configPages(); ++i) {
259         KTextEditor::ConfigPage *page = KTextEditor::Editor::instance()->configPage(i, this);
260         connect(page, &KTextEditor::ConfigPage::changed, this, &KateConfigDialog::slotChanged);
261         m_editorPages.push_back(page);
262         KPageWidgetItem *item = addPage(page, page->name());
263         item->setHeader(page->fullName());
264         item->setIcon(page->icon());
265     }
266 }
267 
addPluginPage(KTextEditor::Plugin * plugin)268 void KateConfigDialog::addPluginPage(KTextEditor::Plugin *plugin)
269 {
270     for (int i = 0; i < plugin->configPages(); i++) {
271         KTextEditor::ConfigPage *cp = plugin->configPage(i, this);
272         KPageWidgetItem *item = addPage(cp, cp->name());
273         item->setHeader(cp->fullName());
274         item->setIcon(cp->icon());
275 
276         PluginPageListItem info;
277         info.plugin = plugin;
278         info.pluginPage = cp;
279         info.idInPlugin = i;
280         info.pageWidgetItem = item;
281         connect(info.pluginPage, &KTextEditor::ConfigPage::changed, this, &KateConfigDialog::slotChanged);
282         m_pluginPages.insert(item, info);
283     }
284 }
285 
removePluginPage(KTextEditor::Plugin * plugin)286 void KateConfigDialog::removePluginPage(KTextEditor::Plugin *plugin)
287 {
288     QList<KPageWidgetItem *> remove;
289     for (QHash<KPageWidgetItem *, PluginPageListItem>::const_iterator it = m_pluginPages.constBegin(); it != m_pluginPages.constEnd(); ++it) {
290         const PluginPageListItem &pli = it.value();
291 
292         if (pli.plugin == plugin) {
293             remove.append(it.key());
294         }
295     }
296 
297     qCDebug(LOG_KATE) << remove.count();
298     while (!remove.isEmpty()) {
299         KPageWidgetItem *wItem = remove.takeLast();
300         PluginPageListItem item = m_pluginPages.take(wItem);
301         delete item.pluginPage;
302         removePage(wItem);
303     }
304 }
305 
slotApply()306 void KateConfigDialog::slotApply()
307 {
308     KSharedConfig::Ptr config = KSharedConfig::openConfig();
309 
310     // if data changed apply the kate app stuff
311     if (m_dataChanged) {
312         KConfigGroup cg = KConfigGroup(config, "General");
313 
314         cg.writeEntry("Restore Window Configuration", sessionConfigUi.restoreVC->isChecked());
315 
316         cg.writeEntry("Recent File List Entry Count", sessionConfigUi.spinBoxRecentFilesCount->value());
317 
318         if (sessionConfigUi.startNewSessionRadioButton->isChecked()) {
319             cg.writeEntry("Startup Session", "new");
320         } else if (sessionConfigUi.loadLastUserSessionRadioButton->isChecked()) {
321             cg.writeEntry("Startup Session", "last");
322         } else {
323             cg.writeEntry("Startup Session", "manual");
324         }
325 
326         cg.writeEntry("Save Meta Infos", sessionConfigUi.saveMetaInfos->isChecked());
327         KateApp::self()->documentManager()->setSaveMetaInfos(sessionConfigUi.saveMetaInfos->isChecked());
328 
329         cg.writeEntry("Days Meta Infos", sessionConfigUi.daysMetaInfos->value());
330         KateApp::self()->documentManager()->setDaysMetaInfos(sessionConfigUi.daysMetaInfos->value());
331 
332         cg.writeEntry("Modified Notification", m_modNotifications->isChecked());
333         m_mainWindow->setModNotificationEnabled(m_modNotifications->isChecked());
334 
335         cg.writeEntry("Close After Last", sessionConfigUi.modCloseAfterLast->isChecked());
336         m_mainWindow->setModCloseAfterLast(sessionConfigUi.modCloseAfterLast->isChecked());
337 
338         cg.writeEntry("Show output view for message type", m_messageTypes->currentIndex());
339 
340         cg.writeEntry("Stash unsaved file changes", sessionConfigUi.stashUnsavedFilesChanges->isChecked());
341         KateApp::self()->stashManager()->setStashUnsavedChanges(sessionConfigUi.stashUnsavedFilesChanges->isChecked());
342         cg.writeEntry("Stash new unsaved files", sessionConfigUi.stashNewUnsavedFiles->isChecked());
343         KateApp::self()->stashManager()->setStashNewUnsavedFiles(sessionConfigUi.stashNewUnsavedFiles->isChecked());
344 
345         cg.writeEntry("Tabbar Tab Limit", m_tabLimit->value());
346 
347         cg.writeEntry("Show Tabs Close Button", m_showTabCloseButton->isChecked());
348 
349         cg.writeEntry("Expand Tabs", m_expandTabs->isChecked());
350 
351         cg.writeEntry("Tab Double Click New Document", m_tabDoubleClickNewDocument->isChecked());
352         cg.writeEntry("Tab Middle Click Close Document", m_tabMiddleClickCloseDocument->isChecked());
353 
354         // patch document modified warn state
355         const QList<KTextEditor::Document *> &docs = KateApp::self()->documentManager()->documentList();
356         for (KTextEditor::Document *doc : docs) {
357             if (auto modIface = qobject_cast<KTextEditor::ModificationInterface *>(doc)) {
358                 modIface->setModifiedOnDiskWarning(!m_modNotifications->isChecked());
359             }
360         }
361 
362         m_mainWindow->saveOptions();
363 
364         // save plugin config !!
365         KateSessionManager *sessionmanager = KateApp::self()->sessionManager();
366         KConfig *sessionConfig = sessionmanager->activeSession()->config();
367         KateApp::self()->pluginManager()->writeConfig(sessionConfig);
368 
369 #ifdef WITH_KUSERFEEDBACK
370         // set current active mode + write back the config for future starts
371         KateApp::self()->userFeedbackProvider().setTelemetryMode(m_userFeedbackWidget->telemetryMode());
372         KateApp::self()->userFeedbackProvider().setSurveyInterval(m_userFeedbackWidget->surveyInterval());
373 #endif
374     }
375 
376     for (const PluginPageListItem &plugin : qAsConst(m_pluginPages)) {
377         if (plugin.pluginPage) {
378             plugin.pluginPage->apply();
379         }
380     }
381 
382     // apply ktexteditor pages
383     for (KTextEditor::ConfigPage *page : qAsConst(m_editorPages)) {
384         page->apply();
385     }
386 
387     config->sync();
388 
389     // emit config change
390     if (m_dataChanged) {
391         KateApp::self()->configurationChanged();
392     }
393 
394     m_dataChanged = false;
395     buttonBox()->button(QDialogButtonBox::Apply)->setEnabled(false);
396 }
397 
slotChanged()398 void KateConfigDialog::slotChanged()
399 {
400     m_dataChanged = true;
401     buttonBox()->button(QDialogButtonBox::Apply)->setEnabled(true);
402 }
403 
showAppPluginPage(KTextEditor::Plugin * p,int id)404 void KateConfigDialog::showAppPluginPage(KTextEditor::Plugin *p, int id)
405 {
406     for (const PluginPageListItem &plugin : qAsConst(m_pluginPages)) {
407         if ((plugin.plugin == p) && (id == plugin.idInPlugin)) {
408             setCurrentPage(plugin.pageWidgetItem);
409             break;
410         }
411     }
412 }
413 
slotHelp()414 void KateConfigDialog::slotHelp()
415 {
416     QDesktopServices::openUrl(QUrl(QStringLiteral("help:/")));
417 }
418 
recentFilesMaxCount()419 int KateConfigDialog::recentFilesMaxCount()
420 {
421     int maxItems = KConfigGroup(KSharedConfig::openConfig(), "General").readEntry("Recent File List Entry Count", 10);
422     return maxItems;
423 }
424 
closeEvent(QCloseEvent * event)425 void KateConfigDialog::closeEvent(QCloseEvent *event)
426 {
427     if (!m_dataChanged) {
428         event->accept();
429         return;
430     }
431 
432     const auto response = KMessageBox::warningYesNoCancel(this,
433                                                           i18n("You have unsaved changes. Do you want to apply the changes or discard them?"),
434                                                           i18n("Warning"),
435                                                           KStandardGuiItem::save(),
436                                                           KStandardGuiItem::discard(),
437                                                           KStandardGuiItem::cancel());
438     switch (response) {
439     case KMessageBox::Yes:
440         slotApply();
441         Q_FALLTHROUGH();
442     case KMessageBox::No:
443         event->accept();
444         break;
445     case KMessageBox::Cancel:
446         event->ignore();
447         break;
448     default:
449         break;
450     }
451 }
452