Skip to content
This repository has been archived by the owner on Sep 16, 2023. It is now read-only.

Commit

Permalink
Close add A-B Loop indicator on progress bar, iina#548
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
  • Loading branch information
low-batt authored and CarterLi committed Apr 15, 2022
1 parent 242f8eb commit 7f07e89
Show file tree
Hide file tree
Showing 14 changed files with 550 additions and 34 deletions.
8 changes: 8 additions & 0 deletions iina.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@
49718004272BA685003F328C /* libswscale.5.dylib in Copy Dylibs */ = {isa = PBXBuildFile; fileRef = 49717FA8272BA677003F328C /* libswscale.5.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
49718005272BA685003F328C /* libuchardet.0.dylib in Copy Dylibs */ = {isa = PBXBuildFile; fileRef = 49717FAF272BA677003F328C /* libuchardet.0.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
49718006272BA685003F328C /* libzstd.1.dylib in Copy Dylibs */ = {isa = PBXBuildFile; fileRef = 49717FBE272BA678003F328C /* libzstd.1.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
51492FCE2714DE0C00C1FC3C /* PlaySlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51492FCD2714DE0C00C1FC3C /* PlaySlider.swift */; };
51B494D82718FDD700B9381A /* PlaySliderLoopKnob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B494D72718FDD700B9381A /* PlaySliderLoopKnob.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 */; };
8400D5C61E1AB2F1006785F5 /* MainWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 8400D5C81E1AB2F1006785F5 /* MainWindowController.xib */; };
Expand Down Expand Up @@ -500,6 +502,8 @@
49717FC2272BA678003F328C /* libavcodec.58.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libavcodec.58.dylib; path = deps/lib/libavcodec.58.dylib; sourceTree = "<group>"; };
49717FC3272BA678003F328C /* libintl.8.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libintl.8.dylib; path = deps/lib/libintl.8.dylib; sourceTree = "<group>"; };
49717FC4272BA678003F328C /* libavutil.56.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libavutil.56.dylib; path = deps/lib/libavutil.56.dylib; sourceTree = "<group>"; };
51492FCD2714DE0C00C1FC3C /* PlaySlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaySlider.swift; sourceTree = "<group>"; };
51B494D72718FDD700B9381A /* PlaySliderLoopKnob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaySliderLoopKnob.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>"; };
5EF9F7E521FB42E900748374 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/MainMenu.strings"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1961,7 +1965,9 @@
E3DBD23D218EF4F100B3AFBF /* AboutWindowContributorAvatarItem.xib */,
9E47DABF1E3CFA6D00457420 /* DurationDisplayTextField.swift */,
843FFD4C1D5DAA01001F3A44 /* RoundedTextFieldCell.swift */,
51492FCD2714DE0C00C1FC3C /* PlaySlider.swift */,
8461C52D1D45FFF6006E91FF /* PlaySliderCell.swift */,
51B494D72718FDD700B9381A /* PlaySliderLoopKnob.swift */,
84F725551D4783EE000DEF1B /* VolumeSliderCell.swift */,
8434BAAC1D5E4546003BECF2 /* SlideUpButton.swift */,
8487BEC01D744FA800FD17B0 /* FlippedView.swift */,
Expand Down Expand Up @@ -2351,6 +2357,7 @@
84EB1F071D2F5E76004FA5A1 /* Utility.swift in Sources */,
84D0FB7E1F5E519300C6A6A7 /* FreeSelectingViewController.swift in Sources */,
840D47A01DFEFC49000D9A64 /* KeyRecordView.swift in Sources */,
51492FCE2714DE0C00C1FC3C /* PlaySlider.swift in Sources */,
84791C8B1D405E9D0069E28A /* PlaybackInfo.swift in Sources */,
E322A4F820A8442E00C67D32 /* PlaylistPlaybackProgressView.swift in Sources */,
84D123B41ECAA405004E0D53 /* TouchBarSupport.swift in Sources */,
Expand Down Expand Up @@ -2428,6 +2435,7 @@
845040471E0B0F500079C194 /* CropSettingsViewController.swift in Sources */,
E33836852026223800ABC812 /* PrefOSCToolbarSettingsSheetController.swift in Sources */,
E301EFDA21312AB300BC8588 /* KeychainAccess.swift in Sources */,
51B494D82718FDD700B9381A /* PlaySliderLoopKnob.swift in Sources */,
8434BAA71D5DF2DA003BECF2 /* Extensions.swift in Sources */,
845404AC1E43980900B02B12 /* LuaScript.swift in Sources */,
847557141F405F8C0006B0FF /* MainWindowMenuActions.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 @@ -110,4 +110,5 @@ extension Notification.Name {
static let iinaHistoryUpdated = Notification.Name("IINAHistoryUpdated")
static let iinaLegacyFullScreen = Notification.Name("IINALegacyFullScreen")
static let iinaKeyBindingChanged = Notification.Name("iinaKeyBindingChanged")
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 @@ -448,7 +448,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 @@ -135,7 +135,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 @@ -578,6 +578,38 @@ class MainWindowController: PlayerWindowController {
self.cachedScreenCount = screenCount
self.videoView.updateDisplayLink()
}

// 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 strongSelf = self else { return }
let seconds = strongSelf.percentToSeconds(strongSelf.playSlider.abLoopA.doubleValue)
strongSelf.player.abLoopA = seconds
strongSelf.player.sendOSD(.abLoopUpdate(.aSet, VideoTime(seconds).stringRepresentation))
}
addObserver(to: .default, forName: .iinaPlaySliderLoopKnobChanged, object: playSlider.abLoopB) { [weak self] _ in
guard let strongSelf = self else { return }
let seconds = strongSelf.percentToSeconds(strongSelf.playSlider.abLoopB.doubleValue)
strongSelf.player.abLoopB = seconds
strongSelf.player.sendOSD(.abLoopUpdate(.bSet, VideoTime(seconds).stringRepresentation))
}
}

/// 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 */
Expand Down Expand Up @@ -1016,14 +1048,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 @@ -47,7 +47,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 @@ -173,12 +174,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
88 changes: 88 additions & 0 deletions iina/PlaySlider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
//
// 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 }

// MARK:- Private Properties

private var abLoopAKnob: PlaySliderLoopKnob!

private var abLoopBKnob: PlaySliderLoopKnob!

// MARK:- Initialization

required init?(coder: NSCoder) {
super.init(coder: coder)
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)
}

// MARK:- Mouse Events

override func mouseDown(with event: NSEvent) {
let clickLocation = convert(event.locationInWindow, from: nil)
// When the knobs are overlapping we assume the user is trying to move the play knob rather than
// change the loop points. So we intentionally test first for the mouse clicking on the play
// knob, then test the B knob and then test the A knob and lastly default to the slider itself,
// which will move the play knob.
if isMousePoint(clickLocation, in: customCell.knobRect(flipped: isFlipped)) {
super.mouseDown(with: event)
return
}
if !abLoopB.isHidden && isMousePoint(clickLocation, in: abLoopB.frame) {
abLoopB.beginDragging(with: event)
return
}
if !abLoopA.isHidden && isMousePoint(clickLocation, in: abLoopA.frame) {
abLoopA.beginDragging(with: event)
return
}
super.mouseDown(with: event)
}

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 of 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 7f07e89

Please sign in to comment.