Skip to content

Commit

Permalink
fix: Show the export save panel as a per-window sheet (#147)
Browse files Browse the repository at this point in the history
  • Loading branch information
jbmorley authored Jan 21, 2023
1 parent 96b45b6 commit f152f5e
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 65 deletions.
4 changes: 4 additions & 0 deletions Symbolic.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
D86B0ECF291BEF7400352367 /* IconDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86B0ECE291BEF7400352367 /* IconDocument.swift */; };
D86B0ED1291BEFC100352367 /* UTType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86B0ED0291BEFC100352367 /* UTType.swift */; };
D870818E297C1F02007CAE83 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D870818D297C1F02007CAE83 /* Settings.swift */; };
D8708190297C6121007CAE83 /* SavePanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D870818F297C6121007CAE83 /* SavePanel.swift */; };
D88661A3297AC893001E41DD /* InfoLabeledContentStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88661A2297AC893001E41DD /* InfoLabeledContentStyle.swift */; };
D88661A5297AD82A001E41DD /* ConditionalLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88661A4297AD82A001E41DD /* ConditionalLink.swift */; };
D8B0B1F1296CD4AB00F907BE /* OffsetGuide.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B0B1F0296CD4AB00F907BE /* OffsetGuide.swift */; };
Expand Down Expand Up @@ -122,6 +123,7 @@
D86B0ECE291BEF7400352367 /* IconDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconDocument.swift; sourceTree = "<group>"; };
D86B0ED0291BEFC100352367 /* UTType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTType.swift; sourceTree = "<group>"; };
D870818D297C1F02007CAE83 /* Settings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
D870818F297C6121007CAE83 /* SavePanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavePanel.swift; sourceTree = "<group>"; };
D88661A2297AC893001E41DD /* InfoLabeledContentStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoLabeledContentStyle.swift; sourceTree = "<group>"; };
D88661A4297AD82A001E41DD /* ConditionalLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalLink.swift; sourceTree = "<group>"; };
D8B0B1F0296CD4AB00F907BE /* OffsetGuide.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OffsetGuide.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -290,6 +292,7 @@
children = (
D88661A4297AD82A001E41DD /* ConditionalLink.swift */,
D857297B296C176C0037E58F /* SymbolPickerCell.swift */,
D870818F297C6121007CAE83 /* SavePanel.swift */,
);
path = Modifiers;
sourceTree = "<group>";
Expand Down Expand Up @@ -543,6 +546,7 @@
files = (
D84B5D6F296C5229005E6C59 /* SceneModel.swift in Sources */,
D826B5C4296B054000693D27 /* WatchGridView.swift in Sources */,
D8708190297C6121007CAE83 /* SavePanel.swift in Sources */,
D821B9E828ACDBE000504AA4 /* Icon.swift in Sources */,
D8B0B1F1296CD4AB00F907BE /* OffsetGuide.swift in Sources */,
D821B9EC28ACDC2600504AA4 /* IconCorners.swift in Sources */,
Expand Down
7 changes: 3 additions & 4 deletions Symbolic/Commands/ExportCommands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import SwiftUI
struct ExportCommands: Commands {

@FocusedObject private var document: IconDocument?
@FocusedObject private var sceneModel: SceneModel?

let applicationModel: ApplicationModel

Expand All @@ -32,10 +32,9 @@ struct ExportCommands: Commands {
@MainActor public var body: some Commands {
CommandGroup(replacing: .importExport) {
Button("Export...") {
guard let document else { return }
applicationModel.export(icon: document.icon)
sceneModel?.export()
}
.disabled(document == nil)
.disabled(sceneModel == nil)
.keyboardShortcut("e")
}
}
Expand Down
56 changes: 0 additions & 56 deletions Symbolic/Models/ApplicationModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -154,17 +154,6 @@ class ApplicationModel: ObservableObject {

let settings = Settings()

@MainActor static func showSavePanel(_ title: String) -> URL? {
let savePanel = NSSavePanel()
savePanel.allowedContentTypes = [.directory]
savePanel.canCreateDirectories = true
savePanel.isExtensionHidden = false
savePanel.allowsOtherFileTypes = false
savePanel.title = title
let response = savePanel.runModal()
return response == .OK ? savePanel.url : nil
}

@MainActor private lazy var aboutWindow: NSWindow = {
return NSWindow(repository: "inseven/symbolic", copyright: "Copyright © 2022-2023 InSeven Limited") {
Action("Website", url: URL(string: "https://symbolic.app")!)
Expand Down Expand Up @@ -203,49 +192,4 @@ class ApplicationModel: ObservableObject {
aboutWindow.makeKeyAndOrderFront(nil)
}

@MainActor func export(icon: Icon) {
guard let url = Self.showSavePanel("Export Icon") else {
return
}
do {
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
for section in Self.icons {
for iconSet in section.sets {
let directoryUrl = url.appendingPathComponent(section.directory,
conformingTo: .directory)
try FileManager.default.createDirectory(at: directoryUrl,
withIntermediateDirectories: true)
for definition in iconSet.definitions {
switch definition.style {
case .macOS:
try icon.saveMacSnapshot(size: definition.size.width,
scale: definition.scale,
directoryURL: directoryUrl)
case .iOS:
try icon.saveSnapshot(size: definition.size.width,
scale: definition.scale,
shadow: false,
directoryURL: directoryUrl)
case .watchOS:
try icon.saveSnapshot(size: definition.size.width,
scale: definition.scale,
shadow: false,
isWatchOS: true,
directoryURL: directoryUrl)
}
}
}
}
if let licenseUrl = LibraryManager.shared.library(for: icon.symbol)?.license.fileURL {
try FileManager.default.copyItem(at: licenseUrl, to: url.appendingPathComponent("LICENSE"))
}

NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: url.absoluteString)

} catch {
print("Failed to write to file with error \(error)")
}

}

}
42 changes: 42 additions & 0 deletions Symbolic/Models/IconDocument.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,46 @@ final class IconDocument: ReferenceFileDocument {
return fileWrapper
}

@MainActor func export(destination url: URL) {
do {
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
for section in ApplicationModel.icons {
for iconSet in section.sets {
let directoryUrl = url.appendingPathComponent(section.directory,
conformingTo: .directory)
try FileManager.default.createDirectory(at: directoryUrl,
withIntermediateDirectories: true)
for definition in iconSet.definitions {
switch definition.style {
case .macOS:
try icon.saveMacSnapshot(size: definition.size.width,
scale: definition.scale,
directoryURL: directoryUrl)
case .iOS:
try icon.saveSnapshot(size: definition.size.width,
scale: definition.scale,
shadow: false,
directoryURL: directoryUrl)
case .watchOS:
try icon.saveSnapshot(size: definition.size.width,
scale: definition.scale,
shadow: false,
isWatchOS: true,
directoryURL: directoryUrl)
}
}
}
}
if let licenseUrl = LibraryManager.shared.library(for: icon.symbol)?.license.fileURL {
try FileManager.default.copyItem(at: licenseUrl, to: url.appendingPathComponent("LICENSE"))
}

NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: url.absoluteString)

} catch {
print("Failed to write to file with error \(error)")
}

}

}
5 changes: 5 additions & 0 deletions Symbolic/Models/SceneModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,10 @@ class SceneModel: ObservableObject {
@Published var showGrid = false
@Published var showOffsetX = false
@Published var showOffsetY = false
@Published var showExportPanel = false

@MainActor func export() {
showExportPanel = true
}

}
100 changes: 100 additions & 0 deletions Symbolic/Modifiers/SavePanel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright (c) 2022-2023 InSeven Limited
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

import SwiftUI
import UniformTypeIdentifiers

import Interact

struct SavePanel: ViewModifier {

struct Options: OptionSet {
let rawValue: Int

static let canCreateDirectories = Options(rawValue: 1 << 0)
static let isExtensionHidden = Options(rawValue: 1 << 1)
static let allowsOtherFileTypes = Options(rawValue: 1 << 2)
}

@Binding var isPresented: Bool
let title: String
let allowedContentTypes: [UTType]
let options: Options
let action: (URL) -> Void
@State var window: NativeWindow? = nil

init(isPresented: Binding<Bool>,
title: String,
allowedContentTypes: [UTType],
options: Options,
perform action: @escaping (URL) -> Void) {
self.title = title
_isPresented = isPresented
self.allowedContentTypes = allowedContentTypes
self.options = options
self.action = action
}

func body(content: Content) -> some View {
content
.hookWindow { window in
self.window = window
}
.onChange(of: isPresented) { newValue in
guard newValue,
let window = window
else {
return
}
let savePanel = NSSavePanel()
savePanel.allowedContentTypes = allowedContentTypes
savePanel.canCreateDirectories = options.contains(.canCreateDirectories)
savePanel.isExtensionHidden = options.contains(.isExtensionHidden)
savePanel.allowsOtherFileTypes = options.contains(.allowsOtherFileTypes)
savePanel.title = title
savePanel.beginSheetModal(for: window) { response in
isPresented = false
guard response == .OK,
let url = savePanel.url
else {
return
}
action(url)
}
}
}

}

extension View {

func savePanel(isPresented: Binding<Bool>,
title: String,
allowedContentTypes: [UTType],
options: SavePanel.Options = [],
perform action: @escaping (URL) -> Void) -> some View {
modifier(SavePanel(isPresented: isPresented,
title: title,
allowedContentTypes: allowedContentTypes,
options: options,
perform: action))
}

}
6 changes: 2 additions & 4 deletions Symbolic/Toolbars/ExportToolbar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,7 @@ extension Icon {

struct ExportToolbar: CustomizableToolbarContent {

@EnvironmentObject var applicationModel: ApplicationModel

var icon: Icon
@FocusedObject private var sceneModel: SceneModel?

var body: some CustomizableToolbarContent {

Expand All @@ -78,7 +76,7 @@ struct ExportToolbar: CustomizableToolbarContent {
// We're dispatching to main here because for some reason the compiler doens't think the button action
// is being performed on MainActor and is giving warnings (which is surprising).
DispatchQueue.main.async {
applicationModel.export(icon: icon)
sceneModel?.export()
}
} label: {
Label("Export", systemImage: "square.and.arrow.up")
Expand Down
8 changes: 7 additions & 1 deletion Symbolic/Views/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,20 @@ struct ContentView: View {
}
.focusedSceneObject(document)
.focusedSceneObject(sceneModel)
.savePanel(isPresented: $sceneModel.showExportPanel,
title: "Export",
allowedContentTypes: [.directory],
options: [.canCreateDirectories]) { url in
document.export(destination: url)
}
.toolbar(id: "main") {
ToolbarItem(id: "grid") {
Toggle(isOn: $sceneModel.showGrid) {
Label("Grid", systemImage: "grid")
}
.help("Hide/show the icon grid")
}
ExportToolbar(icon: document.icon)
ExportToolbar()
}
}
}
Expand Down

0 comments on commit f152f5e

Please sign in to comment.