diff --git a/src/osltoy/osltoyapp.cpp b/src/osltoy/osltoyapp.cpp index 3e6a59d08..70cca5b8c 100644 --- a/src/osltoy/osltoyapp.cpp +++ b/src/osltoy/osltoyapp.cpp @@ -32,6 +32,7 @@ #include #include #include +#include // QT's extension foreach defines a foreach macro which interferes // with an OSL internal foreach method. So we will undefine it here @@ -351,6 +352,276 @@ class OSLToyRenderView final : public QLabel { #endif }; +class OSLToySearchPathLine final : public QLineEdit { + // Q_OBJECT +public: + explicit OSLToySearchPathLine(OSLToySearchPathEditor* editor, int index); + + + bool previouslyHadContent() const { return m_previouslyHadContent; } + + void setPreviouslyHadContent(bool value) { m_previouslyHadContent = value; } + + int getIndex() const { return m_index; } + + QSize sizeHint() const override; + +private: + static QColor getColor(int index) + { + if (index % 2) + return QColor(0xFFE0F0FF); // light blue + else + return Qt::white; + } + + + bool m_previouslyHadContent = false; + int m_index; + OSLToySearchPathEditor* m_editor = nullptr; +}; + + +// More generically, this is a popup window with a list (that grows as needed) of editable text items. +class OSLToySearchPathEditor final : public QWidget { + // Q_OBJECT + + using UpdatePathListAction + = std::function&)>; + +public: + OSLToySearchPathEditor(QWidget* parent, + UpdatePathListAction updatePathsAction) + : QWidget(parent, static_cast( + Qt::Tool | Qt::WindowStaysOnTopHint)) + , m_lines() + , m_updateAction(updatePathsAction) + { + window()->setWindowTitle(tr("#include Search Path List")); + + + int thisWidth = parent->width(); // / 3; + int thisHeight = parent->height(); // / 4; + + resize(thisWidth, thisHeight); + + setFixedSize(size()); + + class MyScrollArea : public QScrollArea { + QWidget* m_parent; + + public: + explicit MyScrollArea(QWidget* parent) + : QScrollArea(parent), m_parent(parent) + { + } + + QSize sizeHint() const override { return m_parent->size(); } + }; + + class MyFrame : public QFrame { + QWidget* m_parent; + + public: + explicit MyFrame(QWidget* parent) : QFrame(parent), m_parent(parent) + { + } + + QSize sizeHint() const override { return m_parent->size(); } + }; + + auto scroll_area = new MyScrollArea(this); + scroll_area->setWidgetResizable(true); + + auto frame = new MyFrame(scroll_area); + // frame->setWidgetResizable(true); + + auto layout = new QVBoxLayout(); + layout->setSpacing(0); + // int topMargin = thisWidth / 6; + // layout->setContentsMargins(topMargin / 2, topMargin, topMargin / 2, topMargin / 2); + + frame->setLayout(layout); + + frame->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + scroll_area->setWidget(frame); + + m_layout = layout; + + scroll_area->show(); + m_scrollArea = scroll_area; + } + + + void set_path_list(const std::vector& paths) + { + while (!m_lines.empty()) + pop_line(); + + + m_maxIndexWithContent + = (int)paths.size() + - 1; // ok that this is -1 if the paths are empty + + + + auto initialLineCount = required_lines(); + + m_lines.reserve(initialLineCount); + + while (m_lines.size() < initialLineCount) { + push_line(); + } + + for (size_t i = 0; i < paths.size(); ++i) { + m_lines[i]->setText(QString::fromStdString(paths[i])); + } + + update_path_list(); + } + + + void observe_changed_text() + { + // Only listen to signals from OSLToySearchPathLine objects + if (auto changedLine = dynamic_cast(sender())) { + bool isNowEmpty = changedLine->text().isEmpty(); + + if (changedLine->previouslyHadContent() && isNowEmpty) { + if (changedLine->getIndex() == m_maxIndexWithContent) { + // Find the next max index with content, or -1 if none. + do { + --m_maxIndexWithContent; + } while (m_maxIndexWithContent >= 0 + && !m_lines[m_maxIndexWithContent] + ->previouslyHadContent()); + + shrink_as_needed(); + } + } else if (!changedLine->previouslyHadContent() && !isNowEmpty) { + if (changedLine->getIndex() > m_maxIndexWithContent) { + m_maxIndexWithContent = changedLine->getIndex(); + grow_as_needed(); + } + } + + changedLine->setPreviouslyHadContent(!isNowEmpty); + } + } + +protected: + void closeEvent(QCloseEvent* ev) override + { + // On close, collate the list of search paths, and if there has been any change, update. + bool has_updated = false; + + for (auto line : m_lines) { + if (line->isModified()) { + if (!has_updated) { + update_path_list(); + has_updated = true; + } + line->setModified(false); + } + } + + ev->accept(); + } + + +private: + void push_line() + { + auto l = new OSLToySearchPathLine(this, (int)m_lines.size()); + + m_layout->addWidget(l); + m_lines.push_back(l); + } + + + void pop_line() + { + auto line = m_lines.back(); + m_lines.pop_back(); + m_layout->removeWidget(line); + } + + void update_path_list() + { + std::vector path_list; + for (auto line : m_lines) { + auto&& text = line->text(); + if (!text.isEmpty()) + path_list.push_back(text.toStdString()); + } + m_updateAction(path_list); + } + + size_t required_lines() const + { + return static_cast( + (std::max)(m_minLineCount, + m_maxIndexWithContent + m_guaranteedEmptyLineCount + 1)); + } + + void grow_as_needed() + { + auto newReqLines = required_lines(); + + while (m_lines.size() < newReqLines) { + push_line(); + } + } + + void shrink_as_needed() + { + auto newReqLines = required_lines(); + + while (m_lines.size() > newReqLines) { + pop_line(); + } + } + + + + int m_minLineCount = 12; + int m_guaranteedEmptyLineCount = 5; + int m_maxIndexWithContent = -1; + std::vector m_lines; + QLayout* m_layout = nullptr; + QScrollArea* m_scrollArea = nullptr; + UpdatePathListAction m_updateAction; +}; + + + +OSLToySearchPathLine::OSLToySearchPathLine(OSLToySearchPathEditor* editor, + int index) + : QLineEdit(), m_index(index), m_editor(editor) +{ + setFrame(true); + + auto p = this->palette(); + p.setColor(QPalette::Base, getColor(index)); + setPalette(p); + + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + + QObject::connect(this, &OSLToySearchPathLine::editingFinished, editor, + &OSLToySearchPathEditor::observe_changed_text); + + // Maybe add a QCompleter that completes known paths + // setCompletion + show(); +} + +QSize +OSLToySearchPathLine::sizeHint() const +{ + return QSize(m_editor->width() - 4, 10); +} + void #if OSL_QT_MAJOR < 6 Magnifier::enterEvent(QEvent* event) @@ -458,6 +729,11 @@ OSLToyMainWindow::OSLToyMainWindow(OSLToyRenderer* rend, int xr, int yr) &OSLToyMainWindow::restart_time); control_area_layout->addWidget(restartButton); + searchPathEditor + = new OSLToySearchPathEditor(this, [this](auto&& paths) mutable { + update_include_search_paths(paths); + }); + auto editorarea = new QWidget; QFontMetrics fontmetrics(CodeEditor::fixedFont()); #if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0) @@ -517,6 +793,11 @@ OSLToyMainWindow::createActions() &OSLToyMainWindow::recompile_shaders); add_action("Enter Full Screen", "", "", &OSLToyMainWindow::action_fullscreen); + + + add_action("search-path-popup", "Edit #include search paths...", + "Shift-Ctrl+P", + &OSLToyMainWindow::action_open_search_path_popup); } @@ -553,6 +834,7 @@ OSLToyMainWindow::createMenus() toolsMenu = new QMenu(tr("&Tools"), this); toolsMenu->addAction(actions["Recompile shaders"]); + toolsMenu->addAction(actions["search-path-popup"]); menuBar()->addMenu(toolsMenu); helpMenu = new QMenu(tr("&Help"), this); @@ -711,6 +993,23 @@ OSLToyMainWindow::open_file(const std::string& filename) } +void +OSLToyMainWindow::set_include_search_paths(const std::vector& paths) +{ + searchPathEditor->set_path_list(paths); +} + +void +OSLToyMainWindow::update_include_search_paths( + const std::vector& paths) +{ + m_include_search_paths = paths; + m_should_regenerate_compile_options = true; + + // Open question: Do we want to force a recompile whenever the list is updated? + // For now, I'm defaulting to no, but this is just a guess. +} + void OSLToyMainWindow::action_saveas() @@ -758,6 +1057,17 @@ OSLToyMainWindow::action_save() +void +OSLToyMainWindow::action_open_search_path_popup() +{ + auto centeredXPos = x() + (width() - searchPathEditor->width()) / 2; + auto centeredYPos = y() + (height() - searchPathEditor->height()) / 2; + searchPathEditor->move(centeredXPos, centeredYPos); + searchPathEditor->show(); +} + + + // Separate thread pool just for the async render kickoff triggers, but use // the default pool for the workers. static OIIO::thread_pool trigger_pool; @@ -836,6 +1146,24 @@ class MyOSLCErrorHandler final : public OIIO::ErrorHandler { }; +void +OSLToyMainWindow::regenerate_compile_options() +{ + // Right now, the only option we consider is include search path (-I) + + // Annoyingly, oslcomp only supports -I flags without any seperator between + // the -I and the path itself, but OIIO::ArgParse does not support parsing + // arguments in this manner. Oy vey. + + m_compile_options.clear(); + + for (auto&& path : m_include_search_paths) + m_compile_options.push_back(std::string("-I").append(path)); + + + m_should_regenerate_compile_options = false; +} + void OSLToyMainWindow::recompile_shaders() @@ -863,11 +1191,19 @@ OSLToyMainWindow::recompile_shaders() MyOSLCErrorHandler errhandler(this); OSLCompiler oslcomp(&errhandler); std::string osooutput; - std::vector options; - ok = oslcomp.compile_buffer(source, osooutput, options, "", - briefname); - set_error_message(tab, - OIIO::Strutil::join(errhandler.errors, "\n")); + + if (m_should_regenerate_compile_options) + regenerate_compile_options(); + + ok = oslcomp.compile_buffer(source, osooutput, m_compile_options, + "", briefname); + + auto error_message = OIIO::Strutil::fmt::format( + "{}\n\nCompiled {} with options: {}", + OIIO::Strutil::join(errhandler.errors, "\n"), briefname, + OIIO::Strutil::join(m_compile_options, " ")); + set_error_message(tab, error_message); + if (ok) { // std::cout << osooutput << "\n"; ok = shadingsys()->LoadMemoryCompiledShader(briefname, diff --git a/src/osltoy/osltoyapp.h b/src/osltoy/osltoyapp.h index 3d4055ee8..14a0d3f40 100644 --- a/src/osltoy/osltoyapp.h +++ b/src/osltoy/osltoyapp.h @@ -57,6 +57,7 @@ class ParamRec final : public OSLQuery::Parameter { }; class OSLToyRenderView; +class OSLToySearchPathEditor; class OSLToyMainWindow final : public QMainWindow { Q_OBJECT @@ -91,6 +92,9 @@ class OSLToyMainWindow final : public QMainWindow { bool open_file(const std::string& filename); + void set_include_search_paths(const std::vector& paths); + void update_include_search_paths(const std::vector& paths); + void rerender_needed() { m_rerender_needed = 1; } private slots: @@ -102,11 +106,12 @@ private slots: // Non-owning pointers to all the widgets we create. Qt is responsible // for deleting. QSplitter* centralSplitter; - OSLToyRenderView* renderView = nullptr; - QTabWidget* textTabs = nullptr; - QScrollArea* paramScroll = nullptr; - QWidget* paramWidget = nullptr; - QGridLayout* paramLayout = nullptr; + OSLToyRenderView* renderView = nullptr; + OSLToySearchPathEditor* searchPathEditor = nullptr; + QTabWidget* textTabs = nullptr; + QScrollArea* paramScroll = nullptr; + QWidget* paramWidget = nullptr; + QGridLayout* paramLayout = nullptr; QLabel* statusFPS; QMenu *fileMenu, *editMenu, *viewMenu, *toolsMenu, *helpMenu; QPushButton *recompileButton, *pauseButton, *restartButton; @@ -165,6 +170,8 @@ private slots: void action_fullscreen() {} void action_about() {} + void action_open_search_path_popup(); + void set_ui_to_paramval(ParamRec* param); void reset_param_to_default(ParamRec* param); void set_param_instance_value(ParamRec* param); @@ -183,6 +190,8 @@ private slots: void make_param_adjustment_row(ParamRec* param, QGridLayout* layout, int row); + void regenerate_compile_options(); + void rebuild_param_area(); void inventory_params(); OIIO::ImageBuf& framebuffer(); @@ -197,6 +206,13 @@ private slots: std::string m_groupname; bool m_shader_uses_time = false; + std::vector m_include_search_paths; + + + bool m_should_regenerate_compile_options = true; + std::vector m_compile_options; + + // Access control mutex for handing things off between the GUI thread // and the shading thread. OIIO::spin_mutex m_job_mutex; diff --git a/src/osltoy/osltoymain.cpp b/src/osltoy/osltoymain.cpp index 9b926e356..661d51171 100644 --- a/src/osltoy/osltoymain.cpp +++ b/src/osltoy/osltoymain.cpp @@ -39,6 +39,7 @@ static bool foreground_mode = true; static int threads = 0; static int xres = 512, yres = 512; static std::vector filenames; +static std::vector include_paths; static void @@ -58,6 +59,9 @@ getargs(int argc, char* argv[]) .help("Set thread count (0=cores)"); ap.arg("--res %d:XRES %d:YRES", &xres, &yres) .help("Set resolution"); + ap.arg("-I DIRPATH") + .action([&](cspan argv){ include_paths.emplace_back(argv[1]); }) + .help("Add DIRPATH to the list of header search paths."); // clang-format on if (ap.parse(argc, (const char**)argv) < 0) { std::cerr << ap.geterror() << std::endl; @@ -94,6 +98,9 @@ main(int argc, char* argv[]) QApplication app(argc, argv); OSLToyMainWindow mainwin(rend, xres, yres); mainwin.show(); + + mainwin.set_include_search_paths(include_paths); + for (auto&& filename : filenames) mainwin.open_file(filename);