diff --git a/CMakeLists.txt b/CMakeLists.txt index 75cefcc..556c81d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -193,6 +193,8 @@ set(re-edit_BUILD_SOURCES "${re-edit_CPP_SRC_DIR}/re/edit/Graphics.cpp" "${re-edit_CPP_SRC_DIR}/re/edit/NetworkManager.h" "${re-edit_CPP_SRC_DIR}/re/edit/NetworkManager.cpp" + "${re-edit_CPP_SRC_DIR}/re/edit/Notification.h" + "${re-edit_CPP_SRC_DIR}/re/edit/Notification.cpp" "${re-edit_CPP_SRC_DIR}/re/edit/Panel.h" "${re-edit_CPP_SRC_DIR}/re/edit/Panel.cpp" "${re-edit_CPP_SRC_DIR}/re/edit/PanelActions.cpp" diff --git a/src/cpp/re/edit/AppContext.cpp b/src/cpp/re/edit/AppContext.cpp index 98ed26c..df4ea5f 100644 --- a/src/cpp/re/edit/AppContext.cpp +++ b/src/cpp/re/edit/AppContext.cpp @@ -24,6 +24,7 @@ #include "Utils.h" #include "stl.h" #include "Clipboard.h" +#include "UIContext.h" #include #include #include @@ -31,12 +32,15 @@ namespace re::edit { +constexpr auto kShortNotificationDuration = std::chrono::seconds(1); +constexpr auto kInfoNotificationDuration = std::chrono::seconds(5); + namespace impl { class UpdateListener : public efsw::FileWatchListener { public: - UpdateListener(AppContext &iCtx, fs::path const &iRoot) : + UpdateListener(AppContext *iCtx, fs::path const &iRoot) : fCtx{iCtx}, fRoot{fs::canonical(iRoot)} { // empty @@ -70,8 +74,13 @@ class UpdateListener : public efsw::FileWatchListener if(file == fRoot / "motherboard_def.lua" || file == fRoot / "info.lua") { - // trigger maybe reloadDevice - fCtx.maybeReloadDevice(true); + if(UIContext::HasCurrent()) + { + UIContext::GetCurrent().execute([ctx = fCtx] { + if(AppContext::IsCurrent(ctx)) + ctx->onDeviceUpdate(); + }); + } } else { @@ -80,15 +89,20 @@ class UpdateListener : public efsw::FileWatchListener std::cmatch m; if(std::regex_search(file.filename().u8string().c_str(), m, FILENAME_REGEX)) { - // trigger maybe scanDirectory - fCtx.maybeReloadTextures(true); + if(UIContext::HasCurrent()) + { + UIContext::GetCurrent().execute([ctx = fCtx] { + if(AppContext::IsCurrent(ctx)) + ctx->onTexturesUpdate(); + }); + } } } } } private: - AppContext &fCtx; + AppContext *fCtx; fs::path fRoot; }; @@ -120,6 +134,17 @@ AppContext::~AppContext() disableFileWatcher(); } +//------------------------------------------------------------------------ +// AppContext::IsCurrent +//------------------------------------------------------------------------ +bool AppContext::IsCurrent(AppContext *iCtx) +{ + if(iCtx && Application::HasCurrent()) + return Application::GetCurrent().getAppContext() == iCtx; + else + return false; +} + //------------------------------------------------------------------------ // AppContext::initPanels //------------------------------------------------------------------------ @@ -262,34 +287,6 @@ void AppContext::render() renderZoomSelection(); renderGridSelection(); - if(hasNotifications()) - { - ImGui::SeparatorText("Notifications"); - if(maybeReloadTextures()) - { - ImGui::AlignTextToFramePadding(); - ReGui::TipIcon();ImGui::SameLine();ImGui::TextUnformatted("Detected image changes"); - ImGui::SameLine(); - if(ImGui::Button(ReGui_Prefix(ReGui_Icon_RescanImages, "Rescan"))) - fReloadTexturesRequested = true; - ImGui::SameLine(); - if(ImGui::Button(ReGui_Prefix(ReGui_Icon_Reset, "Dismiss"))) - maybeReloadTextures(false); - } - - if(maybeReloadDevice()) - { - ImGui::AlignTextToFramePadding(); - ReGui::TipIcon();ImGui::SameLine();ImGui::TextUnformatted("Detected device changes"); - ImGui::SameLine(); - if(ImGui::Button(ReGui_Prefix(ReGui_Icon_ReloadMotherboard, "Reload"))) - fReloadDeviceRequested = true; - ImGui::SameLine(); - if(ImGui::Button(ReGui_Prefix(ReGui_Icon_Reset, "Dismiss"))) - maybeReloadDevice(false); - } - } - ImGui::SeparatorText("Rendering"); ImGui::PushID("Rendering"); @@ -574,10 +571,10 @@ std::string AppContext::getDeviceName() const //------------------------------------------------------------------------ // AppContext::reloadTextures //------------------------------------------------------------------------ -void AppContext::reloadTextures() +bool AppContext::reloadTextures() { markEdited(); - checkForErrors(); + return checkForErrors(); } //------------------------------------------------------------------------ @@ -625,10 +622,10 @@ void AppContext::initGUI2D(Utils::CancellableSPtr const &iCancellable) //------------------------------------------------------------------------ // AppContext::reloadDevice //------------------------------------------------------------------------ -void AppContext::reloadDevice() +bool AppContext::reloadDevice() { initDevice(); - checkForErrors(); + return checkForErrors(); } //------------------------------------------------------------------------ @@ -819,9 +816,9 @@ void AppContext::renderMainMenu() { auto numTextures = importTexturesBlocking(); if(numTextures > 0) - Application::GetCurrent().newDialog("Import") - .text(fmt::printf("%ld images imported successfully", numTextures)) - .buttonOk(); + Application::GetCurrent().newNotification() + .text(fmt::printf("%ld image(s) imported successfully", numTextures)) + .dismissAfter(kInfoNotificationDuration); ; } ImGui::Separator(); @@ -829,20 +826,10 @@ void AppContext::renderMainMenu() { fReloadTexturesRequested = true; } - if(fMaybeReloadTextures) - { - ImGui::SameLine(); - ImGui::TextUnformatted("\u00b7"); - } if(ImGui::MenuItem(ReGui_Prefix(ReGui_Icon_ReloadMotherboard, "Reload motherboard"))) { fReloadDeviceRequested = true; } - if(fMaybeReloadDevice) - { - ImGui::SameLine(); - ImGui::TextUnformatted("\u00b7"); - } ImGui::Separator(); if(ImGui::MenuItem("Delete unused images")) { @@ -971,18 +958,36 @@ void AppContext::beforeRenderFrame() if(fReloadTexturesRequested) { fReloadTexturesRequested = false; - fMaybeReloadTextures = false; fTextureManager->scanDirectory(); - reloadTextures(); + if(reloadTextures()) + { + Application::GetCurrent().newNotification() + .text("Images reloaded. Some errors detected."); + } + else + { + Application::GetCurrent().newNotification() + .text("Images reloaded successfully.") + .dismissAfter(kShortNotificationDuration); + } } if(fReloadDeviceRequested) { fReloadDeviceRequested = false; - fMaybeReloadDevice = false; try { - reloadDevice(); + if(reloadDevice()) + { + Application::GetCurrent().newNotification() + .text("Device reloaded. Some errors detected."); + } + else + { + Application::GetCurrent().newNotification() + .text("Device reloaded successfully.") + .dismissAfter(kShortNotificationDuration); + } } catch(...) { @@ -1041,6 +1046,7 @@ void AppContext::save() Application::saveFile(GUI2D / "gui_2D.cmake", cmake(), &errors); Application::GetCurrent().savePreferences(&errors); if(errors.hasErrors()) + { Application::GetCurrent().newDialog("Error") .preContentMessage("There were some errors during the save operation") .lambda([errors] { @@ -1048,6 +1054,13 @@ void AppContext::save() ImGui::BulletText("%s", error.c_str()); }) .buttonOk(); + } + else + { + Application::GetCurrent().newNotification() + .text("Project saved successfully") + .dismissAfter(kShortNotificationDuration); + } // fAppContext->fUndoManager->clear(); fNeedsSaving = false; fLastSavedUndoAction = fUndoManager->getLastUndoAction(); @@ -1182,7 +1195,7 @@ void AppContext::enableFileWatcher() { if(!fRootWatchID) { - fRootListener = std::make_shared(*this, fRoot); + fRootListener = std::make_shared(this, fRoot); fRootWatchID = fRootWatcher->addWatch(fRoot.u8string(), fRootListener.get(), true); fRootWatcher->watch(); } @@ -1840,6 +1853,52 @@ PanelType AppContext::getPanelType(Action const *iAction) return PanelType::kUnknown; } +//------------------------------------------------------------------------ +// AppContext::onTexturesUpdate +//------------------------------------------------------------------------ +void AppContext::onTexturesUpdate() +{ + Application::GetCurrent() + .newUniqueNotification(ReGui::Notification::Key::from(&fReloadTexturesRequested)) + .lambda([ctx = this]() { + if(!AppContext::IsCurrent(ctx)) + { + return false; + } + + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted("Detected image changes"); + if(ImGui::Button(ReGui_Prefix(ReGui_Icon_RescanImages, "Rescan"))) + { + ctx->fReloadTexturesRequested = true; + } + return !ctx->fReloadTexturesRequested; + }); +} + +//------------------------------------------------------------------------ +// AppContext::onDeviceUpdate +//------------------------------------------------------------------------ +void AppContext::onDeviceUpdate() +{ + Application::GetCurrent() + .newUniqueNotification(ReGui::Notification::Key::from(&fReloadDeviceRequested)) + .lambda([ctx = this]() { + if(!AppContext::IsCurrent(ctx)) + { + return false; + } + + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted("Detected device changes"); + if(ImGui::Button(ReGui_Prefix(ReGui_Icon_ReloadMotherboard, "Reload"))) + { + ctx->fReloadDeviceRequested = true; + } + return !ctx->fReloadDeviceRequested; + }); +} + ////------------------------------------------------------------------------ //// AppContext::onNativeWindowPositionChange ////------------------------------------------------------------------------ diff --git a/src/cpp/re/edit/AppContext.h b/src/cpp/re/edit/AppContext.h index 9bd5a51..1602048 100644 --- a/src/cpp/re/edit/AppContext.h +++ b/src/cpp/re/edit/AppContext.h @@ -104,7 +104,11 @@ class AppContext AppContext(fs::path const &iRoot, std::shared_ptr iTextureManager); ~AppContext(); - static AppContext &GetCurrent() { RE_EDIT_INTERNAL_ASSERT(kCurrent != nullptr); return *kCurrent; } + inline static AppContext &GetCurrent() { RE_EDIT_INTERNAL_ASSERT(kCurrent != nullptr); return *kCurrent; } + + /** + * @return `true` if and only if `iCtx` is not `nullptr` and the current app context is equal to `iCtx` */ + static bool IsCurrent(AppContext *iCtx); ImVec2 getCurrentPanelSize() const; bool renderWidgetDefMenuItems(PanelType iPanelType, std::function const &iAction); @@ -215,12 +219,8 @@ class AppContext friend class Widget; friend class Application; - inline bool maybeReloadTextures() const { return fMaybeReloadTextures; } - inline bool maybeReloadDevice() const { return fMaybeReloadDevice; } - inline bool hasNotifications() const { return maybeReloadDevice() || maybeReloadTextures(); } - - void maybeReloadTextures(bool b) { fMaybeReloadTextures = b; } - void maybeReloadDevice(bool b) { fMaybeReloadDevice = b; } + void onTexturesUpdate(); + void onDeviceUpdate(); std::string getDeviceName() const; constexpr bool hasFoldedPanels() const { return fHasFoldedPanels; } @@ -246,7 +246,7 @@ class AppContext protected: void init(config::Device const &iConfig); config::Device getConfig() const; - void reloadTextures(); + bool reloadTextures(); void markEdited(); bool checkForErrors(); bool computeErrors(); @@ -256,7 +256,7 @@ class AppContext void renderUndoHistory(); void initDevice(); void initGUI2D(Utils::CancellableSPtr const &iCancellable); - void reloadDevice(); + bool reloadDevice(); void save(); void importBuiltIns(UserError *oErrors = nullptr); void applyTextureEffects(UserError *oErrors = nullptr); @@ -321,9 +321,7 @@ class AppContext void *fLastUndoAction{}; bool fRecomputeDimensionsRequested{true}; bool fReloadTexturesRequested{}; - std::atomic fMaybeReloadTextures{}; bool fReloadDeviceRequested{}; - std::atomic fMaybeReloadDevice{}; std::optional fNewLayoutRequested{}; ImGuiMouseCursor fMouseCursor{ImGuiMouseCursor_None}; diff --git a/src/cpp/re/edit/Application.cpp b/src/cpp/re/edit/Application.cpp index 20c0bc2..ac1136d 100644 --- a/src/cpp/re/edit/Application.cpp +++ b/src/cpp/re/edit/Application.cpp @@ -609,6 +609,7 @@ void Application::maybeCloseProject(std::optional const &iDialogTit void Application::closeProject() { savePreferences(); + fNotifications.clear(); fAppContext = nullptr; auto loggingManager = LoggingManager::instance(); loggingManager->clearAll(); @@ -672,8 +673,6 @@ void Application::onNativeDropFiles(std::vector const &iPaths) { RE_EDIT_INTERNAL_ASSERT(fAppContext != nullptr); - bool maybeReloadTextures = fAppContext->maybeReloadTextures(); - std::vector textureKeys{}; textureKeys.reserve(textures.size()); for(auto const &texture: textures) @@ -687,14 +686,14 @@ void Application::onNativeDropFiles(std::vector const &iPaths) { newDialog("Texture Import") .preContentMessage(fmt::printf("Successfully imported %ld graphics", textureKeys.size())) - .lambda([this, keys = textureKeys]() { - if(fAppContext) + .lambda([ctx = fAppContext.get(), keys = textureKeys]() { + if(AppContext::IsCurrent(ctx)) { auto textureSize = ImGui::CalcTextSize("W").y * 2.0f; for(auto &key: keys) { - auto texture = fAppContext->findTexture(key); + auto texture = ctx->findTexture(key); if(texture && texture->isValid()) { texture->ItemFit({textureSize, textureSize}); @@ -704,11 +703,7 @@ void Application::onNativeDropFiles(std::vector const &iPaths) } } }) - .button("Ok", [maybeReloadTextures, this] { - // dismisses the reload textures notification (unless there was one before starting...) - if(fAppContext && !maybeReloadTextures) - fAppContext->maybeReloadTextures(false); - }, true); + .buttonOk("Ok", true); } } }); @@ -778,12 +773,17 @@ void Application::handleFontChangeRequest() //------------------------------------------------------------------------ // Application::newFrame //------------------------------------------------------------------------ -bool Application::newFrame() noexcept +bool Application::newFrame(std::vector const &iFrameActions) noexcept { Utils::StorageRAII current{&kCurrent, this}; try { + if(!iFrameActions.empty()) + { + for(auto &action: iFrameActions) + action(); + } if(!fNewFrameActions.empty()) handleNewFrameActions(); @@ -850,6 +850,7 @@ bool Application::render() noexcept default: break; } + renderNotifications({}); } catch(...) { @@ -1445,6 +1446,30 @@ void Application::newExceptionDialog(std::string iMessage, bool iSaveButton, std } } + +//------------------------------------------------------------------------ +// Application::newNotification +//------------------------------------------------------------------------ +ReGui::Notification &Application::newNotification() +{ + fNotifications.emplace_back(std::make_unique()); + return *fNotifications[fNotifications.size() - 1]; +} + +//------------------------------------------------------------------------ +// Application::newUniqueNotification +//------------------------------------------------------------------------ +ReGui::Notification &Application::newUniqueNotification(ReGui::Notification::Key const &iKey) +{ + auto iter = std::find_if(fNotifications.begin(), fNotifications.end(), [&iKey](auto &n) { + return n->key() == iKey; + }); + if(iter != fNotifications.end()) + (*iter)->dismiss(); + fNotifications.emplace_back(std::make_unique(iKey)); + return *fNotifications[fNotifications.size() - 1]; +} + //------------------------------------------------------------------------ // Application::renderDialog //------------------------------------------------------------------------ @@ -1463,6 +1488,53 @@ void Application::renderDialog() fCurrentDialog = nullptr; } +//------------------------------------------------------------------------ +// Application::renderNotifications +//------------------------------------------------------------------------ +void Application::renderNotifications(ImVec2 const &iOffset) +{ + static std::pair kImGuiStyles = []() { + ImGuiStyle darkStyle{}; + ImGui::StyleColorsDark(&darkStyle); + ImGuiStyle lightStyle{}; + ImGui::StyleColorsLight(&lightStyle); + return std::pair{ darkStyle, lightStyle }; + }(); + + if(fNotifications.empty()) + return; + + auto const notificationSize = ImGui::CalcTextSize("MMMMMMMMMM""MMMMMMMMMM""MMMMMMMMMM""MMMMMMMMMM") * ImVec2{1.0, 5.0}; + auto const notificationPadding = ImGui::GetStyle().FramePadding; + auto mainViewPort = ImGui::GetMainViewport(); + + auto &style = fConfig.fStyle == config::Style::kLight ? kImGuiStyles.first : kImGuiStyles.second; + ImVec2 pos{mainViewPort->WorkSize.x - notificationSize.x - notificationPadding.x, notificationPadding.y}; + pos += iOffset; + + ImGui::PushStyleColor(ImGuiCol_Text, style.Colors[ImGuiCol_Text]); + ImGui::PushStyleColor(ImGuiCol_WindowBg, style.Colors[ImGuiCol_WindowBg]); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2{4.0, 4.0}); + + auto notifications = std::move(fNotifications); + + for(auto ¬ification: notifications) + { + ImGui::SetNextWindowPos(pos); + ImGui::SetNextWindowSize(notificationSize); + if(notification->render()) + pos.y += notificationSize.y + notificationPadding.x; + } + + ImGui::PopStyleVar(); + ImGui::PopStyleColor(2); + + for(auto ¬ification: notifications) + { + if(notification->isActive()) + fNotifications.emplace_back(std::move(notification)); + } +} //------------------------------------------------------------------------ // Application::maybeExit diff --git a/src/cpp/re/edit/Application.h b/src/cpp/re/edit/Application.h index 3b11cf4..201d68b 100644 --- a/src/cpp/re/edit/Application.h +++ b/src/cpp/re/edit/Application.h @@ -32,6 +32,7 @@ #include "Dialog.h" #include "fs.h" #include "Config.h" +#include "Notification.h" #include #include #include @@ -83,6 +84,7 @@ class Application ~Application(); static Application &GetCurrent() { RE_EDIT_INTERNAL_ASSERT(kCurrent != nullptr); return *kCurrent; } + static bool HasCurrent() { return kCurrent != nullptr; } static Config parseArgs(NativePreferencesManager const *iPreferencesManager, std::vector iArgs); @@ -113,7 +115,7 @@ class Application void onNativeDropFiles(std::vector const &iPaths); // inline void onNativeWindowPositionChange(int x, int y, float iFontScale, float iFontDpiScale) { fAppContext.onNativeWindowPositionChange(x, y, iFontScale, iFontDpiScale); } - bool newFrame() noexcept; + bool newFrame(std::vector const &iFrameActions) noexcept; bool render() noexcept; void renderMainMenu(); void maybeExit(); @@ -121,7 +123,10 @@ class Application void exit(); ReGui::Dialog &newDialog(std::string iTitle, bool iHighPriority = false); void newExceptionDialog(std::string iMessage, bool iSaveButton, std::exception_ptr const &iException); + ReGui::Notification &newNotification(); + ReGui::Notification &newUniqueNotification(ReGui::Notification::Key const &iKey); void savePreferences(UserError *oErrors = nullptr) noexcept; + AppContext *getAppContext() const { return fAppContext.get(); } constexpr bool hasException() const { return fState == State::kException; } @@ -164,6 +169,7 @@ class Application void renderAppContext(); // may throw exception void renderLoading(); // may throw exception void renderDialog(); + void renderNotifications(ImVec2 const &iOffset); void renderLogoBox(float iPadding = 10.0f); void renderApplicationMenuItems(); void newAboutDialog(); @@ -204,6 +210,7 @@ class Application std::map> fAsyncActions{}; std::vector fNewFrameActions{}; std::vector> fDialogs{}; + std::vector> fNotifications{}; std::unique_ptr fCurrentDialog{}; std::unique_ptr fWelcomeDialog{}; diff --git a/src/cpp/re/edit/Dialog.cpp b/src/cpp/re/edit/Dialog.cpp index 63f8909..f3f22c0 100644 --- a/src/cpp/re/edit/Dialog.cpp +++ b/src/cpp/re/edit/Dialog.cpp @@ -93,12 +93,6 @@ void Dialog::render() needsSameLine = true; } - auto loggingManager = LoggingManager::instance(); - if(loggingManager->isShowDebug()) - { - loggingManager->debug("dialog.ItemRectMin", "%fx%f", ImGui::GetWindowWidth(), ImGui::GetWindowHeight()); - } - ImGui::EndPopup(); } } diff --git a/src/cpp/re/edit/Notification.cpp b/src/cpp/re/edit/Notification.cpp new file mode 100644 index 0000000..f1555f0 --- /dev/null +++ b/src/cpp/re/edit/Notification.cpp @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2023 pongasoft + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * @author Yan Pujante + */ + +#include "Notification.h" +#include "Constants.h" +#include "ReGui.h" + +#include + +namespace re::edit::ReGui { + +//------------------------------------------------------------------------ +// Notification::Notification +//------------------------------------------------------------------------ +Notification::Notification(Key const &iKey) : + fKey{iKey}, + fWindowName{fmt::printf("Notification_%d", kIota++)} +{ +} + +//------------------------------------------------------------------------ +// Notification::render +//------------------------------------------------------------------------ +bool Notification::render() +{ + static ImVec2 kButtonSize{}; + static ImGuiOnceUponAFrame kOaf{}; + + if(fDismissTime && std::chrono::system_clock::now() >= *fDismissTime) + { + dismiss(); + } + + if(!isActive()) + return false; + + if(kOaf) + kButtonSize = ImGui::CalcTextSize(ReGui::kResetIcon); + + bool rendered = false; + + if(ImGui::Begin(fWindowName.c_str(), nullptr, ImGuiWindowFlags_NoTitleBar + | ImGuiWindowFlags_NoResize + | ImGuiWindowFlags_NoCollapse + | ImGuiWindowFlags_NoDocking + | ImGuiWindowFlags_NoSavedSettings)) + { + auto available = ImGui::GetContentRegionAvail() - ImGui::GetStyle().WindowPadding; + auto cp = ImGui::GetCursorPos(); + auto resetButtonX = available.x - kButtonSize.x; + ImGui::PushTextWrapPos(resetButtonX); + for(auto &content: fContent) + { + if(!content->render()) + dismiss(); + } + ImGui::PopTextWrapPos(); + ImGui::SetCursorPos({resetButtonX, cp.y}); + if(ImGui::Button(ReGui::kResetIcon)) + { + dismiss(); + } + rendered = true; + } + ImGui::End(); + + return rendered; +} + +//------------------------------------------------------------------------ +// Notification::lambda +//------------------------------------------------------------------------ +Notification &Notification::lambda(std::function iLambda) +{ + if(iLambda) + { + auto content = std::make_unique(); + content->fLambda = std::move(iLambda); + fContent.emplace_back(std::move(content)); + } + return *this; +} + +//------------------------------------------------------------------------ +// Notification::text +//------------------------------------------------------------------------ +Notification &Notification::text(std::string iText) +{ + return lambda([text = std::move(iText)]() { ReGui::MultiLineText(text); return true; }); +} + +//------------------------------------------------------------------------ +// Notification::LamdaContent::render +//------------------------------------------------------------------------ +bool Notification::LamdaContent::render() +{ + return fLambda(); +} + +} \ No newline at end of file diff --git a/src/cpp/re/edit/Notification.h b/src/cpp/re/edit/Notification.h new file mode 100644 index 0000000..e4e7bbc --- /dev/null +++ b/src/cpp/re/edit/Notification.h @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2023 pongasoft + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * @author Yan Pujante + */ + +#ifndef RE_EDIT_NOTIFICATION_H +#define RE_EDIT_NOTIFICATION_H + +#include +#include +#include +#include + +namespace re::edit::ReGui { + +class Notification +{ +public: + struct Key { + void *fKey{}; + + constexpr void reset() { fKey = nullptr; } + constexpr bool empty() const { return fKey == nullptr; } + + constexpr bool operator==(Key const &rhs) const { return fKey == rhs.fKey; } + constexpr bool operator!=(Key const &rhs) const { return !(rhs == *this); } + + constexpr static Key none() { return {}; } + constexpr static Key from(void *iKey) { return { iKey }; } + }; + + class Content + { + public: + virtual ~Content() = default; + /** + * @return `false` when render determines that the notification should be dismissed */ + virtual bool render() = 0; + }; + + class LamdaContent : public Content + { + public: + bool render() override; + + public: + std::function fLambda{}; + }; + + void dismiss() { fActive = false; } + constexpr bool isActive() const { return fActive; } + constexpr Key const &key() { return fKey; } + +public: + explicit Notification(Key const &iKey = Key::none()); + + /** + * @return `true` if the notification (= ImGui window) was rendered, `false` otherwise */ + bool render(); + + Notification &text(std::string iText); + Notification &lambda(std::function iLambda); + template> + Notification &dismissAfter(std::chrono::duration const &iDuration); + +private: + std::vector> fContent{}; + bool fActive{true}; + + Key fKey; + std::string fWindowName; + std::optional fDismissTime{}; + inline static long kIota = 1; +}; + +//------------------------------------------------------------------------ +// Notification::dismissAfter +//------------------------------------------------------------------------ +template +Notification &Notification::dismissAfter(std::chrono::duration const &iDuration) +{ + fDismissTime = std::chrono::system_clock::now() + iDuration; + return *this; +} + +} + +#endif //RE_EDIT_NOTIFICATION_H \ No newline at end of file diff --git a/src/cpp/re/edit/UIContext.cpp b/src/cpp/re/edit/UIContext.cpp index 72c82e1..8e7fc22 100644 --- a/src/cpp/re/edit/UIContext.cpp +++ b/src/cpp/re/edit/UIContext.cpp @@ -90,17 +90,14 @@ void UIContext::execute(ui_action_t iAction) } } + //------------------------------------------------------------------------ -// UIContext::processUIActions +// UIContext::collectUIActions //------------------------------------------------------------------------ -void UIContext::processUIActions() +std::vector UIContext::collectUIActions() { std::unique_lock lock(fMutex); - auto actions = std::move(fUIActions); - lock.unlock(); - - for(auto &action: actions) - action(); + return std::move(fUIActions); } //------------------------------------------------------------------------ diff --git a/src/cpp/re/edit/UIContext.h b/src/cpp/re/edit/UIContext.h index cd4feb4..3d1cdac 100644 --- a/src/cpp/re/edit/UIContext.h +++ b/src/cpp/re/edit/UIContext.h @@ -48,7 +48,8 @@ class UIContext * synchronously. Otherwise, the action is enqueued and will be executed on the UI thread in the next frame loop. */ void execute(ui_action_t iAction); - void processUIActions(); + inline bool hasUIActions() const { return !fUIActions.empty(); } + std::vector collectUIActions(); void beginFXShader(ImVec4 const &iTint, float iBrightness, float iContrast); void endFXShader(); diff --git a/src/cpp/re/edit/platform/main.cpp b/src/cpp/re/edit/platform/main.cpp index d5874fc..4e24029 100644 --- a/src/cpp/re/edit/platform/main.cpp +++ b/src/cpp/re/edit/platform/main.cpp @@ -112,11 +112,8 @@ int doMain(int argc, char **argv) // float clear_color[4] = {0.55f, 0.55f, 0.55f, 1.00f}; ClearBackground(Color{127, 127, 127, 255}); - // Execute all actions requiring the ui thread - uiContext.processUIActions(); - // Before New Frame - if(application.newFrame()) + if(application.newFrame(uiContext.collectUIActions())) { rlImGuiBegin(); // Main rendering