#!/usr/bin/python
# -*- coding: utf-8 -*-
import sys
import os
import re
import json
reload(sys)
sys.setdefaultencoding('utf-8')
try:
from Tkinter import *
from tkFont import Font
import tkFileDialog
import tkMessageBox
except Exception as e:
print 'Import error:', str(e)
sys.exit(1)
CONFIG_FILE = 'config.txt'
class Editor:
def __init__(self, root, file_arg=None):
self.root = root
self.root.title(u'\u7F16\u8F91\u5668')
# 默认设置
default_config = {
'width': 1500,
'height': 900,
'font_family': 'Monospace',
'font_size': 10,
'color_mode': 0,
'dark_mode': False
}
# 读取配置
self.config = default_config
if os.path.exists(CONFIG_FILE):
try:
with open(CONFIG_FILE, 'r') as f:
saved = json.load(f)
self.config.update(saved)
except:
pass
w = self.config['width']
h = self.config['height']
self.root.geometry('%dx%d' % (w, h))
self.root.minsize(600, 400)
self.current_font = Font(
family=self.config['font_family'],
size=self.config['font_size']
)
self.file_path = None
self.file_contents = {}
self.file_modifies = {}
self.color_mode = IntVar(root, value=self.config['color_mode'])
self.dark_mode = BooleanVar(root, value=self.config['dark_mode'])
self.current_file_var = StringVar(root)
self._build_ui()
self._create_menus()
# === 快捷键绑定 ===
# 文件
self.root.bind('', lambda e: self._new_file())
self.root.bind('', lambda e: self._new_file())
self.root.bind('', lambda e: self._open_dialog())
self.root.bind('', lambda e: self._open_dialog())
self.root.bind('', lambda e: self._save_file())
self.root.bind('', lambda e: self._save_file())
self.root.bind('', lambda e: self._save_as())
self.root.bind('', lambda e: self._save_as())
self.root.bind('', lambda e: self._close_file())
self.root.bind('', lambda e: self._close_file())
self.root.bind('', lambda e: self._exit_app())
self.root.bind('', lambda e: self._exit_app())
# 编辑
self.root.bind('', lambda e: self._cut())
self.root.bind('', lambda e: self._cut())
self.root.bind('', lambda e: self._copy())
self.root.bind('', lambda e: self._copy())
self.root.bind('', lambda e: self._paste())
self.root.bind('', lambda e: self._paste())
self.root.bind('', lambda e: self._clear())
# 查找与替换
self.root.bind('', lambda e: self._find_dialog())
self.root.bind('', lambda e: self._find_dialog())
self.root.bind('', lambda e: self._replace_dialog())
self.root.bind('', lambda e: self._replace_dialog())
# 帮助
self.root.bind('', lambda e: self._show_about())
if file_arg:
self._open_file(file_arg)
# ? 启动光标位置轮询
self.last_cursor_pos = '1.0'
self.root.after(50, self._poll_cursor)
def _poll_cursor(self):
try:
current = self.text_area.index('insert')
if current != self.last_cursor_pos:
self.last_cursor_pos = current
self._update_status()
except:
pass
self.root.after(50, self._poll_cursor) # 每 50ms 检查一次
def _set_color_mode_1(self):
self.color_mode.set(0)
self._highlight()
def _set_color_mode_2(self):
self.color_mode.set(1)
self._highlight()
def _toggle_dark_mode(self):
self._apply_theme() # ? 已移除 print
def _apply_theme(self):
is_dark = self.dark_mode.get()
bg_main = '#1e1e1e' if is_dark else '#ffffff'
fg_main = '#d4d4d4' if is_dark else '#000000'
bg_line = '#2d2d2d' if is_dark else '#f0f0f0'
fg_line = '#888888' if is_dark else '#555555'
insert_bg = fg_main
self.text_area.config(
bg=bg_main,
fg=fg_main,
insertbackground=insert_bg,
selectbackground='#4a4a4a' if is_dark else '#c0c0c0',
selectforeground=fg_main
)
self.line_text.config(
bg=bg_line,
fg=fg_line
)
self.root.update()
def _build_ui(self):
main_frame = Frame(self.root)
main_frame.pack(fill=BOTH, expand=1)
line_frame = Frame(main_frame, width=40)
line_frame.pack(side=LEFT, fill=Y)
line_frame.pack_propagate(False)
self.line_text = Text(
line_frame,
width=4,
padx=2,
pady=0,
takefocus=0,
border=0,
state=DISABLED,
font=self.current_font,
wrap=NONE,
bg='#f0f0f0',
fg='#555555',
spacing1=0,
spacing2=0,
spacing3=0
)
self.line_text.pack(side=LEFT, fill=Y)
text_container = Frame(main_frame)
text_container.pack(side=LEFT, fill=BOTH, expand=1)
self.h_scroll = Scrollbar(text_container, orient=HORIZONTAL)
self.h_scroll.pack(side=BOTTOM, fill=X)
text_frame = Frame(text_container)
text_frame.pack(side=TOP, fill=BOTH, expand=1)
self.v_scroll = Scrollbar(text_frame, orient=VERTICAL)
self.v_scroll.pack(side=RIGHT, fill=Y)
self.text_area = Text(
text_frame,
wrap=NONE,
undo=True,
font=self.current_font,
padx=0,
pady=0,
borderwidth=0,
highlightthickness=0,
spacing1=0,
spacing2=0,
spacing3=0
)
self.text_area.pack(side=LEFT, fill=BOTH, expand=1)
self.text_area.config(
yscrollcommand=self._sync_scrollbar,
xscrollcommand=self.h_scroll.set
)
self.v_scroll.config(command=self._yview_sync)
self.h_scroll.config(command=self.text_area.xview)
# 保留基本绑定
self.text_area.bind('', lambda e: None) # 轮询会处理
self.text_area.bind('<>', self._on_modified)
self.text_area.bind('', self._on_input)
self.status_frame = Frame(self.root, bd=1, relief=SUNKEN)
self.status_frame.pack(side=BOTTOM, fill=X)
self.status_label = Label(
self.status_frame,
text=u'\u6587\u4EF6\uFF1A\u65E0 \u884C\uFF1A1 \u5217\uFF1A1 \u5B57\u7B26\uFF1A0',
font=('Monospace', 9),
anchor=W
)
self.status_label.pack(fill=X)
self._setup_syntax_tags()
self._apply_theme()
self._update_line_numbers()
def _setup_syntax_tags(self):
colors = [
('m1_neg_deep', '#8B0000'),
('m1_neg_light', '#DC143C'),
('m1_blue', 'deepskyblue'),
('m1_green', 'limegreen'),
('m2_neg_deep', '#8B0000'),
('m2_neg_light', '#DC143C'),
('m2_blue', 'deepskyblue'),
('m2_green', 'limegreen'),
]
for name, color in colors:
self.text_area.tag_configure(name, foreground=color, font=self.current_font)
def _sync_scrollbar(self, first, last):
self.v_scroll.set(first, last)
self.line_text.yview_moveto(first)
def _yview_sync(self, *args):
self.text_area.yview(*args)
self.line_text.yview(*args)
def _update_line_numbers(self):
self.line_text.config(state=NORMAL)
self.line_text.delete(1.0, END)
content = self.text_area.get(1.0, END)
lines = content.split('\n')
line_numbers = '\n'.join(str(i) for i in range(1, len(lines)))
self.line_text.insert(1.0, line_numbers)
self.line_text.config(state=DISABLED)
self.line_text.yview_moveto(self.text_area.yview()[0])
def _on_input(self, event=None):
self._update_line_numbers()
def _on_modified(self, event=None):
if self.text_area.edit_modified():
self.text_area.edit_modified(False)
self._highlight()
self._update_line_numbers()
self._update_status()
def _highlight(self):
tags = ['m1_neg_deep', 'm1_neg_light', 'm1_blue', 'm1_green',
'm2_neg_deep', 'm2_neg_light', 'm2_blue', 'm2_green']
for tag in tags:
self.text_area.tag_remove(tag, '1.0', END)
content = self.text_area.get('1.0', END)
lines = content.split('\n')
for row, line in enumerate(lines):
num_matches = re.finditer(r'-?(?:0|[1-9]\d*)(?:\.\d+)?', line)
for m in num_matches:
try:
val = float(m.group())
start = '%d.%d' % (row + 1, m.start())
end = '%d.%d' % (row + 1, m.end())
if self.color_mode.get() == 0:
if val < -0.2:
self.text_area.tag_add('m1_neg_deep', start, end)
elif val < -0.1:
self.text_area.tag_add('m1_neg_light', start, end)
elif val < -0.05:
self.text_area.tag_add('m1_blue', start, end)
elif val < 0:
self.text_area.tag_add('m1_green', start, end)
else:
if val < -200:
self.text_area.tag_add('m2_neg_deep', start, end)
elif val < -100:
self.text_area.tag_add('m2_neg_light', start, end)
elif val < -50:
self.text_area.tag_add('m2_blue', start, end)
elif val < 0:
self.text_area.tag_add('m2_green', start, end)
except:
continue
def _update_status(self, event=None):
try:
line, col = self.text_area.index('insert').split('.')
line = int(line)
col = int(col) + 1
except:
line, col = 1, 1
content = self.text_area.get('1.0', END)
char_count = len(content) - 1
filename = self.file_path if self.file_path else u'\u65E0'
self.status_label.config(
text=u'\u6587\u4EF6\uFF1A%s \u884C\uFF1A%d \u5217\uFF1A%d \u5B57\u7B26\uFF1A%d' % (filename, line, col, char_count)
)
def _create_menus(self):
menu_bar = Menu(self.root)
self.root.config(menu=menu_bar)
# 文件菜单
file_menu = Menu(menu_bar, tearoff=0)
menu_bar.add_cascade(label=u'\u6587\u4EF6', menu=file_menu)
file_menu.add_command(
label=u'\u65B0\u5EFA',
command=self._new_file,
accelerator='Ctrl+N'
)
file_menu.add_command(
label=u'\u6253\u5F00',
command=self._open_dialog,
accelerator='Ctrl+O'
)
file_menu.add_command(
label=u'\u5173\u95ED\u6587\u4EF6',
command=self._close_file,
accelerator='Ctrl+W'
)
file_menu.add_separator()
file_menu.add_command(
label=u'\u4FDD\u5B58',
command=self._save_file,
accelerator='Ctrl+S'
)
file_menu.add_command(
label=u'\u53E6\u5B58\u4E3A',
command=self._save_as,
accelerator='Ctrl+Shift+S'
)
file_menu.add_separator()
file_menu.add_command(
label=u'\u9000\u51FA',
command=self._exit_app,
accelerator='Ctrl+Q'
)
# 切换文件
switch_menu = Menu(menu_bar, tearoff=0)
menu_bar.add_cascade(label=u'\u5207\u6362\u6587\u4EF6', menu=switch_menu)
self.switch_menu = switch_menu
self._update_switch_menu()
# 着色模式
color_menu = Menu(menu_bar, tearoff=0)
menu_bar.add_cascade(label=u'\u7740\u8272\u6A21\u5F0F', menu=color_menu)
color_menu.add_radiobutton(
label=u'\u6A21\u5F0F1: \u5C0F\u6570',
variable=self.color_mode,
value=0,
command=self._set_color_mode_1
)
color_menu.add_radiobutton(
label=u'\u6A21\u5F0F2: \u6574\u6570',
variable=self.color_mode,
value=1,
command=self._set_color_mode_2
)
# 视图
view_menu = Menu(menu_bar, tearoff=0)
menu_bar.add_cascade(label=u'\u89C6\u56FE', menu=view_menu)
view_menu.add_checkbutton(
label=u'\u6697\u8272\u6A21\u5F0F',
variable=self.dark_mode,
command=self._toggle_dark_mode
)
# 编辑菜单
edit_menu = Menu(menu_bar, tearoff=0)
menu_bar.add_cascade(label=u'\u7F16\u8F91', menu=edit_menu)
edit_menu.add_command(
label=u'\u526A\u5207',
command=self._cut,
accelerator='Ctrl+X'
)
edit_menu.add_command(
label=u'\u590D\u5236',
command=self._copy,
accelerator='Ctrl+C'
)
edit_menu.add_command(
label=u'\u7C98\u8D34',
command=self._paste,
accelerator='Ctrl+V'
)
edit_menu.add_separator()
edit_menu.add_command(
label=u'\u6E05\u9664',
command=self._clear,
accelerator='Del'
)
edit_menu.add_separator()
edit_menu.add_command(
label=u'\u67E5\u627E',
command=self._find_dialog,
accelerator='Ctrl+F'
)
edit_menu.add_command(
label=u'\u66FF\u6362',
command=self._replace_dialog,
accelerator='Ctrl+H'
)
# 设置
setting_menu = Menu(menu_bar, tearoff=0)
menu_bar.add_cascade(label=u'\u8BBE\u7F6E', menu=setting_menu)
setting_menu.add_command(
label=u'\u5B57\u4F53',
command=self._choose_font
)
# 帮助
help_menu = Menu(menu_bar, tearoff=0)
menu_bar.add_cascade(label=u'\u5E2E\u52A9', menu=help_menu)
help_menu.add_command(
label=u'\u5173\u4E8E',
command=self._show_about,
accelerator='F1'
)
def _update_switch_menu(self):
self.switch_menu.delete(0, END)
if not self.file_contents:
self.switch_menu.add_command(label=u'\u65E0', state=DISABLED)
else:
for path in self.file_contents:
name = os.path.basename(path)
self.switch_menu.add_radiobutton(
label=name,
variable=self.current_file_var,
value=path,
command=lambda p=path: self._switch_to(p)
)
if self.file_path:
self.current_file_var.set(self.file_path)
else:
self.current_file_var.set('')
def _switch_to(self, path):
if self.file_path and self.file_modifies.get(self.file_path, False):
if tkMessageBox.askyesno(u'\u4FDD\u5B58', u'\u672A\u4FDD\u5B58\uFF0C\u662F\u5426\u4FDD\u5B58\uFF1F'):
self._save_current()
self.file_path = path
self.text_area.delete(1.0, END)
self.text_area.insert(1.0, self.file_contents[path])
self.text_area.edit_modified(False)
self.root.title(u'\u7F16\u8F91\u5668 \u2014 ' + path)
self._on_modified()
self._update_switch_menu()
def _close_file(self):
if not self.file_path:
return
if self.file_modifies.get(self.file_path, False):
if not tkMessageBox.askyesno(u'\u5173\u95ED', u'\u672A\u4FDD\u5B58\uFF0C\u786E\u8BA4\u5173\u95ED\uFF1F'):
return
del self.file_contents[self.file_path]
del self.file_modifies[self.file_path]
next_path = next(iter(self.file_contents)) if self.file_contents else None
if next_path:
self._switch_to(next_path)
else:
self.file_path = None
self.text_area.delete(1.0, END)
self.root.title(u'\u7F16\u8F91\u5668')
self._update_switch_menu()
def _open_dialog(self):
path = tkFileDialog.askopenfilename(title=u'\u6253\u5F00', filetypes=[])
if path:
self._open_file(path)
def _open_file(self, path):
if path in self.file_contents:
self._switch_to(path)
return
if os.path.getsize(path) > 200 * 1024 * 1024:
tkMessageBox.showwarning(
u'\u6587\u4EF6\u8FC7\u5927',
u'\u6587\u4EF6\u5927\u4E8E200MB\uFF0C\u5DF2\u88AB\u62D2'
)
return
try:
if path.endswith('.gz'):
import gzip
f_gz = gzip.open(path, 'rb')
try:
content = f_gz.read().decode('utf-8')
finally:
f_gz.close()
else:
with open(path, 'r') as f:
content = f.read().decode('utf-8')
self.file_contents[path] = content
self.file_modifies[path] = False
self._switch_to(path)
except Exception as e:
tkMessageBox.showerror(u'\u9519\u8BEF', u'\u6253\u5F00\u5931\u8D25\n' + str(e))
def _save_file(self):
if self.file_path:
self._save_current()
else:
self._save_as()
def _save_as(self):
path = tkFileDialog.asksaveasfilename(filetypes=[])
if path:
self._do_save(path)
self.file_path = path
self.root.title(u'\u7F16\u8F91\u5668 \u2014 ' + path)
self._update_switch_menu()
def _do_save(self, path):
try:
content = self.text_area.get(1.0, END)
raw_content = content.encode('utf-8')
if path.endswith('.gz'):
import gzip
f_gz = gzip.open(path, 'wb')
try:
f_gz.write(raw_content)
finally:
f_gz.close()
else:
with open(path, 'w') as f:
f.write(raw_content)
self.file_contents[path] = content
self.file_modifies[path] = False
tkMessageBox.showinfo(u'\u4FDD\u5B58', u'\u5DF2\u4FDD\u5B58')
self._update_status()
except Exception as e:
tkMessageBox.showerror(u'\u9519\u8BEF', u'\u4FDD\u5B58\u5931\u8D25\n' + str(e))
def _save_current(self):
if self.file_path:
self._do_save(self.file_path)
def _new_file(self):
if self.file_path and self.file_modifies.get(self.file_path, False):
if tkMessageBox.askyesno(u'\u4FDD\u5B58', u'\u672A\u4FDD\u5B58\uFF0C\u662F\u5426\u4FDD\u5B58\uFF1F'):
self._save_current()
self.text_area.delete(1.0, END)
self.file_path = None
self.root.title(u'\u7F16\u8F91\u5668')
self._on_modified()
def _exit_app(self):
self.config['width'] = self.root.winfo_width()
self.config['height'] = self.root.winfo_height()
self.config['font_family'] = self.current_font.cget('family')
self.config['font_size'] = self.current_font.cget('size')
self.config['color_mode'] = self.color_mode.get()
self.config['dark_mode'] = self.dark_mode.get()
try:
with open(CONFIG_FILE, 'w') as f:
json.dump(self.config, f)
except Exception as e:
pass
for path in self.file_contents:
if self.file_modifies.get(path, False):
if tkMessageBox.askyesnocancel(u'\u9000\u51FA', u'\u6709\u672A\u4FDD\u5B58\u6587\u4EF6\uFF0C\u786E\u8BA4\u9000\u51FA\uFF1F'):
self._save_current()
elif tkMessageBox.askyesnocancel is None:
return
self.root.destroy()
def _cut(self):
self.text_area.event_generate('<>')
def _copy(self):
self.text_area.event_generate('<>')
def _paste(self):
self.text_area.event_generate('<>')
def _clear(self):
try:
self.text_area.delete('sel.first', 'sel.last')
except:
pass
def _choose_font(self):
win = Toplevel(self.root)
win.title(u'\u5B57\u4F53')
win.geometry('300x200')
Label(win, text=u'\u5B57\u4F53').pack(anchor=W, padx=10, pady=5)
family_list = Listbox(win, height=5)
families = ['Monospace', 'Courier', 'Consolas', 'Arial']
for f in families:
family_list.insert(END, f)
family_list.pack(fill=X, padx=10)
family_list.selection_set(0)
Label(win, text=u'\u5B57\u53F7').pack(anchor=W, padx=10, pady=5)
size_var = StringVar(value=str(self.current_font.cget('size')))
Entry(win, textvariable=size_var).pack(fill=X, padx=10)
def apply():
try:
idx = family_list.curselection()[0]
family = families[idx]
except:
family = self.current_font.cget('family')
try:
size = int(size_var.get())
except:
size = 10
self.current_font.config(family=family, size=size)
self.text_area.config(font=self.current_font)
self.line_text.config(font=self.current_font)
win.destroy()
Button(win, text=u'\u786E\u5B9A', command=apply).pack(pady=10)
def _show_about(self):
msg = (u'\u6B22\u8FCE\u4F7F\u7528\u672C\u7F16\u8F91\u5668\uFF01\n\n'
u'\u529F\u80FD\uFF1A\n'
u'\u2022 \u67E5\u627E\u66FF\u6362\n'
u'\u2022 \u53CC\u7740\u8272\u6A21\u5F0F\n'
u'\u2022 \u6697\u8272\u6A21\u5F0F\n'
u'\u2022 \u8BB0\u4F4F\u8BBE\u7F6E\n'
u'\u2022 \u5927\u6587\u4EF6\u62E6\u622A\n\n'
u'Python 2.6.6 + Tkinter\n'
u'Version 1.0')
tkMessageBox.showinfo(u'\u5173\u4E8E', msg)
def _find_dialog(self):
if hasattr(self, 'find_window') and self.find_window.winfo_exists():
self.find_window.lift()
return
self.find_window = Toplevel(self.root)
self.find_window.title(u'\u67E5\u627E')
self.find_window.geometry('300x140')
self.find_window.resizable(False, False)
Label(self.find_window, text=u'\u67E5\u627E\u5185\u5BB9\uFF1A').pack(anchor=W, padx=10, pady=(10, 0))
find_var = StringVar()
if hasattr(self, 'last_find'):
find_var.set(self.last_find)
entry = Entry(self.find_window, textvariable=find_var, width=40)
entry.pack(padx=10, pady=5)
case_var = BooleanVar()
Checkbutton(self.find_window, text=u'\u533A\u5206\u5927\u5C0F\u5199', variable=case_var).pack(anchor=W, padx=10)
btn_frame = Frame(self.find_window)
btn_frame.pack(pady=5)
def find_next():
query = find_var.get()
if not query:
return
self.last_find = query
self._find_next(query, case_var.get())
def find_prev():
query = find_var.get()
if not query:
return
self.last_find = query
self._find_prev(query, case_var.get())
Button(btn_frame, text=u'\u67E5\u627E\u4E0B\u4E00\u4E2A', command=find_next).pack(side=LEFT, padx=5)
Button(btn_frame, text=u'\u67E5\u627E\u4E0A\u4E00\u4E2A', command=find_prev).pack(side=LEFT, padx=5)
Button(btn_frame, text=u'\u5173\u95ED', command=self._close_find_dialog).pack(side=LEFT, padx=5)
# ? 支持主回车和小键盘回车
entry.bind('', lambda e: find_next())
entry.bind('', lambda e: find_next())
find_var.trace('w', lambda *args: self._clear_find_tags())
self.find_var = find_var
self.case_var = case_var
self.find_entry = entry
def _close_find_dialog(self):
if hasattr(self, 'find_window'):
self.find_window.destroy()
def _find_next(self, query, match_case=False):
self._clear_find_tags()
self.text_area.tag_configure('find_match', background='yellow', foreground='black')
content = self.text_area.get('1.0', END)
start_pos = self.text_area.index('insert')
if start_pos == '1.0':
start_pos = '1.0 + 1c'
found = False
pattern = re.escape(query)
flags = 0 if match_case else re.IGNORECASE
lines = content.split('\n')
start_line, start_col = map(int, start_pos.split('.'))
start_col += 1
for line_idx in range(start_line - 1, len(lines)):
line = lines[line_idx]
offset = 0
if line_idx == start_line - 1:
line = line[start_col:]
offset = start_col
matches = list(re.finditer(pattern, line, flags))
if matches:
for match in matches:
s = match.start() + offset
e = match.end() + offset
start_index = "%d.%d" % (line_idx + 1, s)
end_index = "%d.%d" % (line_idx + 1, e)
self.text_area.tag_add('find_match', start_index, end_index)
if not found:
self.text_area.mark_set('insert', end_index)
self.text_area.see('insert')
found = True
if not found:
tkMessageBox.showinfo(u'\u67E5\u627E', u'\u672A\u627E\u5230\u201C%s\u201D' % query)
def _find_prev(self, query, match_case=False):
self._clear_find_tags()
self.text_area.tag_configure('find_match', background='yellow', foreground='black')
content = self.text_area.get('1.0', END)
start_pos = self.text_area.index('insert')
if start_pos == '1.0':
start_pos = 'end'
pattern = re.escape(query)
flags = 0 if match_case else re.IGNORECASE
lines = content.split('\n')
line_count = len(lines)
try:
start_line, start_col = map(int, start_pos.split('.'))
except:
start_line, start_col = line_count, 0
found = False
for line_idx in range(start_line - 1, -1, -1):
line = lines[line_idx]
search_line = line if line_idx < start_line - 1 else line[:start_col]
matches = list(re.finditer(pattern, search_line, flags))
if matches:
match = matches[-1]
s = match.start()
e = match.end()
start_index = "%d.%d" % (line_idx + 1, s)
end_index = "%d.%d" % (line_idx + 1, e)
self.text_area.tag_add('find_match', start_index, end_index)
self.text_area.mark_set('insert', start_index)
self.text_area.see('insert')
found = True
break
if not found:
for line_idx in range(len(lines) - 1, start_line - 1, -1):
line = lines[line_idx]
matches = list(re.finditer(pattern, line, flags))
if matches:
match = matches[-1]
s = match.start()
e = match.end()
start_index = "%d.%d" % (line_idx + 1, s)
end_index = "%d.%d" % (line_idx + 1, e)
self.text_area.tag_add('find_match', start_index, end_index)
self.text_area.mark_set('insert', start_index)
self.text_area.see('insert')
found = True
break
if not found:
tkMessageBox.showinfo(u'\u67E5\u627E', u'\u672A\u627E\u5230\u201C%s\u201D' % query)
def _clear_find_tags(self):
self.text_area.tag_remove('find_match', '1.0', END)
def _replace_dialog(self):
if hasattr(self, 'replace_window') and self.replace_window.winfo_exists():
self.replace_window.lift()
return
self.replace_window = Toplevel(self.root)
self.replace_window.title(u'\u66FF\u6362')
self.replace_window.geometry('350x180')
self.replace_window.resizable(False, False)
self.replace_window.transient(self.root)
self.replace_window.grab_set()
Label(self.replace_window, text=u'\u67E5\u627E\u5185\u5BB9\uFF1A').pack(anchor=W, padx=10, pady=(10, 0))
find_var = StringVar()
if hasattr(self, 'last_find'):
find_var.set(self.last_find)
find_entry = Entry(self.replace_window, textvariable=find_var, width=45)
find_entry.pack(padx=10, pady=5)
Label(self.replace_window, text=u'\u66FF\u6362\u4E3A\uFF1A').pack(anchor=W, padx=10, pady=(5, 0))
replace_var = StringVar()
Entry(self.replace_window, textvariable=replace_var, width=45).pack(padx=10, pady=5)
case_var = BooleanVar()
Checkbutton(self.replace_window, text=u'\u533A\u5206\u5927\u5C0F\u5199', variable=case_var).pack(anchor=W, padx=10)
btn_frame = Frame(self.replace_window)
btn_frame.pack(pady=10)
def replace_next():
find_text = find_var.get()
replace_text = replace_var.get()
if not find_text:
return
self.last_find = find_text
count = self._replace_next(find_text, replace_text, case_var.get())
if count == 0:
tkMessageBox.showinfo(u'\u66FF\u6362', u'\u672A\u627E\u5230\u201C%s\u201D' % find_text)
def replace_all():
find_text = find_var.get()
replace_text = replace_var.get()
if not find_text:
return
self.last_find = find_text
count = self._replace_all(find_text, replace_text, case_var.get())
tkMessageBox.showinfo(u'\u66FF\u6362', u'\u5171\u66FF\u6362 %d \u5904' % count)
Button(btn_frame, text=u'\u66FF\u6362', command=replace_next).pack(side=LEFT, padx=10)
Button(btn_frame, text=u'\u5168\u90E8\u66FF\u6362', command=replace_all).pack(side=LEFT, padx=10)
Button(btn_frame, text=u'\u5173\u95ED', command=self._close_replace_dialog).pack(side=LEFT, padx=10)
self.find_var_rep = find_var
self.replace_var = replace_var
self.case_var_rep = case_var
# ? 支持主回车和小键盘回车
self.replace_window.bind('', lambda e: replace_next())
self.replace_window.bind('', lambda e: replace_next())
self.replace_window.bind('', lambda e: self._close_replace_dialog())
find_var.trace('w', lambda *args: self._clear_find_tags())
self.replace_window.update_idletasks()
self.replace_window.focus_set()
def _close_replace_dialog(self):
if hasattr(self, 'replace_window'):
self.replace_window.destroy()
def _replace_next(self, find_str, replace_str, match_case=False):
self._clear_find_tags()
content = self.text_area.get('1.0', END)
start_pos = self.text_area.index('insert')
if start_pos == '1.0':
start_pos = '1.0 + 1c'
pattern = re.escape(find_str)
flags = 0 if match_case else re.IGNORECASE
lines = content.split('\n')
start_line, start_col = map(int, start_pos.split('.'))
start_col += 1
for line_idx in range(start_line - 1, len(lines)):
line = lines[line_idx]
offset = 0
if line_idx == start_line - 1:
line = line[start_col:]
offset = start_col
match = re.search(pattern, line, flags)
if match:
s = match.start() + offset
e = match.end() + offset
start_index = "%d.%d" % (line_idx + 1, s)
end_index = "%d.%d" % (line_idx + 1, e)
self.text_area.delete(start_index, end_index)
self.text_area.insert(start_index, replace_str)
self.text_area.mark_set('insert', start_index)
self.text_area.see('insert')
self._on_modified()
return 1
return 0
def _replace_all(self, find_str, replace_str, match_case=False):
self._clear_find_tags()
content = self.text_area.get('1.0', END)
pattern = re.escape(find_str)
flags = 0 if match_case else re.IGNORECASE
if match_case:
replaced = content.replace(find_str, replace_str)
else:
def replace_func(match):
return replace_str
replaced = re.sub(pattern, replace_func, content, flags=flags)
if replaced != content:
self.text_area.delete('1.0', END)
self.text_area.insert('1.0', replaced)
self._on_modified()
return replaced.count(replace_str)
return 0
if __name__ == '__main__':
root = Tk()
file_arg = sys.argv[1] if len(sys.argv) > 1 else None
app = Editor(root, file_arg)
root.mainloop()
|