Browse Source

Fix photo preview layout and keep the edit button outside the image card.

Restore full-size preview by default, resize only while editing, and prevent adjustment panel setup from crashing on launch.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 1 month ago
parent
commit
7fa91f5c22
1 changed files with 103 additions and 42 deletions
  1. 103 42
      smart_printer/PhotoPreviewView.swift

+ 103 - 42
smart_printer/PhotoPreviewView.swift

@@ -57,7 +57,16 @@ final class PhotoPreviewOverlayView: NSView, AppearanceRefreshable {
     private var cropOverlay: PhotoCropOverlayView?
     private let printButton = PhotoPrintButton()
 
-    private var imageContainerTopConstraint: NSLayoutConstraint!
+    private var imageContainerTopToPreviewConstraint: NSLayoutConstraint?
+    private var imageContainerTopToControlsConstraint: NSLayoutConstraint?
+    private var imageContainerMinHeightConstraint: NSLayoutConstraint?
+    private var editControlsBottomConstraint: NSLayoutConstraint?
+    private var whiteningPanelHeightConstraint: NSLayoutConstraint?
+    private var saturationPanelHeightConstraint: NSLayoutConstraint?
+    private var contrastPanelHeightConstraint: NSLayoutConstraint?
+
+    private static let adjustmentPanelHeight: CGFloat = 56
+    private static let editingImageMinHeight: CGFloat = 320
 
     private var isEditPanelVisible = false
     private var isCropMode = false
@@ -177,7 +186,9 @@ final class PhotoPreviewOverlayView: NSView, AppearanceRefreshable {
         printButton.translatesAutoresizingMaskIntoConstraints = false
 
         editPanel.isHidden = true
-        hideAllAdjustmentPanels()
+        whiteningPanel.isHidden = true
+        saturationPanel.isHidden = true
+        contrastPanel.isHidden = true
 
         backButton.onClick = { [weak self] in self?.dismiss() }
         editButton.onClick = { [weak self] in self?.toggleEditPanel() }
@@ -203,9 +214,9 @@ final class PhotoPreviewOverlayView: NSView, AppearanceRefreshable {
         addSubview(titleLabel)
         addSubview(counterLabel)
         addSubview(previewSection)
-        previewSection.addSubview(editControlsArea)
         previewSection.addSubview(imageContainer)
-        editControlsArea.addSubview(editButton)
+        previewSection.addSubview(editButton)
+        previewSection.addSubview(editControlsArea)
         editControlsArea.addSubview(editPanel)
         editControlsArea.addSubview(whiteningPanel)
         editControlsArea.addSubview(saturationPanel)
@@ -213,6 +224,22 @@ final class PhotoPreviewOverlayView: NSView, AppearanceRefreshable {
         imageContainer.addSubview(imageView)
         addSubview(printButton)
 
+        imageContainerTopToPreviewConstraint = imageContainer.topAnchor.constraint(
+            equalTo: editButton.bottomAnchor,
+            constant: 12
+        )
+        imageContainerTopToControlsConstraint = imageContainer.topAnchor.constraint(
+            equalTo: editControlsArea.bottomAnchor,
+            constant: 8
+        )
+        imageContainerMinHeightConstraint = imageContainer.heightAnchor.constraint(
+            greaterThanOrEqualToConstant: Self.editingImageMinHeight
+        )
+        imageContainerMinHeightConstraint?.priority = .defaultHigh
+        whiteningPanelHeightConstraint = whiteningPanel.heightAnchor.constraint(equalToConstant: 0)
+        saturationPanelHeightConstraint = saturationPanel.heightAnchor.constraint(equalToConstant: 0)
+        contrastPanelHeightConstraint = contrastPanel.heightAnchor.constraint(equalToConstant: 0)
+
         NSLayoutConstraint.activate([
             backButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 24),
             backButton.topAnchor.constraint(equalTo: topAnchor, constant: 20),
@@ -227,21 +254,21 @@ final class PhotoPreviewOverlayView: NSView, AppearanceRefreshable {
 
             previewSection.leadingAnchor.constraint(equalTo: leadingAnchor, constant: AppTheme.contentPanelInset),
             previewSection.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -AppTheme.contentPanelInset),
-            previewSection.topAnchor.constraint(equalTo: backButton.bottomAnchor, constant: 28),
-            previewSection.bottomAnchor.constraint(equalTo: printButton.topAnchor, constant: -28),
+            previewSection.topAnchor.constraint(equalTo: backButton.bottomAnchor, constant: 24),
+            previewSection.bottomAnchor.constraint(equalTo: printButton.topAnchor, constant: -24),
 
-            editControlsArea.leadingAnchor.constraint(equalTo: previewSection.leadingAnchor),
-            editControlsArea.trailingAnchor.constraint(equalTo: previewSection.trailingAnchor),
-            editControlsArea.topAnchor.constraint(equalTo: previewSection.topAnchor),
-
-            editButton.trailingAnchor.constraint(equalTo: editControlsArea.trailingAnchor),
-            editButton.topAnchor.constraint(equalTo: editControlsArea.topAnchor),
+            editButton.trailingAnchor.constraint(equalTo: previewSection.trailingAnchor),
+            editButton.topAnchor.constraint(equalTo: previewSection.topAnchor),
             editButton.widthAnchor.constraint(equalToConstant: 40),
             editButton.heightAnchor.constraint(equalToConstant: 40),
 
+            editControlsArea.leadingAnchor.constraint(equalTo: previewSection.leadingAnchor),
+            editControlsArea.trailingAnchor.constraint(equalTo: previewSection.trailingAnchor),
+            editControlsArea.topAnchor.constraint(equalTo: editButton.bottomAnchor, constant: 8),
+
             editPanel.leadingAnchor.constraint(equalTo: editControlsArea.leadingAnchor),
             editPanel.trailingAnchor.constraint(equalTo: editControlsArea.trailingAnchor),
-            editPanel.topAnchor.constraint(equalTo: editButton.bottomAnchor, constant: 10),
+            editPanel.topAnchor.constraint(equalTo: editControlsArea.topAnchor),
 
             whiteningPanel.leadingAnchor.constraint(equalTo: editControlsArea.leadingAnchor),
             whiteningPanel.trailingAnchor.constraint(equalTo: editControlsArea.trailingAnchor),
@@ -255,17 +282,9 @@ final class PhotoPreviewOverlayView: NSView, AppearanceRefreshable {
             contrastPanel.trailingAnchor.constraint(equalTo: editControlsArea.trailingAnchor),
             contrastPanel.topAnchor.constraint(equalTo: editPanel.bottomAnchor, constant: 8),
 
-            editControlsArea.bottomAnchor.constraint(greaterThanOrEqualTo: whiteningPanel.bottomAnchor),
-            editControlsArea.bottomAnchor.constraint(greaterThanOrEqualTo: saturationPanel.bottomAnchor),
-            editControlsArea.bottomAnchor.constraint(greaterThanOrEqualTo: contrastPanel.bottomAnchor),
-            editControlsArea.bottomAnchor.constraint(greaterThanOrEqualTo: editPanel.bottomAnchor),
-            editControlsArea.bottomAnchor.constraint(greaterThanOrEqualTo: editButton.bottomAnchor),
-
             imageContainer.leadingAnchor.constraint(equalTo: previewSection.leadingAnchor),
             imageContainer.trailingAnchor.constraint(equalTo: previewSection.trailingAnchor),
-            imageContainer.heightAnchor.constraint(equalToConstant: 300),
-            imageContainer.heightAnchor.constraint(lessThanOrEqualToConstant: 340),
-            imageContainer.bottomAnchor.constraint(lessThanOrEqualTo: previewSection.bottomAnchor),
+            imageContainer.bottomAnchor.constraint(equalTo: previewSection.bottomAnchor),
 
             imageView.leadingAnchor.constraint(equalTo: imageContainer.leadingAnchor, constant: 12),
             imageView.trailingAnchor.constraint(equalTo: imageContainer.trailingAnchor, constant: -12),
@@ -275,13 +294,38 @@ final class PhotoPreviewOverlayView: NSView, AppearanceRefreshable {
             printButton.centerXAnchor.constraint(equalTo: centerXAnchor),
             printButton.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -28),
             printButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 220),
+
+            whiteningPanelHeightConstraint!,
+            saturationPanelHeightConstraint!,
+            contrastPanelHeightConstraint!,
         ])
 
-        updateImageContainerTopConstraint()
+        editControlsArea.isHidden = true
+        updatePreviewLayout()
     }
 
-    private func updateImageContainerTopConstraint() {
-        imageContainerTopConstraint?.isActive = false
+    private func updatePreviewLayout() {
+        let editing = isEditPanelVisible
+
+        editControlsArea.isHidden = !editing
+        imageContainerTopToPreviewConstraint?.isActive = !editing
+        imageContainerTopToControlsConstraint?.isActive = editing
+        imageContainerMinHeightConstraint?.isActive = editing
+
+        if editing {
+            updateEditControlsBottomConstraint()
+        } else {
+            editControlsBottomConstraint?.isActive = false
+        }
+
+        NSAnimationContext.runAnimationGroup { context in
+            context.duration = 0.2
+            layoutSubtreeIfNeeded()
+        }
+    }
+
+    private func updateEditControlsBottomConstraint() {
+        editControlsBottomConstraint?.isActive = false
 
         let anchor: NSLayoutYAxisAnchor
         if !whiteningPanel.isHidden {
@@ -290,20 +334,28 @@ final class PhotoPreviewOverlayView: NSView, AppearanceRefreshable {
             anchor = saturationPanel.bottomAnchor
         } else if !contrastPanel.isHidden {
             anchor = contrastPanel.bottomAnchor
-        } else if !editPanel.isHidden {
-            anchor = editPanel.bottomAnchor
         } else {
-            anchor = editButton.bottomAnchor
+            anchor = editPanel.bottomAnchor
         }
 
-        imageContainerTopConstraint = imageContainer.topAnchor.constraint(equalTo: anchor, constant: 14)
-        imageContainerTopConstraint.isActive = true
+        let bottomConstraint = editControlsArea.bottomAnchor.constraint(equalTo: anchor)
+        bottomConstraint.isActive = true
+        editControlsBottomConstraint = bottomConstraint
     }
 
     private func hideAllAdjustmentPanels() {
-        whiteningPanel.isHidden = true
-        saturationPanel.isHidden = true
-        contrastPanel.isHidden = true
+        setAdjustmentPanel(whiteningPanel, heightConstraint: whiteningPanelHeightConstraint, visible: false)
+        setAdjustmentPanel(saturationPanel, heightConstraint: saturationPanelHeightConstraint, visible: false)
+        setAdjustmentPanel(contrastPanel, heightConstraint: contrastPanelHeightConstraint, visible: false)
+    }
+
+    private func setAdjustmentPanel(
+        _ panel: PhotoAdjustSliderPanelView,
+        heightConstraint: NSLayoutConstraint?,
+        visible: Bool
+    ) {
+        panel.isHidden = !visible
+        heightConstraint?.constant = visible ? Self.adjustmentPanelHeight : 0
     }
 
     private func toggleAdjustmentPanel(_ panel: PhotoAdjustSliderPanelView, action: PhotoEditAction, currentValue: Float) {
@@ -314,18 +366,29 @@ final class PhotoPreviewOverlayView: NSView, AppearanceRefreshable {
         editPanel.setActive(.contrast, isActive: false)
 
         if willShow {
-            panel.isHidden = false
+            let heightConstraint: NSLayoutConstraint?
+            switch action {
+            case .whitening:
+                heightConstraint = whiteningPanelHeightConstraint
+            case .saturation:
+                heightConstraint = saturationPanelHeightConstraint
+            case .contrast:
+                heightConstraint = contrastPanelHeightConstraint
+            default:
+                heightConstraint = whiteningPanelHeightConstraint
+            }
+            setAdjustmentPanel(panel, heightConstraint: heightConstraint, visible: true)
             panel.setValue(currentValue)
             editPanel.setActive(action, isActive: true)
         }
-        updateImageContainerTopConstraint()
+        updatePreviewLayout()
     }
 
     private func toggleEditPanel() {
         isEditPanelVisible.toggle()
         editPanel.isHidden = !isEditPanelVisible
         hideAllAdjustmentPanels()
-        updateImageContainerTopConstraint()
+        updatePreviewLayout()
         if !isEditPanelVisible {
             exitCropMode()
         }
@@ -345,7 +408,7 @@ final class PhotoPreviewOverlayView: NSView, AppearanceRefreshable {
             toggleAdjustmentPanel(contrastPanel, action: .contrast, currentValue: contrastAmount)
         case .crop:
             hideAllAdjustmentPanels()
-            updateImageContainerTopConstraint()
+            updatePreviewLayout()
             enterCropMode()
         case .flip:
             applyGeometricEdit { PhotoImageEditor.flipHorizontal($0) }
@@ -355,7 +418,7 @@ final class PhotoPreviewOverlayView: NSView, AppearanceRefreshable {
             isEditPanelVisible = false
             editPanel.isHidden = true
             hideAllAdjustmentPanels()
-            updateImageContainerTopConstraint()
+            updatePreviewLayout()
             exitCropMode()
         }
     }
@@ -381,7 +444,7 @@ final class PhotoPreviewOverlayView: NSView, AppearanceRefreshable {
         isCropMode = true
         editPanel.setActive(.crop, isActive: true)
         hideAllAdjustmentPanels()
-        updateImageContainerTopConstraint()
+        updatePreviewLayout()
 
         let overlay = PhotoCropOverlayView(imageView: imageView)
         overlay.translatesAutoresizingMaskIntoConstraints = false
@@ -468,7 +531,7 @@ final class PhotoPreviewOverlayView: NSView, AppearanceRefreshable {
         whiteningPanel.setValue(0)
         saturationPanel.setValue(1)
         contrastPanel.setValue(1)
-        updateImageContainerTopConstraint()
+        updatePreviewLayout()
         exitCropMode()
 
         imageView.image = originalImage
@@ -737,8 +800,6 @@ private final class PhotoAdjustSliderPanelView: NSView, AppearanceRefreshable {
         addSubview(slider)
 
         NSLayoutConstraint.activate([
-            heightAnchor.constraint(equalToConstant: 56),
-
             titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 14),
             titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
             titleLabel.widthAnchor.constraint(equalToConstant: 78),