1 /*
2   ==============================================================================
3 
4    This file is part of the JUCE library.
5    Copyright (c) 2020 - Raw Material Software Limited
6 
7    JUCE is an open source library subject to commercial or open-source
8    licensing.
9 
10    By using JUCE, you agree to the terms of both the JUCE 6 End-User License
11    Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
12 
13    End User License Agreement: www.juce.com/juce-6-licence
14    Privacy Policy: www.juce.com/juce-privacy-policy
15 
16    Or: You may also use this code under the terms of the GPL v3 (see
17    www.gnu.org/licenses).
18 
19    JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
20    EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
21    DISCLAIMED.
22 
23   ==============================================================================
24 */
25 
26 namespace juce
27 {
28 
29 //==============================================================================
30 struct CustomMenuBarItemHolder    : public Component
31 {
CustomMenuBarItemHolderjuce::CustomMenuBarItemHolder32     CustomMenuBarItemHolder (const ReferenceCountedObjectPtr<PopupMenu::CustomComponent>& customComponent)
33     {
34         setInterceptsMouseClicks (false, true);
35         update (customComponent);
36     }
37 
updatejuce::CustomMenuBarItemHolder38     void update (const ReferenceCountedObjectPtr<PopupMenu::CustomComponent>& newComponent)
39     {
40         jassert (newComponent != nullptr);
41 
42         if (newComponent != custom)
43         {
44             if (custom != nullptr)
45                 removeChildComponent (custom.get());
46 
47             custom = newComponent;
48             addAndMakeVisible (*custom);
49             resized();
50         }
51     }
52 
resizedjuce::CustomMenuBarItemHolder53     void resized() override
54     {
55         custom->setBounds (getLocalBounds());
56     }
57 
58     ReferenceCountedObjectPtr<PopupMenu::CustomComponent> custom;
59 
60     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CustomMenuBarItemHolder)
61 };
62 
63 //==============================================================================
BurgerMenuComponent(MenuBarModel * modelToUse)64 BurgerMenuComponent::BurgerMenuComponent (MenuBarModel* modelToUse)
65 {
66     lookAndFeelChanged();
67     listBox.addMouseListener (this, true);
68 
69     setModel (modelToUse);
70     addAndMakeVisible (listBox);
71 }
72 
~BurgerMenuComponent()73 BurgerMenuComponent::~BurgerMenuComponent()
74 {
75     if (model != nullptr)
76         model->removeListener (this);
77 }
78 
setModel(MenuBarModel * newModel)79 void BurgerMenuComponent::setModel (MenuBarModel* newModel)
80 {
81     if (newModel != model)
82     {
83         if (model != nullptr)
84             model->removeListener (this);
85 
86         model = newModel;
87 
88         if (model != nullptr)
89             model->addListener (this);
90 
91         refresh();
92         listBox.updateContent();
93     }
94 }
95 
getModel() const96 MenuBarModel* BurgerMenuComponent::getModel() const noexcept
97 {
98     return model;
99 }
100 
refresh()101 void BurgerMenuComponent::refresh()
102 {
103     lastRowClicked = inputSourceIndexOfLastClick = -1;
104 
105     rows.clear();
106 
107     if (model != nullptr)
108     {
109         auto menuBarNames = model->getMenuBarNames();
110 
111         for (auto menuIdx = 0; menuIdx < menuBarNames.size(); ++menuIdx)
112         {
113             PopupMenu::Item menuItem;
114             menuItem.text = menuBarNames[menuIdx];
115 
116             String ignore;
117             auto menu = model->getMenuForIndex (menuIdx, ignore);
118 
119             rows.add (Row { true, menuIdx, menuItem });
120             addMenuBarItemsForMenu (menu, menuIdx);
121         }
122     }
123 }
124 
addMenuBarItemsForMenu(PopupMenu & menu,int menuIdx)125 void BurgerMenuComponent::addMenuBarItemsForMenu (PopupMenu& menu, int menuIdx)
126 {
127     for (PopupMenu::MenuItemIterator it (menu); it.next();)
128     {
129         auto& item = it.getItem();
130 
131         if (item.isSeparator)
132             continue;
133 
134         if (hasSubMenu (item))
135             addMenuBarItemsForMenu (*item.subMenu, menuIdx);
136         else
137             rows.add (Row {false, menuIdx, it.getItem()});
138     }
139 }
140 
getNumRows()141 int BurgerMenuComponent::getNumRows()
142 {
143     return rows.size();
144 }
145 
paint(Graphics & g)146 void BurgerMenuComponent::paint (Graphics& g)
147 {
148     getLookAndFeel().drawPopupMenuBackground (g, getWidth(), getHeight());
149 }
150 
paintListBoxItem(int rowIndex,Graphics & g,int w,int h,bool highlight)151 void BurgerMenuComponent::paintListBoxItem (int rowIndex, Graphics& g, int w, int h, bool highlight)
152 {
153     auto& lf = getLookAndFeel();
154     Rectangle<int> r (w, h);
155 
156     auto row = (rowIndex < rows.size() ? rows.getReference (rowIndex)
157                                        : Row { true, 0, {} });
158 
159     g.fillAll (findColour (PopupMenu::backgroundColourId));
160 
161     if (row.isMenuHeader)
162     {
163         lf.drawPopupMenuSectionHeader (g, r.reduced (20, 0), row.item.text);
164         g.setColour (Colours::grey);
165         g.fillRect (r.withHeight (1));
166     }
167     else
168     {
169         auto& item = row.item;
170         auto* colour = item.colour != Colour() ? &item.colour : nullptr;
171 
172         if (item.customComponent == nullptr)
173             lf.drawPopupMenuItem (g, r.reduced (20, 0),
174                                   item.isSeparator,
175                                   item.isEnabled,
176                                   highlight,
177                                   item.isTicked,
178                                   hasSubMenu (item),
179                                   item.text,
180                                   item.shortcutKeyDescription,
181                                   item.image.get(),
182                                   colour);
183     }
184 }
185 
hasSubMenu(const PopupMenu::Item & item)186 bool BurgerMenuComponent::hasSubMenu (const PopupMenu::Item& item)
187 {
188     return item.subMenu != nullptr && (item.itemID == 0 || item.subMenu->getNumItems() > 0);
189 }
190 
listBoxItemClicked(int rowIndex,const MouseEvent & e)191 void BurgerMenuComponent::listBoxItemClicked (int rowIndex, const MouseEvent& e)
192 {
193     auto row = rowIndex < rows.size() ? rows.getReference (rowIndex)
194                                       : Row { true, 0, {} };
195 
196     if (! row.isMenuHeader)
197     {
198         lastRowClicked = rowIndex;
199         inputSourceIndexOfLastClick = e.source.getIndex();
200     }
201 }
202 
refreshComponentForRow(int rowIndex,bool isRowSelected,Component * existing)203 Component* BurgerMenuComponent::refreshComponentForRow (int rowIndex, bool isRowSelected, Component* existing)
204 {
205     auto row = rowIndex < rows.size() ? rows.getReference (rowIndex)
206                                       : Row { true, 0, {} };
207 
208     auto hasCustomComponent = (row.item.customComponent != nullptr);
209 
210     if (existing == nullptr && hasCustomComponent)
211         return new CustomMenuBarItemHolder (row.item.customComponent);
212 
213     if (existing != nullptr)
214     {
215         auto* componentToUpdate = dynamic_cast<CustomMenuBarItemHolder*> (existing);
216         jassert (componentToUpdate != nullptr);
217 
218         if (hasCustomComponent && componentToUpdate != nullptr)
219         {
220             row.item.customComponent->setHighlighted (isRowSelected);
221             componentToUpdate->update (row.item.customComponent);
222         }
223         else
224         {
225             delete existing;
226             existing = nullptr;
227         }
228     }
229 
230     return existing;
231 }
232 
resized()233 void BurgerMenuComponent::resized()
234 {
235     listBox.setBounds (getLocalBounds());
236 }
237 
menuBarItemsChanged(MenuBarModel * menuBarModel)238 void BurgerMenuComponent::menuBarItemsChanged (MenuBarModel* menuBarModel)
239 {
240     setModel (menuBarModel);
241 }
242 
menuCommandInvoked(MenuBarModel *,const ApplicationCommandTarget::InvocationInfo &)243 void BurgerMenuComponent::menuCommandInvoked (MenuBarModel*, const ApplicationCommandTarget::InvocationInfo&)
244 {
245 }
246 
mouseUp(const MouseEvent & event)247 void BurgerMenuComponent::mouseUp (const MouseEvent& event)
248 {
249     auto rowIndex = listBox.getSelectedRow();
250 
251     if (rowIndex == lastRowClicked && rowIndex < rows.size()
252          && event.source.getIndex() == inputSourceIndexOfLastClick)
253     {
254         auto& row = rows.getReference (rowIndex);
255 
256         if (! row.isMenuHeader)
257         {
258             listBox.selectRow (-1);
259 
260             lastRowClicked = -1;
261             inputSourceIndexOfLastClick = -1;
262 
263             topLevelIndexClicked = row.topLevelMenuIndex;
264             auto& item = row.item;
265 
266             if (auto* managerOfChosenCommand = item.commandManager)
267             {
268                 ApplicationCommandTarget::InvocationInfo info (item.itemID);
269                 info.invocationMethod = ApplicationCommandTarget::InvocationInfo::fromMenu;
270 
271                 managerOfChosenCommand->invoke (info, true);
272             }
273 
274             postCommandMessage (item.itemID);
275         }
276     }
277 }
278 
handleCommandMessage(int commandID)279 void BurgerMenuComponent::handleCommandMessage (int commandID)
280 {
281     if (model != nullptr)
282     {
283         model->menuItemSelected (commandID, topLevelIndexClicked);
284         topLevelIndexClicked = -1;
285 
286         refresh();
287         listBox.updateContent();
288     }
289 }
290 
lookAndFeelChanged()291 void BurgerMenuComponent::lookAndFeelChanged()
292 {
293     listBox.setRowHeight (roundToInt (getLookAndFeel().getPopupMenuFont().getHeight() * 2.0f));
294 }
295 
296 } // namespace juce
297