Package node_manager_fkie :: Package editor :: Module text_search_frame
[frames] | no frames]

Source Code for Module node_manager_fkie.editor.text_search_frame

  1  # Software License Agreement (BSD License) 
  2  # 
  3  # Copyright (c) 2012, Fraunhofer FKIE/US, Alexander Tiderko 
  4  # All rights reserved. 
  5  # 
  6  # Redistribution and use in source and binary forms, with or without 
  7  # modification, are permitted provided that the following conditions 
  8  # are met: 
  9  # 
 10  #  * Redistributions of source code must retain the above copyright 
 11  #    notice, this list of conditions and the following disclaimer. 
 12  #  * Redistributions in binary form must reproduce the above 
 13  #    copyright notice, this list of conditions and the following 
 14  #    disclaimer in the documentation and/or other materials provided 
 15  #    with the distribution. 
 16  #  * Neither the name of Fraunhofer nor the names of its 
 17  #    contributors may be used to endorse or promote products derived 
 18  #    from this software without specific prior written permission. 
 19  # 
 20  # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
 21  # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
 22  # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 
 23  # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 
 24  # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 25  # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
 26  # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
 27  # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
 28  # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 
 29  # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 30  # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
 31  # POSSIBILITY OF SUCH DAMAGE. 
 32   
 33  from python_qt_binding.QtCore import Signal, Qt 
 34  import os 
 35  import rospy 
 36   
 37  from node_manager_fkie.common import package_name 
 38   
 39  from .line_edit import EnchancedLineEdit 
 40  from .text_search_thread import TextSearchThread 
 41   
 42   
 43  try: 
 44      from python_qt_binding.QtGui import QCheckBox, QFrame, QLabel, QTreeWidget, QTreeWidgetItem, QPushButton, QGroupBox, QDockWidget 
 45      from python_qt_binding.QtGui import QHBoxLayout, QVBoxLayout, QSpacerItem, QSplitter, QSizePolicy 
 46  except: 
 47      from python_qt_binding.QtWidgets import QCheckBox, QFrame, QLabel, QTreeWidget, QTreeWidgetItem, QPushButton, QGroupBox, QDockWidget 
 48      from python_qt_binding.QtWidgets import QHBoxLayout, QVBoxLayout, QSpacerItem, QSplitter, QSizePolicy 
 49   
 50   
