Skip to content

Commit 66b9d8c

Browse files
mkschulzeclaude
andcommitted
fix(ui): font consistency, button sizing, menu positioning, user count
Bundled UI polish pass addressing several visual issues reported in Bitwig on Windows. Fonts: - ConnectionBar status label ("Connected"/"Disconnected") and BeatBar BPM/BPI numbers were using FontOptions(13.0f).withStyle("Bold"). On Windows without a typeface name, JUCE falls back to a serif face for the bold variant, which looked mismatched against the sans-serif button text. Both now use a plain 15pt / 13pt font matching the TextButton font (LookAndFeel_V4 uses 15pt plain for h=28 buttons). - The normal Connected/Disconnected status text is now suppressed entirely — the colored dot is the sole indicator. "Connecting..." and error messages still render. Button sizing: - Connect/Disconnect button: 90 → 105px so "Disconnect" no longer clips at the default plugin size. - Video button: 44 → 54px so "Video" no longer horizontally squashes. - Default plugin width (kBaseWidth): 1000 → 1030 to accommodate both widened buttons without crowding the right-side cluster. Menus: - UI Scale right-click menu now uses .withParentComponent(this) instead of .withTargetComponent(this), so the popup (a) inherits JamWideLookAndFeel for consistent typography/colours and (b) opens at the mouse position rather than being anchored to the editor's bounds. Applied to both the editor-level and ConnectionBar-level handlers. Session info: - SessionInfoStrip now renders "Users: N/M" when the connected server's max slot count is known from the cached public server list. The NINJAM protocol itself does not expose max-slots, so the editor looks up the current server address in processor.cachedServerList by parsing "host:port" out of lastServerAddress. When unknown (user never browsed), falls back to "Users: N". OSC dot: - OscStatusDot diameter: 10 → 8px to match the server-connection dot drawn in ConnectionBar::paint. BPM/BPI suppression refactor (NinjamRunThread): - Replaced the iteration-count connectGrace_ int counter with a time-based suppressBpmBpiUntilMs_ int64. After a fresh OK status transition we silently track BPM/BPI for 2.5 seconds, during which the NJClient defaults (120/32) are replaced by the server's real config without emitting a bogus "BPM changed from 120 to N" chat message. Prior implementation assumed a fixed number of run-loop iterations, which could fire or mis-fire depending on actual iteration cadence. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 815bbd7 commit 66b9d8c

10 files changed

Lines changed: 124 additions & 35 deletions

