From ec1047207c1ff19334aca2fe685aaf8184fc05ec Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 10 Jan 2025 17:39:20 +0100 Subject: [PATCH] Restructure how icon themes are generated --- pkgutils.py | 16 ++- utils/icon_themes.py | 270 ++++++++++++++++++++++++++++++++++++ utils/material_icons.py | 186 ------------------------- utils/material_symbols.json | 105 ++++++++++++++ 4 files changed, 387 insertions(+), 190 deletions(-) create mode 100644 utils/icon_themes.py delete mode 100644 utils/material_icons.py create mode 100644 utils/material_symbols.json diff --git a/pkgutils.py b/pkgutils.py index 060a1220..aa4ddad6 100755 --- a/pkgutils.py +++ b/pkgutils.py @@ -35,7 +35,7 @@ import zipfile from pathlib import Path -from utils.material_icons import processMaterialIcons +from utils.icon_themes import processFontAwesome, processMaterialIcons CURR_DIR = Path(__file__).parent SETUP_DIR = CURR_DIR / "setup" @@ -330,10 +330,10 @@ def buildSampleZip(args: argparse.Namespace | None = None) -> None: ## def buildIconTheme(args: argparse.Namespace) -> None: - """Build an icon theme.""" + """Build icon themes.""" print("") - print("Build Icon Theme") - print("================") + print("Build Icon Themes") + print("=================") print("") workDir = Path(args.sources).absolute() @@ -384,6 +384,14 @@ def buildIconTheme(args: argparse.Namespace) -> None: }, }) + if style in ("all", "fa"): + processFontAwesome(workDir, iconsDir, { + "font_awesome": { + "name": "Font Awesome 6", + }, + }) + + print("Done") print("") return diff --git a/utils/icon_themes.py b/utils/icon_themes.py new file mode 100644 index 00000000..1371debd --- /dev/null +++ b/utils/icon_themes.py @@ -0,0 +1,270 @@ +""" +novelWriter – Icon Theme Utils +============================== + +This file is a part of novelWriter +Copyright (C) 2025 Veronica Berglyd Olsen and novelWriter contributors + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" +from __future__ import annotations + +import json +import subprocess + +from pathlib import Path +from xml.etree import ElementTree as ET + +UTILS = Path(__file__).parent + +ET.register_namespace("", "http://www.w3.org/2000/svg") +MATERIAL_REPO = "https://github.com/google/material-design-icons.git" +FONT_AWESOME_REPO = "https://github.com/FortAwesome/Font-Awesome.git" +ICONS = [ + "alert_error", + "alert_info", + "alert_question", + "alert_warn", + + "cls_archive", + "cls_character", + "cls_custom", + "cls_entity", + "cls_none", + "cls_novel", + "cls_object", + "cls_plot", + "cls_template", + "cls_timeline", + "cls_trash", + "cls_world", + + "prj_folder", + "prj_document", + "prj_title", + "prj_chapter", + "prj_scene", + "prj_note", + + "fmt_bold", + "fmt_italic", + "fmt_mark", + "fmt_strike", + "fmt_subscript", + "fmt_superscript", + "fmt_underline", + "fmt_toolbar", + + "search", + "search_cancel", + "search_case", + "search_loop", + "search_preserve", + "search_project", + "search_regex", + "search_replace", + "search_word", + + "bullet-off", + "bullet-on", + "unfold-hide", + "unfold-show", + + "add", + "bookmarks", + "browse", + "cancel", + "checked", + "chevron_down", + "chevron_left", + "chevron_right", + "chevron_up", + "close", + "copy", + "document_add", + "document", + "edit", + "exclude", + "export", + "filter", + "fit_height", + "fit_width", + "folder", + "font", + "import", + "language", + "lines", + "list", + "manuscript", + "margin_bottom", + "margin_left", + "margin_right", + "margin_top", + "maximise", + "minimise", + "more_arrow", + "more_vertical", + "noncheckable", + "novel_view", + "open", + "outline", + "panel", + "pin", + "project_copy", + "project_view", + "quote", + "refresh", + "remove", + "revert", + "settings", + "star", + "stats", + "text", + "timer_off", + "timer", + "unchecked", + "view", +] + + +def _loadMap(name: str) -> dict[str, str]: + """Load a theme map file.""" + data = json.loads((UTILS / f"{name}.json").read_text(encoding="utf-8")) + icons = {} + for key in ICONS: + if icon := data.get(key, ""): + icons[key] = icon + else: + print(f"- Missing: {key}") + return icons + + +def _fixXml(svg: ET.Element) -> str: + """Clean up the SVG XML and add needed fields.""" + svg.set("fill", "#000000") + svg.set("height", "128") + svg.set("width", "128") + return ET.tostring(svg).decode() + + +def _writeThemeFile( + path: Path, name: str, author: str, license: str, icons: dict[str, ET.Element] +) -> None: + """Write an icon theme file.""" + with open(path.with_suffix(".icons"), mode="w", encoding="utf-8") as out: + out.write("# This file is automatically generated. Do not edit.\n\n") + out.write("# Meta\n") + out.write(f"meta:name = {name}\n") + out.write(f"meta:author = {author}\n") + out.write(f"meta:license = {license}\n") + out.write("\n") + out.write("# Icons\n") + for key, svg in icons.items(): + out.write(f"icon:{key:<15s} = {_fixXml(svg)}\n") + print(f"- Wrote: {len(icons)} icons") + print(f"- Target: {path.relative_to(UTILS.parent)}") + return + + +def _cloneRepo(repoPath: Path, repoUrl: str) -> None: + """Clone or update a local repo of icons.""" + print(f"Updating: {repoUrl}") + if not repoPath.is_dir(): + subprocess.call(["git", "clone", repoUrl, "--depth", "50"], cwd=repoPath.parent) + else: + subprocess.call(["git", "pull"], cwd=repoPath) + print("") + return + + +def processMaterialIcons(workDir: Path, iconsDir: Path, jobs: dict) -> None: + """Process material icons of a given spec and write output file.""" + srcRepo = workDir / "material-design-icons" + _cloneRepo(srcRepo, MATERIAL_REPO) + + for file, job in jobs.items(): + name: str = job["name"] + style: str = job["style"] + filled: bool = job["filled"] + weight: int = job["weight"] + + kind = f"wght{weight}" if weight != 400 else "" + kind += "fill1" if filled else "" + + print(f"Processing: {name}") + + icons: dict[str, ET.Element] = {} + iconSrc = srcRepo / "symbols" / "web" + for key, icon in _loadMap("material_symbols").items(): + if kind: + fileNmae = f"{icon}_{kind}_24px.svg" + else: + fileNmae = f"{icon}_24px.svg" + iconFile = iconSrc / icon / f"materialsymbols{style}" / fileNmae + if iconFile.is_file(): + icons[key] = ET.fromstring(iconFile.read_text(encoding="utf-8")) + else: + print(f"Not Found: {iconFile}") + + target = iconsDir / f"{file}.icons" + _writeThemeFile(target, name, "Google Inc", "Apache 2.0", icons) + + print("") + + return + + +def processFontAwesome(workDir: Path, iconsDir: Path, jobs: dict) -> None: + """Process Font Awesome icons of a given spec and write output file.""" + srcRepo = workDir / "Font-Awesome" + _cloneRepo(srcRepo, FONT_AWESOME_REPO) + + for file, job in jobs.items(): + name: str = job["name"] + print(f"Processing: {name}") + + icons: dict[str, ET.Element] = {} + iconSrc = srcRepo / "svgs" + for key, value in _loadMap("font_awesome").items(): + icon, _, forced = value.partition(":") + iconSolid = iconSrc / "solid" / f"{icon}.svg" + iconRegular = iconSrc / "regular" / f"{icon}.svg" + if forced == "regular": + iconFile = iconRegular + elif forced == "solid": + iconFile = iconSolid + elif iconSolid.is_file(): + iconFile = iconSolid + elif iconRegular.is_file(): + iconFile = iconRegular + else: + print(f"Not Found: {icon}.svg") + continue + + if iconFile.is_file(): + svg = ET.fromstring(iconFile.read_text(encoding="utf-8")) + viewbox = [int(x) for x in svg.get("viewBox", "").split()] + viewbox = [viewbox[2]//2 - 256, 0, 512, 512] + svg.set("viewBox", " ".join(str(x) for x in viewbox)) + icons[key] = svg + else: + print(f"Not Found: {icon}.svg") + continue + + target = iconsDir / f"{file}.icons" + _writeThemeFile(target, name, "Fonticons Inc", "Font Awesome Free License", icons) + + print("") + + return diff --git a/utils/material_icons.py b/utils/material_icons.py deleted file mode 100644 index b57219d2..00000000 --- a/utils/material_icons.py +++ /dev/null @@ -1,186 +0,0 @@ -""" -novelWriter – Material Icon Theme -================================= - -This file is a part of novelWriter -Copyright (C) 2025 Veronica Berglyd Olsen and novelWriter contributors - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, but -WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -""" -from __future__ import annotations - -import subprocess - -from pathlib import Path -from xml.etree import ElementTree as ET - -ET.register_namespace("", "http://www.w3.org/2000/svg") -MATERIAL_REPO = "https://github.com/google/material-design-icons.git" -ICON_MAP = { - "alert_error": "error", - "alert_info": "info", - "alert_question": "help", - "alert_warn": "warning", - - "cls_archive": "archive", - "cls_character": "group", - "cls_custom": "label", - "cls_entity": "apartment", - "cls_none": "close", - "cls_novel": "book_2", - "cls_object": "key", - "cls_plot": "extension", - "cls_template": "topic", - "cls_timeline": "hourglass_empty", - "cls_trash": "delete", - "cls_world": "globe", - - "prj_folder": "folder", - "prj_document": "news", - "prj_title": "news", - "prj_chapter": "news", - "prj_scene": "news", - "prj_note": "sticky_note_2", - - "fmt_bold": "format_bold", - "fmt_italic": "format_italic", - "fmt_mark": "format_ink_highlighter", - "fmt_strike": "strikethrough_s", - "fmt_subscript": "subscript", - "fmt_superscript": "superscript", - "fmt_underline": "format_underlined", - "fmt_toolbar": "text_format", - - "search": "search", - "search_cancel": "close", - "search_case": "match_case", - "search_loop": "laps", - "search_preserve": "text_fields", - "search_project": "document_search", - "search_regex": "regular_expression", - "search_replace": "find_replace", - "search_word": "match_word", - - "bullet-off": "radio_button_unchecked", - "bullet-on": "radio_button_checked", - "unfold-hide": "arrow_right", - "unfold-show": "arrow_drop_down", - - "add": "add", - "bookmarks": "bookmarks", - "browse": "folder_open", - "cancel": "cancel", - "checked": "check_box", - "chevron_down": "keyboard_arrow_down", - "chevron_left": "arrow_back_ios", - "chevron_right": "arrow_forward_ios", - "chevron_up": "keyboard_arrow_up", - "close": "close", - "copy": "content_copy", - "document_add": "note_add", - "document": "docs", - "edit": "edit", - "exclude": "do_not_disturb_on", - "export": "file_export", - "filter": "filter_alt", - "fit_height": "fit_page_height", - "fit_width": "fit_page_width", - "folder": "folder", - "font": "font_download", - "import": "tab_move", - "language": "translate", - "lines": "reorder", - "list": "format_list_bulleted", - "manuscript": "export_notes", - "margin_bottom": "vertical_align_bottom", - "margin_left": "keyboard_tab_rtl", - "margin_right": "keyboard_tab", - "margin_top": "vertical_align_top", - "maximise": "fullscreen", - "minimise": "fullscreen_exit", - "more_arrow": "arrow_right", - "more_vertical": "more_vert", - "noncheckable": "indeterminate_check_box", - "novel_view": "book_4_spark", - "open": "open_in_new", - "outline": "summarize", - "panel": "dock_to_bottom", - "pin": "keep", - "project_copy": "folder_copy", - "project_view": "bookmark_manager", - "quote": "format_quote", - "refresh": "refresh", - "remove": "remove", - "revert": "settings_backup_restore", - "settings": "settings", - "star": "star", - "stats": "bar_chart", - "text": "subject", - "timer_off": "timer_off", - "timer": "timer", - "unchecked": "disabled_by_default", - "view": "visibility", -} - - -def _fixXml(svg: str) -> str: - """Clean up the SVG XML and add needed fields.""" - xSvg = ET.fromstring(svg) - xSvg.set("fill", "#000000") - xSvg.set("height", "128") - xSvg.set("width", "128") - return ET.tostring(xSvg).decode() - - -def processMaterialIcons(workDir: Path, iconsDir: Path, jobs: dict) -> None: - """Process material icons of a given spec and write output file.""" - srcRepo = workDir / "material-design-icons" - if not srcRepo.is_dir(): - subprocess.call(["git", "clone", MATERIAL_REPO, "--depth", "50"], cwd=workDir) - else: - subprocess.call(["git", "pull"], cwd=srcRepo) - - for file, job in jobs.items(): - name: str = job["name"] - style: str = job["style"] - filled: bool = job["filled"] - weight: int = job["weight"] - - kind = f"wght{weight}" if weight != 400 else "" - kind += "fill1" if filled else "" - - print("") - print(f"Processing: {name}") - print("") - with open(iconsDir / f"{file}.icons", mode="w", encoding="utf-8") as icons: - icons.write("# This file is automatically generated. Do not edit.\n\n") - icons.write("# Meta\n") - icons.write(f"meta:name = {name}\n") - icons.write("meta:author = Google Inc\n") - icons.write("meta:license = Apache 2.0\n") - icons.write("\n") - icons.write("# Icons\n") - iconSrc = srcRepo / "symbols" / "web" - for key, icon in ICON_MAP.items(): - if kind: - fileNmae = f"{icon}_{kind}_24px.svg" - else: - fileNmae = f"{icon}_24px.svg" - iconFile = iconSrc / icon / f"materialsymbols{style}" / fileNmae - if iconFile.is_file(): - svg = iconFile.read_text(encoding="utf-8") - icons.write(f"icon:{key:<15s} = {_fixXml(svg)}\n") - print(f"Wrote: {iconFile.stem}") - else: - print(f"Not Found: {iconFile}") diff --git a/utils/material_symbols.json b/utils/material_symbols.json new file mode 100644 index 00000000..7abb8924 --- /dev/null +++ b/utils/material_symbols.json @@ -0,0 +1,105 @@ +{ + "alert_error": "error", + "alert_info": "info", + "alert_question": "help", + "alert_warn": "warning", + + "cls_archive": "archive", + "cls_character": "group", + "cls_custom": "label", + "cls_entity": "apartment", + "cls_none": "close", + "cls_novel": "book_2", + "cls_object": "key", + "cls_plot": "extension", + "cls_template": "topic", + "cls_timeline": "hourglass_empty", + "cls_trash": "delete", + "cls_world": "globe", + + "prj_folder": "folder", + "prj_document": "news", + "prj_title": "news", + "prj_chapter": "news", + "prj_scene": "news", + "prj_note": "sticky_note_2", + + "fmt_bold": "format_bold", + "fmt_italic": "format_italic", + "fmt_mark": "format_ink_highlighter", + "fmt_strike": "strikethrough_s", + "fmt_subscript": "subscript", + "fmt_superscript": "superscript", + "fmt_underline": "format_underlined", + "fmt_toolbar": "text_format", + + "search": "search", + "search_cancel": "close", + "search_case": "match_case", + "search_loop": "laps", + "search_preserve": "text_fields", + "search_project": "document_search", + "search_regex": "regular_expression", + "search_replace": "find_replace", + "search_word": "match_word", + + "bullet-off": "radio_button_unchecked", + "bullet-on": "radio_button_checked", + "unfold-hide": "arrow_right", + "unfold-show": "arrow_drop_down", + + "add": "add", + "bookmarks": "bookmarks", + "browse": "folder_open", + "cancel": "cancel", + "checked": "check_box", + "chevron_down": "keyboard_arrow_down", + "chevron_left": "arrow_back_ios", + "chevron_right": "arrow_forward_ios", + "chevron_up": "keyboard_arrow_up", + "close": "close", + "copy": "content_copy", + "document_add": "note_add", + "document": "docs", + "edit": "edit", + "exclude": "do_not_disturb_on", + "export": "file_export", + "filter": "filter_alt", + "fit_height": "fit_page_height", + "fit_width": "fit_page_width", + "folder": "folder", + "font": "font_download", + "import": "tab_move", + "language": "translate", + "lines": "reorder", + "list": "format_list_bulleted", + "manuscript": "export_notes", + "margin_bottom": "vertical_align_bottom", + "margin_left": "keyboard_tab_rtl", + "margin_right": "keyboard_tab", + "margin_top": "vertical_align_top", + "maximise": "fullscreen", + "minimise": "fullscreen_exit", + "more_arrow": "arrow_right", + "more_vertical": "more_vert", + "noncheckable": "indeterminate_check_box", + "novel_view": "book_4_spark", + "open": "open_in_new", + "outline": "summarize", + "panel": "dock_to_bottom", + "pin": "keep", + "project_copy": "folder_copy", + "project_view": "bookmark_manager", + "quote": "format_quote", + "refresh": "refresh", + "remove": "remove", + "revert": "settings_backup_restore", + "settings": "settings", + "star": "star", + "stats": "bar_chart", + "text": "subject", + "timer_off": "timer_off", + "timer": "timer", + "unchecked": "disabled_by_default", + "view": "visibility" +}