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