import re
import tkinter as tk
from tkinter import messagebox
from idlelib.config import idleConf
from idlelib.textview import view_text
from idlelib.tooltip import Hovertip
from idlelib import macosx
def count_lines_with_wrapping(s, linewidth=80):
tabwidth = 8 pos = 0
linecount = 1
current_column = 0
for m in re.finditer(r"[\t\n]", s):
numchars = m.start() - pos
pos += numchars
current_column += numchars
if s[pos] == '\n':
if current_column > linewidth:
linecount += (current_column - 1) // linewidth
linecount += 1
current_column = 0
else:
assert s[pos] == '\t'
current_column += tabwidth - (current_column % tabwidth)
if current_column > linewidth:
linecount += 1
current_column = tabwidth
pos += 1
current_column += len(s) - pos
if current_column > 0:
linecount += (current_column - 1) // linewidth
else:
linecount -= 1
return linecount
class ExpandingButton(tk.Button):
def __init__(self, s, tags, numoflines, squeezer):
self.s = s
self.tags = tags
self.numoflines = numoflines
self.squeezer = squeezer
self.editwin = editwin = squeezer.editwin
self.text = text = editwin.text
self.base_text = editwin.per.bottom
line_plurality = "lines" if numoflines != 1 else "line"
button_text = f"Squeezed text ({numoflines} {line_plurality})."
tk.Button.__init__(self, text, text=button_text,
background="#FFFFC0", activebackground="#FFFFE0")
button_tooltip_text = (
"Double-click to expand, right-click for more options."
)
Hovertip(self, button_tooltip_text, hover_delay=80)
self.bind("<Double-Button-1>", self.expand)
if macosx.isAquaTk():
self.bind("<Button-2>", self.context_menu_event)
else:
self.bind("<Button-3>", self.context_menu_event)
self.selection_handle( lambda offset, length: s[int(offset):int(offset) + int(length)])
self.is_dangerous = None
self.after_idle(self.set_is_dangerous)
def set_is_dangerous(self):
dangerous_line_len = 50 * self.text.winfo_width()
self.is_dangerous = (
self.numoflines > 1000 or
len(self.s) > 50000 or
any(
len(line_match.group(0)) >= dangerous_line_len
for line_match in re.finditer(r'[^\n]+', self.s)
)
)
def expand(self, event=None):
if self.is_dangerous is None:
self.set_is_dangerous()
if self.is_dangerous:
confirm = messagebox.askokcancel(
title="Expand huge output?",
message="\n\n".join([
"The squeezed output is very long: %d lines, %d chars.",
"Expanding it could make IDLE slow or unresponsive.",
"It is recommended to view or copy the output instead.",
"Really expand?"
]) % (self.numoflines, len(self.s)),
default=messagebox.CANCEL,
parent=self.text)
if not confirm:
return "break"
index = self.text.index(self)
self.base_text.insert(index, self.s, self.tags)
self.base_text.delete(self)
self.editwin.on_squeezed_expand(index, self.s, self.tags)
self.squeezer.expandingbuttons.remove(self)
def copy(self, event=None):
self.clipboard_clear()
self.clipboard_append(self.s)
def view(self, event=None):
view_text(self.text, "Squeezed Output Viewer", self.s,
modal=False, wrap='none')
rmenu_specs = (
('copy', 'copy'),
('view', 'view'),
)
def context_menu_event(self, event):
self.text.mark_set("insert", "@%d,%d" % (event.x, event.y))
rmenu = tk.Menu(self.text, tearoff=0)
for label, method_name in self.rmenu_specs:
rmenu.add_command(label=label, command=getattr(self, method_name))
rmenu.tk_popup(event.x_root, event.y_root)
return "break"
class Squeezer:
@classmethod
def reload(cls):
cls.auto_squeeze_min_lines = idleConf.GetOption(
"main", "PyShell", "auto-squeeze-min-lines",
type="int", default=50,
)
def __init__(self, editwin):
self.editwin = editwin
self.text = text = editwin.text
self.base_text = editwin.per.bottom
self.window_width_delta = 2 * (
int(text.cget('border')) +
int(text.cget('padx'))
)
self.expandingbuttons = []
def mywrite(s, tags=(), write=editwin.write):
if tags != "stdout":
return write(s, tags)
auto_squeeze_min_lines = self.auto_squeeze_min_lines
if len(s) < auto_squeeze_min_lines:
return write(s, tags)
numoflines = self.count_lines(s)
if numoflines < auto_squeeze_min_lines:
return write(s, tags)
expandingbutton = ExpandingButton(s, tags, numoflines, self)
text.mark_gravity("iomark", tk.RIGHT)
text.window_create("iomark", window=expandingbutton,
padx=3, pady=5)
text.see("iomark")
text.update()
text.mark_gravity("iomark", tk.LEFT)
self.expandingbuttons.append(expandingbutton)
editwin.write = mywrite
def count_lines(self, s):
return count_lines_with_wrapping(s, self.editwin.width)
def squeeze_current_text(self):
tag_names = self.text.tag_names(tk.INSERT)
for tag_name in ("stdout", "stderr"):
if tag_name in tag_names:
break
else:
self.text.bell()
return "break"
start, end = self.text.tag_prevrange(tag_name, tk.INSERT + "+1c")
s = self.text.get(start, end)
if len(s) > 0 and s[-1] == '\n':
end = self.text.index("%s-1c" % end)
s = s[:-1]
self.base_text.delete(start, end)
numoflines = self.count_lines(s)
expandingbutton = ExpandingButton(s, tag_name, numoflines, self)
self.text.window_create(start, window=expandingbutton,
padx=3, pady=5)
i = len(self.expandingbuttons)
while i > 0 and self.text.compare(self.expandingbuttons[i-1],
">", expandingbutton):
i -= 1
self.expandingbuttons.insert(i, expandingbutton)
return "break"
Squeezer.reload()
if __name__ == "__main__":
from unittest import main
main('idlelib.idle_test.test_squeezer', verbosity=2, exit=False)