0c7b6b4442
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>
132 lines
5.6 KiB
Swift
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()) ]")
|
|
}
|
|
|
|
}
|