1 /************************************************************************
2 **
3 **  Copyright (C) 2015-2021 Kevin B. Hendricks, Stratford, Ontario, Canada
4 **  Copyright (C) 2012      John Schember <john@nachtimwald.com>
5 **  Copyright (C) 2012      Dave Heiland
6 **  Copyright (C) 2012      Grant Drake
7 **
8 **  This file is part of Sigil.
9 **
10 **  Sigil is free software: you can redistribute it and/or modify
11 **  it under the terms of the GNU General Public License as published by
12 **  the Free Software Foundation, either version 3 of the License, or
13 **  (at your option) any later version.
14 **
15 **  Sigil is distributed in the hope that it will be useful,
16 **  but WITHOUT ANY WARRANTY; without even the implied warranty of
17 **  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 **  GNU General Public License for more details.
19 **
20 **  You should have received a copy of the GNU General Public License
21 **  along with Sigil.  If not, see <http://www.gnu.org/licenses/>.
22 **
23 *************************************************************************/
24 
25 #include <QtCore/QSignalMapper>
26 #include <QtWidgets/QFileDialog>
27 #include <QtWidgets/QMessageBox>
28 #include <QtGui/QContextMenuEvent>
29 #include <QRegularExpression>
30 
31 #include "Dialogs/SearchEditorItemDelegate.h"
32 #include "Dialogs/SearchEditor.h"
33 #include "Misc/Utility.h"
34 
35 static const QString SETTINGS_GROUP = "saved_searches";
36 static const QString FILE_EXTENSION = "ini";
37 
SearchEditor(QWidget * parent)38 SearchEditor::SearchEditor(QWidget *parent)
39     :
40     QDialog(parent),
41     m_LastFolderOpen(QString()),
42     m_ContextMenu(new QMenu(this)),
43     m_CntrlDelegate(new SearchEditorItemDelegate())
44 {
45     ui.setupUi(this);
46     ui.FilterText->installEventFilter(this);
47     ui.LoadSearch->setDefault(true);
48     SetupSearchEditorTree();
49     CreateContextMenuActions();
50     ConnectSignalsSlots();
51     ExpandAll();
52 }
53 
SetupSearchEditorTree()54 void SearchEditor::SetupSearchEditorTree()
55 {
56     m_SearchEditorModel = SearchEditorModel::instance();
57     ui.SearchEditorTree->setModel(m_SearchEditorModel);
58     ui.SearchEditorTree->setContextMenuPolicy(Qt::CustomContextMenu);
59     ui.SearchEditorTree->setSortingEnabled(false);
60     ui.SearchEditorTree->setWordWrap(true);
61     ui.SearchEditorTree->setAlternatingRowColors(true);
62     ui.SearchEditorTree->installEventFilter(this);
63     QString nametooltip = "<p>" + tr("Right click on an entry to see a context menu of actions.") + "</p>" +
64         "<p>" + tr("You can also right click on the Find text box in the Find & Replace window to select an entry.") + "</p>" +
65         "<dl>" +
66         "<dt><b>" + tr("Name") + "</b><dd>" + tr("Name of your entry or group.") + "</dd></dl>";
67     QString findtooltip = "<dl><dt><b>" + tr("Find") + "</b><dd>" + tr("The text to put into the Find box.")+"</dd></dl>";
68     QString replacetooltip = "<dl><b>" + tr("Replace") + "</b><dd>" + tr("The text to put into the Replace box.")+"</dd></dl>";;
69     QString controlstooltip = "<dl><b>" + tr("Controls") + "</b><dd>" + tr("Two character codes to control the search Mode, Direction, Target and Options.  Codes can be in any order comma or space separated.") + "</dd></dl>" + "<dl>" +
70         "<dd>NL - " + tr("Mode: Normal") + "</dd>" +
71         "<dd>RX - " + tr("Mode: Regular Expression") + "</dd>" +
72         "<dd>CS - " + tr("Mode: Case Sensitive") + "</dd>" +
73         "<dd>&nbsp;</dd>" +
74         "<dd>UP - " + tr("Direction: Up") + "</dd>" +
75         "<dd>DN - " + tr("Direction: Down") + "</dd>" +
76         "<dd>&nbsp;</dd>" +
77         "<dd>CF - " + tr("Target: Current File") + "</dd>" +
78         "<dd>AH - " + tr("Target: All HTML Files") + "</dd>" +
79         "<dd>SH - " + tr("Target: Selected HTML Files") + "</dd>" +
80         "<dd>TH - " + tr("Target: Tabbed HTML Files") + "</dd>" +
81         "<dd>AC - " + tr("Target: All CSS Files") + "</dd>" +
82         "<dd>SC - " + tr("Target: Selected CSS Files") + "</dd>" +
83         "<dd>TC - " + tr("Target: Tabbed CSS Files") + "</dd>" +
84         "<dd>OP - " + tr("Target: OPF File") + "</dd>" +
85         "<dd>NX - " + tr("Target: NCX File") + "</dd>" +
86         "<dd>&nbsp;</dd>" +
87         "<dd>DA - " + tr("Option: DotAll") + "</dd>" +
88         "<dd>MM - " + tr("Option: Minimal Match") + "</dd>" +
89         "<dd>AT - " + tr("Option: Auto Tokenise") + "</dd>" +
90         "<dd>WR - " + tr("Option: Wrap") + "</dd>" + "</dl>";
91 
92     ui.SearchEditorTree->model()->setHeaderData(0,Qt::Horizontal,nametooltip,Qt::ToolTipRole);
93     ui.SearchEditorTree->model()->setHeaderData(1,Qt::Horizontal,findtooltip,Qt::ToolTipRole);
94     ui.SearchEditorTree->model()->setHeaderData(2,Qt::Horizontal,replacetooltip,Qt::ToolTipRole);
95     ui.SearchEditorTree->model()->setHeaderData(3,Qt::Horizontal,controlstooltip,Qt::ToolTipRole);
96 
97     ui.SearchEditorTree->setItemDelegateForColumn(3, m_CntrlDelegate);
98     ui.buttonBox->setToolTip(QString() +
99                              "<dl>" +
100                              "<dt><b>" + tr("Save") + "</b><dd>" + tr("Save your changes.") + "<br/><br/>" + tr("If any other instances of Sigil are running they will be automatically updated with your changes.") + "</dd>" +
101                              "</dl>");
102     ui.SearchEditorTree->header()->setStretchLastSection(true);
103 }
104 
ShowMessage(const QString & message)105 void SearchEditor::ShowMessage(const QString &message)
106 {
107     ui.Message->setText(message);
108     ui.Message->repaint();
109     QApplication::processEvents();
110 }
111 
SaveData(QList<SearchEditorModel::searchEntry * > entries,QString filename)112 bool SearchEditor::SaveData(QList<SearchEditorModel::searchEntry *> entries, QString filename)
113 {
114     QString message = m_SearchEditorModel->SaveData(entries, filename);
115 
116     if (!message.isEmpty()) {
117         Utility::DisplayStdErrorDialog(tr("Cannot save entries.") + "\n\n" + message);
118     }
119 
120     return message.isEmpty();
121 }
122 
SaveTextData(QList<SearchEditorModel::searchEntry * > entries,QString filename,QChar sep)123 bool SearchEditor::SaveTextData(QList<SearchEditorModel::searchEntry *> entries, QString filename, QChar sep)
124 {
125     QString message = m_SearchEditorModel->SaveTextData(entries, filename, sep);
126 
127     if (!message.isEmpty()) {
128         Utility::DisplayStdErrorDialog(tr("Cannot save entries.") + "\n\n" + message);
129     }
130 
131     return message.isEmpty();
132 }
133 
LoadFindReplace()134 void SearchEditor::LoadFindReplace()
135 {
136     // destination needs to delete each searchEntry when done
137     emit LoadSelectedSearchRequest(GetSelectedEntry(false));
138 }
139 
Find()140 void SearchEditor::Find()
141 {
142     // destination needs to delete each searchEntry when done
143     emit FindSelectedSearchRequest(GetSelectedEntries());
144 }
145 
ReplaceCurrent()146 void SearchEditor::ReplaceCurrent()
147 {
148     // destination needs to delete each searchEntry when done
149     emit ReplaceCurrentSelectedSearchRequest(GetSelectedEntries());
150 }
151 
Replace()152 void SearchEditor::Replace()
153 {
154     // destination needs to delete each searchEntry when done
155     emit ReplaceSelectedSearchRequest(GetSelectedEntries());
156 }
157 
CountAll()158 void SearchEditor::CountAll()
159 {
160     // destination needs to delete each searchEntry when done
161     emit CountAllSelectedSearchRequest(GetSelectedEntries());
162 }
163 
ReplaceAll()164 void SearchEditor::ReplaceAll()
165 {
166     // destination needs to delete each searchEntry when done
167     emit ReplaceAllSelectedSearchRequest(GetSelectedEntries());
168 }
169 
showEvent(QShowEvent * event)170 void SearchEditor::showEvent(QShowEvent *event)
171 {
172     bool has_settings = ReadSettings();
173     ui.FilterText->setFocus();
174 
175     // If the user has no persisted columns data yet, just resize automatically
176     if (!has_settings) {
177         for (int column = 0; column < ui.SearchEditorTree->header()->count(); column++) {
178             ui.SearchEditorTree->resizeColumnToContents(column);
179         }
180 
181         // Hitting an issue for first time user resizing the Find column to only width
182         // of the heading. Just force an initial width instead.
183         ui.SearchEditorTree->setColumnWidth(1, 150);
184     }
185 }
186 
eventFilter(QObject * obj,QEvent * event)187 bool SearchEditor::eventFilter(QObject *obj, QEvent *event)
188 {
189     if (obj == ui.FilterText) {
190         if (event->type() == QEvent::KeyPress) {
191             QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
192             int key = keyEvent->key();
193 
194             if (key == Qt::Key_Down) {
195                 ui.SearchEditorTree->setFocus();
196                 return true;
197             }
198         }
199     }
200 
201     // pass the event on to the parent class
202     return QDialog::eventFilter(obj, event);
203 }
204 
SettingsFileModelUpdated()205 void SearchEditor::SettingsFileModelUpdated()
206 {
207     ui.SearchEditorTree->expandAll();
208     emit ShowStatusMessageRequest(tr("Saved Searches loaded from file."));
209 }
210 
ModelItemDropped(const QModelIndex & index)211 void SearchEditor::ModelItemDropped(const QModelIndex &index)
212 {
213     if (index.isValid()) {
214         ui.SearchEditorTree->expand(index);
215     }
216 }
217 
SelectedRowsCount()218 int SearchEditor::SelectedRowsCount()
219 {
220     int count = 0;
221 
222     if (ui.SearchEditorTree->selectionModel()->hasSelection()) {
223         count = ui.SearchEditorTree->selectionModel()->selectedRows(0).count();
224     }
225 
226     return count;
227 }
228 
GetSelectedEntry(bool show_warning)229 SearchEditorModel::searchEntry *SearchEditor::GetSelectedEntry(bool show_warning)
230 {
231     // Note: a SeachEditorModel::searchEntry is a simple struct that is created
232     // by new in SearchEditorModel GetEntry() and GetEntries()
233     // These must be manually deleted when done to prevent memory leaks
234 
235     SearchEditorModel::searchEntry *entry = NULL;
236 
237     if (ui.SearchEditorTree->selectionModel()->hasSelection()) {
238         QStandardItem *item = NULL;
239         QModelIndexList selected_indexes = ui.SearchEditorTree->selectionModel()->selectedRows(0);
240 
241         if (selected_indexes.count() == 1) {
242             item = m_SearchEditorModel->itemFromIndex(selected_indexes.first());
243         } else if (show_warning) {
244             Utility::DisplayStdErrorDialog(tr("You cannot select more than one entry when using this action."));
245             return entry;
246         }
247 
248         if (item) {
249             if (!m_SearchEditorModel->ItemIsGroup(item)) {
250                 entry = m_SearchEditorModel->GetEntry(item);
251             } else if (show_warning) {
252                 Utility::DisplayStdErrorDialog(tr("You cannot select a group for this action."));
253             }
254         }
255     }
256 
257     return entry;
258 }
259 
GetEntriesFromFullName(const QString & name)260 QList<SearchEditorModel::searchEntry *> SearchEditor::GetEntriesFromFullName(const QString &name)
261 {
262     // Note: a SeachEditorModel::searchEntry is a simple struct that is created
263     // by new in SearchEditorModel GetEntry() and GetEntries()
264     // These must be manually deleted when done to prevent memory leaks
265 
266     QList<SearchEditorModel::searchEntry *> selected_entries;
267 
268     QStandardItem * nameditem = m_SearchEditorModel->GetItemFromName(name);
269     if (nameditem) {
270         QList<QStandardItem *> items = m_SearchEditorModel->GetNonGroupItems(nameditem);
271         if (!ItemsAreUnique(items)) {
272             return selected_entries;
273         }
274 
275         selected_entries = m_SearchEditorModel->GetEntries(items);
276     }
277 
278     return selected_entries;
279 }
280 
GetSelectedEntries()281 QList<SearchEditorModel::searchEntry *> SearchEditor::GetSelectedEntries()
282 {
283     // Note: a SeachEditorModel::searchEntry is a simple struct that is created
284     // by new in SearchEditorModel GetEntry() and GetEntries()
285     // These must be manually deleted when done to prevent memory leaks
286 
287     QList<SearchEditorModel::searchEntry *> selected_entries;
288 
289     if (ui.SearchEditorTree->selectionModel()->hasSelection()) {
290         QList<QStandardItem *> items = m_SearchEditorModel->GetNonGroupItems(GetSelectedItems());
291 
292         if (!ItemsAreUnique(items)) {
293             return selected_entries;
294         }
295 
296         selected_entries = m_SearchEditorModel->GetEntries(items);
297     }
298 
299     return selected_entries;
300 }
301 
302 
GetSelectedItems()303 QList<QStandardItem *> SearchEditor::GetSelectedItems()
304 {
305     // Shift-click order is top to bottom regardless of starting position
306     // Ctrl-click order is first clicked to last clicked (included shift-clicks stay ordered as is)
307     QModelIndexList selected_indexes = ui.SearchEditorTree->selectionModel()->selectedRows(0);
308     QList<QStandardItem *> selected_items;
309     foreach(QModelIndex index, selected_indexes) {
310         selected_items.append(m_SearchEditorModel->itemFromIndex(index));
311     }
312     return selected_items;
313 }
314 
ItemsAreUnique(QList<QStandardItem * > items)315 bool SearchEditor::ItemsAreUnique(QList<QStandardItem *> items)
316 {
317     // Although saving a group and a sub item works, it could be confusing to users to
318     // have and entry appear twice so its more predictable just to prevent it and warn the user
319     if (items.toSet().count() != items.count()) {
320     // In Qt 5.15 if (QSet<QStandardItem *>(items.begin(), items.end()).count() != items.count()) {
321         Utility::DisplayStdErrorDialog(tr("You cannot select an entry and a group containing the entry."));
322         return false;
323     }
324 
325     return true;
326 }
327 
AddEntry(bool is_group,SearchEditorModel::searchEntry * search_entry,bool insert_after)328 QStandardItem *SearchEditor::AddEntry(bool is_group, SearchEditorModel::searchEntry *search_entry, bool insert_after)
329 {
330     QStandardItem *parent_item = NULL;
331     QStandardItem *new_item = NULL;
332     int row = 0;
333 
334     // If adding a new/blank entry add it after the selected entries.
335     if (insert_after) {
336         if (ui.SearchEditorTree->selectionModel()->hasSelection()) {
337             parent_item = GetSelectedItems().last();
338 
339             if (!parent_item) {
340                 return parent_item;
341             }
342 
343             if (!m_SearchEditorModel->ItemIsGroup(parent_item)) {
344                 row = parent_item->row() + 1;
345                 parent_item = parent_item->parent();
346             }
347         }
348     }
349 
350     // Make sure the new entry can be seen
351     if (parent_item) {
352         ui.SearchEditorTree->expand(parent_item->index());
353     }
354 
355     new_item = m_SearchEditorModel->AddEntryToModel(search_entry, is_group, parent_item, row);
356     QModelIndex new_index = new_item->index();
357     // Select the added item and set it for editing
358     ui.SearchEditorTree->selectionModel()->clear();
359     ui.SearchEditorTree->setCurrentIndex(new_index);
360     ui.SearchEditorTree->selectionModel()->select(new_index, QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
361     ui.SearchEditorTree->edit(new_index);
362     ui.SearchEditorTree->setCurrentIndex(new_index);
363     ui.SearchEditorTree->selectionModel()->select(new_index, QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
364     return new_item;
365 }
366 
AddGroup()367 QStandardItem *SearchEditor::AddGroup()
368 {
369     return AddEntry(true);
370 }
371 
Edit()372 void SearchEditor::Edit()
373 {
374     ui.SearchEditorTree->edit(ui.SearchEditorTree->currentIndex());
375 }
376 
Cut()377 void SearchEditor::Cut()
378 {
379     if (Copy()) {
380         Delete();
381     }
382 }
383 
Copy()384 bool SearchEditor::Copy()
385 {
386     if (SelectedRowsCount() < 1) {
387         return false;
388     }
389 
390     while (m_SavedSearchEntries.count()) {
391         m_SavedSearchEntries.removeAt(0);
392     }
393 
394     QList<SearchEditorModel::searchEntry *> entries = GetSelectedEntries();
395 
396     if (!entries.count()) {
397         return false;
398     }
399 
400     foreach(QStandardItem * item, GetSelectedItems()) {
401         SearchEditorModel::searchEntry *entry = m_SearchEditorModel->GetEntry(item);
402 
403         if (entry->is_group) {
404             Utility::DisplayStdErrorDialog(tr("You cannot Copy or Cut groups - use drag-and-drop.")) ;
405             return false;
406         }
407     }
408     foreach(SearchEditorModel::searchEntry * entry, entries) {
409         SearchEditorModel::searchEntry *save_entry = new SearchEditorModel::searchEntry();
410         save_entry->name = entry->name;
411         save_entry->find = entry->find;
412         save_entry->replace = entry->replace;
413         save_entry->controls = entry->controls;
414         m_SavedSearchEntries.append(save_entry);
415     }
416     return true;
417 }
418 
Paste()419 void SearchEditor::Paste()
420 {
421     foreach(SearchEditorModel::searchEntry * entry, m_SavedSearchEntries) {
422         AddEntry(entry->is_group, entry);
423     }
424 }
425 
Delete()426 void SearchEditor::Delete()
427 {
428     if (SelectedRowsCount() < 1) {
429         return;
430     }
431 
432     // Delete one at a time as selection may not be contiguous
433     int row = -1;
434     QModelIndex parent_index;
435 
436     while (ui.SearchEditorTree->selectionModel()->hasSelection()) {
437         QModelIndex index = ui.SearchEditorTree->selectionModel()->selectedRows(0).first();
438 
439         if (index.isValid()) {
440             row = index.row();
441             parent_index = index.parent();
442             m_SearchEditorModel->removeRows(row, 1, parent_index);
443         }
444     }
445 
446     // Select the nearest row in the group, or the group if no rows left
447     int parent_row_count;
448 
449     if (parent_index.isValid()) {
450         parent_row_count = m_SearchEditorModel->itemFromIndex(parent_index)->rowCount();
451     } else {
452         parent_row_count = m_SearchEditorModel->invisibleRootItem()->rowCount();
453     }
454 
455     if (parent_row_count && row >= parent_row_count) {
456         row = parent_row_count - 1;
457     }
458 
459     if (parent_row_count == 0) {
460         row = parent_index.row();
461         parent_index = parent_index.parent();
462     }
463 
464     QModelIndex select_index = m_SearchEditorModel->index(row, 0, parent_index);
465     ui.SearchEditorTree->setCurrentIndex(select_index);
466     ui.SearchEditorTree->selectionModel()->select(select_index, QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
467 }
468 
Reload()469 void SearchEditor::Reload()
470 {
471     QMessageBox::StandardButton button_pressed;
472     button_pressed = QMessageBox::warning(this, tr("Sigil"), tr("Are you sure you want to reload all entries?  This will overwrite any unsaved changes."), QMessageBox::Ok | QMessageBox::Cancel);
473 
474     if (button_pressed == QMessageBox::Ok) {
475         m_SearchEditorModel->LoadInitialData();
476     }
477 }
478 
Import()479 void SearchEditor::Import()
480 {
481     if (SelectedRowsCount() > 1) {
482         return;
483     }
484 
485     // Get the filename to import from
486     QMap<QString,QString> file_filters;
487     file_filters[ "ini" ] = tr("Sigil INI files (*.ini)");
488     file_filters[ "csv" ] = tr("CSV files (*.csv)");
489     file_filters[ "txt" ] = tr("Text files (*.txt)");
490     QStringList filters = file_filters.values();
491     QString filter_string = "";
492     foreach(QString filter, filters) {
493         filter_string += filter + ";;";
494     }
495     QString default_filter = file_filters.value("ini");
496 
497     QFileDialog::Options options = QFileDialog::Options();
498 #ifdef Q_OS_MAC
499     options = options | QFileDialog::DontUseNativeDialog;
500 #endif
501     QString filename = QFileDialog::getOpenFileName(this,
502                        tr("Import Search Entries"),
503                        m_LastFolderOpen,
504                        filter_string,
505                        &default_filter,
506                        options);
507 
508     // Load the file and save the last folder opened
509     if (!filename.isEmpty()) {
510         QFileInfo fi(filename);
511         QString ext = fi.suffix().toLower();
512         QChar sep;
513         if (ext == "txt") {
514             sep = QChar(9);
515         } else if (ext == "csv") {
516             sep = QChar(',');
517         }
518         // Create a new group for the imported items after the selected item
519         // Avoids merging with existing groups, etc.
520         QStandardItem *item = AddGroup();
521 
522         if (item) {
523             m_SearchEditorModel->Rename(item, "Imported");
524             if (ext == "ini") {
525                 m_SearchEditorModel->LoadData(filename, item);
526             } else {
527                 m_SearchEditorModel->LoadTextData(filename, item, sep);
528             }
529             m_LastFolderOpen = QFileInfo(filename).absolutePath();
530             WriteSettings();
531         }
532     }
533 }
534 
ExportAll()535 void SearchEditor::ExportAll()
536 {
537     QList<QStandardItem *> items;
538     QStandardItem *item = m_SearchEditorModel->invisibleRootItem();
539     QModelIndex parent_index;
540 
541     for (int row = 0; row < item->rowCount(); row++) {
542         items.append(item->child(row, 0));
543     }
544 
545     ExportItems(items);
546 }
547 
Export()548 void SearchEditor::Export()
549 {
550     if (SelectedRowsCount() < 1) {
551         return;
552     }
553 
554     QList<QStandardItem *> items = GetSelectedItems();
555 
556     if (!ItemsAreUnique(m_SearchEditorModel->GetNonParentItems(items))) {
557         return;
558     }
559 
560     ExportItems(items);
561 }
562 
ExportItems(QList<QStandardItem * > items)563 void SearchEditor::ExportItems(QList<QStandardItem *> items)
564 {
565     QList<SearchEditorModel::searchEntry *> entries;
566     foreach(QStandardItem * item, items) {
567         // Get all subitems of an item not just the item itself
568         QList<QStandardItem *> sub_items = m_SearchEditorModel->GetNonParentItems(item);
569         // Get the parent path of the item
570         QString parent_path = "";
571 
572         if (item->parent()) {
573             parent_path = m_SearchEditorModel->GetFullName(item->parent());
574         }
575 
576         foreach(QStandardItem * item, sub_items) {
577             SearchEditorModel::searchEntry *entry = m_SearchEditorModel->GetEntry(item);
578             // Remove the top level paths since we're exporting a subset
579             entry->fullname.replace(QRegularExpression(parent_path), "");
580             entry->name = entry->fullname;
581             entries.append(entry);
582         }
583     }
584     // Get the filename to use
585     QMap<QString,QString> file_filters;
586     file_filters[ "ini" ] = tr("Sigil INI files (*.ini)");
587     file_filters[ "csv" ] = tr("CSV files (*.csv)");
588     file_filters[ "txt" ] = tr("Text files (*.txt)");
589     QStringList filters = file_filters.values();
590     QString filter_string = "";
591     foreach(QString filter, filters) {
592         filter_string += filter + ";;";
593     }
594     QString default_filter = file_filters.value("ini");
595 
596     QFileDialog::Options options = QFileDialog::Options();
597 #ifdef Q_OS_MAC
598     options = options | QFileDialog::DontUseNativeDialog;
599 #endif
600 
601     QString filename = QFileDialog::getSaveFileName(this,
602                        tr("Export Selected Searches"),
603                        m_LastFolderOpen,
604                        filter_string,
605                        &default_filter,
606                        options);
607 
608     if (filename.isEmpty()) {
609         return;
610     }
611 
612     QString ext = QFileInfo(filename).suffix().toLower();
613     QChar sep;
614     if (ext == "txt") {
615         sep = QChar(9);
616     } else if (ext == "csv") {
617         sep = QChar(',');
618     }
619 
620     if (ext == "ini") {
621         // Save the data, and last folder opened if successful
622         if (SaveData(entries, filename)) {
623             m_LastFolderOpen = QFileInfo(filename).absolutePath();
624             WriteSettings();
625         }
626     } else {
627         if (SaveTextData(entries, filename, sep)) {
628             m_LastFolderOpen = QFileInfo(filename).absolutePath();
629             WriteSettings();
630         }
631     }
632 }
633 
FillControls()634 void SearchEditor::FillControls()
635 {
636     if (ui.SearchEditorTree->selectionModel()->hasSelection()) {
637         QList<QStandardItem *> items = m_SearchEditorModel->GetNonGroupItems(GetSelectedItems());
638 
639         if (!ItemsAreUnique(items)) return;
640 
641         if (items.size() < 2) return;
642 
643         m_SearchEditorModel->FillControls(items);
644     }
645 }
646 
CollapseAll()647 void SearchEditor::CollapseAll()
648 {
649     ui.SearchEditorTree->collapseAll();
650 }
651 
ExpandAll()652 void SearchEditor::ExpandAll()
653 {
654     ui.SearchEditorTree->expandAll();
655 }
656 
FilterEntries(const QString & text,QStandardItem * item)657 bool SearchEditor::FilterEntries(const QString &text, QStandardItem *item)
658 {
659     const QString lowercaseText = text.toLower();
660     bool hidden = false;
661     QModelIndex parent_index;
662 
663     if (item && item->parent()) {
664         parent_index = item->parent()->index();
665     }
666 
667     if (item) {
668         // Hide the entry if it doesn't contain the entered text, otherwise show it
669         SearchEditorModel::searchEntry *entry = m_SearchEditorModel->GetEntry(item);
670 
671         if (ui.Filter->currentIndex() == 0) {
672             hidden = !(text.isEmpty() || entry->name.toLower().contains(lowercaseText));
673         } else {
674             hidden = !(text.isEmpty() || entry->name.toLower().contains(lowercaseText) ||
675                        entry->find.toLower().contains(lowercaseText) ||
676                        entry->replace.toLower().contains(lowercaseText) ||
677                        entry->controls.toLower().contains(lowercaseText));
678         }
679 
680         ui.SearchEditorTree->setRowHidden(item->row(), parent_index, hidden);
681     } else {
682         item = m_SearchEditorModel->invisibleRootItem();
683     }
684 
685     // Recursively set children
686     // Show group if any children are visible, but do not hide in case other children are visible
687     for (int row = 0; row < item->rowCount(); row++) {
688         if (!FilterEntries(text, item->child(row, 0))) {
689             hidden = false;
690             ui.SearchEditorTree->setRowHidden(item->row(), parent_index, hidden);
691         }
692     }
693 
694     return hidden;
695 }
696 
FilterEditTextChangedSlot(const QString & text)697 void SearchEditor::FilterEditTextChangedSlot(const QString &text)
698 {
699     FilterEntries(text);
700     ui.SearchEditorTree->expandAll();
701     ui.SearchEditorTree->selectionModel()->clear();
702 
703     if (!text.isEmpty()) {
704         SelectFirstVisibleNonGroup(m_SearchEditorModel->invisibleRootItem());
705     }
706 
707     return;
708 }
709 
SelectFirstVisibleNonGroup(QStandardItem * item)710 bool SearchEditor::SelectFirstVisibleNonGroup(QStandardItem *item)
711 {
712     QModelIndex parent_index;
713 
714     if (item->parent()) {
715         parent_index = item->parent()->index();
716     }
717 
718     // If the item is not a group and its visible select it and finish
719     if (item != m_SearchEditorModel->invisibleRootItem() && !ui.SearchEditorTree->isRowHidden(item->row(), parent_index)) {
720         if (!m_SearchEditorModel->ItemIsGroup(item)) {
721             ui.SearchEditorTree->selectionModel()->select(m_SearchEditorModel->index(item->row(), 0, parent_index), QItemSelectionModel::Select | QItemSelectionModel::Rows);
722             ui.SearchEditorTree->setCurrentIndex(item->index());
723             return true;
724         }
725     }
726 
727     // Recursively check children of any groups
728     for (int row = 0; row < item->rowCount(); row++) {
729         if (SelectFirstVisibleNonGroup(item->child(row, 0))) {
730             return true;
731         }
732     }
733 
734     return false;
735 }
736 
ReadSettings()737 bool SearchEditor::ReadSettings()
738 {
739     SettingsStore settings;
740     settings.beginGroup(SETTINGS_GROUP);
741     // The size of the window and it's full screen status
742     QByteArray geometry = settings.value("geometry").toByteArray();
743 
744     if (!geometry.isNull()) {
745         restoreGeometry(geometry);
746     }
747 
748     // Column widths
749     int size = settings.beginReadArray("column_data");
750 
751     for (int column = 0; column < size && column < ui.SearchEditorTree->header()->count(); column++) {
752         settings.setArrayIndex(column);
753         int column_width = settings.value("width").toInt();
754 
755         if (column_width) {
756             ui.SearchEditorTree->setColumnWidth(column, column_width);
757         }
758     }
759 
760     settings.endArray();
761     // Last folder open
762     m_LastFolderOpen = settings.value("last_folder_open").toString();
763     settings.endGroup();
764     // Return whether we did have settings to load (based on persisted column data)
765     return size > 0;
766 }
767 
WriteSettings()768 void SearchEditor::WriteSettings()
769 {
770     SettingsStore settings;
771     settings.beginGroup(SETTINGS_GROUP);
772     // The size of the window and it's full screen status
773     settings.setValue("geometry", saveGeometry());
774     // Column widths
775     settings.beginWriteArray("column_data");
776 
777     for (int column = 0; column < ui.SearchEditorTree->header()->count(); column++) {
778         settings.setArrayIndex(column);
779         settings.setValue("width", ui.SearchEditorTree->columnWidth(column));
780     }
781 
782     settings.endArray();
783     // Last folder open
784     settings.setValue("last_folder_open", m_LastFolderOpen);
785     settings.endGroup();
786 }
787 
CreateContextMenuActions()788 void SearchEditor::CreateContextMenuActions()
789 {
790     m_AddEntry  =   new QAction(tr("Add Entry"),          this);
791     m_AddGroup  =   new QAction(tr("Add Group"),          this);
792     m_Edit      =   new QAction(tr("Edit"),               this);
793     m_Cut       =   new QAction(tr("Cut"),                this);
794     m_Copy      =   new QAction(tr("Copy"),               this);
795     m_Paste     =   new QAction(tr("Paste"),              this);
796     m_Delete    =   new QAction(tr("Delete"),             this);
797     m_Import    =   new QAction(tr("Import") + "...",     this);
798     m_Reload    =   new QAction(tr("Reload") + "...",     this);
799     m_Export    =   new QAction(tr("Export") + "...",     this);
800     m_ExportAll =   new QAction(tr("Export All") + "...", this);
801     m_CollapseAll = new QAction(tr("Collapse All"),       this);
802     m_ExpandAll =   new QAction(tr("Expand All"),         this);
803     m_FillIn    =   new QAction(tr("Fill Controls"),      this);
804     m_AddEntry->setShortcut(QKeySequence(Qt::ControlModifier + Qt::Key_E));
805     m_AddGroup->setShortcut(QKeySequence(Qt::ControlModifier + Qt::Key_G));
806     m_Edit->setShortcut(QKeySequence(Qt::Key_F2));
807     m_Cut->setShortcut(QKeySequence(Qt::ControlModifier + Qt::Key_X));
808     m_Copy->setShortcut(QKeySequence(Qt::ControlModifier + Qt::Key_C));
809     m_Paste->setShortcut(QKeySequence(Qt::ControlModifier + Qt::Key_V));
810     m_Delete->setShortcut(QKeySequence::Delete);
811     // Has to be added to the dialog itself for the keyboard shortcut to work.
812     addAction(m_AddEntry);
813     addAction(m_AddGroup);
814     addAction(m_Edit);
815     addAction(m_Cut);
816     addAction(m_Copy);
817     addAction(m_Paste);
818     addAction(m_Delete);
819 }
820 
OpenContextMenu(const QPoint & point)821 void SearchEditor::OpenContextMenu(const QPoint &point)
822 {
823     SetupContextMenu(point);
824     m_ContextMenu->exec(ui.SearchEditorTree->viewport()->mapToGlobal(point));
825     if (!m_ContextMenu.isNull()) {
826         m_ContextMenu->clear();
827         // Make sure every action is enabled - in case shortcut is used after context menu disables some.
828         m_AddEntry->setEnabled(true);
829         m_AddGroup->setEnabled(true);
830         m_Edit->setEnabled(true);
831         m_Cut->setEnabled(true);
832         m_Copy->setEnabled(true);
833         m_Paste->setEnabled(true);
834         m_Delete->setEnabled(true);
835         m_Import->setEnabled(true);
836         m_Reload->setEnabled(true);
837         m_Export->setEnabled(true);
838         m_ExportAll->setEnabled(true);
839         m_CollapseAll->setEnabled(true);
840         m_ExpandAll->setEnabled(true);
841         m_FillIn->setEnabled(true);
842     }
843 }
844 
SetupContextMenu(const QPoint & point)845 void SearchEditor::SetupContextMenu(const QPoint &point)
846 {
847     int selected_rows_count = SelectedRowsCount();
848     m_ContextMenu->addAction(m_AddEntry);
849     m_ContextMenu->addAction(m_AddGroup);
850     m_ContextMenu->addSeparator();
851     m_ContextMenu->addAction(m_Edit);
852     m_ContextMenu->addSeparator();
853     m_ContextMenu->addAction(m_Cut);
854     m_Cut->setEnabled(selected_rows_count > 0);
855     m_ContextMenu->addAction(m_Copy);
856     m_Copy->setEnabled(selected_rows_count > 0);
857     m_ContextMenu->addAction(m_Paste);
858     m_Paste->setEnabled(m_SavedSearchEntries.count());
859     m_ContextMenu->addSeparator();
860     m_ContextMenu->addAction(m_Delete);
861     m_Delete->setEnabled(selected_rows_count > 0);
862     m_ContextMenu->addSeparator();
863     m_ContextMenu->addAction(m_Import);
864     m_Import->setEnabled(selected_rows_count <= 1);
865     m_ContextMenu->addAction(m_Reload);
866     m_ContextMenu->addSeparator();
867     m_ContextMenu->addAction(m_Export);
868     m_Export->setEnabled(selected_rows_count > 0);
869     m_ContextMenu->addAction(m_ExportAll);
870     m_ContextMenu->addSeparator();
871     m_ContextMenu->addAction(m_CollapseAll);
872     m_ContextMenu->addAction(m_ExpandAll);
873     m_ContextMenu->addAction(m_FillIn);
874 }
875 
Apply()876 void SearchEditor::Apply()
877 {
878     LoadFindReplace();
879 }
880 
Save()881 bool SearchEditor::Save()
882 {
883     if (SaveData()) {
884         emit ShowStatusMessageRequest(tr("Search entries saved."));
885         return true;
886     }
887 
888     return false;
889 }
890 
reject()891 void SearchEditor::reject()
892 {
893     WriteSettings();
894 
895     if (MaybeSaveDialogSaysProceed(false)) {
896         QDialog::reject();
897     }
898 }
899 
ForceClose()900 void SearchEditor::ForceClose()
901 {
902     MaybeSaveDialogSaysProceed(true);
903     close();
904 }
905 
MaybeSaveDialogSaysProceed(bool is_forced)906 bool SearchEditor::MaybeSaveDialogSaysProceed(bool is_forced)
907 {
908     if (m_SearchEditorModel->IsDataModified()) {
909         QMessageBox::StandardButton button_pressed;
910         QMessageBox::StandardButtons buttons = is_forced ? QMessageBox::Save | QMessageBox::Discard
911                                                : QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel;
912         button_pressed = QMessageBox::warning(this,
913                                               tr("Sigil: Saved Searches"),
914                                               tr("The Search entries may have been modified.\n"
915                                                       "Do you want to save your changes?"),
916                                               buttons
917                                              );
918 
919         if (button_pressed == QMessageBox::Save) {
920             return Save();
921         } else if (button_pressed == QMessageBox::Cancel) {
922             return false;
923         } else {
924             m_SearchEditorModel->LoadInitialData();
925         }
926     }
927 
928     return true;
929 }
930 
MoveUp()931 void SearchEditor::MoveUp()
932 {
933     MoveVertical(false);
934 }
935 
MoveDown()936 void SearchEditor::MoveDown()
937 {
938     MoveVertical(true);
939 }
940 
MoveVertical(bool move_down)941 void SearchEditor::MoveVertical(bool move_down)
942 {
943     if (!ui.SearchEditorTree->selectionModel()->hasSelection()) {
944         return;
945     }
946 
947     QModelIndexList selected_indexes = ui.SearchEditorTree->selectionModel()->selectedRows(0);
948 
949     if (selected_indexes.count() > 1) {
950         return;
951     }
952 
953     // Identify the selected item
954     QModelIndex index = selected_indexes.first();
955     int row = index.row();
956     QStandardItem *item = m_SearchEditorModel->itemFromIndex(index);
957     QStandardItem *source_parent_item = item->parent();
958 
959     if (!source_parent_item) {
960         source_parent_item = m_SearchEditorModel->invisibleRootItem();
961     }
962 
963     QStandardItem *destination_parent_item = source_parent_item;
964     int destination_row;
965 
966     if (move_down) {
967         if (row >= source_parent_item->rowCount() - 1) {
968             // We are the last child for this group.
969             if (source_parent_item == m_SearchEditorModel->invisibleRootItem()) {
970                 // Can't go any lower than this
971                 return;
972             }
973 
974             // Make this the next child of the parent, as though the user hit Left
975             destination_parent_item = source_parent_item->parent();
976 
977             if (!destination_parent_item) {
978                 destination_parent_item = m_SearchEditorModel->invisibleRootItem();
979             }
980 
981             destination_row = source_parent_item->index().row() + 1;
982         } else {
983             destination_row = row + 1;
984         }
985     } else {
986         if (row == 0) {
987             // We are the first child for this parent.
988             if (source_parent_item == m_SearchEditorModel->invisibleRootItem()) {
989                 // Can't go any higher than this
990                 return;
991             }
992 
993             // Make this the previous child of the parent, as though the user hit Left and Up
994             destination_parent_item = source_parent_item->parent();
995 
996             if (!destination_parent_item) {
997                 destination_parent_item = m_SearchEditorModel->invisibleRootItem();
998             }
999 
1000             destination_row = source_parent_item->index().row();
1001         } else {
1002             destination_row = row - 1;
1003         }
1004     }
1005 
1006     // Swap the item rows
1007     QList<QStandardItem *> row_items = source_parent_item->takeRow(row);
1008     destination_parent_item->insertRow(destination_row, row_items);
1009     // Get index
1010     QModelIndex destination_index = destination_parent_item->child(destination_row, 0)->index();
1011     // Make sure the path to the item is updated
1012     QStandardItem *destination_item = m_SearchEditorModel->itemFromIndex(destination_index);
1013     m_SearchEditorModel->UpdateFullName(destination_item);
1014     // Select the item row again
1015     ui.SearchEditorTree->selectionModel()->clear();
1016     ui.SearchEditorTree->setCurrentIndex(destination_index);
1017     ui.SearchEditorTree->selectionModel()->select(destination_index, QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
1018     ui.SearchEditorTree->expand(destination_parent_item->index());
1019 }
1020 
MoveLeft()1021 void SearchEditor::MoveLeft()
1022 {
1023     MoveHorizontal(true);
1024 }
1025 
MoveRight()1026 void SearchEditor::MoveRight()
1027 {
1028     MoveHorizontal(false);
1029 }
1030 
MoveHorizontal(bool move_left)1031 void SearchEditor::MoveHorizontal(bool move_left)
1032 {
1033     if (!ui.SearchEditorTree->selectionModel()->hasSelection()) {
1034         return;
1035     }
1036 
1037     QModelIndexList selected_indexes = ui.SearchEditorTree->selectionModel()->selectedRows(0);
1038 
1039     if (selected_indexes.count() > 1) {
1040         return;
1041     }
1042 
1043     // Identify the source information
1044     QModelIndex source_index = selected_indexes.first();
1045     int source_row = source_index.row();
1046     QStandardItem *source_item = m_SearchEditorModel->itemFromIndex(source_index);
1047     QStandardItem *source_parent_item = source_item->parent();
1048 
1049     if (!source_parent_item) {
1050         source_parent_item = m_SearchEditorModel->invisibleRootItem();
1051     }
1052 
1053     QStandardItem *destination_parent_item;
1054     int destination_row = 0;
1055 
1056     if (move_left) {
1057         // Skip if at root or otherwise at top level
1058         if (!source_parent_item || source_parent_item == m_SearchEditorModel->invisibleRootItem()) {
1059             return;
1060         }
1061 
1062         // Move below parent
1063         destination_parent_item = source_parent_item->parent();
1064 
1065         if (!destination_parent_item) {
1066             destination_parent_item = m_SearchEditorModel->invisibleRootItem();
1067         }
1068 
1069         destination_row = source_parent_item->index().row() + 1;
1070     } else {
1071         QModelIndex index_above = ui.SearchEditorTree->indexAbove(source_index);
1072 
1073         if (!index_above.isValid()) {
1074             return;
1075         }
1076 
1077         QStandardItem *item = m_SearchEditorModel->itemFromIndex(index_above);
1078 
1079         if (source_parent_item == item) {
1080             return;
1081         }
1082 
1083         SearchEditorModel::searchEntry *entry = m_SearchEditorModel->GetEntry(item);
1084 
1085         // Only move right if immediately under a group
1086         if (entry ->is_group) {
1087             destination_parent_item = item;
1088         } else {
1089             // Or if the item above is in a different group
1090             if (item->parent() && item->parent() != source_parent_item) {
1091                 destination_parent_item = item->parent();
1092             } else {
1093                 return;
1094             }
1095         }
1096 
1097         destination_row = destination_parent_item->rowCount();
1098     }
1099 
1100     // Swap the item rows
1101     QList<QStandardItem *> row_items = source_parent_item->takeRow(source_row);
1102     destination_parent_item->insertRow(destination_row, row_items);
1103     QModelIndex destination_index = destination_parent_item->child(destination_row)->index();
1104     // Make sure the path to the item is updated
1105     QStandardItem *destination_item = m_SearchEditorModel->itemFromIndex(destination_index);
1106     m_SearchEditorModel->UpdateFullName(destination_item);
1107     // Select the item row again
1108     ui.SearchEditorTree->selectionModel()->clear();
1109     ui.SearchEditorTree->setCurrentIndex(destination_index);
1110     ui.SearchEditorTree->selectionModel()->select(destination_index, QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
1111 }
1112 
ConnectSignalsSlots()1113 void SearchEditor::ConnectSignalsSlots()
1114 {
1115     connect(ui.FilterText,      SIGNAL(textChanged(QString)), this, SLOT(FilterEditTextChangedSlot(QString)));
1116     connect(ui.LoadSearch,      SIGNAL(clicked()),            this, SLOT(LoadFindReplace()));
1117     connect(ui.Find,            SIGNAL(clicked()),            this, SLOT(Find()));
1118     connect(ui.ReplaceCurrent,  SIGNAL(clicked()),            this, SLOT(ReplaceCurrent()));
1119     connect(ui.Replace,         SIGNAL(clicked()),            this, SLOT(Replace()));
1120     connect(ui.CountAll,        SIGNAL(clicked()),            this, SLOT(CountAll()));
1121     connect(ui.ReplaceAll,      SIGNAL(clicked()),            this, SLOT(ReplaceAll()));
1122     connect(ui.MoveUp,     SIGNAL(clicked()),            this, SLOT(MoveUp()));
1123     connect(ui.MoveDown,   SIGNAL(clicked()),            this, SLOT(MoveDown()));
1124     connect(ui.MoveLeft,   SIGNAL(clicked()),            this, SLOT(MoveLeft()));
1125     connect(ui.MoveRight,  SIGNAL(clicked()),            this, SLOT(MoveRight()));
1126     connect(ui.buttonBox->button(QDialogButtonBox::Save), SIGNAL(clicked()), this, SLOT(Save()));
1127     connect(ui.buttonBox->button(QDialogButtonBox::Close), SIGNAL(clicked()), this, SLOT(reject()));
1128     connect(ui.SearchEditorTree, SIGNAL(customContextMenuRequested(const QPoint &)),
1129             this,                SLOT(OpenContextMenu(const QPoint &)));
1130     connect(m_AddEntry,    SIGNAL(triggered()), this, SLOT(AddEntry()));
1131     connect(m_AddGroup,    SIGNAL(triggered()), this, SLOT(AddGroup()));
1132     connect(m_Edit,        SIGNAL(triggered()), this, SLOT(Edit()));
1133     connect(m_Cut,         SIGNAL(triggered()), this, SLOT(Cut()));
1134     connect(m_Copy,        SIGNAL(triggered()), this, SLOT(Copy()));
1135     connect(m_Paste,       SIGNAL(triggered()), this, SLOT(Paste()));
1136     connect(m_Delete,      SIGNAL(triggered()), this, SLOT(Delete()));
1137     connect(m_Import,      SIGNAL(triggered()), this, SLOT(Import()));
1138     connect(m_Reload,      SIGNAL(triggered()), this, SLOT(Reload()));
1139     connect(m_Export,      SIGNAL(triggered()), this, SLOT(Export()));
1140     connect(m_ExportAll,   SIGNAL(triggered()), this, SLOT(ExportAll()));
1141     connect(m_CollapseAll, SIGNAL(triggered()), this, SLOT(CollapseAll()));
1142     connect(m_ExpandAll,   SIGNAL(triggered()), this, SLOT(ExpandAll()));
1143     connect(m_FillIn,      SIGNAL(triggered()), this, SLOT(FillControls()));
1144     connect(m_SearchEditorModel, SIGNAL(SettingsFileUpdated()), this, SLOT(SettingsFileModelUpdated()));
1145     connect(m_SearchEditorModel, SIGNAL(ItemDropped(const QModelIndex &)), this, SLOT(ModelItemDropped(const QModelIndex &)));
1146 }
1147