1 /*******************************************************************
2 KNotes -- Notes for the KDE project
3
4 SPDX-FileCopyrightText: 1997-2013 The KNotes Developers
5
6 SPDX-License-Identifier: GPL-2.0-or-later
7 *******************************************************************/
8
9 #include "knote.h"
10 #include "alarms/notealarmdialog.h"
11 #include "attributes/notealarmattribute.h"
12 #include "attributes/notedisplayattribute.h"
13 #include "attributes/notelockattribute.h"
14 #include "configdialog/knotesimpleconfigdialog.h"
15 #include "knotedisplaysettings.h"
16 #include "knoteedit.h"
17 #include "knotes_debug.h"
18 #include "knotesglobalconfig.h"
19 #include "notes/knotebutton.h"
20 #include "noteutils.h"
21 #include "print/knoteprinter.h"
22 #include "print/knoteprintobject.h"
23 #include "print/knoteprintselectthemedialog.h"
24 #include "utils/knoteutils.h"
25
26 #include <Akonadi/ItemModifyJob>
27
28 #include <AkonadiSearch/Debug/akonadisearchdebugdialog.h>
29
30 #include <KMime/KMimeMessage>
31
32 #include <KActionCollection>
33 #include <KComboBox>
34 #include <KFileCustomDialog>
35 #include <KIconEffect>
36 #include <KLocalizedString>
37 #include <KMessageBox>
38 #include <KToggleAction>
39 #include <KToolBar>
40 #include <KWindowSystem>
41 #include <KXMLGUIBuilder>
42 #include <KXMLGUIFactory>
43
44 #include <QApplication>
45 #include <QCheckBox>
46 #include <QDesktopWidget>
47 #include <QInputDialog>
48 #include <QLabel>
49 #include <QMenu>
50 #include <QMimeData>
51 #include <QPointer>
52 #include <QSizeGrip>
53 #include <QTextEdit>
54 #include <QVBoxLayout>
55
56 #if KDEPIM_HAVE_X11
57 #include <KWindowSystem/NETWM>
58 #include <fixx11h.h>
59 #endif
60
61 //#define DEBUG_SAVE_NOTE 1
62
KNote(const QDomDocument & buildDoc,const Akonadi::Item & item,bool allowAkonadiSearchDebug,QWidget * parent)63 KNote::KNote(const QDomDocument &buildDoc, const Akonadi::Item &item, bool allowAkonadiSearchDebug, QWidget *parent)
64 : QFrame(parent, Qt::FramelessWindowHint)
65 , mItem(item)
66 , m_kwinConf(KSharedConfig::openConfig(QStringLiteral("kwinrc")))
67 , mDisplayAttribute(new KNoteDisplaySettings)
68 , mAllowDebugAkonadiSearch(allowAkonadiSearchDebug)
69 {
70 if (mItem.hasAttribute<NoteShared::NoteDisplayAttribute>()) {
71 mDisplayAttribute->setDisplayAttribute(mItem.attribute<NoteShared::NoteDisplayAttribute>());
72 } else {
73 setDisplayDefaultValue();
74 // save default display value
75 }
76 setAcceptDrops(true);
77 setAttribute(Qt::WA_DeleteOnClose);
78 setDOMDocument(buildDoc);
79 setXMLFile(componentName() + QLatin1String("ui.rc"), false, false);
80
81 // create the main layout
82 m_noteLayout = new QVBoxLayout(this);
83 m_noteLayout->setContentsMargins(0, 0, 0, 0);
84 createActions();
85
86 buildGui();
87 prepare();
88 }
89
~KNote()90 KNote::~KNote()
91 {
92 delete mDisplayAttribute;
93 }
94
setDisplayDefaultValue()95 void KNote::setDisplayDefaultValue()
96 {
97 KNoteUtils::setDefaultValue(mItem);
98 auto job = new Akonadi::ItemModifyJob(mItem);
99 #ifdef DEBUG_SAVE_NOTE
100 qCDebug(KNOTES_LOG) << "setDisplayDefaultValue slotNoteSaved(KJob*)";
101 #endif
102 connect(job, &Akonadi::ItemModifyJob::result, this, &KNote::slotNoteSaved);
103 }
104
setChangeItem(const Akonadi::Item & item,const QSet<QByteArray> & set)105 void KNote::setChangeItem(const Akonadi::Item &item, const QSet<QByteArray> &set)
106 {
107 mItem = item;
108 if (item.hasAttribute<NoteShared::NoteDisplayAttribute>()) {
109 mDisplayAttribute->setDisplayAttribute(item.attribute<NoteShared::NoteDisplayAttribute>());
110 }
111 if (set.contains("KJotsLockAttribute")) {
112 m_editor->setReadOnly(item.hasAttribute<NoteShared::NoteLockAttribute>());
113 }
114 if (set.contains("PLD:RFC822")) {
115 loadNoteContent(item);
116 }
117 if (set.contains("NoteDisplayAttribute")) {
118 qCDebug(KNOTES_LOG) << " ATR:NoteDisplayAttribute";
119 slotApplyConfig();
120 }
121 // TODO update display/content etc.
122 updateLabelAlignment();
123 }
124
slotKill(bool force)125 void KNote::slotKill(bool force)
126 {
127 if (!force
128 && KMessageBox::warningContinueCancel(this,
129 i18n("<qt>Do you really want to delete note <b>%1</b>?</qt>", m_label->text()),
130 i18n("Confirm Delete"),
131 KGuiItem(i18n("&Delete"), QStringLiteral("edit-delete")),
132 KStandardGuiItem::cancel(),
133 QStringLiteral("ConfirmDeleteNote"))
134 != KMessageBox::Continue) {
135 return;
136 }
137
138 Q_EMIT sigKillNote(mItem.id());
139 }
140
141 // -------------------- public member functions -------------------- //
142
saveNote(bool force,bool sync)143 void KNote::saveNote(bool force, bool sync)
144 {
145 if (!force && !m_editor->document()->isModified()) {
146 return;
147 }
148 bool needToSave = false;
149 auto attribute = mItem.attribute<NoteShared::NoteDisplayAttribute>(Akonadi::Item::AddIfMissing);
150 const QPoint notePosition = pos();
151 if (attribute->position() != notePosition) {
152 needToSave = true;
153 attribute->setPosition(notePosition);
154 }
155 const QSize currentSize(QSize(width(), height()));
156 if (attribute->size() != currentSize) {
157 needToSave = true;
158 attribute->setSize(currentSize);
159 }
160 #if KDEPIM_HAVE_X11
161 KWindowInfo info(winId(), NET::WMDesktop);
162 const int count = KWindowSystem::numberOfDesktops();
163 for (int n = 1; n <= count; ++n) {
164 if (info.isOnDesktop(n)) {
165 if (attribute->desktop() != n) {
166 needToSave = true;
167 attribute->setDesktop(n);
168 break;
169 }
170 }
171 }
172 #endif
173 if (m_editor->document()->isModified()) {
174 needToSave = true;
175 saveNoteContent();
176 }
177 if (needToSave) {
178 #ifdef DEBUG_SAVE_NOTE
179 qCDebug(KNOTES_LOG) << "save Note slotClose() slotNoteSaved(KJob*) : sync" << sync;
180 #endif
181 auto job = new Akonadi::ItemModifyJob(mItem);
182 if (sync) {
183 job->exec();
184 } else {
185 #ifdef DEBUG_SAVE_NOTE
186 qCDebug(KNOTES_LOG) << "save Note slotClose() slotNoteSaved(KJob*)";
187 #endif
188 connect(job, &Akonadi::ItemModifyJob::result, this, &KNote::slotNoteSaved);
189 }
190 }
191 }
192
slotNoteSaved(KJob * job)193 void KNote::slotNoteSaved(KJob *job)
194 {
195 qCDebug(KNOTES_LOG) << " void KNote::slotNoteSaved(KJob *job)";
196 if (job->error()) {
197 qCDebug(KNOTES_LOG) << " problem during save note:" << job->errorString();
198 } else {
199 m_editor->document()->setModified(false);
200 }
201 }
202
noteId() const203 Akonadi::Item::Id KNote::noteId() const
204 {
205 return mItem.id();
206 }
207
name() const208 QString KNote::name() const
209 {
210 return m_label->text();
211 }
212
text() const213 QString KNote::text() const
214 {
215 return m_editor->text();
216 }
217
setName(const QString & name)218 void KNote::setName(const QString &name)
219 {
220 m_label->setText(name);
221 updateLabelAlignment();
222
223 if (m_editor) { // not called from CTOR?
224 saveNote();
225 }
226 setWindowTitle(name);
227
228 Q_EMIT sigNameChanged(name);
229 }
230
setText(const QString & text)231 void KNote::setText(const QString &text)
232 {
233 m_editor->setText(text);
234
235 saveNote();
236 }
237
isDesktopAssigned() const238 bool KNote::isDesktopAssigned() const
239 {
240 return mDisplayAttribute->rememberDesktop();
241 }
242
isModified() const243 bool KNote::isModified() const
244 {
245 return m_editor->document()->isModified();
246 }
247
248 // ------------------ private slots (menu actions) ------------------ //
249
slotRename()250 void KNote::slotRename()
251 {
252 // pop up dialog to get the new name
253 bool ok;
254 const QString oldName = m_label->text();
255 const QString newName = QInputDialog::getText(this, QString(), i18n("Please enter the new name:"), QLineEdit::Normal, m_label->text(), &ok);
256 if (!ok || (oldName == newName)) { // handle cancel
257 return;
258 }
259
260 setName(newName);
261 }
262
slotUpdateReadOnly()263 void KNote::slotUpdateReadOnly()
264 {
265 const bool readOnly = m_readOnly->isChecked();
266
267 m_editor->setReadOnly(readOnly);
268
269 if (mItem.hasAttribute<NoteShared::NoteLockAttribute>()) {
270 if (!readOnly) {
271 mItem.removeAttribute<NoteShared::NoteLockAttribute>();
272 }
273 } else {
274 if (readOnly) {
275 mItem.attribute<NoteShared::NoteLockAttribute>(Akonadi::Item::AddIfMissing);
276 }
277 }
278 if (!mBlockSave) {
279 updateAllAttributes();
280 auto job = new Akonadi::ItemModifyJob(mItem);
281 #ifdef DEBUG_SAVE_NOTE
282 qCDebug(KNOTES_LOG) << " void KNote::slotUpdateReadOnly() slotNoteSaved(KJob*)";
283 #endif
284 connect(job, &Akonadi::ItemModifyJob::result, this, &KNote::slotNoteSaved);
285 }
286
287 // enable/disable actions accordingly
288 actionCollection()->action(QStringLiteral("configure_note"))->setEnabled(!readOnly);
289 actionCollection()->action(QStringLiteral("delete_note"))->setEnabled(!readOnly);
290 actionCollection()->action(QStringLiteral("format_bold"))->setEnabled(!readOnly);
291 actionCollection()->action(QStringLiteral("format_italic"))->setEnabled(!readOnly);
292 actionCollection()->action(QStringLiteral("format_underline"))->setEnabled(!readOnly);
293 actionCollection()->action(QStringLiteral("format_strikeout"))->setEnabled(!readOnly);
294 actionCollection()->action(QStringLiteral("format_alignleft"))->setEnabled(!readOnly);
295 actionCollection()->action(QStringLiteral("format_aligncenter"))->setEnabled(!readOnly);
296 actionCollection()->action(QStringLiteral("format_alignright"))->setEnabled(!readOnly);
297 actionCollection()->action(QStringLiteral("format_alignblock"))->setEnabled(!readOnly);
298 actionCollection()->action(QStringLiteral("format_list"))->setEnabled(!readOnly);
299 actionCollection()->action(QStringLiteral("format_super"))->setEnabled(!readOnly);
300 actionCollection()->action(QStringLiteral("format_sub"))->setEnabled(!readOnly);
301 actionCollection()->action(QStringLiteral("format_increaseindent"))->setEnabled(!readOnly);
302 actionCollection()->action(QStringLiteral("format_decreaseindent"))->setEnabled(!readOnly);
303 actionCollection()->action(QStringLiteral("text_background_color"))->setEnabled(!readOnly);
304 actionCollection()->action(QStringLiteral("format_size"))->setEnabled(!readOnly);
305 actionCollection()->action(QStringLiteral("format_color"))->setEnabled(!readOnly);
306 actionCollection()->action(QStringLiteral("rename_note"))->setEnabled(!readOnly);
307 actionCollection()->action(QStringLiteral("set_alarm"))->setEnabled(!readOnly);
308 m_keepAbove->setEnabled(!readOnly);
309 m_keepBelow->setEnabled(!readOnly);
310
311 #if KDEPIM_HAVE_X11
312 m_toDesktop->setEnabled(!readOnly);
313 #endif
314
315 updateFocus();
316 }
317
updateAllAttributes()318 void KNote::updateAllAttributes()
319 {
320 auto attribute = mItem.attribute<NoteShared::NoteDisplayAttribute>(Akonadi::Item::AddIfMissing);
321 #if KDEPIM_HAVE_X11
322 KWindowInfo info(winId(), NET::WMDesktop);
323 const int count = KWindowSystem::numberOfDesktops();
324 for (int n = 1; n <= count; ++n) {
325 if (info.isOnDesktop(n)) {
326 attribute->setDesktop(n);
327 }
328 }
329 #endif
330 saveNoteContent();
331 attribute->setIsHidden(true);
332 attribute->setPosition(pos());
333 const QSize currentSize(QSize(width(), height()));
334 if (attribute->size() != currentSize) {
335 attribute->setSize(currentSize);
336 }
337 }
338
slotClose()339 void KNote::slotClose()
340 {
341 updateAllAttributes();
342 m_editor->clearFocus();
343 auto job = new Akonadi::ItemModifyJob(mItem);
344 #ifdef DEBUG_SAVE_NOTE
345 qCDebug(KNOTES_LOG) << "slotClose() slotNoteSaved(KJob*)";
346 #endif
347 connect(job, &Akonadi::ItemModifyJob::result, this, &KNote::slotNoteSaved);
348 hide();
349 }
350
slotSetAlarm()351 void KNote::slotSetAlarm()
352 {
353 QPointer<NoteShared::NoteAlarmDialog> dlg = new NoteShared::NoteAlarmDialog(name(), this);
354 if (mItem.hasAttribute<NoteShared::NoteAlarmAttribute>()) {
355 dlg->setAlarm(mItem.attribute<NoteShared::NoteAlarmAttribute>()->dateTime());
356 }
357 if (dlg->exec()) {
358 bool needToModify = true;
359 QDateTime dateTime = dlg->alarm();
360 if (dateTime.isValid()) {
361 auto attribute = mItem.attribute<NoteShared::NoteAlarmAttribute>(Akonadi::Item::AddIfMissing);
362 attribute->setDateTime(dateTime);
363 } else {
364 if (mItem.hasAttribute<NoteShared::NoteAlarmAttribute>()) {
365 mItem.removeAttribute<NoteShared::NoteAlarmAttribute>();
366 } else {
367 needToModify = false;
368 }
369 }
370 if (needToModify) {
371 // Verify it!
372 saveNoteContent();
373 auto job = new Akonadi::ItemModifyJob(mItem);
374 #ifdef DEBUG_SAVE_NOTE
375 qCDebug(KNOTES_LOG) << "setAlarm() slotNoteSaved(KJob*)";
376 #endif
377 connect(job, &Akonadi::ItemModifyJob::result, this, &KNote::slotNoteSaved);
378 }
379 }
380 delete dlg;
381 }
382
saveNoteContent()383 void KNote::saveNoteContent()
384 {
385 auto message = mItem.payload<KMime::Message::Ptr>();
386 const QByteArray encoding("utf-8");
387 message->subject(true)->fromUnicodeString(name(), encoding);
388 message->contentType(true)->setMimeType(m_editor->acceptRichText() ? "text/html" : "text/plain");
389 message->contentType()->setCharset(encoding);
390 message->contentTransferEncoding(true)->setEncoding(KMime::Headers::CEquPr);
391 message->date(true)->setDateTime(QDateTime::currentDateTime());
392 message->mainBodyPart()->fromUnicodeString(text().isEmpty() ? QStringLiteral(" ") : text());
393
394 auto header = new KMime::Headers::Generic("X-Cursor-Position");
395 header->fromUnicodeString(QString::number(m_editor->cursorPositionFromStart()), "utf-8");
396 message->setHeader(header);
397
398 message->assemble();
399
400 mItem.setPayload(message);
401 }
402
slotPreferences()403 void KNote::slotPreferences()
404 {
405 // create a new preferences dialog...
406 QPointer<KNoteSimpleConfigDialog> dialog = new KNoteSimpleConfigDialog(name(), this);
407 auto attribute = mItem.attribute<NoteShared::NoteDisplayAttribute>(Akonadi::Item::AddIfMissing);
408 attribute->setSize(QSize(width(), height()));
409
410 dialog->load(mItem, m_editor->acceptRichText());
411 connect(this, &KNote::sigNameChanged, dialog.data(), &KNoteSimpleConfigDialog::slotUpdateCaption);
412 if (dialog->exec()) {
413 bool isRichText;
414 dialog->save(mItem, isRichText);
415 m_editor->setAcceptRichText(isRichText);
416 saveNoteContent();
417 auto job = new Akonadi::ItemModifyJob(mItem);
418 #ifdef DEBUG_SAVE_NOTE
419 qCDebug(KNOTES_LOG) << "slotPreference slotNoteSaved(KJob*)";
420 #endif
421 connect(job, &Akonadi::ItemModifyJob::result, this, &KNote::slotNoteSaved);
422 }
423 delete dialog;
424 }
425
slotSend()426 void KNote::slotSend()
427 {
428 NoteShared::NoteUtils noteUtils;
429 noteUtils.sendToNetwork(this, name(), text());
430 }
431
slotMail()432 void KNote::slotMail()
433 {
434 NoteShared::NoteUtils noteUtils;
435 noteUtils.sendToMail(this, m_label->text(), m_editor->toPlainText());
436 }
437
slotPrint()438 void KNote::slotPrint()
439 {
440 print(false);
441 }
442
slotPrintPreview()443 void KNote::slotPrintPreview()
444 {
445 print(true);
446 }
447
print(bool preview)448 void KNote::print(bool preview)
449 {
450 if (isModified()) {
451 saveNote();
452 }
453
454 KNotesGlobalConfig *globalConfig = KNotesGlobalConfig::self();
455 QString printingTheme = globalConfig->theme();
456 if (printingTheme.isEmpty()) {
457 QPointer<KNotePrintSelectThemeDialog> dlg = new KNotePrintSelectThemeDialog(this);
458 if (dlg->exec()) {
459 printingTheme = dlg->selectedTheme();
460 }
461 delete dlg;
462 }
463 if (!printingTheme.isEmpty()) {
464 KNotePrinter printer(this);
465 QList<KNotePrintObject *> lst;
466 lst.append(new KNotePrintObject(mItem));
467 printer.setDefaultFont(mDisplayAttribute->font());
468 printer.printNotes(lst, printingTheme, preview);
469 qDeleteAll(lst);
470 }
471 }
472
slotSaveAs()473 void KNote::slotSaveAs()
474 {
475 // TODO: where to put pdf file support? In the printer??!??!
476 QCheckBox *convert = nullptr;
477 if (m_editor->acceptRichText()) {
478 convert = new QCheckBox(nullptr);
479 convert->setText(i18n("Save note as plain text"));
480 }
481 QUrl url;
482 QPointer<KFileCustomDialog> dlg = new KFileCustomDialog(this);
483 dlg->setCustomWidget(convert);
484 dlg->setUrl(url);
485 dlg->setOperationMode(KFileWidget::Saving);
486 dlg->setWindowTitle(i18nc("@title:window", "Save As"));
487 if (!dlg->exec()) {
488 delete dlg;
489 return;
490 }
491
492 const QString fileName = dlg->fileWidget()->selectedFile();
493 const bool htmlFormatAndSaveAsHtml = (convert && !convert->isChecked());
494 delete dlg;
495 if (fileName.isEmpty()) {
496 return;
497 }
498
499 QFile file(fileName);
500
501 if (file.exists()
502 && KMessageBox::warningContinueCancel(this,
503 i18n("<qt>A file named <b>%1</b> already exists.<br />"
504 "Are you sure you want to overwrite it?</qt>",
505 QFileInfo(file).fileName()))
506 != KMessageBox::Continue) {
507 return;
508 }
509
510 if (file.open(QIODevice::WriteOnly)) {
511 QTextStream stream(&file);
512 if (htmlFormatAndSaveAsHtml) {
513 QString htmlStr = m_editor->toHtml();
514 htmlStr.replace(QStringLiteral("meta name=\"qrichtext\" content=\"1\""),
515 QStringLiteral("meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\""));
516 stream << htmlStr;
517 } else {
518 stream << m_editor->toPlainText();
519 }
520 }
521 }
522
slotPopupActionToDesktop(QAction * act)523 void KNote::slotPopupActionToDesktop(QAction *act)
524 {
525 const int id = act->data().toInt();
526 toDesktop(id); // compensate for the menu separator, -1 == all desktops
527 }
528
529 // ------------------ private slots (configuration) ------------------ //
530
slotApplyConfig()531 void KNote::slotApplyConfig()
532 {
533 m_label->setFont(mDisplayAttribute->titleFont());
534 m_editor->setTextFont(mDisplayAttribute->font());
535 m_editor->setTabStop(mDisplayAttribute->tabSize());
536 m_editor->setAutoIndentMode(mDisplayAttribute->autoIndent());
537
538 setColor(mDisplayAttribute->foregroundColor(), mDisplayAttribute->backgroundColor());
539
540 updateLayout();
541 #if KDEPIM_HAVE_X11
542 slotUpdateShowInTaskbar();
543 #endif
544 resize(mDisplayAttribute->size());
545 }
546
slotKeepAbove()547 void KNote::slotKeepAbove()
548 {
549 if (m_keepBelow->isChecked()) {
550 m_keepBelow->setChecked(false);
551 }
552 updateKeepAboveBelow();
553 }
554
slotKeepBelow()555 void KNote::slotKeepBelow()
556 {
557 if (m_keepAbove->isChecked()) {
558 m_keepAbove->setChecked(false);
559 }
560 updateKeepAboveBelow();
561 }
562
updateKeepAboveBelow(bool save)563 void KNote::updateKeepAboveBelow(bool save)
564 {
565 #if KDEPIM_HAVE_X11
566 NET::States state = KWindowInfo(winId(), NET::WMState).state();
567 #else
568 NET::States state = {}; // neutral state, TODO
569 #endif
570 auto attribute = mItem.attribute<NoteShared::NoteDisplayAttribute>(Akonadi::Item::AddIfMissing);
571 if (m_keepAbove->isChecked()) {
572 attribute->setKeepAbove(true);
573 attribute->setKeepBelow(false);
574 KWindowSystem::setState(winId(), state | NET::KeepAbove);
575 } else if (m_keepBelow->isChecked()) {
576 attribute->setKeepAbove(false);
577 attribute->setKeepBelow(true);
578 KWindowSystem::setState(winId(), state | NET::KeepBelow);
579 } else {
580 attribute->setKeepAbove(false);
581 attribute->setKeepBelow(false);
582 KWindowSystem::clearState(winId(), NET::KeepAbove);
583 KWindowSystem::clearState(winId(), NET::KeepBelow);
584 }
585 if (!mBlockSave && save) {
586 saveNoteContent();
587 auto job = new Akonadi::ItemModifyJob(mItem);
588 #ifdef DEBUG_SAVE_NOTE
589 qCDebug(KNOTES_LOG) << "slotUpdateKeepAboveBelow slotNoteSaved(KJob*)";
590 #endif
591 connect(job, &Akonadi::ItemModifyJob::result, this, &KNote::slotNoteSaved);
592 }
593 }
594
slotUpdateShowInTaskbar()595 void KNote::slotUpdateShowInTaskbar()
596 {
597 #if KDEPIM_HAVE_X11
598 if (!mDisplayAttribute->showInTaskbar()) {
599 KWindowSystem::setState(winId(), KWindowInfo(winId(), NET::WMState).state() | NET::SkipTaskbar);
600 } else {
601 KWindowSystem::clearState(winId(), NET::SkipTaskbar);
602 }
603 #endif
604 }
605
slotUpdateDesktopActions()606 void KNote::slotUpdateDesktopActions()
607 {
608 #if KDEPIM_HAVE_X11
609 m_toDesktop->clear();
610
611 QAction *act = m_toDesktop->addAction(i18n("&All Desktops"));
612 KWindowInfo info(winId(), NET::WMDesktop);
613
614 if (info.onAllDesktops()) {
615 act->setChecked(true);
616 act->setData(NETWinInfo::OnAllDesktops);
617 }
618 auto separator = new QAction(m_toDesktop);
619 separator->setSeparator(true);
620 m_toDesktop->addAction(separator);
621 const int count = KWindowSystem::numberOfDesktops();
622 for (int n = 1; n <= count; ++n) {
623 QAction *desktopAct = m_toDesktop->addAction(QStringLiteral("&%1 %2").arg(n).arg(KWindowSystem::desktopName(n)));
624 desktopAct->setData(n);
625 if (info.isOnDesktop(n)) {
626 desktopAct->setChecked(true);
627 }
628 }
629 #endif
630 }
631
632 // -------------------- private methods -------------------- //
633
buildGui()634 void KNote::buildGui()
635 {
636 createNoteHeader();
637 createNoteEditor(QString());
638
639 KXMLGUIBuilder builder(this);
640 KXMLGUIFactory factory(&builder, this);
641 factory.addClient(this);
642
643 m_menu = qobject_cast<QMenu *>(factory.container(QStringLiteral("note_context"), this));
644 m_tool = qobject_cast<KToolBar *>(factory.container(QStringLiteral("note_tool"), this));
645
646 createNoteFooter();
647 }
648
createActions()649 void KNote::createActions()
650 {
651 // create the menu items for the note - not the editor...
652 // rename, mail, print, save as, insert date, alarm, close, delete, new note
653 auto action = new QAction(QIcon::fromTheme(QStringLiteral("document-new")), i18n("New"), this);
654 actionCollection()->addAction(QStringLiteral("new_note"), action);
655 connect(action, &QAction::triggered, this, &KNote::slotRequestNewNote);
656
657 action = new QAction(QIcon::fromTheme(QStringLiteral("edit-rename")), i18n("Rename..."), this);
658 actionCollection()->addAction(QStringLiteral("rename_note"), action);
659 connect(action, &QAction::triggered, this, &KNote::slotRename);
660
661 m_readOnly = new KToggleAction(QIcon::fromTheme(QStringLiteral("object-locked")), i18n("Lock"), this);
662 actionCollection()->addAction(QStringLiteral("lock_note"), m_readOnly);
663 connect(m_readOnly, &KToggleAction::triggered, this, &KNote::slotUpdateReadOnly);
664 m_readOnly->setCheckedState(KGuiItem(i18n("Unlock"), QStringLiteral("object-unlocked")));
665
666 action = new QAction(QIcon::fromTheme(QStringLiteral("window-close")), i18n("Hide"), this);
667 actionCollection()->addAction(QStringLiteral("hide_note"), action);
668 connect(action, &QAction::triggered, this, &KNote::slotClose);
669 actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::Key_Escape));
670
671 action = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Delete"), this);
672 actionCollection()->addAction(QStringLiteral("delete_note"), action);
673 connect(action, &QAction::triggered, this, &KNote::slotKill);
674
675 action = new QAction(QIcon::fromTheme(QStringLiteral("knotes_alarm")), i18n("Set Alarm..."), this);
676 actionCollection()->addAction(QStringLiteral("set_alarm"), action);
677 connect(action, &QAction::triggered, this, &KNote::slotSetAlarm);
678
679 action = new QAction(QIcon::fromTheme(QStringLiteral("network-wired")), i18n("Send..."), this);
680 actionCollection()->addAction(QStringLiteral("send_note"), action);
681 connect(action, &QAction::triggered, this, &KNote::slotSend);
682
683 action = new QAction(QIcon::fromTheme(QStringLiteral("mail-send")), i18n("Mail..."), this);
684 actionCollection()->addAction(QStringLiteral("mail_note"), action);
685 connect(action, &QAction::triggered, this, &KNote::slotMail);
686
687 action = new QAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("Save As..."), this);
688 actionCollection()->addAction(QStringLiteral("save_note"), action);
689 connect(action, &QAction::triggered, this, &KNote::slotSaveAs);
690 action = actionCollection()->addAction(KStandardAction::Print, QStringLiteral("print_note"));
691 connect(action, &QAction::triggered, this, &KNote::slotPrint);
692
693 action = actionCollection()->addAction(KStandardAction::PrintPreview, QStringLiteral("print_preview_note"));
694 connect(action, &QAction::triggered, this, &KNote::slotPrintPreview);
695
696 action = new QAction(QIcon::fromTheme(QStringLiteral("configure")), i18n("Preferences..."), this);
697 actionCollection()->addAction(QStringLiteral("configure_note"), action);
698 connect(action, &QAction::triggered, this, &KNote::slotPreferences);
699
700 m_keepAbove = new KToggleAction(QIcon::fromTheme(QStringLiteral("go-up")), i18n("Keep Above Others"), this);
701 actionCollection()->addAction(QStringLiteral("keep_above"), m_keepAbove);
702 connect(m_keepAbove, &KToggleAction::triggered, this, &KNote::slotKeepAbove);
703
704 m_keepBelow = new KToggleAction(QIcon::fromTheme(QStringLiteral("go-down")), i18n("Keep Below Others"), this);
705 actionCollection()->addAction(QStringLiteral("keep_below"), m_keepBelow);
706 connect(m_keepBelow, &KToggleAction::triggered, this, &KNote::slotKeepBelow);
707
708 #if KDEPIM_HAVE_X11
709 m_toDesktop = new KSelectAction(i18n("To Desktop"), this);
710 actionCollection()->addAction(QStringLiteral("to_desktop"), m_toDesktop);
711 connect(m_toDesktop, qOverload<QAction *>(&KSelectAction::triggered), this, &KNote::slotPopupActionToDesktop);
712 connect(m_toDesktop->menu(), &QMenu::aboutToShow, this, &KNote::slotUpdateDesktopActions);
713 // initially populate it, otherwise stays disabled
714 slotUpdateDesktopActions();
715 #endif
716 // invisible action to walk through the notes to make this configurable
717 action = new QAction(i18n("Walk Through Notes"), this);
718 actionCollection()->addAction(QStringLiteral("walk_notes"), action);
719 connect(action, &QAction::triggered, this, &KNote::sigShowNextNote);
720 actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::SHIFT | Qt::Key_Backtab));
721
722 actionCollection()->addAssociatedWidget(this);
723 const auto lst = actionCollection()->actions();
724 for (QAction *act : lst) {
725 act->setShortcutContext(Qt::WidgetWithChildrenShortcut);
726 }
727 if (mAllowDebugAkonadiSearch) {
728 // Don't translate it it's just for debugging
729 action = new QAction(QStringLiteral("Debug Akonadi Search..."), this);
730 actionCollection()->addAction(QStringLiteral("debug_akonadi_search"), action);
731 connect(action, &QAction::triggered, this, &KNote::slotDebugAkonadiSearch);
732 }
733 }
734
createNoteHeader()735 void KNote::createNoteHeader()
736 {
737 // load style configuration
738 KConfigGroup styleGroup(m_kwinConf, "Style");
739
740 QBoxLayout::Direction headerLayoutDirection = QBoxLayout::LeftToRight;
741
742 if (styleGroup.readEntry("CustomButtonPositions", false)) {
743 if (styleGroup.readEntry("ButtonsOnLeft").contains(QLatin1Char('X'))) {
744 headerLayoutDirection = QBoxLayout::RightToLeft;
745 }
746 }
747
748 auto headerLayout = new QBoxLayout(headerLayoutDirection);
749
750 // create header label
751 m_label = new QLabel(this);
752 headerLayout->addWidget(m_label);
753 m_label->setFrameStyle(NoFrame);
754 m_label->setBackgroundRole(QPalette::Base);
755 m_label->setLineWidth(0);
756 m_label->setAutoFillBackground(true);
757 m_label->installEventFilter(this); // receive events ( for dragging &
758 // action menu )
759 m_button = new KNoteButton(QStringLiteral("knotes_close"), this);
760 headerLayout->addWidget(m_button);
761
762 connect(m_button, &KNoteButton::clicked, this, &KNote::slotClose);
763
764 m_noteLayout->addLayout(headerLayout);
765 }
766
createNoteEditor(const QString & configFile)767 void KNote::createNoteEditor(const QString &configFile)
768 {
769 Q_UNUSED(configFile)
770 m_editor = new KNoteEdit(actionCollection(), this);
771 m_noteLayout->addWidget(m_editor);
772 m_editor->setNote(this);
773 m_editor->installEventFilter(this); // receive focus events for modified
774 setFocusProxy(m_editor);
775 }
776
slotRequestNewNote()777 void KNote::slotRequestNewNote()
778 {
779 // Be sure to save before to request a new note
780 saveNote();
781 Q_EMIT sigRequestNewNote();
782 }
783
createNoteFooter()784 void KNote::createNoteFooter()
785 {
786 if (m_tool) {
787 m_tool->setIconSize(QSize(10, 10));
788 m_tool->setFixedHeight(24);
789 m_tool->setToolButtonStyle(Qt::ToolButtonIconOnly);
790 }
791
792 // create size grip
793 auto gripLayout = new QHBoxLayout;
794 m_grip = new QSizeGrip(this);
795 m_grip->setFixedSize(m_grip->sizeHint());
796
797 if (m_tool) {
798 gripLayout->addWidget(m_tool);
799 gripLayout->setAlignment(m_tool, Qt::AlignBottom | Qt::AlignLeft);
800 m_tool->hide();
801 }
802
803 gripLayout->addWidget(m_grip);
804 gripLayout->setAlignment(m_grip, Qt::AlignBottom | Qt::AlignRight);
805 m_noteLayout->addLayout(gripLayout);
806
807 // if there was just a way of making KComboBox adhere the toolbar height...
808 if (m_tool) {
809 const auto comboboxs = m_tool->findChildren<KComboBox *>();
810 for (KComboBox *combo : comboboxs) {
811 QFont font = combo->font();
812 font.setPointSize(7);
813 combo->setFont(font);
814 combo->setFixedHeight(14);
815 }
816 }
817 }
818
loadNoteContent(const Akonadi::Item & item)819 void KNote::loadNoteContent(const Akonadi::Item &item)
820 {
821 auto noteMessage = item.payload<KMime::Message::Ptr>();
822 const KMime::Headers::Subject *const subject = noteMessage ? noteMessage->subject(false) : nullptr;
823 setName(subject ? subject->asUnicodeString() : QString());
824 if (noteMessage->contentType()->isHTMLText()) {
825 m_editor->setAcceptRichText(true);
826 m_editor->setAutoFormatting(QTextEdit::AutoAll);
827 m_editor->setHtml(noteMessage->mainBodyPart()->decodedText());
828 } else {
829 m_editor->setAcceptRichText(false);
830 m_editor->setAutoFormatting(QTextEdit::AutoNone);
831 m_editor->setPlainText(noteMessage->mainBodyPart()->decodedText());
832 }
833 if (auto hrd = noteMessage->headerByType("X-Cursor-Position")) {
834 m_editor->setCursorPositionFromStart(hrd->asUnicodeString().toInt());
835 }
836 }
837
prepare()838 void KNote::prepare()
839 {
840 mBlockSave = true;
841 loadNoteContent(mItem);
842
843 resize(mDisplayAttribute->size());
844 const QPoint &position = mDisplayAttribute->position();
845 QRect desk = qApp->desktop()->rect();
846 desk.adjust(10, 10, -10, -10);
847 if (desk.intersects(QRect(position, mDisplayAttribute->size()))) {
848 move(position); // do before calling show() to avoid flicker
849 }
850 if (mDisplayAttribute->isHidden()) {
851 hide();
852 } else {
853 show();
854 }
855 // read configuration settings...
856 slotApplyConfig();
857
858 if (mItem.hasAttribute<NoteShared::NoteLockAttribute>()) {
859 m_editor->setReadOnly(true);
860 m_readOnly->setChecked(true);
861 } else {
862 m_readOnly->setChecked(false);
863 }
864 slotUpdateReadOnly();
865 // if this is a new note put on current desktop - we can't use defaults
866 // in KConfig XT since only _changes_ will be stored in the config file
867 int desktop = mDisplayAttribute->desktop();
868
869 #if KDEPIM_HAVE_X11
870 if ((desktop < 0 && desktop != NETWinInfo::OnAllDesktops) || !mDisplayAttribute->rememberDesktop()) {
871 desktop = KWindowSystem::currentDesktop();
872 }
873 #endif
874
875 // show the note if desired
876 if (desktop != 0 && !mDisplayAttribute->isHidden()) {
877 // to avoid flicker, call this before show()
878 toDesktop(desktop);
879 show();
880
881 // because KWin forgets about that for hidden windows
882 #if KDEPIM_HAVE_X11
883 if (desktop == NETWinInfo::OnAllDesktops) {
884 toDesktop(desktop);
885 }
886 #endif
887 }
888
889 if (mDisplayAttribute->keepAbove()) {
890 m_keepAbove->setChecked(true);
891 } else if (mDisplayAttribute->keepBelow()) {
892 m_keepBelow->setChecked(true);
893 } else {
894 m_keepAbove->setChecked(false);
895 m_keepBelow->setChecked(false);
896 }
897
898 updateKeepAboveBelow();
899 // HACK: update the icon color - again after showing the note, to make kicker
900 // aware of the new colors
901 KIconEffect effect;
902 const QColor col = mDisplayAttribute->backgroundColor();
903 const QPixmap icon = effect.apply(qApp->windowIcon().pixmap(style()->pixelMetric(QStyle::PM_MessageBoxIconSize)), KIconEffect::Colorize, 1, col, false);
904 const QPixmap miniIcon = effect.apply(qApp->windowIcon().pixmap(style()->pixelMetric(QStyle::PM_SmallIconSize)), KIconEffect::Colorize, 1, col, false);
905 KWindowSystem::setIcons(winId(), icon, miniIcon);
906
907 // set up the look&feel of the note
908 setFrameStyle(Panel | Raised);
909 setMinimumSize(20, 20);
910 setBackgroundRole(QPalette::Base);
911
912 m_editor->setContentsMargins(0, 0, 0, 0);
913 m_editor->setBackgroundRole(QPalette::Base);
914 m_editor->setFrameStyle(NoFrame);
915 m_editor->document()->setModified(false);
916 mBlockSave = false;
917 }
918
toDesktop(int desktop)919 void KNote::toDesktop(int desktop)
920 {
921 if (desktop == 0) {
922 return;
923 }
924
925 #if KDEPIM_HAVE_X11
926 if (desktop == NETWinInfo::OnAllDesktops) {
927 KWindowSystem::setOnAllDesktops(winId(), true);
928 } else {
929 KWindowSystem::setOnDesktop(winId(), desktop);
930 }
931 #endif
932 }
933
setColor(const QColor & fg,const QColor & bg)934 void KNote::setColor(const QColor &fg, const QColor &bg)
935 {
936 m_editor->setColor(fg, bg);
937 QPalette p = palette();
938
939 // better: from light(150) to light(100) to light(75)
940 // QLinearGradient g( width()/2, 0, width()/2, height() );
941 // g.setColorAt( 0, bg );
942 // g.setColorAt( 1, bg.darker(150) );
943
944 p.setColor(QPalette::Window, bg);
945 // p.setBrush( QPalette::Window, g );
946 p.setColor(QPalette::Base, bg);
947 // p.setBrush( QPalette::Base, g );
948
949 p.setColor(QPalette::WindowText, fg);
950 p.setColor(QPalette::Text, fg);
951
952 p.setColor(QPalette::Button, bg.darker(116));
953 p.setColor(QPalette::ButtonText, fg);
954
955 // p.setColor( QPalette::Highlight, bg );
956 // p.setColor( QPalette::HighlightedText, fg );
957
958 // order: Light, Midlight, Button, Mid, Dark, Shadow
959
960 // the shadow
961 p.setColor(QPalette::Light, bg.lighter(180));
962 p.setColor(QPalette::Midlight, bg.lighter(150));
963 p.setColor(QPalette::Mid, bg.lighter(150));
964 p.setColor(QPalette::Dark, bg.darker(108));
965 p.setColor(QPalette::Shadow, bg.darker(116));
966
967 setPalette(p);
968
969 // darker values for the active label
970 p.setColor(QPalette::Active, QPalette::Base, bg.darker(116));
971
972 m_label->setPalette(p);
973
974 // set the text color
975 m_editor->setTextColor(fg);
976
977 // update the icon color
978 KIconEffect effect;
979 QPixmap icon = effect.apply(qApp->windowIcon().pixmap(style()->pixelMetric(QStyle::PM_MessageBoxIconSize)), KIconEffect::Colorize, 1, bg, false);
980 QPixmap miniIcon = effect.apply(qApp->windowIcon().pixmap(style()->pixelMetric(QStyle::PM_SmallIconSize)), KIconEffect::Colorize, 1, bg, false);
981 KWindowSystem::setIcons(winId(), icon, miniIcon);
982 // update the color of the title
983 updateFocus();
984 Q_EMIT sigColorChanged();
985 }
986
updateLabelAlignment()987 void KNote::updateLabelAlignment()
988 {
989 // if the name is too long to fit, left-align it, otherwise center it (#59028)
990 const QString labelText = m_label->text();
991 if (m_label->fontMetrics().boundingRect(labelText).width() > m_label->width()) {
992 m_label->setAlignment(Qt::AlignLeft);
993 } else {
994 m_label->setAlignment(Qt::AlignHCenter);
995 }
996 }
997
updateFocus()998 void KNote::updateFocus()
999 {
1000 if (hasFocus()) {
1001 if (!m_editor->isReadOnly()) {
1002 if (m_tool && m_tool->isHidden() && m_editor->acceptRichText()) {
1003 m_tool->show();
1004 updateLayout();
1005 }
1006 m_grip->show();
1007 } else {
1008 if (m_tool && !m_tool->isHidden()) {
1009 m_tool->hide();
1010 updateLayout(); // to update the minimum height
1011 }
1012 m_grip->hide();
1013 }
1014 } else {
1015 m_grip->hide();
1016
1017 if (m_tool && !m_tool->isHidden()) {
1018 m_tool->hide();
1019 updateLayout(); // to update the minimum height
1020 }
1021 }
1022 }
1023
updateLayout()1024 void KNote::updateLayout()
1025 {
1026 // TODO: remove later if no longer needed.
1027 updateLabelAlignment();
1028 }
1029
1030 // -------------------- protected methods -------------------- //
1031
contextMenuEvent(QContextMenuEvent * e)1032 void KNote::contextMenuEvent(QContextMenuEvent *e)
1033 {
1034 if (m_menu) {
1035 m_menu->popup(e->globalPos());
1036 }
1037 }
1038
showEvent(QShowEvent *)1039 void KNote::showEvent(QShowEvent *)
1040 {
1041 if (mDisplayAttribute->isHidden()) {
1042 // KWin does not preserve these properties for hidden windows
1043 updateKeepAboveBelow(false);
1044 #if KDEPIM_HAVE_X11
1045 slotUpdateShowInTaskbar();
1046 #endif
1047 toDesktop(mDisplayAttribute->desktop());
1048 move(mDisplayAttribute->position());
1049 auto attr = mItem.attribute<NoteShared::NoteDisplayAttribute>(Akonadi::Item::AddIfMissing);
1050 saveNoteContent();
1051 attr->setIsHidden(false);
1052 if (!mBlockSave) {
1053 auto job = new Akonadi::ItemModifyJob(mItem);
1054 #ifdef DEBUG_SAVE_NOTE
1055 qCDebug(KNOTES_LOG) << "showEvent slotNoteSaved(KJob*)";
1056 #endif
1057 connect(job, &Akonadi::ItemModifyJob::result, this, &KNote::slotNoteSaved);
1058 }
1059 }
1060 }
1061
resizeEvent(QResizeEvent * qre)1062 void KNote::resizeEvent(QResizeEvent *qre)
1063 {
1064 QFrame::resizeEvent(qre);
1065 updateLayout();
1066 }
1067
closeEvent(QCloseEvent * event)1068 void KNote::closeEvent(QCloseEvent *event)
1069 {
1070 if (qApp->isSavingSession()) {
1071 return;
1072 }
1073 event->ignore(); // We don't want to close (and delete the widget). Just hide it
1074 slotClose();
1075 }
1076
dragEnterEvent(QDragEnterEvent * e)1077 void KNote::dragEnterEvent(QDragEnterEvent *e)
1078 {
1079 if (!m_editor->isReadOnly()) {
1080 e->setAccepted(e->mimeData()->hasColor());
1081 }
1082 }
1083
dropEvent(QDropEvent * e)1084 void KNote::dropEvent(QDropEvent *e)
1085 {
1086 if (m_editor->isReadOnly()) {
1087 return;
1088 }
1089
1090 const QMimeData *md = e->mimeData();
1091 if (md->hasColor()) {
1092 const auto bg = qvariant_cast<QColor>(md->colorData());
1093
1094 auto attr = mItem.attribute<NoteShared::NoteDisplayAttribute>(Akonadi::Item::AddIfMissing);
1095 saveNoteContent();
1096 attr->setBackgroundColor(bg);
1097 auto job = new Akonadi::ItemModifyJob(mItem);
1098 #ifdef DEBUG_SAVE_NOTE
1099 qCDebug(KNOTES_LOG) << "dropEvent slotNoteSaved(KJob*)";
1100 #endif
1101 connect(job, &Akonadi::ItemModifyJob::result, this, &KNote::slotNoteSaved);
1102 }
1103 }
1104
event(QEvent * ev)1105 bool KNote::event(QEvent *ev)
1106 {
1107 if (ev->type() == QEvent::LayoutRequest) {
1108 updateLayout();
1109 return true;
1110 } else {
1111 return QFrame::event(ev);
1112 }
1113 }
1114
eventFilter(QObject * o,QEvent * ev)1115 bool KNote::eventFilter(QObject *o, QEvent *ev)
1116 {
1117 if (ev->type() == QEvent::DragEnter && static_cast<QDragEnterEvent *>(ev)->mimeData()->hasColor()) {
1118 dragEnterEvent(static_cast<QDragEnterEvent *>(ev));
1119 return true;
1120 }
1121
1122 if (ev->type() == QEvent::Drop && static_cast<QDropEvent *>(ev)->mimeData()->hasColor()) {
1123 dropEvent(static_cast<QDropEvent *>(ev));
1124 return true;
1125 }
1126
1127 if (o == m_label) {
1128 auto e = (QMouseEvent *)ev;
1129
1130 if (ev->type() == QEvent::MouseButtonDblClick) {
1131 if (!m_editor->isReadOnly()) {
1132 slotRename();
1133 }
1134 }
1135
1136 if (ev->type() == QEvent::MouseButtonPress
1137 && ((e->buttons() & Qt::LeftButton) == Qt::LeftButton || (e->buttons() & Qt::MiddleButton) == Qt::MiddleButton)) {
1138 mOrigPos = e->pos();
1139 return false;
1140 }
1141
1142 if (ev->type() == QEvent::MouseMove && ((e->buttons() & Qt::LeftButton) == Qt::LeftButton || (e->buttons() & Qt::MiddleButton) == Qt::MiddleButton)) {
1143 QPoint newPos = e->globalPos() - mOrigPos - QPoint(1, 1);
1144 move(newPos);
1145 return true;
1146 }
1147
1148 if (ev->type() == QEvent::MouseButtonRelease
1149 && ((e->buttons() & Qt::LeftButton) == Qt::LeftButton || (e->buttons() & Qt::MiddleButton) == Qt::MiddleButton)) {
1150 QPoint newPos = e->globalPos() - mOrigPos - QPoint(1, 1);
1151 move(newPos);
1152 return false;
1153 }
1154 return false;
1155 }
1156
1157 if (o == m_editor) {
1158 if (ev->type() == QEvent::FocusOut) {
1159 auto fe = static_cast<QFocusEvent *>(ev);
1160 if (fe->reason() != Qt::PopupFocusReason && fe->reason() != Qt::MouseFocusReason) {
1161 updateFocus();
1162 if (!mBlockSave) {
1163 saveNote(true);
1164 }
1165 }
1166 } else if (ev->type() == QEvent::FocusIn) {
1167 updateFocus();
1168 }
1169
1170 return false;
1171 }
1172
1173 return false;
1174 }
1175
item() const1176 Akonadi::Item KNote::item() const
1177 {
1178 return mItem;
1179 }
1180
slotDebugAkonadiSearch()1181 void KNote::slotDebugAkonadiSearch()
1182 {
1183 QPointer<Akonadi::Search::AkonadiSearchDebugDialog> dlg = new Akonadi::Search::AkonadiSearchDebugDialog;
1184 dlg->setAkonadiId(mItem.id());
1185 dlg->setAttribute(Qt::WA_DeleteOnClose);
1186 dlg->setSearchType(Akonadi::Search::AkonadiSearchDebugSearchPathComboBox::Notes);
1187 dlg->doSearch();
1188 dlg->show();
1189 }
1190