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"
+}