diff --git a/README.md b/README.md index fd1349d0b..281f759de 100644 --- a/README.md +++ b/README.md @@ -28,13 +28,6 @@ You can open the folder from within REAPER via [Options] -> [Show REAPER resource path in explorer/finder...] ``` -### REAPER Language Packs - -Please note that the EAR Production Suite (specifically the `reaper_adm` extension) is currently incompatible with REAPER Language Packs. -Attempting to use the extension with a REAPER installation which has a Language Pack installed may cause REAPER to fail to load. - -This will be addressed in a future version. - ### OS X Gatekeeper On macOS Catalina or above you may experience plugin load errors due to the new Gatekeeper feature. You can disable Gatekeeper globally as per [this site](https://cronotek.net/blog/how-to-disable-gatekeeper-on-macos-mojave-and-catalina) diff --git a/reaper-adm-extension/src/reaper_adm/menu.cpp b/reaper-adm-extension/src/reaper_adm/menu.cpp index de40c9217..f2c367fa6 100644 --- a/reaper-adm-extension/src/reaper_adm/menu.cpp +++ b/reaper-adm-extension/src/reaper_adm/menu.cpp @@ -1,13 +1,35 @@ #include "menu.h" #include +#include #include #include "reaperapi.h" #include +#include using namespace admplug; using namespace admplug::detail; namespace { + std::string bufferToString(char const *buffer) { assert(buffer); return buffer; } + +#ifdef WIN32 + using GetMenuItemInfoT = MENUITEMINFOW; + auto GetMenuItemInfoFn = GetMenuItemInfoW; + + std::string bufferToString(wchar_t const* wstr) { + int count = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, NULL, 0, NULL, NULL); + std::vector buffer(count, 0); + WideCharToMultiByte(CP_UTF8, 0, wstr, -1, &buffer[0], count, NULL, NULL); + return bufferToString(buffer.data()); + } +#else + using GetMenuItemInfoT = MENUITEMINFO; + auto GetMenuItemInfoFn = GetMenuItemInfo; +#endif + + using CharT = std::remove_pointer_t().dwTypeData)>; + using BufferT = std::vector; + enum class ReaperMenuState { Initialising = 0, AboutToShow = 1 @@ -23,44 +45,96 @@ namespace { return info; } + std::optional getMenuString(HMENU menu, int position) { + GetMenuItemInfoT info{}; + info.cbSize = sizeof(GetMenuItemInfoT); + info.fType = MFT_STRING; + info.fMask = MIIM_TYPE; + // nullptr here means query string size & store in info.cch + info.dwTypeData = nullptr; + auto success = GetMenuItemInfoFn(menu, position, true, &info); + if (success) { + // query doesn't seem to work with WDL shim, 255 bytes should be enough + // for a short menu string. Truncates if too short. + if(info.cch == 0) { + info.cch = 255; + } + // returned size does not include null terminator so increment + BufferT buffer(++info.cch, '\0'); + info.dwTypeData = &buffer[0]; + // get actual string data + success = GetMenuItemInfoFn(menu, position, true, &info); + if(success) { + return bufferToString(buffer.data()); + } + } + return {}; + } + bool supportsLocalisation(ReaperAPI const &api) { + return std::stof(api.GetAppVersion()) >= 6.11f; + } + + std::string localise(std::string const &name, std::string const §ion, + ReaperAPI const &api) { + return api.LocalizeString(name.c_str(), section.c_str(), 0); + } + + void checkDefaultItemIndex(int defaultIndex, int actualIndex) { + assert(defaultIndex == actualIndex); + } + + void checkDefaultItemIndex(std::optional defaultIndex, int actualIndex) { + if(defaultIndex) { + checkDefaultItemIndex(*defaultIndex, actualIndex); + } + } + int findPositionOfItemWithText(HMENU menu, std::string textOrig) { - std::string text(textOrig); - boost::erase_all(text, "&"); - std::vector buffer(256, '\0'); - auto itemCount = GetMenuItemCount(menu); - for (int itemPosition = 0; itemPosition != itemCount; ++itemPosition) { - buffer[0] = '\0'; - auto info = getStringMenuInfoStruct(&buffer[0], buffer.size()); - if (GetMenuItemInfo(menu, itemPosition, true, &info)) { - std::string menuText = buffer.data(); - boost::erase_all(menuText, "&"); // Win returns amp preceding kbd shortcut char - strip it for comparison - if (menuText == text) { - return itemPosition; - } - } + boost::erase_all(textOrig, "&"); + auto itemCount = GetMenuItemCount(menu); + for (int itemPosition = 0; itemPosition != itemCount; ++itemPosition) { + if (auto menuText = getMenuString(menu, itemPosition)) { + boost::erase_all(*menuText, + "&"); // Win returns amp preceding kbd shortcut + // char - strip it for comparison + if (*menuText == textOrig) { + return itemPosition; + } } - return -1; + } + return -1; + } + + int constrain(int val, int max) { + return std::max(std::min(val, max), 0); + } + + int findInsertionPosition(HMENU menu, std::string itemName, int offset, std::optional fallbackPosition) { + auto itemCount = GetMenuItemCount(menu); + auto index = findPositionOfItemWithText(menu, itemName); + if (index >= 0) { + checkDefaultItemIndex(fallbackPosition, index); + return constrain(itemCount, index + offset); + } + return fallbackPosition ? constrain(*fallbackPosition + offset, itemCount) + : itemCount; } - HMENU findHmenuOfItemWithText(HMENU menu, std::string textOrig) { - std::string text(textOrig); - boost::erase_all(text, "&"); - std::vector buffer(256, '\0'); + HMENU findHmenuOfItemWithText(HMENU menu, std::string text) { + auto position = findPositionOfItemWithText(menu, text); + if (position >= 0) { + return GetSubMenu(menu, position); + } + return nullptr; + } + + HMENU findHmenuOfItemWithPosition(HMENU menu, int itemPosition) { auto itemCount = GetMenuItemCount(menu); - for (int itemPosition = 0; itemPosition != itemCount; ++itemPosition) { - buffer[0] = '\0'; - auto info = getStringMenuInfoStruct(&buffer[0], buffer.size()); - if (GetMenuItemInfo(menu, itemPosition, true, &info)) { - std::string menuText = buffer.data(); - boost::erase_all(menuText, "&"); // Win returns amp preceding kbd shortcut char - strip it for comparison - if (menuText == text) { - return info.hSubMenu; - } - } + if (itemPosition < (itemCount - 1)) { + return GetSubMenu(menu, itemPosition); } return nullptr; } - } Insertable::Insertable(std::shared_ptr item, std::shared_ptr inserter) : @@ -90,9 +164,6 @@ void Insertable::update(HMENU menu) item->update(menu); } -namespace { -} - MenuAction::MenuAction(std::string menuText, std::shared_ptr action) : action{action}, text{menuText.begin(), menuText.end()}, @@ -185,11 +256,25 @@ void RawMenu::init() } } -std::shared_ptr RawMenu::getMenuByText(std::string menuText) -{ - auto hmenu = findHmenuOfItemWithText(hMenu, menuText); - if(hmenu == nullptr) return nullptr; - return std::make_shared(hmenu); +std::shared_ptr RawMenu::getMenuByText(std::string menuText, + std::string section, + int fallbackPosition, + ReaperAPI const &api) { + if (std::stof(api.GetAppVersion()) >= 6.11f) { + auto localised = localise(menuText, section, api); + if(!localised.empty()) { + menuText = localised; +} + } + auto submenu = findHmenuOfItemWithText(hMenu, menuText); + if(submenu) { + checkDefaultItemIndex(fallbackPosition, positionOfItemWithText(menuText)); + } + if(!submenu) { + submenu = findHmenuOfItemWithPosition(hMenu, fallbackPosition); + } + if(!submenu) return nullptr; + return std::make_shared(submenu); } void RawMenu::update(std::string, HMENU) @@ -204,6 +289,9 @@ void RawMenu::insert(std::unique_ptr item, std::shared_ptr= 0) { - return std::min(itemCount, index + 1); - } - return itemCount; + return findInsertionPosition(menu, itemName, 1, fallbackPosition); } BeforeNamedItem::BeforeNamedItem(std::string itemName) : itemName{itemName} @@ -358,9 +449,13 @@ BeforeNamedItem::BeforeNamedItem(std::string itemName) : itemName{itemName} } +BeforeNamedItem::BeforeNamedItem(std::string itemName, std::string section, + int fallbackPosition, ReaperAPI const &api) + : itemName{supportsLocalisation(api) ? localise(itemName, section, api) + : itemName}, + fallbackPosition{fallbackPosition} {} + int BeforeNamedItem::getIndex(HMENU menu) const { - auto index = findPositionOfItemWithText(menu, itemName); - index = std::max(0, index); - return index; -} \ No newline at end of file + return findInsertionPosition(menu, itemName, 0, fallbackPosition); +} diff --git a/reaper-adm-extension/src/reaper_adm/menu.h b/reaper-adm-extension/src/reaper_adm/menu.h index 676d1c1c8..06150893e 100644 --- a/reaper-adm-extension/src/reaper_adm/menu.h +++ b/reaper-adm-extension/src/reaper_adm/menu.h @@ -2,6 +2,7 @@ #include #include #include +#include #include "actionmanager.h" namespace admplug { @@ -44,7 +45,6 @@ class MenuContainer { class TopLevelMenu : public MenuContainer { public: - virtual ~TopLevelMenu() = default; virtual void init(std::string menuId, HMENU menu) = 0; virtual void update(std::string menuId, HMENU menu) = 0; }; @@ -65,7 +65,7 @@ class MenuAction : public MenuItem { class SubMenu : public MenuContainer, public MenuItem { public: - SubMenu(std::string menuText); + explicit SubMenu(std::string menuText); void addTo(HMENU menu, int position) override; void insert(std::unique_ptr item, std::shared_ptr inserter) override; void update(HMENU menu) override; @@ -80,7 +80,7 @@ class SubMenu : public MenuContainer, public MenuItem { class ReaperMenu : public TopLevelMenu { public: - ReaperMenu(std::string menuId); + explicit ReaperMenu(std::string menuId); void init(std::string menuId, HMENU menu) override; void update(std::string menuId, HMENU menu) override; void insert(std::unique_ptr item, std::shared_ptr inserter) override; @@ -91,11 +91,15 @@ class ReaperMenu : public TopLevelMenu { class RawMenu : public TopLevelMenu { public: - RawMenu(HMENU menuHandle); + explicit RawMenu(HMENU menuHandle); void init(std::string menuId, HMENU menu) override; void update(std::string menuId, HMENU menu) override; void insert(std::unique_ptr item, std::shared_ptr inserter) override; - std::shared_ptr getMenuByText(std::string menuText); + std::shared_ptr getMenuByText(std::string menuText, + std::string section, + int fallbackPosition, + ReaperAPI const &api); + int positionOfItemWithText(std::string text) const; void init(); private: HMENU hMenu; @@ -105,7 +109,7 @@ class RawMenu : public TopLevelMenu { class StartOffset : public MenuInserter { public: - StartOffset(std::size_t offset); + explicit StartOffset(std::size_t offset); int getIndex(HMENU) const override; private: std::size_t offset; @@ -113,7 +117,7 @@ class StartOffset : public MenuInserter { class EndOffset : public MenuInserter { public: - EndOffset(std::size_t offset); + explicit EndOffset(std::size_t offset); int getIndex(HMENU) const override; private: std::size_t offset; @@ -121,19 +125,24 @@ class EndOffset : public MenuInserter { class AfterNamedItem : public MenuInserter { public: - AfterNamedItem(std::string itemName); + explicit AfterNamedItem(std::string itemName); + AfterNamedItem(std::string itemName, std::string section, + int fallbackPosition, ReaperAPI const &api); int getIndex(HMENU) const override; private: std::string itemName; + std::optional fallbackPosition; }; class BeforeNamedItem : public MenuInserter { public: - BeforeNamedItem(std::string itemName); + explicit BeforeNamedItem(std::string itemName); + BeforeNamedItem(std::string itemName, std::string section, + int fallbackPosition, ReaperAPI const &api); int getIndex(HMENU) const override; private: std::string itemName; - + std::optional fallbackPosition; }; enum class MenuID { @@ -156,13 +165,10 @@ class MenuManager { private: MenuManager(ReaperAPI const* api, reaper_plugin_info_t *rec); - void init(std::string menuId, HMENU menu); std::vector> menus; std::map menuIdentifiers {{ {MenuID::MEDIA_ITEM_CONTEXT, "Media item context"} }}; HMENU mainMenu; }; - - } diff --git a/reaper-adm-extension/src/reaper_adm/pluginmain.cpp b/reaper-adm-extension/src/reaper_adm/pluginmain.cpp index 86d118c60..be3bf69d4 100644 --- a/reaper-adm-extension/src/reaper_adm/pluginmain.cpp +++ b/reaper-adm-extension/src/reaper_adm/pluginmain.cpp @@ -1,6 +1,8 @@ #define WIN32_LEAN_AND_MEAN #include +#include +#include #include "reaperhost.h" #include "reaperapi.h" #include "actionmanager.h" @@ -49,6 +51,24 @@ struct CrtBreakAllocSetter { CrtBreakAllocSetter g_crtBreakAllocSetter; */ +namespace { +#ifdef WIN32 +const std::map defaultMenuPositions = { + {"&File", 0}, + {"&Insert", 3}, + {"Project &templates", 8}, + {"Empty item", 2}, + {"Group", 4}}; +} +#else +const std::map defaultMenuPositions = { + {"&File", 1}, + {"&Insert", 4}, + {"Project &templates", 8}, + {"Empty item", 2}, + {"Group", 4}}; +} +#endif extern "C" { int REAPER_PLUGIN_DLL_EXPORT REAPER_PLUGIN_ENTRYPOINT(REAPER_PLUGIN_HINSTANCE hInstance, reaper_plugin_info_t *rec) @@ -75,7 +95,7 @@ extern "C" { return 0; } - if(!rec) { + if (!rec) { // unload plugin return 0; } @@ -130,8 +150,10 @@ extern "C" { } auto mediaContextMenu = reaper->getMenu(MenuID::MEDIA_ITEM_CONTEXT); - mediaContextMenu->insert(std::move(admContextMenu), std::make_shared("Group")); - + mediaContextMenu->insert( + std::move(admContextMenu), + std::make_shared("Group", "common", + defaultMenuPositions.at("Group"), *api)); // File menu auto admFileMenu = std::make_unique("Create project from ADM file"); @@ -174,9 +196,14 @@ extern "C" { admFileMenu->insert(std::move(explodeItem), std::make_shared(0)); } - auto reaperFileMenu = reaperMainMenu->getMenuByText("File"); + auto projectTemplateString = "Project &templates"; + auto admFileMenuInserter = std::make_shared( + projectTemplateString, "MENU_102", + defaultMenuPositions.at(projectTemplateString), *api); + auto reaperFileMenu = reaperMainMenu->getMenuByText( + "&File", "common", defaultMenuPositions.at("&File"), *api); assert(reaperFileMenu); - reaperFileMenu->insert(std::move(admFileMenu), std::make_shared("Project templates")); + reaperFileMenu->insert(std::move(admFileMenu), admFileMenuInserter); reaperFileMenu->init(); // Insert menu @@ -217,9 +244,14 @@ extern "C" { admInsertMenu->insert(std::move(explodeItem), std::make_shared(0)); } - auto reaperInsertMenu = reaperMainMenu->getMenuByText("Insert"); + auto reaperInsertMenu = reaperMainMenu->getMenuByText( + "&Insert", "common", defaultMenuPositions.at("&Insert"), *api); + assert(reaperInsertMenu); + reaperInsertMenu->insert(std::move(admInsertMenu), + std::make_shared( + "Empty item", "MENU_102", + defaultMenuPositions.at("Empty item"), *api)); assert(reaperInsertMenu); - reaperInsertMenu->insert(std::move(admInsertMenu), std::make_shared("Empty item")); reaperInsertMenu->init(); return 1; diff --git a/reaper-adm-extension/src/reaper_adm/reaperapi.h b/reaper-adm-extension/src/reaper_adm/reaperapi.h index 84ebeba52..f4f8c5488 100644 --- a/reaper-adm-extension/src/reaper_adm/reaperapi.h +++ b/reaper-adm-extension/src/reaper_adm/reaperapi.h @@ -153,6 +153,7 @@ class ReaperAPI { virtual MediaItem* GetTrackMediaItem(MediaTrack* tr, int itemidx) const = 0; virtual double GetSetProjectInfo(ReaProject * project, const char* desc, double value, bool is_set) const = 0; virtual const char* GetAppVersion() const = 0; + virtual const char* LocalizeString(const char* src_string, const char* section, int flagsOptional) const = 0; static constexpr int RENDER_ITEMS_AS_NEW_TAKE_ID = 41999; static constexpr int CROP_TO_ACTIVE_TAKE_IN_ITEMS = 40131; diff --git a/reaper-adm-extension/src/reaper_adm/reaperapiimpl.cpp b/reaper-adm-extension/src/reaper_adm/reaperapiimpl.cpp index a7393ff26..1087a08d3 100644 --- a/reaper-adm-extension/src/reaper_adm/reaperapiimpl.cpp +++ b/reaper-adm-extension/src/reaper_adm/reaperapiimpl.cpp @@ -493,6 +493,11 @@ const char* admplug::ReaperAPIImpl::GetAppVersion() const return ::GetAppVersion(); } +const char* admplug::ReaperAPIImpl::LocalizeString(const char* src_string, const char* section, int flagsOptional) const +{ + return ::LocalizeString(src_string, section, flagsOptional); +} + void ReaperAPIImpl::UpdateArrangeForAutomation() const { // UpdateArrange() does not force draw of automation. Flipping master track visibility does. std::bitset<4> mstTrkSetting(GetMasterTrackVisibility()); diff --git a/reaper-adm-extension/src/reaper_adm/reaperapiimpl.h b/reaper-adm-extension/src/reaper_adm/reaperapiimpl.h index ca4449e42..e0a98339c 100644 --- a/reaper-adm-extension/src/reaper_adm/reaperapiimpl.h +++ b/reaper-adm-extension/src/reaper_adm/reaperapiimpl.h @@ -111,6 +111,7 @@ class ReaperAPIImpl : public ReaperAPI MediaItem* GetTrackMediaItem(MediaTrack* tr, int itemidx) const override; double GetSetProjectInfo(ReaProject* project, const char* desc, double value, bool is_set) const override; const char* GetAppVersion() const override; + const char* LocalizeString(const char* src_string, const char* section, int flagsOptional) const override; //Custom Funcs void UpdateArrangeForAutomation() const override; diff --git a/reaper-adm-extension/test/reaper_adm/mocks/reaperapi.h b/reaper-adm-extension/test/reaper_adm/mocks/reaperapi.h index 0d07dda94..a5699d532 100644 --- a/reaper-adm-extension/test/reaper_adm/mocks/reaperapi.h +++ b/reaper-adm-extension/test/reaper_adm/mocks/reaperapi.h @@ -185,6 +185,7 @@ class MockReaperAPI : public ReaperAPI { MOCK_CONST_METHOD2(GetTrackMediaItem, MediaItem*(MediaTrack* tr, int itemidx)); MOCK_CONST_METHOD4(GetSetProjectInfo, double(ReaProject* project, const char* desc, double value, bool is_set)); MOCK_CONST_METHOD0(GetAppVersion, const char*()); + MOCK_CONST_METHOD3(LocalizeString, const char*(const char* src_string, const char* section, int flagsOptional)); // Custom funcs MOCK_CONST_METHOD0(UpdateArrangeForAutomation, diff --git a/submodules/vcpkg b/submodules/vcpkg index b1b480822..cdd51899f 160000 --- a/submodules/vcpkg +++ b/submodules/vcpkg @@ -1 +1 @@ -Subproject commit b1b4808228377515acd849b8706ee2ad25ba455d +Subproject commit cdd51899f6ae3736bfbe936f9e387f86d4a63a58