Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PR: Find in Files style and UI improvements #4537

Merged
merged 4 commits into from
Jun 3, 2017
Merged
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
2 changes: 1 addition & 1 deletion spyder/utils/external/binaryornot/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def is_binary(filename):
logger.debug('is_binary: %(filename)r', locals())

# Check if the file extension is in a list of known binary types
binary_extensions = ['pyc', 'iso']
binary_extensions = ['pyc', 'iso', 'zip', 'pdf']
for ext in binary_extensions:
if filename.endswith(ext):
return True
Expand Down
228 changes: 167 additions & 61 deletions spyder/widgets/findinfiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@

# Third party imports
from qtpy.compat import getexistingdirectory
from qtpy.QtCore import QMutex, QMutexLocker, Qt, QThread, Signal, Slot
from qtpy.QtGui import QAbstractTextDocumentLayout, QTextDocument
from qtpy.QtCore import QMutex, QMutexLocker, Qt, QThread, Signal, Slot, QSize
from qtpy.QtWidgets import (QHBoxLayout, QLabel, QRadioButton, QSizePolicy,
QTreeWidgetItem, QVBoxLayout, QWidget,
QHeaderView)
QStyledItemDelegate, QStyleOptionViewItem,
QApplication, QStyle)

# Local imports
from spyder.config.base import _
Expand All @@ -37,6 +39,7 @@
from spyder.widgets.comboboxes import PatternComboBox
from spyder.widgets.onecolumntree import OneColumnTree

from spyder.config.gui import get_font
from spyder.widgets.waitingspinner import QWaitingSpinner


Expand All @@ -45,7 +48,7 @@ class SearchThread(QThread):
sig_finished = Signal(bool)
sig_current_file = Signal(str)
sig_current_folder = Signal(str)
sig_file_match = Signal(dict, int)
sig_file_match = Signal(tuple, int)
sig_out_print = Signal(object)

def __init__(self, parent):
Expand Down Expand Up @@ -125,20 +128,48 @@ def find_files_in_path(self, path):

def truncate_result(self, line, start, end):
ellipsis = '...'
before_offset = 4
if len(line) > 80:
if start <= before_offset:
before_offset = 0
ellipsis = ''
trunc_line = ellipsis + line[start - before_offset:end + 4]
else:
trunc_line = line

left, match, right = line[:start], line[start:end], line[end:]
max_line_length = 40
offset = (len(line) - len(match)) // 2

left = left.split(' ')
num_left_words = len(left)

if num_left_words == 1:
left = left[0]
if len(left) > max_line_length:
left = ellipsis + left[-offset:]
left = [left]

right = right.split(' ')
num_right_words = len(right)

if num_right_words == 1:
right = right[0]
if len(right) > max_line_length:
right = right[:offset] + ellipsis
right = [right]

left = left[-3:]
right = right[:3]

if len(left) < num_left_words:
left = [ellipsis] + left

if len(right) < num_right_words:
right = right + [ellipsis]

left = ' '.join(left)
right = ' '.join(right)

trunc_line = '{0}<b>{1}</b>{2}'.format(left, match, right)
return trunc_line

def find_string_in_file(self, fname):
self.error_flag = False
self.sig_current_file.emit(fname)
results = {}
# results = {}
try:
for lineno, line in enumerate(open(fname, 'rb')):
for text, enc in self.texts:
Expand All @@ -156,28 +187,30 @@ def find_string_in_file(self, fname):
line_dec = line
if self.text_re:
for match in re.finditer(text, line):
res = results.get(osp.abspath(fname), [])
displ_line = self.truncate_result(line_dec,
match.start(),
match.end())
res.append((lineno + 1, match.start(), displ_line))
results[osp.abspath(fname)] = res
self.total_matches += 1
self.sig_file_match.emit((osp.abspath(fname),
lineno + 1,
match.start(), displ_line),
self.total_matches)
else:
found = line.find(text)
while found > -1:
res = results.get(osp.abspath(fname), [])
self.total_matches += 1
displ_line = self.truncate_result(line_dec,
found,
found + len(text))
res.append((lineno + 1, found, displ_line))
results[osp.abspath(fname)] = res

