configdialog.py 103 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414
  1. """IDLE Configuration Dialog: support user customization of IDLE by GUI
  2. Customize font faces, sizes, and colorization attributes. Set indentation
  3. defaults. Customize keybindings. Colorization and keybindings can be
  4. saved as user defined sets. Select startup options including shell/editor
  5. and default window size. Define additional help sources.
  6. Note that tab width in IDLE is currently fixed at eight due to Tk issues.
  7. Refer to comments in EditorWindow autoindent code for details.
  8. """
  9. import re
  10. from tkinter import (Toplevel, Listbox, Canvas,
  11. StringVar, BooleanVar, IntVar, TRUE, FALSE,
  12. TOP, BOTTOM, RIGHT, LEFT, SOLID, GROOVE,
  13. NONE, BOTH, X, Y, W, E, EW, NS, NSEW, NW,
  14. HORIZONTAL, VERTICAL, ANCHOR, ACTIVE, END, TclError)
  15. from tkinter.ttk import (Frame, LabelFrame, Button, Checkbutton, Entry, Label,
  16. OptionMenu, Notebook, Radiobutton, Scrollbar, Style,
  17. Spinbox, Combobox)
  18. from tkinter import colorchooser
  19. import tkinter.font as tkfont
  20. from tkinter import messagebox
  21. from idlelib.config import idleConf, ConfigChanges
  22. from idlelib.config_key import GetKeysWindow
  23. from idlelib.dynoption import DynOptionMenu
  24. from idlelib import macosx
  25. from idlelib.query import SectionName, HelpSource
  26. from idlelib.textview import view_text
  27. from idlelib.autocomplete import AutoComplete
  28. from idlelib.codecontext import CodeContext
  29. from idlelib.parenmatch import ParenMatch
  30. from idlelib.format import FormatParagraph
  31. from idlelib.squeezer import Squeezer
  32. from idlelib.textview import ScrollableTextFrame
  33. changes = ConfigChanges()
  34. # Reload changed options in the following classes.
  35. reloadables = (AutoComplete, CodeContext, ParenMatch, FormatParagraph,
  36. Squeezer)
  37. class ConfigDialog(Toplevel):
  38. """Config dialog for IDLE.
  39. """
  40. def __init__(self, parent, title='', *, _htest=False, _utest=False):
  41. """Show the tabbed dialog for user configuration.
  42. Args:
  43. parent - parent of this dialog
  44. title - string which is the title of this popup dialog
  45. _htest - bool, change box location when running htest
  46. _utest - bool, don't wait_window when running unittest
  47. Note: Focus set on font page fontlist.
  48. Methods:
  49. create_widgets
  50. cancel: Bound to DELETE_WINDOW protocol.
  51. """
  52. Toplevel.__init__(self, parent)
  53. self.parent = parent
  54. if _htest:
  55. parent.instance_dict = {}
  56. if not _utest:
  57. self.withdraw()
  58. self.title(title or 'IDLE Preferences')
  59. x = parent.winfo_rootx() + 20
  60. y = parent.winfo_rooty() + (30 if not _htest else 150)
  61. self.geometry(f'+{x}+{y}')
  62. # Each theme element key is its display name.
  63. # The first value of the tuple is the sample area tag name.
  64. # The second value is the display name list sort index.
  65. self.create_widgets()
  66. self.resizable(height=FALSE, width=FALSE)
  67. self.transient(parent)
  68. self.protocol("WM_DELETE_WINDOW", self.cancel)
  69. self.fontpage.fontlist.focus_set()
  70. # XXX Decide whether to keep or delete these key bindings.
  71. # Key bindings for this dialog.
  72. # self.bind('<Escape>', self.Cancel) #dismiss dialog, no save
  73. # self.bind('<Alt-a>', self.Apply) #apply changes, save
  74. # self.bind('<F1>', self.Help) #context help
  75. # Attach callbacks after loading config to avoid calling them.
  76. tracers.attach()
  77. if not _utest:
  78. self.grab_set()
  79. self.wm_deiconify()
  80. self.wait_window()
  81. def create_widgets(self):
  82. """Create and place widgets for tabbed dialog.
  83. Widgets Bound to self:
  84. frame: encloses all other widgets
  85. note: Notebook
  86. highpage: HighPage
  87. fontpage: FontPage
  88. keyspage: KeysPage
  89. winpage: WinPage
  90. shedpage: ShedPage
  91. extpage: ExtPage
  92. Methods:
  93. create_action_buttons
  94. load_configs: Load pages except for extensions.
  95. activate_config_changes: Tell editors to reload.
  96. """
  97. self.frame = frame = Frame(self, padding="5px")
  98. self.frame.grid(sticky="nwes")
  99. self.note = note = Notebook(frame)
  100. self.extpage = ExtPage(note)
  101. self.highpage = HighPage(note, self.extpage)
  102. self.fontpage = FontPage(note, self.highpage)
  103. self.keyspage = KeysPage(note, self.extpage)
  104. self.winpage = WinPage(note)
  105. self.shedpage = ShedPage(note)
  106. note.add(self.fontpage, text=' Fonts ')
  107. note.add(self.highpage, text='Highlights')
  108. note.add(self.keyspage, text=' Keys ')
  109. note.add(self.winpage, text=' Windows ')
  110. note.add(self.shedpage, text=' Shell/Ed ')
  111. note.add(self.extpage, text='Extensions')
  112. note.enable_traversal()
  113. note.pack(side=TOP, expand=TRUE, fill=BOTH)
  114. self.create_action_buttons().pack(side=BOTTOM)
  115. def create_action_buttons(self):
  116. """Return frame of action buttons for dialog.
  117. Methods:
  118. ok
  119. apply
  120. cancel
  121. help
  122. Widget Structure:
  123. outer: Frame
  124. buttons: Frame
  125. (no assignment): Button (ok)
  126. (no assignment): Button (apply)
  127. (no assignment): Button (cancel)
  128. (no assignment): Button (help)
  129. (no assignment): Frame
  130. """
  131. if macosx.isAquaTk():
  132. # Changing the default padding on OSX results in unreadable
  133. # text in the buttons.
  134. padding_args = {}
  135. else:
  136. padding_args = {'padding': (6, 3)}
  137. outer = Frame(self.frame, padding=2)
  138. buttons_frame = Frame(outer, padding=2)
  139. self.buttons = {}
  140. for txt, cmd in (
  141. ('Ok', self.ok),
  142. ('Apply', self.apply),
  143. ('Cancel', self.cancel),
  144. ('Help', self.help)):
  145. self.buttons[txt] = Button(buttons_frame, text=txt, command=cmd,
  146. takefocus=FALSE, **padding_args)
  147. self.buttons[txt].pack(side=LEFT, padx=5)
  148. # Add space above buttons.
  149. Frame(outer, height=2, borderwidth=0).pack(side=TOP)
  150. buttons_frame.pack(side=BOTTOM)
  151. return outer
  152. def ok(self):
  153. """Apply config changes, then dismiss dialog."""
  154. self.apply()
  155. self.destroy()
  156. def apply(self):
  157. """Apply config changes and leave dialog open."""
  158. self.deactivate_current_config()
  159. changes.save_all()
  160. self.extpage.save_all_changed_extensions()
  161. self.activate_config_changes()
  162. def cancel(self):
  163. """Dismiss config dialog.
  164. Methods:
  165. destroy: inherited
  166. """
  167. changes.clear()
  168. self.destroy()
  169. def destroy(self):
  170. global font_sample_text
  171. font_sample_text = self.fontpage.font_sample.get('1.0', 'end')
  172. self.grab_release()
  173. super().destroy()
  174. def help(self):
  175. """Create textview for config dialog help.
  176. Attributes accessed:
  177. note
  178. Methods:
  179. view_text: Method from textview module.
  180. """
  181. page = self.note.tab(self.note.select(), option='text').strip()
  182. view_text(self, title='Help for IDLE preferences',
  183. contents=help_common+help_pages.get(page, ''))
  184. def deactivate_current_config(self):
  185. """Remove current key bindings.
  186. Iterate over window instances defined in parent and remove
  187. the keybindings.
  188. """
  189. # Before a config is saved, some cleanup of current
  190. # config must be done - remove the previous keybindings.
  191. win_instances = self.parent.instance_dict.keys()
  192. for instance in win_instances:
  193. instance.RemoveKeybindings()
  194. def activate_config_changes(self):
  195. """Apply configuration changes to current windows.
  196. Dynamically update the current parent window instances
  197. with some of the configuration changes.
  198. """
  199. win_instances = self.parent.instance_dict.keys()
  200. for instance in win_instances:
  201. instance.ResetColorizer()
  202. instance.ResetFont()
  203. instance.set_notabs_indentwidth()
  204. instance.ApplyKeybindings()
  205. instance.reset_help_menu_entries()
  206. instance.update_cursor_blink()
  207. for klass in reloadables:
  208. klass.reload()
  209. # class TabPage(Frame): # A template for Page classes.
  210. # def __init__(self, master):
  211. # super().__init__(master)
  212. # self.create_page_tab()
  213. # self.load_tab_cfg()
  214. # def create_page_tab(self):
  215. # # Define tk vars and register var and callback with tracers.
  216. # # Create subframes and widgets.
  217. # # Pack widgets.
  218. # def load_tab_cfg(self):
  219. # # Initialize widgets with data from idleConf.
  220. # def var_changed_var_name():
  221. # # For each tk var that needs other than default callback.
  222. # def other_methods():
  223. # # Define tab-specific behavior.
  224. font_sample_text = (
  225. '<ASCII/Latin1>\n'
  226. 'AaBbCcDdEeFfGgHhIiJj\n1234567890#:+=(){}[]\n'
  227. '\u00a2\u00a3\u00a5\u00a7\u00a9\u00ab\u00ae\u00b6\u00bd\u011e'
  228. '\u00c0\u00c1\u00c2\u00c3\u00c4\u00c5\u00c7\u00d0\u00d8\u00df\n'
  229. '\n<IPA,Greek,Cyrillic>\n'
  230. '\u0250\u0255\u0258\u025e\u025f\u0264\u026b\u026e\u0270\u0277'
  231. '\u027b\u0281\u0283\u0286\u028e\u029e\u02a2\u02ab\u02ad\u02af\n'
  232. '\u0391\u03b1\u0392\u03b2\u0393\u03b3\u0394\u03b4\u0395\u03b5'
  233. '\u0396\u03b6\u0397\u03b7\u0398\u03b8\u0399\u03b9\u039a\u03ba\n'
  234. '\u0411\u0431\u0414\u0434\u0416\u0436\u041f\u043f\u0424\u0444'
  235. '\u0427\u0447\u042a\u044a\u042d\u044d\u0460\u0464\u046c\u04dc\n'
  236. '\n<Hebrew, Arabic>\n'
  237. '\u05d0\u05d1\u05d2\u05d3\u05d4\u05d5\u05d6\u05d7\u05d8\u05d9'
  238. '\u05da\u05db\u05dc\u05dd\u05de\u05df\u05e0\u05e1\u05e2\u05e3\n'
  239. '\u0627\u0628\u062c\u062f\u0647\u0648\u0632\u062d\u0637\u064a'
  240. '\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669\n'
  241. '\n<Devanagari, Tamil>\n'
  242. '\u0966\u0967\u0968\u0969\u096a\u096b\u096c\u096d\u096e\u096f'
  243. '\u0905\u0906\u0907\u0908\u0909\u090a\u090f\u0910\u0913\u0914\n'
  244. '\u0be6\u0be7\u0be8\u0be9\u0bea\u0beb\u0bec\u0bed\u0bee\u0bef'
  245. '\u0b85\u0b87\u0b89\u0b8e\n'
  246. '\n<East Asian>\n'
  247. '\u3007\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\n'
  248. '\u6c49\u5b57\u6f22\u5b57\u4eba\u6728\u706b\u571f\u91d1\u6c34\n'
  249. '\uac00\ub0d0\ub354\ub824\ubaa8\ubd64\uc218\uc720\uc988\uce58\n'
  250. '\u3042\u3044\u3046\u3048\u304a\u30a2\u30a4\u30a6\u30a8\u30aa\n'
  251. )
  252. class FontPage(Frame):
  253. def __init__(self, master, highpage):
  254. super().__init__(master)
  255. self.highlight_sample = highpage.highlight_sample
  256. self.create_page_font()
  257. self.load_font_cfg()
  258. def create_page_font(self):
  259. """Return frame of widgets for Font tab.
  260. Fonts: Enable users to provisionally change font face, size, or
  261. boldness and to see the consequence of proposed choices. Each
  262. action set 3 options in changes structuree and changes the
  263. corresponding aspect of the font sample on this page and
  264. highlight sample on highlight page.
  265. Function load_font_cfg initializes font vars and widgets from
  266. idleConf entries and tk.
  267. Fontlist: mouse button 1 click or up or down key invoke
  268. on_fontlist_select(), which sets var font_name.
  269. Sizelist: clicking the menubutton opens the dropdown menu. A
  270. mouse button 1 click or return key sets var font_size.
  271. Bold_toggle: clicking the box toggles var font_bold.
  272. Changing any of the font vars invokes var_changed_font, which
  273. adds all 3 font options to changes and calls set_samples.
  274. Set_samples applies a new font constructed from the font vars to
  275. font_sample and to highlight_sample on the highlight page.
  276. Widgets for FontPage(Frame): (*) widgets bound to self
  277. frame_font: LabelFrame
  278. frame_font_name: Frame
  279. font_name_title: Label
  280. (*)fontlist: ListBox - font_name
  281. scroll_font: Scrollbar
  282. frame_font_param: Frame
  283. font_size_title: Label
  284. (*)sizelist: DynOptionMenu - font_size
  285. (*)bold_toggle: Checkbutton - font_bold
  286. frame_sample: LabelFrame
  287. (*)font_sample: Label
  288. """
  289. self.font_name = tracers.add(StringVar(self), self.var_changed_font)
  290. self.font_size = tracers.add(StringVar(self), self.var_changed_font)
  291. self.font_bold = tracers.add(BooleanVar(self), self.var_changed_font)
  292. # Define frames and widgets.
  293. frame_font = LabelFrame(self, borderwidth=2, relief=GROOVE,
  294. text=' Shell/Editor Font ')
  295. frame_sample = LabelFrame(self, borderwidth=2, relief=GROOVE,
  296. text=' Font Sample (Editable) ')
  297. # frame_font.
  298. frame_font_name = Frame(frame_font)
  299. frame_font_param = Frame(frame_font)
  300. font_name_title = Label(
  301. frame_font_name, justify=LEFT, text='Font Face :')
  302. self.fontlist = Listbox(frame_font_name, height=15,
  303. takefocus=True, exportselection=FALSE)
  304. self.fontlist.bind('<ButtonRelease-1>', self.on_fontlist_select)
  305. self.fontlist.bind('<KeyRelease-Up>', self.on_fontlist_select)
  306. self.fontlist.bind('<KeyRelease-Down>', self.on_fontlist_select)
  307. scroll_font = Scrollbar(frame_font_name)
  308. scroll_font.config(command=self.fontlist.yview)
  309. self.fontlist.config(yscrollcommand=scroll_font.set)
  310. font_size_title = Label(frame_font_param, text='Size :')
  311. self.sizelist = DynOptionMenu(frame_font_param, self.font_size, None)
  312. self.bold_toggle = Checkbutton(
  313. frame_font_param, variable=self.font_bold,
  314. onvalue=1, offvalue=0, text='Bold')
  315. # frame_sample.
  316. font_sample_frame = ScrollableTextFrame(frame_sample)
  317. self.font_sample = font_sample_frame.text
  318. self.font_sample.config(wrap=NONE, width=1, height=1)
  319. self.font_sample.insert(END, font_sample_text)
  320. # Grid and pack widgets:
  321. self.columnconfigure(1, weight=1)
  322. self.rowconfigure(2, weight=1)
  323. frame_font.grid(row=0, column=0, padx=5, pady=5)
  324. frame_sample.grid(row=0, column=1, rowspan=3, padx=5, pady=5,
  325. sticky='nsew')
  326. # frame_font.
  327. frame_font_name.pack(side=TOP, padx=5, pady=5, fill=X)
  328. frame_font_param.pack(side=TOP, padx=5, pady=5, fill=X)
  329. font_name_title.pack(side=TOP, anchor=W)
  330. self.fontlist.pack(side=LEFT, expand=TRUE, fill=X)
  331. scroll_font.pack(side=LEFT, fill=Y)
  332. font_size_title.pack(side=LEFT, anchor=W)
  333. self.sizelist.pack(side=LEFT, anchor=W)
  334. self.bold_toggle.pack(side=LEFT, anchor=W, padx=20)
  335. # frame_sample.
  336. font_sample_frame.pack(expand=TRUE, fill=BOTH)
  337. def load_font_cfg(self):
  338. """Load current configuration settings for the font options.
  339. Retrieve current font with idleConf.GetFont and font families
  340. from tk. Setup fontlist and set font_name. Setup sizelist,
  341. which sets font_size. Set font_bold. Call set_samples.
  342. """
  343. configured_font = idleConf.GetFont(self, 'main', 'EditorWindow')
  344. font_name = configured_font[0].lower()
  345. font_size = configured_font[1]
  346. font_bold = configured_font[2]=='bold'
  347. # Set sorted no-duplicate editor font selection list and font_name.
  348. fonts = sorted(set(tkfont.families(self)))
  349. for font in fonts:
  350. self.fontlist.insert(END, font)
  351. self.font_name.set(font_name)
  352. lc_fonts = [s.lower() for s in fonts]
  353. try:
  354. current_font_index = lc_fonts.index(font_name)
  355. self.fontlist.see(current_font_index)
  356. self.fontlist.select_set(current_font_index)
  357. self.fontlist.select_anchor(current_font_index)
  358. self.fontlist.activate(current_font_index)
  359. except ValueError:
  360. pass
  361. # Set font size dropdown.
  362. self.sizelist.SetMenu(('7', '8', '9', '10', '11', '12', '13', '14',
  363. '16', '18', '20', '22', '25', '29', '34', '40'),
  364. font_size)
  365. # Set font weight.
  366. self.font_bold.set(font_bold)
  367. self.set_samples()
  368. def var_changed_font(self, *params):
  369. """Store changes to font attributes.
  370. When one font attribute changes, save them all, as they are
  371. not independent from each other. In particular, when we are
  372. overriding the default font, we need to write out everything.
  373. """
  374. value = self.font_name.get()
  375. changes.add_option('main', 'EditorWindow', 'font', value)
  376. value = self.font_size.get()
  377. changes.add_option('main', 'EditorWindow', 'font-size', value)
  378. value = self.font_bold.get()
  379. changes.add_option('main', 'EditorWindow', 'font-bold', value)
  380. self.set_samples()
  381. def on_fontlist_select(self, event):
  382. """Handle selecting a font from the list.
  383. Event can result from either mouse click or Up or Down key.
  384. Set font_name and example displays to selection.
  385. """
  386. font = self.fontlist.get(
  387. ACTIVE if event.type.name == 'KeyRelease' else ANCHOR)
  388. self.font_name.set(font.lower())
  389. def set_samples(self, event=None):
  390. """Update update both screen samples with the font settings.
  391. Called on font initialization and change events.
  392. Accesses font_name, font_size, and font_bold Variables.
  393. Updates font_sample and highlight page highlight_sample.
  394. """
  395. font_name = self.font_name.get()
  396. font_weight = tkfont.BOLD if self.font_bold.get() else tkfont.NORMAL
  397. new_font = (font_name, self.font_size.get(), font_weight)
  398. self.font_sample['font'] = new_font
  399. self.highlight_sample['font'] = new_font
  400. class HighPage(Frame):
  401. def __init__(self, master, extpage):
  402. super().__init__(master)
  403. self.extpage = extpage
  404. self.cd = master.winfo_toplevel()
  405. self.style = Style(master)
  406. self.create_page_highlight()
  407. self.load_theme_cfg()
  408. def create_page_highlight(self):
  409. """Return frame of widgets for Highlights tab.
  410. Enable users to provisionally change foreground and background
  411. colors applied to textual tags. Color mappings are stored in
  412. complete listings called themes. Built-in themes in
  413. idlelib/config-highlight.def are fixed as far as the dialog is
  414. concerned. Any theme can be used as the base for a new custom
  415. theme, stored in .idlerc/config-highlight.cfg.
  416. Function load_theme_cfg() initializes tk variables and theme
  417. lists and calls paint_theme_sample() and set_highlight_target()
  418. for the current theme. Radiobuttons builtin_theme_on and
  419. custom_theme_on toggle var theme_source, which controls if the
  420. current set of colors are from a builtin or custom theme.
  421. DynOptionMenus builtinlist and customlist contain lists of the
  422. builtin and custom themes, respectively, and the current item
  423. from each list is stored in vars builtin_name and custom_name.
  424. Function paint_theme_sample() applies the colors from the theme
  425. to the tags in text widget highlight_sample and then invokes
  426. set_color_sample(). Function set_highlight_target() sets the state
  427. of the radiobuttons fg_on and bg_on based on the tag and it also
  428. invokes set_color_sample().
  429. Function set_color_sample() sets the background color for the frame
  430. holding the color selector. This provides a larger visual of the
  431. color for the current tag and plane (foreground/background).
  432. Note: set_color_sample() is called from many places and is often
  433. called more than once when a change is made. It is invoked when
  434. foreground or background is selected (radiobuttons), from
  435. paint_theme_sample() (theme is changed or load_cfg is called), and
  436. from set_highlight_target() (target tag is changed or load_cfg called).
  437. Button delete_custom invokes delete_custom() to delete
  438. a custom theme from idleConf.userCfg['highlight'] and changes.
  439. Button save_custom invokes save_as_new_theme() which calls
  440. get_new_theme_name() and create_new() to save a custom theme
  441. and its colors to idleConf.userCfg['highlight'].
  442. Radiobuttons fg_on and bg_on toggle var fg_bg_toggle to control
  443. if the current selected color for a tag is for the foreground or
  444. background.
  445. DynOptionMenu targetlist contains a readable description of the
  446. tags applied to Python source within IDLE. Selecting one of the
  447. tags from this list populates highlight_target, which has a callback
  448. function set_highlight_target().
  449. Text widget highlight_sample displays a block of text (which is
  450. mock Python code) in which is embedded the defined tags and reflects
  451. the color attributes of the current theme and changes for those tags.
  452. Mouse button 1 allows for selection of a tag and updates
  453. highlight_target with that tag value.
  454. Note: The font in highlight_sample is set through the config in
  455. the fonts tab.
  456. In other words, a tag can be selected either from targetlist or
  457. by clicking on the sample text within highlight_sample. The
  458. plane (foreground/background) is selected via the radiobutton.
  459. Together, these two (tag and plane) control what color is
  460. shown in set_color_sample() for the current theme. Button set_color
  461. invokes get_color() which displays a ColorChooser to change the
  462. color for the selected tag/plane. If a new color is picked,
  463. it will be saved to changes and the highlight_sample and
  464. frame background will be updated.
  465. Tk Variables:
  466. color: Color of selected target.
  467. builtin_name: Menu variable for built-in theme.
  468. custom_name: Menu variable for custom theme.
  469. fg_bg_toggle: Toggle for foreground/background color.
  470. Note: this has no callback.
  471. theme_source: Selector for built-in or custom theme.
  472. highlight_target: Menu variable for the highlight tag target.
  473. Instance Data Attributes:
  474. theme_elements: Dictionary of tags for text highlighting.
  475. The key is the display name and the value is a tuple of
  476. (tag name, display sort order).
  477. Methods [attachment]:
  478. load_theme_cfg: Load current highlight colors.
  479. get_color: Invoke colorchooser [button_set_color].
  480. set_color_sample_binding: Call set_color_sample [fg_bg_toggle].
  481. set_highlight_target: set fg_bg_toggle, set_color_sample().
  482. set_color_sample: Set frame background to target.
  483. on_new_color_set: Set new color and add option.
  484. paint_theme_sample: Recolor sample.
  485. get_new_theme_name: Get from popup.
  486. create_new: Combine theme with changes and save.
  487. save_as_new_theme: Save [button_save_custom].
  488. set_theme_type: Command for [theme_source].
  489. delete_custom: Activate default [button_delete_custom].
  490. save_new: Save to userCfg['theme'] (is function).
  491. Widgets of highlights page frame: (*) widgets bound to self
  492. frame_custom: LabelFrame
  493. (*)highlight_sample: Text
  494. (*)frame_color_set: Frame
  495. (*)button_set_color: Button
  496. (*)targetlist: DynOptionMenu - highlight_target
  497. frame_fg_bg_toggle: Frame
  498. (*)fg_on: Radiobutton - fg_bg_toggle
  499. (*)bg_on: Radiobutton - fg_bg_toggle
  500. (*)button_save_custom: Button
  501. frame_theme: LabelFrame
  502. theme_type_title: Label
  503. (*)builtin_theme_on: Radiobutton - theme_source
  504. (*)custom_theme_on: Radiobutton - theme_source
  505. (*)builtinlist: DynOptionMenu - builtin_name
  506. (*)customlist: DynOptionMenu - custom_name
  507. (*)button_delete_custom: Button
  508. (*)theme_message: Label
  509. """
  510. self.theme_elements = {
  511. 'Normal Code or Text': ('normal', '00'),
  512. 'Code Context': ('context', '01'),
  513. 'Python Keywords': ('keyword', '02'),
  514. 'Python Definitions': ('definition', '03'),
  515. 'Python Builtins': ('builtin', '04'),
  516. 'Python Comments': ('comment', '05'),
  517. 'Python Strings': ('string', '06'),
  518. 'Selected Text': ('hilite', '07'),
  519. 'Found Text': ('hit', '08'),
  520. 'Cursor': ('cursor', '09'),
  521. 'Editor Breakpoint': ('break', '10'),
  522. 'Shell Prompt': ('console', '11'),
  523. 'Error Text': ('error', '12'),
  524. 'Shell User Output': ('stdout', '13'),
  525. 'Shell User Exception': ('stderr', '14'),
  526. 'Line Number': ('linenumber', '16'),
  527. }
  528. self.builtin_name = tracers.add(
  529. StringVar(self), self.var_changed_builtin_name)
  530. self.custom_name = tracers.add(
  531. StringVar(self), self.var_changed_custom_name)
  532. self.fg_bg_toggle = BooleanVar(self)
  533. self.color = tracers.add(
  534. StringVar(self), self.var_changed_color)
  535. self.theme_source = tracers.add(
  536. BooleanVar(self), self.var_changed_theme_source)
  537. self.highlight_target = tracers.add(
  538. StringVar(self), self.var_changed_highlight_target)
  539. # Create widgets:
  540. # body frame and section frames.
  541. frame_custom = LabelFrame(self, borderwidth=2, relief=GROOVE,
  542. text=' Custom Highlighting ')
  543. frame_theme = LabelFrame(self, borderwidth=2, relief=GROOVE,
  544. text=' Highlighting Theme ')
  545. # frame_custom.
  546. sample_frame = ScrollableTextFrame(
  547. frame_custom, relief=SOLID, borderwidth=1)
  548. text = self.highlight_sample = sample_frame.text
  549. text.configure(
  550. font=('courier', 12, ''), cursor='hand2', width=1, height=1,
  551. takefocus=FALSE, highlightthickness=0, wrap=NONE)
  552. # Prevent perhaps invisible selection of word or slice.
  553. text.bind('<Double-Button-1>', lambda e: 'break')
  554. text.bind('<B1-Motion>', lambda e: 'break')
  555. string_tags=(
  556. ('# Click selects item.', 'comment'), ('\n', 'normal'),
  557. ('code context section', 'context'), ('\n', 'normal'),
  558. ('| cursor', 'cursor'), ('\n', 'normal'),
  559. ('def', 'keyword'), (' ', 'normal'),
  560. ('func', 'definition'), ('(param):\n ', 'normal'),
  561. ('"Return None."', 'string'), ('\n var0 = ', 'normal'),
  562. ("'string'", 'string'), ('\n var1 = ', 'normal'),
  563. ("'selected'", 'hilite'), ('\n var2 = ', 'normal'),
  564. ("'found'", 'hit'), ('\n var3 = ', 'normal'),
  565. ('list', 'builtin'), ('(', 'normal'),
  566. ('None', 'keyword'), (')\n', 'normal'),
  567. (' breakpoint("line")', 'break'), ('\n\n', 'normal'),
  568. ('>>>', 'console'), (' 3.14**2\n', 'normal'),
  569. ('9.8596', 'stdout'), ('\n', 'normal'),
  570. ('>>>', 'console'), (' pri ', 'normal'),
  571. ('n', 'error'), ('t(\n', 'normal'),
  572. ('SyntaxError', 'stderr'), ('\n', 'normal'))
  573. for string, tag in string_tags:
  574. text.insert(END, string, tag)
  575. n_lines = len(text.get('1.0', END).splitlines())
  576. for lineno in range(1, n_lines):
  577. text.insert(f'{lineno}.0',
  578. f'{lineno:{len(str(n_lines))}d} ',
  579. 'linenumber')
  580. for element in self.theme_elements:
  581. def tem(event, elem=element):
  582. # event.widget.winfo_top_level().highlight_target.set(elem)
  583. self.highlight_target.set(elem)
  584. text.tag_bind(
  585. self.theme_elements[element][0], '<ButtonPress-1>', tem)
  586. text['state'] = 'disabled'
  587. self.style.configure('frame_color_set.TFrame', borderwidth=1,
  588. relief='solid')
  589. self.frame_color_set = Frame(frame_custom, style='frame_color_set.TFrame')
  590. frame_fg_bg_toggle = Frame(frame_custom)
  591. self.button_set_color = Button(
  592. self.frame_color_set, text='Choose Color for :',
  593. command=self.get_color)
  594. self.targetlist = DynOptionMenu(
  595. self.frame_color_set, self.highlight_target, None,
  596. highlightthickness=0) #, command=self.set_highlight_targetBinding
  597. self.fg_on = Radiobutton(
  598. frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=1,
  599. text='Foreground', command=self.set_color_sample_binding)
  600. self.bg_on = Radiobutton(
  601. frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=0,
  602. text='Background', command=self.set_color_sample_binding)
  603. self.fg_bg_toggle.set(1)
  604. self.button_save_custom = Button(
  605. frame_custom, text='Save as New Custom Theme',
  606. command=self.save_as_new_theme)
  607. # frame_theme.
  608. theme_type_title = Label(frame_theme, text='Select : ')
  609. self.builtin_theme_on = Radiobutton(
  610. frame_theme, variable=self.theme_source, value=1,
  611. command=self.set_theme_type, text='a Built-in Theme')
  612. self.custom_theme_on = Radiobutton(
  613. frame_theme, variable=self.theme_source, value=0,
  614. command=self.set_theme_type, text='a Custom Theme')
  615. self.builtinlist = DynOptionMenu(
  616. frame_theme, self.builtin_name, None, command=None)
  617. self.customlist = DynOptionMenu(
  618. frame_theme, self.custom_name, None, command=None)
  619. self.button_delete_custom = Button(
  620. frame_theme, text='Delete Custom Theme',
  621. command=self.delete_custom)
  622. self.theme_message = Label(frame_theme, borderwidth=2)
  623. # Pack widgets:
  624. # body.
  625. frame_custom.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
  626. frame_theme.pack(side=TOP, padx=5, pady=5, fill=X)
  627. # frame_custom.
  628. self.frame_color_set.pack(side=TOP, padx=5, pady=5, fill=X)
  629. frame_fg_bg_toggle.pack(side=TOP, padx=5, pady=0)
  630. sample_frame.pack(
  631. side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
  632. self.button_set_color.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=4)
  633. self.targetlist.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=3)
  634. self.fg_on.pack(side=LEFT, anchor=E)
  635. self.bg_on.pack(side=RIGHT, anchor=W)
  636. self.button_save_custom.pack(side=BOTTOM, fill=X, padx=5, pady=5)
  637. # frame_theme.
  638. theme_type_title.pack(side=TOP, anchor=W, padx=5, pady=5)
  639. self.builtin_theme_on.pack(side=TOP, anchor=W, padx=5)
  640. self.custom_theme_on.pack(side=TOP, anchor=W, padx=5, pady=2)
  641. self.builtinlist.pack(side=TOP, fill=X, padx=5, pady=5)
  642. self.customlist.pack(side=TOP, fill=X, anchor=W, padx=5, pady=5)
  643. self.button_delete_custom.pack(side=TOP, fill=X, padx=5, pady=5)
  644. self.theme_message.pack(side=TOP, fill=X, pady=5)
  645. def load_theme_cfg(self):
  646. """Load current configuration settings for the theme options.
  647. Based on the theme_source toggle, the theme is set as
  648. either builtin or custom and the initial widget values
  649. reflect the current settings from idleConf.
  650. Attributes updated:
  651. theme_source: Set from idleConf.
  652. builtinlist: List of default themes from idleConf.
  653. customlist: List of custom themes from idleConf.
  654. custom_theme_on: Disabled if there are no custom themes.
  655. custom_theme: Message with additional information.
  656. targetlist: Create menu from self.theme_elements.
  657. Methods:
  658. set_theme_type
  659. paint_theme_sample
  660. set_highlight_target
  661. """
  662. # Set current theme type radiobutton.
  663. self.theme_source.set(idleConf.GetOption(
  664. 'main', 'Theme', 'default', type='bool', default=1))
  665. # Set current theme.
  666. current_option = idleConf.CurrentTheme()
  667. # Load available theme option menus.
  668. if self.theme_source.get(): # Default theme selected.
  669. item_list = idleConf.GetSectionList('default', 'highlight')
  670. item_list.sort()
  671. self.builtinlist.SetMenu(item_list, current_option)
  672. item_list = idleConf.GetSectionList('user', 'highlight')
  673. item_list.sort()
  674. if not item_list:
  675. self.custom_theme_on.state(('disabled',))
  676. self.custom_name.set('- no custom themes -')
  677. else:
  678. self.customlist.SetMenu(item_list, item_list[0])
  679. else: # User theme selected.
  680. item_list = idleConf.GetSectionList('user', 'highlight')
  681. item_list.sort()
  682. self.customlist.SetMenu(item_list, current_option)
  683. item_list = idleConf.GetSectionList('default', 'highlight')
  684. item_list.sort()
  685. self.builtinlist.SetMenu(item_list, item_list[0])
  686. self.set_theme_type()
  687. # Load theme element option menu.
  688. theme_names = list(self.theme_elements.keys())
  689. theme_names.sort(key=lambda x: self.theme_elements[x][1])
  690. self.targetlist.SetMenu(theme_names, theme_names[0])
  691. self.paint_theme_sample()
  692. self.set_highlight_target()
  693. def var_changed_builtin_name(self, *params):
  694. """Process new builtin theme selection.
  695. Add the changed theme's name to the changed_items and recreate
  696. the sample with the values from the selected theme.
  697. """
  698. old_themes = ('IDLE Classic', 'IDLE New')
  699. value = self.builtin_name.get()
  700. if value not in old_themes:
  701. if idleConf.GetOption('main', 'Theme', 'name') not in old_themes:
  702. changes.add_option('main', 'Theme', 'name', old_themes[0])
  703. changes.add_option('main', 'Theme', 'name2', value)
  704. self.theme_message['text'] = 'New theme, see Help'
  705. else:
  706. changes.add_option('main', 'Theme', 'name', value)
  707. changes.add_option('main', 'Theme', 'name2', '')
  708. self.theme_message['text'] = ''
  709. self.paint_theme_sample()
  710. def var_changed_custom_name(self, *params):
  711. """Process new custom theme selection.
  712. If a new custom theme is selected, add the name to the
  713. changed_items and apply the theme to the sample.
  714. """
  715. value = self.custom_name.get()
  716. if value != '- no custom themes -':
  717. changes.add_option('main', 'Theme', 'name', value)
  718. self.paint_theme_sample()
  719. def var_changed_theme_source(self, *params):
  720. """Process toggle between builtin and custom theme.
  721. Update the default toggle value and apply the newly
  722. selected theme type.
  723. """
  724. value = self.theme_source.get()
  725. changes.add_option('main', 'Theme', 'default', value)
  726. if value:
  727. self.var_changed_builtin_name()
  728. else:
  729. self.var_changed_custom_name()
  730. def var_changed_color(self, *params):
  731. "Process change to color choice."
  732. self.on_new_color_set()
  733. def var_changed_highlight_target(self, *params):
  734. "Process selection of new target tag for highlighting."
  735. self.set_highlight_target()
  736. def set_theme_type(self):
  737. """Set available screen options based on builtin or custom theme.
  738. Attributes accessed:
  739. theme_source
  740. Attributes updated:
  741. builtinlist
  742. customlist
  743. button_delete_custom
  744. custom_theme_on
  745. Called from:
  746. handler for builtin_theme_on and custom_theme_on
  747. delete_custom
  748. create_new
  749. load_theme_cfg
  750. """
  751. if self.theme_source.get():
  752. self.builtinlist['state'] = 'normal'
  753. self.customlist['state'] = 'disabled'
  754. self.button_delete_custom.state(('disabled',))
  755. else:
  756. self.builtinlist['state'] = 'disabled'
  757. self.custom_theme_on.state(('!disabled',))
  758. self.customlist['state'] = 'normal'
  759. self.button_delete_custom.state(('!disabled',))
  760. def get_color(self):
  761. """Handle button to select a new color for the target tag.
  762. If a new color is selected while using a builtin theme, a
  763. name must be supplied to create a custom theme.
  764. Attributes accessed:
  765. highlight_target
  766. frame_color_set
  767. theme_source
  768. Attributes updated:
  769. color
  770. Methods:
  771. get_new_theme_name
  772. create_new
  773. """
  774. target = self.highlight_target.get()
  775. prev_color = self.style.lookup(self.frame_color_set['style'],
  776. 'background')
  777. rgbTuplet, color_string = colorchooser.askcolor(
  778. parent=self, title='Pick new color for : '+target,
  779. initialcolor=prev_color)
  780. if color_string and (color_string != prev_color):
  781. # User didn't cancel and they chose a new color.
  782. if self.theme_source.get(): # Current theme is a built-in.
  783. message = ('Your changes will be saved as a new Custom Theme. '
  784. 'Enter a name for your new Custom Theme below.')
  785. new_theme = self.get_new_theme_name(message)
  786. if not new_theme: # User cancelled custom theme creation.
  787. return
  788. else: # Create new custom theme based on previously active theme.
  789. self.create_new(new_theme)
  790. self.color.set(color_string)
  791. else: # Current theme is user defined.
  792. self.color.set(color_string)
  793. def on_new_color_set(self):
  794. "Display sample of new color selection on the dialog."
  795. new_color = self.color.get()
  796. self.style.configure('frame_color_set.TFrame', background=new_color)
  797. plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
  798. sample_element = self.theme_elements[self.highlight_target.get()][0]
  799. self.highlight_sample.tag_config(sample_element, **{plane: new_color})
  800. theme = self.custom_name.get()
  801. theme_element = sample_element + '-' + plane
  802. changes.add_option('highlight', theme, theme_element, new_color)
  803. def get_new_theme_name(self, message):
  804. "Return name of new theme from query popup."
  805. used_names = (idleConf.GetSectionList('user', 'highlight') +
  806. idleConf.GetSectionList('default', 'highlight'))
  807. new_theme = SectionName(
  808. self, 'New Custom Theme', message, used_names).result
  809. return new_theme
  810. def save_as_new_theme(self):
  811. """Prompt for new theme name and create the theme.
  812. Methods:
  813. get_new_theme_name
  814. create_new
  815. """
  816. new_theme_name = self.get_new_theme_name('New Theme Name:')
  817. if new_theme_name:
  818. self.create_new(new_theme_name)
  819. def create_new(self, new_theme_name):
  820. """Create a new custom theme with the given name.
  821. Create the new theme based on the previously active theme
  822. with the current changes applied. Once it is saved, then
  823. activate the new theme.
  824. Attributes accessed:
  825. builtin_name
  826. custom_name
  827. Attributes updated:
  828. customlist
  829. theme_source
  830. Method:
  831. save_new
  832. set_theme_type
  833. """
  834. if self.theme_source.get():
  835. theme_type = 'default'
  836. theme_name = self.builtin_name.get()
  837. else:
  838. theme_type = 'user'
  839. theme_name = self.custom_name.get()
  840. new_theme = idleConf.GetThemeDict(theme_type, theme_name)
  841. # Apply any of the old theme's unsaved changes to the new theme.
  842. if theme_name in changes['highlight']:
  843. theme_changes = changes['highlight'][theme_name]
  844. for element in theme_changes:
  845. new_theme[element] = theme_changes[element]
  846. # Save the new theme.
  847. self.save_new(new_theme_name, new_theme)
  848. # Change GUI over to the new theme.
  849. custom_theme_list = idleConf.GetSectionList('user', 'highlight')
  850. custom_theme_list.sort()
  851. self.customlist.SetMenu(custom_theme_list, new_theme_name)
  852. self.theme_source.set(0)
  853. self.set_theme_type()
  854. def set_highlight_target(self):
  855. """Set fg/bg toggle and color based on highlight tag target.
  856. Instance variables accessed:
  857. highlight_target
  858. Attributes updated:
  859. fg_on
  860. bg_on
  861. fg_bg_toggle
  862. Methods:
  863. set_color_sample
  864. Called from:
  865. var_changed_highlight_target
  866. load_theme_cfg
  867. """
  868. if self.highlight_target.get() == 'Cursor': # bg not possible
  869. self.fg_on.state(('disabled',))
  870. self.bg_on.state(('disabled',))
  871. self.fg_bg_toggle.set(1)
  872. else: # Both fg and bg can be set.
  873. self.fg_on.state(('!disabled',))
  874. self.bg_on.state(('!disabled',))
  875. self.fg_bg_toggle.set(1)
  876. self.set_color_sample()
  877. def set_color_sample_binding(self, *args):
  878. """Change color sample based on foreground/background toggle.
  879. Methods:
  880. set_color_sample
  881. """
  882. self.set_color_sample()
  883. def set_color_sample(self):
  884. """Set the color of the frame background to reflect the selected target.
  885. Instance variables accessed:
  886. theme_elements
  887. highlight_target
  888. fg_bg_toggle
  889. highlight_sample
  890. Attributes updated:
  891. frame_color_set
  892. """
  893. # Set the color sample area.
  894. tag = self.theme_elements[self.highlight_target.get()][0]
  895. plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
  896. color = self.highlight_sample.tag_cget(tag, plane)
  897. self.style.configure('frame_color_set.TFrame', background=color)
  898. def paint_theme_sample(self):
  899. """Apply the theme colors to each element tag in the sample text.
  900. Instance attributes accessed:
  901. theme_elements
  902. theme_source
  903. builtin_name
  904. custom_name
  905. Attributes updated:
  906. highlight_sample: Set the tag elements to the theme.
  907. Methods:
  908. set_color_sample
  909. Called from:
  910. var_changed_builtin_name
  911. var_changed_custom_name
  912. load_theme_cfg
  913. """
  914. if self.theme_source.get(): # Default theme
  915. theme = self.builtin_name.get()
  916. else: # User theme
  917. theme = self.custom_name.get()
  918. for element_title in self.theme_elements:
  919. element = self.theme_elements[element_title][0]
  920. colors = idleConf.GetHighlight(theme, element)
  921. if element == 'cursor': # Cursor sample needs special painting.
  922. colors['background'] = idleConf.GetHighlight(
  923. theme, 'normal')['background']
  924. # Handle any unsaved changes to this theme.
  925. if theme in changes['highlight']:
  926. theme_dict = changes['highlight'][theme]
  927. if element + '-foreground' in theme_dict:
  928. colors['foreground'] = theme_dict[element + '-foreground']
  929. if element + '-background' in theme_dict:
  930. colors['background'] = theme_dict[element + '-background']
  931. self.highlight_sample.tag_config(element, **colors)
  932. self.set_color_sample()
  933. def save_new(self, theme_name, theme):
  934. """Save a newly created theme to idleConf.
  935. theme_name - string, the name of the new theme
  936. theme - dictionary containing the new theme
  937. """
  938. idleConf.userCfg['highlight'].AddSection(theme_name)
  939. for element in theme:
  940. value = theme[element]
  941. idleConf.userCfg['highlight'].SetOption(theme_name, element, value)
  942. def askyesno(self, *args, **kwargs):
  943. # Make testing easier. Could change implementation.
  944. return messagebox.askyesno(*args, **kwargs)
  945. def delete_custom(self):
  946. """Handle event to delete custom theme.
  947. The current theme is deactivated and the default theme is
  948. activated. The custom theme is permanently removed from
  949. the config file.
  950. Attributes accessed:
  951. custom_name
  952. Attributes updated:
  953. custom_theme_on
  954. customlist
  955. theme_source
  956. builtin_name
  957. Methods:
  958. deactivate_current_config
  959. save_all_changed_extensions
  960. activate_config_changes
  961. set_theme_type
  962. """
  963. theme_name = self.custom_name.get()
  964. delmsg = 'Are you sure you wish to delete the theme %r ?'
  965. if not self.askyesno(
  966. 'Delete Theme', delmsg % theme_name, parent=self):
  967. return
  968. self.cd.deactivate_current_config()
  969. # Remove theme from changes, config, and file.
  970. changes.delete_section('highlight', theme_name)
  971. # Reload user theme list.
  972. item_list = idleConf.GetSectionList('user', 'highlight')
  973. item_list.sort()
  974. if not item_list:
  975. self.custom_theme_on.state(('disabled',))
  976. self.customlist.SetMenu(item_list, '- no custom themes -')
  977. else:
  978. self.customlist.SetMenu(item_list, item_list[0])
  979. # Revert to default theme.
  980. self.theme_source.set(idleConf.defaultCfg['main'].Get('Theme', 'default'))
  981. self.builtin_name.set(idleConf.defaultCfg['main'].Get('Theme', 'name'))
  982. # User can't back out of these changes, they must be applied now.
  983. changes.save_all()
  984. self.extpage.save_all_changed_extensions()
  985. self.cd.activate_config_changes()
  986. self.set_theme_type()
  987. class KeysPage(Frame):
  988. def __init__(self, master, extpage):
  989. super().__init__(master)
  990. self.extpage = extpage
  991. self.cd = master.winfo_toplevel()
  992. self.create_page_keys()
  993. self.load_key_cfg()
  994. def create_page_keys(self):
  995. """Return frame of widgets for Keys tab.
  996. Enable users to provisionally change both individual and sets of
  997. keybindings (shortcut keys). Except for features implemented as
  998. extensions, keybindings are stored in complete sets called
  999. keysets. Built-in keysets in idlelib/config-keys.def are fixed
  1000. as far as the dialog is concerned. Any keyset can be used as the
  1001. base for a new custom keyset, stored in .idlerc/config-keys.cfg.
  1002. Function load_key_cfg() initializes tk variables and keyset
  1003. lists and calls load_keys_list for the current keyset.
  1004. Radiobuttons builtin_keyset_on and custom_keyset_on toggle var
  1005. keyset_source, which controls if the current set of keybindings
  1006. are from a builtin or custom keyset. DynOptionMenus builtinlist
  1007. and customlist contain lists of the builtin and custom keysets,
  1008. respectively, and the current item from each list is stored in
  1009. vars builtin_name and custom_name.
  1010. Button delete_custom_keys invokes delete_custom_keys() to delete
  1011. a custom keyset from idleConf.userCfg['keys'] and changes. Button
  1012. save_custom_keys invokes save_as_new_key_set() which calls
  1013. get_new_keys_name() and create_new_key_set() to save a custom keyset
  1014. and its keybindings to idleConf.userCfg['keys'].
  1015. Listbox bindingslist contains all of the keybindings for the
  1016. selected keyset. The keybindings are loaded in load_keys_list()
  1017. and are pairs of (event, [keys]) where keys can be a list
  1018. of one or more key combinations to bind to the same event.
  1019. Mouse button 1 click invokes on_bindingslist_select(), which
  1020. allows button_new_keys to be clicked.
  1021. So, an item is selected in listbindings, which activates
  1022. button_new_keys, and clicking button_new_keys calls function
  1023. get_new_keys(). Function get_new_keys() gets the key mappings from the
  1024. current keyset for the binding event item that was selected. The
  1025. function then displays another dialog, GetKeysDialog, with the
  1026. selected binding event and current keys and allows new key sequences
  1027. to be entered for that binding event. If the keys aren't
  1028. changed, nothing happens. If the keys are changed and the keyset
  1029. is a builtin, function get_new_keys_name() will be called
  1030. for input of a custom keyset name. If no name is given, then the
  1031. change to the keybinding will abort and no updates will be made. If
  1032. a custom name is entered in the prompt or if the current keyset was
  1033. already custom (and thus didn't require a prompt), then
  1034. idleConf.userCfg['keys'] is updated in function create_new_key_set()
  1035. with the change to the event binding. The item listing in bindingslist
  1036. is updated with the new keys. Var keybinding is also set which invokes
  1037. the callback function, var_changed_keybinding, to add the change to
  1038. the 'keys' or 'extensions' changes tracker based on the binding type.
  1039. Tk Variables:
  1040. keybinding: Action/key bindings.
  1041. Methods:
  1042. load_keys_list: Reload active set.
  1043. create_new_key_set: Combine active keyset and changes.
  1044. set_keys_type: Command for keyset_source.
  1045. save_new_key_set: Save to idleConf.userCfg['keys'] (is function).
  1046. deactivate_current_config: Remove keys bindings in editors.
  1047. Widgets for KeysPage(frame): (*) widgets bound to self
  1048. frame_key_sets: LabelFrame
  1049. frames[0]: Frame
  1050. (*)builtin_keyset_on: Radiobutton - var keyset_source
  1051. (*)custom_keyset_on: Radiobutton - var keyset_source
  1052. (*)builtinlist: DynOptionMenu - var builtin_name,
  1053. func keybinding_selected
  1054. (*)customlist: DynOptionMenu - var custom_name,
  1055. func keybinding_selected
  1056. (*)keys_message: Label
  1057. frames[1]: Frame
  1058. (*)button_delete_custom_keys: Button - delete_custom_keys
  1059. (*)button_save_custom_keys: Button - save_as_new_key_set
  1060. frame_custom: LabelFrame
  1061. frame_target: Frame
  1062. target_title: Label
  1063. scroll_target_y: Scrollbar
  1064. scroll_target_x: Scrollbar
  1065. (*)bindingslist: ListBox - on_bindingslist_select
  1066. (*)button_new_keys: Button - get_new_keys & ..._name
  1067. """
  1068. self.builtin_name = tracers.add(
  1069. StringVar(self), self.var_changed_builtin_name)
  1070. self.custom_name = tracers.add(
  1071. StringVar(self), self.var_changed_custom_name)
  1072. self.keyset_source = tracers.add(
  1073. BooleanVar(self), self.var_changed_keyset_source)
  1074. self.keybinding = tracers.add(
  1075. StringVar(self), self.var_changed_keybinding)
  1076. # Create widgets:
  1077. # body and section frames.
  1078. frame_custom = LabelFrame(
  1079. self, borderwidth=2, relief=GROOVE,
  1080. text=' Custom Key Bindings ')
  1081. frame_key_sets = LabelFrame(
  1082. self, borderwidth=2, relief=GROOVE, text=' Key Set ')
  1083. # frame_custom.
  1084. frame_target = Frame(frame_custom)
  1085. target_title = Label(frame_target, text='Action - Key(s)')
  1086. scroll_target_y = Scrollbar(frame_target)
  1087. scroll_target_x = Scrollbar(frame_target, orient=HORIZONTAL)
  1088. self.bindingslist = Listbox(
  1089. frame_target, takefocus=FALSE, exportselection=FALSE)
  1090. self.bindingslist.bind('<ButtonRelease-1>',
  1091. self.on_bindingslist_select)
  1092. scroll_target_y['command'] = self.bindingslist.yview
  1093. scroll_target_x['command'] = self.bindingslist.xview
  1094. self.bindingslist['yscrollcommand'] = scroll_target_y.set
  1095. self.bindingslist['xscrollcommand'] = scroll_target_x.set
  1096. self.button_new_keys = Button(
  1097. frame_custom, text='Get New Keys for Selection',
  1098. command=self.get_new_keys, state='disabled')
  1099. # frame_key_sets.
  1100. frames = [Frame(frame_key_sets, padding=2, borderwidth=0)
  1101. for i in range(2)]
  1102. self.builtin_keyset_on = Radiobutton(
  1103. frames[0], variable=self.keyset_source, value=1,
  1104. command=self.set_keys_type, text='Use a Built-in Key Set')
  1105. self.custom_keyset_on = Radiobutton(
  1106. frames[0], variable=self.keyset_source, value=0,
  1107. command=self.set_keys_type, text='Use a Custom Key Set')
  1108. self.builtinlist = DynOptionMenu(
  1109. frames[0], self.builtin_name, None, command=None)
  1110. self.customlist = DynOptionMenu(
  1111. frames[0], self.custom_name, None, command=None)
  1112. self.button_delete_custom_keys = Button(
  1113. frames[1], text='Delete Custom Key Set',
  1114. command=self.delete_custom_keys)
  1115. self.button_save_custom_keys = Button(
  1116. frames[1], text='Save as New Custom Key Set',
  1117. command=self.save_as_new_key_set)
  1118. self.keys_message = Label(frames[0], borderwidth=2)
  1119. # Pack widgets:
  1120. # body.
  1121. frame_custom.pack(side=BOTTOM, padx=5, pady=5, expand=TRUE, fill=BOTH)
  1122. frame_key_sets.pack(side=BOTTOM, padx=5, pady=5, fill=BOTH)
  1123. # frame_custom.
  1124. self.button_new_keys.pack(side=BOTTOM, fill=X, padx=5, pady=5)
  1125. frame_target.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
  1126. # frame_target.
  1127. frame_target.columnconfigure(0, weight=1)
  1128. frame_target.rowconfigure(1, weight=1)
  1129. target_title.grid(row=0, column=0, columnspan=2, sticky=W)
  1130. self.bindingslist.grid(row=1, column=0, sticky=NSEW)
  1131. scroll_target_y.grid(row=1, column=1, sticky=NS)
  1132. scroll_target_x.grid(row=2, column=0, sticky=EW)
  1133. # frame_key_sets.
  1134. self.builtin_keyset_on.grid(row=0, column=0, sticky=W+NS)
  1135. self.custom_keyset_on.grid(row=1, column=0, sticky=W+NS)
  1136. self.builtinlist.grid(row=0, column=1, sticky=NSEW)
  1137. self.customlist.grid(row=1, column=1, sticky=NSEW)
  1138. self.keys_message.grid(row=0, column=2, sticky=NSEW, padx=5, pady=5)
  1139. self.button_delete_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
  1140. self.button_save_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
  1141. frames[0].pack(side=TOP, fill=BOTH, expand=True)
  1142. frames[1].pack(side=TOP, fill=X, expand=True, pady=2)
  1143. def load_key_cfg(self):
  1144. "Load current configuration settings for the keybinding options."
  1145. # Set current keys type radiobutton.
  1146. self.keyset_source.set(idleConf.GetOption(
  1147. 'main', 'Keys', 'default', type='bool', default=1))
  1148. # Set current keys.
  1149. current_option = idleConf.CurrentKeys()
  1150. # Load available keyset option menus.
  1151. if self.keyset_source.get(): # Default theme selected.
  1152. item_list = idleConf.GetSectionList('default', 'keys')
  1153. item_list.sort()
  1154. self.builtinlist.SetMenu(item_list, current_option)
  1155. item_list = idleConf.GetSectionList('user', 'keys')
  1156. item_list.sort()
  1157. if not item_list:
  1158. self.custom_keyset_on.state(('disabled',))
  1159. self.custom_name.set('- no custom keys -')
  1160. else:
  1161. self.customlist.SetMenu(item_list, item_list[0])
  1162. else: # User key set selected.
  1163. item_list = idleConf.GetSectionList('user', 'keys')
  1164. item_list.sort()
  1165. self.customlist.SetMenu(item_list, current_option)
  1166. item_list = idleConf.GetSectionList('default', 'keys')
  1167. item_list.sort()
  1168. self.builtinlist.SetMenu(item_list, idleConf.default_keys())
  1169. self.set_keys_type()
  1170. # Load keyset element list.
  1171. keyset_name = idleConf.CurrentKeys()
  1172. self.load_keys_list(keyset_name)
  1173. def var_changed_builtin_name(self, *params):
  1174. "Process selection of builtin key set."
  1175. old_keys = (
  1176. 'IDLE Classic Windows',
  1177. 'IDLE Classic Unix',
  1178. 'IDLE Classic Mac',
  1179. 'IDLE Classic OSX',
  1180. )
  1181. value = self.builtin_name.get()
  1182. if value not in old_keys:
  1183. if idleConf.GetOption('main', 'Keys', 'name') not in old_keys:
  1184. changes.add_option('main', 'Keys', 'name', old_keys[0])
  1185. changes.add_option('main', 'Keys', 'name2', value)
  1186. self.keys_message['text'] = 'New key set, see Help'
  1187. else:
  1188. changes.add_option('main', 'Keys', 'name', value)
  1189. changes.add_option('main', 'Keys', 'name2', '')
  1190. self.keys_message['text'] = ''
  1191. self.load_keys_list(value)
  1192. def var_changed_custom_name(self, *params):
  1193. "Process selection of custom key set."
  1194. value = self.custom_name.get()
  1195. if value != '- no custom keys -':
  1196. changes.add_option('main', 'Keys', 'name', value)
  1197. self.load_keys_list(value)
  1198. def var_changed_keyset_source(self, *params):
  1199. "Process toggle between builtin key set and custom key set."
  1200. value = self.keyset_source.get()
  1201. changes.add_option('main', 'Keys', 'default', value)
  1202. if value:
  1203. self.var_changed_builtin_name()
  1204. else:
  1205. self.var_changed_custom_name()
  1206. def var_changed_keybinding(self, *params):
  1207. "Store change to a keybinding."
  1208. value = self.keybinding.get()
  1209. key_set = self.custom_name.get()
  1210. event = self.bindingslist.get(ANCHOR).split()[0]
  1211. if idleConf.IsCoreBinding(event):
  1212. changes.add_option('keys', key_set, event, value)
  1213. else: # Event is an extension binding.
  1214. ext_name = idleConf.GetExtnNameForEvent(event)
  1215. ext_keybind_section = ext_name + '_cfgBindings'
  1216. changes.add_option('extensions', ext_keybind_section, event, value)
  1217. def set_keys_type(self):
  1218. "Set available screen options based on builtin or custom key set."
  1219. if self.keyset_source.get():
  1220. self.builtinlist['state'] = 'normal'
  1221. self.customlist['state'] = 'disabled'
  1222. self.button_delete_custom_keys.state(('disabled',))
  1223. else:
  1224. self.builtinlist['state'] = 'disabled'
  1225. self.custom_keyset_on.state(('!disabled',))
  1226. self.customlist['state'] = 'normal'
  1227. self.button_delete_custom_keys.state(('!disabled',))
  1228. def get_new_keys(self):
  1229. """Handle event to change key binding for selected line.
  1230. A selection of a key/binding in the list of current
  1231. bindings pops up a dialog to enter a new binding. If
  1232. the current key set is builtin and a binding has
  1233. changed, then a name for a custom key set needs to be
  1234. entered for the change to be applied.
  1235. """
  1236. list_index = self.bindingslist.index(ANCHOR)
  1237. binding = self.bindingslist.get(list_index)
  1238. bind_name = binding.split()[0]
  1239. if self.keyset_source.get():
  1240. current_key_set_name = self.builtin_name.get()
  1241. else:
  1242. current_key_set_name = self.custom_name.get()
  1243. current_bindings = idleConf.GetCurrentKeySet()
  1244. if current_key_set_name in changes['keys']: # unsaved changes
  1245. key_set_changes = changes['keys'][current_key_set_name]
  1246. for event in key_set_changes:
  1247. current_bindings[event] = key_set_changes[event].split()
  1248. current_key_sequences = list(current_bindings.values())
  1249. new_keys = GetKeysWindow(self, 'Get New Keys', bind_name,
  1250. current_key_sequences).result
  1251. if new_keys:
  1252. if self.keyset_source.get(): # Current key set is a built-in.
  1253. message = ('Your changes will be saved as a new Custom Key Set.'
  1254. ' Enter a name for your new Custom Key Set below.')
  1255. new_keyset = self.get_new_keys_name(message)
  1256. if not new_keyset: # User cancelled custom key set creation.
  1257. self.bindingslist.select_set(list_index)
  1258. self.bindingslist.select_anchor(list_index)
  1259. return
  1260. else: # Create new custom key set based on previously active key set.
  1261. self.create_new_key_set(new_keyset)
  1262. self.bindingslist.delete(list_index)
  1263. self.bindingslist.insert(list_index, bind_name+' - '+new_keys)
  1264. self.bindingslist.select_set(list_index)
  1265. self.bindingslist.select_anchor(list_index)
  1266. self.keybinding.set(new_keys)
  1267. else:
  1268. self.bindingslist.select_set(list_index)
  1269. self.bindingslist.select_anchor(list_index)
  1270. def get_new_keys_name(self, message):
  1271. "Return new key set name from query popup."
  1272. used_names = (idleConf.GetSectionList('user', 'keys') +
  1273. idleConf.GetSectionList('default', 'keys'))
  1274. new_keyset = SectionName(
  1275. self, 'New Custom Key Set', message, used_names).result
  1276. return new_keyset
  1277. def save_as_new_key_set(self):
  1278. "Prompt for name of new key set and save changes using that name."
  1279. new_keys_name = self.get_new_keys_name('New Key Set Name:')
  1280. if new_keys_name:
  1281. self.create_new_key_set(new_keys_name)
  1282. def on_bindingslist_select(self, event):
  1283. "Activate button to assign new keys to selected action."
  1284. self.button_new_keys.state(('!disabled',))
  1285. def create_new_key_set(self, new_key_set_name):
  1286. """Create a new custom key set with the given name.
  1287. Copy the bindings/keys from the previously active keyset
  1288. to the new keyset and activate the new custom keyset.
  1289. """
  1290. if self.keyset_source.get():
  1291. prev_key_set_name = self.builtin_name.get()
  1292. else:
  1293. prev_key_set_name = self.custom_name.get()
  1294. prev_keys = idleConf.GetCoreKeys(prev_key_set_name)
  1295. new_keys = {}
  1296. for event in prev_keys: # Add key set to changed items.
  1297. event_name = event[2:-2] # Trim off the angle brackets.
  1298. binding = ' '.join(prev_keys[event])
  1299. new_keys[event_name] = binding
  1300. # Handle any unsaved changes to prev key set.
  1301. if prev_key_set_name in changes['keys']:
  1302. key_set_changes = changes['keys'][prev_key_set_name]
  1303. for event in key_set_changes:
  1304. new_keys[event] = key_set_changes[event]
  1305. # Save the new key set.
  1306. self.save_new_key_set(new_key_set_name, new_keys)
  1307. # Change GUI over to the new key set.
  1308. custom_key_list = idleConf.GetSectionList('user', 'keys')
  1309. custom_key_list.sort()
  1310. self.customlist.SetMenu(custom_key_list, new_key_set_name)
  1311. self.keyset_source.set(0)
  1312. self.set_keys_type()
  1313. def load_keys_list(self, keyset_name):
  1314. """Reload the list of action/key binding pairs for the active key set.
  1315. An action/key binding can be selected to change the key binding.
  1316. """
  1317. reselect = False
  1318. if self.bindingslist.curselection():
  1319. reselect = True
  1320. list_index = self.bindingslist.index(ANCHOR)
  1321. keyset = idleConf.GetKeySet(keyset_name)
  1322. bind_names = list(keyset.keys())
  1323. bind_names.sort()
  1324. self.bindingslist.delete(0, END)
  1325. for bind_name in bind_names:
  1326. key = ' '.join(keyset[bind_name])
  1327. bind_name = bind_name[2:-2] # Trim off the angle brackets.
  1328. if keyset_name in changes['keys']:
  1329. # Handle any unsaved changes to this key set.
  1330. if bind_name in changes['keys'][keyset_name]:
  1331. key = changes['keys'][keyset_name][bind_name]
  1332. self.bindingslist.insert(END, bind_name+' - '+key)
  1333. if reselect:
  1334. self.bindingslist.see(list_index)
  1335. self.bindingslist.select_set(list_index)
  1336. self.bindingslist.select_anchor(list_index)
  1337. @staticmethod
  1338. def save_new_key_set(keyset_name, keyset):
  1339. """Save a newly created core key set.
  1340. Add keyset to idleConf.userCfg['keys'], not to disk.
  1341. If the keyset doesn't exist, it is created. The
  1342. binding/keys are taken from the keyset argument.
  1343. keyset_name - string, the name of the new key set
  1344. keyset - dictionary containing the new keybindings
  1345. """
  1346. idleConf.userCfg['keys'].AddSection(keyset_name)
  1347. for event in keyset:
  1348. value = keyset[event]
  1349. idleConf.userCfg['keys'].SetOption(keyset_name, event, value)
  1350. def askyesno(self, *args, **kwargs):
  1351. # Make testing easier. Could change implementation.
  1352. return messagebox.askyesno(*args, **kwargs)
  1353. def delete_custom_keys(self):
  1354. """Handle event to delete a custom key set.
  1355. Applying the delete deactivates the current configuration and
  1356. reverts to the default. The custom key set is permanently
  1357. deleted from the config file.
  1358. """
  1359. keyset_name = self.custom_name.get()
  1360. delmsg = 'Are you sure you wish to delete the key set %r ?'
  1361. if not self.askyesno(
  1362. 'Delete Key Set', delmsg % keyset_name, parent=self):
  1363. return
  1364. self.cd.deactivate_current_config()
  1365. # Remove key set from changes, config, and file.
  1366. changes.delete_section('keys', keyset_name)
  1367. # Reload user key set list.
  1368. item_list = idleConf.GetSectionList('user', 'keys')
  1369. item_list.sort()
  1370. if not item_list:
  1371. self.custom_keyset_on.state(('disabled',))
  1372. self.customlist.SetMenu(item_list, '- no custom keys -')
  1373. else:
  1374. self.customlist.SetMenu(item_list, item_list[0])
  1375. # Revert to default key set.
  1376. self.keyset_source.set(idleConf.defaultCfg['main']
  1377. .Get('Keys', 'default'))
  1378. self.builtin_name.set(idleConf.defaultCfg['main'].Get('Keys', 'name')
  1379. or idleConf.default_keys())
  1380. # User can't back out of these changes, they must be applied now.
  1381. changes.save_all()
  1382. self.extpage.save_all_changed_extensions()
  1383. self.cd.activate_config_changes()
  1384. self.set_keys_type()
  1385. class WinPage(Frame):
  1386. def __init__(self, master):
  1387. super().__init__(master)
  1388. self.init_validators()
  1389. self.create_page_windows()
  1390. self.load_windows_cfg()
  1391. def init_validators(self):
  1392. digits_or_empty_re = re.compile(r'[0-9]*')
  1393. def is_digits_or_empty(s):
  1394. "Return 's is blank or contains only digits'"
  1395. return digits_or_empty_re.fullmatch(s) is not None
  1396. self.digits_only = (self.register(is_digits_or_empty), '%P',)
  1397. def create_page_windows(self):
  1398. """Return frame of widgets for Windows tab.
  1399. Enable users to provisionally change general window options.
  1400. Function load_windows_cfg initializes tk variable idleConf.
  1401. Radiobuttons startup_shell_on and startup_editor_on set var
  1402. startup_edit. Entry boxes win_width_int and win_height_int set var
  1403. win_width and win_height. Setting var_name invokes the default
  1404. callback that adds option to changes.
  1405. Widgets for WinPage(Frame): > vars, bound to self
  1406. frame_window: LabelFrame
  1407. frame_run: Frame
  1408. startup_title: Label
  1409. startup_editor_on: Radiobutton > startup_edit
  1410. startup_shell_on: Radiobutton > startup_edit
  1411. frame_win_size: Frame
  1412. win_size_title: Label
  1413. win_width_title: Label
  1414. win_width_int: Entry > win_width
  1415. win_height_title: Label
  1416. win_height_int: Entry > win_height
  1417. frame_cursor: Frame
  1418. indent_title: Label
  1419. indent_chooser: Spinbox > indent_spaces
  1420. blink_on: Checkbutton > cursor_blink
  1421. frame_autocomplete: Frame
  1422. auto_wait_title: Label
  1423. auto_wait_int: Entry > autocomplete_wait
  1424. frame_paren1: Frame
  1425. paren_style_title: Label
  1426. paren_style_type: OptionMenu > paren_style
  1427. frame_paren2: Frame
  1428. paren_time_title: Label
  1429. paren_flash_time: Entry > flash_delay
  1430. bell_on: Checkbutton > paren_bell
  1431. frame_format: Frame
  1432. format_width_title: Label
  1433. format_width_int: Entry > format_width
  1434. """
  1435. # Integer values need StringVar because int('') raises.
  1436. self.startup_edit = tracers.add(
  1437. IntVar(self), ('main', 'General', 'editor-on-startup'))
  1438. self.win_width = tracers.add(
  1439. StringVar(self), ('main', 'EditorWindow', 'width'))
  1440. self.win_height = tracers.add(
  1441. StringVar(self), ('main', 'EditorWindow', 'height'))
  1442. self.indent_spaces = tracers.add(
  1443. StringVar(self), ('main', 'Indent', 'num-spaces'))
  1444. self.cursor_blink = tracers.add(
  1445. BooleanVar(self), ('main', 'EditorWindow', 'cursor-blink'))
  1446. self.autocomplete_wait = tracers.add(
  1447. StringVar(self), ('extensions', 'AutoComplete', 'popupwait'))
  1448. self.paren_style = tracers.add(
  1449. StringVar(self), ('extensions', 'ParenMatch', 'style'))
  1450. self.flash_delay = tracers.add(
  1451. StringVar(self), ('extensions', 'ParenMatch', 'flash-delay'))
  1452. self.paren_bell = tracers.add(
  1453. BooleanVar(self), ('extensions', 'ParenMatch', 'bell'))
  1454. self.format_width = tracers.add(
  1455. StringVar(self), ('extensions', 'FormatParagraph', 'max-width'))
  1456. # Create widgets:
  1457. frame_window = LabelFrame(self, borderwidth=2, relief=GROOVE,
  1458. text=' Window Preferences')
  1459. frame_run = Frame(frame_window, borderwidth=0)
  1460. startup_title = Label(frame_run, text='At Startup')
  1461. self.startup_editor_on = Radiobutton(
  1462. frame_run, variable=self.startup_edit, value=1,
  1463. text="Open Edit Window")
  1464. self.startup_shell_on = Radiobutton(
  1465. frame_run, variable=self.startup_edit, value=0,
  1466. text='Open Shell Window')
  1467. frame_win_size = Frame(frame_window, borderwidth=0)
  1468. win_size_title = Label(
  1469. frame_win_size, text='Initial Window Size (in characters)')
  1470. win_width_title = Label(frame_win_size, text='Width')
  1471. self.win_width_int = Entry(
  1472. frame_win_size, textvariable=self.win_width, width=3,
  1473. validatecommand=self.digits_only, validate='key',
  1474. )
  1475. win_height_title = Label(frame_win_size, text='Height')
  1476. self.win_height_int = Entry(
  1477. frame_win_size, textvariable=self.win_height, width=3,
  1478. validatecommand=self.digits_only, validate='key',
  1479. )
  1480. frame_cursor = Frame(frame_window, borderwidth=0)
  1481. indent_title = Label(frame_cursor,
  1482. text='Indent spaces (4 is standard)')
  1483. try:
  1484. self.indent_chooser = Spinbox(
  1485. frame_cursor, textvariable=self.indent_spaces,
  1486. from_=1, to=10, width=2,
  1487. validatecommand=self.digits_only, validate='key')
  1488. except TclError:
  1489. self.indent_chooser = Combobox(
  1490. frame_cursor, textvariable=self.indent_spaces,
  1491. state="readonly", values=list(range(1,11)), width=3)
  1492. cursor_blink_title = Label(frame_cursor, text='Cursor Blink')
  1493. self.cursor_blink_bool = Checkbutton(frame_cursor, text="Cursor blink",
  1494. variable=self.cursor_blink)
  1495. frame_autocomplete = Frame(frame_window, borderwidth=0,)
  1496. auto_wait_title = Label(frame_autocomplete,
  1497. text='Completions Popup Wait (milliseconds)')
  1498. self.auto_wait_int = Entry(
  1499. frame_autocomplete, textvariable=self.autocomplete_wait,
  1500. width=6, validatecommand=self.digits_only, validate='key')
  1501. frame_paren1 = Frame(frame_window, borderwidth=0)
  1502. paren_style_title = Label(frame_paren1, text='Paren Match Style')
  1503. self.paren_style_type = OptionMenu(
  1504. frame_paren1, self.paren_style, 'expression',
  1505. "opener","parens","expression")
  1506. frame_paren2 = Frame(frame_window, borderwidth=0)
  1507. paren_time_title = Label(
  1508. frame_paren2, text='Time Match Displayed (milliseconds)\n'
  1509. '(0 is until next input)')
  1510. self.paren_flash_time = Entry(
  1511. frame_paren2, textvariable=self.flash_delay, width=6,
  1512. validatecommand=self.digits_only, validate='key')
  1513. self.bell_on = Checkbutton(
  1514. frame_paren2, text="Bell on Mismatch", variable=self.paren_bell)
  1515. frame_format = Frame(frame_window, borderwidth=0)
  1516. format_width_title = Label(frame_format,
  1517. text='Format Paragraph Max Width')
  1518. self.format_width_int = Entry(
  1519. frame_format, textvariable=self.format_width, width=4,
  1520. validatecommand=self.digits_only, validate='key',
  1521. )
  1522. # Pack widgets:
  1523. frame_window.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
  1524. # frame_run.
  1525. frame_run.pack(side=TOP, padx=5, pady=0, fill=X)
  1526. startup_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
  1527. self.startup_shell_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
  1528. self.startup_editor_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
  1529. # frame_win_size.
  1530. frame_win_size.pack(side=TOP, padx=5, pady=0, fill=X)
  1531. win_size_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
  1532. self.win_height_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
  1533. win_height_title.pack(side=RIGHT, anchor=E, pady=5)
  1534. self.win_width_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
  1535. win_width_title.pack(side=RIGHT, anchor=E, pady=5)
  1536. # frame_cursor.
  1537. frame_cursor.pack(side=TOP, padx=5, pady=0, fill=X)
  1538. indent_title.pack(side=LEFT, anchor=W, padx=5)
  1539. self.indent_chooser.pack(side=LEFT, anchor=W, padx=10)
  1540. self.cursor_blink_bool.pack(side=RIGHT, anchor=E, padx=15, pady=5)
  1541. # frame_autocomplete.
  1542. frame_autocomplete.pack(side=TOP, padx=5, pady=0, fill=X)
  1543. auto_wait_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
  1544. self.auto_wait_int.pack(side=TOP, padx=10, pady=5)
  1545. # frame_paren.
  1546. frame_paren1.pack(side=TOP, padx=5, pady=0, fill=X)
  1547. paren_style_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
  1548. self.paren_style_type.pack(side=TOP, padx=10, pady=5)
  1549. frame_paren2.pack(side=TOP, padx=5, pady=0, fill=X)
  1550. paren_time_title.pack(side=LEFT, anchor=W, padx=5)
  1551. self.bell_on.pack(side=RIGHT, anchor=E, padx=15, pady=5)
  1552. self.paren_flash_time.pack(side=TOP, anchor=W, padx=15, pady=5)
  1553. # frame_format.
  1554. frame_format.pack(side=TOP, padx=5, pady=0, fill=X)
  1555. format_width_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
  1556. self.format_width_int.pack(side=TOP, padx=10, pady=5)
  1557. def load_windows_cfg(self):
  1558. # Set variables for all windows.
  1559. self.startup_edit.set(idleConf.GetOption(
  1560. 'main', 'General', 'editor-on-startup', type='bool'))
  1561. self.win_width.set(idleConf.GetOption(
  1562. 'main', 'EditorWindow', 'width', type='int'))
  1563. self.win_height.set(idleConf.GetOption(
  1564. 'main', 'EditorWindow', 'height', type='int'))
  1565. self.indent_spaces.set(idleConf.GetOption(
  1566. 'main', 'Indent', 'num-spaces', type='int'))
  1567. self.cursor_blink.set(idleConf.GetOption(
  1568. 'main', 'EditorWindow', 'cursor-blink', type='bool'))
  1569. self.autocomplete_wait.set(idleConf.GetOption(
  1570. 'extensions', 'AutoComplete', 'popupwait', type='int'))
  1571. self.paren_style.set(idleConf.GetOption(
  1572. 'extensions', 'ParenMatch', 'style'))
  1573. self.flash_delay.set(idleConf.GetOption(
  1574. 'extensions', 'ParenMatch', 'flash-delay', type='int'))
  1575. self.paren_bell.set(idleConf.GetOption(
  1576. 'extensions', 'ParenMatch', 'bell'))
  1577. self.format_width.set(idleConf.GetOption(
  1578. 'extensions', 'FormatParagraph', 'max-width', type='int'))
  1579. class ShedPage(Frame):
  1580. def __init__(self, master):
  1581. super().__init__(master)
  1582. self.init_validators()
  1583. self.create_page_shed()
  1584. self.load_shelled_cfg()
  1585. def init_validators(self):
  1586. digits_or_empty_re = re.compile(r'[0-9]*')
  1587. def is_digits_or_empty(s):
  1588. "Return 's is blank or contains only digits'"
  1589. return digits_or_empty_re.fullmatch(s) is not None
  1590. self.digits_only = (self.register(is_digits_or_empty), '%P',)
  1591. def create_page_shed(self):
  1592. """Return frame of widgets for Shell/Ed tab.
  1593. Enable users to provisionally change shell and editor options.
  1594. Function load_shed_cfg initializes tk variables using idleConf.
  1595. Entry box auto_squeeze_min_lines_int sets
  1596. auto_squeeze_min_lines_int. Setting var_name invokes the
  1597. default callback that adds option to changes.
  1598. Widgets for ShedPage(Frame): (*) widgets bound to self
  1599. frame_shell: LabelFrame
  1600. frame_auto_squeeze_min_lines: Frame
  1601. auto_squeeze_min_lines_title: Label
  1602. (*)auto_squeeze_min_lines_int: Entry -
  1603. auto_squeeze_min_lines
  1604. frame_editor: LabelFrame
  1605. frame_save: Frame
  1606. run_save_title: Label
  1607. (*)save_ask_on: Radiobutton - autosave
  1608. (*)save_auto_on: Radiobutton - autosave
  1609. frame_format: Frame
  1610. format_width_title: Label
  1611. (*)format_width_int: Entry - format_width
  1612. frame_line_numbers_default: Frame
  1613. line_numbers_default_title: Label
  1614. (*)line_numbers_default_bool: Checkbutton - line_numbers_default
  1615. frame_context: Frame
  1616. context_title: Label
  1617. (*)context_int: Entry - context_lines
  1618. """
  1619. # Integer values need StringVar because int('') raises.
  1620. self.auto_squeeze_min_lines = tracers.add(
  1621. StringVar(self), ('main', 'PyShell', 'auto-squeeze-min-lines'))
  1622. self.autosave = tracers.add(
  1623. IntVar(self), ('main', 'General', 'autosave'))
  1624. self.line_numbers_default = tracers.add(
  1625. BooleanVar(self),
  1626. ('main', 'EditorWindow', 'line-numbers-default'))
  1627. self.context_lines = tracers.add(
  1628. StringVar(self), ('extensions', 'CodeContext', 'maxlines'))
  1629. # Create widgets:
  1630. frame_shell = LabelFrame(self, borderwidth=2, relief=GROOVE,
  1631. text=' Shell Preferences')
  1632. frame_editor = LabelFrame(self, borderwidth=2, relief=GROOVE,
  1633. text=' Editor Preferences')
  1634. # Frame_shell.
  1635. frame_auto_squeeze_min_lines = Frame(frame_shell, borderwidth=0)
  1636. auto_squeeze_min_lines_title = Label(frame_auto_squeeze_min_lines,
  1637. text='Auto-Squeeze Min. Lines:')
  1638. self.auto_squeeze_min_lines_int = Entry(
  1639. frame_auto_squeeze_min_lines, width=4,
  1640. textvariable=self.auto_squeeze_min_lines,
  1641. validatecommand=self.digits_only, validate='key',
  1642. )
  1643. # Frame_editor.
  1644. frame_save = Frame(frame_editor, borderwidth=0)
  1645. run_save_title = Label(frame_save, text='At Start of Run (F5) ')
  1646. self.save_ask_on = Radiobutton(
  1647. frame_save, variable=self.autosave, value=0,
  1648. text="Prompt to Save")
  1649. self.save_auto_on = Radiobutton(
  1650. frame_save, variable=self.autosave, value=1,
  1651. text='No Prompt')
  1652. frame_line_numbers_default = Frame(frame_editor, borderwidth=0)
  1653. line_numbers_default_title = Label(
  1654. frame_line_numbers_default, text='Show line numbers in new windows')
  1655. self.line_numbers_default_bool = Checkbutton(
  1656. frame_line_numbers_default,
  1657. variable=self.line_numbers_default,
  1658. width=1)
  1659. frame_context = Frame(frame_editor, borderwidth=0)
  1660. context_title = Label(frame_context, text='Max Context Lines :')
  1661. self.context_int = Entry(
  1662. frame_context, textvariable=self.context_lines, width=3,
  1663. validatecommand=self.digits_only, validate='key',
  1664. )
  1665. # Pack widgets:
  1666. frame_shell.pack(side=TOP, padx=5, pady=5, fill=BOTH)
  1667. Label(self).pack() # Spacer -- better solution?
  1668. frame_editor.pack(side=TOP, padx=5, pady=5, fill=BOTH)
  1669. # frame_auto_squeeze_min_lines
  1670. frame_auto_squeeze_min_lines.pack(side=TOP, padx=5, pady=0, fill=X)
  1671. auto_squeeze_min_lines_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
  1672. self.auto_squeeze_min_lines_int.pack(side=TOP, padx=5, pady=5)
  1673. # frame_save.
  1674. frame_save.pack(side=TOP, padx=5, pady=0, fill=X)
  1675. run_save_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
  1676. self.save_auto_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
  1677. self.save_ask_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
  1678. # frame_line_numbers_default.
  1679. frame_line_numbers_default.pack(side=TOP, padx=5, pady=0, fill=X)
  1680. line_numbers_default_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
  1681. self.line_numbers_default_bool.pack(side=LEFT, padx=5, pady=5)
  1682. # frame_context.
  1683. frame_context.pack(side=TOP, padx=5, pady=0, fill=X)
  1684. context_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
  1685. self.context_int.pack(side=TOP, padx=5, pady=5)
  1686. def load_shelled_cfg(self):
  1687. # Set variables for shell windows.
  1688. self.auto_squeeze_min_lines.set(idleConf.GetOption(
  1689. 'main', 'PyShell', 'auto-squeeze-min-lines', type='int'))
  1690. # Set variables for editor windows.
  1691. self.autosave.set(idleConf.GetOption(
  1692. 'main', 'General', 'autosave', default=0, type='bool'))
  1693. self.line_numbers_default.set(idleConf.GetOption(
  1694. 'main', 'EditorWindow', 'line-numbers-default', type='bool'))
  1695. self.context_lines.set(idleConf.GetOption(
  1696. 'extensions', 'CodeContext', 'maxlines', type='int'))
  1697. class ExtPage(Frame):
  1698. def __init__(self, master):
  1699. super().__init__(master)
  1700. self.ext_defaultCfg = idleConf.defaultCfg['extensions']
  1701. self.ext_userCfg = idleConf.userCfg['extensions']
  1702. self.is_int = self.register(is_int)
  1703. self.load_extensions()
  1704. self.create_page_extensions() # Requires extension names.
  1705. def create_page_extensions(self):
  1706. """Configure IDLE feature extensions and help menu extensions.
  1707. List the feature extensions and a configuration box for the
  1708. selected extension. Help menu extensions are in a HelpFrame.
  1709. This code reads the current configuration using idleConf,
  1710. supplies a GUI interface to change the configuration values,
  1711. and saves the changes using idleConf.
  1712. Some changes may require restarting IDLE. This depends on each
  1713. extension's implementation.
  1714. All values are treated as text, and it is up to the user to
  1715. supply reasonable values. The only exception to this are the
  1716. 'enable*' options, which are boolean, and can be toggled with a
  1717. True/False button.
  1718. Methods:
  1719. extension_selected: Handle selection from list.
  1720. create_extension_frame: Hold widgets for one extension.
  1721. set_extension_value: Set in userCfg['extensions'].
  1722. save_all_changed_extensions: Call extension page Save().
  1723. """
  1724. self.extension_names = StringVar(self)
  1725. frame_ext = LabelFrame(self, borderwidth=2, relief=GROOVE,
  1726. text=' Feature Extensions ')
  1727. self.frame_help = HelpFrame(self, borderwidth=2, relief=GROOVE,
  1728. text=' Help Menu Extensions ')
  1729. frame_ext.rowconfigure(0, weight=1)
  1730. frame_ext.columnconfigure(2, weight=1)
  1731. self.extension_list = Listbox(frame_ext, listvariable=self.extension_names,
  1732. selectmode='browse')
  1733. self.extension_list.bind('<<ListboxSelect>>', self.extension_selected)
  1734. scroll = Scrollbar(frame_ext, command=self.extension_list.yview)
  1735. self.extension_list.yscrollcommand=scroll.set
  1736. self.details_frame = LabelFrame(frame_ext, width=250, height=250)
  1737. self.extension_list.grid(column=0, row=0, sticky='nws')
  1738. scroll.grid(column=1, row=0, sticky='ns')
  1739. self.details_frame.grid(column=2, row=0, sticky='nsew', padx=[10, 0])
  1740. frame_ext.configure(padding=10)
  1741. self.config_frame = {}
  1742. self.current_extension = None
  1743. self.outerframe = self # TEMPORARY
  1744. self.tabbed_page_set = self.extension_list # TEMPORARY
  1745. # Create the frame holding controls for each extension.
  1746. ext_names = ''
  1747. for ext_name in sorted(self.extensions):
  1748. self.create_extension_frame(ext_name)
  1749. ext_names = ext_names + '{' + ext_name + '} '
  1750. self.extension_names.set(ext_names)
  1751. self.extension_list.selection_set(0)
  1752. self.extension_selected(None)
  1753. frame_ext.grid(row=0, column=0, sticky='nsew')
  1754. Label(self).grid(row=1, column=0) # Spacer. Replace with config?
  1755. self.frame_help.grid(row=2, column=0, sticky='sew')
  1756. def load_extensions(self):
  1757. "Fill self.extensions with data from the default and user configs."
  1758. self.extensions = {}
  1759. for ext_name in idleConf.GetExtensions(active_only=False):
  1760. # Former built-in extensions are already filtered out.
  1761. self.extensions[ext_name] = []
  1762. for ext_name in self.extensions:
  1763. opt_list = sorted(self.ext_defaultCfg.GetOptionList(ext_name))
  1764. # Bring 'enable' options to the beginning of the list.
  1765. enables = [opt_name for opt_name in opt_list
  1766. if opt_name.startswith('enable')]
  1767. for opt_name in enables:
  1768. opt_list.remove(opt_name)
  1769. opt_list = enables + opt_list
  1770. for opt_name in opt_list:
  1771. def_str = self.ext_defaultCfg.Get(
  1772. ext_name, opt_name, raw=True)
  1773. try:
  1774. def_obj = {'True':True, 'False':False}[def_str]
  1775. opt_type = 'bool'
  1776. except KeyError:
  1777. try:
  1778. def_obj = int(def_str)
  1779. opt_type = 'int'
  1780. except ValueError:
  1781. def_obj = def_str
  1782. opt_type = None
  1783. try:
  1784. value = self.ext_userCfg.Get(
  1785. ext_name, opt_name, type=opt_type, raw=True,
  1786. default=def_obj)
  1787. except ValueError: # Need this until .Get fixed.
  1788. value = def_obj # Bad values overwritten by entry.
  1789. var = StringVar(self)
  1790. var.set(str(value))
  1791. self.extensions[ext_name].append({'name': opt_name,
  1792. 'type': opt_type,
  1793. 'default': def_str,
  1794. 'value': value,
  1795. 'var': var,
  1796. })
  1797. def extension_selected(self, event):
  1798. "Handle selection of an extension from the list."
  1799. newsel = self.extension_list.curselection()
  1800. if newsel:
  1801. newsel = self.extension_list.get(newsel)
  1802. if newsel is None or newsel != self.current_extension:
  1803. if self.current_extension:
  1804. self.details_frame.config(text='')
  1805. self.config_frame[self.current_extension].grid_forget()
  1806. self.current_extension = None
  1807. if newsel:
  1808. self.details_frame.config(text=newsel)
  1809. self.config_frame[newsel].grid(column=0, row=0, sticky='nsew')
  1810. self.current_extension = newsel
  1811. def create_extension_frame(self, ext_name):
  1812. """Create a frame holding the widgets to configure one extension"""
  1813. f = VerticalScrolledFrame(self.details_frame, height=250, width=250)
  1814. self.config_frame[ext_name] = f
  1815. entry_area = f.interior
  1816. # Create an entry for each configuration option.
  1817. for row, opt in enumerate(self.extensions[ext_name]):
  1818. # Create a row with a label and entry/checkbutton.
  1819. label = Label(entry_area, text=opt['name'])
  1820. label.grid(row=row, column=0, sticky=NW)
  1821. var = opt['var']
  1822. if opt['type'] == 'bool':
  1823. Checkbutton(entry_area, variable=var,
  1824. onvalue='True', offvalue='False', width=8
  1825. ).grid(row=row, column=1, sticky=W, padx=7)
  1826. elif opt['type'] == 'int':
  1827. Entry(entry_area, textvariable=var, validate='key',
  1828. validatecommand=(self.is_int, '%P'), width=10
  1829. ).grid(row=row, column=1, sticky=NSEW, padx=7)
  1830. else: # type == 'str'
  1831. # Limit size to fit non-expanding space with larger font.
  1832. Entry(entry_area, textvariable=var, width=15
  1833. ).grid(row=row, column=1, sticky=NSEW, padx=7)
  1834. return
  1835. def set_extension_value(self, section, opt):
  1836. """Return True if the configuration was added or changed.
  1837. If the value is the same as the default, then remove it
  1838. from user config file.
  1839. """
  1840. name = opt['name']
  1841. default = opt['default']
  1842. value = opt['var'].get().strip() or default
  1843. opt['var'].set(value)
  1844. # if self.defaultCfg.has_section(section):
  1845. # Currently, always true; if not, indent to return.
  1846. if (value == default):
  1847. return self.ext_userCfg.RemoveOption(section, name)
  1848. # Set the option.
  1849. return self.ext_userCfg.SetOption(section, name, value)
  1850. def save_all_changed_extensions(self):
  1851. """Save configuration changes to the user config file.
  1852. Attributes accessed:
  1853. extensions
  1854. Methods:
  1855. set_extension_value
  1856. """
  1857. has_changes = False
  1858. for ext_name in self.extensions:
  1859. options = self.extensions[ext_name]
  1860. for opt in options:
  1861. if self.set_extension_value(ext_name, opt):
  1862. has_changes = True
  1863. if has_changes:
  1864. self.ext_userCfg.Save()
  1865. class HelpFrame(LabelFrame):
  1866. def __init__(self, master, **cfg):
  1867. super().__init__(master, **cfg)
  1868. self.create_frame_help()
  1869. self.load_helplist()
  1870. def create_frame_help(self):
  1871. """Create LabelFrame for additional help menu sources.
  1872. load_helplist loads list user_helplist with
  1873. name, position pairs and copies names to listbox helplist.
  1874. Clicking a name invokes help_source selected. Clicking
  1875. button_helplist_name invokes helplist_item_name, which also
  1876. changes user_helplist. These functions all call
  1877. set_add_delete_state. All but load call update_help_changes to
  1878. rewrite changes['main']['HelpFiles'].
  1879. Widgets for HelpFrame(LabelFrame): (*) widgets bound to self
  1880. frame_helplist: Frame
  1881. (*)helplist: ListBox
  1882. scroll_helplist: Scrollbar
  1883. frame_buttons: Frame
  1884. (*)button_helplist_edit
  1885. (*)button_helplist_add
  1886. (*)button_helplist_remove
  1887. """
  1888. # self = frame_help in dialog (until ExtPage class).
  1889. frame_helplist = Frame(self)
  1890. self.helplist = Listbox(
  1891. frame_helplist, height=5, takefocus=True,
  1892. exportselection=FALSE)
  1893. scroll_helplist = Scrollbar(frame_helplist)
  1894. scroll_helplist['command'] = self.helplist.yview
  1895. self.helplist['yscrollcommand'] = scroll_helplist.set
  1896. self.helplist.bind('<ButtonRelease-1>', self.help_source_selected)
  1897. frame_buttons = Frame(self)
  1898. self.button_helplist_edit = Button(
  1899. frame_buttons, text='Edit', state='disabled',
  1900. width=8, command=self.helplist_item_edit)
  1901. self.button_helplist_add = Button(
  1902. frame_buttons, text='Add',
  1903. width=8, command=self.helplist_item_add)
  1904. self.button_helplist_remove = Button(
  1905. frame_buttons, text='Remove', state='disabled',
  1906. width=8, command=self.helplist_item_remove)
  1907. # Pack frame_help.
  1908. frame_helplist.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
  1909. self.helplist.pack(side=LEFT, anchor=E, expand=TRUE, fill=BOTH)
  1910. scroll_helplist.pack(side=RIGHT, anchor=W, fill=Y)
  1911. frame_buttons.pack(side=RIGHT, padx=5, pady=5, fill=Y)
  1912. self.button_helplist_edit.pack(side=TOP, anchor=W, pady=5)
  1913. self.button_helplist_add.pack(side=TOP, anchor=W)
  1914. self.button_helplist_remove.pack(side=TOP, anchor=W, pady=5)
  1915. def help_source_selected(self, event):
  1916. "Handle event for selecting additional help."
  1917. self.set_add_delete_state()
  1918. def set_add_delete_state(self):
  1919. "Toggle the state for the help list buttons based on list entries."
  1920. if self.helplist.size() < 1: # No entries in list.
  1921. self.button_helplist_edit.state(('disabled',))
  1922. self.button_helplist_remove.state(('disabled',))
  1923. else: # Some entries.
  1924. if self.helplist.curselection(): # There currently is a selection.
  1925. self.button_helplist_edit.state(('!disabled',))
  1926. self.button_helplist_remove.state(('!disabled',))
  1927. else: # There currently is not a selection.
  1928. self.button_helplist_edit.state(('disabled',))
  1929. self.button_helplist_remove.state(('disabled',))
  1930. def helplist_item_add(self):
  1931. """Handle add button for the help list.
  1932. Query for name and location of new help sources and add
  1933. them to the list.
  1934. """
  1935. help_source = HelpSource(self, 'New Help Source').result
  1936. if help_source:
  1937. self.user_helplist.append(help_source)
  1938. self.helplist.insert(END, help_source[0])
  1939. self.update_help_changes()
  1940. def helplist_item_edit(self):
  1941. """Handle edit button for the help list.
  1942. Query with existing help source information and update
  1943. config if the values are changed.
  1944. """
  1945. item_index = self.helplist.index(ANCHOR)
  1946. help_source = self.user_helplist[item_index]
  1947. new_help_source = HelpSource(
  1948. self, 'Edit Help Source',
  1949. menuitem=help_source[0],
  1950. filepath=help_source[1],
  1951. ).result
  1952. if new_help_source and new_help_source != help_source:
  1953. self.user_helplist[item_index] = new_help_source
  1954. self.helplist.delete(item_index)
  1955. self.helplist.insert(item_index, new_help_source[0])
  1956. self.update_help_changes()
  1957. self.set_add_delete_state() # Selected will be un-selected
  1958. def helplist_item_remove(self):
  1959. """Handle remove button for the help list.
  1960. Delete the help list item from config.
  1961. """
  1962. item_index = self.helplist.index(ANCHOR)
  1963. del(self.user_helplist[item_index])
  1964. self.helplist.delete(item_index)
  1965. self.update_help_changes()
  1966. self.set_add_delete_state()
  1967. def update_help_changes(self):
  1968. "Clear and rebuild the HelpFiles section in changes"
  1969. changes['main']['HelpFiles'] = {}
  1970. for num in range(1, len(self.user_helplist) + 1):
  1971. changes.add_option(
  1972. 'main', 'HelpFiles', str(num),
  1973. ';'.join(self.user_helplist[num-1][:2]))
  1974. def load_helplist(self):
  1975. # Set additional help sources.
  1976. self.user_helplist = idleConf.GetAllExtraHelpSourcesList()
  1977. self.helplist.delete(0, 'end')
  1978. for help_item in self.user_helplist:
  1979. self.helplist.insert(END, help_item[0])
  1980. self.set_add_delete_state()
  1981. class VarTrace:
  1982. """Maintain Tk variables trace state."""
  1983. def __init__(self):
  1984. """Store Tk variables and callbacks.
  1985. untraced: List of tuples (var, callback)
  1986. that do not have the callback attached
  1987. to the Tk var.
  1988. traced: List of tuples (var, callback) where
  1989. that callback has been attached to the var.
  1990. """
  1991. self.untraced = []
  1992. self.traced = []
  1993. def clear(self):
  1994. "Clear lists (for tests)."
  1995. # Call after all tests in a module to avoid memory leaks.
  1996. self.untraced.clear()
  1997. self.traced.clear()
  1998. def add(self, var, callback):
  1999. """Add (var, callback) tuple to untraced list.
  2000. Args:
  2001. var: Tk variable instance.
  2002. callback: Either function name to be used as a callback
  2003. or a tuple with IdleConf config-type, section, and
  2004. option names used in the default callback.
  2005. Return:
  2006. Tk variable instance.
  2007. """
  2008. if isinstance(callback, tuple):
  2009. callback = self.make_callback(var, callback)
  2010. self.untraced.append((var, callback))
  2011. return var
  2012. @staticmethod
  2013. def make_callback(var, config):
  2014. "Return default callback function to add values to changes instance."
  2015. def default_callback(*params):
  2016. "Add config values to changes instance."
  2017. changes.add_option(*config, var.get())
  2018. return default_callback
  2019. def attach(self):
  2020. "Attach callback to all vars that are not traced."
  2021. while self.untraced:
  2022. var, callback = self.untraced.pop()
  2023. var.trace_add('write', callback)
  2024. self.traced.append((var, callback))
  2025. def detach(self):
  2026. "Remove callback from traced vars."
  2027. while self.traced:
  2028. var, callback = self.traced.pop()
  2029. var.trace_remove('write', var.trace_info()[0][1])
  2030. self.untraced.append((var, callback))
  2031. tracers = VarTrace()
  2032. help_common = '''\
  2033. When you click either the Apply or Ok buttons, settings in this
  2034. dialog that are different from IDLE's default are saved in
  2035. a .idlerc directory in your home directory. Except as noted,
  2036. these changes apply to all versions of IDLE installed on this
  2037. machine. [Cancel] only cancels changes made since the last save.
  2038. '''
  2039. help_pages = {
  2040. 'Fonts/Tabs':'''
  2041. Font sample: This shows what a selection of Basic Multilingual Plane
  2042. unicode characters look like for the current font selection. If the
  2043. selected font does not define a character, Tk attempts to find another
  2044. font that does. Substitute glyphs depend on what is available on a
  2045. particular system and will not necessarily have the same size as the
  2046. font selected. Line contains 20 characters up to Devanagari, 14 for
  2047. Tamil, and 10 for East Asia.
  2048. Hebrew and Arabic letters should display right to left, starting with
  2049. alef, \u05d0 and \u0627. Arabic digits display left to right. The
  2050. Devanagari and Tamil lines start with digits. The East Asian lines
  2051. are Chinese digits, Chinese Hanzi, Korean Hangul, and Japanese
  2052. Hiragana and Katakana.
  2053. You can edit the font sample. Changes remain until IDLE is closed.
  2054. ''',
  2055. 'Highlights': '''
  2056. Highlighting:
  2057. The IDLE Dark color theme is new in October 2015. It can only
  2058. be used with older IDLE releases if it is saved as a custom
  2059. theme, with a different name.
  2060. ''',
  2061. 'Keys': '''
  2062. Keys:
  2063. The IDLE Modern Unix key set is new in June 2016. It can only
  2064. be used with older IDLE releases if it is saved as a custom
  2065. key set, with a different name.
  2066. ''',
  2067. 'General': '''
  2068. General:
  2069. AutoComplete: Popupwait is milliseconds to wait after key char, without
  2070. cursor movement, before popping up completion box. Key char is '.' after
  2071. identifier or a '/' (or '\\' on Windows) within a string.
  2072. FormatParagraph: Max-width is max chars in lines after re-formatting.
  2073. Use with paragraphs in both strings and comment blocks.
  2074. ParenMatch: Style indicates what is highlighted when closer is entered:
  2075. 'opener' - opener '({[' corresponding to closer; 'parens' - both chars;
  2076. 'expression' (default) - also everything in between. Flash-delay is how
  2077. long to highlight if cursor is not moved (0 means forever).
  2078. CodeContext: Maxlines is the maximum number of code context lines to
  2079. display when Code Context is turned on for an editor window.
  2080. Shell Preferences: Auto-Squeeze Min. Lines is the minimum number of lines
  2081. of output to automatically "squeeze".
  2082. ''',
  2083. 'Extensions': '''
  2084. ZzDummy: This extension is provided as an example for how to create and
  2085. use an extension. Enable indicates whether the extension is active or
  2086. not; likewise enable_editor and enable_shell indicate which windows it
  2087. will be active on. For this extension, z-text is the text that will be
  2088. inserted at or removed from the beginning of the lines of selected text,
  2089. or the current line if no selection.
  2090. ''',
  2091. }
  2092. def is_int(s):
  2093. "Return 's is blank or represents an int'"
  2094. if not s:
  2095. return True
  2096. try:
  2097. int(s)
  2098. return True
  2099. except ValueError:
  2100. return False
  2101. class VerticalScrolledFrame(Frame):
  2102. """A pure Tkinter vertically scrollable frame.
  2103. * Use the 'interior' attribute to place widgets inside the scrollable frame
  2104. * Construct and pack/place/grid normally
  2105. * This frame only allows vertical scrolling
  2106. """
  2107. def __init__(self, parent, *args, **kw):
  2108. Frame.__init__(self, parent, *args, **kw)
  2109. # Create a canvas object and a vertical scrollbar for scrolling it.
  2110. vscrollbar = Scrollbar(self, orient=VERTICAL)
  2111. vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE)
  2112. canvas = Canvas(self, borderwidth=0, highlightthickness=0,
  2113. yscrollcommand=vscrollbar.set, width=240)
  2114. canvas.pack(side=LEFT, fill=BOTH, expand=TRUE)
  2115. vscrollbar.config(command=canvas.yview)
  2116. # Reset the view.
  2117. canvas.xview_moveto(0)
  2118. canvas.yview_moveto(0)
  2119. # Create a frame inside the canvas which will be scrolled with it.
  2120. self.interior = interior = Frame(canvas)
  2121. interior_id = canvas.create_window(0, 0, window=interior, anchor=NW)
  2122. # Track changes to the canvas and frame width and sync them,
  2123. # also updating the scrollbar.
  2124. def _configure_interior(event):
  2125. # Update the scrollbars to match the size of the inner frame.
  2126. size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
  2127. canvas.config(scrollregion="0 0 %s %s" % size)
  2128. interior.bind('<Configure>', _configure_interior)
  2129. def _configure_canvas(event):
  2130. if interior.winfo_reqwidth() != canvas.winfo_width():
  2131. # Update the inner frame's width to fill the canvas.
  2132. canvas.itemconfigure(interior_id, width=canvas.winfo_width())
  2133. canvas.bind('<Configure>', _configure_canvas)
  2134. return
  2135. if __name__ == '__main__':
  2136. from unittest import main
  2137. main('idlelib.idle_test.test_configdialog', verbosity=2, exit=False)
  2138. from idlelib.idle_test.htest import run
  2139. run(ConfigDialog)