From ec79d8783e0117ebf2ab1a26e73c427914f51354 Mon Sep 17 00:00:00 2001 From: Matthew Davidson Date: Sun, 24 May 2020 20:15:48 +1000 Subject: [PATCH] Added multiple screen support and improved transition between connecting/disconnecting screens. --- Yippy.xcodeproj/project.pbxproj | 10 +++---- .../Extensions/NSMenuItem+Functional.swift | 1 + Yippy/Sources/Models/Controller.swift | 8 +++--- .../Sources/Models/History/HistoryItem.swift | 10 +++++++ Yippy/Sources/Models/PanelPosition.swift | 27 +++++++++---------- Yippy/Sources/Models/State.swift | 23 +++++++++++++++- .../Windows/Yippy/YippyWindowController.swift | 11 ++++---- YippyUITests/YippyUITests.swift | 6 ++--- 8 files changed, 63 insertions(+), 33 deletions(-) diff --git a/Yippy.xcodeproj/project.pbxproj b/Yippy.xcodeproj/project.pbxproj index e04dd56..0d3df86 100644 --- a/Yippy.xcodeproj/project.pbxproj +++ b/Yippy.xcodeproj/project.pbxproj @@ -1341,7 +1341,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 2.3.0; + MARKETING_VERSION = 2.4.0; PRODUCT_BUNDLE_IDENTIFIER = MatthewDavidson.Yippy; PRODUCT_MODULE_NAME = "$(PRODUCT_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1367,7 +1367,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 2.3.0; + MARKETING_VERSION = 2.4.0; PRODUCT_BUNDLE_IDENTIFIER = MatthewDavidson.Yippy; PRODUCT_MODULE_NAME = "$(PRODUCT_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1541,7 +1541,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 2.3.0; + MARKETING_VERSION = 2.4.0; PRODUCT_BUNDLE_IDENTIFIER = MatthewDavidson.YippyBeta; PRODUCT_MODULE_NAME = "$(PRODUCT_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME) Beta"; @@ -1666,7 +1666,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 2.3.0; + MARKETING_VERSION = 2.4.0; PRODUCT_BUNDLE_IDENTIFIER = MatthewDavidson.YippyBeta; PRODUCT_MODULE_NAME = "$(PRODUCT_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME) Beta"; @@ -1798,7 +1798,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 2.3.0; + MARKETING_VERSION = 2.4.0; PRODUCT_BUNDLE_IDENTIFIER = MatthewDavidson.YippyXCTest; PRODUCT_MODULE_NAME = "$(PRODUCT_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Yippy/Sources/Extensions/NSMenuItem+Functional.swift b/Yippy/Sources/Extensions/NSMenuItem+Functional.swift index ef6a6d4..d1471e2 100644 --- a/Yippy/Sources/Extensions/NSMenuItem+Functional.swift +++ b/Yippy/Sources/Extensions/NSMenuItem+Functional.swift @@ -10,6 +10,7 @@ import Cocoa extension NSMenuItem { + @discardableResult func with(submenu: NSMenu) -> NSMenuItem { self.submenu = submenu return self diff --git a/Yippy/Sources/Models/Controller.swift b/Yippy/Sources/Models/Controller.swift index cf397d1..d008657 100644 --- a/Yippy/Sources/Models/Controller.swift +++ b/Yippy/Sources/Models/Controller.swift @@ -56,7 +56,7 @@ class Controller { self.statusItem.menu = Self.createMenu(settings: settings, state: state, target: self) // Create yippy window controller - self.yippyWindowController = Self.createYippyWindowController(isHistoryPanelShown: state.isHistoryPanelShown, panelPosition: state.panelPosition, disposeBag: state.disposeBag) + self.yippyWindowController = Self.createYippyWindowController(state: state, disposeBag: state.disposeBag) // Create preview window controllers self.previewWindowController = Self.createPreviewWindowController(previewItem: state.previewHistoryItem, disposeBag: state.disposeBag) @@ -152,13 +152,13 @@ class Controller { } } - static func createYippyWindowController(isHistoryPanelShown: BehaviorRelay, panelPosition: BehaviorRelay, disposeBag: DisposeBag) -> YippyWindowController { + static func createYippyWindowController(state: State, disposeBag: DisposeBag) -> YippyWindowController { let controller = YippyWindowController.createYippyWindowController() controller - .subscribeTo(toggle: isHistoryPanelShown) + .subscribeTo(toggle: state.isHistoryPanelShown) .disposed(by: disposeBag) controller - .subscribePositionTo(position: panelPosition) + .subscribeFrameTo(position: state.panelPosition.asObservable(), screen: state.currentScreen.asObservable()) .disposed(by: disposeBag) return controller } diff --git a/Yippy/Sources/Models/History/HistoryItem.swift b/Yippy/Sources/Models/History/HistoryItem.swift index b94607b..001cb76 100644 --- a/Yippy/Sources/Models/History/HistoryItem.swift +++ b/Yippy/Sources/Models/History/HistoryItem.swift @@ -212,6 +212,16 @@ class HistoryItem: NSObject { pasteboard.setData(data, forType: .color) return NSColor(from: pasteboard) } + + private func isStringLink(string: String) -> Bool { + let types: NSTextCheckingResult.CheckingType = [.link] + let detector = try? NSDataDetector(types: types.rawValue) + guard (detector != nil && string.count > 0) else { return false } + if detector!.numberOfMatches(in: string, options: NSRegularExpression.MatchingOptions(rawValue: 0), range: NSMakeRange(0, string.count)) > 0 { + return true + } + return false + } } // MARK: - HistoryItem+NSPasteboardWriting diff --git a/Yippy/Sources/Models/PanelPosition.swift b/Yippy/Sources/Models/PanelPosition.swift index c3c8a03..105c7f6 100644 --- a/Yippy/Sources/Models/PanelPosition.swift +++ b/Yippy/Sources/Models/PanelPosition.swift @@ -19,33 +19,32 @@ enum PanelPosition: Int, Codable, CaseIterable { case centerLarge = 6 case fullScreen = 7 - var frame: NSRect { - // TODO: Use NSEvent.mouseLocation to choose which screen + public func getFrame(forScreen screen: NSScreen) -> NSRect { switch self { case .right: - return NSRect(x: NSScreen.main!.frame.maxX - Constants.panel.menuWidth, y: 0, width: Constants.panel.menuWidth, height: NSScreen.main!.frame.maxY) + return NSRect(x: screen.frame.maxX - Constants.panel.menuWidth, y: screen.frame.minY, width: Constants.panel.menuWidth, height: screen.frame.maxY) case .left: - return NSRect(x: 0, y: 0, width: Constants.panel.menuWidth, height: NSScreen.main!.frame.maxY) + return NSRect(x: screen.frame.minX, y: screen.frame.minY, width: Constants.panel.menuWidth, height: screen.frame.maxY) case .top: - return NSRect(x: 0, y: NSScreen.main!.frame.maxY - Constants.panel.menuHeight, width: NSScreen.main!.frame.width, height: Constants.panel.menuHeight) + return NSRect(x: screen.frame.minX, y: screen.frame.maxY - Constants.panel.menuHeight, width: screen.frame.width, height: Constants.panel.menuHeight) case .bottom: - return NSRect(x: 0, y: 0, width: NSScreen.main!.frame.width, height: Constants.panel.menuHeight) + return NSRect(x: screen.frame.minX, y: screen.frame.minY, width: screen.frame.width, height: Constants.panel.menuHeight) case .centerSmall: - let size = NSSize(width: NSScreen.main!.frame.width / 2, height: NSScreen.main!.frame.height / 2) - return Self.centerRect(ofSize: size, inRect: NSScreen.main!.frame) + let size = NSSize(width: screen.frame.width / 2, height: screen.frame.height / 2) + return Self.centerRect(ofSize: size, inRect: screen.frame) case .centerMedium: - let size = NSSize(width: NSScreen.main!.frame.width * 0.7, height: NSScreen.main!.frame.height * 0.7) - return Self.centerRect(ofSize: size, inRect: NSScreen.main!.frame) + let size = NSSize(width: screen.frame.width * 0.7, height: screen.frame.height * 0.7) + return Self.centerRect(ofSize: size, inRect: screen.frame) case .centerLarge: - let size = NSSize(width: NSScreen.main!.frame.width * 0.85, height: NSScreen.main!.frame.height * 0.85) - return Self.centerRect(ofSize: size, inRect: NSScreen.main!.frame) + let size = NSSize(width: screen.frame.width * 0.85, height: screen.frame.height * 0.85) + return Self.centerRect(ofSize: size, inRect: screen.frame) case .fullScreen: - return NSScreen.main!.frame + return screen.frame } } private static func centerRect(ofSize size: NSSize, inRect rect: NSRect) -> NSRect { - return NSRect(origin: NSPoint(x: (rect.width - size.width) / 2, y: (rect.height - size.height) / 2), size: size) + return NSRect(origin: NSPoint(x: (rect.width - size.width) / 2 + rect.minX, y: (rect.height - size.height) / 2 + rect.minY), size: size) } var title: String { diff --git a/Yippy/Sources/Models/State.swift b/Yippy/Sources/Models/State.swift index b35ff1c..478e213 100644 --- a/Yippy/Sources/Models/State.swift +++ b/Yippy/Sources/Models/State.swift @@ -24,6 +24,8 @@ class State { var panelPosition: BehaviorRelay + var currentScreen: BehaviorRelay + var previewHistoryItem: BehaviorRelay var launchAtLogin: BehaviorRelay @@ -51,6 +53,7 @@ class State { self.launchAtLogin = BehaviorRelay(value: LoginServiceKit.isExistLoginItems()) self.showsRichText = BehaviorRelay(value: settings.showsRichText) self.pastesRichText = BehaviorRelay(value: settings.pastesRichText) + self.currentScreen = BehaviorRelay(value: Self.getCurrentScreen(forMouseLocation: NSEvent.mouseLocation)) self.disposeBag = disposeBag // Setup history @@ -65,8 +68,8 @@ class State { // Setup pasteboard monitor self.pasteboardMonitor = PasteboardMonitor(pasteboard: NSPasteboard.general, changeCount: settings.pasteboardChangeCount, delegate: self.history) - // Self.monitorPastesRichText(state: self) + Self.monitorMousePosition(state: self) } // MARK: - Constructor Helpers @@ -84,4 +87,22 @@ class State { HistoryItem.pastesRichText = $0 }).disposed(by: state.disposeBag) } + + static func monitorMousePosition(state: State) { + Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { (_) in + let currentScreen = getCurrentScreen(forMouseLocation: NSEvent.mouseLocation) + if currentScreen != state.currentScreen.value { + state.currentScreen.accept(currentScreen) + } + } + } + + static func getCurrentScreen(forMouseLocation location: NSPoint) -> NSScreen { + for screen in NSScreen.screens { + if screen.frame.contains(location) { + return screen + } + } + return NSScreen.main! + } } diff --git a/Yippy/Sources/Windows/Yippy/YippyWindowController.swift b/Yippy/Sources/Windows/Yippy/YippyWindowController.swift index 5af56d8..7b01849 100644 --- a/Yippy/Sources/Windows/Yippy/YippyWindowController.swift +++ b/Yippy/Sources/Windows/Yippy/YippyWindowController.swift @@ -44,11 +44,10 @@ class YippyWindowController: NSWindowController { }) } - func subscribePositionTo(position: BehaviorRelay) -> Disposable { - return position - .subscribe(onNext: { - [] in - self.window?.setFrame($0.frame, display: true) - }) + func subscribeFrameTo(position: Observable, screen: Observable) -> Disposable { + Observable.combineLatest(position, screen).subscribe(onNext: { + (position, screen) in + self.window?.setFrame(position.getFrame(forScreen: screen), display: true) + }) } } diff --git a/YippyUITests/YippyUITests.swift b/YippyUITests/YippyUITests.swift index d59ea96..7236e66 100644 --- a/YippyUITests/YippyUITests.swift +++ b/YippyUITests/YippyUITests.swift @@ -106,7 +106,7 @@ class YippyUITests: XCTestCase { XCTAssertTrue(app.yippyWindow.exists) // Check window location is .right - XCTAssertEqual(app.yippyWindow.frame.midX, PanelPosition.right.frame.midX) + XCTAssertEqual(app.yippyWindow.frame.midX, PanelPosition.right.getFrame(forScreen: NSScreen.main!).midX) // Change to position left app.statusItemButton.click() @@ -114,7 +114,7 @@ class YippyUITests: XCTestCase { app.positionLeftButton.click() // Check window location is .left - XCTAssertEqual(app.yippyWindow.frame.midX, PanelPosition.left.frame.midX) + XCTAssertEqual(app.yippyWindow.frame.midX, PanelPosition.left.getFrame(forScreen: NSScreen.main!).midX) // Change to position bottom app.statusItemButton.click() @@ -139,7 +139,7 @@ class YippyUITests: XCTestCase { app.positionRightButton.click() // Check window location is .right - XCTAssertEqual(app.yippyWindow.frame.midX, PanelPosition.right.frame.midX) + XCTAssertEqual(app.yippyWindow.frame.midX, PanelPosition.right.getFrame(forScreen: NSScreen.main!).midX) } func testEmptyYippyHistory() {