Skip to content

Commit

Permalink
Merge pull request #1434 from Eskils/feature/select-which-sizes-to-cy…
Browse files Browse the repository at this point in the history
…cle-between-on-repeated-commands

Repeated commands: Select which sizes to cycle between on repeated half actions
  • Loading branch information
rxhanson committed Aug 9, 2024
2 parents a4fe867 + 49415a3 commit aec69ed
Show file tree
Hide file tree
Showing 8 changed files with 418 additions and 183 deletions.
4 changes: 4 additions & 0 deletions Rectangle.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
6490B39F27BF98840056C220 /* BottomCenterRightEighthCalculation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6490B39E27BF98840056C220 /* BottomCenterRightEighthCalculation.swift */; };
6490B3A127BF98C70056C220 /* BottomRightEighthCalculation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6490B3A027BF98C70056C220 /* BottomRightEighthCalculation.swift */; };
729E0A982AFF76B1006E2F48 /* CenterProminentlyCalculation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 729E0A972AFF76B1006E2F48 /* CenterProminentlyCalculation.swift */; };
7BE578EF2C5BF4EE0083DAE3 /* CycleSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BE578EE2C5BF4ED0083DAE3 /* CycleSize.swift */; };
866661F2257D248A00A9CD2D /* RepeatedExecutionsInThirdsCalculation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 866661F1257D248A00A9CD2D /* RepeatedExecutionsInThirdsCalculation.swift */; };
94E9B08E2C3B8D97004C7F41 /* MacTilingDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94E9B08D2C3B8D97004C7F41 /* MacTilingDefaults.swift */; };
94E9B0902C3E4578004C7F41 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94E9B08F2C3E4578004C7F41 /* StringExtension.swift */; };
Expand Down Expand Up @@ -186,6 +187,7 @@
6490B39E27BF98840056C220 /* BottomCenterRightEighthCalculation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomCenterRightEighthCalculation.swift; sourceTree = "<group>"; };
6490B3A027BF98C70056C220 /* BottomRightEighthCalculation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomRightEighthCalculation.swift; sourceTree = "<group>"; };
729E0A972AFF76B1006E2F48 /* CenterProminentlyCalculation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CenterProminentlyCalculation.swift; sourceTree = "<group>"; };
7BE578EE2C5BF4ED0083DAE3 /* CycleSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CycleSize.swift; sourceTree = "<group>"; };
866661F1257D248A00A9CD2D /* RepeatedExecutionsInThirdsCalculation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepeatedExecutionsInThirdsCalculation.swift; sourceTree = "<group>"; };
94E9B08D2C3B8D97004C7F41 /* MacTilingDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacTilingDefaults.swift; sourceTree = "<group>"; };
94E9B08F2C3E4578004C7F41 /* StringExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -552,6 +554,7 @@
9821405F22B3EFB200ABFB3F /* Defaults.swift */,
984EDB0E29A42ED200D119D2 /* LaunchOnLogin.swift */,
98C1008B2305F1FA006E5344 /* SubsequentExecutionMode.swift */,
7BE578EE2C5BF4ED0083DAE3 /* CycleSize.swift */,
985B9BF422B93EEC00A2E8F0 /* ApplicationToggle.swift */,
9824703022AFA8470037B409 /* RectangleStatusItem.swift */,
9824703622B0F3200037B409 /* WindowAction.swift */,
Expand Down Expand Up @@ -922,6 +925,7 @@
9824703722B0F3200037B409 /* WindowAction.swift in Sources */,
B4521F932BD7CEFB00FD43CC /* ChangeWindowDimensionCalculation.swift in Sources */,
9821402922B3889100ABFB3F /* LowerLeftCalculation.swift in Sources */,
7BE578EF2C5BF4EE0083DAE3 /* CycleSize.swift in Sources */,
9821402122B3884600ABFB3F /* BottomHalfCalculation.swift in Sources */,
98910B42231476B30066EC23 /* PrefsViewController.swift in Sources */,
9851A5C3251BEBA300ECF78C /* OrientationAware.swift in Sources */,
Expand Down
221 changes: 122 additions & 99 deletions Rectangle/Base.lproj/Main.storyboard

