Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 58 additions & 3 deletions Textream/Textream/NotchOverlayController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,11 @@ class NotchOverlayController: NSObject {
baseHeight: panelHeight,
followingCursor: true
)
let contentView = NSHostingView(rootView: floatingView)
// Follow-cursor panels ignore mouse events, so the control would be
// unclickable there.
let contentView = NSHostingView(
rootView: MirroredOverlay(showsControl: false) { floatingView }
)

let panel = NSPanel(
contentRect: initialFrame,
Expand Down Expand Up @@ -321,7 +325,9 @@ class NotchOverlayController: NSObject {
speechRecognizer: speechRecognizer,
mirrorAxis: nil
)
let contentView = NSHostingView(rootView: fullscreenView)
let contentView = NSHostingView(
rootView: MirroredOverlay(showsControl: true) { fullscreenView }
)

let panel = NSPanel(
contentRect: screenFrame,
Expand Down Expand Up @@ -356,7 +362,9 @@ class NotchOverlayController: NSObject {
speechRecognizer: speechRecognizer,
baseHeight: panelHeight
)
let contentView = NSHostingView(rootView: floatingView)
let contentView = NSHostingView(
rootView: MirroredOverlay(showsControl: true) { floatingView }
)

let panel = NSPanel(
contentRect: NSRect(x: xPosition, y: yPosition, width: panelWidth, height: panelHeight),
Expand Down Expand Up @@ -1645,3 +1653,50 @@ struct FloatingOverlayView: View {
.transition(.scale.combined(with: .opacity))
}
}


// MARK: - Overlay Mirroring

/// Flips an overlay for a teleprompter mirror rig and, where the panel accepts
/// clicks, puts a Mirror toggle on top of it. The control is a sibling of the
/// flipped content, so it stays upright and readable while mirroring is on.
struct MirroredOverlay<Content: View>: View {
let showsControl: Bool
@ViewBuilder let content: Content

@State private var isHovering = false

private var axis: MirrorAxis? {
let settings = NotchSettings.shared
return settings.mirrorOverlay ? settings.mirrorAxis : nil
}

var body: some View {
ZStack(alignment: .topTrailing) {
content
.scaleEffect(x: axis?.scaleX ?? 1, y: axis?.scaleY ?? 1)

if showsControl {
Button {
NotchSettings.shared.mirrorOverlay.toggle()
} label: {
Image(systemName: "arrow.left.arrow.right")
.font(.system(size: 11, weight: .semibold))
.foregroundStyle(axis == nil ? Color.white.opacity(0.7) : Color.yellow)
.frame(width: 26, height: 26)
.background(
Circle().fill(axis == nil
? Color.white.opacity(0.12)
: Color.yellow.opacity(0.22))
)
}
.buttonStyle(.plain)
.help(axis == nil ? "Mirror the prompter" : "Stop mirroring")
.padding(10)
.opacity(isHovering ? 1 : 0.35)
.onHover { isHovering = $0 }
.animation(.easeInOut(duration: 0.15), value: isHovering)
}
}
}
}
7 changes: 7 additions & 0 deletions Textream/Textream/NotchSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,12 @@ class NotchSettings {
didSet { UserDefaults.standard.set(mirrorAxis.rawValue, forKey: "mirrorAxis") }
}

/// Mirror the Floating Window and Fullscreen overlays, using `mirrorAxis`.
/// The external display has its own Mirror mode and is unaffected.
var mirrorOverlay: Bool {
didSet { UserDefaults.standard.set(mirrorOverlay, forKey: "mirrorOverlay") }
}

var listeningMode: ListeningMode {
didSet { UserDefaults.standard.set(listeningMode.rawValue, forKey: "listeningMode") }
}
Expand Down Expand Up @@ -491,6 +497,7 @@ class NotchSettings {
let savedScreenID = UserDefaults.standard.integer(forKey: "externalScreenID")
self.externalScreenID = UInt32(savedScreenID)
self.mirrorAxis = MirrorAxis(rawValue: UserDefaults.standard.string(forKey: "mirrorAxis") ?? "") ?? .horizontal
self.mirrorOverlay = UserDefaults.standard.object(forKey: "mirrorOverlay") as? Bool ?? false
self.listeningMode = ListeningMode(rawValue: UserDefaults.standard.string(forKey: "listeningMode") ?? "") ?? .wordTracking
let savedSpeed = UserDefaults.standard.double(forKey: "scrollSpeed")
self.scrollSpeed = savedSpeed > 0 ? savedSpeed : 3
Expand Down
29 changes: 29 additions & 0 deletions Textream/Textream/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,34 @@ struct SettingsView: View {
.font(.system(size: 11))
.foregroundStyle(.secondary)

if settings.overlayMode != .pinned {
Divider()

Toggle(isOn: $settings.mirrorOverlay) {
Text("Mirror the prompter")
.font(.system(size: 13, weight: .medium))
}
.toggleStyle(.switch)

Text("Flips the overlay for a teleprompter mirror rig.")
.font(.system(size: 11))
.foregroundStyle(.secondary)

if settings.mirrorOverlay {
Picker("", selection: $settings.mirrorAxis) {
ForEach(MirrorAxis.allCases) { axis in
Text(axis.label).tag(axis)
}
}
.pickerStyle(.segmented)
.labelsHidden()

Text(settings.mirrorAxis.description)
.font(.system(size: 11))
.foregroundStyle(.secondary)
}
}

if settings.overlayMode == .pinned {
Divider()

Expand Down Expand Up @@ -1416,6 +1444,7 @@ struct SettingsView: View {
settings.externalDisplayMode = .off
settings.externalScreenID = 0
settings.mirrorAxis = .horizontal
settings.mirrorOverlay = false
settings.listeningMode = .wordTracking
settings.scrollSpeed = 3
settings.showElapsedTime = true
Expand Down