Skip to content

Show and switch between multiple images #29

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 7 additions & 24 deletions LayerX/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
var isLockIconHiddenWhileLocked = false {
didSet { viewController.lockIconImageView.isHidden = window.isMovable || isLockIconHiddenWhileLocked }
}
var isSizeHidden = false {
didSet { viewController.sizeTextField.isHidden = isSizeHidden }
}

func applicationDidFinishLaunching(_ aNotification: Notification) {
if let window = NSApp.windows.first as? MCWIndow {
Expand All @@ -48,10 +45,10 @@ fileprivate enum ArrowTag: Int {
extension AppDelegate {

private var originalSize: NSSize {
viewController.imageView.image?.size ?? defaultSize
viewController.imageSize ?? defaultSize
}

private func resizeAspectFit(calculator: (_ original: CGFloat, _ current: CGFloat) -> CGFloat) {
func resizeAspectFit(calculator: (_ original: CGFloat, _ current: CGFloat) -> CGFloat) {
let originalSize = self.originalSize
let width = calculator(originalSize.width, window.frame.size.width)
let height = width / originalSize.width * originalSize.height
Expand Down Expand Up @@ -82,15 +79,11 @@ extension AppDelegate {
}

@IBAction func increaseTransparency(_ sender: AnyObject) {
var alpha = viewController.imageView.alphaValue
alpha -= 0.1
viewController.imageView.alphaValue = max(alpha, 0.05)
viewController.changeTransparency(by: -0.1)
}

@IBAction func reduceTransparency(_ sender: AnyObject) {
var alpha = viewController.imageView.alphaValue
alpha += 0.1
viewController.imageView.alphaValue = min(alpha, 1.0)
viewController.changeTransparency(by: 0.1)
}

func getPasteboardImage() -> NSImage? {
Expand All @@ -112,16 +105,10 @@ extension AppDelegate {

return nil
}

@IBAction func paste(_ sender: AnyObject) {
guard let image = getPasteboardImage() else { return }
let rep = image.representations[0]
viewController.imageView.image = image
let size = NSMakeSize(CGFloat(rep.pixelsWide), CGFloat(rep.pixelsHigh))
window.resizeTo(size, animated: true)
viewController.sizeTextField.isHidden = false
viewController.placeholderTextField.isHidden = true

viewController.updateCurrentImage(image)
}

@IBAction func toggleLockWindow(_ sender: AnyObject) {
Expand Down Expand Up @@ -164,7 +151,7 @@ extension AppDelegate {
@IBAction func toggleSizeVisibility(_ sender: AnyObject) {
let menuItem = sender as! NSMenuItem
menuItem.state = menuItem.state == .on ? .off : .on
isSizeHidden = menuItem.state == .on
viewController.isSizeHidden = menuItem.state == .on
}

@IBAction func moveAround(_ sender: AnyObject) {
Expand Down Expand Up @@ -197,10 +184,6 @@ extension AppDelegate {
window.collectionBehavior = [.managed, .moveToActiveSpace]
}
}

func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
return viewController.imageView.image != nil
}
}

// MARK: - Helper
Expand Down
13 changes: 12 additions & 1 deletion LayerX/Base.lproj/Main.storyboard
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,15 @@
<rect key="frame" x="454" y="243" width="16" height="17"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" refusesFirstResponder="YES" sendsActionOnEndEditing="YES" alignment="center" title="0" drawsBackground="YES" id="Mmp-Eh-bBs">
<font key="font" metaFont="system"/>
<color key="textColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.59999999999999998" colorSpace="calibratedRGB"/>
</textFieldCell>
</textField>
<textField hidden="YES" wantsLayer="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="kNK-da-5FA">
<rect key="frame" x="10" y="244" width="30" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" refusesFirstResponder="YES" sendsActionOnEndEditing="YES" alignment="center" title="⌘ 1" drawsBackground="YES" id="N44-hk-hFy">
<font key="font" metaFont="system"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.59999999999999998" colorSpace="calibratedRGB"/>
</textFieldCell>
</textField>
Expand All @@ -327,12 +335,14 @@
<constraints>
<constraint firstAttribute="bottom" secondItem="3I3-1H-JKb" secondAttribute="bottom" constant="10" id="64H-0g-nnO"/>
<constraint firstItem="8Vs-Az-CYd" firstAttribute="centerY" secondItem="m2S-Jp-Qdl" secondAttribute="centerY" id="6fI-na-pdX"/>
<constraint firstItem="kNK-da-5FA" firstAttribute="top" secondItem="m2S-Jp-Qdl" secondAttribute="top" constant="10" id="9LJ-n2-2gE"/>
<constraint firstItem="8Vs-Az-CYd" firstAttribute="centerX" secondItem="m2S-Jp-Qdl" secondAttribute="centerX" id="RwW-eB-OYh"/>
<constraint firstItem="JAT-ZL-8HB" firstAttribute="top" secondItem="m2S-Jp-Qdl" secondAttribute="top" constant="10" id="WnK-c6-NWw"/>
<constraint firstItem="pOu-B0-2Yd" firstAttribute="top" secondItem="m2S-Jp-Qdl" secondAttribute="top" id="aC7-xh-2BR"/>
<constraint firstItem="pOu-B0-2Yd" firstAttribute="leading" secondItem="m2S-Jp-Qdl" secondAttribute="leading" id="lhy-17-EcM"/>
<constraint firstAttribute="trailing" secondItem="pOu-B0-2Yd" secondAttribute="trailing" id="qbm-Dq-jeg"/>
<constraint firstAttribute="bottom" secondItem="pOu-B0-2Yd" secondAttribute="bottom" id="r2U-99-hj5"/>
<constraint firstItem="kNK-da-5FA" firstAttribute="leading" secondItem="m2S-Jp-Qdl" secondAttribute="leading" constant="10" id="tUz-La-jPF"/>
<constraint firstAttribute="trailing" secondItem="3I3-1H-JKb" secondAttribute="trailing" constant="10" id="und-pJ-9Mz"/>
<constraint firstAttribute="trailing" secondItem="JAT-ZL-8HB" secondAttribute="trailing" constant="10" id="xlU-hc-l09"/>
</constraints>
Expand All @@ -342,6 +352,7 @@
<outlet property="lockIconImageView" destination="3I3-1H-JKb" id="Nka-rj-tDO"/>
<outlet property="placeholderTextField" destination="8Vs-Az-CYd" id="env-Po-Bb7"/>
<outlet property="sizeTextField" destination="JAT-ZL-8HB" id="vIP-80-PVG"/>
<outlet property="tabTextField" destination="kNK-da-5FA" id="bpw-Fo-3hM"/>
</connections>
</viewController>
<customObject id="rPt-NT-nkU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
Expand Down
144 changes: 121 additions & 23 deletions LayerX/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,33 @@ import Cocoa

class ViewController: NSViewController {

@IBOutlet weak var imageView: MCDragAndDropImageView!
@IBOutlet private weak var imageView: MCDragAndDropImageView!
@IBOutlet weak var sizeTextField: NSTextField!
@IBOutlet weak var placeholderTextField: NSTextField!
@IBOutlet weak var lockIconImageView: NSImageView!
@IBOutlet weak var tabTextField: NSTextField!

var isSizeHidden = false {
didSet { updateTextFieldVisibility() }
}

private var currentTab = 1 // Falls in range 1...9
private var tabImages = [Int: NSImage]()

override var acceptsFirstResponder: Bool {
return true
}

lazy var trackingArea: NSTrackingArea = {
let options: NSTrackingArea.Options = [.activeAlways, .mouseEnteredAndExited]
return NSTrackingArea(rect: self.view.bounds, options: options, owner: self, userInfo: nil)
let options: NSTrackingArea.Options = [.activeAlways, .mouseEnteredAndExited, .inVisibleRect]
return NSTrackingArea(rect: view.bounds, options: options, owner: self, userInfo: nil)
}()

private var isMouseInView: Bool {
guard let window = view.window else { return false }
return view.isMousePoint(window.mouseLocationOutsideOfEventStream, in: view.frame)
}

deinit {
NotificationCenter.default.removeObserver(self)
}
Expand All @@ -49,54 +62,114 @@ class ViewController: NSViewController {
NotificationCenter.default.addObserver(self, selector: #selector(windowDidResize(_:)), name: NSWindow.didResizeNotification, object: appDelegate().window)

view.addTrackingArea(trackingArea)

NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event -> NSEvent? in
if event.modifierFlags.contains(.command), event.characters?.count == 1 {
if event.characters?.lowercased() == "w" {
self?.updateCurrentImage(nil)
return nil
} else if let digit = event.characters.flatMap(Int.init), digit != 0 {
self?.selectTab(digit)
return nil
}
}

return event
}
}

@objc func fadeOutSizeTextField() {
let transition = CATransition()
sizeTextField.layer?.add(transition, forKey: "fadeOut")
sizeTextField.layer?.opacity = 0
// MARK: Tabs management

var imageSize: NSSize? {
guard let image = imageView.image else { return nil }
let pixelSize = image.representations.first.map { NSSize(width: $0.pixelsWide, height: $0.pixelsHigh) }
return pixelSize ?? image.size
}

func selectTab(_ tab: Int) {
currentTab = tab
tabTextField.stringValue = "⌘ \(tab)"

let image = tabImages[currentTab]
showImage(image)

if image != nil {
appDelegate().resizeAspectFit(calculator: { $1 })
}
}

func updateCurrentImage(_ image: NSImage?) {
let hadNoImages = tabImages.isEmpty

tabImages[currentTab] = image
showImage(image)

if image != nil {
appDelegate().resizeAspectFit(calculator: { hadNoImages ? $0 : $1 })
}
}

private func showImage(_ image: NSImage?) {
imageView.image = image

tabTextField.isHidden = false
updateTextFieldVisibility()

tabTextField.fadeIn()
if !isMouseInView {
tabTextField.fadeOutAfterDelay()
}
}

private func updateTextFieldVisibility() {
let hasImage = imageView.image != nil

sizeTextField.isHidden = !hasImage || isSizeHidden
placeholderTextField.isHidden = hasImage
}

// MARK: Actions

func changeTransparency(by diff: CGFloat) {
imageView.alphaValue = min(max(imageView.alphaValue + diff, 0.05), 1.0)
}

@objc func windowDidResize(_ notification: Notification) {
let window = notification.object as! NSWindow
let size = window.frame.size
sizeTextField.stringValue = "\(Int(size.width))x\(Int(size.height))"
sizeTextField.layer?.opacity = 1
sizeTextField.fadeIn()

NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(ViewController.fadeOutSizeTextField), object: nil)
perform(#selector(ViewController.fadeOutSizeTextField), with: nil, afterDelay: 2)
if !isMouseInView {
sizeTextField.fadeOutAfterDelay()
}
}

// MARK: Mouse events

override func scrollWheel(with theEvent: NSEvent) {
guard let _ = imageView.image else { return }
guard imageView.image != nil else { return }

let delta = theEvent.deltaY * 0.005;
var alpha = imageView.alphaValue - delta
alpha = min(alpha, 1)
alpha = max(alpha, 0.05)
imageView.alphaValue = alpha
let delta = theEvent.deltaY * 0.005
changeTransparency(by: -delta)
}

override func mouseEntered(with theEvent: NSEvent) {
sizeTextField.layer?.opacity = 1
sizeTextField.fadeIn()
tabTextField.fadeIn()
}

override func mouseExited(with theEvent: NSEvent) {
fadeOutSizeTextField()
sizeTextField.fadeOut()
tabTextField.fadeOut()
}
}

// MARK: - MCDragAndDropImageViewDelegate

extension ViewController: MCDragAndDropImageViewDelegate {
func dragAndDropImageViewDidDrop(_ imageView: MCDragAndDropImageView) {

sizeTextField.isHidden = false
placeholderTextField.isHidden = true

appDelegate().actualSize(nil)
updateCurrentImage(imageView.image)
}
}

Expand All @@ -107,3 +180,28 @@ class MCMovableView: NSView{
return true
}
}

// MARK: - Hiding text

fileprivate extension NSView {

func fadeIn() {
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(fadeOut), object: nil)

layer?.opacity = 1
}

@objc func fadeOut() {
// Fade out is always animated
let transition = CATransition()
layer?.add(transition, forKey: "fadeOut")
layer?.opacity = 0
}

func fadeOutAfterDelay() {
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(fadeOut), object: nil)

perform(#selector(fadeOut), with: nil, afterDelay: 2)
}

}
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ Support Mac OS X 10.10 or later.
- [x] Keyboard Shortcuts
- [x] Adjust transparency
- [x] Lock images
- [X] Aspect ratio scaling
- [x] Aspect ratio scaling
- [x] Switching between multiple images

# Keyboard Shortcuts

Expand All @@ -23,6 +24,8 @@ Support Mac OS X 10.10 or later.
| Key | Action |
|:--- |:--- |
|`⌘ V`| Paste image or file from clipboard. |
|`⌘ 1-9`| Switch between image tabs. |
|`⌘ W`| Remove image assigned to the current tab. |

### Scale

Expand Down