1import textwrap 2import tkinter as tk 3from tkinter import font as tk_font 4from tkinter import ttk 5 6from thonny import get_workbench, tktextext, ui_utils 7from thonny.codeview import CodeView 8from thonny.config_ui import ConfigurationPage 9from thonny.languages import tr 10from thonny.shell import BaseShellText 11from thonny.ui_utils import create_string_var, scrollbar_style 12 13 14class ThemeAndFontConfigurationPage(ConfigurationPage): 15 def __init__(self, master): 16 17 ConfigurationPage.__init__(self, master) 18 19 self._init_themes() 20 self._init_fonts() 21 self._init_previews() 22 23 self.columnconfigure(2, weight=1) 24 self.columnconfigure(4, weight=1) 25 26 self.rowconfigure(31, weight=1) 27 self.rowconfigure(21, weight=1) 28 29 def _init_themes(self): 30 self._original_ui_theme = get_workbench().get_option("view.ui_theme") 31 self._original_syntax_theme = get_workbench().get_option("view.syntax_theme") 32 33 self._ui_theme_variable = create_string_var( 34 self._original_ui_theme, modification_listener=self._update_appearance 35 ) 36 self._syntax_theme_variable = create_string_var( 37 self._original_syntax_theme, modification_listener=self._update_appearance 38 ) 39 40 ttk.Label(self, text=tr("UI theme")).grid( 41 row=1, column=1, sticky="w", pady=(0, 10), padx=(0, 5) 42 ) 43 self._ui_theme_combo = ttk.Combobox( 44 self, 45 exportselection=False, 46 textvariable=self._ui_theme_variable, 47 state="readonly", 48 height=15, 49 values=get_workbench().get_usable_ui_theme_names(), 50 ) 51 self._ui_theme_combo.grid(row=1, column=2, sticky="nwe", pady=(0, 5)) 52 53 ttk.Label(self, text=tr("Syntax theme")).grid( 54 row=2, column=1, sticky="w", pady=(0, 10), padx=(0, 5) 55 ) 56 self._syntax_theme_combo = ttk.Combobox( 57 self, 58 exportselection=False, 59 textvariable=self._syntax_theme_variable, 60 state="readonly", 61 height=15, 62 values=get_workbench().get_syntax_theme_names(), 63 ) 64 self._syntax_theme_combo.grid(row=2, column=2, sticky="nwe", pady=(0, 5)) 65 66 def _init_fonts(self): 67 self._original_editor_family = get_workbench().get_option("view.editor_font_family") 68 self._original_editor_size = get_workbench().get_option("view.editor_font_size") 69 self._original_io_family = get_workbench().get_option("view.io_font_family") 70 self._original_io_size = get_workbench().get_option("view.io_font_size") 71 72 self._editor_family_variable = create_string_var( 73 self._original_editor_family, modification_listener=self._update_appearance 74 ) 75 self._editor_size_variable = create_string_var( 76 self._original_editor_size, modification_listener=self._update_appearance 77 ) 78 self._io_family_variable = create_string_var( 79 self._original_io_family, modification_listener=self._update_appearance 80 ) 81 self._io_size_variable = create_string_var( 82 self._original_io_size, modification_listener=self._update_appearance 83 ) 84 85 ttk.Label(self, text=tr("Editor font")).grid( 86 row=1, column=3, sticky="w", pady=(0, 5), padx=(25, 5) 87 ) 88 editor_family_combo = ttk.Combobox( 89 self, 90 exportselection=False, 91 state="readonly", 92 height=15, 93 textvariable=self._editor_family_variable, 94 values=self._get_families_to_show(), 95 ) 96 editor_family_combo.grid(row=1, column=4, sticky="nwe", pady=(0, 5)) 97 editor_size_combo = ttk.Combobox( 98 self, 99 width=4, 100 exportselection=False, 101 textvariable=self._editor_size_variable, 102 state="readonly", 103 height=15, 104 values=[str(x) for x in range(3, 73)], 105 ) 106 editor_size_combo.grid(row=1, column=5, sticky="nwe", pady=(0, 5), padx=(5, 0)) 107 108 ttk.Label(self, text=tr("IO font")).grid( 109 row=2, column=3, sticky="w", pady=(0, 5), padx=(25, 5) 110 ) 111 io_family_combo = ttk.Combobox( 112 self, 113 exportselection=False, 114 state="readonly", 115 height=15, 116 textvariable=self._io_family_variable, 117 values=self._get_families_to_show(), 118 ) 119 io_family_combo.grid(row=2, column=4, sticky="nwe", pady=(0, 5)) 120 121 io_size_combo = ttk.Combobox( 122 self, 123 width=4, 124 exportselection=False, 125 textvariable=self._io_size_variable, 126 state="readonly", 127 height=15, 128 values=[str(x) for x in range(3, 73)], 129 ) 130 io_size_combo.grid(row=2, column=5, sticky="nwe", pady=(0, 5), padx=(5, 0)) 131 132 def _init_previews(self): 133 ttk.Label(self, text=tr("Preview")).grid( 134 row=20, column=1, sticky="w", pady=(5, 2), columnspan=5 135 ) 136 self._preview_codeview = CodeView( 137 self, height=6, font="EditorFont", relief="groove", borderwidth=1, line_numbers=True 138 ) 139 140 self._preview_codeview.set_content( 141 textwrap.dedent( 142 """ 143 def foo(bar): 144 if bar is None: # """ 145 + tr("This is a comment") 146 + """ 147 print('""" 148 + tr("The answer is") 149 + """', 33) 150 151 """ 152 + tr("unclosed_string") 153 + ''' = "''' 154 + tr("blah, blah") 155 + "\n" 156 ).strip() 157 ) 158 self._preview_codeview.grid(row=21, column=1, columnspan=5, sticky=tk.NSEW) 159 160 self._shell_preview = tktextext.TextFrame( 161 self, 162 text_class=BaseShellText, 163 height=4, 164 vertical_scrollbar_style=scrollbar_style("Vertical"), 165 horizontal_scrollbar_style=scrollbar_style("Horizontal"), 166 horizontal_scrollbar_class=ui_utils.AutoScrollbar, 167 relief="groove", 168 borderwidth=1, 169 font="EditorFont", 170 ) 171 self._shell_preview.grid(row=31, column=1, columnspan=5, sticky=tk.NSEW, pady=(5, 5)) 172 self._shell_preview.text.set_read_only(True) 173 self._insert_shell_text() 174 175 ttk.Label( 176 self, 177 text=tr("NB! Some style elements change only after restarting Thonny!"), 178 font="BoldTkDefaultFont", 179 ).grid(row=40, column=1, columnspan=5, sticky="w", pady=(5, 0)) 180 181 def apply(self): 182 # don't do anything, as preview already did the thing 183 return 184 185 def cancel(self): 186 if ( 187 getattr(self._editor_family_variable, "modified") 188 or getattr(self._editor_size_variable, "modified") 189 or getattr(self._ui_theme_variable, "modified") 190 or getattr(self._syntax_theme_variable, "modified") 191 ): 192 get_workbench().set_option("view.ui_theme", self._original_ui_theme) 193 get_workbench().set_option("view.syntax_theme", self._original_syntax_theme) 194 get_workbench().set_option("view.editor_font_size", self._original_editor_size) 195 get_workbench().set_option("view.editor_font_family", self._original_editor_family) 196 get_workbench().set_option("view.io_font_size", self._original_io_size) 197 get_workbench().set_option("view.io_font_family", self._original_io_family) 198 get_workbench().reload_themes() 199 get_workbench().update_fonts() 200 201 def _update_appearance(self): 202 get_workbench().set_option("view.ui_theme", self._ui_theme_variable.get()) 203 get_workbench().set_option("view.syntax_theme", self._syntax_theme_variable.get()) 204 get_workbench().set_option("view.editor_font_size", int(self._editor_size_variable.get())) 205 get_workbench().set_option("view.editor_font_family", self._editor_family_variable.get()) 206 get_workbench().set_option("view.io_font_size", int(self._io_size_variable.get())) 207 get_workbench().set_option("view.io_font_family", self._io_family_variable.get()) 208 get_workbench().reload_themes() 209 get_workbench().update_fonts() 210 211 def _insert_shell_text(self): 212 text = self._shell_preview.text 213 text._insert_prompt() 214 text.direct_insert("end", "%Run demo.py\n", ("magic", "before_io")) 215 text.tag_add("before_io", "1.0", "1.0 lineend") 216 text.direct_insert("end", tr("Enter an integer") + ": ", ("io", "stdout")) 217 text.direct_insert("end", "2.5\n", ("io", "stdin")) 218 text.direct_insert( 219 "end", "ValueError: invalid literal for int() with base 10: '2.5'\n", ("io", "stderr") 220 ) 221 222 def _get_families_to_show(self): 223 # In Linux, families may contain duplicates (actually different fonts get same names) 224 return sorted(set(filter(lambda name: name[0].isalpha(), tk_font.families()))) 225 226 227def load_plugin() -> None: 228 get_workbench().add_configuration_page( 229 "theme", tr("Theme & Font"), ThemeAndFontConfigurationPage, 40 230 ) 231