Large diffs are not rendered by default.

130 changes: 130 additions & 0 deletions Rectangle/CycleSize.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
//
// CycleSize.swift
// Rectangle
//
// Created by Eskil Gjerde Sviggum on 01/08/2024.
// Copyright © 2024 Ryan Hanson. All rights reserved.
//

import Foundation

enum CycleSize: Int, CaseIterable {
case twoThirds = 0
case oneHalf = 1
case oneThird = 2
case oneQuarter = 3
case threeQuarters = 4

static func fromBits(bits: Int) -> Set<CycleSize> {
Set(
Self.allCases.filter {
(bits >> $0.rawValue) & 1 == 1
}
)
}

static var firstSize = CycleSize.oneHalf
static var defaultSizes: Set<CycleSize> = [.oneHalf, .twoThirds, .oneThird]

// The expected order of the cycle sizes is to start with the
// first division, then go gradually upwards in size and wrap
// around to the smaller sizes.
//
// For example if all cycles are used, the order should be:
// 1/2, 2/3, 3/4, 1/4, 1/3
static var sortedSizes: [CycleSize] = {
let sortedSizes = Self.allCases.sorted(by: { $0.fraction < $1.fraction })

guard let firstSizeIndex = sortedSizes.firstIndex(of: firstSize) else {
return sortedSizes
}

let lessThanFistSizes = sortedSizes[0..<firstSizeIndex]
let greaterThanFistSizes = sortedSizes[(firstSizeIndex + 1)..<sortedSizes.count]

return [firstSize] + greaterThanFistSizes + lessThanFistSizes
}()
}

extension CycleSize {

var title: String {
switch self {
case .twoThirds:
""
case .oneHalf:
"½"
case .oneThird:
""
case .oneQuarter:
"¼"
case .threeQuarters:
"¾"
}
}

var fraction: Float {
switch self {
case .twoThirds:
2 / 3
case .oneHalf:
1 / 2
case .oneThird:
1 / 3
case .oneQuarter:
1 / 4
case .threeQuarters:
3 / 4
}
}

var isAlwaysEnabled: Bool {
if self == .firstSize {
return true
}

return false
}

}

extension Set where Element == CycleSize {
func toBits() -> Int {
var bits = 0
self.forEach {
bits |= 1 << $0.rawValue
}
return bits
}
}

