Skip to content

Commit a97387a

Browse files
feat: osltoy - Add command line and GUI ways to adjust include search paths (#1876)
For Dev Days #1862 Addressing a osltoy feature request, I've added the ability to specify custom search paths. The changes consist of the following: - Added a command line option `-I <path>` to specify search paths (`OIIO::ArgParse` unfortunately can't mirror `OSLCompiler`'s `-Ipath` syntax) - OSLToyMainWindow now stores an array of `OSLCompiler` options alongside a flag indicating whether or not they should be regenerated before compilation. The only option currently able to be specified is `-Ipath`, but this should ease any future extension. - Added a GUI component consiting of a window with a mutable list of the current set of search paths. This list is initially populated with any paths passed via the command line, but any modifications to the list will be reflected when the shader is next recompiled. Modifying this list does not currently force recompilation. - Added an entry to the Tools menu to open the new GUI component. - Added a line of output to the error window indicating what options the compiler was invoked with. I've defaulted to having it print for every run, but this may want to only print on failure. ## Tests I did not add any automated tests for this feature, as is consistent with the rest of osltoy. I did, however, do some manual testing to make sure the functionality works as expected. Shaders that failed to `#include` some file instead succeeded when the parent directory of that file was added to the list of search paths via both the command line and the GUI component. Respectively, when said search path is then removed via the GUI component, any subsequent compilation attempts fail again. The GUI component otherwise behaves as expected. --------- Signed-off-by: Maxwell Iverson <[email protected]>
1 parent f769afd commit a97387a

File tree

3 files changed

+332
-13
lines changed

3 files changed

+332
-13
lines changed

src/osltoy/osltoyapp.cpp

Lines changed: 304 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
#include <QTabWidget>
3333
#include <QTextEdit>
3434
#include <QToolTip>
35+
#include <QVBoxLayout>
3536

3637
// QT's extension foreach defines a foreach macro which interferes
3738
// with an OSL internal foreach method. So we will undefine it here
@@ -351,6 +352,241 @@ class OSLToyRenderView final : public QLabel {
351352
#endif
352353
};
353354

355+
356+
357+
class OSLToySearchPathLine final : public QLineEdit {
358+
// Q_OBJECT
359+
public:
360+
explicit OSLToySearchPathLine(OSLToySearchPathEditor* editor, int index);
361+
362+
bool previouslyHadContent() const { return m_previouslyHadContent; }
363+
364+
void setPreviouslyHadContent(bool value) { m_previouslyHadContent = value; }
365+
366+
int getIndex() const { return m_index; }
367+
368+
QSize sizeHint() const override;
369+
370+
private:
371+
static QColor getColor(int index)
372+
{
373+
if (index % 2)
374+
return QColor(0xFFE0F0FF); // light blue
375+
else
376+
return Qt::white;
377+
}
378+
379+
bool m_previouslyHadContent = false;
380+
int m_index;
381+
OSLToySearchPathEditor* m_editor = nullptr;
382+
};
383+
384+
385+
386+
// More generically, this is a popup window with a list (that grows as needed) of editable text items.
387+
class OSLToySearchPathEditor final : public QWidget {
388+
using UpdatePathListAction
389+
= std::function<void(const std::vector<std::string>&)>;
390+
391+
public:
392+
OSLToySearchPathEditor(QWidget* parent,
393+
UpdatePathListAction updatePathsAction)
394+
: QWidget(parent, static_cast<Qt::WindowFlags>(
395+
Qt::Tool | Qt::WindowStaysOnTopHint))
396+
, m_lines()
397+
, m_updateAction(updatePathsAction)
398+
{
399+
window()->setWindowTitle(tr("#include Search Path List"));
400+
401+
int thisWidth = parent->width(); // / 3;
402+
int thisHeight = parent->height(); // / 4;
403+
resize(thisWidth, thisHeight);
404+
setFixedSize(size());
405+
406+
class MyScrollArea : public QScrollArea {
407+
QWidget* m_parent;
408+
409+
public:
410+
explicit MyScrollArea(QWidget* parent)
411+
: QScrollArea(parent), m_parent(parent)
412+
{
413+
}
414+
415+
QSize sizeHint() const override { return m_parent->size(); }
416+
};
417+
418+
class MyFrame : public QFrame {
419+
QWidget* m_parent;
420+
421+
public:
422+
explicit MyFrame(QWidget* parent) : QFrame(parent), m_parent(parent)
423+
{
424+
}
425+
426+
QSize sizeHint() const override { return m_parent->size(); }
427+
};
428+
429+
auto scroll_area = new MyScrollArea(this);
430+
scroll_area->setWidgetResizable(true);
431+
auto frame = new MyFrame(scroll_area);
432+
auto layout = new QVBoxLayout();
433+
layout->setSpacing(0);
434+
frame->setLayout(layout);
435+
frame->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
436+
scroll_area->setWidget(frame);
437+
scroll_area->show();
438+
m_layout = layout;
439+
m_scrollArea = scroll_area;
440+
}
441+
442+
void set_path_list(const std::vector<std::string>& paths)
443+
{
444+
while (!m_lines.empty())
445+
pop_line();
446+
m_maxIndexWithContent
447+
= (int)paths.size()
448+
- 1; // ok that this is -1 if the paths are empty
449+
auto initialLineCount = required_lines();
450+
m_lines.reserve(initialLineCount);
451+
while (m_lines.size() < initialLineCount)
452+
push_line();
453+
for (size_t i = 0; i < paths.size(); ++i)
454+
m_lines[i]->setText(QString::fromStdString(paths[i]));
455+
update_path_list();
456+
}
457+
458+
void observe_changed_text()
459+
{
460+
// Only listen to signals from OSLToySearchPathLine objects
461+
if (auto changedLine = dynamic_cast<OSLToySearchPathLine*>(sender())) {
462+
bool isNowEmpty = changedLine->text().isEmpty();
463+
if (changedLine->previouslyHadContent() && isNowEmpty) {
464+
if (changedLine->getIndex() == m_maxIndexWithContent) {
465+
// Find the next max index with content, or -1 if none.
466+
do {
467+
--m_maxIndexWithContent;
468+
} while (m_maxIndexWithContent >= 0
469+
&& !m_lines[m_maxIndexWithContent]
470+
->previouslyHadContent());
471+
472+
shrink_as_needed();
473+
}
474+
} else if (!changedLine->previouslyHadContent() && !isNowEmpty) {
475+
if (changedLine->getIndex() > m_maxIndexWithContent) {
476+
m_maxIndexWithContent = changedLine->getIndex();
477+
grow_as_needed();
478+
}
479+
}
480+
481+
changedLine->setPreviouslyHadContent(!isNowEmpty);
482+
}
483+
}
484+
485+
protected:
486+
void closeEvent(QCloseEvent* ev) override
487+
{
488+
// On close, collate the list of search paths, and if there has been any change, update.
489+
bool has_updated = false;
490+
for (auto line : m_lines) {
491+
if (line->isModified()) {
492+
if (!has_updated) {
493+
update_path_list();
494+
has_updated = true;
495+
}
496+
line->setModified(false);
497+
}
498+
}
499+
500+
ev->accept();
501+
}
502+
503+
private:
504+
void push_line()
505+
{
506+
auto l = new OSLToySearchPathLine(this, (int)m_lines.size());
507+
m_layout->addWidget(l);
508+
m_lines.push_back(l);
509+
}
510+
511+
void pop_line()
512+
{
513+
auto line = m_lines.back();
514+
m_lines.pop_back();
515+
m_layout->removeWidget(line);
516+
}
517+
518+
void update_path_list()
519+
{
520+
std::vector<std::string> path_list;
521+
for (auto line : m_lines) {
522+
auto&& text = line->text();
523+
if (!text.isEmpty())
524+
path_list.push_back(text.toStdString());
525+
}
526+
m_updateAction(path_list);
527+
}
528+
529+
size_t required_lines() const
530+
{
531+
return static_cast<size_t>(
532+
(std::max)(m_minLineCount,
533+
m_maxIndexWithContent + m_guaranteedEmptyLineCount + 1));
534+
}
535+
536+
void grow_as_needed()
537+
{
538+
auto newReqLines = required_lines();
539+
while (m_lines.size() < newReqLines) {
540+
push_line();
541+
}
542+
}
543+
544+
void shrink_as_needed()
545+
{
546+
auto newReqLines = required_lines();
547+
while (m_lines.size() > newReqLines) {
548+
pop_line();
549+
}
550+
}
551+
552+
int m_minLineCount = 12;
553+
int m_guaranteedEmptyLineCount = 5;
554+
int m_maxIndexWithContent = -1;
555+
std::vector<OSLToySearchPathLine*> m_lines;
556+
QLayout* m_layout = nullptr;
557+
QScrollArea* m_scrollArea = nullptr;
558+
UpdatePathListAction m_updateAction;
559+
};
560+
561+
562+
563+
OSLToySearchPathLine::OSLToySearchPathLine(OSLToySearchPathEditor* editor,
564+
int index)
565+
: QLineEdit(), m_index(index), m_editor(editor)
566+
{
567+
setFrame(true);
568+
569+
auto p = this->palette();
570+
p.setColor(QPalette::Base, getColor(index));
571+
setPalette(p);
572+
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
573+
QObject::connect(this, &OSLToySearchPathLine::editingFinished, editor,
574+
&OSLToySearchPathEditor::observe_changed_text);
575+
576+
// Maybe add a QCompleter that completes known paths
577+
show();
578+
}
579+
580+
581+
582+
QSize
583+
OSLToySearchPathLine::sizeHint() const
584+
{
585+
return QSize(m_editor->width() - 4, 10);
586+
}
587+
588+
589+
354590
void
355591
#if OSL_QT_MAJOR < 6
356592
Magnifier::enterEvent(QEvent* event)
@@ -402,9 +638,6 @@ OSLToyMainWindow::OSLToyMainWindow(OSLToyRenderer* rend, int xr, int yr)
402638

403639
setWindowTitle(tr("OSL Toy"));
404640

405-
// Set size of the window
406-
// setFixedSize(100, 50);
407-
408641
createActions();
409642
createMenus();
410643
createStatusBar();
@@ -458,6 +691,11 @@ OSLToyMainWindow::OSLToyMainWindow(OSLToyRenderer* rend, int xr, int yr)
458691
&OSLToyMainWindow::restart_time);
459692
control_area_layout->addWidget(restartButton);
460693

