Skip to content

Commit

Permalink
Close add A-B Loop indicator on progress bar, #548 (#3529)
Browse files Browse the repository at this point in the history
This commit adds two additional thumbs to the progress bar representing
the A and B loop points when the A-B loop feature is active. This allows
the user to adjust the loop points.

This commit will:
- Add a new class PlaySliderLoopKnob that adds an additional thumb
- Add a new class PlaySlider that customizes NSSlider to add the thumbs
- Change MainWindowController.xib to use PlaySlider for the OSC
- Change PlayerCore to allow mpv loop points to be updated
- Change MainWindowController to observe the thumbs and update mpv
- Change PlayerWindowController to synchronize the thumbs with mpv
- Change MainMenuActions to call the window controller for A-B Loop
- Change PlaybackInfo to use an enum class for abLoopStatus
- Add a new message to OSDMessage
- Move changing of the slider control size from PlaySliderCell to
  PlaySlider
- Change PlaySliderCell.knobRect to keep thumb within bar
- Remove PlaySliderCell.barRect that increased the width of the bar
  • Loading branch information
low-batt committed Apr 11, 2023
1 parent 3de15d0 commit 24630f1
Show file tree
Hide file tree
Showing 15 changed files with 545 additions and 57 deletions.
10 changes: 9 additions & 1 deletion iina.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
519872FF26879B9B00F84BCC /* AccessibilityPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519872FE26879B9B00F84BCC /* AccessibilityPreferences.swift */; };
51C1BA3A291CA76700C1208A /* InfoDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C1BA39291CA76700C1208A /* InfoDictionary.swift */; };
51CACB9529D500290034CEE5 /* VideoPIPViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CACB9429D500290034CEE5 /* VideoPIPViewController.swift */; };
51E63DFB29CFB031008AFC20 /* PlaySlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E63DFA29CFB031008AFC20 /* PlaySlider.swift */; };
51E63DFD29CFB04C008AFC20 /* PlaySliderLoopKnob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E63DFC29CFB04B008AFC20 /* PlaySliderLoopKnob.swift */; };
51F7974728C7E00200812D0D /* Lock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F7974628C7E00200812D0D /* Lock.swift */; };
6100FF2B1EDF9806002CF0FB /* dsa_pub.pem in Resources */ = {isa = PBXBuildFile; fileRef = 6100FF2A1EDF9806002CF0FB /* dsa_pub.pem */; };
8400D5C41E17C6D2006785F5 /* AboutWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8400D5C21E17C6D2006785F5 /* AboutWindowController.swift */; };
Expand Down Expand Up @@ -812,6 +814,8 @@
519872FE26879B9B00F84BCC /* AccessibilityPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityPreferences.swift; sourceTree = "<group>"; };
51C1BA39291CA76700C1208A /* InfoDictionary.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InfoDictionary.swift; sourceTree = "<group>"; };
51CACB9429D500290034CEE5 /* VideoPIPViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPIPViewController.swift; sourceTree = "<group>"; };
51E63DFA29CFB031008AFC20 /* PlaySlider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaySlider.swift; sourceTree = "<group>"; };
51E63DFC29CFB04B008AFC20 /* PlaySliderLoopKnob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaySliderLoopKnob.swift; sourceTree = "<group>"; };
51F7974628C7E00200812D0D /* Lock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Lock.swift; sourceTree = "<group>"; };
5879479521A87DD700757A6F /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/MiniPlayerWindowController.strings; sourceTree = "<group>"; };
5879479621A87E6100757A6F /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/PreferenceWindowController.strings; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2398,6 +2402,8 @@
E3DBD23D218EF4F100B3AFBF /* AboutWindowContributorAvatarItem.xib */,
9E47DABF1E3CFA6D00457420 /* DurationDisplayTextField.swift */,
843FFD4C1D5DAA01001F3A44 /* RoundedTextFieldCell.swift */,
51E63DFA29CFB031008AFC20 /* PlaySlider.swift */,
51E63DFC29CFB04B008AFC20 /* PlaySliderLoopKnob.swift */,
8461C52D1D45FFF6006E91FF /* PlaySliderCell.swift */,
84F725551D4783EE000DEF1B /* VolumeSliderCell.swift */,
8434BAAC1D5E4546003BECF2 /* SlideUpButton.swift */,
Expand All @@ -2424,8 +2430,8 @@
84D377691D74163B007F7396 /* Video */ = {
isa = PBXGroup;
children = (
84A0BAA01D2FAE7600BC8DA1 /* VideoView.swift */,
8407D13F1E3A684C0043895D /* ViewLayer.swift */,
84A0BAA01D2FAE7600BC8DA1 /* VideoView.swift */,
);
name = Video;
sourceTree = "<group>";
Expand Down Expand Up @@ -2928,6 +2934,7 @@
84A0BA901D2F8D4100BC8DA1 /* IINAError.swift in Sources */,
84C6D3621EAF8D63009BF721 /* HistoryController.swift in Sources */,
E38B3213214FB9EA000F6D27 /* PrefPluginPermissionView.swift in Sources */,
51E63DFD29CFB04C008AFC20 /* PlaySliderLoopKnob.swift in Sources */,
847644081D48B413004F6DF5 /* MPVOption.swift in Sources */,
84817C961DBDCA5F00CC2279 /* SettingsListCellView.swift in Sources */,
84A886F31E26CA24008755BB /* Regex.swift in Sources */,
Expand Down Expand Up @@ -2996,6 +3003,7 @@
845FB0C71D39462E00C011E0 /* ControlBarView.swift in Sources */,
E3513AFB20F120F600F8C347 /* PreferenceViewController.swift in Sources */,
E38BD4AF20054BD9007635FC /* MainWindow.swift in Sources */,
51E63DFB29CFB031008AFC20 /* PlaySlider.swift in Sources */,
84F5D4A01E44F9DB0060A838 /* KeyBindingItem.swift in Sources */,
84E5221323FF3D080006FDA1 /* JavascriptAPIPlaylist.swift in Sources */,
84A0BA991D2FAAA700BC8DA1 /* MPVController.swift in Sources */,
Expand Down
1 change: 1 addition & 0 deletions iina/AppData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,5 @@ extension Notification.Name {
static let iinaPluginChanged = Notification.Name("IINAPluginChanged")
static let iinaPlayerStopped = Notification.Name("iinaPlayerStopped")
static let iinaPlayerShutdown = Notification.Name("iinaPlayerShutdown")
static let iinaPlaySliderLoopKnobChanged = Notification.Name("iinaPlaySliderLoopKnobChanged")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.410",
"green" : "0.410",
"red" : "0.410"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.650",
"green" : "0.650",
"red" : "0.650"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
6 changes: 3 additions & 3 deletions iina/Assets.xcassets/Contents.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
"author" : "xcode",
"version" : 1
}
}
}
8 changes: 4 additions & 4 deletions iina/Base.lproj/MainWindowController.xib
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="17506" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="18122" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="17506"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="18122"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
Expand Down Expand Up @@ -70,7 +70,7 @@
<window title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" restorable="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" tabbingMode="disallowed" id="F0z-JX-Cv5" customClass="MainWindow" customModule="IINA" customModuleProvider="target">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES" fullSizeContentView="YES"/>
<rect key="contentRect" x="196" y="240" width="640" height="400"/>
<rect key="screenRect" x="0.0" y="0.0" width="1680" height="1025"/>
<rect key="screenRect" x="0.0" y="0.0" width="1920" height="1055"/>
<view key="contentView" id="se5-gp-TjO">
<rect key="frame" x="0.0" y="0.0" width="640" height="400"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
Expand Down Expand Up @@ -442,7 +442,7 @@
<customView translatesAutoresizingMaskIntoConstraints="NO" id="BE1-yC-oJL" customClass="TimeLabelOverflowedView" customModule="IINA" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="500" height="29"/>
<subviews>
<slider verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="eBP-6g-bAT">
<slider verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="eBP-6g-bAT" customClass="PlaySlider" customModule="IINA" customModuleProvider="target">
<rect key="frame" x="48" y="-1" width="404" height="28"/>
<sliderCell key="cell" continuous="YES" refusesFirstResponder="YES" state="on" alignment="left" maxValue="100" doubleValue="50" tickMarkPosition="above" sliderType="linear" id="f1c-xF-8a2" customClass="PlaySliderCell" customModule="IINA" customModuleProvider="target"/>
<connections>
Expand Down
1 change: 1 addition & 0 deletions iina/ExtendedColors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ extension NSColor.Name {
static let mainSliderBarChapterStroke = NSColor.Name("MainSliderBarChapterStroke")
static let mainSliderKnob = NSColor.Name("MainSliderKnob")
static let mainSliderKnobActive = NSColor.Name("MainSliderKnobActive")
static let mainSliderLoopKnob = NSColor.Name("MainSliderLoopKnob")

static let titleBarBorder = NSColor.Name("TitleBarBorder")

Expand Down
2 changes: 1 addition & 1 deletion iina/MainMenuActions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ extension MainMenuActionHandler {
}

@objc func menuABLoop(_ sender: NSMenuItem) {
player.abLoop()
player.mainWindow.abLoop()
}

@objc func menuFileLoop(_ sender: NSMenuItem) {
Expand Down
44 changes: 42 additions & 2 deletions iina/MainWindowController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -632,9 +632,41 @@ class MainWindowController: PlayerWindowController {
setWindowFrameForLegacyFullScreen()
}

// Observe the loop knobs on the progress bar and update mpv when the knobs move.
addObserver(to: .default, forName: .iinaPlaySliderLoopKnobChanged, object: playSlider.abLoopA) { [weak self] _ in
guard let self = self else { return }
let seconds = self.percentToSeconds(self.playSlider.abLoopA.doubleValue)
self.player.abLoopA = seconds
self.player.sendOSD(.abLoopUpdate(.aSet, VideoTime(seconds).stringRepresentation))
}
addObserver(to: .default, forName: .iinaPlaySliderLoopKnobChanged, object: playSlider.abLoopB) { [weak self] _ in
guard let self = self else { return }
let seconds = self.percentToSeconds(self.playSlider.abLoopB.doubleValue)
self.player.abLoopB = seconds
self.player.sendOSD(.abLoopUpdate(.bSet, VideoTime(seconds).stringRepresentation))
}

player.events.emit(.windowLoaded)
}

/// Returns the position in seconds for the given percent of the total duration of the video the percentage represents.
///
/// The number of seconds returned must be considered an estimate that could change. The duration of the video is obtained from
/// the [mpv](https://mpv.io/manual/stable/) `duration` property. The documentation for this property cautions that
/// mpv is not always able to determine the duration and when it does return a duration it may be an estimate. If the duration is
/// unknown this method will fallback to using the current playback position, if that is known. Otherwise this method will return zero.
/// - Parameter percent: Position in the video as a percentage of the duration.
/// - Returns: The position in the video the given percentage represents.
private func percentToSeconds(_ percent: Double) -> Double {
if let duration = player.info.videoDuration?.second {
return duration * percent / 100
} else if let position = player.info.videoPosition?.second {
return position * percent / 100
} else {
return 0
}
}

/** Set material for OSC and title bar */
override internal func setMaterial(_ theme: Preference.Theme?) {
if #available(macOS 10.14, *) {
Expand Down Expand Up @@ -1122,14 +1154,22 @@ class MainWindowController: PlayerWindowController {
guard let w = self.window, let cv = w.contentView else { return }
if cv.trackingAreas.isEmpty {
cv.addTrackingArea(NSTrackingArea(rect: cv.bounds,
options: [.activeAlways, .inVisibleRect, .mouseEnteredAndExited, .mouseMoved],
options: [.activeAlways, .enabledDuringMouseDrag, .inVisibleRect, .mouseEnteredAndExited, .mouseMoved],
owner: self, userInfo: ["obj": 0]))
}
if playSlider.trackingAreas.isEmpty {
playSlider.addTrackingArea(NSTrackingArea(rect: playSlider.bounds,
options: [.activeAlways, .inVisibleRect, .mouseEnteredAndExited, .mouseMoved],
options: [.activeAlways, .enabledDuringMouseDrag, .inVisibleRect, .mouseEnteredAndExited, .mouseMoved],
owner: self, userInfo: ["obj": 1]))
}
// Track the thumbs on the progress bar representing the A-B loop points and treat them as part
// of the slider.
if playSlider.abLoopA.trackingAreas.count <= 1 {
playSlider.abLoopA.addTrackingArea(NSTrackingArea(rect: playSlider.abLoopA.bounds, options: [.activeAlways, .enabledDuringMouseDrag, .inVisibleRect, .mouseEnteredAndExited, .mouseMoved], owner: self, userInfo: ["obj": 1]))
}
if playSlider.abLoopB.trackingAreas.count <= 1 {
playSlider.abLoopB.addTrackingArea(NSTrackingArea(rect: playSlider.abLoopB.bounds, options: [.activeAlways, .enabledDuringMouseDrag, .inVisibleRect, .mouseEnteredAndExited, .mouseMoved], owner: self, userInfo: ["obj": 1]))
}

// update timer
updateTimer()
Expand Down
24 changes: 19 additions & 5 deletions iina/OSDMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ enum OSDMessage {
case mute
case unMute
case screenshot
case abLoop(Int)
case abLoop(PlaybackInfo.LoopStatus)
case abLoopUpdate(PlaybackInfo.LoopStatus, String)
case stop
case chapter(String)
case track(MPVTrack)
Expand Down Expand Up @@ -181,12 +182,25 @@ enum OSDMessage {
return (NSLocalizedString("osd.screenshot", comment: "Screenshot Captured"), .normal)

case .abLoop(let value):
if value == 1 {
// The A-B loop command was invoked.
switch (value) {
case .cleared:
return (NSLocalizedString("osd.abloop.clear", comment: "AB-Loop: Cleared"), .normal)
case .aSet:
return (NSLocalizedString("osd.abloop.a", comment: "AB-Loop: A"), .withText("{{position}} / {{duration}}"))
} else if value == 2 {
case .bSet:
return (NSLocalizedString("osd.abloop.b", comment: "AB-Loop: B"), .withText("{{position}} / {{duration}}"))
} else {
return (NSLocalizedString("osd.abloop.clear", comment: "AB-Loop: Cleared"), .normal)
}

case .abLoopUpdate(let value, let position):
// One of the A-B loop points has been updated to the given position.
switch (value) {
case .cleared:
Logger.fatal("Attempt to display invalid OSD message, type: .abLoopUpdate value: .cleared position \(position)")
case .aSet:
return (NSLocalizedString("osd.abloop.a", comment: "AB-Loop: A"), .withText("\(position) / {{duration}}"))
case .bSet:
return (NSLocalizedString("osd.abloop.b", comment: "AB-Loop: B"), .withText("\(position) / {{duration}}"))
}

case .stop:
Expand Down
79 changes: 79 additions & 0 deletions iina/PlaySlider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//
// PlaySlider.swift
// iina
//
// Created by low-batt on 10/11/21.
// Copyright © 2021 lhc. All rights reserved.
//

import Cocoa

/// A custom [slider](https://developer.apple.com/design/human-interface-guidelines/macos/selectors/sliders/)
/// for the onscreen controller.
///
/// This slider adds two thumbs (referred to as knobs in code) to the progress bar slider to show the A and B loop points of the
/// [mpv](https://mpv.io/manual/stable/) A-B loop feature and allow the loop points to be adjusted. When the feature is
/// disabled the additional thumbs are hidden.
/// - Requires: The custom slider cell provided by `PlaySliderCell` **must** be used with this class.
/// - Note: Unlike `NSSlider` the `draw` method of this class will do nothing if the view is hidden.
final class PlaySlider: NSSlider {

/// Knob representing the A loop point for the mpv A-B loop feature.
var abLoopA: PlaySliderLoopKnob { abLoopAKnob }

/// Knob representing the B loop point for the mpv A-B loop feature.
var abLoopB: PlaySliderLoopKnob { abLoopBKnob }

/// The slider's cell correctly typed for convenience.
var customCell: PlaySliderCell { cell as! PlaySliderCell }

/// Range of values the slider is configured to return.
var range: ClosedRange<Double> { minValue...maxValue }

/// Span of the range of values the slider is configured to return.
var span: Double { maxValue - minValue }

// MARK:- Private Properties

private var abLoopAKnob: PlaySliderLoopKnob!

private var abLoopBKnob: PlaySliderLoopKnob!

// MARK:- Initialization

required init?(coder: NSCoder) {
super.init(coder: coder)
if #available(macOS 11, *) {
// Apple increased the height of sliders in Big Sur. Until we have time to restructure the
// on screen controller to accommodate a larger slider reduce the size of the slider from
// regular to small. This makes the slider match the behavior seen under Catalina. This MUST
// be set before creating the loop knobs as it changes the height of knobs which is referenced
// during loop knob initialization.
controlSize = .small
}
abLoopAKnob = PlaySliderLoopKnob(slider: self, toolTip: "A-B loop A")
abLoopBKnob = PlaySliderLoopKnob(slider: self, toolTip: "A-B loop B")
}

// MARK:- Drawing

override func draw(_ dirtyRect: NSRect) {
// With the onscreen controller hidden and a movie playing spindumps showed time being spent
// drawing the slider even though it was not visible. Apparently NSSlider is missing the
// following check.
guard !isHiddenOrHasHiddenAncestor else { return }
super.draw(dirtyRect)
abLoopA.draw(dirtyRect)
abLoopB.draw(dirtyRect)
}

override func viewDidUnhide() {
super.viewDidUnhide()
// When IINA is not the application being used and the onscreen controller is hidden if the
// mouse is moved over an IINA window the IINA will unhide the controller. If the slider is
// not marked as needing display the controller will show without the slider. I would have
// thought the NSView method would do this. The current Apple documentation does not say what
// the NSView method does or even if it needs to be called by subclasses.
needsDisplay = true
}
}

0 comments on commit 24630f1

Please sign in to comment.