RAM2118 commited on
Commit
6063997
·
verified ·
1 Parent(s): d966811

Upload Source/Plugin/PluginBridgeEditor.mm

Browse files
Files changed (1) hide show
  1. Source/Plugin/PluginBridgeEditor.mm +387 -0
Source/Plugin/PluginBridgeEditor.mm ADDED
@@ -0,0 +1,387 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include "PluginBridgeEditor.h"
2
+
3
+ #if JUCE_MAC
4
+ #import <Cocoa/Cocoa.h>
5
+ #import <QuartzCore/QuartzCore.h>
6
+ #endif
7
+
8
+ #include "json.hpp"
9
+ using json = nlohmann::json;
10
+
11
+ static const juce::Colour kBgDark (0xff0d0d1a);
12
+ static const juce::Colour kBgPanel (0xff1a1a2e);
13
+ static const juce::Colour kAccent (0xff6c63ff);
14
+ static const juce::Colour kTextPrimary (0xffe8e8f0);
15
+ static const juce::Colour kTextSecondary(0xff8888aa);
16
+ static const juce::Colour kGreen (0xff4cef50);
17
+ static const juce::Colour kRed (0xffef5350);
18
+
19
+ PluginBridgeEditor::PluginBridgeEditor(PluginBridgeProcessor& p)
20
+ : AudioProcessorEditor(&p), processorRef(p)
21
+ {
22
+ setSize(kDefaultWidth, kDefaultHeight);
23
+ setResizable(true, true);
24
+
25
+ titleLabel.setText("PluginBridge", juce::dontSendNotification);
26
+ titleLabel.setFont(juce::FontOptions(13.0f));
27
+ titleLabel.setJustificationType(juce::Justification::centredLeft);
28
+ titleLabel.setColour(juce::Label::textColourId, kTextPrimary);
29
+ addAndMakeVisible(titleLabel);
30
+
31
+ mcpStatusLabel.setFont(juce::FontOptions(10.0f));
32
+ mcpStatusLabel.setJustificationType(juce::Justification::centredRight);
33
+ addAndMakeVisible(mcpStatusLabel);
34
+
35
+ pluginStatusLabel.setFont(juce::FontOptions(10.0f));
36
+ pluginStatusLabel.setJustificationType(juce::Justification::centredLeft);
37
+ pluginStatusLabel.setColour(juce::Label::textColourId, kTextSecondary);
38
+ addAndMakeVisible(pluginStatusLabel);
39
+
40
+ selectButton.onClick = [this]() { showPluginMenu(); };
41
+ selectButton.setColour(juce::TextButton::buttonColourId, kAccent);
42
+ selectButton.setColour(juce::TextButton::textColourOffId, kTextPrimary);
43
+ addAndMakeVisible(selectButton);
44
+
45
+ // Callbacks
46
+ processorRef.onPluginLoaded = [this]()
47
+ {
48
+ juce::MessageManager::callAsync([this]()
49
+ {
50
+ showingError = false;
51
+ // Request GUI display
52
+ processorRef.showPluginGui();
53
+ });
54
+ };
55
+
56
+ processorRef.onPluginUnloaded = [this]()
57
+ {
58
+ juce::MessageManager::callAsync([this]()
59
+ {
60
+ disconnectRemoteLayer();
61
+ setSize(kDefaultWidth, kDefaultHeight);
62
+ });
63
+ };
64
+
65
+ processorRef.onGuiReady = [this]()
66
+ {
67
+ juce::MessageManager::callAsync([this]() { connectRemoteLayer(); });
68
+ };
69
+
70
+ processorRef.onGuiResized = [this](int w, int h)
71
+ {
72
+ juce::MessageManager::callAsync([this, w, h]()
73
+ {
74
+ setSize(std::max(w, kDefaultWidth), h + kTopBarHeight);
75
+ });
76
+ };
77
+
78
+ processorRef.onLoadFailed = [this](const juce::String& error)
79
+ {
80
+ juce::MessageManager::callAsync([this, error]()
81
+ {
82
+ pluginStatusLabel.setText(error, juce::dontSendNotification);
83
+ pluginStatusLabel.setColour(juce::Label::textColourId, kRed);
84
+ showingError = true;
85
+ });
86
+ };
87
+
88
+ // If already loaded (restoring state)
89
+ if (processorRef.isPluginLoaded() && processorRef.getRemoteLayerPortName().isNotEmpty())
90
+ connectRemoteLayer();
91
+
92
+ startTimerHz(4);
93
+ }
94
+
95
+ PluginBridgeEditor::~PluginBridgeEditor()
96
+ {
97
+ stopTimer();
98
+ processorRef.onPluginLoaded = nullptr;
99
+ processorRef.onPluginUnloaded = nullptr;
100
+ processorRef.onGuiReady = nullptr;
101
+ processorRef.onGuiResized = nullptr;
102
+ processorRef.onLoadFailed = nullptr;
103
+ disconnectRemoteLayer();
104
+ }
105
+
106
+ void PluginBridgeEditor::paint(juce::Graphics& g)
107
+ {
108
+ g.fillAll(kBgDark);
109
+
110
+ auto topBar = getLocalBounds().removeFromTop(kTopBarHeight);
111
+ g.setColour(kBgPanel);
112
+ g.fillRect(topBar);
113
+
114
+ g.setColour(kAccent.withAlpha(0.3f));
115
+ g.drawHorizontalLine(kTopBarHeight - 1, 0.0f, (float)getWidth());
116
+
117
+ // If no remote layer connected, show message in the body area
118
+ if (!layerConnected && !processorRef.isPluginLoaded())
119
+ {
120
+ g.setColour(kTextSecondary);
121
+ g.setFont(juce::FontOptions(12.0f));
122
+ auto body = getLocalBounds().withTrimmedTop(kTopBarHeight);
123
+ if (body.getHeight() > 20)
124
+ g.drawText("Select a plugin to load", body, juce::Justification::centred);
125
+ }
126
+ }
127
+
128
+ void PluginBridgeEditor::resized()
129
+ {
130
+ auto area = getLocalBounds();
131
+ auto topBar = area.removeFromTop(kTopBarHeight).reduced(4, 2);
132
+
133
+ titleLabel.setBounds(topBar.removeFromLeft(80));
134
+ selectButton.setBounds(topBar.removeFromLeft(100).reduced(0, 2));
135
+ topBar.removeFromLeft(4);
136
+ pluginStatusLabel.setBounds(topBar.removeFromLeft(topBar.getWidth() - 60));
137
+ mcpStatusLabel.setBounds(topBar);
138
+
139
+ #if JUCE_MAC
140
+ // Resize the remote CALayer to fill the area below the top bar
141
+ if (layerConnected && remoteLayerClient)
142
+ {
143
+ CARemoteLayerClient* client = (__bridge CARemoteLayerClient*)remoteLayerClient;
144
+ if (client.layer)
145
+ {
146
+ auto body = getLocalBounds().withTrimmedTop(kTopBarHeight);
147
+ client.layer.frame = CGRectMake(0, 0, body.getWidth(), body.getHeight());
148
+ }
149
+ }
150
+ #endif
151
+ }
152
+
153
+ void PluginBridgeEditor::timerCallback()
154
+ {
155
+ if (processorRef.isMcpServerRunning())
156
+ {
157
+ mcpStatusLabel.setText("MCP:" + juce::String(processorRef.getMcpServerPort()), juce::dontSendNotification);
158
+ mcpStatusLabel.setColour(juce::Label::textColourId, kGreen);
159
+ }
160
+ else
161
+ {
162
+ mcpStatusLabel.setText("MCP:off", juce::dontSendNotification);
163
+ mcpStatusLabel.setColour(juce::Label::textColourId, kRed);
164
+ }
165
+
166
+ if (processorRef.isPluginLoaded())
167
+ {
168
+ selectButton.setButtonText(processorRef.getLoadedPluginName());
169
+ if (!showingError)
170
+ {
171
+ pluginStatusLabel.setText(juce::String(processorRef.getLoadedPluginParamCount()) + " params", juce::dontSendNotification);
172
+ pluginStatusLabel.setColour(juce::Label::textColourId, kGreen);
173
+ }
174
+ }
175
+ else if (!showingError)
176
+ {
177
+ selectButton.setButtonText("Select Plugin");
178
+ pluginStatusLabel.setText("", juce::dontSendNotification);
179
+ }
180
+
181
+ // Check for crash
182
+ if (processorRef.getConnectionState() == HelperConnection::State::Crashed)
183
+ {
184
+ disconnectRemoteLayer();
185
+ pluginStatusLabel.setText("Plugin crashed — select again to retry", juce::dontSendNotification);
186
+ pluginStatusLabel.setColour(juce::Label::textColourId, kRed);
187
+ showingError = true;
188
+ }
189
+ }
190
+
191
+ // --- Remote Layer Connection ---
192
+
193
+ void PluginBridgeEditor::connectRemoteLayer()
194
+ {
195
+ #if JUCE_MAC
196
+ disconnectRemoteLayer();
197
+
198
+ auto portName = processorRef.getRemoteLayerPortName();
199
+ if (portName.isEmpty()) return;
200
+
201
+ int guiW = processorRef.getRemoteGuiWidth();
202
+ int guiH = processorRef.getRemoteGuiHeight();
203
+ if (guiW <= 0 || guiH <= 0) return;
204
+
205
+ // Look up the Mach port from the bootstrap server
206
+ NSString* nsPortName = [NSString stringWithUTF8String:portName.toRawUTF8()];
207
+ NSMachPort* machPort = (NSMachPort*)[[NSMachBootstrapServer sharedInstance]
208
+ portForName:nsPortName];
209
+
210
+ if (!machPort)
211
+ {
212
+ DBG("PluginBridgeEditor: Failed to look up Mach port: " + portName);
213
+ return;
214
+ }
215
+
216
+ mach_port_t port = [machPort machPort];
217
+
218
+ // Create remote layer client
219
+ CARemoteLayerClient* client = [[CARemoteLayerClient alloc] initWithServerPort:port];
220
+ if (!client)
221
+ {
222
+ DBG("PluginBridgeEditor: Failed to create CARemoteLayerClient");
223
+ return;
224
+ }
225
+
226
+ remoteLayerClient = (__bridge_retained void*)client;
227
+
228
+ // Get our NSView and make it layer-backed
229
+ if (auto* peer = getPeer())
230
+ {
231
+ NSView* nsView = (NSView*)peer->getNativeHandle();
232
+ [nsView setWantsLayer:YES];
233
+
234
+ // Add remote layer as sublayer, positioned below the top bar
235
+ CALayer* remoteLayer = client.layer;
236
+ if (remoteLayer)
237
+ {
238
+ remoteLayer.frame = CGRectMake(0, 0, guiW, guiH);
239
+ // Position at the bottom of the view (below top bar)
240
+ // NSView coordinates are flipped (0,0 = bottom-left)
241
+ [nsView.layer addSublayer:remoteLayer];
242
+ }
243
+ }
244
+
245
+ layerConnected = true;
246
+
247
+ // Resize editor to fit plugin GUI + top bar
248
+ setSize(std::max(guiW, kDefaultWidth), guiH + kTopBarHeight);
249
+
250
+ DBG("PluginBridgeEditor: Remote layer connected (" + juce::String(guiW) + "x" + juce::String(guiH) + ")");
251
+ #endif
252
+ }
253
+
254
+ void PluginBridgeEditor::disconnectRemoteLayer()
255
+ {
256
+ #if JUCE_MAC
257
+ if (remoteLayerClient)
258
+ {
259
+ CARemoteLayerClient* client = (__bridge_transfer CARemoteLayerClient*)remoteLayerClient;
260
+ if (client.layer)
261
+ [client.layer removeFromSuperlayer];
262
+ remoteLayerClient = nullptr;
263
+ }
264
+ layerConnected = false;
265
+ #endif
266
+ }
267
+
268
+ // --- Mouse Event Forwarding ---
269
+
270
+ void PluginBridgeEditor::sendMouseEvent(const juce::String& type, float x, float y, int button)
271
+ {
272
+ // Adjust Y for top bar offset
273
+ float adjustedY = y - kTopBarHeight;
274
+ if (adjustedY < 0) return; // Click is in top bar, not plugin area
275
+
276
+ json cmd = {{"cmd", "mouse"}, {"type", type.toStdString()},
277
+ {"x", x}, {"y", adjustedY}, {"button", button}};
278
+ processorRef.getHelperConnection().sendCommand(cmd.dump());
279
+ }
280
+
281
+ void PluginBridgeEditor::mouseDown(const juce::MouseEvent& e)
282
+ {
283
+ if (e.y >= kTopBarHeight && layerConnected)
284
+ sendMouseEvent("down", (float)e.x, (float)e.y, e.mods.isLeftButtonDown() ? 0 : 1);
285
+ else
286
+ juce::AudioProcessorEditor::mouseDown(e);
287
+ }
288
+
289
+ void PluginBridgeEditor::mouseUp(const juce::MouseEvent& e)
290
+ {
291
+ if (e.y >= kTopBarHeight && layerConnected)
292
+ sendMouseEvent("up", (float)e.x, (float)e.y, 0);
293
+ else
294
+ juce::AudioProcessorEditor::mouseUp(e);
295
+ }
296
+
297
+ void PluginBridgeEditor::mouseMove(const juce::MouseEvent& e)
298
+ {
299
+ if (e.y >= kTopBarHeight && layerConnected)
300
+ sendMouseEvent("move", (float)e.x, (float)e.y);
301
+ else
302
+ juce::AudioProcessorEditor::mouseMove(e);
303
+ }
304
+
305
+ void PluginBridgeEditor::mouseDrag(const juce::MouseEvent& e)
306
+ {
307
+ if (e.y >= kTopBarHeight && layerConnected)
308
+ sendMouseEvent("drag", (float)e.x, (float)e.y, 0);
309
+ else
310
+ juce::AudioProcessorEditor::mouseDrag(e);
311
+ }
312
+
313
+ void PluginBridgeEditor::mouseWheelMove(const juce::MouseEvent& e, const juce::MouseWheelDetails& w)
314
+ {
315
+ if (e.y >= kTopBarHeight && layerConnected)
316
+ {
317
+ json cmd = {{"cmd", "mouse"}, {"type", "scroll"},
318
+ {"x", (float)e.x}, {"y", (float)(e.y - kTopBarHeight)},
319
+ {"dx", w.deltaX}, {"dy", w.deltaY}};
320
+ processorRef.getHelperConnection().sendCommand(cmd.dump());
321
+ }
322
+ else
323
+ {
324
+ juce::AudioProcessorEditor::mouseWheelMove(e, w);
325
+ }
326
+ }
327
+
328
+ // --- Plugin Menu ---
329
+
330
+ void PluginBridgeEditor::showPluginMenu()
331
+ {
332
+ auto files = processorRef.getInstalledPluginFiles();
333
+
334
+ juce::PopupMenu menu;
335
+
336
+ if (files.isEmpty())
337
+ {
338
+ menu.addItem(-1, "(No plugins found)", false);
339
+ menu.showMenuAsync(juce::PopupMenu::Options().withTargetComponent(&selectButton), [](int) {});
340
+ return;
341
+ }
342
+
343
+ juce::PopupMenu vst3Menu;
344
+ juce::PopupMenu auMenu;
345
+ int itemId = 1;
346
+
347
+ for (int i = 0; i < files.size(); ++i)
348
+ {
349
+ const auto& file = files[i];
350
+ auto name = file.getFileNameWithoutExtension();
351
+ auto path = file.getFullPathName();
352
+ bool blocked = processorRef.isBlocked(path);
353
+
354
+ if (file.getFileExtension() == ".vst3")
355
+ vst3Menu.addItem(itemId, blocked ? name + " (not compatible)" : name, !blocked);
356
+ else
357
+ auMenu.addItem(itemId, blocked ? name + " (not compatible)" : name, !blocked);
358
+
359
+ itemId++;
360
+ }
361
+
362
+ if (vst3Menu.getNumItems() > 0)
363
+ menu.addSubMenu("VST3", vst3Menu);
364
+ if (auMenu.getNumItems() > 0)
365
+ menu.addSubMenu("Audio Units", auMenu);
366
+
367
+ menu.addSeparator();
368
+ menu.addItem(9999, "Clear blocklist");
369
+
370
+ menu.showMenuAsync(juce::PopupMenu::Options().withTargetComponent(&selectButton),
371
+ [this, files](int result)
372
+ {
373
+ if (result == 9999)
374
+ {
375
+ processorRef.getBlocklist().clear();
376
+ return;
377
+ }
378
+
379
+ if (result > 0 && result <= files.size())
380
+ {
381
+ showingError = false;
382
+ pluginStatusLabel.setText("Loading...", juce::dontSendNotification);
383
+ pluginStatusLabel.setColour(juce::Label::textColourId, kTextSecondary);
384
+ processorRef.loadPlugin(files[result - 1].getFullPathName());
385
+ }
386
+ });
387
+ }