694+
searchPathEditor
695+
= new OSLToySearchPathEditor(this, [this](auto&& paths) mutable {
696+
update_include_search_paths(paths);
697+
});
698+
461699
auto editorarea = new QWidget;
462700
QFontMetrics fontmetrics(CodeEditor::fixedFont());
463701
#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
@@ -517,6 +755,9 @@ OSLToyMainWindow::createActions()
517755
&OSLToyMainWindow::recompile_shaders);
518756
add_action("Enter Full Screen", "", "",
519757
&OSLToyMainWindow::action_fullscreen);
758+
add_action("search-path-popup", "Edit #include search paths...",
759+
"Shift-Ctrl+P",
760+
&OSLToyMainWindow::action_open_search_path_popup);
520761
}
521762

522763

@@ -553,6 +794,7 @@ OSLToyMainWindow::createMenus()
553794

554795
toolsMenu = new QMenu(tr("&Tools"), this);
555796
toolsMenu->addAction(actions["Recompile shaders"]);
797+
toolsMenu->addAction(actions["search-path-popup"]);
556798
menuBar()->addMenu(toolsMenu);
557799

558800
helpMenu = new QMenu(tr("&Help"), this);
@@ -711,6 +953,23 @@ OSLToyMainWindow::open_file(const std::string& filename)
711953
}
712954

