sysmon_pytk.font_utils
Font utilities.
1# SPDX-FileCopyrightText: © 2024 Stacey Adams <stacey.belle.rose@gmail.com> 2# SPDX-License-Identifier: MIT 3 4""" 5Font utilities. 6""" 7 8from __future__ import annotations 9 10import dataclasses 11from enum import Enum 12from tkinter import font 13from tkinter.font import Font 14from typing import Literal, TypeVar 15 16from typing_extensions import Self, TypeAlias 17 18from .app_locale import get_translator 19 20_ = get_translator() 21 22MAIN_FONT_FAMILY = "Source Sans Pro" 23FIXED_FONT_FAMILY = "Source Code Pro" 24 25T = TypeVar("T") 26 27FontWeight: TypeAlias = Literal["normal", "bold"] 28"""Font weight, one of `normal`, `bold`. For use with tk.Font.""" 29 30FontSlant: TypeAlias = Literal["roman", "italic"] 31"""Font slant, one of `roman`, `italic`. For use with tk.Font.""" 32 33 34class FontStyle(Enum): 35 """ 36 Font style. 37 """ 38 39 REGULAR = "r" 40 """Font style: Regular.""" 41 42 BOLD = "b" 43 """Font style: Bold.""" 44 45 ITALIC = "i" 46 """Font style: Italic.""" 47 48 BOLD_ITALIC = "bi" 49 """Font style: Bold Italic.""" 50 51 @classmethod 52 def _missing_(cls, value: object) -> Self | None: 53 for member in cls: 54 if member.value == value: 55 return member 56 return None 57 58 def is_bold(self) -> bool: 59 """ 60 Determine whether the font style is bold. 61 """ 62 return "b" in self.value 63 64 def is_italic(self) -> bool: 65 """ 66 Determine whether the font style is italc. 67 """ 68 return "i" in self.value 69 70 def get_weight(self) -> FontWeight: 71 """ 72 Get the weight of the font style, either `normal` or `bold`. 73 """ 74 return "bold" if self.is_bold() else "normal" 75 76 def get_slant(self) -> FontSlant: 77 """ 78 Get the slant of the font style, either `roman` or `italic`. 79 """ 80 return "italic" if self.is_italic() else "roman" 81 82 def to_weight_and_slant(self) -> tuple[FontWeight, FontSlant]: 83 """ 84 Convert the font style to a weight and slant, for use with tk.Font. 85 """ 86 weight: FontWeight = "normal" 87 slant: FontSlant = "roman" 88 if self == FontStyle.BOLD: 89 weight = "bold" 90 elif self == FontStyle.ITALIC: 91 slant = "italic" 92 elif self == FontStyle.BOLD_ITALIC: 93 weight = "bold" 94 slant = "italic" 95 return (weight, slant) 96 97 @classmethod 98 def from_weight_and_slant(cls, weight: FontWeight, slant: FontSlant) -> FontStyle: 99 """ 100 Given a weight and slant, return a FontStyle. 101 102 Parameters 103 ---------- 104 weight : FontWeight 105 Font weight, one of `normal`, `bold`. 106 slant : FontSlant 107 Font slant, one of `roman`, `italic`. 108 """ 109 if weight == "bold": 110 if slant == "italic": 111 return cls.BOLD_ITALIC 112 return cls.BOLD 113 if slant == "italic": 114 return cls.ITALIC 115 return cls.REGULAR 116 117 118@dataclasses.dataclass 119class FontDescription: 120 """ 121 Font data, like what is returned by the `actual` method of a `Font` object. 122 """ 123 124 family: str 125 """The font family name.""" 126 size: int 127 """The font size.""" 128 weight: FontWeight 129 """The font weight.""" 130 slant: FontSlant 131 """The font slant.""" 132 underline: bool 133 """Whether the font uses underline.""" 134 overstrike: bool 135 """Whether the font uses strikethrough.""" 136 137 def get_font(self) -> Font: 138 """ 139 Return a Tk font based on this object's data. 140 """ 141 return Font( 142 family=self.family, size=self.size, weight=self.weight, 143 slant=self.slant, underline=self.underline, overstrike=self.overstrike 144 ) 145 146 def get_string(self) -> str: 147 """ 148 Get the string which describes the font. 149 """ 150 style = self.get_style() 151 style_text = "" 152 if style == FontStyle.BOLD: 153 style_text = _("Bold") 154 elif style == FontStyle.ITALIC: 155 style_text = _("Italic") 156 elif style == FontStyle.BOLD_ITALIC: 157 style_text = _("Bold Italic") 158 effects = self.get_effects() 159 return " ".join(f"{self.family} {style_text} {effects} {self.size}".split()) 160 161 def get_style(self) -> FontStyle: 162 """ 163 Get the font style, based on its slant and weight. 164 """ 165 return FontStyle.from_weight_and_slant(self.weight, self.slant) 166 167 def get_effects(self) -> str: 168 """ 169 Get the font effects (underline/strikethrough). 170 """ 171 underline = _("Underline") if self.underline else "" 172 overstrike = _("Overstrike") if self.overstrike else "" 173 return f"{underline} {overstrike}".strip() 174 175 176def modify_named_font( # pylint: disable=too-many-arguments 177 font_name: str, *, 178 size: int | None = None, 179 weight: FontWeight | None = None, 180 slant: FontSlant | None = None, 181 underline: bool | None = None, 182 overstrike: bool | None = None 183) -> Font: 184 """ 185 Modify a named font by optionally changing weight, size, slant, etc. 186 187 Parameters 188 ---------- 189 font_name : str 190 The name of the font to use 191 size : int, optional 192 The font size to use 193 weight : FontWeight, optional 194 The font weight to use 195 slant : FontSlant, optional 196 The font slant to use 197 underline : bool, optional 198 Whether the font should be underlined 199 overstrike : bool, optional 200 Whether the font should have strikethrough 201 202 Returns 203 ------- 204 Font 205 A new font, with the parameters given. 206 207 Example 208 ------- 209 >>> modify_named_font("TkDefaultFont", size=13, weight="bold").actual() 210 { 'family': 'Bitstream Vera Sans', 211 'size': 13, 212 'weight': 'bold', 213 'slant': 'roman', 214 'underline': 0, 215 'overstrike': 0 } 216 """ 217 if font_name in font.names(): 218 fnt = font.nametofont(font_name).actual() 219 return Font( 220 family=fnt["family"], 221 size=get_with_fallback(size, fnt["size"]), 222 weight=get_with_fallback(weight, fnt["weight"]), 223 slant=get_with_fallback(slant, fnt["slant"]), 224 underline=get_with_fallback(underline, fnt["underline"]), 225 overstrike=get_with_fallback(overstrike, fnt["overstrike"]) 226 ) 227 return Font(name="TkDefaultFont") 228 229 230def get_with_fallback(value: T | None, fallback: T) -> T: 231 """ 232 Return the value provided unless it is None. In that case, return the fallback. 233 234 Parameters 235 ---------- 236 value : Optional[T] 237 Any value, possibly None 238 fallback : T 239 A fallback value, cannot be None 240 241 Returns 242 ------- 243 T 244 A value that is not None 245 """ 246 return value if value is not None else fallback
Font weight, one of normal, bold. For use with tk.Font.
Font slant, one of roman, italic. For use with tk.Font.
35class FontStyle(Enum): 36 """ 37 Font style. 38 """ 39 40 REGULAR = "r" 41 """Font style: Regular.""" 42 43 BOLD = "b" 44 """Font style: Bold.""" 45 46 ITALIC = "i" 47 """Font style: Italic.""" 48 49 BOLD_ITALIC = "bi" 50 """Font style: Bold Italic.""" 51 52 @classmethod 53 def _missing_(cls, value: object) -> Self | None: 54 for member in cls: 55 if member.value == value: 56 return member 57 return None 58 59 def is_bold(self) -> bool: 60 """ 61 Determine whether the font style is bold. 62 """ 63 return "b" in self.value 64 65 def is_italic(self) -> bool: 66 """ 67 Determine whether the font style is italc. 68 """ 69 return "i" in self.value 70 71 def get_weight(self) -> FontWeight: 72 """ 73 Get the weight of the font style, either `normal` or `bold`. 74 """ 75 return "bold" if self.is_bold() else "normal" 76 77 def get_slant(self) -> FontSlant: 78 """ 79 Get the slant of the font style, either `roman` or `italic`. 80 """ 81 return "italic" if self.is_italic() else "roman" 82 83 def to_weight_and_slant(self) -> tuple[FontWeight, FontSlant]: 84 """ 85 Convert the font style to a weight and slant, for use with tk.Font. 86 """ 87 weight: FontWeight = "normal" 88 slant: FontSlant = "roman" 89 if self == FontStyle.BOLD: 90 weight = "bold" 91 elif self == FontStyle.ITALIC: 92 slant = "italic" 93 elif self == FontStyle.BOLD_ITALIC: 94 weight = "bold" 95 slant = "italic" 96 return (weight, slant) 97 98 @classmethod 99 def from_weight_and_slant(cls, weight: FontWeight, slant: FontSlant) -> FontStyle: 100 """ 101 Given a weight and slant, return a FontStyle. 102 103 Parameters 104 ---------- 105 weight : FontWeight 106 Font weight, one of `normal`, `bold`. 107 slant : FontSlant 108 Font slant, one of `roman`, `italic`. 109 """ 110 if weight == "bold": 111 if slant == "italic": 112 return cls.BOLD_ITALIC 113 return cls.BOLD 114 if slant == "italic": 115 return cls.ITALIC 116 return cls.REGULAR
Font style.
59 def is_bold(self) -> bool: 60 """ 61 Determine whether the font style is bold. 62 """ 63 return "b" in self.value
Determine whether the font style is bold.
65 def is_italic(self) -> bool: 66 """ 67 Determine whether the font style is italc. 68 """ 69 return "i" in self.value
Determine whether the font style is italc.
71 def get_weight(self) -> FontWeight: 72 """ 73 Get the weight of the font style, either `normal` or `bold`. 74 """ 75 return "bold" if self.is_bold() else "normal"
Get the weight of the font style, either normal or bold.
77 def get_slant(self) -> FontSlant: 78 """ 79 Get the slant of the font style, either `roman` or `italic`. 80 """ 81 return "italic" if self.is_italic() else "roman"
Get the slant of the font style, either roman or italic.
83 def to_weight_and_slant(self) -> tuple[FontWeight, FontSlant]: 84 """ 85 Convert the font style to a weight and slant, for use with tk.Font. 86 """ 87 weight: FontWeight = "normal" 88 slant: FontSlant = "roman" 89 if self == FontStyle.BOLD: 90 weight = "bold" 91 elif self == FontStyle.ITALIC: 92 slant = "italic" 93 elif self == FontStyle.BOLD_ITALIC: 94 weight = "bold" 95 slant = "italic" 96 return (weight, slant)
Convert the font style to a weight and slant, for use with tk.Font.
98 @classmethod 99 def from_weight_and_slant(cls, weight: FontWeight, slant: FontSlant) -> FontStyle: 100 """ 101 Given a weight and slant, return a FontStyle. 102 103 Parameters 104 ---------- 105 weight : FontWeight 106 Font weight, one of `normal`, `bold`. 107 slant : FontSlant 108 Font slant, one of `roman`, `italic`. 109 """ 110 if weight == "bold": 111 if slant == "italic": 112 return cls.BOLD_ITALIC 113 return cls.BOLD 114 if slant == "italic": 115 return cls.ITALIC 116 return cls.REGULAR
Given a weight and slant, return a FontStyle.
Parameters
- weight (FontWeight):
Font weight, one of
normal,bold. - slant (FontSlant):
Font slant, one of
roman,italic.
Inherited Members
- enum.Enum
- name
- value
119@dataclasses.dataclass 120class FontDescription: 121 """ 122 Font data, like what is returned by the `actual` method of a `Font` object. 123 """ 124 125 family: str 126 """The font family name.""" 127 size: int 128 """The font size.""" 129 weight: FontWeight 130 """The font weight.""" 131 slant: FontSlant 132 """The font slant.""" 133 underline: bool 134 """Whether the font uses underline.""" 135 overstrike: bool 136 """Whether the font uses strikethrough.""" 137 138 def get_font(self) -> Font: 139 """ 140 Return a Tk font based on this object's data. 141 """ 142 return Font( 143 family=self.family, size=self.size, weight=self.weight, 144 slant=self.slant, underline=self.underline, overstrike=self.overstrike 145 ) 146 147 def get_string(self) -> str: 148 """ 149 Get the string which describes the font. 150 """ 151 style = self.get_style() 152 style_text = "" 153 if style == FontStyle.BOLD: 154 style_text = _("Bold") 155 elif style == FontStyle.ITALIC: 156 style_text = _("Italic") 157 elif style == FontStyle.BOLD_ITALIC: 158 style_text = _("Bold Italic") 159 effects = self.get_effects() 160 return " ".join(f"{self.family} {style_text} {effects} {self.size}".split()) 161 162 def get_style(self) -> FontStyle: 163 """ 164 Get the font style, based on its slant and weight. 165 """ 166 return FontStyle.from_weight_and_slant(self.weight, self.slant) 167 168 def get_effects(self) -> str: 169 """ 170 Get the font effects (underline/strikethrough). 171 """ 172 underline = _("Underline") if self.underline else "" 173 overstrike = _("Overstrike") if self.overstrike else "" 174 return f"{underline} {overstrike}".strip()
Font data, like what is returned by the actual method of a Font object.
138 def get_font(self) -> Font: 139 """ 140 Return a Tk font based on this object's data. 141 """ 142 return Font( 143 family=self.family, size=self.size, weight=self.weight, 144 slant=self.slant, underline=self.underline, overstrike=self.overstrike 145 )
Return a Tk font based on this object's data.
147 def get_string(self) -> str: 148 """ 149 Get the string which describes the font. 150 """ 151 style = self.get_style() 152 style_text = "" 153 if style == FontStyle.BOLD: 154 style_text = _("Bold") 155 elif style == FontStyle.ITALIC: 156 style_text = _("Italic") 157 elif style == FontStyle.BOLD_ITALIC: 158 style_text = _("Bold Italic") 159 effects = self.get_effects() 160 return " ".join(f"{self.family} {style_text} {effects} {self.size}".split())
Get the string which describes the font.
162 def get_style(self) -> FontStyle: 163 """ 164 Get the font style, based on its slant and weight. 165 """ 166 return FontStyle.from_weight_and_slant(self.weight, self.slant)
Get the font style, based on its slant and weight.
168 def get_effects(self) -> str: 169 """ 170 Get the font effects (underline/strikethrough). 171 """ 172 underline = _("Underline") if self.underline else "" 173 overstrike = _("Overstrike") if self.overstrike else "" 174 return f"{underline} {overstrike}".strip()
Get the font effects (underline/strikethrough).
177def modify_named_font( # pylint: disable=too-many-arguments 178 font_name: str, *, 179 size: int | None = None, 180 weight: FontWeight | None = None, 181 slant: FontSlant | None = None, 182 underline: bool | None = None, 183 overstrike: bool | None = None 184) -> Font: 185 """ 186 Modify a named font by optionally changing weight, size, slant, etc. 187 188 Parameters 189 ---------- 190 font_name : str 191 The name of the font to use 192 size : int, optional 193 The font size to use 194 weight : FontWeight, optional 195 The font weight to use 196 slant : FontSlant, optional 197 The font slant to use 198 underline : bool, optional 199 Whether the font should be underlined 200 overstrike : bool, optional 201 Whether the font should have strikethrough 202 203 Returns 204 ------- 205 Font 206 A new font, with the parameters given. 207 208 Example 209 ------- 210 >>> modify_named_font("TkDefaultFont", size=13, weight="bold").actual() 211 { 'family': 'Bitstream Vera Sans', 212 'size': 13, 213 'weight': 'bold', 214 'slant': 'roman', 215 'underline': 0, 216 'overstrike': 0 } 217 """ 218 if font_name in font.names(): 219 fnt = font.nametofont(font_name).actual() 220 return Font( 221 family=fnt["family"], 222 size=get_with_fallback(size, fnt["size"]), 223 weight=get_with_fallback(weight, fnt["weight"]), 224 slant=get_with_fallback(slant, fnt["slant"]), 225 underline=get_with_fallback(underline, fnt["underline"]), 226 overstrike=get_with_fallback(overstrike, fnt["overstrike"]) 227 ) 228 return Font(name="TkDefaultFont")
Modify a named font by optionally changing weight, size, slant, etc.
Parameters
- font_name (str): The name of the font to use
- size (int, optional): The font size to use
- weight (FontWeight, optional): The font weight to use
- slant (FontSlant, optional): The font slant to use
- underline (bool, optional): Whether the font should be underlined
- overstrike (bool, optional): Whether the font should have strikethrough
Returns
- Font: A new font, with the parameters given.
Example
>>> modify_named_font("TkDefaultFont", size=13, weight="bold").actual()
{ 'family': 'Bitstream Vera Sans',
'size': 13,
'weight': 'bold',
'slant': 'roman',
'underline': 0,
'overstrike': 0 }
231def get_with_fallback(value: T | None, fallback: T) -> T: 232 """ 233 Return the value provided unless it is None. In that case, return the fallback. 234 235 Parameters 236 ---------- 237 value : Optional[T] 238 Any value, possibly None 239 fallback : T 240 A fallback value, cannot be None 241 242 Returns 243 ------- 244 T 245 A value that is not None 246 """ 247 return value if value is not None else fallback
Return the value provided unless it is None. In that case, return the fallback.
Parameters
- value (Optional[T]): Any value, possibly None
- fallback (T): A fallback value, cannot be None
Returns
- T: A value that is not None