51 -class TextSearchFrame(QDockWidget):
52 ''' 53 A frame to find text in the Editor. 54 ''' 55 search_result_signal = Signal(str, bool, str, int) 56 ''' @ivar: A signal emitted after search_threaded was started. 57 (search text, found or not, file, position in text) 58 for each result a signal will be emitted. 59 ''' 60 replace_signal = Signal(str, str, int, str) 61 ''' @ivar: A signal emitted to replace string at given position. 62 (search text, file, position in text, replaced by text) 63 ''' 64
65 - def __init__(self, tabwidget, parent=None):
66 QDockWidget.__init__(self, "Find", parent) 67 self.setObjectName('SearchFrame') 68 self.setFeatures(QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetFloatable) 69 self._dockwidget = QFrame(self) 70 self.vbox_layout = QVBoxLayout(self._dockwidget) 71 self.layout().setContentsMargins(0, 0, 0, 0) 72 self.layout().setSpacing(1) 73 # frame with two rows for find and replace 74 find_replace_frame = QFrame(self) 75 find_replace_vbox_layout = QVBoxLayout(find_replace_frame) 76 find_replace_vbox_layout.setContentsMargins(0, 0, 0, 0) 77 find_replace_vbox_layout.setSpacing(1) 78 # find_replace_vbox_layout.addSpacerItem(QSpacerItem(1, 1, QSizePolicy.Expanding, QSizePolicy.Expanding)) 79 # create frame with find row 80 find_frame = self._create_find_frame() 81 find_replace_vbox_layout.addWidget(find_frame) 82 rplc_frame = self._create_replace_frame() 83 find_replace_vbox_layout.addWidget(rplc_frame) 84 # frame for find&replace and search results 85 self.vbox_layout.addWidget(find_replace_frame) 86 self.vbox_layout.addWidget(self._create_found_frame()) 87 # self.vbox_layout.addStretch(2024) 88 self.setWidget(self._dockwidget) 89 # intern search parameters 90 self._tabwidget = tabwidget 91 self.current_search_text = '' 92 self.search_results = [] 93 self.search_results_fileset = set() 94 self._search_result_index = -1 95 self._search_recursive = False 96 self._search_thread = None
97
98 - def _create_find_frame(self):
99 find_frame = QFrame(self) 100 find_hbox_layout = QHBoxLayout(find_frame) 101 find_hbox_layout.setContentsMargins(0, 0, 0, 0) 102 find_hbox_layout.setSpacing(1) 103 self.search_field = EnchancedLineEdit(find_frame) 104 self.search_field.setPlaceholderText('search text') 105 self.search_field.textChanged.connect(self.on_search_text_changed) 106 self.search_field.returnPressed.connect(self.on_search) 107 find_hbox_layout.addWidget(self.search_field) 108 self.search_result_label = QLabel(find_frame) 109 self.search_result_label.setText(' ') 110 find_hbox_layout.addWidget(self.search_result_label) 111 self.find_button_back = QPushButton("<") 112 self.find_button_back.setFixedWidth(44) 113 self.find_button_back.clicked.connect(self.on_search_back) 114 find_hbox_layout.addWidget(self.find_button_back) 115 self.find_button = QPushButton(">") 116 self.find_button.setDefault(True) 117 # self.find_button.setFlat(True) 118 self.find_button.setFixedWidth(44) 119 self.find_button.clicked.connect(self.on_search) 120 find_hbox_layout.addWidget(self.find_button) 121 return find_frame
122
123 - def _create_replace_frame(self):
124 # create frame with replace row 125 self.rplc_frame = rplc_frame = QFrame(self) 126 rplc_hbox_layout = QHBoxLayout(rplc_frame) 127 rplc_hbox_layout.setContentsMargins(0, 0, 0, 0) 128 rplc_hbox_layout.setSpacing(1) 129 self.replace_field = EnchancedLineEdit(rplc_frame) 130 self.replace_field.setPlaceholderText('replace text') 131 self.replace_field.returnPressed.connect(self.on_replace) 132 rplc_hbox_layout.addWidget(self.replace_field) 133 self.replace_result_label = QLabel(rplc_frame) 134 self.replace_result_label.setText(' ') 135 rplc_hbox_layout.addWidget(self.replace_result_label) 136 self.replace_button = replace_button = QPushButton("> &Replace >") 137 replace_button.setFixedWidth(90) 138 replace_button.clicked.connect(self.on_replace_click) 139 rplc_hbox_layout.addWidget(replace_button) 140 rplc_frame.setVisible(False) 141 return rplc_frame
142
143 - def _create_found_frame(self):
144 ff_frame = QFrame(self) 145 self.found_files_vbox_layout = QVBoxLayout(ff_frame) 146 self.found_files_vbox_layout.setContentsMargins(0, 0, 0, 0) 147 self.recursive_search_box = QCheckBox("recursive search") 148 self.found_files_vbox_layout.addWidget(self.recursive_search_box) 149 self.found_files_list = QTreeWidget(ff_frame) 150 self.found_files_list.setColumnCount(1) 151 self.found_files_list.setFrameStyle(QFrame.StyledPanel) 152 self.found_files_list.setHeaderHidden(True) 153 self.found_files_list.itemActivated.connect(self.on_itemActivated) 154 self.found_files_list.setStyleSheet( 155 "QTreeWidget {" 156 "background-color:transparent;" 157 "}" 158 "QTreeWidget::item {" 159 "background-color:transparent;" 160 "}" 161 "QTreeWidget::item:selected {" 162 "background-color: darkgray;" 163 "}") 164 self.found_files_vbox_layout.addWidget(self.found_files_list) 165 self.recursive_search_box.setChecked(False) 166 return ff_frame
167
168 - def keyPressEvent(self, event):
169 ''' 170 Enable the shortcats for search and replace 171 ''' 172 self.parent().keyPressEvent(event)
173
174 - def on_search(self):
175 ''' 176 Initiate the new search or request a next search result. 177 ''' 178 if self.current_search_text != self.search_field.text() or self._search_recursive != self.recursive_search_box.isChecked(): 179 # clear current search results 180 self._reset() 181 self.current_search_text = self.search_field.text() 182 if self.current_search_text: 183 path_text = {} 184 self._wait_for_result = True 185 for i in range(self._tabwidget.count()): 186 path_text[self._tabwidget.widget(i).filename] = self._tabwidget.widget(i).document().toPlainText() 187 self._search_recursive = self.recursive_search_box.isChecked() 188 self._search_thread = TextSearchThread(self.current_search_text, self._tabwidget.currentWidget().filename, path_text=path_text, recursive=self._search_recursive) 189 self._search_thread.search_result_signal.connect(self.on_search_result) 190 self._search_thread.warning_signal.connect(self.on_warning_result) 191 self._search_thread.start() 192 elif self.search_results: 193 self._check_position() 194 if self.search_results: 195 if self._search_result_index + 1 >= len(self.search_results): 196 self._search_result_index = -1 197 self._search_result_index += 1 198 (id, search_text, found, path, index, linenr, line) = self.search_results[self._search_result_index] 199 self.search_result_signal.emit(search_text, found, path, index) 200 self.replace_button.setEnabled(True) 201 self._update_label()
202
203 - def on_search_back(self):
204 ''' 205 Slot to handle the search back function. 206 ''' 207 self._check_position(False) 208 if self.search_results: 209 self._search_result_index -= 1 210 if self._search_result_index < 0: 211 self._search_result_index = len(self.search_results) - 1 212 self._update_label() 213 (id, search_text, found, path, index, linenr, line) = self.search_results[self._search_result_index] 214 self.search_result_signal.emit(search_text, found, path, index) 215 self.replace_button.setEnabled(True)
216
217 - def _check_position(self, forward=True):
218 try: 219 # if the position of the textCursor was changed by the user, move the search index 220 cur_pos = self._tabwidget.currentWidget().textCursor().position() 221 id, st, _f, pa, idx, lnr, ltxt = self.search_results[self._search_result_index] 222 sear_pos = idx + len(st) 223 if cur_pos != sear_pos: 224 first_idx = self._get_current_index_for_current_file() 225 if first_idx != -1: 226 id, st, _f, pa, idx, lnr, ltxt = self.search_results[first_idx] 227 sear_pos = idx + len(st) 228 while cur_pos > sear_pos and self._tabwidget.currentWidget().filename == pa: 229 first_idx += 1 230 id, st, _f, pa, idx, lnr, ltxt = self.search_results[first_idx] 231 sear_pos = idx + len(st) 232 self._search_result_index = first_idx 233 if forward: 234 self._search_result_index -= 1 235 else: 236 self._reset(True) 237 except: 238 pass
239
241 for index in range(len(self.search_results)): 242 id, _st, _f, pa, _idx = self.search_results[index] 243 if self._tabwidget.currentWidget().filename == pa: 244 return index 245 return -1
246
247 - def on_search_result(self, search_text, found, path, index, linenr, line):
248 ''' 249 Slot to handle the signals for search result. This signals are forwarded used 250 search_result_signal. 251 ''' 252 if found and search_text == self.current_search_text: 253 id = "%d:%s" % (index, path) 254 self.search_results_fileset.add(path) 255 item = (search_text, found, path, index) 256 if item not in self.search_results: 257 self.search_results.append((id, search_text, found, path, index, linenr, line)) 258 if self._wait_for_result: 259 self._search_result_index += 1 260 if index >= self._tabwidget.currentWidget().textCursor().position() or self._tabwidget.currentWidget().filename != path: 261 self._wait_for_result = False 262 self.search_result_signal.emit(search_text, found, path, index) 263 self.replace_button.setEnabled(True) 264 pkg, rpath = package_name(os.path.dirname(path)) 265 itemstr = '%s [%s]' % (os.path.basename(path), pkg) 266 if not self.found_files_list.findItems(itemstr, Qt.MatchExactly): 267 list_item = QTreeWidgetItem(self.found_files_list) 268 list_item.setText(0, itemstr) 269 list_item.setToolTip(0, path) 270 self.found_files_list.insertTopLevelItem(0, list_item) 271 self.found_files_list.expandAll() 272 for i in range(self.found_files_list.topLevelItemCount()): 273 top_item = self.found_files_list.topLevelItem(i) 274 if top_item.text(0) == itemstr: 275 sub_item_str = "%d: %s" % (linenr, line) 276 list_item2 = QTreeWidgetItem() 277 list_item2.setText(0, sub_item_str) 278 list_item2.setWhatsThis(0, id) 279 top_item.addChild(list_item2) 280 #self.found_files_list.setVisible(len(self.search_results_fileset) > 0) 281 self._update_label()
282
283 - def on_warning_result(self, text):
284 rospy.logwarn(text)
285
286 - def on_replace_click(self):
287 self.on_replace() 288 self.on_search()
289
290 - def on_replace(self):
291 ''' 292 Emits the replace signal, but only if currently selected text is equal to the searched one. 293 ''' 294 if self.search_results: 295 try: 296 id, search_text, _found, path, index, linenr, line_text = self.search_results[self._search_result_index] 297 cursor = self._tabwidget.currentWidget().textCursor() 298 if cursor.selectedText() == search_text: 299 rptxt = self.replace_field.text() 300 for rindex in range(self._search_result_index + 1, len(self.search_results)): 301 iid, st, _f, pa, idx, lnr, ltxt = self.search_results[rindex] 302 if path == pa: 303 self.search_results.pop(rindex) 304 self.search_results.insert(rindex, (iid, st, _f, pa, idx + len(rptxt) - len(st), lnr, ltxt)) 305 else: 306 break 307 self._remove_search_result(self._search_result_index) 308 self._search_result_index -= 1 309 self.replace_signal.emit(search_text, path, index, rptxt) 310 else: 311 self.replace_button.setEnabled(False) 312 except: 313 import traceback 314 print traceback.format_exc() 315 pass
316
317 - def on_itemActivated(self, item):
318 ''' 319 Go to the results for the selected file entry in the list. 320 ''' 321 splits = item.whatsThis(0).split(':') 322 if len(splits) == 2: 323 item_index = int(splits[0]) 324 item_path = splits[1] 325 new_search_index = -1 326 tmp_index = -1 327 search_index = -1 328 tmp_search_text = '' 329 for id, search_text, found, path, index, linenr, line_text in self.search_results: 330 new_search_index += 1 331 if item_path == path and item_index == index: 332 self._search_result_index = new_search_index 333 self.search_result_signal.emit(search_text, found, path, index) 334 self._update_label()
335
336 - def on_search_text_changed(self, _text):
337 ''' 338 Clear search result if the text was changed. 339 ''' 340 self._reset()
341
342 - def _update_label(self, clear_label=False):
343 ''' 344 Updates the status label for search results. The info is created from search result lists. 345 ''' 346 msg = ' ' 347 if self.search_results: 348 count_files = len(self.search_results_fileset) 349 msg = '%d/%d' % (self._search_result_index + 1, len(self.search_results)) 350 if count_files > 1: 351 msg = '%s(%d)' % (msg, count_files) 352 if self._search_thread is not None and self._search_thread.is_alive(): 353 msg = 'searching..%s' % msg 354 elif not msg.strip() and self.current_search_text: 355 msg = '0 found' 356 self.current_search_text = '' 357 if clear_label: 358 msg = ' ' 359 self.search_result_label.setText(msg) 360 self.find_button_back.setEnabled(len(self.search_results)) 361 self._select_current_item_in_box(self._search_result_index)
362
363 - def file_changed(self, path):
364 ''' 365 Clears search results if for changed file are some search results are available 366 :param path: changed file path 367 :type path: str 368 ''' 369 if path in self.search_results_fileset: 370 self._reset()
371
372 - def set_replace_visible(self, value):
373 self.rplc_frame.setVisible(value) 374 self.raise_() 375 self.activateWindow() 376 if value: 377 self.replace_field.setFocus() 378 self.replace_field.selectAll() 379 self.setWindowTitle("Find / Replace") 380 else: 381 self.setWindowTitle("Find") 382 self.search_field.setFocus()
383
384 - def is_replace_visible(self):
385 return self.rplc_frame.isVisible()
386
387 - def _reset(self, force_new_search=False):
388 # clear current search results 389 if self._search_thread is not None: 390 self._search_thread.search_result_signal.disconnect() 391 self._search_thread.stop() 392 self._search_thread = None 393 self.current_search_text = '' 394 self.search_results = [] 395 self.search_results_fileset = set() 396 self.found_files_list.clear() 397 # self.found_files_list.setVisible(False) 398 self._update_label(True) 399 self._search_result_index = -1 400 self.find_button_back.setEnabled(False) 401 if force_new_search: 402 self.on_search()
403
404 - def enable(self):
405 self.setVisible(True) 406 # self.show() 407 self.raise_() 408 self.activateWindow() 409 self.search_field.setFocus() 410 self.search_field.selectAll()
411
412 - def _select_current_item_in_box(self, index):
413 try: 414 (id, search_text, found, path, index, linenr, line) = self.search_results[index] 415 for topidx in range(self.found_files_list.topLevelItemCount()): 416 topitem = self.found_files_list.topLevelItem(topidx) 417 for childdx in range(topitem.childCount()): 418 child = topitem.child(childdx) 419 if child.whatsThis(0) == id: 420 child.setSelected(True) 421 elif child.isSelected(): 422 child.setSelected(False) 423 except: 424 pass
425
426 - def _remove_search_result(self, index):
427 try: 428 (id, search_text, found, path, index, linenr, line) = self.search_results.pop(index) 429 pkg, rpath = package_name(os.path.dirname(path)) 430 itemstr = '%s [%s]' % (os.path.basename(path), pkg) 431 found_items = self.found_files_list.findItems(itemstr, Qt.MatchExactly) 432 for item in found_items: 433 for chi in range(item.childCount()): 434 child = item.child(chi) 435 if child.whatsThis(0) == id: 436 item.removeChild(child) 437 break 438 # delete top level item if it is now empty 439 for topidx in range(self.found_files_list.topLevelItemCount()): 440 if self.found_files_list.topLevelItem(topidx).childCount() == 0: 441 self.found_files_list.takeTopLevelItem(topidx) 442 break 443 # create new set with files contain the search text 444 new_path_set = set(path for _id, _st, _fd, path, _idx, lnr, lntxt in self.search_results) 445 self.search_results_fileset = new_path_set 446 # self.found_files_list.setVisible(len(self.search_results_fileset) > 0) 447 except: 448 import traceback 449 print traceback.format_exc()
450