1414 scripting , perl , objective , jsx )
1515from pygments .styles import get_style_by_name , get_all_styles
1616from .keybinding import _register_keybind
17- from typing import Union
17+ from typing import Union , Optional
1818import pyperclip
19+ from dataclasses import dataclass
1920
2021
2122# Custom Exception Classes
@@ -39,6 +40,30 @@ class LexerError(CTkCodeBoxError):
3940 pass
4041
4142
43+ @dataclass (frozen = True )
44+ class MenuSettings :
45+ enabled : bool = True
46+ fg_color : Optional [str ] = None
47+ text_color : Optional [str ] = None
48+ hover_color : Optional [str ] = None
49+
50+
51+ @dataclass ()
52+ class HistorySettings :
53+ enabled : bool = True
54+ cooldown : int = 1500 #ms
55+ max : int = 100
56+
57+
58+ @dataclass (frozen = True )
59+ class NumberingSettings :
60+ enabled : bool = True
61+ color : Optional [str ] = None
62+ justify : str = "left"
63+ padx : int = 30
64+ auto_padx : bool = True
65+
66+
4267class CTkCodeBox (customtkinter .CTkTextbox ):
4368 """A code-oriented CTk textbox with syntax highlighting and UX helpers.
4469
@@ -58,20 +83,14 @@ def __init__(self,
5883 language : Union [str , lexer .Lexer ],
5984 height : int = 200 ,
6085 theme : str = "solarized-light" ,
61- line_numbering : bool = True ,
62- numbering_color : str = None ,
63- menu : bool = True ,
64- menu_fg_color : str = None ,
65- menu_text_color : str = None ,
66- menu_hover_color : str = None ,
86+ numbering_settings : NumberingSettings = NumberingSettings (),
87+ menu_settings : MenuSettings = MenuSettings (),
6788 wrap : bool = True ,
6889 select_color : str = None ,
6990 cursor_color : str = None ,
7091 highlight_current_line : bool = True ,
7192 highlight_current_line_color : str = None ,
72- history_enabled : bool = True ,
73- history_cooldown : int = 2500 ,
74- history_max : int = 100 ,
93+ history_settings : HistorySettings = HistorySettings (),
7594 indent_width : int = 4 ,
7695 ** kwargs ):
7796 """Initialize a CTkCodeBox instance.
@@ -81,25 +100,19 @@ def __init__(self,
81100 language: Pygments language name (str) or a lexer class.
82101 height: Widget height in pixels (passed to CTkTextbox).
83102 theme: Pygments style name used for highlighting.
84- line_numbering: Enable line numbers.
85- numbering_color: Color for line numbers.
86- menu: Enable context menu.
87- menu_fg_color: Context menu background color.
88- menu_text_color: Context menu text color.
89- menu_hover_color: Context menu active background color.
103+ numbering_settings: NumberingSettings object for the line nums.
104+ menu_settings: MenuSettings object for the context menu.
90105 wrap: Enable word wrap.
91106 select_color: Override selection background color.
92107 cursor_color: Cursor color I (blinking).
93108 highlight_current_line: Highlight the active line.
94109 highlight_current_line_color: Explicit color for active line.
95- history_enabled: Enable built-in undo/redo history.
96- history_cooldown: Cooldown for pushing history in _on_keypress_history().
97- history_max: Maximum undo frames to keep.
110+ history_settings: HistorySettings object for custom history.
98111 indent_width: Number of spaces for indent/outdent.
99112 **kwargs: Additional arguments passed to CTkTextbox.
100113 """
101114 # Do not enable Tk's built-in undo/history
102- if 'undo' in kwargs and history_enabled :
115+ if 'undo' in kwargs and history_settings . enabled :
103116 try :
104117 del kwargs ['undo' ]
105118 except Exception :
@@ -109,8 +122,11 @@ def __init__(self,
109122 # Wrap management
110123 self .wrap_enabled = bool (wrap )
111124 self .set_wrap (self .wrap_enabled )
112- if line_numbering :
113- self .line_nums = AddLineNums (self , text_color = numbering_color )
125+ if numbering_settings .enabled :
126+ self .line_nums = AddLineNums (self , text_color = numbering_settings .color ,
127+ justify = numbering_settings .justify ,
128+ padx = numbering_settings .padx ,
129+ auto_padx = numbering_settings .auto_padx )
114130
115131 self .select_color = select_color
116132 if select_color :
@@ -133,12 +149,10 @@ def __init__(self,
133149 _register_keybind (self , "CmdOrCtrl+Z" , lambda : self .undo (), bind_scope = 'widget' )
134150
135151 # Custom history (undo/redo)
136- self ._history_enabled = history_enabled
137- self ._history_max = history_max
138152 self ._undo_stack = []
139153 self ._redo_stack = []
140154 self ._history_typing_cooldown = None
141- self ._history_cooldown = history_cooldown
155+ self .history_settings = history_settings
142156
143157 # Job id for debounced highlighting
144158 self ._highlight_job = None
@@ -221,8 +235,8 @@ def __init__(self,
221235 self .bind ("<Double-Button-1>" , self ._on_double_click , add = True )
222236 self .bind ("<Triple-Button-1>" , self ._on_triple_click , add = True )
223237
224- if menu :
225- self .text_menu = TextMenu (self , fg_color = menu_fg_color , text_color = menu_text_color , hover_color = menu_hover_color )
238+ if menu_settings . enabled :
239+ self .text_menu = TextMenu (self , fg_color = menu_settings . fg_color , text_color = menu_settings . text_color , hover_color = menu_settings . hover_color )
226240
227241 def check_lexer (self ):
228242 """Resolve and set the lexer.
@@ -320,15 +334,27 @@ def highlight_code(self, code):
320334 start_line = end_line
321335 start_index = end_index
322336
323- def configure (self , param = None , ** kwargs ):
337+ def configure (self , param = None , ** kwargs ): #param here ONLY for tklinenums module
324338 """Extended configure with CTkCodeBox options.
325-
326- Supports additional keyword options: theme, language, wrap, history_enabled
327- select_color, cursor_color, history_max, history_cooldown, highlight_current_line,
328- current_line_color, indent_width.
329339 Re-highlights when theme or language changes.
330340 Remaining options are passed to the base widget.
341+ Args:
342+ **kwargs: Configuration parameters (Supports additional keyword options: theme, language, wrap, history_enabled,
343+ select_color, cursor_color, history_max, history_cooldown, highlight_current_line,
344+ current_line_color, history_settings, numbering_padx, numbering_auto_padx, indent_width)
331345 """
346+ one_action_handlers = {
347+ "history_enabled" : self .set_history_enabled ,
348+ "history_max" : self .set_history_limit ,
349+ "history_cooldown" : lambda val : setattr (self .history_settings , "cooldown" , val ),
350+ "history_settings" : lambda val : setattr (self , "history_settings" , val ),
351+ "numbering_padx" : lambda val : self .line_nums .set_padx (val ),
352+ "numbering_auto_padx" : lambda val : setattr (self .line_nums , "auto_padx" , val ),
353+ "indent_width" : lambda val : setattr (self , "indent_width" , val ),
354+ }
355+ for param , value in list (kwargs .items ()):
356+ if param in one_action_handlers :
357+ one_action_handlers [param ](value )
332358 if "theme" in kwargs :
333359 self .theme_name = kwargs .pop ("theme" )
334360 self .configure_tags ()
@@ -351,31 +377,48 @@ def configure(self, param=None, **kwargs):
351377 if "cursor_color" in kwargs :
352378 self .cursor_color = kwargs .pop ("cursor_color" )
353379 self ._textbox .config (insertbackground = self .cursor_color )
354- if "history_enabled" in kwargs :
355- self .set_history_enabled (kwargs .pop ("history_enabled" ))
356- if "history_max" in kwargs :
357- self .set_history_limit (kwargs .pop ("history_max" ))
358- if "history_cooldown" in kwargs :
359- self ._history_cooldown = kwargs .pop ("history_cooldown" )
360- if "highlight_current_line" in kwargs or "current_line_color" in kwargs :
380+ if "highlight_current_line" in kwargs or "current_line_color" in kwargs or "highlight_current_line_color" in kwargs :
361381 self ._highlight_current_line = kwargs .pop ("highlight_current_line" , self ._highlight_current_line )
362- self ._current_line_color = kwargs .pop ("current_line_color" , self ._current_line_color )
382+ self ._current_line_color = kwargs .pop ("current_line_color" , kwargs . pop ( "highlight_current_line_color" , self ._current_line_color ) )
363383 self .tag_config ("current_line" , background = self ._get_current_line_color ())
364- if "indent_width" in kwargs :
365- self .indent_width = kwargs .pop ("indent_width" )
366384
367385 # Re-apply highlighting if theme or language changed
368386 self ._schedule_highlight (0 )
369- super ().configure (** kwargs )
387+ if kwargs :
388+ super ().configure (** kwargs )
370389
371390 def cget (self , param ):
372- """Query CTkCodeBox options in addition to base options."""
373- if param == "theme" :
374- return self .theme_name
375- elif getattr (self , param , None ):
376- return getattr (self , param )
377- elif getattr (self , "_" + param , None ):
378- return getattr (self , "_" + param )
391+ """Get configuration parameter value with support for custom parameters.
392+ Args:
393+ param: Parameter name to retrieve
394+ Returns:
395+ Parameter value
396+ """
397+ custom_params = {
398+ "theme" : lambda : self .theme_name ,
399+ "wrap_enabled" : lambda : self .wrap_enabled ,
400+ "edited" : self .is_edited ,
401+ "undo_stack" : lambda : self ._undo_stack ,
402+ "redo_stack" : lambda : self ._redo_stack ,
403+ "history_cd_active" : lambda : True if self ._history_typing_cooldown else False ,
404+ "all_themes" : lambda : self .all_themes ,
405+ "common_langs" : lambda : self .common_langs ,
406+ "text_menu" : lambda : self .text_menu ,
407+ "line_nums" : lambda : self .line_nums ,
408+ "history_enabled" : lambda : self .history_settings .enabled ,
409+ "history_max" : lambda : self .history_settings .max ,
410+ "history_cooldown" : lambda : self .history_settings .cooldown ,
411+ "history_settings" : lambda : self .history_settings ,
412+ "numbering_auto_padx" : lambda : self .line_nums .auto_padx ,
413+ "language" : lambda : self .language ,
414+ "select_color" : lambda : self .select_color ,
415+ "cursor_color" : lambda : self .cursor_color ,
416+ "highlight_current_line" : lambda : self ._highlight_current_line ,
417+ "current_line_color" : lambda : self ._current_line_color ,
418+ "highlight_current_line_color" : lambda : self ._current_line_color ,
419+ }
420+ if param in custom_params :
421+ return custom_params [param ]()
379422 return super ().cget (param )
380423
381424 # General helpers
@@ -660,16 +703,16 @@ def select_all_text(self):
660703 # History API
661704 def set_history_enabled (self , enabled : bool ):
662705 """Enable/disable the internal undo/redo history."""
663- self ._history_enabled = bool (enabled )
706+ self .history_settings . enabled = bool (enabled )
664707 return "Success"
665708
666709 def set_history_limit (self , limit : int ):
667710 """Set maximum number of undo frames to keep."""
668711 try :
669- self ._history_max = max (0 , int (limit ))
712+ self .history_settings . max = max (0 , int (limit ))
670713 # Trim if needed
671- if self ._history_max and len (self ._undo_stack ) > self ._history_max :
672- self ._undo_stack = self ._undo_stack [- self ._history_max :]
714+ if self .history_settings . max and len (self ._undo_stack ) > self .history_settings . max :
715+ self ._undo_stack = self ._undo_stack [- self .history_settings . max :]
673716 return "Success"
674717 except Exception :
675718 return None
@@ -683,7 +726,7 @@ def clear_history(self):
683726 def undo (self ):
684727 """Undo the last change if history is enabled."""
685728
686- if not self ._history_enabled or not self ._undo_stack :
729+ if not self .history_settings . enabled or not self ._undo_stack :
687730 return "Nothing to undo"
688731 try :
689732 current = self ._save_state ()
@@ -697,7 +740,7 @@ def undo(self):
697740
698741 def redo (self ):
699742 """Redo the last undone change if available."""
700- if not self ._history_enabled or not self ._redo_stack :
743+ if not self .history_settings . enabled or not self ._redo_stack :
701744 return "Nothing to redo"
702745 try :
703746 current = self ._save_state ()
@@ -754,22 +797,22 @@ def _restore_state(self, state):
754797
755798 def _history_push_current (self ):
756799 """Push a snapshot of the current buffer state onto the undo stack."""
757- if not self ._history_enabled :
800+ if not self .history_settings . enabled :
758801 return
759802 try :
760803 state = self ._save_state ()
761804 self ._undo_stack .append (state )
762805 # Limit size
763- if self ._history_max and len (self ._undo_stack ) > self ._history_max :
764- self ._undo_stack = self ._undo_stack [- self ._history_max :]
806+ if self .history_settings . max and len (self ._undo_stack ) > self .history_settings . max :
807+ self ._undo_stack = self ._undo_stack [- self .history_settings . max :]
765808 # Clear redo on new action
766809 self ._redo_stack .clear ()
767810 except Exception :
768811 pass
769812
770813 def _on_keypress_history (self , event ):
771814 """Typing into undo frames and snapshot when replacing selection."""
772- if not self ._history_enabled :
815+ if not self .history_settings . enabled :
773816 return
774817 try :
775818 ks = getattr (event , 'keysym' , '' )
@@ -784,7 +827,7 @@ def _on_keypress_history(self, event):
784827 return
785828 if self ._history_typing_cooldown is None :
786829 self ._history_push_current ()
787- self ._history_typing_cooldown = self .after (self ._history_cooldown , self ._reset_history_cooldown )
830+ self ._history_typing_cooldown = self .after (self .history_settings . cooldown , self ._reset_history_cooldown )
788831 except Exception :
789832 pass
790833
@@ -880,6 +923,8 @@ def _handle_chars_wrap(self, chars: str):
880923
881924 def _on_backspace (self , event = None ):
882925 """Smart-backspace: delete paired quotes/brackets if cursor is between them."""
926+ self ._history_push_current ()
927+ self ._notify_content_changed ()
883928 try :
884929 # If there is a selection, let default backspace handle it (history is handled elsewhere)
885930 if self .tag_ranges ('sel' ):
@@ -890,9 +935,7 @@ def _on_backspace(self, event=None):
890935 pairs = {('(' , ')' ), ('[' , ']' ), ('{' , '}' ), ('<' , '>' )}
891936 if prev_ch and next_ch :
892937 if (prev_ch , next_ch ) in pairs or (prev_ch == next_ch and prev_ch in ('"' , "'" , '`' )):
893- self ._history_push_current ()
894938 self .delete ("insert -1c" , "insert +1c" )
895- self ._notify_content_changed ()
896939 return "break"
897940 except Exception :
898941 pass
0 commit comments