self.sig_file_match.emit((osp.abspath(fname),
lineno + 1,
found, displ_line),
self.total_matches)
for text, enc in self.texts:
found = line.find(text, found + 1)
if found > -1:
break
self.total_matches += 1
if len(results) > 0:
self.sig_file_match.emit(results, self.total_matches)
except IOError as xxx_todo_changeme:
(_errno, _strerror) = xxx_todo_changeme.args
self.error_flag = _("permission denied errors were encountered")
Expand Down Expand Up @@ -230,13 +263,13 @@ def __init__(self, parent, search_text, search_text_regexp, search_path,
self.more_options.setChecked(more_options)

self.ok_button = create_toolbutton(self, text=_("Search"),
icon=ima.icon('DialogApplyButton'),
icon=ima.icon('find'),
triggered=lambda: self.find.emit(),
tip=_("Start search"),
text_beside_icon=True)
self.ok_button.clicked.connect(self.update_combos)
self.stop_button = create_toolbutton(self, text=_("Stop"),
icon=ima.icon('stop'),
icon=ima.icon('editclear'),
triggered=lambda:
self.stop.emit(),
tip=_("Stop search"),
Expand Down Expand Up @@ -266,28 +299,29 @@ def __init__(self, parent, search_text, search_text_regexp, search_path,

# Layout 3
hlayout3 = QHBoxLayout()
search_label = QLabel(_("Search on: "))

self.global_path_search = QRadioButton(_("Current Path"), self)
self.global_path_search = QRadioButton(_("Path"), self)
self.global_path_search.setChecked(True)
self.global_path_search.setToolTip(_("Search in all files and "
"directories present on the"
"current Spyder path"))

self.project_search = QRadioButton(_("Current Project"), self)
self.project_search = QRadioButton(_("Project"), self)
self.project_search.setToolTip(_("Search in all files and "
"directories present on the"
"current project path (If opened)"))

self.project_search.setEnabled(False)

self.file_search = QRadioButton(_("Current File"), self)
self.file_search = QRadioButton(_("File"), self)
self.file_search.setToolTip(_("Search in current opened file"))

for wid in [search_label, self.global_path_search,
for wid in [self.global_path_search,
self.project_search, self.file_search]:
hlayout3.addWidget(wid)

hlayout3.addStretch(1)

self.search_text.valid.connect(lambda valid: self.find.emit())
self.exclude_pattern.valid.connect(lambda valid: self.find.emit())

Expand Down Expand Up @@ -429,9 +463,92 @@ def keyPressEvent(self, event):
QWidget.keyPressEvent(self, event)


class ResultsHeader(QHeaderView):
class LineMatchItem(QTreeWidgetItem):
def __init__(self, parent, lineno, colno, match):
self.lineno = lineno
self.colno = colno
self.match = match
QTreeWidgetItem.__init__(self, parent, [self.__repr__()],
QTreeWidgetItem.Type)

def __repr__(self):
match = to_text_string(self.match).rstrip()
font = get_font()
_str = to_text_string("<b>{1}</b> ({2}): "
"<span style='font-family:{0};"
"font-size:75%;'>{3}</span>")
return _str.format(font.family(), self.lineno, self.colno, match)

def __unicode__(self):
return self.__repr__()

def __str__(self):
return self.__repr__()

def __lt__(self, x):
return self.lineno < x.lineno

def __ge__(self, x):
return self.lineno >= x.lineno


class FileMatchItem(QTreeWidgetItem):
def __init__(self, parent, filename):

self.filename = osp.basename(filename)

title = ('<b>{0}</b><br>'
'<small><em>{1}</em>'
'</small>'.format(osp.basename(filename),
osp.dirname(filename)))
QTreeWidgetItem.__init__(self, parent, [title], QTreeWidgetItem.Type)

self.setToolTip(0, filename)

def __lt__(self, x):
return self.filename < x.filename

def __ge__(self, x):
return self.filename >= x.filename


class ItemDelegate(QStyledItemDelegate):
def __init__(self, parent):
QHeaderView.__init__(self, parent)
QStyledItemDelegate.__init__(self, parent)

def paint(self, painter, option, index):
options = QStyleOptionViewItem(option)
self.initStyleOption(options, index)

style = (QApplication.style() if options.widget is None
else options.widget.style())

doc = QTextDocument()
doc.setDocumentMargin(0)
doc.setHtml(options.text)

options.text = ""
style.drawControl(QStyle.CE_ItemViewItem, options, painter)

ctx = QAbstractTextDocumentLayout.PaintContext()

textRect = style.subElementRect(QStyle.SE_ItemViewItemText, options)
painter.save()

painter.translate(textRect.topLeft())
painter.setClipRect(textRect.translated(-textRect.topLeft()))
doc.documentLayout().draw(painter, ctx)
painter.restore()

def sizeHint(self, option, index):
options = QStyleOptionViewItem(option)
self.initStyleOption(options, index)

doc = QTextDocument()
doc.setHtml(options.text)
doc.setTextWidth(options.rect.width())

return QSize(doc.idealWidth(), doc.size().height())


class ResultsBrowser(OneColumnTree):
Expand All @@ -443,11 +560,14 @@ def __init__(self, parent):
self.error_flag = None
self.completed = None
self.data = None
self.files = None
self.set_title('')
self.root_items = None
self.sortByColumn(0, Qt.AscendingOrder)
self.setSortingEnabled(True)
self.header().setSectionsClickable(True)
# self.setHeaderLabel(_("Filename"))
self.setItemDelegate(ItemDelegate(self))
self.setUniformRowHeights(False)

def activated(self, item):
"""Double-click event"""
Expand All @@ -464,15 +584,22 @@ def clear_title(self, search_text):
self.clear()
self.num_files = 0
self.data = {}
self.files = {}
self.search_text = search_text
title = "'%s' - " % search_text
text = _('String not found')
self.set_title(title + text)

@Slot(dict, int)
@Slot(tuple, int)
def append_result(self, results, num_matches):
"""Real-time update of search results"""
self.num_files += 1
filename, lineno, colno, line = results

if filename not in self.files:
item = FileMatchItem(self, filename)
self.files[filename] = item
self.num_files += 1

search_text = self.search_text
title = "'%s' - " % search_text
nb_files = self.num_files
Expand All @@ -486,21 +613,11 @@ def append_result(self, results, num_matches):
text = "%d %s %d %s" % (num_matches, text_matches,
nb_files, text_files)
self.set_title(title + text)
for filename in sorted(results.keys()):
file_item = QTreeWidgetItem(self, [osp.basename(filename) +
u" - " + osp.dirname(filename)],
QTreeWidgetItem.Type)
file_item.setToolTip(0, filename)
file_item.setIcon(0, get_filetype_icon(filename))
for lineno, colno, line in results[filename]:
item = QTreeWidgetItem(file_item,
[u"{0} ({1}): {2}".format(lineno,
colno,
line.rstrip()
)],
QTreeWidgetItem.Type)
item.setIcon(0, ima.icon('arrow'))
self.data[id(item)] = (filename, lineno, colno)

file_item = self.files[filename]
item = LineMatchItem(file_item, lineno, colno, line)

self.data[id(item)] = (filename, lineno, colno)


class FileProgressBar(QWidget):
Expand Down Expand Up @@ -573,21 +690,9 @@ def __init__(self, parent,

self.result_browser = ResultsBrowser(self)

collapse_btn = create_toolbutton(self)
collapse_btn.setDefaultAction(self.result_browser.collapse_all_action)
expand_btn = create_toolbutton(self)
expand_btn.setDefaultAction(self.result_browser.expand_all_action)
restore_btn = create_toolbutton(self)
restore_btn.setDefaultAction(self.result_browser.restore_action)

btn_layout = QVBoxLayout()
btn_layout.setAlignment(Qt.AlignTop)
for widget in [collapse_btn, expand_btn, restore_btn]:
btn_layout.addWidget(widget)

hlayout = QHBoxLayout()
hlayout.addWidget(self.result_browser)
hlayout.addLayout(btn_layout)
# hlayout.addLayout(btn_layout)

layout = QVBoxLayout()
left, _x, right, bottom = layout.getContentsMargins()
Expand Down Expand Up @@ -653,6 +758,7 @@ def search_complete(self, completed):
self.find_options.ok_button.setEnabled(True)
self.find_options.stop_button.setEnabled(False)
self.status_bar.hide()
self.result_browser.expandAll()
if self.search_thread is None:
return
self.sig_finished.emit()
Expand Down