juce/JamWideJuceEditor.cpp

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,12 @@ void JamWideJuceEditor::mouseDown(const juce::MouseEvent& e)
191191
menu.addItem(3, "2x", true, juce::approximatelyEqual(processorRef.scaleFactor, 2.0f));
192192
menu.addSeparator();
193193
menu.addItem(4, "Show Session Info", true, infoStripVisible);
194-
menu.showMenuAsync(juce::PopupMenu::Options(),
194+
// withParentComponent(this) — NOT withTargetComponent — so the menu
195+
// (a) inherits JamWideLookAndFeel from the editor for consistent
196+
// typography/colours, and (b) still opens at the mouse position
197+
// (withTargetComponent would anchor the menu to the editor's bounds,
198+
// which is wrong for a right-click context menu).
199+
menu.showMenuAsync(juce::PopupMenu::Options().withParentComponent(this),
195200
[this](int result) {
196201
if (result >= 1 && result <= 3)
197202
{
@@ -274,7 +279,33 @@ void JamWideJuceEditor::timerCallback()
274279
int syncState = processorRef.syncState_.load(std::memory_order_relaxed);
275280

276281
int userCount = processorRef.userCount.load(std::memory_order_relaxed);
277-
sessionInfoStrip.update(intervalCount, elapsedMs, beat, bpi, syncState, isStandalone, userCount);
282+
283+
// Look up max user slots from the cached public server list. The
284+
// NINJAM protocol itself does not expose max-slots, so this is only
285+
// populated when the user has refreshed the server browser and the
286+
// connected server appears in the list. maxUsers=0 => unknown; the
287+
// strip falls back to rendering just the current count.
288+
int maxUsers = 0;
289+
const juce::String& addr = processorRef.lastServerAddress;
290+
if (addr.isNotEmpty() && !processorRef.cachedServerList.empty())
291+
{
292+
// Parse "host:port" out of the editor's last-used address string.
293+
// Port is optional — match on host alone if it's missing.
294+
int colon = addr.lastIndexOfChar(':');
295+
juce::String host = colon >= 0 ? addr.substring(0, colon) : addr;
296+
int port = colon >= 0 ? addr.substring(colon + 1).getIntValue() : 0;
297+
for (const auto& entry : processorRef.cachedServerList)
298+
{
299+
if (juce::String(entry.host).equalsIgnoreCase(host)
300+
&& (port == 0 || entry.port == port))
301+
{
302+
maxUsers = entry.max_users;
303+
break;
304+
}
305+
}
306+
}
307+
308+
sessionInfoStrip.update(intervalCount, elapsedMs, beat, bpi, syncState, isStandalone, userCount, maxUsers);
278309
}
279310

280311
// Note: VU updates are driven by ChannelStripArea's own 30Hz timer (REVIEW FIX #7)

juce/JamWideJuceEditor.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,11 @@ class JamWideJuceEditor : public juce::AudioProcessorEditor,
9999
bool infoStripVisible = true;
100100
int prevPollStatus_ = -1; // REVIEW FIX: member, not static
101101

102-
static constexpr int kBaseWidth = 1000;
102+
// 1030 accommodates the widened Connect/Disconnect button (105px) and
103+
// Video button (54px) so "Disconnect" and "Video" both fit at the default
104+
// plugin size without the right-side Fit/Video/Sync/Route/Vorbis cluster
105+
// crowding the status label.
106+
static constexpr int kBaseWidth = 1030;
103107
static constexpr int kBaseHeight = 700;
104108
static constexpr int kConnectionBarHeight = 44;
105109
static constexpr int kBeatBarHeight = 22;

juce/NinjamRunThread.cpp

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -278,11 +278,14 @@ void NinjamRunThread::run()
278278

279279
lastStatus_ = currentStatus;
280280

281-
// On connect: grace period to suppress false BPM/BPI "changed" messages.
282-
// Server sends actual config AFTER status becomes OK, so the first
283-
// few iterations see default→real transitions that aren't real changes.
281+
// BPM/BPI suppression window: after a fresh OK transition,
282+
// silently track values for 2.5s so the NJClient defaults
283+
// (120/32) can be replaced by the real server config
284+
// without emitting a bogus "changed" chat message.
284285
if (currentStatus == NJClient::NJC_STATUS_OK)
285-
connectGrace_ = 5;
286+
suppressBpmBpiUntilMs_ = juce::Time::currentTimeMillis() + 2500;
287+
else
288+
suppressBpmBpiUntilMs_ = 0;
286289

287290
// On connect: set up all 4 local channels per D-12
288291
if (currentStatus == NJClient::NJC_STATUS_OK)
@@ -380,11 +383,15 @@ void NinjamRunThread::run()
380383
int bpi = client->GetBPI();
381384
processor.uiSnapshot.bpi.store(bpi, std::memory_order_relaxed);
382385

383-
// Detect BPM/BPI changes (skip during connect grace period —
384-
// server sends actual config AFTER status becomes OK)
385-
if (connectGrace_ > 0)
386+
// Detect BPM/BPI changes, but only AFTER the post-connect
387+
// suppression window has expired. During the window the NJClient
388+
// defaults (120/32) transition to the server's real config, and
389+
// that transition must not be reported as a "changed" message.
390+
if (suppressBpmBpiUntilMs_ > 0
391+
&& juce::Time::currentTimeMillis() < suppressBpmBpiUntilMs_)
386392
{
387-
--connectGrace_;
393+
// Still in the post-connect suppression window — silently
394+
// track values via the uiSnapshot store above.
388395
}
389396
else
390397
{

juce/NinjamRunThread.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,14 @@ class NinjamRunThread : public juce::Thread
3434
JamWideJuceProcessor& processor;
3535
jamwide::ServerListFetcher serverListFetcher;
3636
int lastStatus_ = -1; // NJClient::NJC_STATUS_DISCONNECTED
37-
int connectGrace_ = 0; // Skip BPM/BPI change messages for N iterations after connect
37+
// BPM/BPI change detection: suppress the initial default→real transition
38+
// that fires when the server sends its actual config on login. NJClient
39+
// constructs with m_bpm=120, m_bpi=32 defaults — GetActualBPM()/GetBPI()
40+
// return those until the server sends its config message, which can take
41+
// hundreds of ms. Use a time-based suppression window: after status goes
42+
// OK, silently track values for ~2.5s without emitting chat messages.
43+
// After the window expires, normal change detection resumes.
44+
juce::int64 suppressBpmBpiUntilMs_ = 0;
3845

3946
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(NinjamRunThread)
4047
};

juce/osc/OscStatusDot.cpp

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,10 @@ void OscStatusDot::paint(juce::Graphics& g)
2828
g.drawText("OSC", juce::Rectangle<float>(labelX, labelY, labelWidth, 9.0f),
2929
juce::Justification::centredLeft, false);
3030

31-
// Dot: 10px diameter, 4px gap after "OSC" text, centered vertically
31+
// Dot: 8px diameter to match the server-connection dot drawn in
32+
// ConnectionBar::paint. 4px gap after "OSC" text, centered vertically.
3233
const float dotX = labelX + labelWidth + 4.0f;
33-
const float dotY = (bounds.getHeight() - 10.0f) * 0.5f;
34+
const float dotY = (bounds.getHeight() - 8.0f) * 0.5f;
3435

3536
// Color based on OscServer state (per D-09: 3-state indicator)
3637
juce::Colour dotColour;
@@ -42,7 +43,7 @@ void OscStatusDot::paint(juce::Graphics& g)
4243
dotColour = juce::Colour(JamWideLookAndFeel::kAccentConnect); // Green: active
4344

4445
g.setColour(dotColour);
45-
g.fillEllipse(dotX, dotY, 10.0f, 10.0f);
46+
g.fillEllipse(dotX, dotY, 8.0f, 8.0f);
4647
}
4748

4849
void OscStatusDot::mouseUp(const juce::MouseEvent& e)

juce/ui/BeatBar.cpp

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,16 @@ void BeatBar::paint(juce::Graphics& g)
4848
}
4949
}
5050

