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/QCoreApplication>
26 #include <QByteArray>
27 #include <QDataStream>
28 #include <QtCore/QTime>
29 #include <QFileInfo>
30 #include <QFile>
31 #include <QRegularExpression>
32 #include <QDebug>
33 
34 #include "MiscEditors/SearchEditorModel.h"
35 #include "Misc/Utility.h"
36 #include "sigil_constants.h"
37 #include "sigil_exception.h"
38 
39 static const QString SETTINGS_FILE          = "sigil_searches_v2.ini";
40 static const QString OLD_SETTINGS_FILE      = "sigil_searches.ini";
41 
42 static const QString SETTINGS_GROUP         = "search_entries";
43 static const QString ENTRY_NAME             = "Name";
44 static const QString ENTRY_FIND             = "Find";
45 static const QString ENTRY_REPLACE          = "Replace";
46 static const QString ENTRY_CONTROLS         = "Controls";
47 
48 const int COLUMNS = 4;
49 
50 static const int IS_GROUP_ROLE = Qt::UserRole + 1;
51 static const int FULLNAME_ROLE = Qt::UserRole + 2;
52 
53 static const QString SEARCH_EXAMPLES_FILE = "search_entries.ini";
54 
55 SearchEditorModel *SearchEditorModel::m_instance = 0;
56 
instance()57 SearchEditorModel *SearchEditorModel::instance()
58 {
59     if (m_instance == 0) {
60         m_instance = new SearchEditorModel();
61     }
62 
63     return m_instance;
64 }
65 
SearchEditorModel(QObject * parent)66 SearchEditorModel::SearchEditorModel(QObject *parent)
67     : QStandardItemModel(parent),
68       m_FSWatcher(new QFileSystemWatcher()),
69       m_IsDataModified(false)
70 {
71     m_SettingsPath = Utility::DefinePrefsDir() + "/" + SETTINGS_FILE;
72     QString OldSettingsPath = Utility::DefinePrefsDir() + "/" + OLD_SETTINGS_FILE;
73     QFileInfo fi(m_SettingsPath);
74     if (!fi.exists()) {
75         QFileInfo oldfi(OldSettingsPath);
76         if (oldfi.exists() && oldfi.isFile()) {
77             // create v2 settings file from old one
78             bool success = QFile::copy(oldfi.absoluteFilePath(), m_SettingsPath);
79             if (!success) {
80                 qDebug() << "Failed to create v2 saved searches from old";
81             }
82         }
83     }
84     QStringList header;
85     header.append(tr("Name"));
86     header.append(tr("Find"));
87     header.append(tr("Replace"));
88     header.append(tr("Controls"));
89     setHorizontalHeaderLabels(header);
90     LoadInitialData();
91     // Save it to make sure we have a file in case it was loaded from examples
92     SaveData();
93 
94     if (!m_FSWatcher->files().contains(m_SettingsPath)) {
95         m_FSWatcher->addPath(m_SettingsPath);
96     }
97 
98     connect(m_FSWatcher, SIGNAL(fileChanged(const QString &)),
99             this,        SLOT(SettingsFileChanged(const QString &)), Qt::DirectConnection);
100     connect(this, SIGNAL(itemChanged(QStandardItem *)),
101             this, SLOT(ItemChangedHandler(QStandardItem *)));
102     connect(this, SIGNAL(rowsRemoved(const QModelIndex &, int, int)),
103             this, SLOT(RowsRemovedHandler(const QModelIndex &, int, int)));
104 }
105 
~SearchEditorModel()106 SearchEditorModel::~SearchEditorModel()
107 {
108     if (m_instance) {
109         delete m_instance;
110         m_instance = 0;
111     }
112 }
113 
SetDataModified(bool modified)114 void SearchEditorModel::SetDataModified(bool modified)
115 {
116     m_IsDataModified = modified;
117 }
118 
IsDataModified()119 bool SearchEditorModel::IsDataModified()
120 {
121     return m_IsDataModified;
122 }
123 
supportedDropActions() const124 Qt::DropActions SearchEditorModel::supportedDropActions() const
125 {
126     return Qt::MoveAction;
127 }
128 
dropMimeData(const QMimeData * data,Qt::DropAction action,int row,int column,const QModelIndex & parent)129 bool SearchEditorModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
130 {
131     QModelIndex new_parent(parent);
132 
133     if (parent.column() > 0) {
134         // User is trying to drop onto a child column - treat it as though they are dropping in the initial column.
135         new_parent = index(parent.row(), 0, parent.parent());
136     }
137 
138     if (column > 0) {
139         // Same as above but for when user drops into the between row selector in a child column.
140         column = 0;
141     }
142 
143     // If dropped on an non-group entry convert to parent item group/row
144     if (new_parent.isValid() && !itemFromIndex(new_parent)->data(IS_GROUP_ROLE).toBool()) {
145         row = new_parent.row() + 1;
146         new_parent = new_parent.parent();
147     }
148 
149     if (!QStandardItemModel::dropMimeData(data, action, row, column, new_parent)) {
150         return false;
151     }
152 
153     // As our drag/drop completed successfully, recalculate the fullname for all the items in the model
154     UpdateFullName(invisibleRootItem());
155 
156     // If we dropped onto a parent group, make sure it is expanded.
157     if (new_parent.isValid()) {
158         emit ItemDropped(new_parent);
159     }
160 
161     SetDataModified(true);
162     return true;
163 }
164 
RowsRemovedHandler(const QModelIndex & parent,int start,int end)165 void SearchEditorModel::RowsRemovedHandler(const QModelIndex &parent, int start, int end)
166 {
167     SetDataModified(true);
168 }
169 
170 
ItemChangedHandler(QStandardItem * item)171 void SearchEditorModel::ItemChangedHandler(QStandardItem *item)
172 {
173     Q_ASSERT(item);
174 
175     if (item->column() == 3) {
176         QString controls = item->text();
177         item->setToolTip(BuildControlsToolTip(controls));
178     }
179 
180     if (item->column() != 0) {
181         SetDataModified(true);
182         return;
183     }
184 
185     // Restore name if nothing entered or contains a group indicator
186     if (item->text().isEmpty() || item->text().contains("/")) {
187         QString name = item->data(FULLNAME_ROLE).toString();
188         name.replace(QRegularExpression("/$"), "");
189 
190         if (name.contains("/")) {
191             name = name.split("/").last();
192         }
193 
194         item->setText(name);
195         return;
196     }
197 
198     Rename(item);
199 }
200 
Rename(QStandardItem * item,const QString & name)201 void SearchEditorModel::Rename(QStandardItem *item, const QString &name)
202 {
203     // Update the name and all its children
204     // Disconnect change signal while changing the items
205     disconnect(this, SIGNAL(itemChanged(QStandardItem *)),
206                this, SLOT(ItemChangedHandler(QStandardItem *)));
207 
208     // If item name not already changed, set it
209     if (name != "") {
210         disconnect(this, SIGNAL(itemChanged(QStandardItem *)),
211                    this, SLOT(ItemChangedHandler(QStandardItem *)));
212         item->setText(name);
213         connect(this, SIGNAL(itemChanged(QStandardItem *)),
214                 this, SLOT(ItemChangedHandler(QStandardItem *)));
215     }
216 
217     UpdateFullName(item);
218     connect(this, SIGNAL(itemChanged(QStandardItem *)),
219             this, SLOT(ItemChangedHandler(QStandardItem *)));
220     SetDataModified(true);
221 }
222 
UpdateFullName(QStandardItem * item)223 void SearchEditorModel::UpdateFullName(QStandardItem *item)
224 {
225     QStandardItem *parent_item = item->parent();
226 
227     if (!parent_item) {
228         parent_item = invisibleRootItem();
229     }
230 
231     QString fullname = parent_item->data(FULLNAME_ROLE).toString() + item->text();
232 
233     if (item->data(IS_GROUP_ROLE).toBool()) {
234         fullname.append("/");
235     }
236 
237     item->setToolTip(fullname);
238     item->setData(fullname, FULLNAME_ROLE);
239 
240     for (int row = 0; row < item->rowCount(); row++) {
241         UpdateFullName(item->child(row, 0));
242     }
243 }
244 
SettingsFileChanged(const QString & path) const245 void SearchEditorModel::SettingsFileChanged(const QString &path) const
246 {
247     // The file may have been deleted prior to writing a new version - give it a chance to write.
248     QTime wake_time = QTime::currentTime().addMSecs(1000);
249 
250     while (!QFile::exists(path) && QTime::currentTime() < wake_time) {
251         QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
252     }
253 
254     // The signal is also received after resource files are removed / renamed,
255     // but it can be safely ignored because QFileSystemWatcher automatically stops watching them.
256     if (QFile::exists(path)) {
257         // Some editors write the updated contents to a temporary file
258         // and then atomically move it over the watched file.
259         // In this case QFileSystemWatcher loses track of the file, so we have to add it again.
260         if (!m_FSWatcher->files().contains(path)) {
261             m_FSWatcher->addPath(path);
262         }
263 
264         instance()->LoadInitialData();
265         emit SettingsFileUpdated();
266     }
267 }
268 
ItemIsGroup(QStandardItem * item)269 bool SearchEditorModel::ItemIsGroup(QStandardItem *item)
270 {
271     return item->data(IS_GROUP_ROLE).toBool();
272 }
273 
GetFullName(QStandardItem * item)274 QString SearchEditorModel::GetFullName(QStandardItem *item)
275 {
276     return item->data(FULLNAME_ROLE).toString();
277 }
278 
GetEntryFromName(const QString & name,QStandardItem * item)279 SearchEditorModel::searchEntry *SearchEditorModel::GetEntryFromName(const QString &name, QStandardItem *item)
280 {
281     return GetEntry(GetItemFromName(name, item));
282 }
283 
GetItemFromName(const QString & name,QStandardItem * item)284 QStandardItem *SearchEditorModel::GetItemFromName(const QString &name, QStandardItem *item)
285 {
286     QStandardItem *found_item = NULL;
287 
288     if (!item) {
289         item = invisibleRootItem();
290     }
291 
292     if (item != invisibleRootItem() && item->data(FULLNAME_ROLE).toString() == name) {
293         return item;
294     }
295 
296     for (int row = 0; row < item->rowCount(); row++) {
297         found_item = GetItemFromName(name, item->child(row, 0));
298 
299         // Return with first found entry
300         if (found_item) {
301             return found_item;
302         }
303     }
304 
305     return found_item;
306 }
307 
LoadInitialData()308 void SearchEditorModel::LoadInitialData()
309 {
310     removeRows(0, rowCount());
311     LoadData();
312 
313     if (invisibleRootItem()->rowCount() == 0) {
314         AddExampleEntries();
315     }
316 
317     SetDataModified(false);
318 }
319 
LoadData(const QString & filename,QStandardItem * item)320 void SearchEditorModel::LoadData(const QString &filename, QStandardItem *item)
321 {
322     QString settings_path = filename;
323     if (settings_path.isEmpty()) settings_path = m_SettingsPath;
324 
325     SettingsStore ss(settings_path);
326 
327     int size = ss.beginReadArray(SETTINGS_GROUP);
328 
329     // Add one entry at a time to the list
330     for (int i = 0; i < size; ++i) {
331         ss.setArrayIndex(i);
332         SearchEditorModel::searchEntry *entry = new SearchEditorModel::searchEntry();
333         QString fullname = ss.value(ENTRY_NAME).toString();
334         fullname.replace(QRegularExpression("\\s*/+\\s*"), "/");
335         fullname.replace(QRegularExpression("^/"), "");
336         entry->is_group = fullname.endsWith("/");
337         // Name is set to fullname only while looping through parent groups when adding
338         entry->name = fullname;
339         entry->fullname = fullname;
340         entry->find = ss.value(ENTRY_FIND).toString();
341         entry->replace = ss.value(ENTRY_REPLACE).toString();
342         entry->controls = ss.value(ENTRY_CONTROLS, "").toString();
343         AddFullNameEntry(entry, item);
344         // done with the temporary entry so remove it
345         delete entry;
346     }
347     ss.endArray();
348 }
349 
350 
LoadTextData(const QString & filename,QStandardItem * item,const QChar & sep)351 void SearchEditorModel::LoadTextData(const QString &filename, QStandardItem *item, const QChar &sep)
352 {
353     QFileInfo fi(filename);
354     QString groupname = fi.fileName() + "/";
355     int cnt = 1;
356     if (fi.exists()) {
357         QString data = Utility::ReadUnicodeTextFile(filename);
358         QStringList datalines = data.split('\n');
359         foreach(QString aline, datalines) {
360             if (!aline.isEmpty()) {
361                 QStringList findreplace;
362                 if (sep == ',') {
363                     findreplace  = Utility::parseCSVLine(aline);
364                 } else {
365                     findreplace = aline.split(sep);
366                 }
367                 // add to end to prevent errors
368                 findreplace << "" << "" << "" << "";
369                 QString localname = "rep" + QStringLiteral("%1").arg(cnt, 5, 10, QLatin1Char('0'));
370                 QString fullname;
371                 SearchEditorModel::searchEntry *entry = new SearchEditorModel::searchEntry();
372                 // if no name info appears
373                 if (findreplace.at(0).isEmpty()) {
374                     fullname = groupname + localname;
375                 } else {
376                     // this was created by an Export so fullname is present
377                     fullname = findreplace.at(0);
378                 }
379                 fullname.replace(QRegularExpression("\\s*/+\\s*"), "/");
380                 fullname.replace(QRegularExpression("^/"), "");
381                 entry->is_group = fullname.endsWith("/");
382                 // Name is set to fullname only while looping through parent groups when adding
383                 entry->name = fullname;
384                 entry->fullname = fullname;
385                 entry->find = findreplace.at(1);
386                 entry->replace = findreplace.at(2);
387                 entry->controls = findreplace.at(3);
388 
389                 AddFullNameEntry(entry, item);
390                 // done with the temporary entry so remove it
391                 delete entry;
392                 cnt++;
393             }
394         }
395     }
396 }
397 
398 
AddFullNameEntry(SearchEditorModel::searchEntry * entry,QStandardItem * parent_item,int row)399 void SearchEditorModel::AddFullNameEntry(SearchEditorModel::searchEntry *entry, QStandardItem *parent_item, int row)
400 {
401     if (!parent_item) {
402         parent_item = invisibleRootItem();
403     }
404 
405     if (parent_item != invisibleRootItem() && !parent_item->data(IS_GROUP_ROLE).toBool()) {
406         row = parent_item->row() + 1;
407         parent_item = parent_item->parent();
408     }
409 
410     if (row < 0) {
411         row = parent_item->rowCount();
412     }
413 
414     QString entry_name = entry->name;
415 
416     if (entry->name.contains("/")) {
417         QStringList group_names = entry->name.split("/", QString::SkipEmptyParts);
418         entry_name = group_names.last();
419 
420         if (!entry->is_group) {
421             group_names.removeLast();
422         }
423 
424         foreach(QString group_name, group_names) {
425             bool found = false;
426 
427             for (int r = 0; r < parent_item->rowCount(); r++) {
428                 if (parent_item->child(r, 0)->data(IS_GROUP_ROLE).toBool() && parent_item->child(r, 0)->text() == group_name) {
429                     parent_item = parent_item->child(r, 0);
430                     found = true;
431                     break;
432                 }
433             }
434 
435             if (!found) {
436                 SearchEditorModel::searchEntry *new_entry = new SearchEditorModel::searchEntry();
437                 new_entry->is_group = true;
438                 new_entry->name = group_name;
439                 parent_item = AddEntryToModel(new_entry, new_entry->is_group, parent_item, parent_item->rowCount());
440                 delete new_entry;
441             }
442         }
443         row = parent_item->rowCount();
444     }
445 
446     if (!entry->is_group) {
447         entry->name = entry_name;
448         AddEntryToModel(entry, entry->is_group, parent_item, row);
449     }
450 }
451 
452 
FillControls(const QList<QStandardItem * > & items)453 void SearchEditorModel::FillControls(const QList<QStandardItem*> &items)
454 {
455     if (items.isEmpty()) return;
456 
457     QStandardItem *source_item = items.at(0);
458 
459     QStandardItem *parent_item = invisibleRootItem();
460     if (source_item->parent()) {
461         parent_item = source_item->parent();
462     }
463     QString controls = parent_item->child(source_item->row(), 3)->text();
464 
465     for (int i = 1; i < items.size(); i++) {
466         QStandardItem* aitem = items.at(i);
467         parent_item = invisibleRootItem();
468         if (aitem->parent()) {
469             parent_item = aitem->parent();
470         }
471         parent_item->child(aitem->row(), 3)->setText(controls);
472     }
473     SetDataModified(true);
474 }
475 
476 
BuildControlsToolTip(const QString & controls)477 QString SearchEditorModel::BuildControlsToolTip(const QString & controls)
478 {
479     QString tooltip_controls = "";
480     if (controls != "") {
481         if (controls.contains("NL")) {
482             tooltip_controls.append("NL - " + tr("Mode: Normal") + "\n");
483         }
484         if (controls.contains("RX")) {
485             tooltip_controls.append("RX - " + tr("Mode: Regular Expression") + "\n");
486         }
487         if (controls.contains("CS")) {
488             tooltip_controls.append("CS - " + tr("Mode: Case Sensitive") + "\n");
489         }
490         if (controls.contains("UP")) {
491             tooltip_controls.append("UP - " + tr("Direction: Up") + "\n");
492         }
493         if (controls.contains("DN")) {
494             tooltip_controls.append("DN - " + tr("Direction: Down") + "\n");
495         }
496         if (controls.contains("CF")) {
497             tooltip_controls.append("CF - " + tr("Target: Current File") + "\n");
498         }
499         if (controls.contains("AH")) {
500             tooltip_controls.append("AH - " + tr("Target: All HTML Files") + "\n");
501         }
502         if (controls.contains("SH")) {
503             tooltip_controls.append("SH - " + tr("Target: Selected HTML Files") + "\n");
504         }
505         if (controls.contains("TH")) {
506             tooltip_controls.append("TH - " + tr("Target: Tabbed HTML Files") + "\n");
507         }
508         if (controls.contains("AC")) {
509             tooltip_controls.append("AC - " + tr("Target: All CSS Files") + "\n");
510         }
511         if (controls.contains("SC")) {
512             tooltip_controls.append("SC - " + tr("Target: Selected CSS Files") + "\n");
513         }
514         if (controls.contains("TC")) {
515             tooltip_controls.append("TC - " + tr("Target: Tabbed CSS Files") + "\n");
516         }
517         if (controls.contains("OP")) {
518             tooltip_controls.append("OP - " + tr("Target: OPF File") + "\n");
519         }
520         if (controls.contains("NX")) {
521             tooltip_controls.append("NX - " + tr("Target: NCX File") + "\n");
522         }
523         if (controls.contains("DA")) {
524             tooltip_controls.append("DA - " + tr("Option: DotAll") + "\n");
525         }
526         if (controls.contains("MM")) {
527             tooltip_controls.append("MM - " + tr("Option: Minimal Match") + "\n");
528         }
529         if (controls.contains("AT")) {
530             tooltip_controls.append("AT - " + tr("Option: Auto Tokenise") + "\n");
531         }
532         if (controls.contains("WR")) {
533             tooltip_controls.append("WR - " + tr("Option: Wrap") + "\n");
534         }
535     }
536     return tooltip_controls;
537 }
538 
AddEntryToModel(SearchEditorModel::searchEntry * entry,bool is_group,QStandardItem * parent_item,int row)539 QStandardItem *SearchEditorModel::AddEntryToModel(SearchEditorModel::searchEntry *entry, bool is_group, QStandardItem *parent_item, int row)
540 {
541     // parent_item must be a group item
542     if (!parent_item) {
543         parent_item = invisibleRootItem();
544     }
545 
546     // Create an empty entry if none supplied
547     if (!entry) {
548         entry = new SearchEditorModel::searchEntry();
549         entry->is_group = is_group;
550 
551         if (!is_group) {
552             entry->name = "Search";
553             entry->find = "";
554             entry->replace = "";
555             entry->controls = "";
556         } else {
557             entry->name = "Group";
558         }
559     }
560 
561     entry->fullname = entry->name;
562 
563     if (parent_item != invisibleRootItem()) {
564         // Set the fullname based on the parent entry
565         entry->fullname = parent_item->data(FULLNAME_ROLE).toString() + entry->name;
566     }
567 
568     if (entry->is_group) {
569         entry->fullname += "/";
570     }
571 
572     QList<QStandardItem *> rowItems;
573     QStandardItem *group_item = parent_item;
574 
575     if (entry->is_group) {
576         group_item = new QStandardItem(entry->name);
577         QFont font;
578         font.setWeight(QFont::Bold);
579         group_item->setFont(font);
580         rowItems << group_item;
581 
582         for (int col = 1; col < COLUMNS ; col++) {
583             QStandardItem *item = new QStandardItem("");
584             item->setEditable(false);
585             rowItems << item;
586         }
587     } else {
588         rowItems << new QStandardItem(entry->name);
589         rowItems << new QStandardItem(entry->find);
590         rowItems << new QStandardItem(entry->replace);
591         rowItems << new QStandardItem(entry->controls);
592     }
593 
594     rowItems[0]->setData(entry->is_group, IS_GROUP_ROLE);
595     rowItems[0]->setData(entry->fullname, FULLNAME_ROLE);
596     rowItems[0]->setToolTip(entry->fullname);
597     rowItems[3]->setToolTip(BuildControlsToolTip(entry->controls));
598 
599     // Add the new item to the model at the specified row
600     QStandardItem *new_item;
601 
602     if (row < 0 || row >= parent_item->rowCount()) {
603         parent_item->appendRow(rowItems);
604         new_item = parent_item->child(parent_item->rowCount() - 1, 0);
605     } else {
606         parent_item->insertRow(row, rowItems);
607         new_item = parent_item->child(row, 0);
608     }
609 
610     SetDataModified(true);
611     return new_item;
612 }
613 
AddExampleEntries()614 void SearchEditorModel::AddExampleEntries()
615 {
616     QString examples_dir;
617 #ifdef Q_OS_MAC
618     examples_dir = QCoreApplication::applicationDirPath() + "/../examples/";
619 #elif defined(Q_OS_WIN32)
620     examples_dir = QCoreApplication::applicationDirPath() + "/examples/";
621 #else
622     // all flavours of linux / unix
623     // user supplied environment variable to 'share/sigil' directory will override everything
624     if (!sigil_extra_root.isEmpty()) {
625         examples_dir = sigil_extra_root + "/examples/";
626     } else {
627         examples_dir = sigil_share_root + "/examples/";
628     }
629 #endif
630     LoadData(examples_dir % SEARCH_EXAMPLES_FILE);
631 }
632 
GetNonGroupItems(QList<QStandardItem * > items)633 QList<QStandardItem *> SearchEditorModel::GetNonGroupItems(QList<QStandardItem *> items)
634 {
635     QList<QStandardItem *> all_items;
636     foreach(QStandardItem * item, items) {
637         all_items.append(GetNonGroupItems(item));
638     }
639     return all_items;
640 }
641 
GetNonGroupItems(QStandardItem * item)642 QList<QStandardItem *> SearchEditorModel::GetNonGroupItems(QStandardItem *item)
643 {
644     QList<QStandardItem *> items;
645 
646     if (!item->data(IS_GROUP_ROLE).toBool()) {
647         items.append(item);
648     }
649 
650     for (int row = 0; row < item->rowCount(); row++) {
651         items.append(GetNonGroupItems(item->child(row, 0)));
652     }
653 
654     return items;
655 }
656 
GetNonParentItems(QList<QStandardItem * > items)657 QList<QStandardItem *> SearchEditorModel::GetNonParentItems(QList<QStandardItem *> items)
658 {
659     QList<QStandardItem *> all_items;
660     foreach(QStandardItem * item, items) {
661         all_items.append(GetNonParentItems(item));
662     }
663     return all_items;
664 }
665 
GetNonParentItems(QStandardItem * item)666 QList<QStandardItem *> SearchEditorModel::GetNonParentItems(QStandardItem *item)
667 {
668     QList<QStandardItem *> items;
669 
670     if (item->rowCount() == 0) {
671         if (item != invisibleRootItem()) {
672             items.append(item);
673         }
674     }
675 
676     for (int row = 0; row < item->rowCount(); row++) {
677         items.append(GetNonParentItems(item->child(row, 0)));
678     }
679 
680     return items;
681 }
682 
GetEntries(QList<QStandardItem * > items)683 QList<SearchEditorModel::searchEntry *> SearchEditorModel::GetEntries(QList<QStandardItem *> items)
684 {
685     QList<SearchEditorModel::searchEntry *> entries;
686     foreach(QStandardItem * item, items) {
687         entries.append(GetEntry(item));
688     }
689     return entries;
690 }
691 
GetEntry(QStandardItem * item)692 SearchEditorModel::searchEntry *SearchEditorModel::GetEntry(QStandardItem *item)
693 {
694     QStandardItem *parent_item;
695 
696     if (item->parent()) {
697         parent_item = item->parent();
698     } else {
699         parent_item = invisibleRootItem();
700     }
701 
702     SearchEditorModel::searchEntry *entry = new SearchEditorModel::searchEntry();
703     entry->is_group =    parent_item->child(item->row(), 0)->data(IS_GROUP_ROLE).toBool();
704     entry->fullname =    parent_item->child(item->row(), 0)->data(FULLNAME_ROLE).toString();
705     entry->name =        parent_item->child(item->row(), 0)->text();
706     entry->find =        parent_item->child(item->row(), 1)->text();
707     entry->replace =     parent_item->child(item->row(), 2)->text();
708     entry->controls =    parent_item->child(item->row(), 3)->text();
709     return entry;
710 }
711 
GetItemFromId(quintptr id,int row,QStandardItem * item) const712 QStandardItem *SearchEditorModel::GetItemFromId(quintptr id, int row, QStandardItem *item) const
713 {
714     QStandardItem *found_item = NULL;
715 
716     if (!item) {
717         item = invisibleRootItem();
718     }
719 
720     if (item->index().internalId() == id) {
721         if (item->parent()) {
722             item = item->parent();
723         } else {
724             item = invisibleRootItem();
725         }
726 
727         return item->child(row, 0);
728     }
729 
730     for (int r = 0; r < item->rowCount(); r++) {
731         found_item = GetItemFromId((quintptr)id, row, item->child(r, 0));
732 
733         // Return with first found entry
734         if (found_item) {
735             return found_item;
736         }
737     }
738 
739     return found_item;
740 }
741 
742 
SaveTextData(QList<SearchEditorModel::searchEntry * > entries,const QString & filename,const QChar & sep)743 QString SearchEditorModel::SaveTextData(QList<SearchEditorModel::searchEntry *> entries,
744                                         const QString &filename,
745                                         const QChar &sep)
746 {
747     QString message = "";
748     bool clean_up_needed = false;
749     QString save_path = filename;
750 
751     // Save everything if no entries selected
752     if (entries.isEmpty()) {
753         QList<QStandardItem *> items = GetNonParentItems(invisibleRootItem());
754 
755         if (!items.isEmpty()) {
756             // GetEntries calls GetEntry which creates each entry with new
757             entries = GetEntries(items);
758             clean_up_needed = true;
759         }
760     }
761 
762     // Create Data to write to file
763     QStringList res;
764     foreach(SearchEditorModel::searchEntry * entry, entries) {
765         QStringList data;
766         data << entry->fullname;
767         if (!entry->is_group) {
768            data << entry->find;
769            data << entry->replace;
770            data << entry->controls;
771         }
772         if (sep == ',') {
773             res << Utility::createCSVLine(data);
774         } else {
775             res << data.join(sep);
776         }
777     }
778     QString text = res.join('\n');
779     try {
780         Utility::WriteUnicodeTextFile(text, save_path);
781     } catch (CannotOpenFile& e) {
782         message = QString(e.what());
783     }
784 
785     // delete each entry if we created them above
786     if (clean_up_needed) {
787         foreach(SearchEditorModel::searchEntry* entry, entries) {
788             delete entry;
789         }
790     }
791     return message;
792 }
793 
794 
SaveData(QList<SearchEditorModel::searchEntry * > entries,const QString & filename)795 QString SearchEditorModel::SaveData(QList<SearchEditorModel::searchEntry *> entries, const QString &filename)
796 {
797     QString message = "";
798     bool clean_up_needed = false;
799     QString settings_path = filename;
800     if (settings_path.isEmpty()) settings_path = m_SettingsPath;
801 
802     // Save everything if no entries selected
803     if (entries.isEmpty()) {
804         QList<QStandardItem *> items = GetNonParentItems(invisibleRootItem());
805 
806         if (!items.isEmpty()) {
807             // GetEntries calls GetEntry which creates each entry with new
808             entries = GetEntries(items);
809             clean_up_needed = true;
810         }
811     }
812 
813     // Stop watching the file while we save it
814     if (m_FSWatcher->files().contains(settings_path)) {
815         m_FSWatcher->removePath(settings_path);
816     }
817 
818     // Open the default file for save, or specific file for export
819     {
820         SettingsStore ss(settings_path);
821 
822         // ss.sync();
823 
824         if (!ss.isWritable()) {
825             message = tr("Unable to create file %1").arg(filename);
826             // Watch the file again
827             m_FSWatcher->addPath(settings_path);
828             // delete each entry if we created them above
829             if (clean_up_needed) {
830                 foreach(SearchEditorModel::searchEntry* entry, entries) {
831                     delete entry;
832                 }
833             }
834             return message;
835         }
836 
837         // Remove the old values to account for deletions
838         ss.remove(SETTINGS_GROUP);
839         ss.beginWriteArray(SETTINGS_GROUP);
840         int i = 0;
841         foreach(SearchEditorModel::searchEntry * entry, entries) {
842             ss.setArrayIndex(i++);
843             ss.setValue(ENTRY_NAME, entry->fullname);
844 
845             if (!entry->is_group) {
846                 ss.setValue(ENTRY_FIND, entry->find);
847                 ss.setValue(ENTRY_REPLACE, entry->replace);
848                 ss.setValue(ENTRY_CONTROLS, entry->controls);
849             }
850         }
851 
852         // delete each entry if we created them above
853         if (clean_up_needed) {
854             foreach(SearchEditorModel::searchEntry* entry, entries) {
855                 delete entry;
856             }
857         }
858 
859         ss.endArray();
860 
861         // Make sure file is created/updated so it can be checked
862         ss.sync();
863     }
864 
865     // Watch the file again
866     m_FSWatcher->addPath(settings_path);
867     SetDataModified(false);
868     return message;
869 }
870 
data(const QModelIndex & index,int role) const871 QVariant SearchEditorModel::data(const QModelIndex &index, int role) const
872 {
873     if (index.isValid() && index.column() > 0 && role == Qt::SizeHintRole) {
874         // Make all rows the same height using the name column to ensure text limited to a single line
875         return data(this->index(0, 0), role).toSize();
876     }
877 
878     return QStandardItemModel::data(index, role);
879 }
880