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