Mema
Memory Matrix — multi-channel audio matrix monitor and router
Loading...
Searching...
No Matches
Main.cpp
Go to the documentation of this file.
1/* Copyright (c) 2024, Christian Ahrens
2 *
3 * This file is part of Mema <https://github.com/ChristianAhrens/Mema>
4 *
5 * This tool is free software; you can redistribute it and/or modify it under
6 * the terms of the GNU Lesser General Public License version 3.0 as published
7 * by the Free Software Foundation.
8 *
9 * This tool is distributed in the hope that it will be useful, but WITHOUT
10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
12 * details.
13 *
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this tool; if not, write to the Free Software Foundation, Inc.,
16 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17 */
18
19#include <JuceHeader.h>
20
21#include "Mema.h"
22#include "MemaUIComponent.h"
24#include "HeadlessCLIMenu.h"
25
26#include <AppConfigurationBase.h>
27
28#if JUCE_MAC
29#include <signal.h>
30#endif
31
32#if JUCE_WINDOWS
33// Required for AttachConsole / AllocConsole used in headless mode.
34#include <windows.h>
35#include <cstdio>
36#endif
37
38//==============================================================================
39class MemaMacMainMenuMenuBarModel : public juce::MenuBarModel
40{
41public:
42 //==============================================================================
43 void addMenu (int topLevelMenuIndex, const String& menuName, const juce::PopupMenu& popupMenu)
44 {
45 m_popupMenus[topLevelMenuIndex] = popupMenu;
46 m_popupMenuNames[topLevelMenuIndex] = menuName;
47 }
48
49 juce::String getMenuNameForIndex(int topLevelMenuIndex)
50 {
51 jassert(m_popupMenuNames.count(topLevelMenuIndex) != 0);
52
53 return m_popupMenuNames[topLevelMenuIndex];
54 }
55
56 juce::PopupMenu& getMenuRefForIndex(int topLevelMenuIndex)
57 {
58 jassert(m_popupMenus.count(topLevelMenuIndex) != 0);
59
60 return m_popupMenus[topLevelMenuIndex];
61 }
62
63 //==============================================================================
64 juce::StringArray getMenuBarNames() override
65 {
66 juce::StringArray menuBarNames;
67 for (auto const& menuName : m_popupMenuNames)
68 menuBarNames.add(menuName.second);
69
70 return menuBarNames;
71 }
72
73 juce::PopupMenu getMenuForIndex (int topLevelMenuIndex, const String& menuName) override
74 {
75 ignoreUnused(menuName);
76
77 if (m_popupMenus.count(topLevelMenuIndex) == 0) { jassertfalse; return {}; }
78 if (m_popupMenuNames.count(topLevelMenuIndex) == 0) { jassertfalse; return {}; }
79
80 return m_popupMenus[topLevelMenuIndex];
81 }
82
83 void menuItemSelected (int menuItemID, int topLevelMenuIndex) override
84 {
86 onMenuItemSelected(menuItemID, topLevelMenuIndex);
87 }
88
89 //==============================================================================
90 std::function<void(int, int)> onMenuItemSelected;
91
92private:
93 //==============================================================================
94 std::map<int, juce::PopupMenu> m_popupMenus;
95 std::map<int, juce::String> m_popupMenuNames;
96};
97
98//==============================================================================
99class MemaApplication : public juce::JUCEApplication
100{
101public:
102 //==============================================================================
106
107 Mema::Mema::deleteInstance();
108 };
109
110 const juce::String getApplicationName() override { return ProjectInfo::projectName; }
111 const juce::String getApplicationVersion() override { return ProjectInfo::versionString; }
112 bool moreThanOneInstanceAllowed() override { return false; }
113
114 //==============================================================================
115 void initialise (const juce::String& commandLine) override
116 {
117 auto isHeadless = commandLine.contains("--headless");
118
119#if JUCE_WINDOWS
120 if (isHeadless)
121 {
122 // JUCE GUI applications are built as SUBSYSTEM:WINDOWS, so the process
123 // has no console attached even when launched from a terminal.
124 // Attach to the parent shell's console (cmd.exe / PowerShell), or open
125 // a fresh console window when Mema was not launched from a terminal.
126 if (!AttachConsole(ATTACH_PARENT_PROCESS))
127 AllocConsole();
128
129 // Reopen the standard C streams so std::cin / std::cout work correctly.
130 FILE* dummy = nullptr;
131 freopen_s(&dummy, "CON", "r", stdin);
132 freopen_s(&dummy, "CON", "w", stdout);
133 freopen_s(&dummy, "CON", "w", stderr);
134 }
135#endif
136
137#if JUCE_MAC
138 // Ignore SIGPIPE globally, to prevent occasional unexpected app
139 // termination when Mema.Mo instances disconnect while sending by
140 // writing to socket is ongoing
141 signal(SIGPIPE, SIG_IGN);
142#endif
143
144 if (!isHeadless)
145 {
146 // a single instance of tooltip window is required and used by JUCE everywhere a tooltip is required.
147 m_toolTipWindowInstance = std::make_unique<TooltipWindow>();
148
149 m_taskbarComponent = std::make_unique<MemaTaskbarComponent>([=](juce::Point<int> mousePosition) { showUiAsCalloutBox(mousePosition); });
150 m_taskbarComponent->setName(getApplicationName() + " taskbar icon");
151 }
152
153 Mema::Mema::getInstance();
154
155 if (isHeadless)
156 {
157 // Start the interactive CLI configuration menu on its own thread so
158 // that blocking stdin reads never stall the JUCE message loop.
159 auto& proc = Mema::Mema::getInstance()->getMemaProcessor();
160 jassert(proc);
161 if (proc != nullptr)
162 {
163 m_cliMenu = std::make_unique<Mema::HeadlessCLIMenu>(*proc);
164 m_cliMenu->startThread();
165 }
166 }
167
168 if (!isHeadless)
169 {
170#if JUCE_MAC
171 m_macMainMenu = std::make_unique<MemaMacMainMenuMenuBarModel>();
172 auto optionsPopupMenu = juce::PopupMenu();
173 optionsPopupMenu.addItem("Show as standalone window", true, false, [=]() {
174 if (auto currentProcEditor = Mema::Mema::getInstance()->getMemaProcessorEditor())
175 if (auto callout = currentProcEditor->findParentComponentOfClass<juce::CallOutBox>())
176 callout->exitModalState(0);
177 juce::MessageManager::callAsync([=]() { showUiAsStandaloneWindow(); });
178 });
179 optionsPopupMenu.addSeparator();
180 optionsPopupMenu.addItem("Load config...", true, false, [=]() {
181 Mema::Mema::getInstance()->triggerPromptLoadConfig();
182 });
183 optionsPopupMenu.addItem("Save config...", true, false, [=]() {
184 Mema::Mema::getInstance()->triggerPromptSaveConfig();
185 });
186 m_macMainMenu->addMenu(0, "Options", optionsPopupMenu);
187
188 juce::MenuBarModel::setMacMainMenu(m_macMainMenu.get());
189#elif JUCE_LINUX
191#endif
192 }
193 }
194
195 void shutdown() override
196 {
197 // Stop the CLI menu thread before the processor is destroyed.
198 if (m_cliMenu != nullptr)
199 {
200 m_cliMenu->stopThread(3000);
201 m_cliMenu.reset();
202 }
203
204#if JUCE_MAC
205 juce::MenuBarModel::setMacMainMenu(nullptr);
206#endif
207 }
208
209 //==============================================================================
210 std::unique_ptr<Mema::MemaUIComponent> createAndConnectMemaUIComponent()
211 {
212 if (!Mema::Mema::getInstanceWithoutCreating())
213 return {};
214
215 auto memaUIComponent = std::make_unique<Mema::MemaUIComponent>().release();
216 Mema::Mema::getInstance()->onEditorSizeChangeRequested = [memaUIComponent, this](juce::Rectangle<int> requestedSize) {
217 m_lastRequestedEditorSize = requestedSize;
218 jassert(memaUIComponent);
219 if (memaUIComponent) memaUIComponent->handleEditorSizeChangeRequest(requestedSize);
220 };
221 Mema::Mema::getInstance()->onCpuUsageUpdate = [=](int loadPercent) {
222 jassert(memaUIComponent);
223 if (memaUIComponent) memaUIComponent->updateCpuUsageBar(loadPercent);
224 };
225 Mema::Mema::getInstance()->onNetworkUsageUpdate = [=](std::map<int, std::pair<double, bool>> netLoads) {
226 jassert(memaUIComponent);
227 if (memaUIComponent) memaUIComponent->updateNetworkUsage(netLoads);
228 };
229 Mema::Mema::getInstance()->onServiceDiscoveryTopologyUpdate = [=](const JUCEAppBasics::SessionServiceTopology& sessionServiceTopology) {
230 jassert(memaUIComponent);
231 if (memaUIComponent) memaUIComponent->updateSessionServiceTopology(sessionServiceTopology);
232 };
233 memaUIComponent->setEditorComponent(Mema::Mema::getInstance()->getMemaProcessorEditor());
234 memaUIComponent->setVisible(m_isMainComponentVisible);
235 memaUIComponent->addToDesktop(juce::ComponentPeer::windowHasDropShadow);
236 memaUIComponent->setTopLeftPosition(m_taskbarComponent->getX(), 50);
237 memaUIComponent->setName(ProjectInfo::projectName);
238 memaUIComponent->onToggleStandaloneWindow = [=](bool standalone) {
239 if (standalone)
240 {
241 if (auto callout = memaUIComponent->findParentComponentOfClass<juce::CallOutBox>())
242 callout->exitModalState(0);
243 juce::MessageManager::callAsync([=]() { showUiAsStandaloneWindow(); });
244 }
245 else
246 {
247 if (m_memaUIComponent)
248 m_memaUIComponent->setStandaloneWindow(false);
250 }
251 };
252 memaUIComponent->onLookAndFeelChanged = [=]() {
253 if (Mema::Mema::getInstanceWithoutCreating()) Mema::Mema::getInstance()->propagateLookAndFeelChanged();
254 };
255 memaUIComponent->onAudioSetupMenuClicked = [=]() {
256 if (Mema::Mema::getInstanceWithoutCreating())
257 {
258 juce::PopupMenu setupMenu;
259 setupMenu.addCustomItem(1, std::make_unique<CustomAboutItem>(Mema::Mema::getInstance()->getDeviceSetupComponent(), juce::Rectangle<int>(300, 350)), nullptr, "Audio Device Setup");
260 setupMenu.showMenuAsync(juce::PopupMenu::Options());
261 }
262 };
263 memaUIComponent->onDeleted = [this]() {
264 if (Mema::Mema::getInstanceWithoutCreating())
265 {
266 Mema::Mema::getInstance()->clearUICallbacks();
267 }
268
269 m_memaUIComponent.release();
270 m_memaUIComponent = nullptr;
271 };
272 memaUIComponent->onSettingsChanged = [=]() {
273 jassert(memaUIComponent);
274 if (memaUIComponent && Mema::Mema::getInstanceWithoutCreating())
275 {
276 Mema::Mema::getInstance()->setUIConfigState(memaUIComponent->createStateXml());
277 JUCEAppBasics::AppConfigurationBase::getInstance()->triggerConfigurationDump();
278 }
279 };
280 memaUIComponent->onPaletteStyleChange = [=](const JUCEAppBasics::CustomLookAndFeel::PaletteStyle& paletteStyle) {
281 m_lookAndFeel = std::make_unique<JUCEAppBasics::CustomLookAndFeel>(paletteStyle);
282 juce::Desktop::getInstance().setDefaultLookAndFeel(m_lookAndFeel.get());
283 };
284 memaUIComponent->onLoadConfig = [=]() {
285 Mema::Mema::getInstance()->triggerPromptLoadConfig();
286 };
287 memaUIComponent->onSaveConfig = [=]() {
288 Mema::Mema::getInstance()->triggerPromptSaveConfig();
289 };
290
291 memaUIComponent->handleEditorSizeChangeRequest(m_lastRequestedEditorSize);
292 memaUIComponent->lookAndFeelChanged();
293 memaUIComponent->setStateXml(Mema::Mema::getInstance()->getUIConfigState().get());
294 memaUIComponent->resized();
295 memaUIComponent->grabKeyboardFocus();
296
297 return std::unique_ptr<Mema::MemaUIComponent>(memaUIComponent);
298 }
299
301 {
302 if (Mema::Mema::getInstanceWithoutCreating())
303 {
304 Mema::Mema::getInstance()->onServiceDiscoveryTopologyUpdate = nullptr;
305 Mema::Mema::getInstance()->onEditorSizeChangeRequested = nullptr;
306 Mema::Mema::getInstance()->onCpuUsageUpdate = nullptr;
307 Mema::Mema::getInstance()->onNetworkUsageUpdate = nullptr;
308 }
309
310 m_memaUIComponent.reset();
311 }
312
313 void showUiAsCalloutBox(const juce::Point<int>& positionToPointTo)
314 {
315 // in case the ui is currently shown as standalone window, clean that up first
317
318 auto const display = juce::Desktop::getInstance().getDisplays().getPrimaryDisplay();
319 jassert(display);
320 auto position = display->userArea.getConstrainedPoint(positionToPointTo);
321
322 // On OSX, there can be problems launching a menu when we're not the foreground
323 // process, so just in case, we'll first make our process active,
324 // and bring our windows to the front.
325 juce::Process::makeForegroundProcess();
326
327 juce::CallOutBox::launchAsynchronously(createAndConnectMemaUIComponent(), { position, position }, nullptr);
328 }
329
331 {
332 m_memaUIComponent.reset();
333 m_memaUIComponent = createAndConnectMemaUIComponent();
334 jassert(m_memaUIComponent);
335 m_memaUIComponent->setStandaloneWindow(true);
336
337 auto showPosition = juce::Desktop::getInstance().getMousePosition();
338 auto const display = juce::Desktop::getInstance().getDisplays().getPrimaryDisplay();
339 if (nullptr != display && nullptr != m_memaUIComponent)
340 {
341 if (display->userArea.getHeight() < showPosition.getY() + m_memaUIComponent->getHeight())
342 showPosition.setY(showPosition.getY() - m_memaUIComponent->getHeight() - 30);
343 if (display->userArea.getWidth() < showPosition.getX() + m_memaUIComponent->getWidth())
344 showPosition.setX(showPosition.getX() - m_memaUIComponent->getWidth() - 30);
345 }
346 m_memaUIComponent->setTopLeftPosition(showPosition);
347 }
348
349 // Just add a simple icon to the Window system tray area or Mac menu bar..
350 struct MemaTaskbarComponent : public juce::SystemTrayIconComponent
351 {
352 MemaTaskbarComponent(std::function<void(juce::Point<int>)> callback) : onMouseDownWithPosition(callback)
353 {
354 setIconImage(juce::ImageFileFormat::loadFrom(BinaryData::grid_4x4_24dp_png, BinaryData::grid_4x4_24dp_pngSize),
355 juce::ImageFileFormat::loadFrom(BinaryData::grid_4x4_24dp_png, BinaryData::grid_4x4_24dp_pngSize));
356 setIconTooltip(JUCEApplication::getInstance()->getApplicationName());
357 }
358
359 void mouseDown(const juce::MouseEvent&) override
360 {
361 if (onMouseDownWithPosition)
362 onMouseDownWithPosition(juce::Desktop::getMousePosition());
363 }
364
365 private:
366 std::function<void(juce::Point<int>)> onMouseDownWithPosition;
367 };
368
369private:
370 bool m_isMainComponentVisible = false;
371 juce::Rectangle<int> m_lastRequestedEditorSize;
372
373 std::unique_ptr<Mema::MemaUIComponent> m_memaUIComponent;
374 std::unique_ptr<juce::Component> m_taskbarComponent;
375 std::unique_ptr<juce::TooltipWindow> m_toolTipWindowInstance;
376 std::unique_ptr<juce::LookAndFeel> m_lookAndFeel;
377
379 std::unique_ptr<Mema::HeadlessCLIMenu> m_cliMenu;
380
381#if JUCE_MAC
382 std::unique_ptr<MemaMacMainMenuMenuBarModel> m_macMainMenu;
383#endif
384
385 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MemaApplication)
386};
387
388//==============================================================================
389// This macro generates the main() routine that launches the app.
390START_JUCE_APPLICATION (MemaApplication)
void disconnectAndDeleteMemaUIComponent()
Definition Main.cpp:300
void shutdown() override
Definition Main.cpp:195
const juce::String getApplicationName() override
Definition Main.cpp:110
const juce::String getApplicationVersion() override
Definition Main.cpp:111
void showUiAsCalloutBox(const juce::Point< int > &positionToPointTo)
Definition Main.cpp:313
void showUiAsStandaloneWindow()
Definition Main.cpp:330
std::unique_ptr< Mema::MemaUIComponent > createAndConnectMemaUIComponent()
Definition Main.cpp:210
bool moreThanOneInstanceAllowed() override
Definition Main.cpp:112
void initialise(const juce::String &commandLine) override
Definition Main.cpp:115
juce::String getMenuNameForIndex(int topLevelMenuIndex)
Definition Main.cpp:49
void menuItemSelected(int menuItemID, int topLevelMenuIndex) override
Definition Main.cpp:83
juce::PopupMenu & getMenuRefForIndex(int topLevelMenuIndex)
Definition Main.cpp:56
juce::PopupMenu getMenuForIndex(int topLevelMenuIndex, const String &menuName) override
Definition Main.cpp:73
juce::StringArray getMenuBarNames() override
Definition Main.cpp:64
void addMenu(int topLevelMenuIndex, const String &menuName, const juce::PopupMenu &popupMenu)
Definition Main.cpp:43
std::function< void(int, int)> onMenuItemSelected
Definition Main.cpp:90
void mouseDown(const juce::MouseEvent &) override
Definition Main.cpp:359
MemaTaskbarComponent(std::function< void(juce::Point< int >)> callback)
Definition Main.cpp:352