Skip to content
Merged
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
119 changes: 113 additions & 6 deletions apps/desktop/src/main/document/DocumentSession.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
import { dialog, type MessageBoxOptions, type SaveDialogOptions } from "electron";
import {
dialog,
type MessageBoxOptions,
type OpenDialogOptions,
type SaveDialogOptions,
} from "electron";
import path from "node:path";
import { errorToMessage } from "../../shared/errors";
import type { WorkspaceDocumentState } from "../../shared/workspace/protocol";
import type { Window } from "../windows/Window";
import type { Document } from "./DocumentClient";
import { createShiftLogger, type ShiftLogger } from "../logging";

const OPEN_FONT_EXTENSIONS = [
"shift",
"ttf",
"otf",
"glyphs",
"glyphspackage",
"ufo",
"designspace",
];

export type DocumentSessionOptions = {
document: Document;
dialogWindow: () => Window | null;
Expand All @@ -14,7 +29,7 @@ export type DocumentSessionOptions = {
log?: ShiftLogger;
};

export type CloseReason = "window" | "quit";
export type CloseReason = "window" | "quit" | "replace-document";
type DirtyDocumentChoice = "save" | "discard" | "cancel";

/**
Expand Down Expand Up @@ -42,6 +57,43 @@ export class DocumentSession {
this.#log = options.log ?? createShiftLogger("document.session");
}

/** Creates an untitled workspace through the renderer edit lane. */
async create(): Promise<void> {
this.#log.info("new document requested");
if (!(await this.confirmClose("replace-document"))) {
this.#log.info("new document canceled by close guard");
return;
}

await this.#document.create();
this.#log.info("new document created");
}

/** Runs Open from main with a native open dialog. */
async open(): Promise<void> {
this.#log.info("open document requested");
const openPath = await this.#showOpenDialog();
if (!openPath) {
this.#log.info("open document canceled before file selection");
return;
}

if (!(await this.confirmClose("replace-document"))) {
this.#log.info("open document canceled by close guard", { path: openPath });
return;
}

try {
await this.#requestOpen(openPath);
} catch (error) {
this.#log.warn("open document failed", { path: openPath }, error);
await this.#showOpenFailedDialog(openPath, error);
return;
}

this.#log.info("open document completed", { path: openPath });
}

/** Returns whether a close transition needs document confirmation. */
shouldConfirmClose(): boolean {
const shouldConfirm = this.#document.connected || this.#state?.dirty === true;
Expand Down Expand Up @@ -78,7 +130,7 @@ export class DocumentSession {
return true;
}

const choice = await this.#showDirtyDocumentDialog(state);
const choice = await this.#showDirtyDocumentDialog(state, reason);
this.#log.info("dirty document dialog completed", {
reason,
choice,
Expand Down Expand Up @@ -211,6 +263,11 @@ export class DocumentSession {
return state;
}

async #requestOpen(openPath: string): Promise<void> {
this.#log.info("document open sent to renderer", { path: openPath });
await this.#document.open(openPath);
}

async #showSaveDialog(state: WorkspaceDocumentState): Promise<string | null> {
const options: SaveDialogOptions = {
title: "Save Shift Document",
Expand All @@ -227,7 +284,10 @@ export class DocumentSession {
return result.canceled ? null : (result.filePath ?? null);
}

async #showDirtyDocumentDialog(state: WorkspaceDocumentState): Promise<DirtyDocumentChoice> {
async #showDirtyDocumentDialog(
state: WorkspaceDocumentState,
reason: CloseReason,
): Promise<DirtyDocumentChoice> {
const name = state.saveTarget ? path.basename(state.saveTarget) : "Untitled";
const options: MessageBoxOptions = {
type: "warning",
Expand All @@ -236,7 +296,7 @@ export class DocumentSession {
cancelId: 2,
noLink: true,
title: this.#applicationName(),
message: this.#dirtyDocumentMessage(name),
message: this.#dirtyDocumentMessage(reason, name),
detail: "Your changes will be lost if you don't save them.",
};

Expand Down Expand Up @@ -269,10 +329,57 @@ export class DocumentSession {
await dialog.showMessageBox(options);
}

#dirtyDocumentMessage(name: string): string {
async #showOpenFailedDialog(openPath: string, error: unknown): Promise<void> {
const options: MessageBoxOptions = {
type: "error",
buttons: ["OK"],
defaultId: 0,
title: this.#applicationName(),
message: "The font could not be loaded.",
detail: `${openPath}\n\n${errorToMessage(error)}`,
};

const window = this.#dialogWindow();
if (window) {
await dialog.showMessageBox(window.window, options);
return;
}

await dialog.showMessageBox(options);
}

#dirtyDocumentMessage(reason: CloseReason, name: string): string {
if (reason === "replace-document") {
return `Save changes to ${name} before replacing it?`;
}

return `Save changes to ${name} before closing?`;
}

async #showOpenDialog(): Promise<string | null> {
const options: OpenDialogOptions = {
title: "Open Font",
filters: [
{ name: "Supported Fonts", extensions: OPEN_FONT_EXTENSIONS },
{ name: "Shift Source Package", extensions: ["shift"] },
{ name: "TrueType/OpenType", extensions: ["ttf", "otf"] },
{ name: "Glyphs", extensions: ["glyphs", "glyphspackage"] },
{ name: "UFO/Designspace", extensions: ["ufo", "designspace"] },
],
properties: process.platform === "darwin" ? ["openFile", "openDirectory"] : ["openFile"],
};

const window = this.#dialogWindow();
const result = window
? await dialog.showOpenDialog(window.window, options)
: await dialog.showOpenDialog(options);

if (result.canceled) return null;
if (result.filePaths.length !== 1) return null;

return result.filePaths[0];
}

#updateWindowTitle(): void {
const state = this.#state;
const windows = this.#windows();
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/renderer/src/app/Screens.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ const WorkspaceScreens = () => {
}
}

editor.scene.setLocation(font.defaultLocation());
editor.setDesignLocation(font.defaultLocation());

void maximiseWorkspaceWindow();
}, [documentLoaded, editor, font]);

Expand Down
13 changes: 6 additions & 7 deletions apps/desktop/src/renderer/src/components/editor/Canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,21 @@ export const Canvas: FC = () => {
useEffect(() => {
if (!glyphIdParam) {
editor.scene.clear();
return;
return undefined;
}

const glyphId = asGlyphId(glyphIdParam);
if (!editor.font.hasGlyph(glyphId)) {
if (!editor.font.recordForId(glyphId)) {
editor.scene.clear();
return;
return undefined;
}

editor.scene.clear();
const itemId = editor.scene.addGlyph({ glyphId, origin: { x: 0, y: 0 } });
editor.scene.setGeometryItems([itemId]);

return () => {
editor.scene.clear();
};
}, [glyphIdParam]);
return () => editor.scene.clear();
}, [editor, glyphIdParam]);

useEffect(() => {
const element = containerRef.current;
Expand Down
Loading
Loading