diff --git a/.github/workflows/cleanzip-liquid-glass-icon.yml b/.github/workflows/cleanzip-liquid-glass-icon.yml index c603d4a..85c3321 100644 --- a/.github/workflows/cleanzip-liquid-glass-icon.yml +++ b/.github/workflows/cleanzip-liquid-glass-icon.yml @@ -6,7 +6,7 @@ on: release_tag: description: "Release tag to update" required: true - default: "v2.6.32" + default: "v2.6.33" upload_release: description: "Upload pkg, zip, and checksums to the release" required: true diff --git a/README.md b/README.md index 8db7260..db32c67 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ · Product page · - CleanZip 2.6.32 + CleanZip 2.6.33 · Build from source

@@ -59,7 +59,7 @@ It is intentionally small: no always-on background app, no history database, no ## Download -Download `CleanZip-2.6.32.pkg` from the [latest release](https://github.com/lyc280705/CleanZip/releases/latest). For most users, the `.pkg` installer is the easiest option. +Download `CleanZip-2.6.33.pkg` from the [latest release](https://github.com/lyc280705/CleanZip/releases/latest). For most users, the `.pkg` installer is the easiest option. The installer places: @@ -70,9 +70,9 @@ CleanZip is ad-hoc signed for open source distribution, but it is not notarized For the `.pkg` installer: -1. In Finder, Control-click `CleanZip-2.6.32.pkg` and choose **Open**. +1. In Finder, Control-click `CleanZip-2.6.33.pkg` and choose **Open**. 2. If the same warning still appears with only **Done** and **Move to Trash**, open **System Settings** -> **Privacy & Security**. -3. At the bottom of Privacy & Security, choose **Open Anyway** for `CleanZip-2.6.32.pkg`, then confirm. +3. At the bottom of Privacy & Security, choose **Open Anyway** for `CleanZip-2.6.33.pkg`, then confirm. After installation, if macOS blocks `CleanZip.app` itself, Control-click `CleanZip.app` in `/Applications` and choose **Open**. If it is still blocked, use **System Settings** -> **Privacy & Security** -> **Open Anyway** for `CleanZip.app`. @@ -81,11 +81,11 @@ After you approve the installer or app once, macOS opens it normally. Advanced terminal alternative for the downloaded package: ```bash -xattr -dr com.apple.quarantine ~/Downloads/CleanZip-2.6.32.pkg -open ~/Downloads/CleanZip-2.6.32.pkg +xattr -dr com.apple.quarantine ~/Downloads/CleanZip-2.6.33.pkg +open ~/Downloads/CleanZip-2.6.33.pkg ``` -Manual installation is also available from `CleanZip-2.6.32.zip`: move `CleanZip.app` to `/Applications` and `CleanZipService.service` to `/Library/Services`. +Manual installation is also available from `CleanZip-2.6.33.zip`: move `CleanZip.app` to `/Applications` and `CleanZipService.service` to `/Library/Services`. ## Compatibility @@ -172,7 +172,7 @@ gh workflow run cleanzip-liquid-glass-icon.yml --repo lyc280705/CleanZip --ref m To rebuild and update an existing GitHub release asset set: ```sh -gh workflow run cleanzip-liquid-glass-icon.yml --repo lyc280705/CleanZip --ref main -f release_tag=v2.6.32 -f upload_release=true +gh workflow run cleanzip-liquid-glass-icon.yml --repo lyc280705/CleanZip --ref main -f release_tag=v2.6.33 -f upload_release=true ``` Lightweight local fallback with Command Line Tools: diff --git a/work/CleanZipBuild/src/CleanZip-Info.plist b/work/CleanZipBuild/src/CleanZip-Info.plist index 6ddc2bc..5971867 100644 --- a/work/CleanZipBuild/src/CleanZip-Info.plist +++ b/work/CleanZipBuild/src/CleanZip-Info.plist @@ -132,7 +132,7 @@ CFBundleShortVersionString 2.6 CFBundleVersion - 32 + 33 LSApplicationCategoryType public.app-category.utilities LSMinimumSystemVersion diff --git a/work/CleanZipBuild/src/CleanZipService-Info.plist b/work/CleanZipBuild/src/CleanZipService-Info.plist index 6095686..04d5340 100644 --- a/work/CleanZipBuild/src/CleanZipService-Info.plist +++ b/work/CleanZipBuild/src/CleanZipService-Info.plist @@ -37,7 +37,7 @@ CFBundleShortVersionString 2.6 CFBundleVersion - 32 + 33 LSMinimumSystemVersion 14.0 LSUIElement diff --git a/work/CleanZipBuild/src/main.swift b/work/CleanZipBuild/src/main.swift index cdc4e1a..9d00ff1 100644 --- a/work/CleanZipBuild/src/main.swift +++ b/work/CleanZipBuild/src/main.swift @@ -681,12 +681,16 @@ enum FinderTableBehavior { table.allowsMultipleSelection = allowsMultipleSelection table.rowHeight = 26 table.headerView = NSTableHeaderView() - table.columnAutoresizingStyle = .firstColumnOnlyAutoresizingStyle + table.columnAutoresizingStyle = .reverseSequentialColumnAutoresizingStyle table.autosaveName = autosaveName table.autosaveTableColumns = true table.backgroundColor = .clear } + static func resizingMask(isFlexibleColumn: Bool) -> NSTableColumn.ResizingOptions { + isFlexibleColumn ? [.userResizingMask, .autoresizingMask] : [.userResizingMask] + } + static func shouldReorderColumn( in tableView: NSTableView, columnIndex: Int, @@ -703,55 +707,6 @@ enum FinderTableBehavior { } } -final class FinderTableScrollView: NSScrollView { - private var isRebalancingColumns = false - private var lastViewportWidth: CGFloat = 0 - private var columnsWereFitted = true - - override func layout() { - super.layout() - rebalanceColumnsToViewport() - } - - func rebalanceColumnsToViewport(resizedColumn: NSTableColumn? = nil) { - guard !isRebalancingColumns, - let table = documentView as? NSTableView, - let firstColumn = table.tableColumns.first, - table.numberOfColumns > 0 else { return } - - let viewportWidth = floor(contentView.bounds.width) - guard viewportWidth > 0 else { return } - let columnsWidth = ceil(table.rect(ofColumn: table.numberOfColumns - 1).maxX) - let isInitialLayout = lastViewportWidth <= 0 - let viewportChanged = !isInitialLayout && abs(viewportWidth - lastViewportWidth) > 0.5 - guard isInitialLayout || viewportChanged || resizedColumn != nil else { return } - - let gap = viewportWidth - columnsWidth - let nonFirstColumnChanged = resizedColumn != nil && resizedColumn !== firstColumn - let shouldRebalance = isInitialLayout || gap > 0.5 || (columnsWereFitted && (viewportChanged || nonFirstColumnChanged)) - - if shouldRebalance && abs(gap) > 0.5 { - isRebalancingColumns = true - firstColumn.width = min(firstColumn.maxWidth, max(firstColumn.minWidth, firstColumn.width + gap)) - isRebalancingColumns = false - } - - let fittedWidth = ceil(table.rect(ofColumn: table.numberOfColumns - 1).maxX) - columnsWereFitted = abs(viewportWidth - fittedWidth) <= 1 - lastViewportWidth = viewportWidth - - let documentWidth = max(viewportWidth, fittedWidth) - if abs(table.frame.width - documentWidth) > 0.5 { - isRebalancingColumns = true - let autoresizingStyle = table.columnAutoresizingStyle - table.columnAutoresizingStyle = .noColumnAutoresizing - table.setFrameSize(NSSize(width: documentWidth, height: table.frame.height)) - table.columnAutoresizingStyle = autoresizingStyle - isRebalancingColumns = false - } - } -} - struct SystemContentBackground: NSViewRepresentable { func makeNSView(context: Context) -> NSVisualEffectView { let view = NSVisualEffectView() @@ -1099,17 +1054,6 @@ struct ArchiveEntriesTable: NSViewRepresentable { return cell } - func tableViewColumnDidResize(_ notification: Notification) { - guard let tableView = notification.object as? NSTableView else { return } - let resizedColumn = notification.userInfo?["NSTableColumn"] as? NSTableColumn - (tableView.enclosingScrollView as? FinderTableScrollView)?.rebalanceColumnsToViewport(resizedColumn: resizedColumn) - } - - func tableViewColumnDidMove(_ notification: Notification) { - guard let tableView = notification.object as? NSTableView else { return } - (tableView.enclosingScrollView as? FinderTableScrollView)?.rebalanceColumnsToViewport() - } - func tableView(_ tableView: NSTableView, shouldReorderColumn columnIndex: Int, toColumn newColumnIndex: Int) -> Bool { FinderTableBehavior.shouldReorderColumn( in: tableView, @@ -1131,10 +1075,10 @@ struct ArchiveEntriesTable: NSViewRepresentable { FinderTableBehavior.configure( table, allowsMultipleSelection: false, - autosaveName: "local.codex.cleanzip.archiveEntriesTable.v7" + autosaveName: "local.codex.cleanzip.archiveEntriesTable.v13" ) let columns: [(String, String, CGFloat, CGFloat, CGFloat)] = [ - ("name", L10n.tr("column.name"), 380, 180, .greatestFiniteMagnitude), + ("name", L10n.tr("column.name"), 360, 180, .greatestFiniteMagnitude), ("size", L10n.tr("column.size"), 140, 96, 280), ("modified", L10n.tr("column.modified"), 240, 190, 2000) ] @@ -1144,12 +1088,12 @@ struct ArchiveEntriesTable: NSViewRepresentable { column.width = spec.2 column.minWidth = spec.3 column.maxWidth = spec.4 - column.resizingMask = [.userResizingMask, .autoresizingMask] + column.resizingMask = FinderTableBehavior.resizingMask(isFlexibleColumn: spec.0 == "name") table.addTableColumn(column) } table.delegate = context.coordinator table.dataSource = context.coordinator - let scroll = FinderTableScrollView() + let scroll = NSScrollView() scroll.hasVerticalScroller = true scroll.hasHorizontalScroller = true scroll.autohidesScrollers = true @@ -1167,7 +1111,6 @@ struct ArchiveEntriesTable: NSViewRepresentable { context.coordinator.filter = filter table.reloadData() } - (scrollView as? FinderTableScrollView)?.rebalanceColumnsToViewport() } } @@ -1222,17 +1165,6 @@ struct SelectedItemsTable: NSViewRepresentable { } } - func tableViewColumnDidResize(_ notification: Notification) { - guard let tableView = notification.object as? NSTableView else { return } - let resizedColumn = notification.userInfo?["NSTableColumn"] as? NSTableColumn - (tableView.enclosingScrollView as? FinderTableScrollView)?.rebalanceColumnsToViewport(resizedColumn: resizedColumn) - } - - func tableViewColumnDidMove(_ notification: Notification) { - guard let tableView = notification.object as? NSTableView else { return } - (tableView.enclosingScrollView as? FinderTableScrollView)?.rebalanceColumnsToViewport() - } - func tableView(_ tableView: NSTableView, shouldReorderColumn columnIndex: Int, toColumn newColumnIndex: Int) -> Bool { FinderTableBehavior.shouldReorderColumn( in: tableView, @@ -1335,14 +1267,14 @@ struct SelectedItemsTable: NSViewRepresentable { FinderTableBehavior.configure( table, allowsMultipleSelection: true, - autosaveName: "local.codex.cleanzip.selectedItemsTable.v7" + autosaveName: "local.codex.cleanzip.selectedItemsTable.v13" ) let columns: [(String, String, CGFloat, CGFloat, CGFloat)] = [ - ("selectedName", L10n.tr("column.name"), 260, 180, .greatestFiniteMagnitude), - ("selectedType", L10n.tr("column.type"), 90, 70, 160), - ("selectedSize", L10n.tr("column.size"), 120, 90, 220), - ("selectedLocation", L10n.tr("column.location"), 360, 220, 2000) + ("selectedName", L10n.tr("column.name"), 230, 180, .greatestFiniteMagnitude), + ("selectedType", L10n.tr("column.type"), 80, 70, 160), + ("selectedSize", L10n.tr("column.size"), 100, 90, 220), + ("selectedLocation", L10n.tr("column.location"), 330, 220, 2000) ] for spec in columns { let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier(spec.0)) @@ -1350,13 +1282,13 @@ struct SelectedItemsTable: NSViewRepresentable { column.width = spec.2 column.minWidth = spec.3 column.maxWidth = spec.4 - column.resizingMask = [.userResizingMask, .autoresizingMask] + column.resizingMask = FinderTableBehavior.resizingMask(isFlexibleColumn: spec.0 == "selectedName") table.addTableColumn(column) } table.delegate = context.coordinator table.dataSource = context.coordinator - let scroll = FinderTableScrollView() + let scroll = NSScrollView() scroll.hasVerticalScroller = true scroll.hasHorizontalScroller = true scroll.autohidesScrollers = true @@ -1381,7 +1313,6 @@ struct SelectedItemsTable: NSViewRepresentable { context.coordinator.applySelection(to: table) } } - (scrollView as? FinderTableScrollView)?.rebalanceColumnsToViewport() } } diff --git a/work/CleanZipBuild/tests/TableBehaviorTests.swift b/work/CleanZipBuild/tests/TableBehaviorTests.swift index 5d3595e..ee60c0b 100644 --- a/work/CleanZipBuild/tests/TableBehaviorTests.swift +++ b/work/CleanZipBuild/tests/TableBehaviorTests.swift @@ -11,7 +11,7 @@ struct TableBehaviorTests { testNativeTableConfiguration() testArchiveColumnReorderingPolicy() testSelectedItemsColumnReorderingPolicy() - testFinderStyleColumnSizing() + testNativeColumnSizing() testSelectedItemMetadataSnapshot() testProgressPublishingIsDeduplicated() testArchiveDerivedStateIsCached() @@ -38,7 +38,12 @@ struct TableBehaviorTests { expect(table.allowsColumnResizing, "column resizing should be enabled") expect(!table.allowsColumnSelection, "column headers must never become selected") expect(table.allowsMultipleSelection, "selected-items rows should allow multiple selection") - expect(table.columnAutoresizingStyle == .firstColumnOnlyAutoresizingStyle, "only the first column should autoresize with the window") + expect(table.columnAutoresizingStyle == .reverseSequentialColumnAutoresizingStyle, "window resizing should use AppKit's reverse sequential policy") + + let flexibleMask = FinderTableBehavior.resizingMask(isFlexibleColumn: true) + let fixedMask = FinderTableBehavior.resizingMask(isFlexibleColumn: false) + expect(flexibleMask.contains(.userResizingMask) && flexibleMask.contains(.autoresizingMask), "the name column should support user and window resizing") + expect(fixedMask.contains(.userResizingMask) && !fixedMask.contains(.autoresizingMask), "later columns should resize only when their divider is dragged") } private static func testArchiveColumnReorderingPolicy() { @@ -68,8 +73,8 @@ struct TableBehaviorTests { expect(coordinator.tableView(table, shouldReorderColumn: 3, toColumn: 1), "selected-item non-name columns should reorder") } - private static func testFinderStyleColumnSizing() { - let scroll = FinderTableScrollView(frame: NSRect(x: 0, y: 0, width: 820, height: 400)) + private static func testNativeColumnSizing() { + let scroll = NSScrollView(frame: NSRect(x: 0, y: 0, width: 820, height: 400)) scroll.hasVerticalScroller = true scroll.hasHorizontalScroller = true scroll.autohidesScrollers = true @@ -81,53 +86,39 @@ struct TableBehaviorTests { (120, 90, 220), (260, 220, 2_000) ] - for (column, sizing) in zip(table.tableColumns, widths) { + for (index, pair) in zip(table.tableColumns.indices, zip(table.tableColumns, widths)) { + let (column, sizing) = pair column.width = sizing.0 column.minWidth = sizing.1 column.maxWidth = sizing.2 - column.resizingMask = [.userResizingMask, .autoresizingMask] + column.resizingMask = FinderTableBehavior.resizingMask(isFlexibleColumn: index == 0) } - table.columnAutoresizingStyle = .firstColumnOnlyAutoresizingStyle + table.columnAutoresizingStyle = .reverseSequentialColumnAutoresizingStyle scroll.documentView = table scroll.layoutSubtreeIfNeeded() - scroll.rebalanceColumnsToViewport() - let fixedWidths = Array(table.tableColumns.dropFirst().map(\.width)) - expect(isFitted(table, in: scroll), "initial columns should snap to the viewport") - expect(!canScrollHorizontally(scroll), "a fitted table must not scroll horizontally") - - let firstBeforeGrow = table.tableColumns[0].width - scroll.setFrameSize(NSSize(width: 980, height: 400)) - scroll.layoutSubtreeIfNeeded() - scroll.rebalanceColumnsToViewport() - expect(table.tableColumns[0].width > firstBeforeGrow, "growing the window should grow the first column") - expect(equalWidths(Array(table.tableColumns.dropFirst().map(\.width)), fixedWidths), "growing the window must not resize later columns") - expect(isFitted(table, in: scroll), "grown columns should remain fitted") - expect(!canScrollHorizontally(scroll), "a grown fitted table must not scroll horizontally") - - let firstBeforeShrink = table.tableColumns[0].width - scroll.setFrameSize(NSSize(width: 860, height: 400)) - scroll.layoutSubtreeIfNeeded() - scroll.rebalanceColumnsToViewport() - expect(table.tableColumns[0].width < firstBeforeShrink, "shrinking the window should shrink the first column") - expect(equalWidths(Array(table.tableColumns.dropFirst().map(\.width)), fixedWidths), "shrinking the window must not resize later columns") - expect(isFitted(table, in: scroll), "shrunk columns should remain fitted while the first column has room") - expect(!canScrollHorizontally(scroll), "a shrunk fitted table must not scroll horizontally") + expect(!canScrollHorizontally(scroll), "columns that fit should not scroll horizontally") + let firstColumn = table.tableColumns[0] let typeColumn = table.tableColumns[1] - let firstBeforeDividerDrag = table.tableColumns[0].width + let firstBeforeDividerDrag = firstColumn.width + let typeBeforeDividerDrag = typeColumn.width typeColumn.width += 36 - scroll.rebalanceColumnsToViewport(resizedColumn: typeColumn) - expect(approximately(table.tableColumns[0].width, firstBeforeDividerDrag - 36), "resizing a later divider should absorb the delta in the first column") - expect(isFitted(table, in: scroll), "later-column resizing should snap back to the viewport") + scroll.layoutSubtreeIfNeeded() + expect(approximately(firstColumn.width, firstBeforeDividerDrag), "resizing one divider must not rewrite another column") + expect(approximately(typeColumn.width, typeBeforeDividerDrag + 36), "the dragged column should keep its native AppKit width") + expect(columnsWidth(table) > scroll.contentView.bounds.width, "a wider user-resized column should create native table overflow") - table.tableColumns[0].width = table.tableColumns[0].minWidth + scroll.setFrameSize(NSSize(width: 980, height: 400)) + scroll.layoutSubtreeIfNeeded() + expect(!canScrollHorizontally(scroll), "expanding the viewport should naturally remove overflow") + + firstColumn.width = firstColumn.minWidth scroll.setFrameSize(NSSize(width: 420, height: 400)) scroll.layoutSubtreeIfNeeded() - scroll.rebalanceColumnsToViewport(resizedColumn: table.tableColumns[0]) - expect(approximately(table.tableColumns[0].width, table.tableColumns[0].minWidth), "the first column must respect its minimum width") - expect(columnsWidth(table) > scroll.contentView.bounds.width + 1, "the table should overflow only after the first column reaches its minimum") - expect(canScrollHorizontally(scroll), "horizontal scrolling should become available after minimum-width overflow") + expect(approximately(firstColumn.width, firstColumn.minWidth), "the first column must respect its minimum width") + expect(columnsWidth(table) > scroll.contentView.bounds.width + 1, "columns should overflow only when their native minimum widths cannot fit") + expect(canScrollHorizontally(scroll), "minimum-width overflow should use the standard horizontal scroller") } private static func testSelectedItemMetadataSnapshot() { @@ -246,10 +237,6 @@ struct TableBehaviorTests { return table.rect(ofColumn: table.numberOfColumns - 1).maxX } - private static func isFitted(_ table: NSTableView, in scroll: NSScrollView) -> Bool { - approximately(columnsWidth(table), scroll.contentView.bounds.width, tolerance: 1.5) - } - private static func canScrollHorizontally(_ scroll: NSScrollView) -> Bool { let clipView = scroll.contentView var proposedBounds = clipView.bounds @@ -258,10 +245,6 @@ struct TableBehaviorTests { return abs(constrainedBounds.origin.x - clipView.bounds.origin.x) > 1 } - private static func equalWidths(_ lhs: [CGFloat], _ rhs: [CGFloat]) -> Bool { - lhs.count == rhs.count && zip(lhs, rhs).allSatisfy { approximately($0, $1) } - } - private static func approximately(_ lhs: CGFloat, _ rhs: CGFloat, tolerance: CGFloat = 1) -> Bool { abs(lhs - rhs) <= tolerance }