diff --git a/Cartfile.resolved b/Cartfile.resolved index 4eea97a..5faa16a 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,4 +1,4 @@ -github "Clipy/LoginServiceKit" "v1.0.0" -github "Clipy/Magnet" "v2.0.0" -github "sparkle-project/Sparkle" "1.15.1" -github "Clipy/KeyHolder" "v2.0.0" +github "Clipy/KeyHolder" "v2.1.0" +github "Clipy/LoginServiceKit" "v1.2.0" +github "Clipy/Magnet" "v2.1.0" +github "sparkle-project/Sparkle" "1.19.0" diff --git a/Fuwari.xcodeproj/project.pbxproj b/Fuwari.xcodeproj/project.pbxproj index 18b0618..8198fc7 100644 --- a/Fuwari.xcodeproj/project.pbxproj +++ b/Fuwari.xcodeproj/project.pbxproj @@ -31,6 +31,7 @@ BEC844FB1DED859300A4A57A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BEC844FA1DED859300A4A57A /* Assets.xcassets */; }; BEC844FE1DED859300A4A57A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BEC844FC1DED859300A4A57A /* Main.storyboard */; }; BEC8450C1DF59ECB00A4A57A /* CaptureGuideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEC8450B1DF59ECB00A4A57A /* CaptureGuideView.swift */; }; + BECCBA7F2096F95000D13978 /* StateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BECCBA7E2096F95000D13978 /* StateManager.swift */; }; BEF57FE51DFD46AE006595B6 /* PreferencesWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEF57FE41DFD46AE006595B6 /* PreferencesWindowController.swift */; }; BEF57FED1DFD53D3006595B6 /* UpdatePreferenceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEF57FEC1DFD53D3006595B6 /* UpdatePreferenceViewController.swift */; }; BEF57FEF1DFD58D4006595B6 /* NSColor+Fuwari.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEF57FEE1DFD58D4006595B6 /* NSColor+Fuwari.swift */; }; @@ -69,6 +70,7 @@ BEC844FD1DED859300A4A57A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; BEC844FF1DED859300A4A57A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; BEC8450B1DF59ECB00A4A57A /* CaptureGuideView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CaptureGuideView.swift; sourceTree = ""; }; + BECCBA7E2096F95000D13978 /* StateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateManager.swift; sourceTree = ""; }; BEF57FE41DFD46AE006595B6 /* PreferencesWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesWindowController.swift; sourceTree = ""; }; BEF57FEC1DFD53D3006595B6 /* UpdatePreferenceViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdatePreferenceViewController.swift; sourceTree = ""; }; BEF57FEE1DFD58D4006595B6 /* NSColor+Fuwari.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSColor+Fuwari.swift"; sourceTree = ""; }; @@ -135,6 +137,7 @@ BEC8450B1DF59ECB00A4A57A /* CaptureGuideView.swift */, BE7E82581DF72E7A00F3F2F8 /* FullScreenWindow.swift */, BE7E825A1DF74A2D00F3F2F8 /* FloatWindow.swift */, + BECCBA7E2096F95000D13978 /* StateManager.swift */, BE5BE7A31E065D7800820201 /* HotKeyManager.swift */, BE9777F71E100D1F00E5934F /* MenuManager.swift */, BE6D5E741E12CF5E005492B2 /* AboutWindowController.swift */, @@ -220,12 +223,13 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0810; - LastUpgradeCheck = 0810; + LastUpgradeCheck = 0820; ORGANIZATIONNAME = AppKnop; TargetAttributes = { BEC844F21DED859300A4A57A = { CreatedOnToolsVersion = 8.1; DevelopmentTeam = QZJ74E9669; + LastSwiftMigration = 0930; ProvisioningStyle = Automatic; }; }; @@ -317,6 +321,7 @@ BE6D5E761E12CF5E005492B2 /* AboutWindowController.swift in Sources */, BE5BE7A41E065D7800820201 /* HotKeyManager.swift in Sources */, BE5896A11DFCED26007CE7AC /* LocalizedString.swift in Sources */, + BECCBA7F2096F95000D13978 /* StateManager.swift in Sources */, BEC8450C1DF59ECB00A4A57A /* CaptureGuideView.swift in Sources */, BEC844F71DED859300A4A57A /* AppDelegate.swift in Sources */, BE9777ED1E0E613700E5934F /* UserDefaults+ArchiveData.swift in Sources */, @@ -405,6 +410,7 @@ CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -455,6 +461,7 @@ CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -494,7 +501,8 @@ MACOSX_DEPLOYMENT_TARGET = 10.11; PRODUCT_BUNDLE_IDENTIFIER = com.appknop.Fuwari; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 3.0; + SWIFT_SWIFT3_OBJC_INFERENCE = On; + SWIFT_VERSION = 4.0; }; name = Debug; }; @@ -514,7 +522,8 @@ MACOSX_DEPLOYMENT_TARGET = 10.11; PRODUCT_BUNDLE_IDENTIFIER = com.appknop.Fuwari; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 3.0; + SWIFT_SWIFT3_OBJC_INFERENCE = On; + SWIFT_VERSION = 4.0; }; name = Release; }; diff --git a/Fuwari.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Fuwari.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Fuwari.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Fuwari/AboutWindowController.swift b/Fuwari/AboutWindowController.swift index 4f6b0a9..64f5e19 100644 --- a/Fuwari/AboutWindowController.swift +++ b/Fuwari/AboutWindowController.swift @@ -10,7 +10,7 @@ import Cocoa class AboutWindowController: NSWindowController { - static let shared = AboutWindowController(windowNibName: "AboutWindowController") + static let shared = AboutWindowController(windowNibName: NSNib.Name(rawValue: "AboutWindowController")) @IBOutlet private weak var versionTextField: NSTextField! { didSet { diff --git a/Fuwari/AppDelegate.swift b/Fuwari/AppDelegate.swift index d9e2af6..d11d450 100644 --- a/Fuwari/AppDelegate.swift +++ b/Fuwari/AppDelegate.swift @@ -16,8 +16,8 @@ import Crashlytics @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { - var eventMonitor: Any? - let defaults = UserDefaults.standard + private var eventMonitor: Any? + private let defaults = UserDefaults.standard func applicationDidFinishLaunching(_ aNotification: Notification) { Fabric.with([Answers.self, Crashlytics.self]) @@ -27,6 +27,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { promptToAddLoginItems() } + HotKeyManager.shared.configure() MenuManager.shared.configure() } @@ -47,7 +48,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { @objc func capture() { NSApp.activate(ignoringOtherApps: true) NotificationCenter.default.post(name: NSNotification.Name(rawValue: Constants.Notification.capture), object: nil) - eventMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.mouseMoved, .leftMouseUp], handler: { + eventMonitor = NSEvent.addGlobalMonitorForEvents(matching: [NSEvent.EventTypeMask.mouseMoved, NSEvent.EventTypeMask.leftMouseUp], handler: { (event: NSEvent) in switch event.type { case .mouseMoved: @@ -67,7 +68,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { NSApp.terminate(nil) } - fileprivate func promptToAddLoginItems() { + private func promptToAddLoginItems() { let alert = NSAlert() alert.messageText = LocalizedString.LaunchFuwari.value alert.informativeText = LocalizedString.LaunchSettingInfo.value @@ -77,18 +78,18 @@ class AppDelegate: NSObject, NSApplicationDelegate { NSApp.activate(ignoringOtherApps: true) // Launch on system startup - if alert.runModal() == NSAlertFirstButtonReturn { + if alert.runModal() == NSApplication.ModalResponse.alertFirstButtonReturn { defaults.set(true, forKey: Constants.UserDefaults.loginItem) toggleLoginItemState() } // Do not show this message again - if alert.suppressionButton?.state == NSOnState { + if alert.suppressionButton?.state == .on { defaults.set(true, forKey: Constants.UserDefaults.suppressAlertForLoginItem) } defaults.synchronize() } - fileprivate func toggleAddingToLoginItems(_ enable: Bool) { + private func toggleAddingToLoginItems(_ enable: Bool) { let appPath = Bundle.main.bundlePath LoginServiceKit.removeLoginItems(at: appPath) if enable { @@ -96,7 +97,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } - fileprivate func toggleLoginItemState() { + private func toggleLoginItemState() { let isInLoginItems = defaults.bool(forKey: Constants.UserDefaults.loginItem) toggleAddingToLoginItems(isInLoginItems) } diff --git a/Fuwari/Base.lproj/Localizable.strings b/Fuwari/Base.lproj/Localizable.strings index 04921c5..18d71ea 100644 --- a/Fuwari/Base.lproj/Localizable.strings +++ b/Fuwari/Base.lproj/Localizable.strings @@ -31,3 +31,13 @@ "Capture" = "Capture"; "About" = "About Fuwari..."; + +"Save" = "Save"; + +"Copy" = "Copy"; + +"Close" = "Close"; + +"Zoom In" = "Zoom In"; + +"Zoom Out" = "Zoom Out"; diff --git a/Fuwari/CaptureGuideView.swift b/Fuwari/CaptureGuideView.swift index e116b54..bc5976e 100644 --- a/Fuwari/CaptureGuideView.swift +++ b/Fuwari/CaptureGuideView.swift @@ -55,27 +55,27 @@ class CaptureGuideView: NSView { if startPoint != .zero { NSColor(red: 0, green: 0, blue: 0, alpha: 0.25).set() guideWindowRect = NSRect(x: floor(fmin(startPoint.x, cursorPoint.x)), y: floor(fmin(startPoint.y, cursorPoint.y)), width: floor(fabs(cursorPoint.x - startPoint.x)), height: floor(fabs(cursorPoint.y - startPoint.y))) - NSRectFill(guideWindowRect) + guideWindowRect.fill() NSColor.white.set() - NSFrameRectWithWidth(guideWindowRect, cursorGuideWidth) + guideWindowRect.frame(withWidth: cursorGuideWidth) } } private func drawCursor() { NSColor.darkGray.set() let cursorRectWidth = NSRect(x: cursorPoint.x - cursorSize / 2, y: cursorPoint.y, width: cursorSize, height: 1) - NSRectFill(cursorRectWidth) + cursorRectWidth.fill() let cursorRectHeight = NSRect(x: cursorPoint.x, y: cursorPoint.y - cursorSize / 2, width: 1, height: cursorSize) - NSRectFill(cursorRectHeight) + cursorRectHeight.fill() NSColor(red: 0, green: 0, blue: 0, alpha: 0.25).set() let cursorCenter = NSRect(x: cursorPoint.x - cursorSize / 4 + cursorGuideWidth / 2, y: cursorPoint.y - cursorSize / 4 + cursorGuideWidth / 2, width: cursorSize / 2, height: cursorSize / 2) let path = NSBezierPath(ovalIn: cursorCenter) path.fill() - (Int(cursorPoint.x).description as NSString).draw(at: NSPoint(x: cursorPoint.x + cursorSize / 2, y: cursorPoint.y - cursorSize / 2), withAttributes: [NSFontAttributeName : cursorFont, NSShadowAttributeName : coordinateLabelShadow]) - (Int(frame.height - cursorPoint.y).description as NSString).draw(at: NSPoint(x: cursorPoint.x + cursorSize / 2, y: cursorPoint.y - cursorSize), withAttributes: [NSFontAttributeName : cursorFont, NSShadowAttributeName : coordinateLabelShadow]) + (Int(cursorPoint.x).description as NSString).draw(at: NSPoint(x: cursorPoint.x + cursorSize / 2, y: cursorPoint.y - cursorSize / 2), withAttributes: [.font : cursorFont, .shadow : coordinateLabelShadow]) + (Int(frame.height - cursorPoint.y).description as NSString).draw(at: NSPoint(x: cursorPoint.x + cursorSize / 2, y: cursorPoint.y - cursorSize), withAttributes: [.font : cursorFont, .shadow : coordinateLabelShadow]) } func reset() { diff --git a/Fuwari/FloatWindow.swift b/Fuwari/FloatWindow.swift index 3ad4358..9cf12fd 100644 --- a/Fuwari/FloatWindow.swift +++ b/Fuwari/FloatWindow.swift @@ -7,8 +7,14 @@ // import Cocoa +import Magnet import Carbon +protocol FloatDelegate { + func save(floatWindow: FloatWindow, image: CGImage) + func close(floatWindow: FloatWindow) +} + class FloatWindow: NSWindow { override var canBecomeKey: Bool { return true } @@ -16,65 +22,73 @@ class FloatWindow: NSWindow { var floatDelegate: FloatDelegate? - init(contentRect: NSRect, styleMask style: NSWindowStyleMask = .borderless, backing bufferingType: NSBackingStoreType = .buffered, defer flag: Bool = false, image: CGImage) { + private var originalRect = NSRect() + private var popUpLabel = NSTextField() + private var windowScale = CGFloat(1.0) + private let windowScaleInterval = CGFloat(0.25) + private let minWindowScale = CGFloat(0.25) + private let maxWindowScale = CGFloat(2.5) + + init(contentRect: NSRect, styleMask style: NSWindow.StyleMask = .borderless, backing bufferingType: NSWindow.BackingStoreType = .buffered, defer flag: Bool = false, image: CGImage) { super.init(contentRect: contentRect, styleMask: style, backing: bufferingType, defer: flag) - level = Int(CGWindowLevelForKey(.floatingWindow)) + originalRect = contentRect + level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.floatingWindow))) isMovableByWindowBackground = true hasShadow = true contentView?.wantsLayer = true contentView?.layer?.contents = image - fade(isIn: true, completion: nil) + popUpLabel = NSTextField(frame: NSRect(x: 10, y: 10, width: 80, height: 26)) + popUpLabel.textColor = .white + popUpLabel.font = NSFont.boldSystemFont(ofSize: 20) + popUpLabel.alignment = .center + popUpLabel.drawsBackground = true + popUpLabel.backgroundColor = NSColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.4) + popUpLabel.wantsLayer = true + popUpLabel.layer?.cornerRadius = 10.0 + popUpLabel.isBordered = false + popUpLabel.isEditable = false + popUpLabel.isSelectable = false + popUpLabel.alphaValue = 0.0 + contentView?.addSubview(popUpLabel) + + menu = NSMenu() + menu?.addItem(NSMenuItem(title: LocalizedString.Save.value, action: #selector(saveImage), keyEquivalent: "s")) + menu?.addItem(NSMenuItem(title: LocalizedString.Copy.value, action: #selector(copyImage), keyEquivalent: "c")) + menu?.addItem(NSMenuItem.separator()) + menu?.addItem(NSMenuItem(title: LocalizedString.ZoomIn.value, action: #selector(zoomInWindow), keyEquivalent: "+")) + menu?.addItem(NSMenuItem(title: LocalizedString.ZoomOut.value, action: #selector(zoomOutWindow), keyEquivalent: "-")) + menu?.addItem(NSMenuItem.separator()) + menu?.addItem(NSMenuItem(title: LocalizedString.Close.value, action: #selector(closeWindow), keyEquivalent: "w")) + + fadeWindow(isIn: true) } override func keyDown(with event: NSEvent) { - super.keyDown(with: event) + if StateManager.shared.isCapturing { + return + } - if event.modifierFlags.rawValue & NSEventModifierFlags.command.rawValue != 0 { - switch event.keyCode { - case UInt16(kVK_ANSI_S): - let saveLabel = NSTextField(frame: NSRect(x: 10, y: 10, width: 80, height: 26)) - saveLabel.stringValue = "Save" - saveLabel.textColor = .white - saveLabel.font = NSFont.boldSystemFont(ofSize: 20) - saveLabel.alignment = .center - saveLabel.drawsBackground = true - saveLabel.backgroundColor = NSColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.4) - saveLabel.wantsLayer = true - saveLabel.layer?.cornerRadius = 10.0 - saveLabel.isBordered = false - saveLabel.isEditable = false - saveLabel.isSelectable = false - contentView?.addSubview(saveLabel) - - DispatchQueue.main.asyncAfter(deadline: .now()) { - if let image = self.contentView?.layer?.contents { - saveLabel.removeFromSuperview() - self.floatDelegate?.save(floatWindow: self, image: image as! CGImage) - } - } - case UInt16(kVK_ANSI_W): - fade(isIn: false) { - self.floatDelegate?.close(floatWindow: self) - } - case UInt16(kVK_ANSI_C): - DispatchQueue.main.asyncAfter(deadline: .now()) { - if let image = self.contentView?.layer?.contents { - let cgImage = image as! CGImage - let size = CGSize(width: cgImage.width, height: cgImage.height) - let nsImage = NSImage(cgImage: cgImage, size: size) - NSPasteboard.general().clearContents() - NSPasteboard.general().writeObjects([nsImage]) - } - } + let combo = KeyCombo(keyCode: Int(event.keyCode), cocoaModifiers: event.modifierFlags) + if event.modifierFlags.rawValue & NSEvent.ModifierFlags.command.rawValue != 0 { + guard let char = combo?.characters.first else { return } + switch char { + case "S": // ⌘S + saveImage() + case "C": // ⌘C + copyImage() + case "=": // ⌘+ + zoomInWindow() + case "-": // ⌘- + zoomOutWindow() + case "W": // ⌘W + closeWindow() default: break } } else if event.keyCode == UInt16(kVK_Escape) { - fade(isIn: false) { - self.floatDelegate?.close(floatWindow: self) - } + closeWindow() } } @@ -86,18 +100,78 @@ class FloatWindow: NSWindow { alphaValue = 1.0 } - private func fade(isIn: Bool, completion: (() -> Void)?) { - alphaValue = isIn ? 0.0 : 1.0 + override func rightMouseDown(with event: NSEvent) { + if let menu = menu, let contentView = contentView { + NSMenu.popUpContextMenu(menu, with: event, for: contentView) + } + } + + @objc private func saveImage() { + DispatchQueue.main.asyncAfter(deadline: .now()) { + if let image = self.contentView?.layer?.contents { + self.showPopUp(text: "Save") + self.floatDelegate?.save(floatWindow: self, image: image as! CGImage) + } + } + } + + @objc private func copyImage() { + DispatchQueue.main.asyncAfter(deadline: .now()) { + if let image = self.contentView?.layer?.contents { + let cgImage = image as! CGImage + let size = CGSize(width: cgImage.width, height: cgImage.height) + let nsImage = NSImage(cgImage: cgImage, size: size) + NSPasteboard.general.clearContents() + NSPasteboard.general.writeObjects([nsImage]) + self.showPopUp(text: "Copy") + } + } + } + + @objc private func zoomInWindow() { + if windowScale < maxWindowScale { + windowScale += windowScaleInterval + setFrame(NSRect(x: frame.origin.x - (originalRect.width / 2 * windowScaleInterval), y: frame.origin.y - (originalRect.height / 2 * windowScaleInterval), width: originalRect.width * windowScale, height: originalRect.height * windowScale), display: true) + } + + showPopUp(text: "\(Int(windowScale * 100))%") + } + + @objc private func zoomOutWindow() { + if windowScale > minWindowScale { + windowScale -= windowScaleInterval + setFrame(NSRect(x: frame.origin.x + (originalRect.width / 2 * windowScaleInterval), y: frame.origin.y + (originalRect.height / 2 * windowScaleInterval), width: originalRect.width * windowScale, height: originalRect.height * windowScale), display: true) + } + + showPopUp(text: "\(Int(windowScale * 100))%") + } + + @objc private func closeWindow() { + floatDelegate?.close(floatWindow: self) + } + + private func showPopUp(text: String, duration: Double = 0.3) { + popUpLabel.stringValue = text + + NSAnimationContext.runAnimationGroup({ context in + context.duration = duration + context.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) + popUpLabel.animator().alphaValue = 1.0 + }) { + NSAnimationContext.runAnimationGroup({ context in + context.duration = duration + context.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn) + self.popUpLabel.animator().alphaValue = 0.0 + }) + } + } + + func fadeWindow(isIn: Bool, completion: (() -> Void)? = nil) { makeKeyAndOrderFront(self) NSAnimationContext.beginGrouping() - NSAnimationContext.current().completionHandler = completion - NSAnimationContext.current().duration = 0.2 + NSAnimationContext.current.completionHandler = completion + NSAnimationContext.current.duration = 0.2 animator().alphaValue = isIn ? 1.0 : 0.0 NSAnimationContext.endGrouping() } } - -protocol FloatDelegate { - func save(floatWindow: FloatWindow, image: CGImage) - func close(floatWindow: FloatWindow) -} diff --git a/Fuwari/FullScreenWindow.swift b/Fuwari/FullScreenWindow.swift index f4b4797..fe21492 100644 --- a/Fuwari/FullScreenWindow.swift +++ b/Fuwari/FullScreenWindow.swift @@ -9,6 +9,11 @@ import Cocoa import Carbon +protocol CaptureDelegate { + func didCanceled() + func didCaptured(rect: NSRect, image: CGImage) +} + class FullScreenWindow: NSWindow { var captureDelegate: CaptureDelegate? @@ -19,10 +24,10 @@ class FullScreenWindow: NSWindow { }() private var mouseLocation: NSPoint { - return NSPoint(x: NSEvent.mouseLocation().x - frame.origin.x, y: NSEvent.mouseLocation().y - frame.origin.y) + return NSPoint(x: NSEvent.mouseLocation.x - frame.origin.x, y: NSEvent.mouseLocation.y - frame.origin.y) } - override init(contentRect: NSRect, styleMask style: NSWindowStyleMask, backing bufferingType: NSBackingStoreType, defer flag: Bool) { + override init(contentRect: NSRect, styleMask style: NSWindow.StyleMask, backing bufferingType: NSWindow.BackingStoreType, defer flag: Bool) { super.init(contentRect: contentRect, styleMask: .borderless, backing: .buffered, defer: false) isReleasedWhenClosed = true @@ -32,7 +37,7 @@ class FullScreenWindow: NSWindow { hasShadow = false ignoresMouseEvents = false acceptsMouseMovedEvents = true - level = Int(CGWindowLevelForKey(.assistiveTechHighWindow)) + level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.assistiveTechHighWindow))) makeKeyAndOrderFront(self) contentView = captureGuideView @@ -40,7 +45,9 @@ class FullScreenWindow: NSWindow { NSEvent.addLocalMonitorForEvents(matching: .keyDown) { (event: NSEvent) -> NSEvent? in if event.keyCode == UInt16(kVK_Escape) { - self.captureDelegate?.didCanceled() + if StateManager.shared.isCapturing { + self.captureDelegate?.didCanceled() + } } return event } @@ -83,8 +90,3 @@ class FullScreenWindow: NSWindow { captureDelegate?.didCaptured(rect: rect.offsetBy(dx: frame.origin.x, dy: frame.origin.y), image: cgImage) } } - -protocol CaptureDelegate { - func didCanceled() - func didCaptured(rect: NSRect, image: CGImage) -} diff --git a/Fuwari/HotKeyManager.swift b/Fuwari/HotKeyManager.swift index 8e5dfac..0107658 100644 --- a/Fuwari/HotKeyManager.swift +++ b/Fuwari/HotKeyManager.swift @@ -27,9 +27,7 @@ final class HotKeyManager: NSObject { }() fileprivate(set) var captureHotKey: HotKey? - override init() { - super.init() - + func configure() { registerHotKey(keyCombo: captureKeyCombo) } } diff --git a/Fuwari/Info.plist b/Fuwari/Info.plist index 3f49489..048a929 100644 --- a/Fuwari/Info.plist +++ b/Fuwari/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.3 + 0.4 CFBundleVersion - 0.3 + 0.40 Fabric APIKey diff --git a/Fuwari/LocalizedString.swift b/Fuwari/LocalizedString.swift index 4b8a7b3..bb0c656 100644 --- a/Fuwari/LocalizedString.swift +++ b/Fuwari/LocalizedString.swift @@ -18,6 +18,11 @@ enum LocalizedString: String { case DontLaunch = "Don't Launch" case Capture = "Capture" case About = "About" + case Save = "Save" + case Copy = "Copy" + case Close = "Close" + case ZoomIn = "ZoomIn" + case ZoomOut = "ZoomOut" case TabGeneral = "General" case TabMenu = "Menu" diff --git a/Fuwari/MenuManager.swift b/Fuwari/MenuManager.swift index 95be665..1eae679 100644 --- a/Fuwari/MenuManager.swift +++ b/Fuwari/MenuManager.swift @@ -12,13 +12,13 @@ import Magnet class MenuManager: NSObject { static let shared = MenuManager() - let statusItem = NSStatusBar.system().statusItem(withLength: NSSquareStatusItemLength) + let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength) private var captureItem = NSMenuItem() func configure() { if let button = statusItem.button { - button.image = NSImage(named: "MenuIcon") + button.image = NSImage(named: NSImage.Name(rawValue: "MenuIcon")) } captureItem = NSMenuItem(title: LocalizedString.Capture.value, action: #selector(AppDelegate.capture), keyEquivalent: HotKeyManager.shared.captureKeyCombo.characters.lowercased()) diff --git a/Fuwari/PreferencesWindowController.swift b/Fuwari/PreferencesWindowController.swift index a3acaaa..27f5d91 100644 --- a/Fuwari/PreferencesWindowController.swift +++ b/Fuwari/PreferencesWindowController.swift @@ -10,7 +10,7 @@ import Cocoa class PreferencesWindowController: NSWindowController { - static let shared = PreferencesWindowController(windowNibName: "PreferencesWindowController") + static let shared = PreferencesWindowController(windowNibName: NSNib.Name(rawValue: "PreferencesWindowController")) @IBOutlet fileprivate weak var toolBar: NSView! @IBOutlet fileprivate weak var generalImageView: NSImageView! @@ -24,9 +24,9 @@ class PreferencesWindowController: NSWindowController { @IBOutlet fileprivate weak var shortcutButton: NSButton! fileprivate let defaults = UserDefaults.standard - fileprivate let viewController = [NSViewController(nibName: "GeneralPreferenceViewController", bundle: nil)!, - UpdatePreferenceViewController(nibName: "UpdatePreferenceViewController", bundle: nil)!, - ShortcutsPreferenceViewController(nibName: "ShortcutsPreferenceViewController", bundle: nil)!] + fileprivate let viewController = [NSViewController(nibName: NSNib.Name(rawValue: "GeneralPreferenceViewController"), bundle: nil), + UpdatePreferenceViewController(nibName: NSNib.Name(rawValue: "UpdatePreferenceViewController"), bundle: nil), + ShortcutsPreferenceViewController(nibName: NSNib.Name(rawValue: "ShortcutsPreferenceViewController"), bundle: nil)] override func windowDidLoad() { super.windowDidLoad() @@ -64,9 +64,9 @@ extension PreferencesWindowController: NSWindowDelegate { // MARK: - Layout fileprivate extension PreferencesWindowController { private func resetImages() { - generalImageView.image = NSImage(named: Constants.ImageName.generalOff) - updatesImageView.image = NSImage(named: Constants.ImageName.updatesOff) - shortcutImageView.image = NSImage(named: Constants.ImageName.shortcutOff) + generalImageView.image = NSImage(named: NSImage.Name(rawValue: Constants.ImageName.generalOff)) + updatesImageView.image = NSImage(named: NSImage.Name(rawValue: Constants.ImageName.updatesOff)) + shortcutImageView.image = NSImage(named: NSImage.Name(rawValue: Constants.ImageName.shortcutOff)) generalTextField.textColor = .tabTitle updatesTextField.textColor = .tabTitle @@ -78,13 +78,13 @@ fileprivate extension PreferencesWindowController { switch index { case 0: - generalImageView.image = NSImage(named: Constants.ImageName.generalOn) + generalImageView.image = NSImage(named: NSImage.Name(rawValue: Constants.ImageName.generalOn)) generalTextField.textColor = .main case 1: - updatesImageView.image = NSImage(named: Constants.ImageName.updatesOn) + updatesImageView.image = NSImage(named: NSImage.Name(rawValue: Constants.ImageName.updatesOn)) updatesTextField.textColor = .main case 2: - shortcutImageView.image = NSImage(named: Constants.ImageName.shortcutOn) + shortcutImageView.image = NSImage(named: NSImage.Name(rawValue: Constants.ImageName.shortcutOn)) shortcutTextField.textColor = .main default: break } diff --git a/Fuwari/StateManager.swift b/Fuwari/StateManager.swift new file mode 100644 index 0000000..86534d1 --- /dev/null +++ b/Fuwari/StateManager.swift @@ -0,0 +1,14 @@ +// +// StateManager.swift +// Fuwari +// +// Created by Kengo Yokoyama on 2018/04/30. +// Copyright © 2018年 AppKnop. All rights reserved. +// + +import Cocoa + +class StateManager: NSObject { + static let shared = StateManager() + var isCapturing = false +} diff --git a/Fuwari/ViewController.swift b/Fuwari/ViewController.swift index 5c0cc6d..818fb2c 100644 --- a/Fuwari/ViewController.swift +++ b/Fuwari/ViewController.swift @@ -7,16 +7,18 @@ // import Cocoa +import Quartz class ViewController: NSViewController { - fileprivate var windowControllers = [NSWindowController]() - fileprivate var fullScreenWindows = [FullScreenWindow]() + private var windowControllers = [NSWindowController]() + private var fullScreenWindows = [FullScreenWindow]() + private var isCancelled = false override func viewDidLoad() { super.viewDidLoad() - NSScreen.screens()!.forEach { + NSScreen.screens.forEach { let fullScreenWindow = FullScreenWindow(contentRect: $0.frame, styleMask: .borderless, backing: .buffered, defer: false) fullScreenWindow.captureDelegate = self fullScreenWindows.append(fullScreenWindow) @@ -29,7 +31,11 @@ class ViewController: NSViewController { NotificationCenter.default.addObserver(self, selector: #selector(startCapture), name: Notification.Name(rawValue: Constants.Notification.capture), object: nil) } - fileprivate func createFloatWindow(rect: NSRect, image: CGImage) { + deinit { + NotificationCenter.default.removeObserver(self, name: Notification.Name(rawValue: Constants.Notification.capture), object: nil) + } + + private func createFloatWindow(rect: NSRect, image: CGImage) { let floatWindow = FloatWindow(contentRect: rect, image: image) floatWindow.floatDelegate = self let floatWindowController = NSWindowController(window: floatWindow) @@ -39,7 +45,7 @@ class ViewController: NSViewController { @objc private func startCapture() { NSCursor.hide() - + StateManager.shared.isCapturing = true fullScreenWindows.forEach { $0.startCapture() } } } @@ -48,18 +54,29 @@ extension ViewController: CaptureDelegate { func didCaptured(rect: NSRect, image: CGImage) { createFloatWindow(rect: rect, image: image) NSCursor.unhide() + StateManager.shared.isCapturing = false fullScreenWindows.forEach { $0.orderOut(nil) } + isCancelled = false } func didCanceled() { NSCursor.unhide() + StateManager.shared.isCapturing = false + isCancelled = true fullScreenWindows.forEach { $0.orderOut(nil) } } } extension ViewController: FloatDelegate { func close(floatWindow: FloatWindow) { - windowControllers.filter({ $0.window === floatWindow }).first?.close() + if !isCancelled { + if windowControllers.filter({ $0.window === floatWindow }).first != nil { + floatWindow.fadeWindow(isIn: false) { + floatWindow.close() + } + } + } + isCancelled = false } func save(floatWindow: FloatWindow, image: CGImage) { @@ -69,19 +86,17 @@ extension ViewController: FloatDelegate { let savePanel = NSSavePanel() savePanel.canCreateDirectories = true savePanel.showsTagField = false - savePanel.nameFieldStringValue = "screenshot-\(formatter.string(from: Date())).png" - savePanel.level = Int(CGWindowLevelForKey(.modalPanelWindow)) - savePanel.begin { (result) in - if result == NSFileHandlingPanelOKButton { - guard let url = savePanel.url else { return } - - let bitmapRep = NSBitmapImageRep(cgImage: image) - let data = bitmapRep.representation(using: .PNG, properties: [:]) - do { - try data?.write(to: url, options: .atomicWrite) - } catch { - print(error.localizedDescription) - } + savePanel.nameFieldStringValue = "screenshot-\(formatter.string(from: Date()))" + savePanel.level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.modalPanelWindow))) + let saveOptions = IKSaveOptions(imageProperties: [:], imageUTType: kUTTypePNG as String?) + saveOptions?.addAccessoryView(to: savePanel) + + let result = savePanel.runModal() + if result == .OK { + if let url = savePanel.url as CFURL?, let type = saveOptions?.imageUTType as CFString? { + guard let destination = CGImageDestinationCreateWithURL(url, type, 1, nil) else { return } + CGImageDestinationAddImage(destination, image, saveOptions!.imageProperties! as CFDictionary) + CGImageDestinationFinalize(destination) } } } diff --git a/Fuwari/ja.lproj/Localizable.strings b/Fuwari/ja.lproj/Localizable.strings index bbdb957..21a2a1b 100644 --- a/Fuwari/ja.lproj/Localizable.strings +++ b/Fuwari/ja.lproj/Localizable.strings @@ -31,3 +31,13 @@ "Capture" = "キャプチャ"; "About" = "Fuwariについて..."; + +"Save" = "保存"; + +"Copy" = "コピー"; + +"Close" = "閉じる"; + +"Zoom In" = "拡大"; + +"Zoom Out" = "縮小"; diff --git a/README.md b/README.md index 133ed01..b5c8c78 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@ Floating screenshot like a sticky.

## Requirement -- Swift3+ -- Xcode8+ +- Swift4+ +- Xcode9.3+ ## Contributing 1. Fork it ( https://github.com/kentya6/Fuwari/fork ) @@ -24,7 +24,7 @@ Floating screenshot like a sticky. The MIT License (MIT) -Copyright (c) 2016 Kengo YOKOYAMA +Copyright (c) 2018 Kengo YOKOYAMA ## Author