Skip to content

Commit 641dde9

Browse files
Add files via upload
1 parent a1bd3b7 commit 641dde9

40 files changed

Lines changed: 1979 additions & 0 deletions
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""
2+
CustomTkinter Code Viewer Widget
3+
Author: Akash Bora
4+
License: MIT
5+
Homepage: https://github.com/Akascape/CTkCodeBox
6+
"""
7+
8+
__version__ = '0.1'
9+
10+
from .ctk_code_box import CTkCodeBox
11+
from .text_menu import TextMenu
12+
from .ctk_code_box import AddLineNums
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
"""
2+
CTkCodeBox
3+
Author: Akash Bora
4+
License: MIT
5+
"""
6+
7+
import customtkinter
8+
from .text_menu import TextMenu
9+
from tklinenums import TkLineNumbers
10+
from pygments import lex
11+
from pygments.lexers import python, javascript, c_cpp, dotnet, html, css, data, go, jvm, php, ruby, rust, scripting, perl, objective, jsx
12+
from pygments.styles import get_style_by_name, get_all_styles
13+
14+
class AddLineNums(TkLineNumbers):
15+
"""
16+
Class for adding line numbers in CTkTextbox using TkLineNumbers
17+
"""
18+
def __init__(self,
19+
master,
20+
text_color=None,
21+
justify="left",
22+
padx=30,
23+
**kwargs):
24+
25+
self.master = master
26+
self.text_color = self.master.cget("border_color") if text_color is None else text_color
27+
self.fg_color = self.master.cget("fg_color")
28+
29+
customtkinter.windows.widgets.appearance_mode.CTkAppearanceModeBaseClass.__init__(self)
30+
31+
super().__init__(self.master, self.master, justify=justify,
32+
colors=(self.master._apply_appearance_mode(self.text_color),
33+
self.master._apply_appearance_mode(self.fg_color)),
34+
relief="flat", height=self.master.winfo_reqheight(), **kwargs)
35+
36+
padding = self.master.cget("border_width") + self.master.cget("corner_radius")
37+
38+
super().grid(row=0, column=0, sticky="nsw", padx=(padding,0), pady=padding-1)
39+
40+
self.master._textbox.grid_configure(padx=(padx, 0))
41+
self.master._textbox.lift()
42+
self.master._textbox.configure(yscrollcommand=self.set_scrollbar)
43+
self.master._textbox.bind("<<ContentChanged>>", self.redraw, add=True)
44+
self.master.bind("<Key>", lambda e: self.after(10, self.redraw), add=True)
45+
46+
def set_scrollbar(self, x,y):
47+
self.redraw(x,y)
48+
self.master._y_scrollbar.set(x,y)
49+
50+
def _set_appearance_mode(self, mode_string):
51+
self.colors = (self.master._apply_appearance_mode(self.text_color),
52+
self.master._apply_appearance_mode(self.fg_color))
53+
self.set_colors()
54+
55+
class CTkCodeBox(customtkinter.CTkTextbox):
56+
"""
57+
Widget for displaying code in CTk
58+
"""
59+
def __init__(self,
60+
master,
61+
language,
62+
height=200,
63+
theme:str="solarized-light",
64+
line_numbering:bool=True,
65+
numbering_color:str=None,
66+
undo:bool=True,
67+
menu:bool=True,
68+
menu_fg_color:str=None,
69+
menu_text_color:str=None,
70+
menu_hover_color:str=None,
71+
wrap:bool=True,
72+
select_color:str=None,
73+
cursor_color:str=None,
74+
**kwargs):
75+
76+
super().__init__(master, undo=undo, height=height, **kwargs)
77+
78+
if wrap:
79+
self.configure(wrap="word")
80+
if line_numbering:
81+
AddLineNums(self, text_color=numbering_color)
82+
if menu:
83+
TextMenu(self, fg_color=menu_fg_color, text_color=menu_text_color, hover_color=menu_hover_color)
84+
85+
self.select_color = select_color
86+
if select_color:
87+
self._textbox.config(selectbackground=self.select_color)
88+
self.cursor_color = cursor_color
89+
90+
if self.cursor_color:
91+
self._textbox.config(insertbackground=self.cursor_color)
92+
93+
self.bind('<KeyRelease>', self.update_code) # When a key is released, update the code
94+
self.bind('<<ContentChanged>>', self.update_code)
95+
self.bind("<Control-a>", lambda e: self.after(200, self._select_all), add=True)
96+
97+
self.theme_name = theme
98+
self.all_themes = list(get_all_styles())
99+
100+
self.common_langs = {
101+
"python": python.PythonLexer,
102+
"c": c_cpp.CLexer,
103+
"cpp": c_cpp.CppLexer,
104+
"c++": c_cpp.CppLexer,
105+
"c#": dotnet.CSharpLexer,
106+
"html": html.HtmlLexer,
107+
"javascript": javascript.JavascriptLexer,
108+
"xml": html.XmlLexer,
109+
"css": css.CssLexer,
110+
"json": data.JsonLexer,
111+
"yaml": data.YamlLexer,
112+
"go": go.GoLexer,
113+
"typescript": javascript.TypeScriptLexer,
114+
"kotlin": jvm.KotlinLexer,
115+
"php": php.PhpLexer,
116+
"ruby": ruby.RubyLexer,
117+
"rust": rust.RustLexer,
118+
"lua": scripting.LuaLexer,
119+
"java": jvm.JavaLexer,
120+
"perl": perl.PerlLexer,
121+
"swift": objective.SwiftLexer,
122+
"react": jsx.JsxLexer,
123+
}
124+
self.language = language
125+
self.check_lexer()
126+
self.configure_tags()
127+
self.edited = False
128+
129+
def check_lexer(self):
130+
if type(self.language) is str:
131+
if self.language.lower() in self.common_langs:
132+
self.lexer = self.common_langs[self.language.lower()]
133+
else:
134+
raise ValueError("This language is not available, try to pass the pygments lexer instead. \nAvailable lexers: https://pygments.org/docs/lexers")
135+
else:
136+
self.lexer = self.language
137+
138+
def configure_tags(self):
139+
if self.theme_name not in self.all_themes:
140+
raise ValueError(f"Invalid theme name: {self.theme_name}, \nAvailable themes: {self.all_themes}")
141+
style = get_style_by_name(self.theme_name)
142+
for token, values in style:
143+
foreground = values['color']
144+
if foreground:
145+
self.tag_config(str(token), foreground=f'#{foreground}')
146+
147+
def update_code(self, event=None, edited=True):
148+
code = self.get('0.0', 'end-1c')
149+
self.clear_code()
150+
self.highlight_code(code)
151+
if edited:
152+
self.edited = True
153+
154+
def clear_code(self):
155+
for tag in self.tag_names():
156+
self.tag_remove(tag, '0.0', 'end')
157+
158+
def _select_all(self):
159+
self.tag_add("sel", "0.0", "end")
160+
161+
def insert(self, start, end):
162+
super().insert(start, end)
163+
self.update_code(edited=False)
164+
self.edit_reset()
165+
self.edited = False
166+
167+
def highlight_code(self, code):
168+
code = code.replace("\n"," ",1)
169+
try:
170+
tokens = list(lex(code, self.lexer()))
171+
except:
172+
raise ValueError("Not a valid lexer. Available lexers: https://pygments.org/docs/lexers")
173+
start_line=1
174+
start_index = 0
175+
for token in tokens:
176+
end_line, end_index = self.index(f'{start_line}.{start_index}+{len(token[1])} chars').split('.')
177+
self.tag_add(str(token[0]), f'{start_line}.{start_index}', f'{end_line}.{end_index}')
178+
start_line = end_line
179+
start_index = end_index
180+
181+
def configure(self, param=None, **kwargs):
182+
if "theme" in kwargs:
183+
self.theme_name = kwargs.pop("theme")
184+
self.configure_tags()
185+
if "language" in kwargs:
186+
self.language = kwargs.pop("language")
187+
self.check_lexer()
188+
if "select_color" in kwargs:
189+
self.select_color = kwargs.pop("select_color")
190+
self._textbox.config(selectbackground=self.select_color)
191+
if "cursor_color" in kwargs:
192+
self.cursor_color = kwargs.pop("cursor_color")
193+
self._textbox.config(insertbackground=self.cursor_color)
194+
super().configure(**kwargs)
195+
196+
def cget(self, param):
197+
if param=="theme":
198+
return self.theme_name
199+
if param=="language":
200+
return self.language
201+
if param=="select_color":
202+
return self.select_color
203+
if param=="cursor_color":
204+
return self.cursor_color
205+
return super().cget(param)
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import customtkinter
2+
import tkinter
3+
4+
class TextMenu(tkinter.Menu):
5+
"""
6+
Popup Text Menu for CTkTextbox and CTkEntry
7+
Author: Akash Bora
8+
"""
9+
def __init__(self,
10+
widget,
11+
fg_color=None,
12+
text_color=None,
13+
hover_color=None,
14+
**kwargs):
15+
16+
super().__init__(tearoff=False, title="menu", borderwidth=0, bd=0, relief="flat", **kwargs)
17+
18+
self.fg_color = customtkinter.ThemeManager.theme["CTkFrame"]["top_fg_color"] if fg_color is None else fg_color
19+
self.text_color = customtkinter.ThemeManager.theme["CTkLabel"]["text_color"] if text_color is None else text_color
20+
self.hover_color = customtkinter.ThemeManager.theme["CTkButton"]["hover_color"] if hover_color is None else hover_color
21+
22+
self.widget = widget
23+
24+
self.add_command(label="Cut", command=self.cut_text)
25+
self.add_command(label="Copy", command=self.copy_text)
26+
self.add_command(label="Paste", command=self.paste_text)
27+
self.add_command(label="Delete", command=self.clear_text)
28+
self.add_command(label="Clear All", command=self.clear_all_text)
29+
self.add_command(label="Select All", command=self.select_all_text)
30+
31+
self.add_command(label="Undo", command=self.undo_text)
32+
33+
self.widget.bind("<Button-3>", lambda event: self.do_popup(event))
34+
self.widget.bind("<Button-2>", lambda event: self.do_popup(event))
35+
36+
def do_popup(self, event):
37+
""" open the popup menu """
38+
39+
super().config(bg=self.widget._apply_appearance_mode(self.fg_color),
40+
fg=self.widget._apply_appearance_mode(self.text_color),
41+
activebackground=self.widget._apply_appearance_mode(self.hover_color))
42+
self.tk_popup(event.x_root, event.y_root)
43+
44+
def cut_text(self):
45+
""" cut text operation """
46+
self.copy_text()
47+
try: self.widget.delete(tkinter.SEL_FIRST, tkinter.SEL_LAST)
48+
except: pass
49+
50+
def copy_text(self):
51+
""" copy text operation """
52+
try:
53+
self.clipboard_clear()
54+
self.clipboard_append(self.widget.get(tkinter.SEL_FIRST, tkinter.SEL_LAST))
55+
except: pass
56+
57+
def paste_text(self):
58+
""" paste text operation """
59+
try: self.widget.insert(self.widget.index('insert'), self.clipboard_get())
60+
except: pass
61+
62+
def clear_all_text(self):
63+
""" clears sll the text """
64+
try:
65+
self.widget.delete(0.0,"end")
66+
except: pass
67+
68+
def select_all_text(self):
69+
""" clears sll the text """
70+
try:
71+
self.widget.tag_add("sel", "0.0", "end")
72+
except: pass
73+
74+
def clear_text(self):
75+
""" clears sll the text """
76+
try: self.widget.delete(tkinter.SEL_FIRST, tkinter.SEL_LAST)
77+
except: pass
78+
79+
def undo_text(self):
80+
try:
81+
self.widget.edit_undo()
82+
self.widget.event_generate("<<ContentChanged>>")
83+
except: pass
84+
66.1 KB
Binary file not shown.

0 commit comments

Comments
 (0)