def __init__(self, loadFirst=’’, loadEncode=’’):
if not isinstance(self, GuiMaker):
raise TypeError(‘TextEditor needs a GuiMaker mixin’)
self.setFileName(None)
self.lastfind = None
self.openDialog = None
self.saveDialog = None
self.knownEncoding = None # 2.1 кодировки: заполняется Open или Save
self.text.focus() # иначе придется щелкнуть лишний раз
if loadFirst:
self.update() # 2.1: иначе строка 2;
self.onOpen(loadFirst, loadEncode) # см. описание в книге
(‘Paste’, ‘separator’,
|
0,
|
self.onPaste),
|
(‘Delete’,
|
0,
|
self.onDelete),
|
(‘Select All’,
|
0,
|
self.onSelectAll)]
|
),
(‘Search’, 0,
|
[(‘Goto…’,
|
0, self.onGoto),
|
|
(‘Find…’,
|
0, self.onFind),
|
|
(‘Refind’,
|
0, self.onRefind),
|
|
(‘Change…’,
|
0, self.onChange),
|
|
(‘Grep…’,
|
3, self.onGrep)]
|
),
|
|
|
|
(‘Tools’, 0,
|
|
|
|
[(‘Pick Font…
|
’, 6,
|
self.onPickFont),
|
|
(‘Font List’,
|
0,
|
self.onFontList),
|
|
‘separator’,
|
|
|
|
(‘Pick Bg…’,
|
3,
|
self.onPickBg),
|
|
(‘Pick Fg…’,
|
0,
|
self.onPickFg),
|
|
(‘Color List’,
|
0,
|
self.onColorList),
|
|
‘separator’,
|
|
|
|
(‘Info…’,
|
0,
|
self.onInfo),
|
|
(‘Clone’,
|
1,
|
self.onClone),
|
|
(‘Run Code’,
|
0,
|
self.onRunCode)]
|
)] self.toolBar = [
(‘Save’,
|
self.onSave,
|
{‘side’
|
: LEFT}),
|
(‘Cut’,
|
self.onCut,
|
{‘side’
|
: LEFT}),
|
(‘Copy’,
|
self.onCopy,
|
{‘side’
|
: LEFT}),
|
(‘Paste’,
|
self.onPaste,
|
{‘side’
|
: LEFT}),
|
(‘Find’,
|
self.onRefind,
|
{‘side’
|
: LEFT}),
|
(‘Help’,
|
self.help,
|
{‘side’
|
: RIGHT}),
|
(‘Quit’,
|
self.onQuit,
|
{‘side’
|
: RIGHT})]
|
def makeWidgets(self): # вызывается из GuiMaker.__init__
name = Label(self, bg=’black’, fg=’white’) # ниже меню, выше панели name.pack(side=TOP, fill=X) # компоновка меню/панелей
# фрейм GuiMaker
# компонуется сам
vbar = Scrollbar(self)
hbar = Scrollbar(self, orient=’horizontal’)
text = Text(self, padx=5, wrap=’none’) # запретить перенос строк text.config(undo=1, autoseparators=1) # 2.0, по умолчанию 0, 1
vbar.pack(side=RIGHT, fill=Y)
hbar.pack(side=BOTTOM, fill=X) # скомпоновать Text последним
text.pack(side=TOP, fill=BOTH, expand=YES) # иначе обрежутся полосы
# прокрутки
text.config(yscrollcommand=vbar.set) # вызывать vbar.set при
text.config(xscrollcommand=hbar.set) # перемещении по тексту
vbar.config(command=text.yview) # вызывать text.yview при прокрутке hbar.config(command=text.xview) # или hbar[‘command’]=text.xview
# 2.0: применить пользовательские настройки или умолчания
startfont = configs.get(‘font’, self.fonts[0])
startbg = configs.get(‘bg’, self.colors[0][‘bg’])
startfg = configs.get(‘fg’, self.colors[0][‘fg’]) text.config(font=startfont, bg=startbg, fg=startfg) if ‘height’ in configs: text.config(height=configs[‘height’]) if ‘width’ in configs: text.config(width =configs[‘width’]) self.text = text
self.filelabel = name
########################################################################## # Операции меню File
##########################################################################
def my_askopenfilename(self): # объекты запоминают каталог/файл
if not self.openDialog: # последней операции
self.openDialog = Open(initialdir=self.startfiledir, filetypes=self.ftypes)
return self.openDialog.show()
def my_asksaveasfilename(self): # объекты запоминают каталог/файл
if not self.saveDialog: # последней операции
self.saveDialog = SaveAs(initialdir=self.startfiledir, filetypes=self.ftypes)
return self.saveDialog.show()
def onOpen(self, loadFirst=’’, loadEncode=’’):
2.1: полностью переписан для поддержки Юникода; открывает в текстовом режиме с кодировкой, переданной в аргументе, введенной пользователем, заданной в модуле textconfig или с кодировкой по умолчанию;
в крайнем случае открывает файл в двоичном режиме и отбрасывает символы \r в Windows, если они присутствуют, чтобы обеспечить нормальное отображение текста; содержимое извлекается и возвращается в виде строки str, поэтому при сохранении его требуется кодировать: сохраняет кодировку, используемую здесь;
предварительно проверяет возможность открытия файла;
мы могли бы также вручную загружать и декодировать bytes в str, чтобы избежать необходимости выполнять несколько попыток открытия, но этот прием подходит не для всех случаев;
порядок выбора кодировки настраивается в локальном textConfig.py:
1) сначала применяется кодировка, переданная клиентом (например, кодировка из заголовка сообщения электронной почты)
2) затем, если opensAskUser возвращает True, применяется кодировка, введенная пользователем (предварительно в диалог записывается кодировка по умолчанию)
3) затем, если opensEncoding содержит непустую строку, применяется эта кодировка: ‘latin-1’ и так далее.
4) затем выполняется попытка применить кодировку sys.getdefaultencoding()
5) в крайнем случае выполняется чтение в двоичном режиме и используется алгоритм, заложенный в библиотеку Tk
if self.text_edit_modified(): # 2.0
if not askyesno(‘PyEdit’, ‘Text has changed: discard changes?’): return
file = loadFirst or self.my_askopenfilename() if not file:
return
if not os.path.isfile(file):
showerror(‘PyEdit’, ‘Could not open file ‘ + file) return
# применить известную кодировку, если указана
# (например, из заголовка сообщения электронной почты)
text = None # пустой файл = ‘’ = False: проверка на None!
if loadEncode:
try:
text = open(file, ‘r’, encoding=loadEncode).read() self.knownEncoding = loadEncode
except (UnicodeError, LookupError, IOError): # Lookup: ошибка pass # в имени
# применить кодировку, введенную пользователем,
# предварительно записать в диалог следующий вариант, как значение
# по умолчанию
if text == None and self.opensAskUser:
self.update() # иначе в некоторых случаях диалог не появится askuser = askstring(‘PyEdit’, ‘Enter Unicode encoding for open’, initialvalue=(self.opensEncoding or
sys.getdefaultencoding() or ‘’)) if askuser:
try:
text = open(file, ‘r’, encoding=askuser).read() self.knownEncoding = askuser
except (UnicodeError, LookupError, IOError): pass
# применить кодировку из файла с настройками (может быть, выполнять
# эту попытку до того, как запрашивать кодировку у пользователя?)
if text == None and self.opensEncoding:
try:
text = open(file, ‘r’, encoding=self.opensEncoding).read() self.knownEncoding = self.opensEncoding
except (UnicodeError, LookupError, IOError): pass
# применить системную кодировку по умолчанию (utf-8 в windows;
# всегда пытаться использовать utf8?) if text == None:
try:
text = open(file, ‘r’,
encoding=sys.getdefaultencoding()).read() self.knownEncoding = sys.getdefaultencoding()
except (UnicodeError, LookupError, IOError): pass
# крайний случай: использовать двоичный режим и положиться на # возможности Tk if text == None:
try:
text = open(file, ‘rb’).read() # строка bytes text = text.replace(b’\r\n’, b’\n’) # для отображения self.knownEncoding = None # и последующего сохранения
except IOError:
pass
if text == None:
showerror(‘PyEdit’, ‘Could not decode and open file ‘ + file) else:
self.setAllText(text)
self.setFileName(file)
self.text.edit_reset() # 2.0: очистка стеков undo/redo
self.text.edit_modified(0) # 2.0: сбросить флаг наличия изменений
def onSave(self):
self.onSaveAs(self.currfile) # may be None
def onSaveAs(self, forcefile=None):
2.1: полностью переписан для поддержки Юникода: виджет Text всегда возвращает содержимое в виде строки str, поэтому нам необходимо побеспокоиться о кодировке, чтобы сохранить файл, независимо от режима, в котором открывается выходной файл (для двоичного режима необходимо будет получить bytes, а для текстового необходимо указать кодировку); пытается применить кодировку, использовавшуюся при открытии или сохранении (если известна), предлагаемую пользователем, указанную в файле с настройками, и системную кодировку по умолчанию; в большинстве случаев можно использовать системную кодировку по умолчанию;
в случае успешного выполнения операции сохраняет кодировку для использования в дальнейшем, потому что это может быть первая операция Save после операции New или вставки текста вручную; в файле с настройками можно определить, чтобы обе операции, Save и Save As, использовали последнюю известную кодировку (однако если для операции Save это оправданно, то в случае с операцией Save As это не так очевидно); графический интерфейс предварительно записывает эту кодировку в диалог, если она известна;
выполняет text.encode() вручную, чтобы избежать создания файла; для текстовых файлов автоматически выполняется преобразование символов конца строки: в Windows добавляются символы \r, отброшенные при открытии файла в текстовом (автоматически) или в двоичном (вручную) режиме; Если содержимое вставлялось вручную, здесь необходимо предварительно удалить символы \r, иначе они будут продублированы; knownEncoding=None перед первой операцией Open или Save, после New и если операция Open открыла файл в двоичном режиме;
порядок выбора кодировки настраивается в локальном textConfig.py:
1) если savesUseKnownEncoding > 0, применить кодировку, использованную в последней операции Open или Save
2) если savesAskUser = True, применить кодировку, указанную пользователем (предлагать известную в качестве значения по умолчанию?)
3) если savesEncoding — непустая строка, применить эту кодировку: ‘utf-8’ и так далее
4) в крайнем случае применить sys.getdefaultencoding()
filename = forcefile or self.my_asksaveasfilename() if not filename:
return
text = self.getAllText() # 2.1: строка str, без символов \r, encpick = None # даже если текст читался/вставлялся
# в двоичном виде
# применить известную кодировку, использовавшуюся в последней операции # Open или Save, если известна
if self.knownEncoding and ( # известна?
(forcefile and self.savesUseKnownEncoding >= 1) or # для Save? (not forcefile and self.savesUseKnownEncoding >= 2)):# для SaveAs? try:
text.encode(self.knownEncoding)
encpick = self.knownEncoding
except UnicodeError: pass
# применить кодировку, введенную пользователем,
# предварительно записать в диалог следующий вариант, как значение # по умолчанию
if not encpick and self.savesAskUser:
self.update()# иначе в некоторых случаях диалог не появится askuser = askstring(‘PyEdit’, ‘Enter Unicode encoding for save’, initialvalue=(self.knownEncoding or self.savesEncoding or sys.getdefaultencoding() or ‘’)) if askuser:
try:
text.encode(askuser)
encpick = askuser
except (UnicodeError, LookupError): # LookupError: ошибка в имени pass # UnicodeError: ошибка
# кодирования
# применить кодировку из файла с настройками
if not encpick and self.savesEncoding:
try:
text.encode(self.savesEncoding)
encpick = self.savesEncoding
except (UnicodeError, LookupError): pass
# применить системную кодировку по умолчанию (utf8 в windows)
if not encpick:
try:
text.encode(sys.getdefaultencoding())
encpick = sys.getdefaultencoding()
except (UnicodeError, LookupError): pass
# открыть в текстовом режиме, чтобы автоматически выполнить
# преобразование символов конца строки и применить кодировку
if not encpick:
showerror(‘PyEdit’, ‘Could not encode for file ‘ + filename)
else:
try:
file = open(filename, ‘w’, encoding=encpick)
file.write(text)
file.close()
except:
showerror(‘PyEdit’, ‘Could not write file ‘ + filename)
else:
self.setFileName(filename) # может быть вновь созданным
self.text.edit_modified(0) # 2.0: сбросить флаг изменений
self.knownEncoding = encpick # 2.1: запомнить кодировку # не сбрасывать стеки undo/redo!
def onNew(self):
запускает редактирование совершенно нового файла в текущем окне;
смотрите метод onClone, который вместо этого создает независимое окно редактирования;
if self.text_edit_modified(): # 2.0
if not askyesno(‘PyEdit’, ‘Text has changed: discard changes?’): return
self.setFileName(None)
self.clearAllText()
self.text.edit_reset() # 2.0: очистить стеки undo/redo
self.text.edit_modified(0) # 2.0: сбросить флаг наличия изменений
self.knownEncoding = None # 2.1: кодировка неизвестна
def onQuit(self):
вызывается выбором операции Quit в меню/панели инструментов и щелчком на кнопке X в заголовке окна;
2.1: не завершать приложение при наличии несохраненных изменений;
2.2: не выводить запрос на подтверждение, если нет изменений в self; перемещен в классы окон верхнего уровня ниже, так как его
реализация может зависеть от особенностей использования: операция Quit в графическом интерфейсе может вызывать метод quit() для завершения, destroy() — чтобы просто закрыть окно Toplevel, Tk или фрейм с редактором, эта операция может даже вообще не предоставляться, если редактор присоединяется, как компонент; проверяет self на наличие несохраненных изменений, а если предполагается вызов метода quit(), главные окна должны также проверить наличие несохраненных изменений в других окнах, присутствующих в глобальном списке процесса;
assert False, ‘onQuit must be defined in window-specific sublass’
def text_edit_modified(self):
2.3: теперь действует! кажется, проблема заключалась в типе bool результата в tkinter;
2.0: self.text.edit_modified() не работает в Python 2.4: выполнить проверку вручную;
return self.text.edit_modified()
#return self.tk.call((self.text._w, ‘edit’) + (‘modified’, None))
########################################################################## # Операции меню Edit
##########################################################################
def onUndo(self): # 2.0
try: # tk8.4 поддерживает стеки undo/redo
self.text.edit_undo() # возбуждает исключение, если стеки пустые except TclError: # меню открывается для быстрого доступа
showinfo(‘PyEdit’, ‘Nothing to undo’) # к операциям
def onRedo(self): # 2.0: возврат отмененной операции
try: # редактирования
self.text.edit_redo()
except TclError:
showinfo(‘PyEdit’, ‘Nothing to redo’)
def onCopy(self): # получить текст, выделенный мышью
if not self.text.tag_ranges(SEL): # сохранить в системном буфере
showerror(‘PyEdit’, ‘No text selected’) else:
text = self.text.get(SEL_FIRST, SEL_LAST)
self.clipboard_clear()
self.clipboard_append(text)
def onDelete(self): # удалить выделенный текст без сохранения
if not self.text.tag_ranges(SEL):
showerror(‘PyEdit’, ‘No text selected’) else:
self.text.delete(SEL_FIRST, SEL_LAST)
def onCut(self):
if not self.text.tag_ranges(SEL):
showerror(‘PyEdit’, ‘No text selected’) else:
self.onCopy() # сохранить и удалить выделенный текст
self.onDelete()
def onPaste(self):
try:
text = self.selection_get(selection=’CLIPBOARD’)
except TclError:
showerror(‘PyEdit’, ‘Nothing to paste’) return
self.text.insert(INSERT, text) # вставить в текущую позицию курсора self.text.tag_remove(SEL, ‘1.0’, END)
self.text.tag_add(SEL, INSERT+’-%dc’ % len(text), INSERT)
self.text.see(INSERT) # выделить, чтобы можно было вырезать
def onSelectAll(self):
self.text.tag_add(SEL, ‘1.0’, END+’-1c’)# выделить весь текст
self.text.mark_set(INSERT, ‘1.0’) # переместить позицию в начало
self.text.see(INSERT) # прокрутить в начало
########################################################################## # Операции меню Search
##########################################################################
def onGoto(self, forceline=None):
line = forceline or askinteger(‘PyEdit’, ‘Enter line number’) self.text.update() self.text.focus()
if line is not None:
maxindex = self.text.index(END+’-1c’)
maxline = int(maxindex.split(‘.’)[0])
if line > 0 and line <= maxline:
self.text.mark_set(INSERT, ‘%d.0’ % line) # перейти к стр.
self.text.tag_remove(SEL, ‘1.0’, END) # снять выделен.
self.text.tag_add(SEL, INSERT, ‘insert + 1l’) # выделить стр.
self.text.see(INSERT) # прокрутить
else: # до строки
showerror(‘PyEdit’, ‘Bad line number’)
def onFind(self, lastkey=None):
key = lastkey or askstring(‘PyEdit’, ‘Enter search string’) self.text.update()
self.text.focus() self.lastfind = key if key: # 2.0: без учета регистра символов
nocase = configs.get(‘caseinsens’, True) # 2.0: настройки where = self.text.search(key, INSERT, END, nocase=nocase) if not where: # не переходить
showerror(‘PyEdit’, ‘String not found’)# в начало else:
pastkey = where + ‘+%dc’ % len(key) # позиция после ключа self.text.tag_remove(SEL, ‘1.0’, END) # снять выделение self.text.tag_add(SEL, where, pastkey) # выделить ключ self.text.mark_set(INSERT, pastkey) # для след. поиска self.text.see(where) # прокрутить экран
def onRefind(self):
self.onFind(self.lastfind)
def onChange(self):
немодальный диалог поиска с заменой
2.1: поля ввода диалога передаются обработчику, допускается открывать одновременно несколько диалогов поиска с заменой
new = Toplevel(self)
new.title(‘PyEdit — change’)
Label(new, text=’Find text?’, relief=RIDGE, width=15).grid(row=0,
column=0)
Label(new, text=’Change to?’, relief=RIDGE, width=15).grid(row=1, column=0) entry1 = Entry(new) entry2 = Entry(new) entry1.grid(row=0, column=1, sticky=EW) entry2.grid(row=1, column=1, sticky=EW)
def onFind(): # использует поле ввода из внешней обл. видимости
self.onFind(entry1.get()) # вызов обработчика диалога поиска
def onApply():
self.onDoChange(entry1.get(), entry2.get())
Button(new, text=’Find’, command=onFind ).grid(row=0,
column=2, sticky=EW) Button(new, text=’Apply’, command=onApply).grid(row=1,
column=2, sticky=EW) new.columnconfigure(1, weight=1) # растягиваемые поля ввода
def onDoChange(self, findtext, changeto):
# реализует замену для диалога поиска с заменой:
# заменяет и повторяет поиск
if self.text.tag_ranges(SEL): # сначала найти
self.text.delete(SEL_FIRST, SEL_LAST)
self.text.insert(INSERT, changeto) # удалит, если пусто self.text.see(INSERT)
self.onFind(findtext) # переход к следующему
self.text.update() # принудительное обновление
def onGrep(self):
новое в версии 2.1: многопоточная реализация поиска во внешних файлах; выполняет поиск указанной строки в файлах, имена которых соответствуют заданному шаблону; щелчок на элементе в списке открывает соответствующий файл, при этом выполняется переход к строке с найденным вхождением;
поиск выполняется в отдельном потоке, чтобы графический интерфейс не блокировался и оставался активным и чтобы позволить одновременно выполнять несколько операций поиска; можно было бы использовать модуль, если прекращать цикл проверки при отсутствии активных операций поиска;
алгоритм выбора кодировки при выполнении поиска: содержимое текстовых файлов в дереве, где выполняется поиск, может храниться в любых кодировках: мы не предлагаем вводить имя кодировки для каждого файла (как при открытии), однако позволяем указать кодировку для всего дерева, предварительно устанавливая общесистемную кодировку по умолчанию, используемую файловой системой или для представления текста, и пропускаем файлы, декодирование которых терпит неудачу; в самом тяжелом случае пользователю может потребоваться выполнить поиск N раз, если в дереве могут присутствовать файлы с текстом в N различных кодировках; иначе операция открытия будет возбуждать исключение, а открытие в двоичном режиме может не дать совпадения кодированного текста с испытуемой строкой;
TBD: может, лучше было бы выводить сообщение об ошибке при встрече с файлом, который не удалось декодировать?
но файлы с кодировкой utf-16 (2 байта на символ), созданные в Notepad, благополучно могут декодироваться с применением кодировки utf-8, однако строка при этом не будет найдена;
TBD: можно было бы позволить вводить несколько имен кодировок, отделяя их друг от друга запятыми, и пробовать применять их поочередно к каждому файлу, помимо loadEncode
from PP4E.Gui.ShellGui.formrows import makeFormRow
# немодальный диалог: ввод имени каталога, шаблон имени файла, # искомая строка popup = Toplevel()
popup.title(‘PyEdit — grep’)
var1 = makeFormRow(popup, label=’Directory root’, width=18, browse=False)
var2 = makeFormRow(popup, label=’Filename pattern’, width=18, browse=False)
var3 = makeFormRow(popup, label=’Search string’, width=18, browse=False)
var4 = makeFormRow(popup, label=’Content encoding’, width=18, browse=False)
var1.set(‘.’) # текущий каталог
var2.set(‘*.py’) # начальные значения
var4.set(sys.getdefaultencoding()) # для содержимого файлов, а не имен cb = lambda: self.onDoGrep(var1.get(), var2.get(), var3.get(), var4.get())
Button(popup, text=’Go’,command=cb).pack()
def onDoGrep(self, dirname, filenamepatt, grepkey, encoding):
вызывается щелчком на кнопке Go в диалоге Grep: заполняет список найденными совпадениями
tbd: возможно, следует запускать поток-производитель как демон, чтобы он автоматически завершался вместе с приложением?
import threading, queue
# создать немодальный и незакрываемый диалог mypopup = Tk()
mypopup.title(‘PyEdit — grepping’)
status = Label(mypopup,
text=’Grep thread searching for: %r…’ % grepkey) status.pack(padx=20, pady=20)
mypopup.protocol(‘WM_DELETE_WINDOW’, lambda: None) # игнорировать
# кнопку X
# запустить поток—производитель, цикл проверки результатов myqueue = queue.Queue() threadargs = (filenamepatt, dirname, grepkey, encoding, myqueue) threading.Thread(target=self.grepThreadProducer, args=threadargs).start()
self.grepThreadConsumer(grepkey, encoding, myqueue, mypopup)
def grepThreadProducer(self, filenamepatt, dirname, grepkey, encoding, myqueue):
выполняется в параллельном потоке, не имеющем отношения к графическому интерфейсу: помещает в очередь список с результатами find.find; найденные совпадения можно было бы помещать в очередь по мере их обнаружения, но для этого необходимо обеспечить сохранение окна на экране; здесь могут возникать ошибки декодирования не только содержимого, но и имен файлов;
TBD: чтобы избежать ошибок декодирования имен файлов в os.walk/listdir, можно было бы передавать методу find() строку bytes, но какую кодировку использовать: sys.getfilesystemencoding(), если она не равна None? Смотрите также примечание в разделе “Модуль fnmatch” в главе 6: в версии 3.1 модуль fnmatch всегда преобразует текст в двоичное представление, используя кодировку Latin-1;
from PP4E.Tools.find import find
matches = []
try:
for filepath in find(pattern=filenamepatt, startdir=dirname): try:
textfile = open(filepath, encoding=encoding)
for (linenum, linestr) in enumerate(textfile): if grepkey in linestr:
msg = ‘%s@%d [%s]’ % (filepath,
linenum + 1, linestr) matches.append(msg)
except UnicodeError as X: # напр.: декодир.,
print(‘Unicode error in:’, filepath, X) # двоичный режим
except IOError as X:
print(‘IO error in:’, filepath, X) # напр.: права доступа finally:
myqueue.put(matches) # остановить цикл потребителя при исключении: # имена файлов?
def grepThreadConsumer(self, grepkey, encoding, myqueue, mypopup):
выполняется в главном потоке графического интерфейса: просматривает очередь в ожидании результатов или []; может иметься несколько активных потоков/циклов/очередей, связанных с поиском; в процессе могут присутствовать другие типы потоков/циклов проверки, особенно если PyEdit прикрепляется как компонент (PyMailGUI);
import queue
try:
matches = myqueue.get(block=False)
except queue.Empty:
myargs = (grepkey, encoding, myqueue, mypopup)
self.after(250, self.grepThreadConsumer, *myargs) else:
mypopup.destroy() # закрыть информационный диалог self.update() # и стереть его с экрана if not matches:
showinfo(‘PyEdit’, ‘Grep found no matches for: %r’ % grepkey) else:
self.grepMatchesList(matches, grepkey, encoding)
def grepMatchesList(self, matches, grepkey, encoding):
заполняет список найденными совпадениями в случае успеха;
так как поиск увенчался успехом, кодировка уже известна: использовать ее в обработчике щелчка на файле в списке, чтобы обеспечить его открытие без обращения к пользователю;
from PP4E.Gui.Tour.scrolledlist import ScrolledList print(‘Matches for %s: %s’ % (grepkey, len(matches)))
# перехватывает двойной щелчок на списке
class ScrolledFilenames(ScrolledList):
def runCommand(self, selection):
file, line = selection.split(‘ [‘, 1)[0].split(‘@’)
editor = TextEditorMainPopup(
loadFirst=file, winTitle=’ grep match’,
loadEncode=encoding)
editor.onGoto(int(line))
editor.text.focus_force() # на самом деле не требуется
# новое модальное окно
popup = Tk()
popup.title(‘PyEdit — grep matches: %r (%s)’ % (grepkey, encoding))
ScrolledFilenames(parent=popup, options=matches) ########################################################################## # Операции меню Tools
########################################################################## def onFontList(self):
self.fonts.append(self.fonts[0]) # выбрать следующий шрифт в списке
del self.fonts[0] # изменит размер текстовой области
self.text.config(font=self.fonts[0]) def onColorList(self):
self.colors.append(self.colors[0]) # выбрать следующий цвет в списке
del self.colors[0] # текущий сместить в конец
self.text.config(fg=self.colors[0][‘fg’], bg=self.colors[0][‘bg’])
def onPickFg(self):
self.pickColor(‘fg’) # добавлено 10/02/00
def onPickBg(self): # выбрать произвольный цвет
self.pickColor(‘bg’) # в стандартном диалоге выбора цвета
def pickColor(self, part): # это очень просто
(triple, hexstr) = askcolor() if hexstr:
self.text.config(**{part: hexstr})
def onInfo(self):
диалог с информацией о тексте и о местоположении курсора;
ВНИМАНИЕ (2.1): при вычислении позиции курсора библиотека Tk считает символ табуляции, как один символ: следует умножать их на 8, чтобы обеспечить соответствие с визуальным положением?
text = self.getAllText() # добавлено 5/3/00 за 15 мин.
bytes = len(text) # словами считается все, что
lines = len(text.split(‘\n’)) # отделяется пробелами
words = len(text.split()) # 3.x: в bytes — символы
index = self.text.index(INSERT) # в str — кодовые пункты Юникода
where = tuple(index.split(‘.’)) showinfo(‘PyEdit Information’,
‘Current location:\n\n’ + ‘line:\t%s\ncolumn:\t%s\n\n’ % where + ‘File text statistics:\n\n’ + ‘chars:\t%d\nlines:\t%d\nwords:\t%d\n’ % (bytes, lines, words))
def onClone(self, makewindow=True):
открывает новое окно редактора, не изменяя уже открытое (onNew); наследует поведение операции Quit и других от окна, копия которого создается;
2.1: подклассы должны переопределять/замещать этот метод, если будут создавать собственные окна,
иначе этот метод создаст дополнительное поддельное пустое окно;
if not makewindow:
new = None # предполагается, что класс создает
else: # собственное окно
new = Toplevel() # новое окно редактора в том же процессе myclass = self.__class__ # объект класса экземпляра (самый нижний) myclass(new) # прикрепить/запустить экземпляр моего класса
def onRunCode(self, parallelmode=True):
выполнение редактируемого программного кода Python — это не IDE, но удобно; пытается выполнить в каталоге файла, не в cwd (может быть корнем PP4E); вводит и добавляет аргументы командной строки для файлов сценариев;
stdin/out/err для программного кода = стартовое окно редактора, если оно есть: запускайте редактор в окне консоли, чтобы увидеть вывод, производимый программным кодом; если parallelmode=True, открывает окно DOS для операций ввода-вывода; путь поиска модулей будет включать ‘.’ при запуске; при выполнении программного кода как отдельной строки корневым окном может быть окно PyEdit; здесь также можно использовать модули subprocess и multiprocessing;
2.1: исправлено на использование базового имени файла после chdir, без пути;
2.1: использует StartArgs для передачи аргументов в режиме запуска файлов в Windows;
2.1: вызывает update() после первого диалога, в противном случае второй диалог иногда не появляется на экране;
def askcmdargs():
return askstring(‘PyEdit’, ‘Commandline arguments?’) or ‘’
from PP4E.launchmodes import System, Start, StartArgs, Fork filemode = False
thefile = str(self.getFileName())
if os.path.exists(thefile):
filemode = askyesno(‘PyEdit’, ‘Run from file?’)
self.update() # 2.1: вызывает update()
if not filemode: # выполнить как строку
cmdargs = askcmdargs()
namespace = {‘__name__’: ‘__main__’} # выполнить как сценарий
sys.argv = [thefile] + cmdargs.split() # можно использов. потоки exec(self.getAllText() + ‘\n’, namespace)# игнорировать исключения
elif self.text_edit_modified(): # 2.0: проверка изменений
showerror(‘PyEdit’, ‘Text changed: you must save before run’)
else:
cmdargs = askcmdargs()
mycwd = os.getcwd() # cwd может быть корнем
dirname, filename = os.path.split(thefile) # каталог, базовое имя
os.chdir(dirname or mycwd)
thecmd = filename + ‘ ‘ + cmdargs if not parallelmode:
System(thecmd, thecmd)()
else:
if sys.platform[:3] == ‘win’:
run = StartArgs if cmdargs else Start # 2.1: аргументы
run(thecmd, thecmd)() # или всегда Spawn
else:
Fork(thecmd, thecmd)() # породить параллельно
os.chdir(mycwd) # вернуться в каталог def onPickFont(self):
2.2: немодальный диалог выбора шрифта
2.3: поля ввода диалога передаются обработчику, допускается открывать одновременно несколько диалогов поиска выбора шрифта
from PP4E.Gui.ShellGui.formrows import makeFormRow
popup = Toplevel(self)
popup.title(‘PyEdit — font’)
var1 = makeFormRow(popup, label=’Family’, browse=False)
var2 = makeFormRow(popup, label=’Size’, browse=False)
var3 = makeFormRow(popup, label=’Style’, browse=False)
var1.set(‘courier’)
var2.set(‘12’) # предлагаемые значения
var3.set(‘bold italic’) # смотрите допустимые значения в списке выбора
Button(popup, text=’Apply’, command=
lambda: self.onDoFont(var1.get(), var2.get(),
var3.get())).pack()
def onDoFont(self, family, size, style):
try:
self.text.config(font=(family, int(size), style)) except:
showerror(‘PyEdit’, ‘Bad font specification’)
########################################################################## # Прочие утилиты, полезные за пределами этого класса
##########################################################################
def isEmpty(self):
return not self.getAllText()
def getAllText(self):
return self.text.get(‘1.0’, END+’-1c’) # извлечь текст как строку str
def setAllText(self, text):
вызывающий: должен предварительно вызвать self.update(), если только что был прикреплен, иначе начальная позиция может оказаться не в первой, а во второй строке (2.1; ошибка Tk?)
self.text.delete(‘1.0’, END) # записать текстовую строку в виджет
self.text.insert(END, text) # или ‘1.0’; текст = bytes или str
self.text.mark_set(INSERT, ‘1.0’) # переместить точку ввода в начало self.text.see(INSERT) # прокрутить в начало, в точку вставки
def clearAllText(self):
self.text.delete(‘1.0’, END) # очистить текст в виджете
def getFileName(self): return self.currfile
def setFileName(self, name): # смотрите также: onGoto(linenum) self.currfile = name # для последующего сохранения self.filelabel.config(text=str(name))
def setKnownEncoding(self, encoding=’utf-8’): # 2.1: для сохранения self.knownEncoding = encoding # иначе будут использованы настройки, # запрос?
def setBg(self, color):
self.text.config(bg=color) # для установки вручную из программы def setFg(self, color):
self.text.config(fg=color) # ‘black’, шестнадцатеричная строка
def setFont(self, font):
self.text.config(font=font) # (‘семейство’, размер, ‘стиль’)
def setHeight(self, lines): # по умолчанию = 24 строки x 80 символов
self.text.config(height=lines)# можно также взять из textCongif.py
def setWidth(self, chars):
self.text.config(width=chars)
def clearModified(self):
self.text.edit_modified(0) # сбросить флаг наличия изменений def isModified(self): # были изменения с момента
return self.text_edit_modified() # последнего сброса флага?
def help(self):
showinfo(‘About PyEdit’, helptext % ((Version,)*2))
##############################################################################
# Готовые к употреблению классы редактора, подмешиваемые в подкласс
# фрейма GuiMaker, создающий меню и панели инструментов.
#
# Эти классы реализуют типичные случаи использования, однако возможны и другие
# реализации; для запуска PyEdit, как самостоятельной программы, следует
# вызвать метод TextEditorMain().mainloop(); переопределяйте/расширяйте
# в подклассах метод onQuit, чтобы обеспечить перехват события завершения
# приложения или уничтожения окна (смотрите пример PyView);
# ВНИМАНИЕ: можно было бы использовать windows.py для создания ярлыков,
# но здесь используется собственный протокол завершения.
##############################################################################
#
# 2.1: в quit(), не завершать без предупреждения, если в процессе открыты
# другие окна редактора и в них имеются несохраненные изменения — изменения
# будут потеряны, потому что все остальные окна тоже закрываются, включая
# множественные родительские окна Tk, включающие редактор; для слежения за
# всеми окнами PyEdit используется список экземпляров, созданных в процессе;
# это может оказаться чрезмерной мерой (если вместо quit() вызывается
# destroy(), когда достаточно проверить только дочернее окно редактирования
# уничтожаемого родителя), но лучше перестраховаться; метод onQuit перемещен
# сюда, потому что его реализация отличается для окон разных типов и может
# присутствовать не во всех окнах;
#
# предполагается, что TextEditorMainPopup никогда не будет играть роль
# родителя для других окон редактирования — дочерние виджеты Toplevel
# уничтожаются вместе со своими родителями; это не позволяет предотвратить
# закрытие из-за пределов классов PyEdit (метод quit в tkinter доступен
# во всех виджетах, и любой виджет может быть родителем для Toplevel!);
# ответственность за проверку наличия изменений в содержимом редактора
# полностью возлагается на клиента; обратите внимание, что в данной ситуации
# привязка события <Destroy> не даст ровным счетом ничего, потому что его
# обработчик не может выполнять операции с графическим интерфейсом, такие как
# проверка наличия изменений и извлечение текста, — дополнительную информацию
# об этом событии смотрите в книге и в модуле destroyer.py;
# ########################################
# когда текстовый редактор владеет окном
########################################
class TextEditorMain(TextEditor, GuiMakerWindowMenu):
главное окно редактора PyEdit, которое вызывает метод quit() при выполнении операции Quit графического интерфейса для завершения приложения и конструирует меню в окне; родителем может быть окно Tk, по умолчанию, окно Tk, создаваемое явно, или объект Toplevel:
родитель должен быть окном и, вероятно, окном Tk, чтобы избежать закрытия без предупреждения вместе с родителем; при выполнении операции Quit графического интерфейса все главные окна PyEdit проверяют остальные окна
PyEdit, открытые в процессе, на наличие несохраненных изменений, поскольку вызов метода quit() здесь приведет к завершению всего приложения; фрейм редактора необязательно должен занимать окно целиком (окно может включать и другие компоненты: смотрите PyView), но его операция Quit завершает программу; метод onQuit вызывается операцией Quit, выполняемой щелчком на кнопке в панели инструментов, выбором пункта в меню File, а также щелчком на кнопке X в заголовке окна;
def __init__(self, parent=None, loadFirst=’’, loadEncode=’’):
# редактор занимает все родительское окно
GuiMaker.__init__(self, parent) # использует главное меню окна
TextEditor.__init__(self, loadFirst, loadEncode)# фрейм GuiMaker
# прикрепляет себя сам self.master.title(‘PyEdit ‘ + Version) # заголовок, кнопка X, если self.master.iconname(‘PyEdit’) # выполняется как отдельная
self.master.protocol(‘WM_DELETE_WINDOW’, self.onQuit) # программа TextEditor.editwindows.append(self)
def onQuit(self): # вызывается операцией Quit
close = not self.text_edit_modified() # проверить себя, запросить, if not close: # проверить другие
close = askyesno(‘PyEdit’,
‘Text changed: quit and discard changes?’) if close:
windows = TextEditor.editwindows
changed = [w for w in windows
if w != self and w.text_edit_modified()]
if not changed:
GuiMaker.quit(self) # завершить все приложение, независимо от else: # типа виджета
numchange = len(changed) verify = ‘%s other edit window%s changed: ‘ verify = verify + ‘quit and discard anyhow?’ verify = verify % (numchange, ‘s’ if numchange > 1 else ‘’) if askyesno(‘PyEdit’, verify):
GuiMaker.quit(self)
class TextEditorMainPopup(TextEditor, GuiMakerWindowMenu):
всплывающее окно PyEdit, которое вызывает метод destroy() при выполнении операции Quit графического интерфейса, закрывает только себя и создает меню в окне; создает собственного родителя Toplevel, который является дочерним для окна Tk по умолчанию (если передается значение None) или для другого указанного окна или виджета (например, для фрейма);
добавляется в список для проверки при закрытии любого главного окна PyEdit; если будет создано главное окно PyEdit, родитель данного окна также должен быть родителем главного окна PyEdit, чтобы оно не было закрыто без предупреждения; метод onQuit вызывается операцией Quit, выполняемой щелчком на кнопке в панели инструментов, выбором пункта в меню File, а также щелчком на кнопке X в заголовке окна;
def __init__(self, parent=None, loadFirst=’’, winTitle=’’, loadEncode=’’): # создать собственное окно
self.popup = Toplevel(parent)
GuiMaker.__init__(self, self.popup) # использует главное меню окна TextEditor.__init__(self, loadFirst, loadEncode) # фрейм в новом окне assert self.master == self.popup self.popup.title(‘PyEdit ‘ + Version + winTitle) self.popup.iconname(‘PyEdit’) self.popup.protocol(‘WM_DELETE_WINDOW’, self.onQuit) TextEditor.editwindows.append(self)
def onQuit(self): close = not self.text_edit_modified() if not close:
close = askyesno(‘PyEdit’,
‘Text changed: quit and discard changes?’) if close:
self.popup.destroy() # закрыть только это окно
TextEditor.editwindows.remove(self) # (и все дочерние окна)
def onClone(self):
TextEditor.onClone(self, makewindow=False) # я создаю собственное окно
########################################### # когда редактор встраивается в другое окно ###########################################
class TextEditorComponent(TextEditor, GuiMakerFrameMenu):
прикрепляемый фрейм компонента PyEdit с полными меню/панелью инструментов, который вызывает destroy() при выполнении операции Quit графического интерфейса и стирает только себя; при выполнении операции Quit проверяется наличие несохраненных изменений только в этом редакторе; не перехватывает щелчок на кнопке X в заголовке окна: не имеет собственного окна; не добавляет себя в список отслеживаемых окон: является частью более крупного приложения;
def __init__(self, parent=None, loadFirst=’’, loadEncode=’’):
# использовать меню на основе фрейма
GuiMaker.__init__(self, parent) # все меню, кнопки в GuiMaker должны TextEditor.__init__(self, loadFirst, loadEncode) # создаваться первыми
def onQuit(self):
close = not self.text_edit_modified() if not close:
close = askyesno(‘PyEdit’,
‘Text changed: quit and discard changes?’) if close:
self.destroy() # стереть свой фрейм, но не завершать вмещающее # приложение
class TextEditorComponentMinimal(TextEditor, GuiMakerFrameMenu):