1 /*
2 * Copyright Disney Enterprises, Inc.  All rights reserved.
3 * Copyright (C) 2020 L. E. Segovia <amy@amyspark.me>
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License
7 * and the following modification to it: Section 6 Trademarks.
8 * deleted and replaced with:
9 *
10 * 6. Trademarks. This License does not grant permission to use the
11 * trade names, trademarks, service marks, or product names of the
12 * Licensor and its affiliates, except as required for reproducing
13 * the content of the NOTICE file.
14 *
15 * You may obtain a copy of the License at
16 * http://www.apache.org/licenses/LICENSE-2.0
17 *
18 * @file ExprBrowser.cpp
19 * @brief Qt browser widget for list of expressions
20 * @author  aselle
21 */
22 #include <QDir>
23 #include <QFileInfo>
24 #include <QTreeWidget>
25 #include <QTreeWidgetItem>
26 #include <QVBoxLayout>
27 #include <QTabWidget>
28 #include <QHeaderView>
29 #include <QLabel>
30 #include <QTextBrowser>
31 #include <QPushButton>
32 #include <QSpacerItem>
33 #include <QSizePolicy>
34 #include <QSortFilterProxyModel>
35 #include <QFileDialog>
36 #include <QMessageBox>
37 #include <QTextStream>
38 
39 #include <cassert>
40 #include "Debug.h"
41 #include "ExprEditor.h"
42 #include "ExprBrowser.h"
43 
44 #define P3D_CONFIG_ENVVAR "P3D_CONFIG_PATH"
45 
46 class ExprTreeItem {
47   public:
ExprTreeItem(ExprTreeItem * parent,const QString & label,const QString & path)48     ExprTreeItem(ExprTreeItem* parent, const QString& label, const QString& path)
49         : row(-1), parent(parent), label(label), path(path), populated(parent == 0) {}
50 
~ExprTreeItem()51     ~ExprTreeItem() {
52         for (unsigned int i = 0; i < children.size(); i++) delete children[i];
53     }
54 
find(QString path)55     ExprTreeItem* find(QString path) {
56         if (this->path == path)
57             return this;
58         else {
59             populate();
60             for (unsigned int i = 0; i < children.size(); i++) {
61                 ExprTreeItem* ret = children[i]->find(path);
62                 if (ret) return ret;
63             }
64         }
65         return 0;
66     }
67 
clear()68     void clear() {
69         for (unsigned int i = 0; i < children.size(); i++) {
70             delete children[i];
71         }
72         children.clear();
73     }
74 
populate()75     void populate() {
76         if (populated) return;
77         populated = true;
78         QFileInfo info(path);
79         if (info.isDir()) {
80             QFileInfoList infos = QDir(path).entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
81 
82             // dbgSeExpr <<"is dir and populating "<<path.toStdString();
83             for (QList<QFileInfo>::ConstIterator it = infos.constBegin(); it != infos.constEnd(); ++it) {
84                 const QFileInfo* fi = &*it;
85                 if (fi->isDir() || fi->fileName().endsWith(QString::fromLatin1(".se"))) {
86                     addChild(new ExprTreeItem(this, fi->fileName(), fi->filePath()));
87                 }
88             }
89         }
90     }
91 
addChild(ExprTreeItem * child)92     void addChild(ExprTreeItem* child) {
93         child->row = children.size();
94         children.push_back(child);
95     }
96 
getChild(const int row)97     ExprTreeItem* getChild(const int row) {
98         populate();
99         if (row < 0 || row > (int)children.size()) {
100             assert(false);
101         }
102         return children[row];
103     }
104 
getChildCount()105     int getChildCount() {
106         populate();
107         return children.size();
108     }
109 
regen()110     void regen() {
111         std::vector<QString> labels, paths;
112         for (unsigned int i = 0; i < children.size(); i++) {
113             labels.push_back(children[i]->label);
114             paths.push_back(children[i]->path);
115             delete children[i];
116         }
117         children.clear();
118 
119         for (unsigned int i = 0; i < labels.size(); i++) addChild(new ExprTreeItem(this, labels[i], paths[i]));
120     }
121 
122     int row;
123     ExprTreeItem* parent;
124     QString label;
125     QString path;
126 
127   private:
128     std::vector<ExprTreeItem*> children;
129     bool populated;
130 };
131 
132 class ExprTreeModel : public QAbstractItemModel {
133     ExprTreeItem* root;
134 
135   public:
ExprTreeModel()136     ExprTreeModel() : root(new ExprTreeItem(0, QString(), QString())) {}
137 
~ExprTreeModel()138     ~ExprTreeModel() { delete root; }
139 
update()140     void update()
141     {
142         beginResetModel();
143         endResetModel();
144     }
145 
clear()146     void clear() {
147         beginResetModel();
148         root->clear();
149         endResetModel();
150     }
151 
addPath(const char * label,const char * path)152     void addPath(const char* label, const char* path) { root->addChild(new ExprTreeItem(root, QString::fromLatin1(label), QString::fromLatin1(path))); }
153 
parent(const QModelIndex & index) const154     QModelIndex parent(const QModelIndex& index) const {
155         if (!index.isValid()) return QModelIndex();
156         ExprTreeItem* item = (ExprTreeItem*)(index.internalPointer());
157         ExprTreeItem* parentItem = item->parent;
158         if (parentItem == root)
159             return QModelIndex();
160         else
161             return createIndex(parentItem->row, 0, parentItem);
162     }
163 
index(int row,int column,const QModelIndex & parent=QModelIndex ()) const164     QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const {
165         if (!hasIndex(row, column, parent))
166             return QModelIndex();
167         else if (!parent.isValid())
168             return createIndex(row, column, root->getChild(row));
169         else {
170             ExprTreeItem* item = (ExprTreeItem*)(parent.internalPointer());
171             return createIndex(row, column, item->getChild(row));
172         }
173     }
174 
columnCount(const QModelIndex & parent) const175     int columnCount(const QModelIndex& parent) const {
176         Q_UNUSED(parent);
177         return 1;
178     }
179 
rowCount(const QModelIndex & parent=QModelIndex ()) const180     int rowCount(const QModelIndex& parent = QModelIndex()) const {
181         if (!parent.isValid())
182             return root->getChildCount();
183         else {
184             ExprTreeItem* item = (ExprTreeItem*)(parent.internalPointer());
185             if (!item)
186                 return root->getChildCount();
187             else
188                 return item->getChildCount();
189         }
190     }
191 
data(const QModelIndex & index,int role=Qt::DisplayRole) const192     QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const {
193         if (!index.isValid()) return QVariant();
194         if (role != Qt::DisplayRole) return QVariant();
195         ExprTreeItem* item = (ExprTreeItem*)(index.internalPointer());
196         if (!item)
197             return QVariant();
198         else
199             return QVariant(item->label);
200     }
201 
find(QString path)202     QModelIndex find(QString path) {
203         ExprTreeItem* item = root->find(path);
204         if (!item) {
205             beginResetModel();
206             root->regen();
207             endResetModel();
208             item = root->find(path);
209         }
210         if (item) {
211             dbgSeExpr << "found it " ;
212             return createIndex(item->row, 0, item);
213         }
214 
215         return QModelIndex();
216     }
217 };
218 
219 class ExprTreeFilterModel : public QSortFilterProxyModel {
220   public:
ExprTreeFilterModel(QWidget * parent=0)221     ExprTreeFilterModel(QWidget* parent = 0) : QSortFilterProxyModel(parent) {}
222 
update()223     void update()
224     {
225         beginResetModel();
226         endResetModel();
227     }
228 
filterAcceptsRow(int sourceRow,const QModelIndex & sourceParent) const229     bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const {
230         if (sourceParent.isValid() && sourceModel()->data(sourceParent).toString().contains(filterRegExp()))
231             return true;
232         QString data = sourceModel()->data(sourceModel()->index(sourceRow, 0, sourceParent)).toString();
233         bool keep = data.contains(filterRegExp());
234 
235         QModelIndex subIndex = sourceModel()->index(sourceRow, 0, sourceParent);
236         if (subIndex.isValid()) {
237             for (int i = 0; i < sourceModel()->rowCount(subIndex); ++i) keep = keep || filterAcceptsRow(i, subIndex);
238         }
239         return keep;
240     }
241 };
242 
~ExprBrowser()243 ExprBrowser::~ExprBrowser() { delete treeModel; }
244 
ExprBrowser(QWidget * parent,ExprEditor * editor)245 ExprBrowser::ExprBrowser(QWidget* parent, ExprEditor* editor)
246     : QWidget(parent), editor(editor), _context(QString()), _searchPath(QString()), _applyOnSelect(true) {
247     QVBoxLayout* rootLayout = new QVBoxLayout;
248     rootLayout->setMargin(0);
249     this->setLayout(rootLayout);
250     // search and clear widgets
251     QHBoxLayout* searchAndClearLayout = new QHBoxLayout();
252     exprFilter = new QLineEdit();
253     connect(exprFilter, SIGNAL(textChanged(const QString&)), SLOT(filterChanged(const QString&)));
254     searchAndClearLayout->addWidget(exprFilter, 2);
255     QPushButton* clearFilterButton = new QPushButton(tr("X"));
256     clearFilterButton->setFixedWidth(24);
257     searchAndClearLayout->addWidget(clearFilterButton, 1);
258     rootLayout->addLayout(searchAndClearLayout);
259     connect(clearFilterButton, SIGNAL(clicked()), SLOT(clearFilter()));
260     // model of tree
261     treeModel = new ExprTreeModel();
262     proxyModel = new ExprTreeFilterModel(this);
263     proxyModel->setSourceModel(treeModel);
264     // tree widget
265     treeNew = new QTreeView;
266     treeNew->setModel(proxyModel);
267     treeNew->hideColumn(1);
268     treeNew->setHeaderHidden(true);
269     rootLayout->addWidget(treeNew);
270     // selection mode and signal
271     treeNew->setSelectionMode(QAbstractItemView::SingleSelection);
272     connect(treeNew->selectionModel(),
273             SIGNAL(currentChanged(const QModelIndex&, const QModelIndex&)),
274             SLOT(handleSelection(const QModelIndex&, const QModelIndex&)));
275 }
276 
addPath(const std::string & name,const std::string & path)277 void ExprBrowser::addPath(const std::string& name, const std::string& path) {
278     labels.append(QString::fromStdString(name));
279     paths.append(QString::fromStdString(path));
280     treeModel->addPath(name.c_str(), path.c_str());
281 }
282 
setSearchPath(const QString & context,const QString & path)283 void ExprBrowser::setSearchPath(const QString& context, const QString& path) {
284     _context = context;
285     _searchPath = path;
286 }
287 
getSelectedPath()288 std::string ExprBrowser::getSelectedPath() {
289     QModelIndex sel = treeNew->currentIndex();
290     if (sel.isValid()) {
291         QModelIndex realCurrent = proxyModel->mapToSource(sel);
292         ExprTreeItem* item = (ExprTreeItem*)realCurrent.internalPointer();
293         return item->path.toStdString();
294     }
295     return std::string("");
296 }
297 
selectPath(const char * path)298 void ExprBrowser::selectPath(const char* path) {
299     QModelIndex index = treeModel->find(QString::fromLatin1(path));
300     treeNew->setCurrentIndex(proxyModel->mapFromSource(index));
301 }
302 
update()303 void ExprBrowser::update() {
304     treeModel->update();
305     proxyModel->update();
306 }
307 
handleSelection(const QModelIndex & current,const QModelIndex & previous)308 void ExprBrowser::handleSelection(const QModelIndex& current, const QModelIndex& previous) {
309     Q_UNUSED(previous)
310     if (current.isValid()) {
311         QModelIndex realCurrent = proxyModel->mapToSource(current);
312         ExprTreeItem* item = (ExprTreeItem*)realCurrent.internalPointer();
313         QString path = item->path;
314         if (path.endsWith(QString::fromLatin1(".se"))) {
315             QFile file(path);
316             if (file.open(QIODevice::ReadOnly)) {
317                 QTextStream fileContents(&file);
318                 editor->setExpr(fileContents.readAll(), _applyOnSelect);
319             }
320         }
321     }
322 }
323 
clear()324 void ExprBrowser::clear() {
325     labels.clear();
326     paths.clear();
327     clearSelection();
328 
329     treeModel->clear();
330 }
331 
clearSelection()332 void ExprBrowser::clearSelection() { treeNew->clearSelection(); }
333 
clearFilter()334 void ExprBrowser::clearFilter() { exprFilter->clear(); }
335 
filterChanged(const QString & str)336 void ExprBrowser::filterChanged(const QString& str) {
337     proxyModel->setFilterRegExp(QRegExp(str));
338     proxyModel->setFilterKeyColumn(0);
339     if (!str.isEmpty()) {
340         treeNew->expandAll();
341     } else {
342         treeNew->collapseAll();
343     }
344 }
345 
saveExpressionAs()346 void ExprBrowser::saveExpressionAs() {
347     QString path = QFileDialog::getSaveFileName(this, tr("Save Expression"), QString::fromStdString(_userExprDir), tr("*.se"));
348 
349     if (path.length() > 0) {
350         std::ofstream file(path.toStdString().c_str());
351         if (!file) {
352             QString msg = tr("Could not open file %1 for writing").arg(path);
353             QMessageBox::warning(this, tr("Error"), QString::fromLatin1("<font face=fixed>%1</font>").arg(msg));
354             return;
355         }
356         file << editor->getExpr().toStdString();
357         file.close();
358 
359         update();
360         selectPath(path.toStdString().c_str());
361     }
362 }
363 
saveLocalExpressionAs()364 void ExprBrowser::saveLocalExpressionAs() {
365     QString path = QFileDialog::getSaveFileName(this, tr("Save Expression"), QString::fromStdString(_localExprDir), tr("*.se"));
366 
367     if (path.length() > 0) {
368         std::ofstream file(path.toStdString().c_str());
369         if (!file) {
370             QString msg = tr("Could not open file %1 for writing").arg(path);
371             QMessageBox::warning(this, tr("Error"), QString::fromLatin1("<font face=fixed>%1</font>").arg(msg));
372             return;
373         }
374         file << editor->getExpr().toStdString();
375         file.close();
376 
377         update();
378         selectPath(path.toStdString().c_str());
379     }
380 }
381 
saveExpression()382 void ExprBrowser::saveExpression() {
383     std::string path = getSelectedPath();
384     if (path.length() == 0) {
385         saveExpressionAs();
386         return;
387     }
388     std::ofstream file(path.c_str());
389     if (!file) {
390         QString msg =
391             tr("Could not open file %1 for writing.  Is it read-only?").arg(QString::fromStdString(path));
392         QMessageBox::warning(this, tr("Error"), tr("<font face=fixed>%1</font>").arg(msg));
393         return;
394     }
395     file << editor->getExpr().toStdString();
396     file.close();
397 }
398 
expandAll()399 void ExprBrowser::expandAll() { treeNew->expandAll(); }
400 
expandToDepth(int depth)401 void ExprBrowser::expandToDepth(int depth) { treeNew->expandToDepth(depth); }
402 
403 // Location for storing user's expression files
addUserExpressionPath(const std::string & context)404 void ExprBrowser::addUserExpressionPath(const std::string& context) {
405     char* homepath = getenv("HOME");
406     if (homepath) {
407         std::string path = std::string(homepath) + "/" + context + "/expressions/";
408         if (QDir(QString::fromStdString(path)).exists()) {
409             _userExprDir = path;
410             addPath("My Expressions", path);
411         }
412     }
413 }
414 
415 /*
416  * NOTE: The hard-coded paint3d assumptions can be removed once
417  * it (and bonsai?) are adjusted to call setSearchPath(context, path)
418  */
419 
getExpressionDirs()420 bool ExprBrowser::getExpressionDirs() {
421     const char* env;
422     bool enableLocal = false;
423     /*bool homeFound = false; -- for xgen's config.txt UserRepo section below */
424 
425     if (_searchPath.length() > 0)
426         env = _searchPath.toStdString().c_str();
427     else
428         env = getenv(P3D_CONFIG_ENVVAR); /* For backwards compatibility */
429 
430     if (!env) return enableLocal;
431 
432     std::string context;
433     if (_context.length() > 0) {
434         context = _context.toStdString();
435     } else {
436         context = "paint3d"; /* For backwards compatibility */
437     }
438 
439     clear();
440 
441     std::string configFile = std::string(env) + "/config.txt";
442     std::ifstream file(configFile.c_str());
443     if (file) {
444 
445         std::string key;
446         while (file) {
447             file >> key;
448 
449             if (key[0] == '#') {
450                 char buffer[1024];
451                 file.getline(buffer, 1024);
452             } else {
453                 if (key == "ExpressionDir") {
454                     std::string label, path;
455                     file >> label;
456                     file >> path;
457                     if (QDir(QString::fromStdString(path)).exists()) addPath(label, path);
458                 } else if (key == "ExpressionSubDir") {
459                     std::string path;
460                     file >> path;
461                     _localExprDir = path;
462                     if (QDir(QString::fromStdString(path)).exists()) {
463                         addPath("Local", _localExprDir);
464                         enableLocal = true;
465                     }
466                     /* These are for compatibility with xgen.
467                      * Long-term, xgen should use the same format.
468                      * Longer-term, we should use JSON or something */
469                 } else if (key == "GlobalRepo") {
470                     std::string path;
471                     file >> path;
472                     path += "/expressions/";
473                     if (QDir(QString::fromStdString(path)).exists()) addPath("Global", path);
474                 } else if (key == "LocalRepo") {
475                     std::string path;
476                     file >> path;
477                     path += "/expressions/";
478                     _localExprDir = path;
479                     if (QDir(QString::fromStdString(path)).exists()) {
480                         addPath("Local", _localExprDir);
481                         enableLocal = true;
482                     }
483 
484                     /*
485                      * xgen's config.txt has a "UserRepo" section but we
486                      * intentionally ignore it since we already add the user dir
487                      * down where the HOME stuff is handled
488                      */
489 
490                     /*
491                     } else if (key == "UserRepo") {
492                         std::string path;
493                         file>>path;
494                         path += "/expressions/";
495 
496                         size_t found = path.find("${HOME}");
497 
498                         if (found != std::string::npos) {
499                             char *homepath = getenv("HOME");
500                             if (homepath) {
501                                 path.replace(found, strlen("${HOME}"), homepath);
502                             } else {
503                                 continue;
504                             }
505                         }
506                         if(QDir(QString(path.c_str())).exists()){
507                             addPath("User", path);
508                             homeFound = true;
509                         }
510                     */
511                 } else {
512                     char buffer[1024];
513                     file.getline(buffer, 1024);
514                 }
515             }
516         }
517     }
518     addUserExpressionPath(context);
519     update();
520     return enableLocal;
521 }
522