713955

956+
void
957+
OSLToyMainWindow::set_include_search_paths(const std::vector<std::string>& paths)
958+
{
959+
searchPathEditor->set_path_list(paths);
960+
}
961+
962+
void
963+
OSLToyMainWindow::update_include_search_paths(
964+
const std::vector<std::string>& paths)
965+
{
966+
m_include_search_paths = paths;
967+
m_should_regenerate_compile_options = true;
968+
969+
// Open question: Do we want to force a recompile whenever the list is updated?
970+
// For now, I'm defaulting to no, but this is just a guess.
971+
}
972+
714973

715974
void
716975
OSLToyMainWindow::action_saveas()
@@ -758,6 +1017,17 @@ OSLToyMainWindow::action_save()
7581017

7591018

7601019

1020+
void
1021+
OSLToyMainWindow::action_open_search_path_popup()
1022+
{
1023+
auto centeredXPos = x() + (width() - searchPathEditor->width()) / 2;
1024+
auto centeredYPos = y() + (height() - searchPathEditor->height()) / 2;
1025+
searchPathEditor->move(centeredXPos, centeredYPos);
1026+
searchPathEditor->show();
1027+
}
1028+
1029+
1030+
7611031
// Separate thread pool just for the async render kickoff triggers, but use
7621032
// the default pool for the workers.
7631033
static OIIO::thread_pool trigger_pool;
@@ -836,6 +1106,24 @@ class MyOSLCErrorHandler final : public OIIO::ErrorHandler {
8361106
};
8371107

8381108

1109+
void
1110+
OSLToyMainWindow::regenerate_compile_options()
1111+
{
1112+
// Right now, the only option we consider is include search path (-I)
1113+
1114+
// Annoyingly, oslcomp only supports -I flags without any seperator between
1115+
// the -I and the path itself, but OIIO::ArgParse does not support parsing
1116+
// arguments in this manner. Oy vey.
1117+
1118+
m_compile_options.clear();
1119+
1120+
for (auto&& path : m_include_search_paths)
1121+
m_compile_options.push_back(std::string("-I").append(path));
1122+
1123+
1124+
m_should_regenerate_compile_options = false;
1125+
}
1126+
8391127

8401128
void
8411129
OSLToyMainWindow::recompile_shaders()
@@ -863,11 +1151,19 @@ OSLToyMainWindow::recompile_shaders()
8631151
MyOSLCErrorHandler errhandler(this);
8641152
OSLCompiler oslcomp(&errhandler);
8651153
std::string osooutput;
866-
std::vector<std::string> options;
867-
ok = oslcomp.compile_buffer(source, osooutput, options, "",
868-
briefname);
869-
set_error_message(tab,
870-
OIIO::Strutil::join(errhandler.errors, "\n"));
1154+
1155+
if (m_should_regenerate_compile_options)
1156+
regenerate_compile_options();
1157+
1158+
ok = oslcomp.compile_buffer(source, osooutput, m_compile_options,
1159+
"", briefname);
1160+
1161+
auto error_message = OIIO::Strutil::fmt::format(
1162+
"{}\n\nCompiled {} with options: {}",
1163+
OIIO::Strutil::join(errhandler.errors, "\n"), briefname,
1164+
OIIO::Strutil::join(m_compile_options, " "));
1165+
set_error_message(tab, error_message);
1166+
8711167
if (ok) {
8721168
// std::cout << osooutput << "\n";
8731169
ok = shadingsys()->LoadMemoryCompiledShader(briefname,

0 commit comments

Comments
 (0)