Skip to content

Commit e1aee80

Browse files
committed
Version 1.1:
fix: Fix of history non-saving on backspace feat: Add auto_padx for line nums refactor: Add 3 new dataclasses for providing CTkCodeBox args instead of values like "history_enabled, history_cooldown, history_max" docs: Update README.md
1 parent 86cf919 commit e1aee80

File tree

6 files changed

+162
-91
lines changed

6 files changed

+162
-91
lines changed

CTkCodeBoxPlus/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
Homepage: https://github.com/KiTant/CTkCodeBoxPlus
77
"""
88

9-
__version__ = '1.0'
9+
__version__ = '1.1'
1010

1111
from .ctk_code_box import CTkCodeBox
1212
from .text_menu import TextMenu

CTkCodeBoxPlus/add_line_nums.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@ def __init__(self,
1111
text_color=None,
1212
justify="left",
1313
padx=30,
14+
auto_padx=True,
1415
**kwargs):
1516

1617
self.master = master
1718
self.text_color = self.master.cget("border_color") if text_color is None else text_color
1819
self.fg_color = self.master.cget("fg_color")
19-
padx = padx * (int(self.master.cget("font").cget("size")) / 10)
20+
self.auto_padx = auto_padx
21+
if self.auto_padx:
22+
padx = padx * (int(self.master.cget("font").cget("size")) / 10)
2023

2124
customtkinter.windows.widgets.appearance_mode.CTkAppearanceModeBaseClass.__init__(self)
2225

@@ -27,18 +30,23 @@ def __init__(self,
2730

2831
padding = self.master.cget("border_width") + self.master.cget("corner_radius")
2932

30-
super().grid(row=0, column=0, sticky="nsw", padx=(padding, 0), pady=padding-1)
33+
self.grid(row=0, column=0, sticky="nsw", padx=(padding, 0), pady=padding-1)
3134

3235
self.master._textbox.grid_configure(padx=(padx, 0))
3336
self.master._textbox.lift()
3437
self.master._textbox.configure(yscrollcommand=self.set_scrollbar)
35-
self.master._textbox.bind("<<ContentChanged>>", self.redraw, add=True)
36-
self.master.bind("<Key>", lambda e: self.after(10, self.redraw), add=True)
38+
self.master._textbox.bind("<<ContentChanged>>", lambda e: self.after(20, self.redraw), add=True)
39+
self.master.bind("<Key>", lambda e: self.after(20, self.redraw), add=True)
3740

3841
def set_scrollbar(self, x, y):
3942
self.redraw(x, y)
4043
self.master._y_scrollbar.set(x, y)
4144

45+
def set_padx(self, padx=30):
46+
if self.auto_padx:
47+
padx = padx * (int(self.master.cget("font").cget("size")) / 10)
48+
self.master._textbox.grid_configure(padx=(padx, 0))
49+
4250
def _set_appearance_mode(self, mode_string):
4351
self.colors = (self.master._apply_appearance_mode(self.text_color),
4452
self.master._apply_appearance_mode(self.fg_color))

CTkCodeBoxPlus/ctk_code_box.py

Lines changed: 106 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
scripting, perl, objective, jsx)
1515
from pygments.styles import get_style_by_name, get_all_styles
1616
from .keybinding import _register_keybind
17-
from typing import Union
17+
from typing import Union, Optional
1818
import 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+
4267
class 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

Comments
 (0)