Files
virtual-controller/remote-client-ios/remote-client-ios/Core/VC-Arrange/Arrange-Functions-ButtonActions.swift
T
Paul Lipscomb 0c7b6b4442 JL Cooper MIDI Controller I/O, Focus widget hardware binding, iOS Channel Focus polish
Desktop:
- New dedicated MIDI Controller In port (separate from MIDI In), same CC learn/bind pipeline but never passes notes through; positioned first in the IO column order
- Fixed MIDI In "Not Connected" not persisting across restart (missing save_io_config call)
- Fixed MIDI Out/Transport Out mutual-exclusion incorrectly flagging "Not Connected" as an in-use port
- Renamed "Remote" checkbox to "App" on Fader/Toggle/Transport widgets
- FocusFaderWidget/FocusToggleWidget: new Hardware section (Learn + CC/CH bind) so a physical controller can drive a Channel Focus widget alongside the app/OSC, plus an independent Feedback checkbox that sends translated CC out (for motorized fader / LED sync)
- JL Cooper touch-sense profile ("No Profile" / "MIDI JL Cooper CC Mode") on FaderWidget/FocusFaderWidget: touch channel (value channel - 1, same CC) is filtered out of the value and gates incoming OSC/DAW feedback while touching
- FocusFeedbackWidget: "Custom" checkbox to show a manually-typed static string instead of live OSC feedback, disabling Learn/OSC input and suppressing the iPad broadcast while active
- New daw-config-reaper/ folder with reaper-osc-config-paul-custom.ReaperOSC

iOS:
- New FocusToggleWidget (dedicated Channel Focus toggle, split out of shared ToggleWidget) with colorName-aware darken/border and isMomentary handling — keeps Channel-Focus-only behavior off the regular ToggleWidget
- Arrange mode long-press menu: text alignment (left/center/right) and a 10-color named-palette Text Color picker for TitleWidget/FocusFeedbackWidget, persisted in the saved layout
- FocusFeedbackWidget: Reset Size action, monospaced-digit font fix for timestamp/counter jitter
- New "Cancel Changes" button in Arrange mode — reverts layout/hides/aligns to the state at Arrange-mode entry and restores desktop-side Remote/dest assignments
- Grid line brightness increased for visibility
- Bonjour discovery for desktop auto-connect

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-06 22:17:37 -04:00

132 lines
5.6 KiB
Swift