class CycleSizesDefault: Default {
public private(set) var key: String = "selectedCycleSizes"
private var initialized = false

var value: Set<CycleSize> {
didSet {
if initialized {
UserDefaults.standard.set(value.toBits(), forKey: key)
}
}
}

init() {
let bits = UserDefaults.standard.integer(forKey: key)
value = CycleSize.fromBits(bits: bits)
initialized = true
}

func load(from codable: CodableDefault) {
if let bits = codable.int {
let divisions = CycleSize.fromBits(bits: bits)
value = divisions
}
}

func toCodable() -> CodableDefault {
return CodableDefault(int: value.toBits())
}

}
4 changes: 4 additions & 0 deletions Rectangle/Defaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ class Defaults {
static let hideMenuBarIcon = BoolDefault(key: "hideMenubarIcon")
static let alternateDefaultShortcuts = BoolDefault(key: "alternateDefaultShortcuts") // switch to magnet defaults
static let subsequentExecutionMode = SubsequentExecutionDefault()
static let selectedCycleSizes = CycleSizesDefault()
static let cycleSizesIsChanged = BoolDefault(key: "cycleSizesIsChanged")
static let allowAnyShortcut = BoolDefault(key: "allowAnyShortcut")
static let windowSnapping = OptionalBoolDefault(key: "windowSnapping")
static let almostMaximizeHeight = FloatDefault(key: "almostMaximizeHeight")
Expand Down Expand Up @@ -95,6 +97,8 @@ class Defaults {
hideMenuBarIcon,
alternateDefaultShortcuts,
subsequentExecutionMode,
selectedCycleSizes,
cycleSizesIsChanged,
allowAnyShortcut,
windowSnapping,
almostMaximizeHeight,
Expand Down
121 changes: 117 additions & 4 deletions Rectangle/PrefsWindow/SettingsViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,17 @@ class SettingsViewController: NSViewController {
@IBOutlet weak var stageSlider: NSSlider!
@IBOutlet weak var stageLabel: NSTextField!

@IBOutlet weak var cycleSizesView: NSStackView!

@IBOutlet var cycleSizesViewHeightConstraint: NSLayoutConstraint!

@IBOutlet var todoViewHeightConstraint: NSLayoutConstraint!


private var aboutTodoWindowController: NSWindowController?

private var cycleSizeCheckboxes = [NSButton]()

@IBAction func toggleLaunchOnLogin(_ sender: NSButton) {
let newSetting: Bool = sender.state == .on
if #available(macOS 13, *) {
Expand Down Expand Up @@ -64,6 +73,7 @@ class SettingsViewController: NSViewController {
}

Defaults.subsequentExecutionMode.value = mode
initializeCycleSizesView(animated: true)
}

@IBAction func gapSliderChanged(_ sender: NSSlider) {
Expand Down Expand Up @@ -123,7 +133,7 @@ class SettingsViewController: NSViewController {
@IBAction func toggleTodoMode(_ sender: NSButton) {
let newSetting: Bool = sender.state == .on
Defaults.todo.enabled = newSetting
showHideTodoModeSettings()
showHideTodoModeSettings(animated: true)
Notification.Name.todoMenuToggled.post()
}

Expand Down Expand Up @@ -226,9 +236,22 @@ class SettingsViewController: NSViewController {

initializeTodoModeSettings()

self.cycleSizeCheckboxes.forEach {
$0.removeFromSuperview()
}

let cycleSizeCheckboxes = makeCycleSizeCheckboxes()
cycleSizeCheckboxes.forEach { checkbox in
cycleSizesView.addArrangedSubview(checkbox)
}
self.cycleSizeCheckboxes = cycleSizeCheckboxes

initializeCycleSizesView(animated: false)

Notification.Name.configImported.onPost(using: {_ in
self.initializeTodoModeSettings()
self.initializeToggles()
self.initializeCycleSizesView(animated: false)
})

Notification.Name.menuBarIconHidden.onPost(using: {_ in
Expand All @@ -249,11 +272,15 @@ class SettingsViewController: NSViewController {
TodoManager.initReflowShortcut()
toggleTodoShortcutView.setAssociatedUserDefaultsKey(TodoManager.toggleDefaultsKey, withTransformerName: MASDictionaryTransformerName)
reflowTodoShortcutView.setAssociatedUserDefaultsKey(TodoManager.reflowDefaultsKey, withTransformerName: MASDictionaryTransformerName)
showHideTodoModeSettings()
showHideTodoModeSettings(animated: false)
}

private func showHideTodoModeSettings() {
todoView.isHidden = !Defaults.todo.userEnabled
private func showHideTodoModeSettings(animated: Bool) {
animateChanges(animated: animated) {
let isEnabled = Defaults.todo.userEnabled
todoView.isHidden = !isEnabled
todoViewHeightConstraint.isActive = !isEnabled
}
}

func initializeToggles() {
Expand Down Expand Up @@ -282,6 +309,92 @@ class SettingsViewController: NSViewController {
} else {
stageView.isHidden = true
}


setToggleStatesForCycleSizeCheckboxes()
}

private func initializeCycleSizesView(animated: Bool = false) {
let showOptionsView = Defaults.subsequentExecutionMode.value == .resize

if showOptionsView {
setToggleStatesForCycleSizeCheckboxes()
}

animateChanges(animated: animated) {
cycleSizesView.isHidden = !showOptionsView
cycleSizesViewHeightConstraint.isActive = !showOptionsView
}
}

private func animateChanges(animated: Bool, block: () -> Void) {
if animated {
NSAnimationContext.runAnimationGroup({context in
context.duration = 0.3
context.allowsImplicitAnimation = true

block()
view.layoutSubtreeIfNeeded()
}, completionHandler: nil)
} else {
block()
}
}

private func makeCycleSizeCheckboxes() -> [NSButton] {
CycleSize.sortedSizes.map { division in
let button = NSButton(checkboxWithTitle: division.title, target: self, action: #selector(didCheckCycleSizeCheckbox(sender:)))
button.tag = division.rawValue
button.setContentCompressionResistancePriority(.required, for: .vertical)
return button
}
}

@objc private func didCheckCycleSizeCheckbox(sender: Any?) {
guard let checkbox = sender as? NSButton else {
Logger.log("Expected action to be sent from NSButton. Instead, sender is: \(String(describing: sender))")
return
}

let rawValue = checkbox.tag

guard let cycleSize = CycleSize(rawValue: rawValue) else {
Logger.log("Expected tag of cycle size checkbox to match a value of CycleSize. Got: \(String(describing: rawValue))")
return
}

// If selected cycle sizes has not been changed, write the defaults.
if !Defaults.cycleSizesIsChanged.enabled {
Defaults.selectedCycleSizes.value = CycleSize.defaultSizes
}

Defaults.cycleSizesIsChanged.enabled = true

if checkbox.state == .on {
Defaults.selectedCycleSizes.value.insert(cycleSize)
} else {
Defaults.selectedCycleSizes.value.remove(cycleSize)
}
}

private func setToggleStatesForCycleSizeCheckboxes() {
let useDefaultCycleSizes = !Defaults.cycleSizesIsChanged.enabled
let cycleSizes = useDefaultCycleSizes ? CycleSize.defaultSizes : Defaults.selectedCycleSizes.value

cycleSizeCheckboxes.forEach { checkbox in
guard let cycleSizeForCheckbox = CycleSize(rawValue: checkbox.tag) else {
return
}

let isAlwaysEnabled = cycleSizeForCheckbox.isAlwaysEnabled
let isChecked = isAlwaysEnabled || cycleSizes.contains(cycleSizeForCheckbox)
checkbox.state = isChecked ? .on : .off

// Show that the box cannot be unchecked.
if isAlwaysEnabled {
checkbox.isEnabled = false
}
}
}

}
Expand Down
22 changes: 9 additions & 13 deletions Rectangle/WindowCalculation/RepeatedExecutionsCalculation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ protocol RepeatedExecutionsCalculation {

func calculateFirstRect(_ params: RectCalculationParameters) -> RectResult

func calculateSecondRect(_ params: RectCalculationParameters) -> RectResult

func calculateThirdRect(_ params: RectCalculationParameters) -> RectResult
func calculateRect(for cycleDivision: CycleSize, params: RectCalculationParameters) -> RectResult

}

Expand All @@ -27,18 +25,16 @@ extension RepeatedExecutionsCalculation {
else {
return calculateFirstRect(params)
}

let position = count % 3

switch (position) {
case 1:
return calculateSecondRect(params)
case 2:
return calculateThirdRect(params)
default:
return calculateFirstRect(params)
}
let useDefaultPositions = !Defaults.cycleSizesIsChanged.enabled
let positions = useDefaultPositions ? CycleSize.defaultSizes : Defaults.selectedCycleSizes.value

let sortedPositions = CycleSize.sortedSizes
.filter { positions.contains($0) }

let position = count % sortedPositions.count

return calculateRect(for: sortedPositions[position], params: params)
}

}
Loading

0 comments on commit aec69ed

Please sign in to comment.