51+
// BPM/BPI: use a plain 13pt font. .withStyle("Bold") without a typeface
52+
// name falls back to a serif face on Windows (same JUCE quirk as the
53+
// status label), which makes the numbers look mismatched against the
54+
// sans-serif "Intervals:" / "Elapsed:" labels in the strip below.
55+
const auto numberFont = juce::FontOptions(13.0f);
56+
5157
// BPM value (right-aligned in first 36px)
5258
auto bpmArea = labelArea.removeFromLeft(36);
5359
g.setColour(textCol);
54-
g.setFont(juce::FontOptions(13.0f).withStyle("Bold"));
60+
g.setFont(numberFont);
5561
g.drawText(juce::String(static_cast<int>(currentBpm_)), bpmArea,
5662
juce::Justification::centredRight, false);
5763

@@ -63,7 +69,7 @@ void BeatBar::paint(juce::Graphics& g)
6369

6470
// BPI value (left-aligned in remaining ~28px)
6571
g.setColour(textCol);
66-
g.setFont(juce::FontOptions(13.0f).withStyle("Bold"));
72+
g.setFont(numberFont);
6773
g.drawText(juce::String(bpi_), labelArea, juce::Justification::centredLeft, false);
6874
}
6975

juce/ui/ConnectionBar.cpp

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,19 @@ ConnectionBar::ConnectionBar(JamWideJuceProcessor& processor)
9393
browseButton.onClick = [this]() { if (onBrowseClicked) onBrowseClicked(); };
9494
addAndMakeVisible(browseButton);
9595

