#include "Debug.h" #include "Engine.h" #include "Utility.h" #include "UserInterface.h" // class View : public irr::IEventReceiver; // avoid incomplete type when accessing member of this type from friend class #include "View.h" // avoid incomplete type when accessing member of this type from friend class #include #include #include // NOTE: to use filesystem, you must also include the fs library such // as via the `-lstdc++fs` linker option -- see b3view.pro // #include // requires C++17 #include // requires C++14 such as gcc 8.2.1 using namespace irr; using namespace irr::core; using namespace irr::gui; using std::string; using std::wstring; using namespace std; // C++14: namespace filesystem = std::experimental::filesystem; // namespace fs = std::filesystem; // doesn't work (not a namespace in gcc's C++17) // using namespace std::filesystem; // doesn't work (not a namespace in gcc's C++17) namespace fs = std::experimental::filesystem; // namespace fs = std::filesystem; // doesn't work (not a namespace in gcc's C++17) const u32 UserInterface::UIC_FILE_RECENT_FIRST = UIE_RECENTMENU + 1; // PRIVATE void UserInterface::setupUserInterface() { this->recent_initialized = false; this->recentMenu = nullptr; bool enableVerbose = false; if (this->m_Engine != nullptr) { enableVerbose = this->m_Engine->m_EnableVerbose; } else { std::cerr << "Error: The engine is not ready in setupUserInterface." << std::endl; } // Menu menu = m_Gui->addMenu(); this->fileMenuIdx = menu->addItem(L"File", UIE_FILEMENU, true, true); this->playbackMenuIdx = menu->addItem(L"Playback", UIE_PLAYBACKMENU, true, true); this->viewMenuIdx = menu->addItem(L"View", UIE_VIEWMENU, true, true); // File Menu fileMenu = menu->getSubMenu(this->fileMenuIdx); fileMenu->addItem(L"Open", UIC_FILE_OPEN); this->fileRecentIdx = fileMenu->addItem(L"Open Recent", UIC_FILE_RECENT, true, true); std::vector recentPaths = this->m_Engine->recentPaths(); fileMenu->addItem(L"Reload Model F5", UIC_FILE_RELOAD_MESH); fileMenu->addItem(L"Reload Texture Shift F5", UIC_FILE_RELOAD_TEXTURE); fileMenu->addItem(L"Change Texture", UIC_FILE_OPEN_TEXTURE); fileMenu->addItem(L"Previous Texture Shift F3", UIC_FILE_PREVIOUS_TEXTURE); fileMenu->addItem(L"Next Texture F3", UIC_FILE_NEXT_TEXTURE); fileMenu->addItem(L"Export DAE (non-Blender COLLADA)", UIC_FILE_EXPORT_DAE); fileMenu->addItem(L"Export IRR (Irrlicht Scene settings and mesh paths only)", UIC_FILE_EXPORT_IRR); fileMenu->addItem(L"Export IRRMESH (Static Irrlicht Mesh)", UIC_FILE_EXPORT_IRRMESH); fileMenu->addItem(L"Export OBJ (Wavefront)", UIC_FILE_EXPORT_OBJ); fileMenu->addItem(L"Export STL (stereolithography)", UIC_FILE_EXPORT_STL); fileMenu->addItem(L"Quit", UIC_FILE_QUIT); // File, Open Recent submenu this->recentMenu = fileMenu->getSubMenu(this->fileRecentIdx); if (enableVerbose) { std::cerr << "+this->recentMenu text:\"" << Utility::toString((wstring)this->recentMenu->getText()) << "\"" << " idx:" << Utility::toString((int)this->fileRecentIdx) << " id:" << Utility::toString((int)this->recentMenu->getID()) << std::endl; } this->fileRecentClearIdx = this->recentMenu->addItem(L"Clear Recent", UIC_FILE_RECENT_CLEAR); if (enableVerbose) { std::cerr << "+this->fileRecentClearIdx: " << this->fileRecentClearIdx << std::endl; } this->uic_file_recent_next = UserInterface::UIC_FILE_RECENT_FIRST; this->m_file_recent_first_idx = -1; this->m_file_recent_last_idx = -1; this->recent_initialized = true; this->addRecentMenuItems(recentPaths, false); // Playback Menu playbackMenu = menu->getSubMenu(this->playbackMenuIdx); playbackMenu->addItem(L"Previous Frame Left", UIC_PLAYBACK_PREVIOUS, true, false, false, false); playbackMenu->addItem(L"Next Frame Right", UIC_PLAYBACK_NEXT, true, false, false, false); playbackMenu->addItem(L"Slower Ctrl Left", UIC_PLAYBACK_SLOWER, true, false, false, false); playbackMenu->addItem(L"Faster Ctrl Right", UIC_PLAYBACK_FASTER, true, false, false, false); // View Menu viewMenu = menu->getSubMenu(this->viewMenuIdx); viewWireframeIdx = viewMenu->addItem(L"Wireframe", UIC_VIEW_WIREFRAME, true, false, this->m_Engine->getEnableWireframe(), true); viewLightingIdx = viewMenu->addItem(L"Lighting", UIC_VIEW_LIGHTING, true, false, this->m_Engine->getEnableLighting(), true); viewAxisWidgetIdx = viewMenu->addItem(L"Origin Axis Widget", UIC_VIEW_AXIS_WIDGET, true, false, true, true); viewTargetIdx = viewMenu->addItem(L"Camera Target", UIC_VIEW_TARGET, true, false, false, true); viewTextureInterpolationIdx = viewMenu->addItem(L"Texture Interpolation Ctrl i", UIC_VIEW_TEXTURE_INTERPOLATION, true, false, this->m_Engine->getEnableTextureInterpolation(), true); viewYUpIdx = viewMenu->addItem(L"Y Up", UIC_VIEW_Y_UP, true, false, true, true); viewZUpIdx = viewMenu->addItem(L"Z Up", UIC_VIEW_Z_UP, true, false, false, true); // Playback Control Window dimension2d windowSize = m_Engine->m_Driver->getScreenSize(); playbackWindow = m_Gui->addWindow( rect(vector2d(windowSize.Width - 4 - 160, 28), dimension2d(160, 500)), false, L"Playback", nullptr, UIE_PLAYBACKWINDOW ); playbackWindow->getCloseButton()->setVisible(false); s32 spacing_x = 4; s32 margin_y = 4; spacing_y = 4; s32 size_x = playbackWindow->getClientRect().getWidth() - 8; s32 size_y = 24; s32 y = 24; playbackStartFrameStaticText = m_Gui->addStaticText( L"Start Frame:", rect(vector2d(spacing_x, y), dimension2d(size_x, size_y)), true, true, playbackWindow, UIE_PLAYBACKSTARTFRAMESTATICTEXT, false); y += size_y; playbackStartFrameEditBox = m_Gui->addEditBox( L"", rect(vector2d(spacing_x, y), dimension2d(size_x, size_y)), true, playbackWindow, UIE_PLAYBACKSTARTFRAMEEDITBOX); y += size_y + spacing_y; playbackEndFrameStaticText = m_Gui->addStaticText( L"End Frame:", rect(vector2d(spacing_x, y), dimension2d(size_x, size_y)), true, true, playbackWindow, UIE_PLAYBACKENDFRAMESTATICTEXT, false); y += size_y; playbackEndFrameEditBox = m_Gui->addEditBox( L"", rect(vector2d(spacing_x, y), dimension2d(size_x, size_y)), true, playbackWindow, UIE_PLAYBACKENDFRAMEEDITBOX); y += size_y + spacing_y; playbackStartStopButton = m_Gui->addButton( rect(vector2d(spacing_x, y), dimension2d(size_x, size_y)), playbackWindow, UIE_PLAYBACKSTARTSTOPBUTTON, L"Start/Stop", nullptr); y += size_y + spacing_y; playbackSetFrameEditBox = m_Gui->addEditBox( L"", rect(vector2d(spacing_x, y), dimension2d(size_x, size_y)), true, playbackWindow, UIE_PLAYBACKSETFRAMEEDITBOX); y += margin_y; y += size_y + spacing_y; playbackIncreaseButton = m_Gui->addButton( rect(vector2d(spacing_x, y), dimension2d(size_x, size_y)), playbackWindow, UIE_PLAYBACKINCREASEBUTTON, L"Faster", nullptr); y += size_y + spacing_y; playbackDecreaseButton = m_Gui->addButton( rect(vector2d(spacing_x, y), dimension2d(size_x, size_y)), playbackWindow, UIE_PLAYBACKDECREASEBUTTON, L"Slower", nullptr); y += size_y + spacing_y; playbackFPSEditBox = m_Gui->addEditBox( L"", rect(vector2d(spacing_x, y), dimension2d(size_x, size_y)), true, playbackWindow, UIE_FPSEDITBOX); y += margin_y; y += size_y + spacing_y; texturePathStaticText = m_Gui->addStaticText( L"Texture Path:", rect(vector2d(spacing_x, y), dimension2d(size_x, size_y)), true, true, playbackWindow, UIE_TEXTUREPATHSTATICTEXT, false); y += size_y + spacing_y; texturePathEditBox = m_Gui->addEditBox( L"", rect(vector2d(spacing_x, y), dimension2d(size_x, size_y)), true, playbackWindow, UIE_TEXTUREPATHEDITBOX); texturePathEditBox ->setTextAlignment(EGUIA_LOWERRIGHT, EGUIA_UPPERLEFT); y += margin_y; y += size_y + spacing_y; axisSizeStaticText = m_Gui->addStaticText( L"Axis Size:", rect(vector2d(spacing_x, y), dimension2d(size_x, size_y)), true, true, playbackWindow, UIE_AXISSIZESTATICTEXT, false); y += size_y + spacing_y; axisSizeEditBox = m_Gui->addEditBox( std::to_wstring(this->m_Engine->m_AxisLength).c_str(), rect(vector2d(spacing_x, y), dimension2d(size_x, size_y)), true, playbackWindow, UIE_AXISSIZEEDITBOX); y += margin_y; y += size_y + spacing_y; // Set Font for UI Elements m_GuiFontFace = new CGUITTFace(); // irrString defines stringc as string if (!Utility::isFile(m_Engine->m_FontPath)) { m_Engine->m_FontPath = L"C:\\Windows\\Fonts\\calibrib.ttf"; } if (!Utility::isFile(m_Engine->m_FontPath)) { m_Engine->m_FontPath = L"C:\\Windows\\Fonts\\arialbd.ttf"; } if (!Utility::isFile(m_Engine->m_FontPath)) { m_Engine->m_FontPath = L"/usr/share/fonts/liberation/LiberationSans-Bold.ttf"; } if (!Utility::isFile(m_Engine->m_FontPath)) { m_Engine->m_FontPath = L"/usr/share/fonts/gnu-free/FreeSansBold.ttf"; } if (!Utility::isFile(m_Engine->m_FontPath)) { m_Engine->m_FontPath = L"/usr/share/fonts/dejavu/DejaVuSans-Bold.ttf"; } if (!Utility::isFile(m_Engine->m_FontPath)) { m_Engine->m_FontPath = L"/usr/share/fonts/google-droid/DroidSans-Bold.ttf"; } if (!Utility::isFile(m_Engine->m_FontPath)) { m_Engine->m_FontPath = L"/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf"; // ^ This is present on Debian 10. No previous fonts further up are. } // if (!Utility::isFile(m_Engine->m_FontPath)) { // m_Engine->m_FontPath = L"/usr/share/fonts-droid-fallback/truetype/DroidSansFallback.ttf"; // /usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf // ^ These are CJK fonts! // } if (m_GuiFontFace->load(m_Engine->m_FontPath.c_str())) { // actually takes `const io::path &` m_GuiFont = new CGUITTFont(m_Gui); m_GuiFont->attach(m_GuiFontFace, 14); m_Gui->getSkin()->setFont(m_GuiFont); } else { if (enableVerbose) { std::wcerr << L"WARNING: '" << m_Engine->m_FontPath << L"' is missing." << endl; } delete m_GuiFontFace; m_GuiFontFace = nullptr; if (m_GuiFont != nullptr) { if (enableVerbose) { std::wcerr << L" - The old font will remain loaded." << endl; } } } // } } void UserInterface::displayLoadFileDialog() { m_Gui->addFileOpenDialog(L"Select a file to load", true, nullptr, UIE_LOADFILEDIALOG); } void UserInterface::displaySaveFileDialog() { m_Gui->addFileOpenDialog(L"Select where to save export.dae", true, nullptr, UIE_SAVEFILEDIALOG); // NOTE: if restoreCWD is false (default), cwd changes. } void UserInterface::displayLoadTextureDialog() { m_Gui->addFileOpenDialog(L"Select a file to load", true, nullptr, UIE_LOADTEXTUREDIALOG); } void UserInterface::incrementFrame(f32 frameCount, bool enableRound) { if (this->m_Engine->m_LoadedMesh != nullptr) { if (this->m_Engine->m_IsPlaying) this->m_Engine->toggleAnimation(); this->m_Engine->m_LoadedMesh->setCurrentFrame( enableRound ? (round(this->m_Engine->m_LoadedMesh->getFrameNr()) + frameCount) : (round(this->m_Engine->m_LoadedMesh->getFrameNr()) + frameCount) ); this->playbackSetFrameEditBox->setText( Utility::toWstring( this->m_Engine->m_LoadedMesh->getFrameNr() ).c_str() ); } } bool UserInterface::handleMenuItemPressed(const SEvent::SGUIEvent* ge) { // ^ formerly ...(IGUIContextMenu* menu): called as ...(static_cast(ge->Caller)); protytyped as ...(irr::gui::IGUIContextMenu* menu) bool handled = true; IGUIContextMenu* menu = static_cast(ge->Caller); s32 callerID = ge->Caller->getID(); s32 selected = menu->getSelectedItem(); s32 commandID = menu->getItemCommandId(static_cast(selected)); bool enableVerbose = this->m_Engine->m_EnableVerbose; switch (callerID) { case UIE_RECENTMENU: // if ((ge->Caller->getID() >= this->m_file_recent_first_idx) // && (ge->Caller->getID() <= m_file_recent_last_idx)) { // NOTE: ge->Caller->getID() is probably UIE_RECENTMENU now, but that is not to be used directly! if (enableVerbose) { cerr << "selected " << selected << std::endl; } if (std::find(this->recentIndices.begin(), this->recentIndices.end(), commandID) != this->recentIndices.end()) { // ge->Caller->getText() // Don't do this. Caller is the parent! if (enableVerbose) { cerr << "parent callerID: " << callerID << endl; // ^ callerID is the parent such as 1100 (or whatever UI_RECENTMENU is) cerr << " commandID: " << commandID << std::endl; // ^ commandID is a menu id specified on creation // such as starting from 1101 // or from whatever UIC_FILE_RECENT_FIRST is--usually UI_RECENTMENU+1). // selectedItemID is a sequential number. } // std::wstring menuItemText = menu->getItemText(selected); this->openRecent(commandID, selected); } else { cerr << "Unknown commandID: " << commandID << " Text:" << Utility::toString(menu->getItemText(selected)) << endl; // ^ getItemText takes the index (NOT the commandID specified on creation) if (this->recentIndices.size() < 1) { cerr << "- recentIndices.size(): " << recentIndices.size() << endl; } else { if (enableVerbose) { cerr << " recentIndices: " << recentIndices.size() << endl; // range based for loop requires C++11 or higher: for(irr::u32 i : this->recentIndices) { cerr << " - " << i << endl; } } } handled = false; } // cerr << "[UserInterface::handleMenuItemPressed] Unknown caller id: " << callerID << endl; break; default: //if (selected > -1) { cerr << "Some other menu was used: " << callerID << " Text:" << Utility::toString(menu->getItemText(selected)) << std::endl; cerr << " - checking command id..." << std::endl; switch (commandID) { case UIC_FILE_OPEN: displayLoadFileDialog(); break; case UIC_FILE_RECENT_CLEAR: clearRecent(); break; case UIC_FILE_EXPORT_DAE: exportMeshToHome("dae"); break; case UIC_FILE_EXPORT_IRR: exportMeshToHome("irr"); break; case UIC_FILE_EXPORT_IRRMESH: exportMeshToHome("irrmesh"); break; case UIC_FILE_EXPORT_OBJ: exportMeshToHome("obj"); break; case UIC_FILE_EXPORT_STL: exportMeshToHome("stl"); break; case UIC_FILE_RELOAD_MESH: m_Engine->reloadMesh(); break; case UIC_FILE_RELOAD_TEXTURE: m_Engine->reloadTexture(); break; case UIC_FILE_OPEN_TEXTURE: if (m_Engine->m_LoadedMesh != nullptr) { displayLoadTextureDialog(); } else { this->m_Engine->m_Device->getGUIEnvironment()->addMessageBox( L"Change Texture", L"You must load a model before a texture."); } break; case UIC_FILE_PREVIOUS_TEXTURE: if (m_Engine->m_LoadedMesh != nullptr) { loadNextTexture(-1); } else { this->m_Engine->m_Device->getGUIEnvironment()->addMessageBox( L"Change Texture", L"You must load a model before a texture."); } break; case UIC_FILE_NEXT_TEXTURE: if (m_Engine->m_LoadedMesh != nullptr) { loadNextTexture(1); } else { this->m_Engine->m_Device->getGUIEnvironment()->addMessageBox( L"Change Texture", L"You must load a model before a texture."); } break; case UIC_FILE_QUIT: m_Engine->m_RunEngine = false; break; case UIC_PLAYBACK_PREVIOUS: this->incrementFrame(-1.0f, true); break; case UIC_PLAYBACK_NEXT: this->incrementFrame(1.0f, true); break; case UIC_PLAYBACK_SLOWER: //if (ge->EventType == EGET_BUTTON_CLICKED) { this->m_Engine->incrementAnimationFPS(-5); //} break; case UIC_PLAYBACK_FASTER: //if (ge->EventType == EGET_BUTTON_CLICKED) { this->m_Engine->incrementAnimationFPS(5); //} break; case UIC_VIEW_TARGET: // break; case UIC_VIEW_Y_UP: m_Engine->setZUp(false); viewMenu->setItemChecked(viewZUpIdx, false); break; case UIC_VIEW_Z_UP: m_Engine->setZUp(true); viewMenu->setItemChecked(viewYUpIdx, false); break; case UIC_VIEW_WIREFRAME: m_Engine->setEnableWireframe( !m_Engine->getEnableWireframe() ); viewMenu->setItemChecked( viewWireframeIdx, m_Engine->getEnableWireframe() ); break; case UIC_VIEW_LIGHTING: m_Engine->setEnableLighting( !m_Engine->getEnableLighting() ); viewMenu->setItemChecked( viewLightingIdx, m_Engine->getEnableLighting() ); break; case UIC_VIEW_TEXTURE_INTERPOLATION: m_Engine->setEnableTextureInterpolation( !m_Engine->getEnableTextureInterpolation() ); viewMenu->setItemChecked( viewTextureInterpolationIdx, m_Engine->getEnableTextureInterpolation() ); break; default: cerr << "Unknown command id: " << commandID << " Text:" << Utility::toString(menu->getItemText(selected)) << endl; break; } break; } return handled; } void UserInterface::updateSettingsDisplay() { viewMenu->setItemChecked( viewWireframeIdx, m_Engine->getEnableWireframe() ); viewMenu->setItemChecked( viewLightingIdx, m_Engine->getEnableLighting() ); viewMenu->setItemChecked( viewTextureInterpolationIdx, m_Engine->getEnableTextureInterpolation() ); } void UserInterface::snapWidgets() { dimension2d screenSize = m_Engine->m_Driver->getScreenSize(); rect newRect; // newRect.LowerRightCorner.X = static_cast(size.Width); // newRect.LowerRightCorner.Y = static_cast(size.Height); rect prevRect = playbackWindow->getRelativePosition(); newRect.UpperLeftCorner.X = static_cast(screenSize.Width) - prevRect.getWidth() - spacing_y; // debug() << "screen size: " << screenSize.Width << "x" << screenSize.Height; // debug() << " prevRect: " // << prevRect.UpperLeftCorner.X << "," // << prevRect.UpperLeftCorner.Y << "," // << prevRect.LowerRightCorner.X << "," // << prevRect.LowerRightCorner.Y // << " size=(" << prevRect.getWidth() << "," // << prevRect.getHeight() << ")" << endl; newRect.UpperLeftCorner.Y = prevRect.UpperLeftCorner.Y; newRect.LowerRightCorner.X = newRect.UpperLeftCorner.X + prevRect.getWidth(); newRect.LowerRightCorner.Y = newRect.UpperLeftCorner.Y + prevRect.getHeight(); playbackWindow->setRelativePosition(newRect); m_WindowSize.Width = m_Engine->m_Driver->getScreenSize().Width; m_WindowSize.Height = m_Engine->m_Driver->getScreenSize().Height; } // PUBLIC UserInterface::UserInterface(Engine* engine) { this->recent_initialized = false; viewTextureInterpolationIdx = 0; viewWireframeIdx = 0; viewLightingIdx = 0; this->playbackStartStopButton = nullptr; m_Engine = engine; m_Gui = engine->getGUIEnvironment(); playbackWindow = nullptr; setupUserInterface(); } UserInterface::~UserInterface() { delete m_GuiFont; delete m_GuiFontFace; } IGUIEnvironment* UserInterface::getGUIEnvironment() const { return m_Gui; } void UserInterface::drawStatusLine() const { } bool UserInterface::OnSelectMesh() { this->m_MatchingTextures.clear(); this->m_AllTextures.clear(); return true; } void UserInterface::setPlaybackText(s32 id, const wchar_t* text) { switch (id) { case (UIE_PLAYBACKSTARTFRAMEEDITBOX): this->playbackStartFrameEditBox->setText(text); break; case (UIE_PLAYBACKENDFRAMEEDITBOX): this->playbackEndFrameEditBox->setText(text); break; default: std::cerr << "ERROR: setPlaybackText got a bad id: " << id << std::endl; // break; } } /** * Load the next texture from the list of found textures. * Files are only listed once for speed, so you must reload the * model to trigger another list ("dir") operation (since loading * a mesh calls OnSelectMesh() which clears allTextures and matchingTextures). * * @param direction Specify <0 to choose previous texture, >0 for next, 0 to * reload current texture if any; otherwise, only select a texture if any * matching textures (named like model) are present in . or ../textures. * @return Any texture was loaded (true/false). */ bool UserInterface::loadNextTexture(int direction) { bool enableVerbose = false; if (this->m_Engine != nullptr) { enableVerbose = this->m_Engine->m_EnableVerbose; } if (enableVerbose) { cerr << "Loading texture..." << flush; } bool ret = false; std::wstring basePath = L"."; if (this->m_Engine->m_LoadedMeshPath.length() > 0) { std::wstring prevModelName = Utility::basename( this->m_Engine->m_LoadedMeshPath ); wstring foundPath; wstring prevModelNoExt; prevModelNoExt = Utility::withoutExtension(prevModelName); /* vector names; names.push_back(prevModelNoExt+L"_mesh"); names.push_back(prevModelNoExt); names.push_back(prevModelNoExt+L"_1"); names.push_back(prevModelNoExt+L"_2"); names.push_back(prevModelNoExt+L"_1"); names.push_back(prevModelNoExt+L"_2"); names.push_back(prevModelNoExt+L"_01"); names.push_back(prevModelNoExt+L"_02"); names.push_back(prevModelNoExt+L"_01"); names.push_back(prevModelNoExt+L"_02"); names.push_back(prevModelNoExt+L"_child"); names.push_back(prevModelNoExt+L"_female"); names.push_back(prevModelNoExt+L"_f"); names.push_back(prevModelNoExt+L"_male"); names.push_back(prevModelNoExt+L"_m"); */ vector badSuffixes; badSuffixes.push_back(L"_inv"); std::wstring lastDirPath = Utility::parentOfPath( this->m_Engine->m_LoadedMeshPath ); std::wstring parentPath = Utility::parentOfPath(lastDirPath); std::wstring dirSeparator = Utility::delimiter( this->m_Engine->m_LoadedMeshPath ); std::wstring texturesPath = parentPath + dirSeparator + L"textures"; std::wstring tryTexPath = texturesPath + dirSeparator + prevModelNoExt + L".png"; vector texturePaths; texturePaths.push_back(lastDirPath); if (fs::is_directory(fs::status(texturesPath))) { texturePaths.push_back(texturesPath); } vector dotExts; for (auto ext : this->m_Engine->m_TextureExtensions) { dotExts.push_back(L"." + ext); } if (this->m_MatchingTextures.size() + this->m_AllTextures.size() < 1) { for (auto path : texturePaths) { for (const auto& itr : fs::directory_iterator(path)) { if (fs::is_regular_file(itr.status())) { std::wstring name = itr.path().filename().wstring(); std::wstring suffix = Utility::getSuffix(name, dotExts, true); bool isUsable = true; std::wstring nameNoExt = Utility::withoutExtension( name ); if (Utility::endsWithAny(nameNoExt, badSuffixes, true)) isUsable = false; if (isUsable && suffix.length() > 0) { this->m_AllTextures.push_back( path + dirSeparator + name ); if (Utility::startsWith(name, prevModelNoExt)) { this->m_MatchingTextures.push_back( path + dirSeparator + name ); } else if (name.find(prevModelNoExt) != std::wstring::npos) { this->m_MatchingTextures.push_back( path + dirSeparator + name ); } else if (name.find(Utility::replaceAll(prevModelNoExt, L"_", L"")) != std::wstring::npos) { this->m_MatchingTextures.push_back( path + dirSeparator + name ); } } } } } } vector paths = this->m_MatchingTextures; if (this->m_MatchingTextures.size() < 1) { paths = this->m_AllTextures; if (enableVerbose) { debug() << "There were no matching textures so" << " the entire list of " << this->m_AllTextures.size() << " found textures will be used..." << std::flush; } } else { // Assume the user wants to view name-matched texture using // the render settings of Minetest. this->m_Engine->setEnableTextureInterpolation(false); viewMenu->setItemChecked( viewTextureInterpolationIdx, this->m_Engine->getEnableTextureInterpolation() ); } std::wstring prevTexture = L""; std::wstring nextTexture = L""; std::wstring lastTexture = L""; std::wstring firstTexture = L""; bool found = false; for (auto path : paths) { if (firstTexture.length() == 0) firstTexture = path; lastTexture = path; if (this->m_Engine->m_LoadedTexturePath.length() > 0) { if (path == this->m_Engine->m_LoadedTexturePath) { found = true; } else if (!found) { prevTexture = path; } else { if (nextTexture.length() == 0) nextTexture = path; } } else { prevTexture = path; // Use the last one as the previous. if (nextTexture.length() == 0) nextTexture = path; } } if (nextTexture.length() == 0) nextTexture = firstTexture; // The last is current, so next is 1st. if (prevTexture.length() == 0) { if (lastTexture != firstTexture) prevTexture = lastTexture; // Wrap to end. else prevTexture = firstTexture; // Use the only texture. } if (lastTexture.length() > 0) { if (direction < 0) { if (enableVerbose) { cerr << "loading the previous texture..."; } ret = this->m_Engine->loadTexture(prevTexture, false); } else if (direction > 0) { if (enableVerbose) { cerr << "loading the next texture..."; } ret = this->m_Engine->loadTexture(nextTexture, false); } else { // If direction is 0 (such as when a model is loaded that has // no texture), only load a specified or matching texture. if (this->m_Engine->m_LoadedTexturePath.length() > 0) { if (enableVerbose) { cerr << "using a specified texture..."; } ret = this->m_Engine->loadTexture( this->m_Engine->m_LoadedTexturePath, false ); } else if (this->m_MatchingTextures.size() >= 1) { if (enableVerbose) { cerr << "loading matching texture..."; } ret = this->m_Engine->loadTexture(firstTexture, false); } else { ret = true; if (enableVerbose) { cerr << "(cycling was off and there is no matching texture) "; } } } } else if (this->m_Engine->m_LoadedTexturePath.length() > 0) { if (enableVerbose) { cerr << "loading the first texture..."; } ret = this->m_Engine->loadTexture( this->m_Engine->m_LoadedTexturePath, false ); } } else { if (enableVerbose) { debug() << "Can't cycle texture since no file was opened" << endl; } } if (enableVerbose) { cerr << (ret?"OK":"FAILED") << endl; } return ret; } void UserInterface::exportMeshToHome(std::string extension) { if (this->m_Engine->m_LoadedMesh != nullptr) { // this->m_Engine->m_LoadedMesh->getName(); // displaySaveFileDialog(); irr::io::path where = irr::io::path(); if (const char* env_p = std::getenv("HOME")) { // std::cout << "Your PATH is: " << env_p << '\n'; where = irr::io::path(env_p); std::cout << "Your PATH is: " << where.c_str() << '\n'; } else if (const char* env_p = std::getenv("USERPROFILE")) { // std::cout << "Your PATH is: " << env_p << '\n'; where = irr::io::path(env_p); std::cout << "Your PATH is: " << where.c_str() << '\n'; } std::string name = ""; if (m_Engine->m_LoadedMeshPath.length() > 0) { name = Utility::toString(Utility::withoutExtension(Utility::basename(m_Engine->m_LoadedMeshPath))); } wstring result = m_Engine->saveMesh(where, name, extension); std::wstring caption = L"Export Failed"; std::wstring msg = L"The format or home variable is unwriteable"; if (result.length() > 0) { caption = L"Export Finished"; msg = L"Saved " + result; } std::cout << "Exported as: " << Utility::toString(result) << '\n'; this->m_Engine->m_Device->getGUIEnvironment()->addMessageBox( caption.c_str(), msg.c_str()); } else { this->m_Engine->m_Device->getGUIEnvironment()->addMessageBox( L"Export", L"There is nothing to export."); } } void UserInterface::clearRecent() { // for (int idx=this->uic_file_recent_next-1; idx>=UserInterface::uic_file_recent_first; idx--) { for (std::vector::iterator idxIt = this->recentIndices.begin(); idxIt != this->recentIndices.end(); ++idxIt) { this->recentMenu->removeItem(*idxIt); } this->recentIndices.clear(); this->uic_file_recent_next = UserInterface::UIC_FILE_RECENT_FIRST; this->m_file_recent_first_idx = -1; this->m_file_recent_last_idx = -1; } void UserInterface::addRecentMenuItem(std::string path, bool addToEngine) { bool enableVerbose = false; if (this->m_Engine != nullptr) { enableVerbose = this->m_Engine->m_EnableVerbose; } else { std::cerr << "Error: m_Engine isn't ready in addRecentMenuItem." << std::endl; } if (!this->recent_initialized) { throw std::runtime_error("The UI is not ready in addRecentMenuItem."); } if (enableVerbose) { std::cerr << "[addRecentMenuItem] " << path << "..." << std::endl; } if (!this->hasRecent(path)) { if (enableVerbose) { std::cerr << "* adding since new..." << std::endl; } wstring path_ws = Utility::toWstring(path); if (this->uic_file_recent_next < UserInterface::UIC_FILE_RECENT_FIRST) { throw std::runtime_error("this->uic_file_recent_next is " + std::to_string(this->uic_file_recent_next) + " but should be equal to or greater than first: " + std::to_string(this->uic_file_recent_next)); } // The first this->uic_file_recent_next is 1101 or whatever // UserInterface::UIC_FILE_RECENT_FIRST (usually UIC_FILE_RECENT+1) is. u32 newI = this->recentMenu->addItem(path_ws.c_str(), this->uic_file_recent_next); if (enableVerbose) { std::cerr << "+this->recentMenu->addItem" << " idx:" << newI << " commandID:" << this->uic_file_recent_next << std::endl; } // IGUIContextMenu* menu = this->recentMenu->getSubMenu(newI); // NOTE: Caller would be the parent menu id on click! // newI is a sequential number starting at 1 which becomes the // selected item (See menu->getSelectedItem() in handleMenuItemPressed) // this->recentIndices.push_back(newI); this->recentIndices.push_back(this->uic_file_recent_next); this->uic_file_recent_next++; /* if (this->m_file_recent_first_idx < 0) { this->m_file_recent_first_idx = menu->getID(); // SIGSEGV crash } this->m_file_recent_last_idx = menu->getID(); */ if (addToEngine) { this->m_Engine->addRecent(path); } } } void UserInterface::addRecentMenuItems(std::vector paths, bool addToEngine) { if (!this->recent_initialized) { throw std::runtime_error("The UI is not ready in addRecent."); } for (std::vector::iterator it = paths.begin() ; it != paths.end(); ++it) { try { this->addRecentMenuItem(*it, addToEngine); } catch (const std::runtime_error& ex) { cerr << ex.what() << std::endl; break; } } } bool UserInterface::hasRecent(std::string path) { bool enableVerbose = false; if (this->m_Engine != nullptr) { enableVerbose = this->m_Engine->m_EnableVerbose; } else { std::cerr << "Error: The engine is not ready in hasRecent." << std::endl; } if (!this->recent_initialized) { throw std::runtime_error("The UI is not ready in addRecent."); } if (enableVerbose) { std::cerr << " [hasRecent]" << std::endl; std::cerr << " * checking children..." << std::endl; } // See http://irrlicht.sourceforge.net/docu/_i_g_u_i_element_8h_source.html#l00570 // core::list< IGUIElement * > Children = this->getChildren(); // ^ ‘class UserInterface’ has no member named ‘getChildren’ // core::list< IGUIElement * > Children = this->recentMenu->getChildren(); // ^ gets no results // core::list< IGUIElement * > Children = this->menu->getChildren(); // ^ only gets UIE_FILEMENU, UIE_PLAYBACKMENU, and UIE_VIEWMENU std::string thisItemStr = " "; for (u32 i = 0; i < this->recentMenu->getItemCount(); i++) { thisItemStr = Utility::toString((wstring)this->recentMenu->getItemText(i)); // getItemText gets wchar_t* // std::cerr << " * text:" << thisItemStr // << std::endl; if (thisItemStr == path) return true; } return false; // The commented section below is not valid since there are no children // apparently, only items (inaccessible internals obtained by index). /* core::list< IGUIElement * > Children = this->recentMenu->getChildren(); core::list::Iterator it = Children.begin(); for (; it != Children.end(); ++it) { irr::gui::IGUIElement* child = *it; std::cerr << " * text:" << Utility::toString((std::wstring)child->getText()) << " parent:" << Utility::toString((std::wstring)child->getParent()->getText()) << " id:" << Utility::toString(child->getID()) << std::endl; // - getText is a const wchar_t* // - getParent is an IGUIElement* // (*it)->Parent; } // ^ gets no results std::cerr << " * checking recent menu items for " << path << "..." << std::endl; for (std::vector::iterator uiIt = this->recentIndices.begin() ; uiIt != this->recentIndices.end(); ++uiIt) { // In the comments below, 1 is selected (idx) and 1101 is commandID. // None of the commands work! An item is not an IGUI element apparently! // IGUIContextMenu* child = this->recentMenu->getSubMenu(*uiIt); // ^ null for 1 or 1101 // irr::gui::IGUIElement* child = this->recentMenu->getElementFromId(*uiIt, true); // ^ null for 1101 // irr::gui::IGUIElement* child = this->menu->getElementFromId(*uiIt, true); // ^ null for 1 or 1101 // irr::gui::IGUIElement* child = this->recentMenu->getElementFromId(*uiIt, true); // ^ null for 1 or 1101 irr::gui::IGUIElement* child = this->getGUIEnvironment()->getRootGUIElement()->getElementFromId(1, true); // ^ null for 1 or 1101 // ^ cast to the specific IGUI... interface before use! // ^ See if (child != nullptr) { std::cerr << " - " << *uiIt << " (1): " << Utility::toString(child->getText()) << std::endl; if (Utility::toString(child->getText()) == path) { return true; } } else { std::cerr << " - null at " << *uiIt << std::endl; std::string uiItMsg = std::to_string(*uiIt); // std::string uiItMsg = "recent_initialized) { throw std::runtime_error("The UI is not ready in openRecent."); } // IGUIElement* submenu = this->recentMenu->getElementFromId(commandID); // ^ There is no element for menuID (such as 1100) nor for commandID (such as 1) // IGUIElement* submenu = this->recentMenu->getSubMenu(selectedItemID); // ^ There is no submenu for selectedItemID (such as 1) // IGUIElement* submenu = this->recentMenu->getSubMenu(commandID); // ^ There is no submenu for commandID (such as 1101) // IGUIElement* submenu = this->menu->getElementFromId(commandID); // ^ There is no elemend for commandID (such as 1101) // IGUIElement* submenu = this->menu->getElementFromId(selectedItemID); // ^ There is no element for selectedItemID (such as 1) // IGUIElement* submenu = this->menu->getSubMenu(commandID); // ^ There is no submenu for commandID (such as 1101) IGUIElement* submenu = this->menu->getSubMenu(selectedItemID); // ^ requires the sequential selectedItemID (NOT commandID specified on create) if (submenu != nullptr) { std::wstring menuText = this->recentMenu->getItemText(selectedItemID); // std::string path = Utility::toString(submenu->getText()); // ^ blank std::string path = Utility::toString(menuText); // blank cerr << "path: " << path << endl; cerr << "selectedItemID: " << selectedItemID << endl; cerr << "menuText: " << Utility::toString(menuText) << endl; result = m_Engine->loadMesh(menuText, true); // true to adjust recent menu order if (!result) { this->m_Engine->m_Device->getGUIEnvironment()->addMessageBox( L"Load Mesh", L"The model is inaccessible or not in a compatible format."); } } else { cerr << "[UserInterface::openRecent] Error: There is no submenu for selectedItemID " << selectedItemID << std::endl; // for (auto it : this->recentMenu->getChildren()) { // cerr << " - " << it << std::endl; // } // ^ iterates 0 times; ranged for } return result; } // IEventReceiver bool UserInterface::OnEvent(const SEvent& event) { // Note EventHandler::OnEvent calls other handlers for // Certain event types (See ERT_3DVIEW there for instance). bool handled = false; if (event.EventType == EET_USER_EVENT) { // debug() << "EET_USER_EVENT..." << endl; if (event.UserEvent.UserData1 == UEI_WINDOWSIZECHANGED) { if ((m_WindowSize.Width != m_Engine->m_Driver->getScreenSize().Width) || (m_WindowSize.Height != m_Engine->m_Driver->getScreenSize().Height)) { snapWidgets(); } handled = true; } } else if (event.EventType == EET_GUI_EVENT) { // debug() << "EET_GUI_EVENT..." << endl; handled = true; // set to false below if not handled const SEvent::SGUIEvent* ge = &(event.GUIEvent); s32 callerID = ge->Caller->getID(); switch (ge->EventType) { // See http://irrlicht.sourceforge.net/docu/example009.html case EGET_BUTTON_CLICKED: switch(callerID) { case UIE_PLAYBACKSTARTSTOPBUTTON: this->m_Engine->toggleAnimation(); break; case UIE_PLAYBACKINCREASEBUTTON: this->m_Engine->incrementAnimationFPS(5); break; case UIE_PLAYBACKDECREASEBUTTON: this->m_Engine->incrementAnimationFPS(-5); break; default: cerr << "EGET_BUTTON_CLICKED wasn't expected from ID " << callerID << std::endl; handled = false; break; } case EGET_FILE_SELECTED: this->m_Engine->m_View->m_MouseUser = ""; switch(callerID) { case UIE_LOADFILEDIALOG: { IGUIFileOpenDialog* fileOpenDialog = static_cast(ge->Caller); wstring path = fileOpenDialog->getFileName(); bool result = false; wstring extension = Utility::extensionOf(path); if (Utility::toLower(Utility::toString(extension)) == "irr") { result = m_Engine->loadScene(fileOpenDialog->getFileName()); } else { result = m_Engine->loadMesh(fileOpenDialog->getFileName(), true); } if (result) { try { this->addRecentMenuItem(Utility::toString(path), true); } catch (const std::runtime_error& ex) { cerr << ex.what() << std::endl; break; } } if (!result) { this->m_Engine->m_Device->getGUIEnvironment()->addMessageBox( L"Load Mesh", L"The model is inaccessible or not in a compatible format."); } } break; case UIE_SAVEFILEDIALOG: { if (m_Engine->m_LoadedMesh != nullptr) { IGUIFileOpenDialog* fileOpenDialog = static_cast(ge->Caller); // fileOpenDialog->getFileName() m_Engine->saveMesh(fileOpenDialog->getDirectoryName(), "", "dae"); } else { this->m_Engine->m_Device->getGUIEnvironment()->addMessageBox( L"Export", L"There is nothing to save."); } } break; case UIE_LOADTEXTUREDIALOG: { IGUIFileOpenDialog* fileOpenDialog = static_cast(ge->Caller); m_Engine->loadTexture(fileOpenDialog->getFileName(), true); } break; default: cerr << "EGET_FILE_SELECTED wasn't expected from ID: " << callerID << std::endl; handled = false; break; } case EGET_EDITBOX_ENTER: switch (callerID) { case UIE_PLAYBACKSETFRAMEEDITBOX: if (this->m_Engine->m_LoadedMesh != nullptr) { this->m_Engine->m_LoadedMesh->setCurrentFrame( Utility::toF32(this->playbackSetFrameEditBox->getText()) ); } break; case UIE_PLAYBACKSTARTFRAMEEDITBOX: if (this->m_Engine->m_LoadedMesh != nullptr) { this->m_Engine->m_LoadedMesh->setFrameLoop( Utility::toF32(this->playbackStartFrameEditBox->getText()), Utility::toF32(this->playbackEndFrameEditBox->getText()) ); } break; case UIE_PLAYBACKENDFRAMEEDITBOX: if (this->m_Engine->m_LoadedMesh != nullptr) { this->m_Engine->m_LoadedMesh->setFrameLoop( Utility::toF32(this->playbackStartFrameEditBox->getText()), Utility::toF32(this->playbackEndFrameEditBox->getText()) ); } break; case UIE_TEXTUREPATHEDITBOX: if (this->m_Engine->m_LoadedMesh != nullptr) { this->m_Engine->loadTexture(texturePathEditBox->getText(), false); } break; case UIE_FPSEDITBOX: if (this->m_Engine->m_LoadedMesh != nullptr) { this->m_Engine->m_LoadedMesh->setAnimationSpeed( Utility::toF32(this->playbackFPSEditBox->getText()) ); } break; case UIE_AXISSIZEEDITBOX: this->m_Engine->m_AxisLength = Utility::toF32( this->axisSizeEditBox->getText() ); break; default: cerr << "EGET_EDITBOX_ENTER isn't processed for ID: " << callerID << std::endl; handled = false; break; } case EGET_MESSAGEBOX_YES: this->m_Engine->m_View->m_MouseUser = ""; break; case EGET_MESSAGEBOX_NO: this->m_Engine->m_View->m_MouseUser = ""; break; case EGET_MESSAGEBOX_OK: this->m_Engine->m_View->m_MouseUser = ""; break; case EGET_MESSAGEBOX_CANCEL: this->m_Engine->m_View->m_MouseUser = ""; break; case EGET_FILE_CHOOSE_DIALOG_CANCELLED: // ^ 12 usually this->m_Engine->m_View->m_MouseUser = ""; break; case EGET_ELEMENT_FOCUS_LOST: // ^ 0 usually switch (callerID) { case UIE_LOADFILEDIALOG: this->m_Engine->m_View->m_MouseUser = ""; break; case UIE_SAVEFILEDIALOG: this->m_Engine->m_View->m_MouseUser = ""; break; default: break; } debug() << callerID << " lost focus." << std::endl; handled = false; break; case EGET_ELEMENT_FOCUSED: switch (callerID) { case UIE_LOADFILEDIALOG: this->m_Engine->m_View->m_MouseUser = "UIE_LOADFILEDIALOG"; break; case UIE_SAVEFILEDIALOG: this->m_Engine->m_View->m_MouseUser = "UIE_SAVEFILEDIALOG"; break; default: break; } debug() << callerID << " got focus." << std::endl; handled = false; break; case EGET_ELEMENT_HOVERED: // debug() << "hovered over " << callerID << "." << std::endl; handled = false; break; case EGET_ELEMENT_LEFT: // debug() << "left " << callerID << "." << std::endl; handled = false; break; case EGET_MENU_ITEM_SELECTED: handled = handleMenuItemPressed(ge); break; case EGET_SCROLL_BAR_CHANGED: handled = false; break; case EGET_COMBO_BOX_CHANGED: handled = false; break; case EGET_ELEMENT_CLOSED: debug() << "closed " << callerID << "." << std::endl; handled = false; break; case EGET_DIRECTORY_SELECTED: this->m_Engine->m_View->m_MouseUser = ""; handled = false; break; case EGET_EDITBOX_MARKING_CHANGED: this->m_Engine->m_View->m_MouseUser = "edit box"; handled = false; break; default: // EET_MOUSE_INPUT_EVENT EET_KEY_INPUT_EVENT EET_JOYSTICK_INPUT_EVENT cerr << "[UserInterface] (verbose message) event.GUIEvent.EventType " << ge->EventType << " (See EGET_* in irrlicht/IEventReciever.h) is not handled (event.EventType is EET_GUI_EVENT)." << std::endl; handled = false; break; } } else if (event.EventType == EET_KEY_INPUT_EVENT) { // debug() << "EET_KEY_INPUT_EVENT..." << endl; handled = true; // set to false below if not handled if (event.KeyInput.PressedDown && !m_Engine->KeyIsDown[event.KeyInput.Key]) { if (event.KeyInput.Key == irr::KEY_F5) { if (m_Engine->KeyIsDown[irr::KEY_LSHIFT] || m_Engine->KeyIsDown[irr::KEY_RSHIFT]) { bool result = m_Engine->reloadTexture(); if (!result) { this->m_Engine->m_Device->getGUIEnvironment()->addMessageBox( L"Reload Texture", L"The texture is inaccessible or not in a compatible format."); } } else { if (m_Engine->m_LoadedMeshPath.length() > 0) { bool result = m_Engine->reloadMesh(); if (!result) { this->m_Engine->m_Device->getGUIEnvironment()->addMessageBox( L"Reload Mesh", L"The model is inaccessible or not in a compatible format."); } } else { debug() << " - No mesh is loaded." << endl; } } } else if (event.KeyInput.Key == irr::KEY_F3) { if (m_Engine->KeyIsDown[irr::KEY_LSHIFT] || m_Engine->KeyIsDown[irr::KEY_RSHIFT]) { loadNextTexture(-1); debug() << " - back" << endl; } else loadNextTexture(1); } else if (event.KeyInput.Key == irr::KEY_KEY_I) { if (m_Engine->KeyIsDown[irr::KEY_LCONTROL] || m_Engine->KeyIsDown[irr::KEY_RCONTROL]) { // IGUIContextMenu* textureInterpolationElement = ( // dynamic_cast( // viewMenu->getElementFromId( // UIC_VIEW_TEXTURE_INTERPOLATION // ) // ); // ) m_Engine->setEnableTextureInterpolation( !m_Engine->getEnableTextureInterpolation() ); viewMenu->setItemChecked( viewTextureInterpolationIdx, m_Engine->getEnableTextureInterpolation() ); } else handled = false; } else if (event.KeyInput.Key == irr::KEY_RIGHT) { if (m_Engine->KeyIsDown[irr::KEY_LCONTROL] || m_Engine->KeyIsDown[irr::KEY_RCONTROL]) { m_Engine->incrementAnimationFPS(5); } else { incrementFrame(1.0f, true); } } else if (event.KeyInput.Key == irr::KEY_LEFT) { if (m_Engine->KeyIsDown[irr::KEY_LCONTROL] || m_Engine->KeyIsDown[irr::KEY_RCONTROL]) { m_Engine->incrementAnimationFPS(-5); } else { incrementFrame(-1.0f, true); } } else if (event.KeyInput.Char == L' ') { m_Engine->toggleAnimation(); } else handled = false; // std::wcerr << "Char: " << event.KeyInput.Char << endl; } m_Engine->KeyIsDown[event.KeyInput.Key] = event.KeyInput.PressedDown; return true; } else if (event.EventType == EET_MOUSE_INPUT_EVENT) { // debug() << "EET_MOUSE_INPUT_EVENT..." << endl; handled = true; // set to false below if not handled // TODO: improve this copypasta (or elsewhere use states 1 and 3 as // events, and add 1 as "handled" (or set back to 0 for no drag feature) // as intended for drag or long press handling). switch (event.MouseInput.Event) { case EMIE_LMOUSE_LEFT_UP: if (m_Engine->LMouseState == 2) { m_Engine->LMouseState = 3; } break; case EMIE_LMOUSE_PRESSED_DOWN: if (m_Engine->LMouseState == 0) { m_Engine->LMouseState = 1; } break; case EMIE_RMOUSE_LEFT_UP: if (m_Engine->RMouseState == 2) { m_Engine->RMouseState = 3; } break; case EMIE_RMOUSE_PRESSED_DOWN: if (m_Engine->RMouseState == 0) { m_Engine->RMouseState = 1; } break; default: handled = false; } } else { cerr << "[UserInterface] (verbose message) event.EventType " << event.EventType << " is ignored." << std::endl; } return handled; }