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