96-
// Status label
97-
statusLabel.setFont(juce::FontOptions(13.0f).withStyle("Bold"));
98-
statusLabel.setText("Disconnected", juce::dontSendNotification);
96+
// Status label — match TextButton font (LookAndFeel_V4 uses 15pt plain for
97+
// h=28 buttons via jmin(15, h-6)). Avoid .withStyle("Bold") because, with
98+
// no typeface name specified, JUCE on Windows falls back to a serif face
99+
// for the bold variant, which makes this label look mismatched against
100+
// the sans-serif button text around it.
101+
//
102+
// For the normal Connected/Disconnected states the label is intentionally
103+
// empty — the colored status dot painted to its left is the sole indicator
104+
// (green = connected, grey = disconnected). Transient states and errors
105+
// still get text: "Connecting..." in yellow, and the server's auth/connect
106+
// error message in red.
107+
statusLabel.setFont(juce::FontOptions(15.0f));
108+
statusLabel.setText({}, juce::dontSendNotification);
99109
statusLabel.setColour(juce::Label::textColourId, juce::Colour(JamWideLookAndFeel::kTextSecondary));
100110
addAndMakeVisible(statusLabel);
101111

@@ -225,8 +235,10 @@ void ConnectionBar::resized()
225235
x += 120 + gap;
226236
}
227237

228-
connectButton.setBounds(x, y, 90, h);
229-
x += 90 + gap;
238+
// 105px fits "Disconnect" at the 15pt button font without clipping.
239+
// "Connect" also fits; the extra slack is visually balanced.
240+
connectButton.setBounds(x, y, 105, h);
241+
x += 105 + gap;
230242

231243
browseButton.setBounds(x, y, 70, h);
232244
x += 70 + 16; // 16px gap before status section
@@ -254,9 +266,11 @@ void ConnectionBar::resized()
254266
oscStatusDot->setBounds(rightX - 44, 0, 44, getHeight());
255267
rightX -= 44 + gap;
256268
}
257-
// Video button (D-01: between OSC dot and Fit, UI-SPEC: 44x28)
258-
videoButton.setBounds(rightX - 44, y, 44, h);
259-
rightX -= 44 + gap;
269+
// Video button — 54px so "Video" fits at the 15pt button font without
270+
// JUCE drawFittedText horizontally squashing it. The original 44px spec
271+
// was calibrated against a different font and clipped on Windows.
272+
videoButton.setBounds(rightX - 54, y, 54, h);
273+
rightX -= 54 + gap;
260274
fitButton.setBounds(rightX - 36, y, 36, h);
261275
}
262276

