Skip to content

Commit

Permalink
Merge pull request #2082 from googlefonts/issue-2023-edit-font-infos-…
Browse files Browse the repository at this point in the history
…metadata-2

Edit font infos metadata 2
  • Loading branch information
ollimeier authored Mar 5, 2025
2 parents 1884b97 + bd8131b commit aa43661
Show file tree
Hide file tree
Showing 4 changed files with 511 additions and 214 deletions.
28 changes: 23 additions & 5 deletions src-js/fontra-core/src/font-info-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,20 @@ function getDescenderDefault(fontObject = undefined) {
return fontObject.lineMetricsHorizontalLayout?.descender.value || -200;
}

function getFamilyNameDefault(fontObject = undefined) {
return fontObject.familyName || "Family Name";
}

function getSubfamilyNameDefault(fontObject = undefined) {
return fontObject.name || "Subfamily Name";
}

function getVersionNameDefault(fontObject = undefined) {
return fontObject.versionMajor
? `Version ${fontObject.versionMajor}.${fontObject.versionMinor}`
: "Version 1.0";
}

function getStrikeoutPositionDefault(fontObject = undefined) {
return fontObject.lineMetricsHorizontalLayout?.ascender.value / 2 || 250;
}
Expand All @@ -39,7 +49,11 @@ function getCreatedDefault() {

export const customDataNameMapping = {
// vertical metrics values
openTypeHheaAscender: { default: getAscenderDefault, formatter: _NumberFormatter },
openTypeHheaAscender: {
default: getAscenderDefault,
formatter: _NumberFormatter,
info: "Integer. Ascender value. Corresponds to the OpenType hhea table `Ascender` field.",
},
openTypeHheaDescender: { default: getDescenderDefault, formatter: _NumberFormatter },
openTypeHheaLineGap: { default: () => 0, formatter: _NumberFormatter },
openTypeOS2TypoAscender: { default: getAscenderDefault, formatter: _NumberFormatter },
Expand All @@ -62,18 +76,22 @@ export const customDataNameMapping = {
openTypeOS2StrikeoutSize: { default: () => 50, formatter: _NumberFormatter },
// name table entries
openTypeNameUniqueID: { default: () => "Unique ID Name ID 3" }, // Name ID 3
openTypeNameVersion: { default: () => "Version 1.0" }, // Name ID 7
openTypeNamePreferredFamilyName: { default: () => "Family Name" }, // Name ID 16
openTypeNameVersion: { default: getVersionNameDefault }, // Name ID 7
openTypeNamePreferredFamilyName: { default: getFamilyNameDefault }, // Name ID 16
openTypeNamePreferredSubfamilyName: { default: getSubfamilyNameDefault }, // Name ID 17
openTypeNameCompatibleFullName: { default: () => "Compatible Full Name" }, // Name ID 18
openTypeNameWWSFamilyName: { default: () => "Family Name" }, // Name ID 21
openTypeNameWWSFamilyName: { default: getFamilyNameDefault }, // Name ID 21
openTypeNameWWSSubfamilyName: { default: getSubfamilyNameDefault }, // Name ID 22
// misc
openTypeOS2WeightClass: { default: () => 400, formatter: _NumberFormatter },
openTypeOS2WidthClass: { default: () => 5, formatter: _NumberFormatter },
openTypeHeadCreated: { default: getCreatedDefault }, // The timezone is UTC.
openTypeOS2Selection: { default: () => [], formatter: ArrayFormatter }, // 7 = Use Typo Metrics, 8 = has WWS name, https://github.com/fonttools/fonttools/blob/598b974f87f35972da24e96e45bd0176d18930a0/Lib/fontTools/ufoLib/__init__.py#L1889
openTypeOS2Type: { default: () => [3], formatter: ArrayFormatter }, // https://github.com/googlefonts/glyphsLib/blob/c4db6b981d577f456d64ebe9993818770e170454/Lib/glyphsLib/builder/custom_params.py#L1166
openTypeOS2Type: {
default: () => [3],
formatter: ArrayFormatter,
info: `Font embedding bit:\n2 = "Preview & Print embedding"\n3 = "Editable embedding" (default)`,
},
openTypeOS2Panose: {
default: () => [2, 11, 5, 2, 4, 5, 4, 2, 2, 4],
formatter: FixedLengthArrayFormatter(10),
Expand Down
319 changes: 319 additions & 0 deletions src-js/fontra-webcomponents/src/custom-data-list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
import { customDataNameMapping } from "@fontra/core/font-info-data.js";
import * as html from "@fontra/core/html-utils.js";
import { SimpleElement } from "@fontra/core/html-utils.js";
import { translate } from "@fontra/core/localization.js";
import { ObservableController } from "@fontra/core/observable-object.js";
import { DefaultFormatter, labeledTextInput } from "@fontra/core/ui-utils.js";
import { dialogSetup, message } from "@fontra/web-components/modal-dialog.js";
import { UIList } from "@fontra/web-components/ui-list.js";
import { themeColorCSS } from "./theme-support.js";

// TODO: Refactor this, copy from panel-axes.js
function arraysEqual(arrayA, arrayB) {
if (arrayA.length !== arrayB.length) {
return false;
}
for (const [itemA, itemB] of zip(arrayA, arrayB)) {
if (itemA !== itemB) {
return false;
}
}
return true;
}

// TODO: Refactor this, copy from panel-axes.js
function updateRemoveButton(list, buttons) {
list.addEventListener("listSelectionChanged", (event) => {
buttons.disableRemoveButton = list.getSelectedItemIndex() === undefined;
});
}

const colors = {
"menu-bar-link-hover": ["#e1e1e1", "rgb(47, 47, 47)"],
};

export class CustomDataList extends SimpleElement {
static styles = `
${themeColorCSS(colors)}
.fontra-ui-font-info-sources-panel-list-element {
min-width: max-content;
max-width: 29.5em; // 4.5 + 25
max-height: 12em;
}
`;

// fontObject can either be FontInfo or FontSource.
constructor(options) {
super();
this.contentElement = this.shadowRoot.appendChild(html.div());
this.controller = options.controller;
this.fontObject = options.fontObject;
this.supportedAttributes =
options.supportedAttributes || Object.keys(customDataNameMapping);
this.render();
}

buildCustomDataList() {
const customDataNames = Object.keys(customDataNameMapping);
const model = this.controller.model;

const makeItem = ([key, value]) => {
const item = new ObservableController({ key: key, value: value });
item.addListener((event) => {
const sortedItems = [...labelList.items];
sortedItems.sort(
(a, b) =>
(customDataNames.indexOf(a.key) != -1
? customDataNames.indexOf(a.key)
: customDataNames.length) -
(customDataNames.indexOf(b.key) != -1
? customDataNames.indexOf(b.key)
: customDataNames.length)
);

if (!arraysEqual(labelList.items, sortedItems)) {
labelList.setItems(sortedItems);
}

const newCustomData = sortedItems.map((customData) => {
return { ...customData };
});
model.customData = newCustomData;
});
return item.model;
};

const sortedItems = Object.entries(model);
sortedItems.sort(
(a, b) =>
(customDataNames.indexOf(a[0]) != -1
? customDataNames.indexOf(a[0])
: customDataNames.length) -
(customDataNames.indexOf(b[0]) != -1
? customDataNames.indexOf(b[0])
: customDataNames.length)
);
const items = sortedItems?.map(makeItem) || [];

const labelList = new UIList();
labelList.classList.add("fontra-ui-font-info-sources-panel-list-element");
labelList.style = `min-width: 12em;`;
labelList.columnDescriptions = [
{
key: "key",
title: "Key", // TODO: translation
width: "14em",
editable: true,
continuous: false,
},
{
key: "value",
title: "Value", // TODO: translation
width: "10em",
editable: true,
continuous: false,
},
];
labelList.showHeader = true;
labelList.minHeight = "5em";
labelList.setItems(items);

const deleteSelectedItem = () => {
const index = labelList.getSelectedItemIndex();
if (index === undefined) {
return;
}
const items = [...labelList.items];
items.splice(index, 1);
labelList.setItems(items);
const newCustomData = items.map((customData) => {
return { ...customData };
});
model.customData = newCustomData;
addRemoveButton.scrollIntoView({
behavior: "auto",
block: "nearest",
inline: "nearest",
});
labelList.setSelectedItemIndex(items.length - 1);
};

labelList.addEventListener("deleteKey", deleteSelectedItem);
const addRemoveButton = html.createDomElement("add-remove-buttons", {
addButtonCallback: async () => {
const currentKeys = labelList.items.map((customData) => {
return customData.key;
});
let nextKey = undefined;
for (const key of this.supportedAttributes) {
if (!currentKeys.includes(key)) {
nextKey = key;
break;
}
}
const { key, value } = await this._customDataPropertiesRunDialog(
this.fontObject,
nextKey,
this.supportedAttributes
);
if (key == undefined || value == undefined) {
return;
}

const newItem = makeItem([key, value]);
const newItems = [...labelList.items, newItem];
model.customData = newItems.map((label) => {
return { ...label };
});
labelList.setItems(newItems);
labelList.editCell(newItems.length - 1, "key");
addRemoveButton.scrollIntoView({
behavior: "auto",
block: "nearest",
inline: "nearest",
});
},
removeButtonCallback: deleteSelectedItem,
disableRemoveButton: true,
});

updateRemoveButton(labelList, addRemoveButton);

return html.div({ style: "display: grid; grid-gap: 0.3em;" }, [
labelList,
addRemoveButton,
]);
}

render() {
this.contentElement.appendChild(this.buildCustomDataList());
}

async _customDataPropertiesRunDialog(fontObject, nextKey, supportedAttributes) {
const title = translate("Add advanced information"); // TODO: translation

const validateInput = () => {
const infos = [];
const warnings = [];
const customDataKey =
nameController.model.customDataKey == ""
? undefined
: nameController.model.customDataKey;
nameController.model.suggestedCustomDataValue = `Please enter the correct value`;
nameController.model.suggestedCustomDataKey = `Please enter a valid key`;
if (customDataKey != undefined) {
const customDataInfo = customDataNameMapping[customDataKey]?.info;
if (customDataInfo) {
infos.push(customDataInfo);
}

if (!customDataNameMapping[customDataKey]) {
warnings.push(`⚠️ ${translate("Unkown custom data key")}`); // TODO: translation
}

const customDataValue =
nameController.model.customDataValue == ""
? undefined
: nameController.model.customDataValue;
if (customDataValue != undefined) {
const formatter =
customDataNameMapping[customDataKey]?.formatter || DefaultFormatter;
const result = formatter.fromString(customDataValue);
if (result.value == undefined) {
const msg = result.error ? ` "${result.error}"` : "";
warnings.push(`⚠️ Invalid value${msg}`); // TODO: translation
}
}
}

warningElement.innerText = warnings.length ? warnings.join("\n") : "";
dialog.defaultButton.classList.toggle("disabled", warnings.length);
infoElement.innerText = infos.length ? infos.join("\n") : "";
};

const nameController = new ObservableController({
customDataKey: nextKey,
suggestedCustomDataKey: nextKey,
customDataValue: customDataNameMapping[nextKey].default(fontObject),
suggestedCustomDataValue: "Enter number, list or boolean",
});

nameController.addKeyListener("customDataKey", (event) => {
validateInput();
nameController.model.customDataValue =
customDataNameMapping[nameController.model.customDataKey]?.default(fontObject);
});

nameController.addKeyListener("customDataValue", (event) => {
validateInput();
});

const { contentElement, warningElement, infoElement } =
this._customDataPropertiesContentElement(nameController, supportedAttributes);

const dialog = await dialogSetup(title, null, [
{ title: translate("dialog.cancel"), isCancelButton: true },
{ title: translate("dialog.add"), isDefaultButton: true },
]);
dialog.setContent(contentElement);

setTimeout(
() => contentElement.querySelector("#source-name-text-input")?.focus(),
0
);

validateInput();

if (!(await dialog.run())) {
// User cancelled
return {};
}

return {
key: nameController.model.customDataKey,
value: nameController.model.customDataValue,
};
}

_customDataPropertiesContentElement(nameController, supportedAttributes) {
const warningElement = html.div({
id: "warning-text",
style: `grid-column: 1 / -1; min-height: 1.5em;`,
});

const infoElement = html.div({
id: "info-text",
style: `grid-column: 1 / -1; min-height: 1.5em;`,
});

const contentElement = html.div(
{
style: `overflow: hidden;
white-space: nowrap;
display: grid;
gap: 0.5em;
grid-template-columns: max-content auto;
align-items: center;
height: 100%;
min-height: 0;
`,
},
[
...labeledTextInput(translate("Key"), nameController, "customDataKey", {
placeholderKey: "suggestedCustomDataKey",
choices: supportedAttributes,
}),
...labeledTextInput(translate("Value"), nameController, "customDataValue", {
placeholderKey: "suggestedCustomDataValue",
}),
html.br(),
infoElement,
warningElement,
]
);
return { contentElement, warningElement, infoElement };
}
}

customElements.define("custom-data-list", CustomDataList);
Loading

0 comments on commit aa43661

Please sign in to comment.