Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions Lib/idlelib/idle_test/test_textview.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import os
import unittest
import unittest.mock as mock
from tkinter import Tk, TclError, CHAR, NONE, WORD
from tkinter.ttk import Button
from idlelib.idle_test.mock_idle import Func
Expand Down Expand Up @@ -186,6 +187,32 @@ def test_nowrap(self):
text_widget = view.viewframe.textframe.text
self.assertEqual(text_widget.cget('wrap'), 'none')

def test_find(self):
view = tv.view_text(root, 'Title', 'test text', modal=False)
with mock.patch('idlelib.textview.search') as mock_search:
view.viewframe.text.event_generate('<<find>>')
root.update_idletasks()
mock_search.find.assert_called_once()

def test_find_selection_and_find_again(self):
view = tv.view_text(root, 'Title', 'test text', modal=False)

# Select the first "te".
view.viewframe.text.tag_add('sel', '1.0', '1.2')
selection_range = view.viewframe.text.tag_nextrange('sel', '1.0')
self.assertEqual(selection_range, ('1.0', '1.2'))

# After <<find-selection>>, the second "te" should be selected.
view.viewframe.text.event_generate('<<find-selection>>')
root.update_idletasks()
selection_range = view.viewframe.text.tag_nextrange('sel', '1.0')
self.assertEqual(selection_range, ('1.5', '1.7'))

# After <<find-again>>, the selection should return to the first "te".
view.viewframe.text.event_generate('<<find-again>>')
selection_range = view.viewframe.text.tag_nextrange('sel', '1.0')
self.assertEqual(selection_range, ('1.0', '1.2'))


# Call ViewWindow with _utest=True.
class ButtonClickTest(unittest.TestCase):
Expand Down
15 changes: 13 additions & 2 deletions Lib/idlelib/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@
from idlelib import searchengine
from idlelib.searchbase import SearchDialogBase


def _get_selected_text(text):
"""Return selected text in a text widget.

Returns None if no text is selected.
"""
try:
return text.get("sel.first", "sel.last")
except TclError:
return None

def _setup(text):
"""Return the new or existing singleton SearchDialog instance.

Expand All @@ -32,7 +43,7 @@ def find(text):
used as the search phrase; otherwise, the previous entry
is used. No search is done with this command.
"""
pat = text.get("sel.first", "sel.last")
pat = _get_selected_text(text)
return _setup(text).open(text, pat) # Open is inherited from SDBase.

def find_again(text):
Expand Down Expand Up @@ -126,7 +137,7 @@ def find_selection(self, text):
selected text. If the selected text isn't changed, then use
the prior search phrase.
"""
pat = text.get("sel.first", "sel.last")
pat = _get_selected_text(text)
if pat:
self.engine.setcookedpat(pat)
return self.find_again(text)
Expand Down
61 changes: 50 additions & 11 deletions Lib/idlelib/textview.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
from tkinter.ttk import Frame, Scrollbar, Button
from tkinter.messagebox import showerror

from idlelib import search
from idlelib.colorizer import color_config
from idlelib.config import idleConf


class AutoHideScrollbar(Scrollbar):
Expand Down Expand Up @@ -70,44 +72,83 @@ def __init__(self, master, wrap=NONE, **kwargs):


class ViewFrame(Frame):
"Display TextFrame and Close button."
"""Display a TextFrame and "Close" and "Search" buttons."""
def __init__(self, parent, contents, wrap='word'):
"""Create a frame for viewing text with a "Close" button.
"""Create a frame for viewing text.

parent - parent widget for this frame
contents - text to display
wrap - type of text wrapping to use ('word', 'char' or 'none')

The Text widget is accessible via the 'text' attribute.
The frame has "Close" and "Search" buttons.
"""
super().__init__(parent)
self.parent = parent
self.bind('<Return>', self.ok)
self.bind('<Escape>', self.ok)
self.textframe = ScrollableTextFrame(self, relief=SUNKEN, height=700)
self.textframe = ScrollableTextFrame(self, relief=SUNKEN, height=900)

text = self.text = self.textframe.text
text.insert('1.0', contents)
text.configure(wrap=wrap, highlightthickness=0, state='disabled')
color_config(text)
text.focus_set()

buttons = Frame(self, padding=2)
self.button_ok = button_ok = Button(
self, text='Close', command=self.ok, takefocus=False)
buttons, text='Close', command=self.ok, takefocus=False,
)
self.textframe.pack(side='top', expand=True, fill='both')
button_ok.pack(side='bottom')

def ok(self, event=None):
button_ok.pack(side='left', padx=5)

self.button_search = button_search = Button(
buttons, text='Search',
command=lambda: self.find_event(None),
takefocus=False,
)
button_search.pack(side='left', padx=5)

keydefs = idleConf.GetCurrentKeySet()
for pseudoevent, handler in [
('<<find>>', self.find_event),
('<<find-again>>', self.find_again_event),
('<<find-selection>>', self.find_selection_event),
]:
keylist = keydefs[pseudoevent]
if keylist:
text.event_add(pseudoevent, *keylist)
text.bind(pseudoevent, handler)

buttons.pack(side='bottom')

def ok(self):
"""Dismiss text viewer dialog."""
self.parent.destroy()
return "break"

def find_event(self, event):
"""Open the search dialog."""
search.find(self.text)
return "break"

def find_again_event(self, event):
"""Search for the previously searched pattern again."""
search.find_again(self.text)
return "break"

def find_selection_event(self, event):
"""Search for the currently selected text."""
search.find_selection(self.text)
return "break"


class ViewWindow(Toplevel):
"A simple text viewer dialog for IDLE."

def __init__(self, parent, title, contents, modal=True, wrap=WORD,
*, _htest=False, _utest=False):
"""Show the given text in a scrollable window with a 'close' button.
"""Show the given text in a scrollable window.

If modal is left True, users cannot interact with other windows
until the textview window is closed.
Expand All @@ -124,13 +165,11 @@ def __init__(self, parent, title, contents, modal=True, wrap=WORD,
# Place dialog below parent if running htest.
x = parent.winfo_rootx() + 10
y = parent.winfo_rooty() + (10 if not _htest else 100)
self.geometry(f'=750x500+{x}+{y}')
self.geometry(f'=800x600+{x}+{y}')

self.title(title)
self.viewframe = ViewFrame(self, contents, wrap=wrap)
self.protocol("WM_DELETE_WINDOW", self.ok)
self.button_ok = button_ok = Button(self, text='Close',
command=self.ok, takefocus=False)
self.viewframe.pack(side='top', expand=True, fill='both')

self.is_modal = modal
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add search to the "Squeezed Output Viewer".