@@ -328,7 +342,10 @@ void ConnectionBar::mouseDown(const juce::MouseEvent& e)
328342
menu.addItem(1, "1x", true, juce::approximatelyEqual(processorRef.scaleFactor, 1.0f));
329343
menu.addItem(2, "1.5x", true, juce::approximatelyEqual(processorRef.scaleFactor, 1.5f));
330344
menu.addItem(3, "2x", true, juce::approximatelyEqual(processorRef.scaleFactor, 2.0f));
331-
menu.showMenuAsync(juce::PopupMenu::Options(),
345+
// withParentComponent(this) inherits JamWideLookAndFeel AND lets the
346+
// menu open at the mouse position (withTargetComponent would anchor
347+
// it to the component's bounds — wrong for a right-click context).
348+
menu.showMenuAsync(juce::PopupMenu::Options().withParentComponent(this),
332349
[this](int result) {
333350
float newScale = 1.0f;
334351
if (result == 2) newScale = 1.5f;
@@ -425,11 +442,14 @@ void ConnectionBar::updateStatus(int njcStatus, int /*numUsers*/)
425442
{
426443
currentStatus = njcStatus;
427444

428-
// Update status text
445+
// Update status text. Normal Connected/Disconnected states render as an
446+
// empty label — the colored dot to the left is the indicator. Transient
447+
// "Connecting..." and error messages still get text so the user gets
448+
// feedback that matters.
429449
switch (njcStatus)
430450
{
431451
case NJClient::NJC_STATUS_OK:
432-
statusLabel.setText("Connected", juce::dontSendNotification);
452+
statusLabel.setText({}, juce::dontSendNotification);
433453
statusLabel.setColour(juce::Label::textColourId, juce::Colour(JamWideLookAndFeel::kAccentConnect));
434454
processorRef.lastErrorMsg = {}; // Clear previous error on successful connect
435455
break;
@@ -454,7 +474,7 @@ void ConnectionBar::updateStatus(int njcStatus, int /*numUsers*/)
454474
break;
455475
}
456476
default:
457-
statusLabel.setText("Disconnected", juce::dontSendNotification);
477+
statusLabel.setText({}, juce::dontSendNotification);
458478
statusLabel.setColour(juce::Label::textColourId, juce::Colour(JamWideLookAndFeel::kTextSecondary));
459479
break;
460480
}

juce/ui/SessionInfoStrip.cpp

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ SessionInfoStrip::SessionInfoStrip()
99
void SessionInfoStrip::update(int intervalCount, unsigned int elapsedMs,
1010
int currentBeat, int totalBeats,
1111
int syncState, bool isStandalone,
12-
int userCount)
12+
int userCount, int maxUsers)
1313
{
1414
intervalCount_ = intervalCount;
1515
elapsedMs_ = elapsedMs;
@@ -18,6 +18,7 @@ void SessionInfoStrip::update(int intervalCount, unsigned int elapsedMs,
1818
syncState_ = syncState;
1919
isStandalone_ = isStandalone;
2020
userCount_ = userCount;
21+
maxUsers_ = maxUsers;
2122
repaint();
2223
}
2324

@@ -77,15 +78,26 @@ void SessionInfoStrip::paint(juce::Graphics& g)
7778
else
7879
g.drawText("--/--", area.removeFromLeft(40), juce::Justification::centredLeft, false);
7980

80-
// Users section
81+
// Users section — renders "N/M" when the server's max slot count is known
82+
// (populated from the public server list cache by the editor), "N"
83+
// otherwise. maxUsers_ == 0 means "unknown" — the NINJAM protocol itself
84+
// doesn't expose max-slots, we only know it when the user has browsed the
85+
// public list and the connected server appears there.
8186
area.removeFromLeft(16); // gap
8287
g.setFont(labelFont);
8388
g.setColour(labelCol);
8489
g.drawText("Users: ", area.removeFromLeft(35), juce::Justification::centredRight, false);
8590
g.setFont(valueFont);
8691
g.setColour(valueCol);
87-
g.drawText(connected ? juce::String(userCount_) : "--",
88-
area.removeFromLeft(25), juce::Justification::centredLeft, false);
92+
juce::String usersStr;
93+
if (!connected)
94+
usersStr = "--";
95+
else if (maxUsers_ > 0)
96+
usersStr = juce::String(userCount_) + "/" + juce::String(maxUsers_);
97+
else
98+
usersStr = juce::String(userCount_);
99+
// 50px fits "NN/NN" comfortably at the 11pt value font.
100+
g.drawText(usersStr, area.removeFromLeft(50), juce::Justification::centredLeft, false);
89101

90102
// Sync section (hidden in standalone per D-07)
91103
if (!isStandalone_)

juce/ui/SessionInfoStrip.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ class SessionInfoStrip : public juce::Component
77
SessionInfoStrip();
88
void update(int intervalCount, unsigned int elapsedMs, int currentBeat,
99
int totalBeats, int syncState, bool isStandalone,
10-
int userCount);
10+
int userCount, int maxUsers);
1111
void paint(juce::Graphics& g) override;
1212

1313
private:
@@ -18,6 +18,7 @@ class SessionInfoStrip : public juce::Component
1818
int syncState_ = 0;
1919
bool isStandalone_ = false;
2020
int userCount_ = 0;
21+
int maxUsers_ = 0; // 0 = unknown (not in server list cache) → show "N" only
2122

2223
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SessionInfoStrip)
2324
};

src/build_number.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
#pragma once
2-
#define JAMWIDE_BUILD_NUMBER 171
2+
#define JAMWIDE_BUILD_NUMBER 182

0 commit comments

Comments
 (0)