diff --git a/src/Internat.cpp b/src/Internat.cpp index 6a5c5f97a..687373cec 100644 --- a/src/Internat.cpp +++ b/src/Internat.cpp @@ -229,3 +229,12 @@ wxString Internat::StripAccelerators(const wxString &s) } return result; } + +wxString Internat::Parenthesize(const wxString &str) +{ + /* i18n-hint: An opening parenthesis, in some languages a right parenthesis */ + auto open = _("("); + /* i18n-hint: A closing parenthesis, in some languages a left parenthesis */ + auto close = _(")"); + return open + str + close; +} diff --git a/src/Internat.h b/src/Internat.h index 1a0306096..dd8465b81 100644 --- a/src/Internat.h +++ b/src/Internat.h @@ -73,6 +73,8 @@ public: * when they aren't, saving translators effort. */ static wxString StripAccelerators(const wxString& str); + static wxString Parenthesize(const wxString &str); + private: static wxChar mDecimalSeparator; diff --git a/src/Project.cpp b/src/Project.cpp index 5cc178c1e..b87c47050 100644 --- a/src/Project.cpp +++ b/src/Project.cpp @@ -545,7 +545,7 @@ AudacityProject *CreateNewAudacityProject() // Okay, GetActiveProject() is ready. Now we can get its CommandManager, // and add the shortcut keys to the tooltips. - p->GetControlToolBar()->RegenerateToolsTooltips(); + p->GetToolManager()->RegenerateTooltips(); ModuleManager::Get().Dispatch(ProjectInitialized); diff --git a/src/Project.h b/src/Project.h index f485f1bd8..29d9173e6 100644 --- a/src/Project.h +++ b/src/Project.h @@ -78,7 +78,6 @@ class Scrubber; class ScrubbingToolBar; class SelectionBar; class SpectralSelectionBar; -class Toolbar; class ToolManager; class ToolsToolBar; class TranscriptionToolBar; diff --git a/src/TrackPanel.cpp b/src/TrackPanel.cpp index 44e00f498..0aad99f64 100644 --- a/src/TrackPanel.cpp +++ b/src/TrackPanel.cpp @@ -491,6 +491,9 @@ TrackPanel::TrackPanel(wxWindow * parent, wxWindowID id, mAdjustLeftSelectionCursor = std::make_unique(wxCURSOR_POINT_LEFT); mAdjustRightSelectionCursor = std::make_unique(wxCURSOR_POINT_RIGHT); + // Menu pointers are set to NULL here. Delay building of menus until after + // the command managter is finished, so that we can look up shortcut + // key strings that need to appear in some of the popup menus. mWaveTrackMenu = NULL; mChannelItemsInsertionPoint = 0; @@ -500,8 +503,6 @@ TrackPanel::TrackPanel(wxWindow * parent, wxWindowID id, mRulerWaveformMenu = mRulerSpectrumMenu = NULL; - BuildMenus(); - mTrackArtist = new TrackArtist(); mTrackArtist->SetInset(1, kTopMargin, kRightMargin, kBottomMargin); @@ -586,6 +587,12 @@ TrackPanel::~TrackPanel() delete mInitialTrackSelection; } +void TrackPanel::BuildMenusIfNeeded(void) +{ + if (!mRateMenu) + BuildMenus(); +} + void TrackPanel::BuildMenus(void) { // Get rid of existing menus @@ -676,6 +683,8 @@ void TrackPanel::BuildCommonDropMenuItems(wxMenu * menu) { menu->Append(OnSetNameID, _("&Name...")); menu->AppendSeparator(); + // It is not correct to use KeyStringDisplay here -- wxWidgets will apply + // its equivalent to the key names passed to menu functions. menu->Append(OnMoveUpID, _("Move Track &Up") + wxT("\t") + (GetProject()->GetCommandManager()->GetKeyFromName(wxT("TrackMoveUp")))); menu->Append(OnMoveDownID, _("Move Track &Down") + wxT("\t") + @@ -708,6 +717,8 @@ void TrackPanel::DeleteMenus(void) { // Note that the submenus (mRateMenu, ...) // are deleted by their parent + mRateMenu = mFormatMenu = nullptr; + if (mWaveTrackMenu) { delete mWaveTrackMenu; mWaveTrackMenu = NULL; @@ -7480,6 +7491,8 @@ void TrackPanel::ScrollIntoView(int x) void TrackPanel::OnTrackMenu(Track *t) { + BuildMenusIfNeeded(); + if(!t) { t = GetFocusedTrack(); if(!t) return; @@ -8083,6 +8096,8 @@ void TrackPanel::SetRate(Track * pTrack, double rate) /// track menu. void TrackPanel::OnFormatChange(wxCommandEvent & event) { + BuildMenusIfNeeded(); + int id = event.GetId(); wxASSERT(id >= On16BitID && id <= OnFloatID); wxASSERT(mPopupMenuTarget @@ -8174,6 +8189,8 @@ static int gRates[nRates] = { 8000, 11025, 16000, 22050, 44100, 48000, 88200, 96 /// submenu of the track menu, except for "Other" (/see OnRateOther). void TrackPanel::OnRateChange(wxCommandEvent & event) { + BuildMenusIfNeeded(); + int id = event.GetId(); wxASSERT(id >= OnRate8ID && id <= OnRate384ID); wxASSERT(mPopupMenuTarget @@ -8199,6 +8216,8 @@ int TrackPanel::IdOfRate( int rate ) void TrackPanel::OnRateOther(wxCommandEvent &event) { + BuildMenusIfNeeded(); + wxASSERT(mPopupMenuTarget && mPopupMenuTarget->GetKind() == Track::Wave); diff --git a/src/TrackPanel.h b/src/TrackPanel.h index 49f5dd355..25c8de333 100644 --- a/src/TrackPanel.h +++ b/src/TrackPanel.h @@ -147,6 +147,7 @@ class AUDACITY_DLL_API TrackPanel final : public OverlayPanel { virtual ~ TrackPanel(); + virtual void BuildMenusIfNeeded(void); virtual void BuildMenus(void); virtual void DeleteMenus(void); @@ -758,20 +759,20 @@ protected: mStretchCursor, mStretchLeftCursor, mStretchRightCursor; #endif - wxMenu *mWaveTrackMenu; - size_t mChannelItemsInsertionPoint; + wxMenu *mWaveTrackMenu {}; + size_t mChannelItemsInsertionPoint {}; - wxMenu *mNoteTrackMenu; - wxMenu *mTimeTrackMenu; - wxMenu *mLabelTrackMenu; - wxMenu *mRateMenu; - wxMenu *mFormatMenu; - wxMenu *mLabelTrackInfoMenu; + wxMenu *mNoteTrackMenu {}; + wxMenu *mTimeTrackMenu {}; + wxMenu *mLabelTrackMenu {}; + wxMenu *mRateMenu {}; + wxMenu *mFormatMenu {}; + wxMenu *mLabelTrackInfoMenu {}; - wxMenu *mRulerWaveformMenu; - wxMenu *mRulerSpectrumMenu; + wxMenu *mRulerWaveformMenu {}; + wxMenu *mRulerSpectrumMenu {}; - Track *mPopupMenuTarget; + Track *mPopupMenuTarget {}; friend class TrackPanelAx; diff --git a/src/commands/Keyboard.cpp b/src/commands/Keyboard.cpp index 194e2a138..6b052bd0e 100644 --- a/src/commands/Keyboard.cpp +++ b/src/commands/Keyboard.cpp @@ -43,13 +43,25 @@ wxString KeyStringNormalize(const wxString & key) #endif } -wxString KeyStringDisplay(const wxString & key) +wxString KeyStringDisplay(const wxString & key, bool useSspecialChars) { wxString newkey = KeyStringNormalize(key); #if defined(__WXMAC__) - newkey.Replace(wxT("XCtrl+"), wxT("Control+")); - newkey.Replace(wxT("Alt+"), wxT("Option+")); - newkey.Replace(wxT("Ctrl+"), wxT("Command+")); + + if (!useSspecialChars) { + // Compose user-visible keystroke names, all ASCII + newkey.Replace(wxT("XCtrl+"), wxT("Control+")); + newkey.Replace(wxT("Alt+"), wxT("Option+")); + newkey.Replace(wxT("Ctrl+"), wxT("Command+")); + } + else { + // Compuse user-visible keystroke names, with special characters + newkey.Replace(wxT("Shift+"), wxT("\u21e7")); + newkey.Replace(wxT("XCtrl+"), wxT("Control+")); + newkey.Replace(wxT("Alt+"), wxT("\u2325")); + newkey.Replace(wxT("Ctrl+"), wxT("\u2318")); + } + #endif return newkey; diff --git a/src/commands/Keyboard.h b/src/commands/Keyboard.h index 2f3ce7559..4f39e6663 100644 --- a/src/commands/Keyboard.h +++ b/src/commands/Keyboard.h @@ -14,5 +14,5 @@ #include wxString KeyStringNormalize(const wxString & key); -wxString KeyStringDisplay(const wxString & key); +wxString KeyStringDisplay(const wxString & key, bool useSpecialChars = false); wxString KeyEventToKeyString(const wxKeyEvent & keyEvent); diff --git a/src/toolbars/ControlToolBar.cpp b/src/toolbars/ControlToolBar.cpp index 720cb5fd6..4ead8e117 100644 --- a/src/toolbars/ControlToolBar.cpp +++ b/src/toolbars/ControlToolBar.cpp @@ -188,7 +188,7 @@ void ControlToolBar::Populate() mRecord->FollowModifierKeys(); #if wxUSE_TOOLTIPS - RegenerateToolsTooltips(); + RegenerateTooltips(); wxToolTip::Enable(true); wxToolTip::SetDelay(1000); #endif @@ -197,47 +197,41 @@ void ControlToolBar::Populate() ArrangeButtons(); } -void ControlToolBar::RegenerateToolsTooltips() +void ControlToolBar::RegenerateTooltips() { #if wxUSE_TOOLTIPS + std::vector commands; for (long iWinID = ID_PLAY_BUTTON; iWinID < BUTTON_COUNT; iWinID++) { - wxWindow* pCtrl = this->FindWindow(iWinID); - wxString strToolTip = pCtrl->GetLabel(); - AudacityProject* pProj = GetActiveProject(); - CommandManager* pCmdMgr = (pProj) ? pProj->GetCommandManager() : NULL; - if (pCmdMgr) + commands.clear(); + auto pCtrl = static_cast(this->FindWindow(iWinID)); + commands.push_back(pCtrl->GetLabel()); + switch (iWinID) { - wxString strKey(wxT(" (")); - switch (iWinID) - { - case ID_PLAY_BUTTON: - strKey += pCmdMgr->GetKeyFromName(wxT("Play")); - strKey += _(") / Loop Play ("); - strKey += pCmdMgr->GetKeyFromName(wxT("PlayLooped")); - break; - case ID_RECORD_BUTTON: - strKey += pCmdMgr->GetKeyFromName(wxT("Record")); - strKey += _(") / Append Record ("); - strKey += pCmdMgr->GetKeyFromName(wxT("RecordAppend")); - break; - case ID_PAUSE_BUTTON: - strKey += pCmdMgr->GetKeyFromName(wxT("Pause")); - break; - case ID_STOP_BUTTON: - strKey += pCmdMgr->GetKeyFromName(wxT("Stop")); - break; - case ID_FF_BUTTON: - strKey += pCmdMgr->GetKeyFromName(wxT("SkipEnd")); - break; - case ID_REW_BUTTON: - strKey += pCmdMgr->GetKeyFromName(wxT("SkipStart")); - break; - } - strKey += wxT(")"); - strToolTip += strKey; + case ID_PLAY_BUTTON: + commands.push_back(wxT("Play")); + commands.push_back(_("Loop Play")); + commands.push_back(wxT("PlayLooped")); + break; + case ID_RECORD_BUTTON: + commands.push_back(wxT("Record")); + commands.push_back(_("Append Record")); + commands.push_back(wxT("RecordAppend")); + break; + case ID_PAUSE_BUTTON: + commands.push_back(wxT("Pause")); + break; + case ID_STOP_BUTTON: + commands.push_back(wxT("Stop")); + break; + case ID_FF_BUTTON: + commands.push_back(wxT("SkipEnd")); + break; + case ID_REW_BUTTON: + commands.push_back(wxT("SkipStart")); + break; } - pCtrl->SetToolTip(strToolTip); + ToolBar::SetButtonToolTip(*pCtrl, commands); } #endif } @@ -262,14 +256,14 @@ void ControlToolBar::UpdatePrefs() if( updated ) { - ReCreateButtons(); // side effect: calls RegenerateToolsTooltips() + ReCreateButtons(); // side effect: calls RegenerateTooltips() Updated(); } else // The other reason to regenerate tooltips is if keyboard shortcuts for // transport buttons changed, but that's too much work to check for, so just // always do it. (Much cheaper than calling ReCreateButtons() in all cases. - RegenerateToolsTooltips(); + RegenerateTooltips(); // Set label to pull in language change @@ -383,7 +377,7 @@ void ControlToolBar::ReCreateButtons() EnableDisableButtons(); - RegenerateToolsTooltips(); + RegenerateTooltips(); } void ControlToolBar::Repaint( wxDC *dc ) diff --git a/src/toolbars/ControlToolBar.h b/src/toolbars/ControlToolBar.h index 29fc21893..9171dfcef 100644 --- a/src/toolbars/ControlToolBar.h +++ b/src/toolbars/ControlToolBar.h @@ -101,7 +101,7 @@ class ControlToolBar final : public ToolBar { void EnableDisableButtons() override; void ReCreateButtons() override; - void RegenerateToolsTooltips(); + void RegenerateTooltips() override; int WidthForStatusBar(wxStatusBar* const); void UpdateStatusBar(AudacityProject *pProject); diff --git a/src/toolbars/DeviceToolBar.h b/src/toolbars/DeviceToolBar.h index 261d3ccb6..c1a114119 100644 --- a/src/toolbars/DeviceToolBar.h +++ b/src/toolbars/DeviceToolBar.h @@ -63,7 +63,7 @@ class DeviceToolBar final : public ToolBar { void SetDevices(const DeviceSourceMap *in, const DeviceSourceMap *out); void RepositionCombos(); void SetNames(); - void RegenerateTooltips(); + void RegenerateTooltips() override; void ShowComboDialog(wxChoice *combo, const wxString &title); diff --git a/src/toolbars/EditToolBar.cpp b/src/toolbars/EditToolBar.cpp index 2dca40e34..009fc6b71 100644 --- a/src/toolbars/EditToolBar.cpp +++ b/src/toolbars/EditToolBar.cpp @@ -198,24 +198,40 @@ void EditToolBar::UpdatePrefs() void EditToolBar::RegenerateTooltips() { #if wxUSE_TOOLTIPS - mButtons[ETBCutID]->SetToolTip(_("Cut")); - mButtons[ETBCopyID]->SetToolTip(_("Copy")); - mButtons[ETBPasteID]->SetToolTip(_("Paste")); - mButtons[ETBTrimID]->SetToolTip(_("Trim Audio")); - mButtons[ETBSilenceID]->SetToolTip(_("Silence Audio")); - mButtons[ETBUndoID]->SetToolTip(_("Undo")); - mButtons[ETBRedoID]->SetToolTip(_("Redo")); - #ifdef EXPERIMENTAL_SYNC_LOCK - mButtons[ETBSyncLockID]->SetToolTip(_("Sync-Lock Tracks")); - #endif - mButtons[ETBZoomInID]->SetToolTip(_("Zoom In")); - mButtons[ETBZoomOutID]->SetToolTip(_("Zoom Out")); - mButtons[ETBZoomSelID]->SetToolTip(_("Fit Selection")); - mButtons[ETBZoomFitID]->SetToolTip(_("Fit Project")); + static const struct Entry { + int tool; + wxString commandName; + wxString untranslatedLabel; + } table[] = { + { ETBCutID, wxT("Cut"), XO("Cut") }, + { ETBCopyID, wxT("Copy"), XO("Copy") }, + { ETBPasteID, wxT("Paste"), XO("Paste") }, + { ETBTrimID, wxT("Trim"), XO("Trim Audio") }, + { ETBSilenceID, wxT("Silence"), XO("Silence Audio") }, + { ETBUndoID, wxT("Undo"), XO("Undo") }, + { ETBRedoID, wxT("Redo"), XO("Redo") }, + +#ifdef EXPERIMENTAL_SYNC_LOCK + { ETBSyncLockID, wxT("SyncLock"), XO("Sync-Lock Tracks") }, +#endif + + { ETBZoomInID, wxT("ZoomIn"), XO("Zoom In") }, + { ETBZoomOutID, wxT("ZoomOut"), XO("Zoom Out") }, + { ETBZoomSelID, wxT("ZoomSel"), XO("Fit Selection") }, + { ETBZoomFitID, wxT("FitInWindow"), XO("Fit Project") }, #if defined(EXPERIMENTAL_EFFECTS_RACK) - mButtons[ETBEffectsID]->SetToolTip(_("Open Effects Rack")); + { ETBEffectsID, wxT(""), XO("Open Effects Rack") }, #endif + }; + + std::vector commands; + for (const auto &entry : table) { + commands.clear(); + commands.push_back(wxGetTranslation(entry.untranslatedLabel)); + commands.push_back(entry.commandName); + ToolBar::SetButtonToolTip(*mButtons[entry.tool], commands); + } #endif } diff --git a/src/toolbars/EditToolBar.h b/src/toolbars/EditToolBar.h index 7f3496b51..aedf2a1bd 100644 --- a/src/toolbars/EditToolBar.h +++ b/src/toolbars/EditToolBar.h @@ -83,7 +83,7 @@ class EditToolBar final : public ToolBar { void MakeButtons(); - void RegenerateTooltips(); + void RegenerateTooltips() override; AButton *mButtons[ETBNumButtons]; @@ -136,7 +136,7 @@ public: void EnableDisableButtons(); void UpdatePrefs(); - void RegenerateTooltips(); + void RegenerateTooltips() override; private: diff --git a/src/toolbars/MeterToolBar.h b/src/toolbars/MeterToolBar.h index 6b5dac7f5..e6af911aa 100644 --- a/src/toolbars/MeterToolBar.h +++ b/src/toolbars/MeterToolBar.h @@ -53,7 +53,7 @@ class MeterToolBar final : public ToolBar { wxSize GetDockedSize(); private: - void RegenerateTooltips(); + void RegenerateTooltips() override; AudacityProject *mProject; int mWhichMeters; diff --git a/src/toolbars/MixerToolBar.cpp b/src/toolbars/MixerToolBar.cpp index 172d1c6af..43a5c0644 100644 --- a/src/toolbars/MixerToolBar.cpp +++ b/src/toolbars/MixerToolBar.cpp @@ -196,6 +196,8 @@ void MixerToolBar::UpdatePrefs() // Set label to pull in language change SetLabel(_("Mixer")); + RegenerateTooltips(); + // Give base class a chance ToolBar::UpdatePrefs(); } diff --git a/src/toolbars/MixerToolBar.h b/src/toolbars/MixerToolBar.h index cc94c67df..3ef6ed155 100644 --- a/src/toolbars/MixerToolBar.h +++ b/src/toolbars/MixerToolBar.h @@ -49,6 +49,8 @@ class MixerToolBar final : public ToolBar { void AdjustOutputGain(int adj); void AdjustInputGain(int adj); + void RegenerateTooltips() override {}; + protected: float mInputSliderVolume; float mOutputSliderVolume; diff --git a/src/toolbars/SelectionBar.cpp b/src/toolbars/SelectionBar.cpp index ce665490c..0eaa66344 100644 --- a/src/toolbars/SelectionBar.cpp +++ b/src/toolbars/SelectionBar.cpp @@ -296,6 +296,8 @@ void SelectionBar::UpdatePrefs() // Set label to pull in language change SetLabel(_("Selection")); + RegenerateTooltips(); + // Give base class a chance ToolBar::UpdatePrefs(); } diff --git a/src/toolbars/SelectionBar.h b/src/toolbars/SelectionBar.h index fe3f19e2e..4756e282d 100644 --- a/src/toolbars/SelectionBar.h +++ b/src/toolbars/SelectionBar.h @@ -49,6 +49,7 @@ class SelectionBar final : public ToolBar { void SetSelectionFormat(const wxString & format); void SetRate(double rate); void SetListener(SelectionBarListener *l); + void RegenerateTooltips() override {}; private: diff --git a/src/toolbars/SpectralSelectionBar.cpp b/src/toolbars/SpectralSelectionBar.cpp index 7dc674892..bef455779 100644 --- a/src/toolbars/SpectralSelectionBar.cpp +++ b/src/toolbars/SpectralSelectionBar.cpp @@ -209,6 +209,8 @@ void SpectralSelectionBar::UpdatePrefs() // Set label to pull in language change SetLabel(_("Spectral Selection")); + RegenerateTooltips(); + // Give base class a chance ToolBar::UpdatePrefs(); } diff --git a/src/toolbars/SpectralSelectionBar.h b/src/toolbars/SpectralSelectionBar.h index 75581f98f..54ad0bc32 100644 --- a/src/toolbars/SpectralSelectionBar.h +++ b/src/toolbars/SpectralSelectionBar.h @@ -46,6 +46,8 @@ public: void SetBandwidthSelectionFormatName(const wxString & formatName); void SetListener(SpectralSelectionBarListener *l); + void RegenerateTooltips() override {}; + private: void ValuesToControls(); diff --git a/src/toolbars/ToolBar.cpp b/src/toolbars/ToolBar.cpp index 433d7c851..b949dcfd6 100644 --- a/src/toolbars/ToolBar.cpp +++ b/src/toolbars/ToolBar.cpp @@ -48,6 +48,7 @@ in which buttons can be placed. #include "../ImageManipulation.h" #include "../Project.h" #include "../Theme.h" +#include "../commands/Keyboard.h" #include "../widgets/AButton.h" #include "../widgets/Grabber.h" @@ -761,6 +762,33 @@ void ToolBar::MakeAlternateImages(AButton &button, int idx, button.SetAlternateImages(idx, *up, *hilite, *down, *disable); } +void ToolBar::SetButtonToolTip +(AButton &button, const std::vector &commands, const wxString &separator) +{ + const auto project = GetActiveProject(); + const auto commandManager = project ? project->GetCommandManager() : nullptr; + wxString result; + auto iter = commands.begin(), end = commands.end(); + while (iter != end) { + result += *iter++; + if (iter != end) { + if (!iter->empty()) { + if (commandManager) { + auto keyStr = commandManager->GetKeyFromName(*iter); + if (keyStr.empty()) + keyStr = _("no key"); + result += wxT(" "); + result += Internat::Parenthesize(KeyStringDisplay(keyStr, true)); + } + } + ++iter; + } + if (iter != end) + result += separator; + } + button.SetToolTip(result); +} + // // This changes the state a button (from up to down or vice versa) // diff --git a/src/toolbars/ToolBar.h b/src/toolbars/ToolBar.h index 1e6d28575..8b7f0b3a9 100644 --- a/src/toolbars/ToolBar.h +++ b/src/toolbars/ToolBar.h @@ -15,6 +15,7 @@ #include "../Experimental.h" +#include #include #include #include @@ -98,6 +99,7 @@ class ToolBar /* not final */ : public wxPanel virtual void EnableDisableButtons() = 0; virtual void ReCreateButtons(); virtual void UpdatePrefs(); + virtual void RegenerateTooltips() = 0; int GetType(); wxString GetTitle(); @@ -149,7 +151,18 @@ class ToolBar /* not final */ : public wxPanel teBmps eStandardDown, teBmps eDisabled, wxSize size); - + + static + void SetButtonToolTip + (AButton &button, + // An array, alternating user-visible strings, and + // non-user-visible command names. If a shortcut key is defined + // for the command, then it is appended, parenthesized, after the + // user-visible string. + const std::vector &commands, + // If more than one pair of strings is given, then use this separator. + const wxString &separator = wxT(" / ")); + protected: void SetButton(bool down, AButton *button); diff --git a/src/toolbars/ToolDock.cpp b/src/toolbars/ToolDock.cpp index 6ff329d53..e74bc004c 100644 --- a/src/toolbars/ToolDock.cpp +++ b/src/toolbars/ToolDock.cpp @@ -22,6 +22,7 @@ *//**********************************************************************/ #include "../Audacity.h" +#include // For compilers that support precompilation, includes "wx/wx.h". #include @@ -39,6 +40,8 @@ #include "ToolManager.h" #include "ToolDock.h" +#include + #include "../AColor.h" #include "../AllThemeResources.h" #include "../ImageManipulation.h" @@ -48,6 +51,280 @@ #include "../widgets/AButton.h" #include "../widgets/Grabber.h" +const ToolBarConfiguration::Position + ToolBarConfiguration::UnspecifiedPosition { false }; + +auto ToolBarConfiguration::FindPlace(const ToolBar *bar) const + -> Iterator +{ + auto This = const_cast(this); + return std::find_if(This->begin(), This->end(), + [=](const Place &place){ + return place.pTree->pBar == bar; + }); +} + +auto ToolBarConfiguration::FindParent(const ToolBar *bar) + -> std::pair +{ + auto findTree = [=](Forest &forest){ + return std::find_if(forest.begin(), forest.end(), + [=](const Tree &tree){ return tree.pBar == bar; }); + }; + + auto iter1 = findTree(mForest); + if (iter1 != mForest.end()) + return { &mForest, iter1 }; + + Forest::iterator result; + auto iter = std::find_if(begin(), end(), + [&](const Place &place){ + auto &children = place.pTree->children; + return (result = findTree(children)) != children.end(); + } + ); + if (iter != end()) + return { &iter->pTree->children, result }; + + return { nullptr, Forest::iterator{} }; +} + +auto ToolBarConfiguration::Find(const ToolBar *bar) const -> Position +{ + auto iter = FindPlace(bar); + if (iter == end()) + return UnspecifiedPosition; + else + return iter->position; +} + +void ToolBarConfiguration::Insert(ToolBar *bar, Position position) +{ + if (position == UnspecifiedPosition) { + // Add at the "end" of the layout + Forest *pForest = &mForest; + while (!pForest->empty()) + pForest = &pForest->back().children; + pForest->push_back( Tree {} ); + pForest->back().pBar = bar; + } + else { + auto pForest = &mForest; + if (position.rightOf) { + const auto parent = FindPlace(position.rightOf); + if (parent != end()) + pForest = &parent->pTree->children; + } + + const auto begin = pForest->begin(); + auto iter = begin; + const auto end = pForest->end(); + bool adopt = false; + + if (position.below) { + iter = std::find_if(begin, end, + [=](const Tree &tree){ return tree.pBar == position.below; } + ); + if (iter != end) { + ++iter; + if (iter != end) + adopt = true; + } + else + // Not found, default to topmost + iter = begin; + } + else + adopt = (iter != end); + + // Adopt the child only if the insertion point specifies that + if (adopt && position.adopt) { + // Make new node with one child + Tree tree; + tree.pBar = bar; + tree.children.push_back(Tree{}); + + // Do adoption + auto &child = tree.children.back(); + child.pBar = iter->pBar; + child.children.swap(iter->children); + + // Put the node in the tree + (*iter).swap(tree); + } + else + pForest->insert(iter, Tree {})->pBar = bar; + } +} + +void ToolBarConfiguration::InsertAtPath + (ToolBar *bar, const std::vector &path) +{ + auto pForest = &mForest; + Tree *pTree {}; + + // Guarantee the existence of nodes + for (auto ii : path) { + Forest::size_type uu = std::max(0, ii); + pForest->resize(std::max(uu + 1, pForest->size())); + pTree = &(*pForest)[uu]; + pForest = &pTree->children; + } + + if (pTree) + pTree->pBar = bar; +} + +void ToolBarConfiguration::Remove(Forest &forest, Forest::iterator iter) +{ + Tree tree; + tree.swap(*iter); + iter = forest.erase(iter); + auto &children = tree.children; + auto cIter = children.rbegin(), cEnd = children.rend(); + while (cIter != cEnd) { + iter = forest.insert(iter, Tree{}); + (*iter).swap(*cIter); + ++cIter; + } +} + +void ToolBarConfiguration::Remove(const ToolBar *bar) +{ + auto results = FindParent(bar); + auto pForest = results.first; + if (pForest) { + // Reparent all of the children of the deleted node + auto iter = results.second; + wxASSERT(iter->pBar == bar); + Remove(*pForest, iter); + } +} + +void ToolBarConfiguration::Show(ToolBar *bar) +{ + // Do not assume the bar is absent, though in practice that is always so + if (!Contains(bar)) + Insert(bar); +} + +void ToolBarConfiguration::Hide(ToolBar *bar) +{ + // Future: might hide a bar without eliminating it from the configuration + Remove(bar); +} + +bool ToolBarConfiguration::IsRightmost(const ToolBar *bar) const +{ + auto iter = FindPlace(bar); + auto endit = end(); + if (iter == endit) + // not present + return true; + if (++iter == endit) + // Last of all + return true; + if (bar->GetRect().y != iter->pTree->pBar->GetRect().y) + // + return true; + return false; +} + +bool ToolBarConfiguration::Read + (ToolBarConfiguration *pConfiguration, + Legacy *pLegacy, + ToolBar *bar, bool &visible, bool defaultVisible) +{ + bool result = true; + + if (pConfiguration) { + int ord; + gPrefs->Read( wxT("Order"), &ord, -1 ); + // Index was written 1-based + --ord; + if (ord >= ToolBarCount) + result = false; + else if (ord >= 0) + { + // Legacy preferences + while (pLegacy->bars.size() <= size_t(ord)) + pLegacy->bars.push_back(nullptr); + pLegacy->bars[ord] = bar; + } + else { + wxString strPath; + gPrefs->Read( wxT("Path"), &strPath ); + if (!strPath.empty()) { + wxStringTokenizer toker { strPath, wxT(",") }; + std::vector path; + while(toker.HasMoreTokens()) { + auto token = toker.GetNextToken(); + auto ii = wxAtoi(token); + path.push_back(ii); + } + pConfiguration->InsertAtPath(bar, path); + } + } + } + + // Future: might remember visibility in the configuration, not forgetting + // positions of hidden bars. + gPrefs->Read( wxT("Show"), &visible, defaultVisible); + + return result; +} + +void ToolBarConfiguration::RemoveNulls(Forest &forest) +{ + for (size_t ii = 0; ii < forest.size(); ++ii) { + if(forest[ii].pBar == nullptr) + Remove(forest, forest.begin() + ii--); + } + + // Now do the same recursively + for (auto &tree : forest) + RemoveNulls(tree.children); +} + +void ToolBarConfiguration::PostRead(Legacy &legacy) +{ + // Be sure no nodes contain NULL, + // against the case of obsolete preferences, perhaps + RemoveNulls(mForest); + + ToolBar *prev {}; + for (auto pBar : legacy.bars) { + if (!pBar) + continue; + + Position position{ prev }; + Insert(pBar, position); + + prev = pBar; + } +} + +void ToolBarConfiguration::Write + (const ToolBarConfiguration *pConfiguration, const ToolBar *bar) +{ + if (pConfiguration) { + wxString strPath; + const auto cIter = pConfiguration->FindPlace(bar); + const auto path = cIter.GetPath(); + if (!path.empty()) { + auto iter = path.begin(), end = path.end(); + strPath += wxString::Format(wxT("%d"), *iter++); + while (iter != end) + strPath += wxString::Format(wxT(",%d"), *iter++); + } + gPrefs->Write(wxT("Path"), strPath); + + // Remove any legacy configuration info. + gPrefs->DeleteEntry(wxT("Order")); + } + gPrefs->Write( wxT("Show"), bar->IsVisible() ); +} + IMPLEMENT_CLASS( ToolDock, wxPanel ); //////////////////////////////////////////////////////////// @@ -90,41 +367,22 @@ ToolDock::~ToolDock() { } -// -// Returns the order of the toolbar within the dock -// -int ToolDock::GetOrder( ToolBar *bar ) -{ - int order = mDockedBars.Index( bar ); - - if( order == wxNOT_FOUND ) - { - if( bar->GetDock() == this ) - { - return 999; - } - - return 0; - } - - return order + 1; -} - // // Remove the toolbar from our control // void ToolDock::Undock( ToolBar *bar ) { - if( mDockedBars.Index( bar ) != wxNOT_FOUND ) + if( mConfiguration.Contains( bar ) ) { - mDockedBars.Remove( bar ); + mConfiguration.Remove( bar ); + mBars[ bar->GetId() ] = nullptr; } } // // Handle ToolDock events // -void ToolDock::Dock( ToolBar *bar, bool deflate, int before ) +void ToolDock::Dock( ToolBar *bar, bool deflate, ToolBarConfiguration::Position position ) { // Adopt the toolbar into our family bar->Reparent( this ); @@ -139,14 +397,8 @@ void ToolDock::Dock( ToolBar *bar, bool deflate, int before ) ); // Park the NEW bar in the correct berth - if( before >= 0 && before < (int)mDockedBars.GetCount() ) - { - mDockedBars.Insert( bar, before ); - } - else - { - mDockedBars.Add( bar ); - } + if (!mConfiguration.Contains(bar)) + mConfiguration.Insert( bar, position ); // Inform toolbar of change bar->SetDocked( this, false ); @@ -156,270 +408,382 @@ void ToolDock::Dock( ToolBar *bar, bool deflate, int before ) Updated(); } -// -// Layout the toolbars -// -void ToolDock::LayoutToolBars() +// Initial docking of bars +void ToolDock::LoadConfig() { - wxRect stack[ ToolBarCount + 1 ]; - wxPoint cpos, lpos; - ToolBar *lt = NULL; - int ndx, stkcnt = 0; - int width, height; - - // Get size of our parent since we haven't been sized yet - GetParent()->GetClientSize( &width, &height ); - width -= toolbarGap; - height -= toolbarGap; - - // Get the number of docked toolbars and take a quick exit - // if we don't have any - int cnt = mDockedBars.GetCount(); - if( cnt == 0 ) - { - SetMinSize( wxSize( width, toolbarGap ) ); - return; + // Add all ordered toolbars + for(const auto &place : GetConfiguration()) { + auto bar = place.pTree->pBar; + this->Dock(bar, false); + // Show it -- hidden bars are not (yet) ever saved as part of a + // configuration + Expose( bar->GetId(), true ); } - - // Set initial stack entry to maximum size - stack[ 0 ].SetX( toolbarGap ); - stack[ 0 ].SetY( toolbarGap ); - stack[ 0 ].SetWidth( width ); - stack[ 0 ].SetHeight( height ); - - // Process all docked and visible toolbars - for( ndx = 0; ndx < cnt; ndx++ ) - { - // Cache toolbar pointer - ToolBar *ct = (ToolBar *)mDockedBars[ ndx ]; - - // Get and cache the toolbar sizes - wxSize sz = ct->GetSize(); - int tw = sz.GetWidth() + toolbarGap; - int th = sz.GetHeight() + toolbarGap; - - // Will this one fit in remaining horizontal space? - if( ( tw > stack[ stkcnt ].GetWidth() ) || - ( th > stack[ stkcnt ].GetHeight() ) ) - { - // Destack entries until one is found in which this bar - // will fit or until we run out of stacked entries - while( stkcnt > 0 ) - { - stkcnt--; - - // Get out if it will fit - if( ( tw <= stack[ stkcnt ].GetWidth() ) && - ( th <= stack[ stkcnt ].GetHeight() ) ) - { - break; - } - } - } - - // The current stack entry position is where the bar - // will be placed. - cpos = stack[ stkcnt ].GetPosition(); - - // We'll be using at least a portion of this stack entry, so - // adjust the location and size. It is possible that these - // will become zero if this entry and the toolbar have the - // same height. This is what we want as it will be destacked - // in the next iteration. - stack[ stkcnt ].SetY( stack[ stkcnt ].GetY() + th ); - stack[ stkcnt ].SetHeight( stack[ stkcnt ].GetHeight() - th ); - - // Calc the next possible horizontal location. - int x = cpos.x + tw; - - // Add a NEW stack entry - stkcnt++; - stack[ stkcnt ].SetX( x ); - stack[ stkcnt ].SetY( cpos.y ); - stack[ stkcnt ].SetWidth( width - x ); - stack[ stkcnt ].SetHeight( th ); - - // Position the previous toolbar - if( ndx > 0 ) - { - // Keep the tab order in order - ct->MoveAfterInTabOrder( lt ); - - // Place the last toolbar - lt->SetPosition( wxPoint( lpos.x, lpos.y ) ); - } - - // Place the final toolbar - if( ndx == cnt - 1 ) - { - ct->SetPosition( wxPoint( cpos.x, cpos.y ) ); - } - - // Remember for next iteration - lt = ct; - lpos = cpos; - } - - // Set the final size of the dock window - SetMinSize( wxSize( -1, stack[ 0 ].GetY() ) ); - - // Clean things up - Refresh( false ); } -// -// Determine the location and bar before which a NEW bar would be placed -// -// 'rect' will be the rectangle for the dock marker. -int ToolDock::PositionBar( ToolBar *t, wxPoint & pos, wxRect & rect ) +class ToolDock::LayoutVisitor { - struct +public: + virtual void ModifySize + (ToolBar *ct, + const wxRect &rect, + ToolBarConfiguration::Position prevPosition, + ToolBarConfiguration::Position position, + wxSize &sz) { - wxRect rect; - wxSize min; - } tinfo[ ToolBarCount + 1 ]; + ct; rect; prevPosition; position; sz; + } - wxRect stack[ ToolBarCount + 1 ]; - wxPoint cpos, lpos; - int ct, lt = 0; - int ndx, stkcnt = 0; - int tindx = -1; - int cnt = mDockedBars.GetCount(); - int width, height; + virtual void Visit + (ToolBar *ct, wxPoint point) = 0; + + virtual bool ShouldVisitSpaces() = 0; + + virtual void FinalRect + (const wxRect &rect, ToolBarConfiguration::Position finalPosition) + { + rect; finalPosition; + } +}; + +void ToolDock::VisitLayout(LayoutVisitor &visitor, + ToolBarConfiguration *pWrappedConfiguration) +{ + if (pWrappedConfiguration) + pWrappedConfiguration->Clear(); // Get size of our parent since we haven't been sized yet + int width, height; GetParent()->GetClientSize( &width, &height ); width -= toolbarGap; height -= toolbarGap; - // Set initial stack entry to maximum size - stack[ 0 ].SetX( toolbarGap ); - stack[ 0 ].SetY( toolbarGap ); - // The stack width and height are the remaining width and height. - stack[ 0 ].SetWidth( width ); - stack[ 0 ].SetHeight( height ); + // Rectangle of space to allocate + wxRect main{ toolbarGap, toolbarGap, + // Allow limited width, but arbitrary height, for the root rectangle + width, std::numeric_limits::max() }; + + // For recording the nested subdivisions of the rectangle + struct Item { + int myBarID { NoBarID }; + int parentBarID { NoBarID }; + ToolBar *lastSib {}; + ToolBar *lastWrappedChild {}; + wxRect rect; + } layout[ ToolBarCount ]; + + ToolBar *lastRoot {}; + ToolBar *lastWrappedRoot {}; // Process all docked and visible toolbars - // - // Careful...slightly different from above in that we expect to - // process one more bar than is currently docked (<= in for) - for (ndx = 0, ct = 0; ndx <= cnt; ndx++, ct++) + for ( const auto &place : this->GetConfiguration() ) { - // If last entry, then it is the - if (ndx == cnt) - { - // ...so check to see if the NEW bar has been placed yet - if (tindx == -1) - { - // Add the NEW bars' dimensions to the mix - tinfo[ct].rect = t->GetRect(); - tinfo[ct].min = t->GetDockedSize(); - tindx = ct; - } + // Cache toolbar pointer + const auto ct = place.pTree->pBar; + + // set up the chain of ancestors. + const auto parent = place.position.rightOf; + const auto type = ct->GetType(); + auto &newItem = layout[ type ]; + newItem.parentBarID = parent ? parent->GetType() : NoBarID; + // Mark the slots that really were visited, for final pass through + // the spaces. + newItem.myBarID = type; + + const auto parentItem = parent ? &layout[ parent->GetType() ] : nullptr; + ToolBar *prevSib; + if (!parent) { + prevSib = lastRoot; + lastRoot = ct; } - else + else { + auto &sib = parentItem->lastSib; + prevSib = sib; + sib = ct; + } + ToolBarConfiguration::Position prevPosition = { parent, prevSib }; + + // Determine the size of the toolbar to fit, with advice from + // the visitor object + wxSize sz = ct->GetSize(); { - // Cache toolbar pointer - ToolBar *b = (ToolBar *)mDockedBars[ndx]; - - // Remember current bars' dimensions - tinfo[ct].rect = b->GetRect(); - tinfo[ct].min = b->GetSize(); - - // Maybe insert the NEW bar if it hasn't already been done - // and is in the right place. - if (tindx == -1) - { - wxRect r; - - // Get bar rect and make gap part of it - r.SetPosition(b->GetParent()->ClientToScreen(b->GetPosition())); - r.SetSize(b->IsResizable() ? b->GetSize() : b->GetSize()); - r.width += toolbarGap; - r.height += toolbarGap; - - // Does the location fall within this bar? - if (r.Contains(pos) || pos.y <= r.y) - { - // Add the NEW bars' dimensions to the mix - tinfo[ct].rect = t->GetRect(); - tinfo[ct].min = t->GetDockedSize(); - tindx = ct; - ndx--; - } - } + wxRect temp; + temp.SetPosition(ct->GetParent()->ClientToScreen(ct->GetPosition())); + temp.SetSize(sz); + visitor.ModifySize(ct, temp, prevPosition, place.position, sz); } - // Get and cache the toolbar sizes - wxSize sz = tinfo[ct].min; + // Inflate the size to leave margins int tw = sz.GetWidth() + toolbarGap; int th = sz.GetHeight() + toolbarGap; - // This loop reduces stkcnt until it gives a box - // that we fit in. - while (stkcnt > 0) + // Choose the rectangle to subdivide + // Find a box that we fit in by going up the tree as needed -- + // thus when parent space is exhausted, fall back on ancestors -- + // so if the tree has too much depth for the width of the + // window, the toolbars may "wrap." + // Can always fall back to the main rectangle even if the bar is too + // wide. + auto pItem = parentItem; + auto pRect = pItem ? &pItem->rect : &main; + while (pRect != &main) { // Get out if it will fit - bool bTooWide = tw > stack[stkcnt].GetWidth(); + bool bTooWide = tw > pRect->GetWidth(); // We'd like to be able to add a tall toolbar in at the start of a row, // even if there isn't enough height for it. // If so, we'd have to at least change how we calculate 'bTooHigh'. - bool bTooHigh = th > stack[stkcnt].GetHeight(); + bool bTooHigh = th > pRect->GetHeight(); //bTooHigh &= stack[stkcnt].GetWidth() < (width - toolbarGap); //bTooHigh = false; if (!bTooWide && !bTooHigh) break; - stkcnt--; + + if (pItem->parentBarID == NoBarID) { + pItem = nullptr; + pRect = &main; + } + else { + pItem = &layout[ pItem->parentBarID ]; + pRect = &pItem->rect; + } } - // The current stack entry position is where the bar - // will be placed. - cpos = stack[stkcnt].GetPosition(); + // Record where the toolbar wrapped + ToolBar *& sib = pItem ? pItem->lastWrappedChild : lastWrappedRoot; + ToolBarConfiguration::Position newPosition { + pItem ? this->mBars[ pItem->myBarID ] : nullptr, + sib + }; + sib = ct; + if (pWrappedConfiguration) + pWrappedConfiguration->Insert(ct, newPosition); - // We'll be using at least a portion of this stack entry, so - // adjust the location and size. It is possible that these - // will become zero if this entry and the toolbar have the - // same height. This is (?) what we want as it will be destacked - // in the next iteration. - stack[stkcnt].SetY(stack[stkcnt].GetY() + th); - stack[stkcnt].SetHeight(stack[stkcnt].GetHeight() - th); + // Place the toolbar at the upper left part of the rectangle. + const auto cpos = pRect->GetPosition(); + visitor.Visit(ct, cpos); - // Calc the next possible horizontal location. + // Allocate an upper portion of the rectangle to this bar. + pRect->y += th; + pRect->height -= th; + + // A right portion of that upper portion remains available for + // descendant bars and is remembered in the layout array. int x = cpos.x + tw; + newItem.rect = wxRect{ x, cpos.y, width - x, th }; + } - // Add a NEW stack entry - stkcnt++; - stack[stkcnt].SetX(x); - stack[stkcnt].SetY(cpos.y); - stack[stkcnt].SetWidth(width - x); - stack[stkcnt].SetHeight(th); + if (visitor.ShouldVisitSpaces()) { + // Visit the fringe where new leaves of the tree could go - // Position the previous toolbar - if (ndx > 0) - { - // Place the unstretched toolbar - tinfo[lt].rect.x = lpos.x; - tinfo[lt].rect.y = lpos.y; - } + // Find the items with leftover spaces + const auto end = std::remove_if(layout, layout + ToolBarCount, + [](const Item &item){ + return item.myBarID == NoBarID || item.rect.IsEmpty(); + } + ); + // Sort top to bottom for definiteness, though perhaps not really needed + std::sort(layout, end, + [](const Item &lhs, const Item &rhs){ + return lhs.rect.y < rhs.rect.y; + } + ); + for (auto iter = layout; iter != end; ++iter) { + const auto &item = *iter; + const auto &rect = item.rect; - // Remember for next iteration - lt = ct; - lpos = cpos; + auto globalRect = rect; + globalRect.SetPosition( this->ClientToScreen(rect.GetPosition()) ); - // If we've placed it, we're done. - if (tindx != -1) - { - tinfo[tindx].rect.x = cpos.x; - tinfo[tindx].rect.y = cpos.y; - break; + // Let the visitor determine size + wxSize sz {}; + ToolBarConfiguration::Position + position { this->mBars[ item.myBarID ], item.lastWrappedChild }, + prevPosition {}; + visitor.ModifySize(nullptr, globalRect, prevPosition, position, sz); + int tw = sz.GetWidth() + toolbarGap; + int th = sz.GetHeight() + toolbarGap; + + // Test fit + bool bTooWide = tw > rect.GetWidth(); + bool bTooHigh = th > rect.GetHeight(); + if (!bTooWide && !bTooHigh) { + // Call visitor again to confirm the placement + const auto cpos = rect.GetPosition(); + visitor.Visit(nullptr, cpos); + } } } - // Fill in the final position - rect = tinfo[ tindx ].rect; + // Report the final bounding box of all the bars, and a position where + // you can insert a new bar at bottom left. + ToolBarConfiguration::Position finalPosition { nullptr, lastRoot }; + visitor.FinalRect( + wxRect { toolbarGap, toolbarGap, main.width, main.y }, finalPosition + ); +} - return tindx; +// +// Layout the toolbars +// +void ToolDock::LayoutToolBars() +{ + struct SizeSetter final : public LayoutVisitor + { + SizeSetter (ToolDock *d) : dock{ d } {} + + void Visit + (ToolBar *bar, wxPoint point) + override + { + // Place the toolbar + if(bar) + bar->SetPosition( point ); + } + + bool ShouldVisitSpaces() override + { + return false; + } + + virtual void FinalRect + (const wxRect &rect, ToolBarConfiguration::Position) + override + { + // Set the final size of the dock window + dock->SetMinSize( rect.GetSize() ); + } + + ToolDock *dock; + } sizeSetter { + this + }; + VisitLayout(sizeSetter, &mWrappedConfiguration); + + // Set tab order + { + ToolBar *lt{}; + for ( const auto &place : GetConfiguration() ) { + auto ct = place.pTree->pBar; + if( lt ) + ct->MoveAfterInTabOrder( lt ); + lt = ct; + } + } + + // Clean things up + Refresh( false ); +} + +// Determine the position where a NEW bar would be placed +// +// 'rect' will be the rectangle for the dock marker. +ToolBarConfiguration::Position + ToolDock::PositionBar( ToolBar *t, const wxPoint & pos, wxRect & rect ) +{ + // Set width and size, but we must still find x and y. + rect = t->GetRect(); + + using Position = ToolBarConfiguration::Position; + Position result { ToolBarConfiguration::UnspecifiedPosition }; + struct Inserter : public LayoutVisitor + { + struct Stop {}; + + Inserter(Position &p, wxRect &r, const wxPoint &pt, ToolBar *t) + : result(p), rect(r), point(pt), tb(t) + {} + + void ModifySize + (ToolBar *ct, + const wxRect &rect, + ToolBarConfiguration::Position prevPosition, + ToolBarConfiguration::Position position, + wxSize &sz) + override + { + // Maybe insert the NEW bar if it hasn't already been done + // and is in the right place. + + // Does the location fall within this bar? + if (rect.Contains(point)) + { + sz = tb->GetDockedSize(); + // Choose a position always, if there is a bar to displace. + // Else, only if the fit is possible. + if (ct || (sz.x <= rect.width && sz.y <= rect.height)) { + // May choose current or previous. + if (ct && point.y < (rect.GetTop() + rect.GetBottom()) / 2) + // "Wedge" the bar into a crack alone, not adopting others + usedPrev = true, result = prevPosition, result.adopt = false; + else + result = position; + } + // Now wait until the other callback below to discover x and y + } + } + + void Visit + (ToolBar *, wxPoint point) + override + { + if (result != ToolBarConfiguration::UnspecifiedPosition) { + // If we've placed it, we're done. + rect.x = point.x; + rect.y = point.y; + if (usedPrev) + rect.y -= tb->GetDockedSize().GetHeight() / 2; + + throw Stop {}; + } + } + + bool ShouldVisitSpaces() override + { + return true; + } + + void FinalRect + (const wxRect &finalRect, ToolBarConfiguration::Position finalPosition) + override + { + if (result == ToolBarConfiguration::UnspecifiedPosition) { + // Default of all other placements. + result = finalPosition; + wxPoint point { finalRect.GetLeft(), finalRect.GetBottom() }; + rect.SetPosition(point); + } + } + + + Position &result; + wxRect ▭ + const wxPoint point; + ToolBar *const tb; + bool usedPrev { false }; + } inserter { + result, rect, pos, t + }; + + try { this->VisitLayout(inserter); } catch (const Inserter::Stop&) {} + + // rect is decided + return result; +} + +void ToolDock::WrapConfiguration(ToolBarConfiguration &backup) +{ + backup.Clear(); + backup.Swap(mConfiguration); + mConfiguration.Swap(mWrappedConfiguration); +} + +void ToolDock::RestoreConfiguration(ToolBarConfiguration &backup) +{ + mWrappedConfiguration.Clear(); + mWrappedConfiguration.Swap(mConfiguration); + mConfiguration.Swap(backup); } // @@ -430,20 +794,11 @@ void ToolDock::Expose( int type, bool show ) ToolBar *t = mBars[ type ]; // Maintain the docked array - if( show ) - { - if( mDockedBars.Index( t ) == wxNOT_FOUND ) - { - mDockedBars.Add( t ); - } - } - else - { - if( mDockedBars.Index( t ) != wxNOT_FOUND ) - { - mDockedBars.Remove( t ); - } - } + const auto shown = mConfiguration.Shows( t ); + if( show && !shown ) + mConfiguration.Show( t ); + else if( !show && shown ) + mConfiguration.Hide( t ); // Make it (dis)appear t->Expose( show ); @@ -453,11 +808,6 @@ void ToolDock::Expose( int type, bool show ) Updated(); } -int ToolDock::Find(ToolBar *bar) const -{ - return mDockedBars.Index(bar); -} - // // Queues an EVT_TOOLBAR_UPDATED command event to notify any // interested parties of an updated toolbar or dock layout @@ -482,7 +832,7 @@ void ToolDock::OnGrabber( GrabberEvent & event ) mManager->ProcessEvent( event ); // We no longer have control - mDockedBars.Remove( t ); + mConfiguration.Remove( t ); } } @@ -530,30 +880,29 @@ void ToolDock::OnPaint( wxPaintEvent & WXUNUSED(event) ) AColor::Line(dc, 0, 0, 0, sz.GetHeight() ); // Draw the gap between each bar - int ndx, cnt = mDockedBars.GetCount(); - for( ndx = 0; ndx < cnt; ndx++ ) + for (const auto &place : GetConfiguration()) { - wxRect r = ( (ToolBar *)mDockedBars[ ndx ] )->GetRect(); + auto toolbar = place.pTree->pBar; + if (!toolbar) + continue; + + wxRect r = toolbar->GetRect(); AColor::Line( dc, - r.GetLeft(), - r.GetBottom() + 1, - sz.GetWidth(), - r.GetBottom() + 1 ); + r.GetLeft(), + r.GetBottom() + 1, + sz.GetWidth(), + r.GetBottom() + 1 ); // For all bars but the last... - if( ndx < cnt - 1 ) - { - // ...and for bars that aren't the last in a row, draw an - // horizontal gap line - if( r.y == ( (ToolBar *)mDockedBars[ ndx + 1 ] )->GetRect().y ) - { - AColor::Line(dc, - r.GetRight() + 1, - r.GetTop(), - r.GetRight() + 1, - r.GetBottom() + 1 ); - } + // ...and for bars that aren't the last in a row, draw an + // horizontal gap line + if (!mConfiguration.IsRightmost(toolbar)) { + AColor::Line(dc, + r.GetRight() + 1, + r.GetTop(), + r.GetRight() + 1, + r.GetBottom() + 1 ); } } } diff --git a/src/toolbars/ToolDock.h b/src/toolbars/ToolDock.h index cadee0df6..6437c8329 100644 --- a/src/toolbars/ToolDock.h +++ b/src/toolbars/ToolDock.h @@ -13,6 +13,8 @@ #ifndef __AUDACITY_TOOLDOCK__ #define __AUDACITY_TOOLDOCK__ +#include +#include "../MemoryX.h" // for std::move #include #include @@ -45,23 +47,270 @@ enum DockCount = 2 }; +class ToolBarConfiguration +{ + struct Tree; + using Forest = std::vector; + +public: + + void Swap(ToolBarConfiguration &that) + { + mForest.swap(that.mForest); + } + + void Clear() + { + mForest.clear(); + } + + struct Position { + ToolBar *rightOf {}; + ToolBar *below {}; + bool adopt {true}; + bool valid {true}; + + // Default constructor + Position() {} + + Position( + ToolBar *r, + ToolBar *b = nullptr, + bool shouldAdopt = true + ) + : rightOf{ r }, below{ b }, adopt{ shouldAdopt } + {} + + // Constructor for the invalid value + explicit Position(bool /* dummy */) : valid{ false } {} + + friend inline bool operator == + (const Position &lhs, const Position &rhs) + { return lhs.valid == rhs.valid && + (!lhs.valid || + (lhs.rightOf == rhs.rightOf + && lhs.below == rhs.below + && lhs.adopt == rhs.adopt + )); + } + + friend inline bool operator != + (const Position &lhs, const Position &rhs) + { return !(lhs == rhs); } + }; + + static const Position UnspecifiedPosition; + + struct Place { + Tree *pTree {}; + Position position; + }; + + // This iterator visits the nodes of the forest in pre-order, and at each + // stop, makes the parent, previous sibling, and children accessible. + class Iterator + : public std::iterator + { + public: + const Place &operator * () const { return mPlace; } + const Place *operator -> () const { return &**this; } + Iterator &operator ++ () + { + // This is a feature: advance position even at the end + mPlace.position = + { mPlace.pTree ? mPlace.pTree->pBar : nullptr }; + + if (!mIters.empty()) + { + auto triple = &mIters.back(); + auto &children = triple->current->children; + if (children.empty()) { + while (++triple->current == triple->end) { + mIters.pop_back(); + if (mIters.empty()) + break; + triple = &mIters.back(); + } + } + else { + auto b = children.begin(); + mIters.push_back( Triple { b, b, children.end() } ); + } + } + + if (mIters.empty()) { + mPlace.pTree = nullptr; + // Leave mPlace.position as above + } + else { + const auto &triple = mIters.back(); + mPlace.pTree = &*triple.current; + + if (mIters.size() == 1) + mPlace.position.rightOf = nullptr; + else + mPlace.position.rightOf = (mIters.rbegin() + 1)->current->pBar; + + if (triple.begin == triple.current) + mPlace.position.below = nullptr; + else + mPlace.position.below = (triple.current - 1)->pBar; + } + + return *this; + } + + // This may be called on the end iterator, and then returns empty + std::vector GetPath() const + { + std::vector path; + path.reserve(mIters.size()); + for (const auto &triple : mIters) + path.push_back(triple.current - triple.begin); + return std::move(path); + } + + friend inline bool operator == + (const Iterator &lhs, const Iterator &rhs) + { + const auto &li = lhs.mIters; + const auto &ri = rhs.mIters; + return li.size() == ri.size() && + std::equal(li.begin(), li.end(), ri.begin()); + } + + friend inline bool operator != + (const Iterator &lhs, const Iterator &rhs) + { + return !(lhs == rhs); + } + + private: + friend ToolBarConfiguration; + Iterator () {} + explicit Iterator(ToolBarConfiguration &conf) + { + auto &forest = conf.mForest; + if (!forest.empty()) { + auto b = forest.begin(); + mIters.push_back( Triple { b, b, forest.end() } ); + mPlace.pTree = &*b; + } + } + + Place mPlace; + + using FIter = Forest::iterator; + struct Triple + { + Triple (FIter b, FIter c, FIter e) + : begin{b}, current{c}, end{e} {} + FIter begin, current, end; + + friend inline bool operator == + (const Triple &lhs, const Triple &rhs) + { + // Really need only to compare current + return + // lhs.begin == rhs.begin && + lhs.current == rhs.current + // lhs.end == rhs.end + ; + } + }; + std::vector mIters; + }; + + Iterator begin() { return Iterator { *this }; } + Iterator end() const { return Iterator {}; } + + Position Find(const ToolBar *bar) const; + + bool Contains(const ToolBar *bar) const + { + return Find(bar) != UnspecifiedPosition; + } + + // Default position inserts at the end + void Insert(ToolBar *bar, + Position position = UnspecifiedPosition); + void InsertAtPath(ToolBar *bar, const std::vector &path); + void Remove(const ToolBar *bar); + + // Future: might allow a state that the configuration remembers + // a hidden bar, but for now, it's equivalent to Contains(): + bool Shows(const ToolBar *bar) const { return Contains(bar); } + + void Show(ToolBar *bar); + void Hide(ToolBar *bar); + + bool IsRightmost(const ToolBar *bar) const; + + struct Legacy { + std::vector bars; + }; + + static bool Read + (ToolBarConfiguration *pConfiguration, + Legacy *pLegacy, + ToolBar *bar, bool &visible, bool defaultVisible); + void PostRead(Legacy &legacy); + + static void Write + (const ToolBarConfiguration *pConfiguration, const ToolBar *bar); + +private: + + void Remove(Forest &forest, Forest::iterator iter); + void RemoveNulls(Forest &forest); + + struct Tree + { + ToolBar *pBar {}; + Forest children; + + void swap(Tree &that) + { + std::swap(pBar, that.pBar); + children.swap(that.children); + } + }; + + Iterator FindPlace(const ToolBar *bar) const; + std::pair FindParent(const ToolBar *bar); + + Forest mForest; +}; + class ToolDock final : public wxPanel { - - public: +public: ToolDock( ToolManager *manager, wxWindow *parent, int dockid ); ~ToolDock(); bool AcceptsFocus() const override { return false; }; + void LoadConfig(); void LayoutToolBars(); void Expose( int type, bool show ); - int Find(ToolBar *bar) const; int GetOrder( ToolBar *bar ); - void Dock( ToolBar *bar, bool deflate, int ndx = -1 ); + void Dock( ToolBar *bar, bool deflate, + ToolBarConfiguration::Position ndx + = ToolBarConfiguration::UnspecifiedPosition); void Undock( ToolBar *bar ); - int PositionBar( ToolBar *t, wxPoint & pos, wxRect & rect ); + ToolBarConfiguration::Position + PositionBar( ToolBar *t, const wxPoint & pos, wxRect & rect ); + + ToolBarConfiguration &GetConfiguration() + { return mConfiguration; } + + // backup gets old contents of the configuration; the configuration is + // set to the wrapped configuration. + void WrapConfiguration(ToolBarConfiguration &backup); + + // Reverse what was done by WrapConfiguration. + void RestoreConfiguration(ToolBarConfiguration &backup); protected: @@ -72,6 +321,9 @@ class ToolDock final : public wxPanel void OnMouseEvents(wxMouseEvent &event); private: + class LayoutVisitor; + void VisitLayout(LayoutVisitor &visitor, + ToolBarConfiguration *pWrappedConfiguration = nullptr); void Updated(); @@ -80,7 +332,12 @@ class ToolDock final : public wxPanel ToolManager *mManager; - wxArrayPtrVoid mDockedBars; + // Stores adjacency relations that we want to realize in the dock layout + ToolBarConfiguration mConfiguration; + + // Configuration as modified by the constraint of the main window width + ToolBarConfiguration mWrappedConfiguration; + ToolBar *mBars[ ToolBarCount ]; public: diff --git a/src/toolbars/ToolManager.cpp b/src/toolbars/ToolManager.cpp index afdaafb69..adfb02868 100644 --- a/src/toolbars/ToolManager.cpp +++ b/src/toolbars/ToolManager.cpp @@ -498,16 +498,59 @@ ToolManager::~ToolManager() delete mDown; } +// This table describes the default configuration of the toolbars as +// a "tree" and must be kept in pre-order traversal. + +// In fact this tree is more of a broom -- nothing properly branches except +// at the root. + +// "Root" corresponds to left edge of the main window, and successive siblings +// go from top to bottom. But in practice layout may wrap this abstract +// configuration if the window size is narrow. + +static struct DefaultConfigEntry { + int barID; + int rightOf; // parent + int below; // preceding sibling +} DefaultConfigTable [] = { + // Top dock row, may wrap + { TransportBarID, NoBarID, NoBarID }, + { ToolsBarID, TransportBarID, NoBarID }, + { RecordMeterBarID, ToolsBarID, NoBarID }, + { PlayMeterBarID, RecordMeterBarID, NoBarID }, + { MixerBarID, PlayMeterBarID, NoBarID }, + { EditBarID, MixerBarID, NoBarID }, + { TranscriptionBarID, EditBarID, NoBarID }, + + // start another top dock row + { ScrubbingBarID, NoBarID, TransportBarID }, + { DeviceBarID, ScrubbingBarID, NoBarID }, + + // Hidden by default in top dock + { MeterBarID, NoBarID, NoBarID }, + + // Bottom dock + { SelectionBarID, NoBarID, NoBarID }, + + // Hidden by default in bottom dock + { SpectralSelectionBarID, NoBarID, NoBarID }, +}; + void ToolManager::Reset() { - int ndx; - // Disconnect all docked bars - for( ndx = 0; ndx < ToolBarCount; ndx++ ) + for ( const auto &entry : DefaultConfigTable ) { + int ndx = entry.barID; + ToolBar *bar = mBars[ ndx ]; + + ToolBarConfiguration::Position position { + (entry.rightOf == NoBarID) ? nullptr : mBars[ entry.rightOf ], + (entry.below == NoBarID) ? nullptr : mBars[ entry.below ] + }; + wxWindow *floater; ToolDock *dock; - ToolBar *bar = mBars[ ndx ]; bool expose = true; // Disconnect the bar @@ -558,7 +601,7 @@ void ToolManager::Reset() if( dock != NULL ) { // when we dock, we reparent, so bar is no longer a child of floater. - dock->Dock( bar, false ); + dock->Dock( bar, false, position ); Expose( ndx, expose ); //OK (and good) to DELETE floater, as bar is no longer in it. if( floater ) @@ -598,6 +641,14 @@ void ToolManager::Reset() Updated(); } +void ToolManager::RegenerateTooltips() +{ + for (auto bar : mBars) { + if (bar) + bar->RegenerateTooltips(); + } +} + // // Read the toolbar states // @@ -605,30 +656,23 @@ void ToolManager::ReadConfig() { wxString oldpath = gPrefs->GetPath(); wxArrayInt unordered[ DockCount ]; - int order[ DockCount ][ ToolBarCount ]; bool show[ ToolBarCount ]; int width[ ToolBarCount ]; int height[ ToolBarCount ]; int x, y; - int dock, ord, ndx; + int dock, ndx; + bool someFound { false }; #if defined(__WXMAC__) // Disable window animation wxSystemOptions::SetOption( wxMAC_WINDOW_PLAIN_TRANSITION, 1 ); #endif - // Invalidate all order entries - for( dock = 0; dock < DockCount; dock++ ) - { - for( ord = 0; ord < ToolBarCount; ord++ ) - { - order[ dock ][ ord ] = NoBarID; - } - } - // Change to the bar root gPrefs->SetPath( wxT("/GUI/ToolBars") ); + ToolBarConfiguration::Legacy topLegacy, botLegacy; + // Load and apply settings for each bar for( ndx = 0; ndx < ToolBarCount; ndx++ ) { @@ -657,9 +701,27 @@ void ToolManager::ReadConfig() #endif // Read in all the settings - gPrefs->Read( wxT("Dock"), &dock, defaultDock ); - gPrefs->Read( wxT("Order"), &ord, NoBarID ); - gPrefs->Read( wxT("Show"), &show[ ndx ], bShownByDefault); + gPrefs->Read( wxT("Dock"), &dock, -1); + const bool found = (dock != -1); + if (found) + someFound = true; + if (!found) + dock = defaultDock; + + ToolDock *d; + ToolBarConfiguration::Legacy *pLegacy; + switch(dock) + { + case TopDockID: d = mTopDock; pLegacy = &topLegacy; break; + case BotDockID: d = mBotDock; pLegacy = &botLegacy; break; + default: d = nullptr; pLegacy = nullptr; break; + } + + bool ordered = ToolBarConfiguration::Read + (d ? &d->GetConfiguration() : nullptr, + pLegacy, + bar, show[ ndx ], bShownByDefault) + && found; gPrefs->Read( wxT("X"), &x, -1 ); gPrefs->Read( wxT("Y"), &y, -1 ); @@ -719,15 +781,8 @@ void ToolManager::ReadConfig() } } #endif - // Is order within range and unoccupied? - if( ( ord >= 0 ) && - ( ord < ToolBarCount ) && - ( order[ dock - 1 ][ ord ] == NoBarID ) ) - { - // Insert at ordered location - order[ dock - 1 ][ ord ] = ndx; - } - else + + if (!ordered) { // These must go at the end unordered[ dock - 1 ].Add( ndx ); @@ -768,31 +823,18 @@ void ToolManager::ReadConfig() gPrefs->SetPath( wxT("/GUI/ToolBars") ); } + mTopDock->GetConfiguration().PostRead(topLegacy); + mBotDock->GetConfiguration().PostRead(botLegacy); + // Add all toolbars to their target dock for( dock = 0; dock < DockCount; dock++ ) { ToolDock *d = ( dock + 1 == TopDockID ? mTopDock : mBotDock ); - // Add all ordered toolbars - for( ord = 0; ord < ToolBarCount; ord++ ) - { - ndx = order[ dock ][ ord ]; - - // Bypass empty slots - if( ndx != NoBarID ) - { - ToolBar *t = mBars[ ndx ]; - - // Dock it - d->Dock( t, false ); - - // Show or hide it - Expose( t->GetId(), show[ t->GetId() ] ); - } - } + d->LoadConfig(); // Add all unordered toolbars - for( ord = 0; ord < (int) unordered[ dock ].GetCount(); ord++ ) + for( int ord = 0; ord < (int) unordered[ dock ].GetCount(); ord++ ) { ToolBar *t = mBars[ unordered[ dock ][ ord ] ]; @@ -811,6 +853,9 @@ void ToolManager::ReadConfig() // Reinstate original transition wxSystemOptions::SetOption( wxMAC_WINDOW_PLAIN_TRANSITION, mTransition ); #endif + + if (!someFound) + Reset(); } // @@ -838,13 +883,14 @@ void ToolManager::WriteConfig() gPrefs->SetPath( bar->GetSection() ); // Search both docks for toolbar order - int to = mTopDock->GetOrder( bar ); - int bo = mBotDock->GetOrder( bar ); + bool to = mTopDock->GetConfiguration().Contains( bar ); + bool bo = mBotDock->GetConfiguration().Contains( bar ); // Save gPrefs->Write( wxT("Dock"), (int) (to ? TopDockID : bo ? BotDockID : NoDockID )); - gPrefs->Write( wxT("Order"), to + bo ); - gPrefs->Write( wxT("Show"), IsVisible( ndx ) ); + auto dock = to ? mTopDock : bo ? mBotDock : nullptr; + ToolBarConfiguration::Write + (dock ? &dock->GetConfiguration() : nullptr, bar); wxPoint pos( -1, -1 ); wxSize sz = bar->GetSize(); @@ -858,13 +904,17 @@ void ToolManager::WriteConfig() gPrefs->Write( wxT("W"), sz.x ); gPrefs->Write( wxT("H"), sz.y ); - // Kill the bar - bar->Destroy(); - // Change back to the bar root gPrefs->SetPath( wxT("..") ); } + // Kill the bars + for( ndx = 0; ndx < ToolBarCount; ndx++ ) + { + ToolBar *bar = mBars[ ndx ]; + bar->Destroy(); + } + // Restore original config path gPrefs->SetPath( oldpath ); gPrefs->Flush(); @@ -1245,7 +1295,8 @@ void ToolManager::OnGrabber( GrabberEvent & event ) if (mDragBar->IsDocked()) { mPrevDock = dynamic_cast(mDragBar->GetParent()); wxASSERT(mPrevDock); - mPrevSlot = mPrevDock->Find(mDragBar); + mPrevSlot = mPrevDock->GetConfiguration().Find(mDragBar); + mPrevDock->WrapConfiguration(mPrevConfiguration); } else mPrevPosition = mDragBar->GetParent()->GetPosition(); @@ -1307,6 +1358,7 @@ void ToolManager::HandleEscapeKey() // Why don't you leave me alone? // Well, I feel so break up // I want to go home. + mPrevDock->RestoreConfiguration(mPrevConfiguration); mPrevDock->Dock( mDragBar, true, mPrevSlot ); // Done with the floater @@ -1340,7 +1392,8 @@ void ToolManager::DoneDragging() mDragDock = NULL; mDragBar = NULL; mPrevDock = NULL; - mPrevSlot = -1; + mPrevSlot = { ToolBarConfiguration::UnspecifiedPosition }; + mPrevConfiguration.Clear(); mLastPos.x = mBarPos.x = -1; mLastPos.y = mBarPos.y = -1; mTimer.Stop(); diff --git a/src/toolbars/ToolManager.h b/src/toolbars/ToolManager.h index 9c3050c12..4c4da2ece 100644 --- a/src/toolbars/ToolManager.h +++ b/src/toolbars/ToolManager.h @@ -65,6 +65,7 @@ class ToolManager final : public wxEvtHandler ToolDock *GetBotDock(); void Reset(); + void RegenerateTooltips(); private: @@ -90,7 +91,7 @@ class ToolManager final : public wxEvtHandler ToolDock *mDragDock; ToolBar *mDragBar {}; wxPoint mDragOffset; - int mDragBefore; + ToolBarConfiguration::Position mDragBefore {}; wxPoint mLastPos; wxRect mBarPos; @@ -114,7 +115,9 @@ class ToolManager final : public wxEvtHandler wxPoint mPrevPosition {}; ToolDock *mPrevDock {}; - int mPrevSlot {-1}; + ToolBarConfiguration::Position mPrevSlot + { ToolBarConfiguration::UnspecifiedPosition }; + ToolBarConfiguration mPrevConfiguration; public: diff --git a/src/toolbars/ToolsToolBar.cpp b/src/toolbars/ToolsToolBar.cpp index 20f0f3d03..e75842e16 100644 --- a/src/toolbars/ToolsToolBar.cpp +++ b/src/toolbars/ToolsToolBar.cpp @@ -111,7 +111,7 @@ ToolsToolBar::~ToolsToolBar() { } -void ToolsToolBar::RegenerateToolsTooltips() +void ToolsToolBar::RegenerateTooltips() { // JKC: @@ -136,12 +136,28 @@ void ToolsToolBar::RegenerateToolsTooltips() // wxSafeYield(); //Deal with some queued up messages... #if wxUSE_TOOLTIPS - mTool[selectTool]->SetToolTip(_("Selection Tool")); - mTool[envelopeTool]->SetToolTip(_("Envelope Tool")); - mTool[slideTool]->SetToolTip(_("Time Shift Tool")); - mTool[zoomTool]->SetToolTip(_("Zoom Tool")); - mTool[drawTool]->SetToolTip(_("Draw Tool")); - mTool[multiTool]->SetToolTip(_("Multi-Tool Mode")); + + static const struct Entry { + int tool; + wxString commandName; + wxString untranslatedLabel; + } table[] = { + { selectTool, wxT("SelectTool"), XO("Selection Tool") }, + { envelopeTool, wxT("EnvelopeTool"), XO("Envelope Tool") }, + { slideTool, wxT("TimeShiftTool"), XO("Time Shift Tool") }, + { zoomTool, wxT("ZoomTool"), XO("Zoom Tool") }, + { drawTool, wxT("DrawTool"), XO("Draw Tool") }, + { multiTool, wxT("MultiTool"), XO("Multi Tool") }, + }; + + std::vector commands; + for (const auto &entry : table) { + commands.clear(); + commands.push_back(wxGetTranslation(entry.untranslatedLabel)); + commands.push_back(entry.commandName); + ToolBar::SetButtonToolTip(*mTool[entry.tool], commands); + } + #endif // wxSafeYield(); @@ -150,7 +166,7 @@ void ToolsToolBar::RegenerateToolsTooltips() void ToolsToolBar::UpdatePrefs() { - RegenerateToolsTooltips(); + RegenerateTooltips(); } AButton * ToolsToolBar::MakeTool( teBmps eTool, @@ -183,7 +199,7 @@ void ToolsToolBar::Populate() mTool[mCurrentTool]->PushDown(); - RegenerateToolsTooltips(); + RegenerateTooltips(); } /// Gets the currently active tool diff --git a/src/toolbars/ToolsToolBar.h b/src/toolbars/ToolsToolBar.h index 4cc7937e9..0e76696c9 100644 --- a/src/toolbars/ToolsToolBar.h +++ b/src/toolbars/ToolsToolBar.h @@ -70,7 +70,7 @@ class ToolsToolBar final : public ToolBar { private: - void RegenerateToolsTooltips(); + void RegenerateTooltips() override; wxImage *MakeToolImage(wxImage *tool, wxImage *mask, int style); AButton *MakeTool(teBmps eTool, int id, const wxChar *label); diff --git a/src/toolbars/TranscriptionToolBar.cpp b/src/toolbars/TranscriptionToolBar.cpp index c4f22af04..334a26de2 100644 --- a/src/toolbars/TranscriptionToolBar.cpp +++ b/src/toolbars/TranscriptionToolBar.cpp @@ -298,7 +298,25 @@ void TranscriptionToolBar::UpdatePrefs() void TranscriptionToolBar::RegenerateTooltips() { - mButtons[TTB_PlaySpeed]->SetToolTip(_("Play-at-speed")); + // We could also mention the shift- and ctrl-modified versions in the + // tool tip... but it would get long + + static const struct Entry { + int tool; + wxString commandName; + wxString untranslatedLabel; + } table[] = { + { TTB_PlaySpeed, wxT("PlayAtSpeed"), XO("Play-at-speed") }, + }; + + std::vector commands; + for (const auto &entry : table) { + commands.clear(); + commands.push_back(wxGetTranslation(entry.untranslatedLabel)); + commands.push_back(entry.commandName); + ToolBar::SetButtonToolTip(*mButtons[entry.tool], commands); + } + #ifdef EXPERIMENTAL_VOICE_DETECTION mButtons[TTB_StartOn]->SetToolTip(TRANSLATABLE("Left-to-On")); diff --git a/src/toolbars/TranscriptionToolBar.h b/src/toolbars/TranscriptionToolBar.h index 734b37cce..28b7810d9 100644 --- a/src/toolbars/TranscriptionToolBar.h +++ b/src/toolbars/TranscriptionToolBar.h @@ -131,7 +131,7 @@ class TranscriptionToolBar final : public ToolBar { int id, unsigned altIdx); void GetSamples(WaveTrack *t, sampleCount *s0, sampleCount *slen); void SetButton(bool newstate, AButton *button); - void RegenerateTooltips(); + void RegenerateTooltips() override; AButton *mButtons[TTBNumButtons]; wxImage *upImage; diff --git a/src/widgets/AButton.cpp b/src/widgets/AButton.cpp index 83667a5d9..a5110932d 100644 --- a/src/widgets/AButton.cpp +++ b/src/widgets/AButton.cpp @@ -426,18 +426,8 @@ void AButton::OnMouseEvent(wxMouseEvent & event) if (newState != prevState) { Refresh(false); - if (mCursorIsInWindow) { - #if wxUSE_TOOLTIPS // Not available in wxX11 - // Display the tooltip in the status bar - wxToolTip * pTip = this->GetToolTip(); - if( pTip ) { - wxString tipText = pTip->GetTip(); - if (!mEnabled) - tipText += _(" (disabled)"); - GetActiveProject()->TP_DisplayStatusMessage(tipText); - } - #endif - } + if (mCursorIsInWindow) + UpdateStatus(); else { GetActiveProject()->TP_DisplayStatusMessage(wxT("")); } @@ -446,6 +436,22 @@ void AButton::OnMouseEvent(wxMouseEvent & event) event.Skip(); } +void AButton::UpdateStatus() +{ + if (mCursorIsInWindow) { +#if wxUSE_TOOLTIPS // Not available in wxX11 + // Display the tooltip in the status bar + wxToolTip * pTip = this->GetToolTip(); + if( pTip ) { + wxString tipText = pTip->GetTip(); + if (!mEnabled) + tipText += _(" (disabled)"); + GetActiveProject()->TP_DisplayStatusMessage(tipText); + } +#endif + } +} + void AButton::OnCaptureLost(wxMouseCaptureLostEvent & WXUNUSED(event)) { wxMouseEvent e(wxEVT_LEFT_UP); diff --git a/src/widgets/AButton.h b/src/widgets/AButton.h index 6858e7df8..3f1e3eeb4 100644 --- a/src/widgets/AButton.h +++ b/src/widgets/AButton.h @@ -99,6 +99,11 @@ class AButton final : public wxWindow { void OnPaint(wxPaintEvent & event); void OnSize(wxSizeEvent & event); void OnMouseEvent(wxMouseEvent & event); + + // Update the status bar message if the pointer is in the button. + // Else do nothing. + void UpdateStatus(); + void OnCaptureLost(wxMouseCaptureLostEvent & event); void OnKeyDown(wxKeyEvent & event); void OnSetFocus(wxFocusEvent & event); diff --git a/src/widgets/Ruler.cpp b/src/widgets/Ruler.cpp index bed7b01ae..ed0938c88 100644 --- a/src/widgets/Ruler.cpp +++ b/src/widgets/Ruler.cpp @@ -2060,19 +2060,6 @@ void AdornedRulerPanel::UpdatePrefs() RegenerateTooltips(mPrevZone); } -namespace -{ - wxString ComposeButtonLabel - (AudacityProject &project, const wxString &commandName, const wxString &label) - { - auto pCmdMgr = project.GetCommandManager(); - const auto &keyString = pCmdMgr->GetKeyFromName(commandName); - return keyString.empty() - ? label - : label + wxT(" (") + keyString + wxT(")"); - } -} - void AdornedRulerPanel::ReCreateButtons() { for (auto & button : mButtons) { @@ -2797,10 +2784,14 @@ void AdornedRulerPanel::OnContextMenu(wxContextMenuEvent & WXUNUSED(event)) void AdornedRulerPanel::UpdateButtonStates() { auto common = [this] - (wxWindow *button, const wxString &commandName, const wxString &label){ - const auto &fullLabel = ComposeButtonLabel(*mProject, commandName, label); - button->SetLabel(fullLabel); - button->SetToolTip(fullLabel); + (AButton &button, const wxString &commandName, const wxString &label) { + std::vector commands; + commands.push_back(label); + commands.push_back(commandName); + ToolBar::SetButtonToolTip(button, commands); + button.SetLabel(button.GetToolTipText()); + + button.UpdateStatus(); }; { @@ -2813,7 +2804,7 @@ void AdornedRulerPanel::UpdateButtonStates() // (which is, to toggle the state) ? _("Pinned play/record Head") : _("Unpinned play/record Head"); - common(pinButton, wxT("PinnedHead"), label); + common(*pinButton, wxT("PinnedHead"), label); } auto &scrubber = mProject->GetScrubber();