import UIKit
extension ArrangeObjects {
// MARK: - Button Actions
@objc func buttonTappedConnectButton() {
if self.connectionState == .connected {
//disconnect path
self.wsClient?.cancel(with: .goingAway, reason: nil)
self.wsClient = nil
self.updateUIFromConnectionState(.disconnected)
} else {
//connect path
let ip = self.ipField.text?.trimmingCharacters(in: .whitespaces) ?? ""
let port = self.portField.text?.trimmingCharacters(in: .whitespaces) ?? ""
if let idText = self.tabletIdField.text?.trimmingCharacters(in: .whitespaces),
let idVal = Int(idText), idVal > 0 {
self.myTabletId = idVal
}
guard checkerIPAddressInput(ip: ip) else { return }
guard checkerPortInput(port: port) else { return }
guard let url = buildWebSocketURL(ip: ip, port: port) else { return }
UserDefaults.standard.set(ip, forKey: "last_ip")
UserDefaults.standard.set(port, forKey: "last_port")
self.connectWebSocket(url: url)
}
}
@objc func buttonTappedGridButton() {
let green = UIColor(red: 0, green: 0.9, blue: 0.46, alpha: 1)
gridEnabled.toggle()
gridOverlay.isHidden = !gridEnabled
gridButton.backgroundColor = gridEnabled ? green : UIColor(white: 0.15, alpha: 1)
gridButton.setTitleColor(gridEnabled ? .black : .lightGray, for: .normal)
}
@objc func tabletIdFieldChanged() {
guard let text = tabletIdField.text, let idVal = Int(text), idVal > 0 else { return }
myTabletId = idVal
if let preset = currentPreset {
buildUIFromPreset(preset)
}
}
@objc func buttonTappedClearLayouts() {
UserDefaults.standard.removeObject(forKey: "layout_\(self.presetUuid)")
loggingView.log("cleared layout for \(self.presetUuid)", category: .local)
if let preset = currentPreset { buildUIFromPreset(preset) }
}
@objc func buttonTappedShowLogging() {
let visible = (self.logWidget.isHidden == false)
self.logWidget.isHidden = visible
self.showLoggingButton.setTitle(visible ? "Hide Logging" : "Show Logging", for: .normal)
}
@objc func buttonTappedAutoSwitchButton() {
autoSwitchEnabled.toggle()
let green = UIColor(red: 0, green: 0.9, blue: 0.46, alpha: 1)
autoSwitchButton.backgroundColor = autoSwitchEnabled ? green : UIColor(white: 0.15, alpha: 1)
autoSwitchButton.setTitleColor(autoSwitchEnabled ? .black : .lightGray, for: .normal)
loggingView.log("auto switch \(autoSwitchEnabled ? "enabled" : "disabled")", category: .local)
}
@objc func buttonTappedChannelFocus() {
guard currentPreset != nil else {
loggingView.log("channel focus: no preset received yet", category: .local)
return
}
isChannelFocusMode.toggle()
let green = UIColor(red: 0, green: 0.9, blue: 0.46, alpha: 1)
channelFocusButton.backgroundColor = isChannelFocusMode ? green : UIColor(white: 0.15, alpha: 1)
channelFocusButton.setTitleColor(isChannelFocusMode ? .black : .lightGray, for: .normal)
if isChannelFocusMode {
buildChannelFocusUI()
} else if let preset = currentPreset {
buildUIFromPreset(preset)
}
loggingView.log("channel focus \(isChannelFocusMode ? "enabled" : "disabled")", category: .local)
}
@objc func buttonTappedArrangeButton() {
isLocked.toggle()
updateUIArrangeLockButtons()
if isLocked {
saveLayout()
arrangeSessionLayoutKey = nil
arrangeSessionLayoutSnapshot = nil
widgets.values.forEach { $0.setArrangeMode(false) }
} else {
if let key = currentLayoutKey {
arrangeSessionLayoutKey = key
arrangeSessionLayoutSnapshot = UserDefaults.standard.dictionary(forKey: key) ?? [:]
}
widgets.values.forEach { $0.setArrangeMode(true) }
}
}
// Reverts to how things were the moment Arrange mode was entered
// undoes drags/resizes/alignment changes/hides made this session, even
// though most of those already auto-saved along the way.
@objc func buttonTappedCancelChanges() {
guard let key = arrangeSessionLayoutKey else { return }
UserDefaults.standard.set(arrangeSessionLayoutSnapshot ?? [:], forKey: key)
// Hiding a regular (non-Channel-Focus) widget tells the desktop to
// uncheck its Remote assignment and saves that to the preset file
// resend every regular widget's original destId so the desktop's
// state matches again. Channel Focus uids aren't tracked server-side
// for this message, so this is a no-op for those either way.
if let preset = currentPreset {
for f in preset.faders { wsClientSendJSON(["event": "widget_visibility", "uid": f.uid, "dest_id": f.destId]) }
for t in preset.toggles { wsClientSendJSON(["event": "widget_visibility", "uid": t.uid, "dest_id": t.destId]) }
for t in preset.transports { wsClientSendJSON(["event": "widget_visibility", "uid": t.uid, "dest_id": t.destId]) }
}
arrangeSessionLayoutKey = nil
arrangeSessionLayoutSnapshot = nil
isLocked = true
updateUIArrangeLockButtons()
if isChannelFocusMode {
buildChannelFocusUI()
} else if let preset = currentPreset {
buildUIFromPreset(preset)
}
showStatus("[ Cancelled \(self.statusTimestamp()) ]")
}
}