Skip to content

Commit

Permalink
Merge pull request #1033 from googlefonts/get-used-by
Browse files Browse the repository at this point in the history
Implement "glyphs used by glyph"
  • Loading branch information
justvanrossum authored Dec 18, 2023
2 parents 36eccc0 + 31d66c8 commit e0ff434
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 29 deletions.
20 changes: 14 additions & 6 deletions src/fontra/client/core/font-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export class FontController {
this._rootObject["unitsPerEm"] = await this.font.getUnitsPerEm();
this._rootObject["customData"] = await this.font.getCustomData();
this._rootClassDef = (await getClassSchema())["Font"];
this.backendInfo = await this.font.getBackEndInfo();
this._resolveInitialized();
}

Expand Down Expand Up @@ -129,7 +130,7 @@ export class FontController {

await this.getGlyph(glyphName);

for (const subGlyphName of this.iterGlyphMadeOf(glyphName)) {
for (const subGlyphName of this.iterGlyphsMadeOfRecursively(glyphName)) {
todo.add(subGlyphName);
}
};
Expand Down Expand Up @@ -276,7 +277,7 @@ export class FontController {
}

async glyphChanged(glyphName, senderInfo) {
const glyphNames = [glyphName, ...this.iterGlyphUsedBy(glyphName)];
const glyphNames = [glyphName, ...this.iterGlyphsUsedByRecursively(glyphName)];
for (const glyphName of glyphNames) {
this._purgeInstanceCache(glyphName);
}
Expand Down Expand Up @@ -507,7 +508,8 @@ export class FontController {
return cachedPattern;
}

*iterGlyphMadeOf(glyphName, seenGlyphNames = null) {
*iterGlyphsMadeOfRecursively(glyphName, seenGlyphNames = null) {
// Yield the names of all glyphs that are used as a component in `glyphName`, recursively.
if (!seenGlyphNames) {
seenGlyphNames = new Set();
} else if (seenGlyphNames.has(glyphName)) {
Expand All @@ -517,7 +519,7 @@ export class FontController {
seenGlyphNames.add(glyphName);
for (const dependantGlyphName of this.glyphMadeOf[glyphName] || []) {
yield dependantGlyphName;
for (const deeperGlyphName of this.iterGlyphMadeOf(
for (const deeperGlyphName of this.iterGlyphsMadeOfRecursively(
dependantGlyphName,
seenGlyphNames
)) {
Expand All @@ -526,7 +528,8 @@ export class FontController {
}
}

*iterGlyphUsedBy(glyphName, seenGlyphNames = null) {
*iterGlyphsUsedByRecursively(glyphName, seenGlyphNames = null) {
// Yield the names of all *loaded* glyphs that use `glyphName` as a component, recursively.
if (!seenGlyphNames) {
seenGlyphNames = new Set();
} else if (seenGlyphNames.has(glyphName)) {
Expand All @@ -536,7 +539,7 @@ export class FontController {
seenGlyphNames.add(glyphName);
for (const dependantGlyphName of this.glyphUsedBy[glyphName] || []) {
yield dependantGlyphName;
for (const deeperGlyphName of this.iterGlyphUsedBy(
for (const deeperGlyphName of this.iterGlyphsUsedByRecursively(
dependantGlyphName,
seenGlyphNames
)) {
Expand All @@ -545,6 +548,11 @@ export class FontController {
}
}

async getGlyphsUsedBy(glyphName) {
// Ask the backend about which glyphs use glyph `glyphName` as a component, non-recursively.
return await this.font.getGlyphsUsedBy(glyphName);
}

_purgeGlyphCache(glyphName) {
this._glyphsPromiseCache.delete(glyphName);
this._purgeInstanceCache(glyphName);
Expand Down
23 changes: 13 additions & 10 deletions src/fontra/core/fonthandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,13 @@ async def useConnection(self, connection) -> AsyncGenerator[None, None]:
if not self.connections and self.allConnectionsClosedCallback is not None:
await self.allConnectionsClosedCallback()

@remoteMethod
async def getBackEndInfo(self, *, connection=None) -> dict:
features = {}
for key, methodName in [("glyphs-used-by", "getGlyphsUsedBy")]:
features[key] = hasattr(self.backend, methodName)
return dict(name=self.backend.__class__.__name__, features=features)

@remoteMethod
async def getGlyph(
self, glyphName: str, *, connection=None
Expand Down Expand Up @@ -226,6 +233,12 @@ def _getClientData(self, connection, key, default=None):
def _setClientData(self, connection, key, value):
self.clientData[connection.clientUUID][key] = value

@remoteMethod
async def getGlyphsUsedBy(self, glyphName: str, *, connection) -> list[str]:
if hasattr(self.backend, "getGlyphsUsedBy"):
return await self.backend.getGlyphsUsedBy(glyphName)
return []

@remoteMethod
async def subscribeChanges(self, pathOrPattern, wantLiveChanges, *, connection):
pattern = _ensurePattern(pathOrPattern)
Expand Down Expand Up @@ -402,16 +415,6 @@ async def scheduleDataWrite(self, writeKey, writeFunc, connection):
self._processWritesEvent.set() # write: go!
self._writingInProgressEvent.clear()

def iterGlyphMadeOf(self, glyphName):
for dependantGlyphName in self.glyphMadeOf.get(glyphName, ()):
yield dependantGlyphName
yield from self.iterGlyphMadeOf(dependantGlyphName)

def iterGlyphUsedBy(self, glyphName):
for dependantGlyphName in self.glyphUsedBy.get(glyphName, ()):
yield dependantGlyphName
yield from self.iterGlyphUsedBy(dependantGlyphName)

def updateGlyphDependencies(self, glyphName, glyph):
# Zap previous used-by data for this glyph, if any
for componentName in self.glyphMadeOf.get(glyphName, ()):
Expand Down
127 changes: 115 additions & 12 deletions src/fontra/views/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -619,11 +619,7 @@ export class EditorController {
if (!glyphName) {
return;
}
const codePoint = this.fontController.codePointForGlyph(glyphName);
const glyphInfo = { glyphName: glyphName };
if (codePoint !== undefined) {
glyphInfo["character"] = getCharFromUnicode(codePoint);
}
const glyphInfo = glyphInfoFromGlyphName(glyphName, this.fontController);
let selectedGlyphState = this.sceneSettings.selectedGlyph;
const glyphLines = [...this.sceneSettings.glyphLines];
if (selectedGlyphState) {
Expand Down Expand Up @@ -678,12 +674,7 @@ export class EditorController {
if (location) {
localLocations[glyphName] = location;
}
const glyphInfo = { glyphName: glyphName };
const codePoint = this.fontController.codePointForGlyph(glyphName);
if (codePoint !== undefined) {
glyphInfo["character"] = getCharFromUnicode(codePoint);
}
glyphInfos.push(glyphInfo);
glyphInfos.push(glyphInfoFromGlyphName(glyphName, this.fontController));
}
this.sceneController.updateLocalLocations(localLocations);
const selectedGlyphInfo = this.sceneSettings.selectedGlyph;
Expand Down Expand Up @@ -792,6 +783,12 @@ export class EditorController {
},
});
}

this.glyphSelectedContextMenuItems.push({
title: () => `Find glyphs that use '${this.sceneSettings.selectedGlyphName}'`,
enabled: () => this.fontController.backendInfo.features["glyphs-used-by"],
callback: () => this.doFindGlyphsUsedBy(),
});
}

initShortCuts() {
Expand Down Expand Up @@ -1447,7 +1444,7 @@ export class EditorController {

const dialog = await dialogSetup("Add Component", null, [
{ title: "Cancel", isCancelButton: true },
{ title: "Add", isDefaultButton: true, result: "ok", disabled: true },
{ title: "Add", isDefaultButton: true, resultValue: "ok", disabled: true },
]);

dialog.setContent(glyphsSearch);
Expand Down Expand Up @@ -1553,6 +1550,95 @@ export class EditorController {
this.sceneSettings.selectedSourceIndex = newSourceIndex;
}

async doFindGlyphsUsedBy() {
const glyphName = this.sceneSettings.selectedGlyphName;

const usedBy = await loaderSpinner(this.fontController.getGlyphsUsedBy(glyphName));

if (!usedBy.length) {
await dialog(
`Glyph '${glyphName}' is not used as a component by any glyph.`,
null,
[{ title: "Okay", resultValue: "ok" }]
);
return;
}

usedBy.sort();

const glyphMap = Object.fromEntries(
usedBy.map((glyphName) => [glyphName, this.fontController.glyphMap[glyphName]])
);

const glyphsSearch = document.createElement("glyphs-search");
glyphsSearch.glyphMap = glyphMap;

glyphsSearch.addEventListener("selectedGlyphNameDoubleClicked", (event) => {
theDialog.defaultButton.click();
});

const theDialog = await dialogSetup(
`Glyphs that use glyph '${glyphName}' as a component`,
null,
[
{ title: "Cancel", isCancelButton: true },
{ title: "Copy names", resultValue: "copy" },
{
title: "Add to text",
isDefaultButton: true,
resultValue: "add",
},
]
);

theDialog.setContent(glyphsSearch);

setTimeout(() => glyphsSearch.focusSearchField(), 0); // next event loop iteration

switch (await theDialog.run()) {
case "copy": {
const glyphNamesString = chunks(usedBy, 16)
.map((chunked) => chunked.map((glyphName) => "/" + glyphName).join(""))
.join("\n");
const clipboardObject = {
"text/plain": glyphNamesString,
};
await writeToClipboard(clipboardObject);
break;
}
case "add": {
const glyphName = glyphsSearch.getSelectedGlyphName();
const MAX_NUM_GLYPHS = 100;
const truncate = !glyphName && usedBy.length > MAX_NUM_GLYPHS;
const glyphNames = glyphName
? [glyphName]
: truncate
? usedBy.slice(0, MAX_NUM_GLYPHS)
: usedBy;

const glyphInfos = glyphNames.map((glyphName) =>
glyphInfoFromGlyphName(glyphName, this.fontController)
);
const selectedGlyphInfo = this.sceneSettings.selectedGlyph;
const glyphLines = [...this.sceneSettings.glyphLines];
glyphLines[selectedGlyphInfo.lineIndex].splice(
selectedGlyphInfo.glyphIndex + 1,
0,
...glyphInfos
);
this.sceneSettings.glyphLines = glyphLines;
if (truncate) {
await dialog(
`The number of added glyphs was truncated to ${MAX_NUM_GLYPHS}`,
null,
[{ title: "Okay", resultValue: "ok" }]
);
}
break;
}
}
}

keyUpHandler(event) {
if (event.code === "Space") {
this.spaceKeyUpHandler();
Expand Down Expand Up @@ -1995,3 +2081,20 @@ async function runDialogWholeGlyphPaste() {

return result === "ok" ? controller.model.behavior : null;
}

function chunks(array, n) {
const chunked = [];
for (const i of range(0, array.length, n)) {
chunked.push(array.slice(i, i + n));
}
return chunked;
}

function glyphInfoFromGlyphName(glyphName, fontController) {
const glyphInfo = { glyphName: glyphName };
const codePoint = fontController.codePointForGlyph(glyphName);
if (codePoint !== undefined) {
glyphInfo["character"] = getCharFromUnicode(codePoint);
}
return glyphInfo;
}
5 changes: 4 additions & 1 deletion src/fontra/views/editor/scene-model.js
Original file line number Diff line number Diff line change
Expand Up @@ -742,7 +742,10 @@ function getUsedGlyphNames(fontController, positionedLines) {
for (const line of positionedLines) {
for (const glyph of line.glyphs) {
usedGlyphNames.add(glyph.glyph.name);
updateSet(usedGlyphNames, fontController.iterGlyphMadeOf(glyph.glyph.name));
updateSet(
usedGlyphNames,
fontController.iterGlyphsMadeOfRecursively(glyph.glyph.name)
);
}
}
return usedGlyphNames;
Expand Down

0 comments on commit e0ff434

Please sign in to comment.