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> </dd>" +
74 "<dd>UP - " + tr("Direction: Up") + "</dd>" +
75 "<dd>DN - " + tr("Direction: Down") + "</dd>" +
76 "<dd> </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> </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