1 /*
2  * Copyright (C) Pedram Pourang (aka Tsu Jan) 2014-2020 <tsujan2000@gmail.com>
3  *
4  * FeatherPad is free software: you can redistribute it and/or modify it
5  * under the terms of the GNU General Public License as published by the
6  * Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * FeatherPad is distributed in the hope that it will be useful, but
10  * WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12  * See the GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License along
15  * with this program.  If not, see <http://www.gnu.org/licenses/>.
16  *
17  * @license GPL-3.0+ <https://spdx.org/licenses/GPL-3.0+.html>
18  */
19 
20 #include "singleton.h"
21 #include "ui_fp.h"
22 
23 #include "session.h"
24 #include "ui_sessionDialog.h"
25 #include <QFileInfo>
26 
27 namespace FeatherPad {
28 
29 // Since we don't want extra prompt dialogs, we make the
30 // session dialog behave like a prompt dialog when needed.
SessionDialog(QWidget * parent)31 SessionDialog::SessionDialog (QWidget *parent):QDialog (parent), ui (new Ui::SessionDialog)
32 {
33     ui->setupUi (this);
34     parent_ = parent;
35     setObjectName ("sessionDialog");
36     ui->promptLabel->setStyleSheet ("QLabel {background-color: #7d0000; color: white; border-radius: 3px; margin: 2px; padding: 5px;}");
37     ui->listWidget->setSizeAdjustPolicy (QAbstractScrollArea::AdjustToContents);
38     ui->listWidget->setContextMenuPolicy (Qt::CustomContextMenu);
39     filterTimer_ = nullptr;
40 
41     connect (ui->listWidget, &QListWidget::itemDoubleClicked, [=]{openSessions();});
42     connect (ui->listWidget, &QListWidget::itemSelectionChanged, this, &SessionDialog::selectionChanged);
43     connect (ui->listWidget, &QWidget::customContextMenuRequested, this, &SessionDialog::showContextMenu);
44     connect (ui->listWidget->itemDelegate(), &QAbstractItemDelegate::commitData, this, &SessionDialog::OnCommittingName);
45 
46     QSettings settings ("featherpad", "fp");
47     settings.beginGroup ("sessions");
48     allItems_ = settings.allKeys();
49     settings.endGroup();
50     if (allItems_.count() > 0)
51     {
52         /* use ListWidgetItem to add items with a natural sorting */
53         for (const auto &item : qAsConst (allItems_))
54         {
55             ListWidgetItem *lwi = new ListWidgetItem (item, ui->listWidget);
56             ui->listWidget->addItem (lwi);
57         }
58         ui->listWidget->setCurrentRow (0);
59         QTimer::singleShot (0, ui->listWidget, QOverload<>::of(&QWidget::setFocus));
60     }
61     else
62     {
63         onEmptinessChanged (true);
64         QTimer::singleShot (0, ui->lineEdit, QOverload<>::of(&QWidget::setFocus));
65     }
66 
67     ui->listWidget->installEventFilter (this);
68 
69     connect (ui->saveBtn, &QAbstractButton::clicked, this, &SessionDialog::saveSession);
70     connect (ui->lineEdit, &QLineEdit::returnPressed, this, &SessionDialog::saveSession);
71     /* we don't want to open a session by pressing Enter inside the line-edit */
72     connect (ui->lineEdit, &LineEdit::receivedFocus, [=]{ui->openBtn->setDefault (false);});
73     connect (ui->lineEdit, &QLineEdit::textEdited, [=](const QString &text){ui->saveBtn->setEnabled (!text.isEmpty());});
74     connect (ui->openBtn, &QAbstractButton::clicked, this, &SessionDialog::openSessions);
75     connect (ui->actionOpen, &QAction::triggered, this, &SessionDialog::openSessions);
76     connect (ui->clearBtn, &QAbstractButton::clicked, [=]{showPrompt (CLEAR);});
77     connect (ui->removeBtn, &QAbstractButton::clicked, [=]{showPrompt (REMOVE);});
78     connect (ui->actionRemove, &QAction::triggered, [=]{showPrompt (REMOVE);});
79 
80     connect (ui->actionRename, &QAction::triggered, this, &SessionDialog::renameSession);
81 
82     connect (ui->cancelBtn, &QAbstractButton::clicked, this, &SessionDialog::closePrompt);
83     connect (ui->confirmBtn, &QAbstractButton::clicked, this, &SessionDialog::closePrompt);
84 
85     connect (ui->closeButton, &QAbstractButton::clicked, this, &QDialog::close);
86 
87     connect (ui->filterLineEdit, &QLineEdit::textChanged, this, &SessionDialog::filter);
88 
89     /* for the tooltip mess in Qt 5.12 */
90     const auto widgets = findChildren<QWidget*>();
91     for (QWidget *w : widgets)
92     {
93         QString tip = w->toolTip();
94         if (!tip.isEmpty())
95         {
96             w->setToolTip ("<p style='white-space:pre'>" + tip + "</p>");
97         }
98     }
99 
100     resize (QSize (parent_->size().width()/2, 3*parent_->size().height()/4));
101 }
102 /*************************/
~SessionDialog()103 SessionDialog::~SessionDialog()
104 {
105     if (filterTimer_)
106     {
107         disconnect (filterTimer_, &QTimer::timeout, this, &SessionDialog::reallyApplyFilter);
108         filterTimer_->stop();
109         delete filterTimer_;
110     }
111     delete ui; ui = nullptr;
112 }
113 /*************************/
eventFilter(QObject * watched,QEvent * event)114 bool SessionDialog::eventFilter (QObject *watched, QEvent *event)
115 {
116     if (watched == ui->listWidget && event->type() == QEvent::KeyPress)
117     { // when a text is typed inside the list, type it inside the filter line-edit too
118         if (QKeyEvent *ke = static_cast<QKeyEvent*>(event))
119         {
120             ui->filterLineEdit->pressKey (ke);
121             return false;
122         }
123     }
124     return QDialog::eventFilter (watched, event);
125 }
126 /*************************/
showContextMenu(const QPoint & p)127 void SessionDialog::showContextMenu (const QPoint &p)
128 {
129     QModelIndex index = ui->listWidget->indexAt (p);
130     if (!index.isValid()) return;
131     ui->listWidget->selectionModel()->select (index, QItemSelectionModel::ClearAndSelect);
132 
133     QMenu menu;
134     menu.addAction (ui->actionOpen);
135     menu.addAction (ui->actionRemove);
136     menu.addSeparator();
137     menu.addAction (ui->actionRename);
138     menu.exec (ui->listWidget->mapToGlobal (p));
139 }
140 /*************************/
saveSession()141 void SessionDialog::saveSession()
142 {
143     if (ui->lineEdit->text().isEmpty()) return;
144 
145     bool hasFile (false);
146     if (ui->windowBox->isChecked())
147     {
148         FPwin *win = static_cast<FPwin *>(parent_);
149         for (int i = 0; i < win->ui->tabWidget->count(); ++i)
150         {
151             if (!qobject_cast<TabPage*>(win->ui->tabWidget->widget (i))
152                 ->textEdit()->getFileName().isEmpty())
153             {
154                 hasFile = true;
155                 break;
156             }
157         }
158     }
159     else
160     {
161         FPsingleton *singleton = static_cast<FPsingleton*>(qApp);
162         for (int i = 0; i < singleton->Wins.count(); ++i)
163         {
164             FPwin *win = singleton->Wins.at (i);
165             for (int j = 0; j < win->ui->tabWidget->count(); ++j)
166             {
167                 if (!qobject_cast<TabPage*>(win->ui->tabWidget->widget (j))
168                     ->textEdit()->getFileName().isEmpty())
169                 {
170                     hasFile = true;
171                     break;
172                 }
173             }
174             if (hasFile) break;
175         }
176     }
177 
178     if (!hasFile)
179         showPrompt (tr ("Nothing saved.<br>No file was opened."));
180     else if (allItems_.contains (ui->lineEdit->text()))
181         showPrompt (NAME);
182     else
183         reallySaveSession();
184 }
185 /*************************/
reallySaveSession()186 void SessionDialog::reallySaveSession()
187 {
188     QList<QListWidgetItem*> sameItems = ui->listWidget->findItems (ui->lineEdit->text(), Qt::MatchExactly);
189     for (int i = 0; i < sameItems.count(); ++i)
190         delete ui->listWidget->takeItem (ui->listWidget->row (sameItems.at (i)));
191 
192     QStringList files;
193     if (ui->windowBox->isChecked())
194     {
195         FPwin *win = static_cast<FPwin *>(parent_);
196         for (int i = 0; i < win->ui->tabWidget->count(); ++i)
197         {
198             TextEdit *textEdit = qobject_cast<TabPage*>(win->ui->tabWidget->widget (i))->textEdit();
199             if (!textEdit->getFileName().isEmpty())
200             {
201                 files << textEdit->getFileName();
202                 textEdit->setSaveCursor (true);
203             }
204         }
205     }
206     else
207     {
208         FPsingleton *singleton = static_cast<FPsingleton*>(qApp);
209         for (int i = 0; i < singleton->Wins.count(); ++i)
210         {
211             FPwin *win = singleton->Wins.at (i);
212             for (int j = 0; j < win->ui->tabWidget->count(); ++j)
213             {
214                 TextEdit *textEdit = qobject_cast<TabPage*>(win->ui->tabWidget->widget (j))->textEdit();
215                 if (!textEdit->getFileName().isEmpty())
216                 {
217                     files << textEdit->getFileName();
218                     textEdit->setSaveCursor (true);
219                 }
220             }
221         }
222     }
223     /* there's always an opened file here */
224     allItems_ << ui->lineEdit->text();
225     allItems_.removeDuplicates();
226     QRegularExpression exp (ui->filterLineEdit->text(), QRegularExpression::CaseInsensitiveOption);
227     if (allItems_.filter (exp).contains (ui->lineEdit->text()))
228     {
229         ListWidgetItem *lwi = new ListWidgetItem (ui->lineEdit->text(), ui->listWidget);
230         ui->listWidget->addItem (lwi);
231     }
232     onEmptinessChanged (false);
233     QSettings settings ("featherpad", "fp");
234     settings.beginGroup ("sessions");
235     settings.setValue (ui->lineEdit->text(), files);
236     settings.endGroup();
237 }
238 /*************************/
openSessions()239 void SessionDialog::openSessions()
240 {
241     QList<QListWidgetItem*> items = ui->listWidget->selectedItems();
242     int count = items.count();
243     if (count == 0) return;
244 
245     QSettings settings ("featherpad", "fp");
246     settings.beginGroup ("sessions");
247     QStringList files;
248     for (int i = 0; i < count; ++i)
249         files += settings.value (items.at (i)->text()).toStringList();
250     settings.endGroup();
251 
252     if (!files.isEmpty())
253     {
254         if (FPwin *win = static_cast<FPwin *>(parent_))
255         {
256             Config& config = static_cast<FPsingleton*>(qApp)->getConfig();
257             int broken = 0;
258             bool multiple (files.count() > 1 || win->isLoading());
259             for (int i = 0; i < files.count(); ++i)
260             {
261                 if (!QFileInfo (files.at (i)).isFile())
262                 {
263                     /* first, clean up the cursor config file */
264                     config.removeCursorPos (files.at (i));
265 
266                     ++broken;
267                     continue;
268                 }
269                 win->newTabFromName (files.at (i),
270                                      1, // to save the cursor position
271                                      0, // irrelevant
272                                      multiple);
273             }
274             if (broken > 0)
275             {
276                 if (broken == files.count())
277                     showPrompt (tr ("No file exists or can be opened."));
278                 else
279                     showPrompt (tr ("Not all files exist or can be opened."));
280             }
281         }
282     }
283 }
284 /*************************/
285 // These slots are called for processes to have time to be completed,
286 // especially for the returnPressed signal of the line-edit to be emiited.
showMainPage()287 void SessionDialog::showMainPage()
288 {
289     if (!rename_.newName.isEmpty() && !rename_.oldName.isEmpty()) // renaming cancelled
290     {
291         if (QListWidgetItem* cur = ui->listWidget->currentItem())
292             cur->setText (rename_.oldName);
293         rename_.newName = rename_.oldName = QString();
294     }
295     ui->stackedWidget->setCurrentIndex (0);
296 }
showPromptPage()297 void SessionDialog::showPromptPage()
298 {
299     ui->stackedWidget->setCurrentIndex (1);
300     QTimer::singleShot (0, ui->confirmBtn, QOverload<>::of(&QWidget::setFocus));
301 }
302 /*************************/
showPrompt(const QString & message)303 void SessionDialog::showPrompt (const QString& message)
304 {
305     disconnect (ui->confirmBtn, &QAbstractButton::clicked, this, &SessionDialog::removeAll);
306     disconnect (ui->confirmBtn, &QAbstractButton::clicked, this, &SessionDialog::removeSelected);
307     disconnect (ui->confirmBtn, &QAbstractButton::clicked, this, &SessionDialog::reallySaveSession);
308     disconnect (ui->confirmBtn, &QAbstractButton::clicked, this, &SessionDialog::reallyRenameSession);
309 
310     if (message.isEmpty()) return;
311 
312     QTimer::singleShot (0, this, &SessionDialog::showPromptPage);
313 
314     ui->confirmBtn->setText (tr ("&OK"));
315     ui->cancelBtn->setVisible (false);
316     ui->promptLabel->setText ("<b>" + message + "</b>");
317 }
318 /*************************/
showPrompt(PROMPT prompt)319 void SessionDialog::showPrompt (PROMPT prompt)
320 {
321     disconnect (ui->confirmBtn, &QAbstractButton::clicked, this, &SessionDialog::removeAll);
322     disconnect (ui->confirmBtn, &QAbstractButton::clicked, this, &SessionDialog::removeSelected);
323     disconnect (ui->confirmBtn, &QAbstractButton::clicked, this, &SessionDialog::reallySaveSession);
324     disconnect (ui->confirmBtn, &QAbstractButton::clicked, this, &SessionDialog::reallyRenameSession);
325 
326     QTimer::singleShot (0, this, &SessionDialog::showPromptPage);
327 
328     ui->confirmBtn->setText (tr ("&Yes"));
329     ui->cancelBtn->setVisible (true);
330 
331     if (prompt == CLEAR)
332     {
333         ui->promptLabel->setText ("<b>" + tr ("Do you really want to remove all saved sessions?") + "</b>");
334         connect (ui->confirmBtn, &QAbstractButton::clicked, this, &SessionDialog::removeAll);
335     }
336     else if (prompt == REMOVE)
337     {
338         if (ui->listWidget->selectedItems().count() > 1)
339             ui->promptLabel->setText ("<b>" + tr ("Do you really want to remove the selected sessions?") + "</b>");
340         else
341             ui->promptLabel->setText ("<b>" + tr ("Do you really want to remove the selected session?") + "</b>");
342         connect (ui->confirmBtn, &QAbstractButton::clicked, this, &SessionDialog::removeSelected);
343     }
344     else// if (prompt == NAME || prompt == RENAME)
345     {
346         ui->promptLabel->setText ("<b>" + tr ("A session with the same name exists.<br>Do you want to overwrite it?") + "</b>");
347         if (prompt == NAME)
348             connect (ui->confirmBtn, &QAbstractButton::clicked, this, &SessionDialog::reallySaveSession);
349         else
350             connect (ui->confirmBtn, &QAbstractButton::clicked, this, &SessionDialog::reallyRenameSession);
351     }
352 }
353 /*************************/
closePrompt()354 void SessionDialog::closePrompt()
355 {
356     ui->promptLabel->clear();
357     QTimer::singleShot (0, this, &SessionDialog::showMainPage);
358 }
359 /*************************/
removeSelected()360 void SessionDialog::removeSelected()
361 {
362     QList<QListWidgetItem*> items = ui->listWidget->selectedItems();
363     int count = items.count();
364     if (count == 0) return;
365 
366     Config& config = static_cast<FPsingleton*>(qApp)->getConfig();
367     QSettings settings ("featherpad", "fp");
368     settings.beginGroup ("sessions");
369     for (int i = 0; i < count; ++i)
370     {
371         /* first, clean up the cursor config file */
372         QStringList files = settings.value (items.at (i)->text()).toStringList();
373         for (int j = 0; j < files.count(); ++j)
374             config.removeCursorPos (files.at (j));
375 
376         settings.remove (items.at (i)->text());
377         allItems_.removeOne (items.at (i)->text());
378         delete ui->listWidget->takeItem (ui->listWidget->row (items.at (i)));
379     }
380     settings.endGroup();
381 
382     if (config.savedCursorPos().isEmpty())
383     {
384         Settings curSettings ("featherpad", "fp_cursor_pos");
385         curSettings.remove ("cursorPositions");
386     }
387 
388     if (allItems_.count() == 0)
389         onEmptinessChanged (true);
390 }
391 /*************************/
removeAll()392 void SessionDialog::removeAll()
393 {
394     /* first, clean up the cursor config file */
395     Config& config = static_cast<FPsingleton*>(qApp)->getConfig();
396     config.removeAllCursorPos();
397     Settings curSettings ("featherpad", "fp_cursor_pos");
398     curSettings.remove ("cursorPositions");
399 
400     ui->listWidget->clear();
401     onEmptinessChanged (true);
402     QSettings settings ("featherpad", "fp");
403     settings.beginGroup ("sessions");
404     settings.remove ("");
405     settings.endGroup();
406 }
407 /*************************/
selectionChanged()408 void SessionDialog::selectionChanged()
409 {
410     bool noSel = ui->listWidget->selectedItems().isEmpty();
411     ui->openBtn->setEnabled (!noSel);
412     ui->removeBtn->setEnabled (!noSel);
413     /* we want to open sessions by pressing Enter inside the list widget
414        without connecting to QAbstractItemView::activated() */
415     ui->openBtn->setDefault (true);
416 }
417 /*************************/
renameSession()418 void SessionDialog::renameSession()
419 {
420     if (QListWidgetItem* cur = ui->listWidget->currentItem())
421     {
422         rename_.oldName = cur->text();
423         cur->setFlags (cur->flags() | Qt::ItemIsEditable);
424         ui->listWidget->editItem (cur);
425     }
426 }
427 /*************************/
OnCommittingName(QWidget * editor)428 void SessionDialog::OnCommittingName (QWidget* editor)
429 {
430     if (QListWidgetItem* cur = ui->listWidget->currentItem())
431         cur->setFlags (cur->flags() & ~Qt::ItemIsEditable);
432 
433     rename_.newName = reinterpret_cast<QLineEdit*>(editor)->text();
434     if (rename_.newName.isEmpty()
435         || rename_.newName == rename_.oldName)
436     {
437         rename_.newName = rename_.oldName = QString(); // reset
438         return;
439     }
440 
441     if (allItems_.contains (rename_.newName))
442         showPrompt (RENAME);
443     else
444         reallyRenameSession();
445 }
446 /*************************/
reallyRenameSession()447 void SessionDialog::reallyRenameSession()
448 {
449     if (rename_.newName.isEmpty() || rename_.oldName.isEmpty()) // impossible
450     {
451         rename_.newName = rename_.oldName = QString();
452         return;
453     }
454 
455     QSettings settings ("featherpad", "fp");
456     settings.beginGroup ("sessions");
457     QStringList files = settings.value (rename_.oldName).toStringList();
458     settings.remove (rename_.oldName);
459     settings.setValue (rename_.newName, files);
460     settings.endGroup();
461 
462     allItems_.removeOne (rename_.oldName);
463     allItems_ << rename_.newName;
464     allItems_.removeDuplicates();
465 
466     if (QListWidgetItem* cur = ui->listWidget->currentItem())
467     {
468         bool isFiltered (false);
469         /* if the renamed item is filtered, remove it
470            with all items that have the same name */
471         if (!ui->filterLineEdit->text().isEmpty())
472         {
473             QRegularExpression exp (ui->filterLineEdit->text(), QRegularExpression::CaseInsensitiveOption);
474             if (!allItems_.filter (exp).contains (rename_.newName))
475                 isFiltered = true;
476         }
477         /* if there's another item with the new name, remove it */
478         QList<QListWidgetItem*> sameItems = ui->listWidget->findItems (rename_.newName, Qt::MatchExactly);
479         for (int i = 0; i < sameItems.count(); ++i)
480         {
481             if (isFiltered || sameItems.at (i) != cur)
482                 delete ui->listWidget->takeItem (ui->listWidget->row (sameItems.at (i)));
483         }
484         if (!isFiltered)
485             ui->listWidget->scrollToItem (cur);
486     }
487 
488     rename_.newName = rename_.oldName = QString(); // reset
489 }
490 /*************************/
filter(const QString &)491 void SessionDialog::filter (const QString&/*text*/)
492 {
493     if (!filterTimer_)
494     {
495         filterTimer_ = new QTimer();
496         filterTimer_->setSingleShot (true);
497         connect (filterTimer_, &QTimer::timeout, this, &SessionDialog::reallyApplyFilter);
498     }
499     filterTimer_->start (200);
500 }
501 /*************************/
reallyApplyFilter()502 void SessionDialog::reallyApplyFilter()
503 {
504     /* first, get the selection */
505     QStringList sel;
506     QList<QListWidgetItem*> items = ui->listWidget->selectedItems();
507     for (int i = 0; i < items.count(); ++i)
508         sel << items.at (i)->text();
509     /* then, clear the current list and add the filtered one */
510     ui->listWidget->clear();
511     QRegularExpression exp (ui->filterLineEdit->text(), QRegularExpression::CaseInsensitiveOption);
512     const QStringList filtered = allItems_.filter (exp);
513     for (const auto &item : filtered)
514     {
515         ListWidgetItem *lwi = new ListWidgetItem (item, ui->listWidget);
516         ui->listWidget->addItem (lwi);
517     }
518     /* finally, restore the selection as far as possible */
519     if (filtered.count() == 1)
520         ui->listWidget->setCurrentRow (0);
521     else if (!sel.isEmpty())
522     {
523         for (int i = 0; i < ui->listWidget->count(); ++i)
524         {
525             if (sel.contains (ui->listWidget->item (i)->text()))
526                 ui->listWidget->setCurrentRow (i, QItemSelectionModel::Select);
527         }
528     }
529 }
530 /*************************/
onEmptinessChanged(bool empty)531 void SessionDialog::onEmptinessChanged (bool empty)
532 {
533     ui->clearBtn->setEnabled (!empty);
534     if (empty)
535     {
536         allItems_.clear();
537         ui->filterLineEdit->clear();
538     }
539     ui->filterLineEdit->setEnabled (!empty);
540 }
541 
542 }
543