Robot Framework Integrated Development Environment (RIDE)
keywordsearch.py
Go to the documentation of this file.
1 # Copyright 2008-2015 Nokia Networks
2 # Copyright 2016- Robot Framework Foundation
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
7 #
8 # http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 
16 import os.path
17 from functools import (cmp_to_key)
18 
19 import wx
20 from wx import Colour
21 from wx.lib.mixins.listctrl import ListCtrlAutoWidthMixin
22 
23 from .. import utils
24 from ..controller.filecontrollers import ResourceFileController, TestCaseFileController
25 from ..pluginapi import Plugin
26 from ..action import ActionInfo
27 from ..publish.messages import RideOpenSuite, RideOpenResource, RideImportSetting, RideUserKeyword, RideNewProject
28 from ..usages.UsageRunner import Usages
29 from ..widgets import PopupMenuItem, ButtonWithHandler, Label, Font, HtmlWindow, ImageProvider, RIDEDialog
30 
31 ALL_KEYWORDS = '<all keywords>'
32 ALL_USER_KEYWORDS = '<all user keywords>'
33 ALL_LIBRARY_KEYWORDS = '<all library keywords>'
34 
35 
36 
38 
39  def __init__(self, app):
40  Plugin.__init__(self, app)
41  self.all_keywordsall_keywords = []
42  self._criteria_criteria = _SearchCriteria()
43  self.dirtydirty = False
44 
45  def enable(self):
46  action = ActionInfo('Tools', 'Search Keywords', self.OnSearchOnSearch,
47  shortcut='F5',
48  doc='Search keywords from libraries and resources',
49  icon=ImageProvider().KW_SEARCH_ICON,
50  position=51)
51  self.register_actionregister_action(action)
52  self.register_search_actionregister_search_action('Search Keywords', self.show_search_forshow_search_for, ImageProvider().KW_SEARCH_ICON)
53  self.subscribesubscribe(self.mark_dirtymark_dirty, RideOpenSuite, RideOpenResource,
54  RideImportSetting, RideUserKeyword, RideNewProject)
55  self._dialog_dialog = KeywordSearchDialog(self.frameframe, self)
56  self.treetree.register_context_menu_hook(self._search_resource_search_resource)
57 
58  def OnSearch(self, event):
59  self._dialog_dialog.show_search_with_criteria()
60 
61  def mark_dirty(self, message):
62  self.dirtydirty = True
63 
65  if not self.dirtydirty:
66  return False
67  self._update_update()
68  return True
69 
70  def _update(self):
71  self.dirtydirty = False
72  self.all_keywordsall_keywords = self.modelmodel.get_all_keywords()
73 
74  def search(self, pattern, search_docs, source_filter):
75  self._criteria_criteria = _SearchCriteria(pattern, search_docs, source_filter)
76  return self._search_search()
77 
78  def _search(self):
79  return [ kw for kw in self.all_keywordsall_keywords if self._criteria_criteria.matches(kw) ]
80 
81  def _search_resource(self, item):
82  if isinstance(item, (TestCaseFileController, ResourceFileController)):
83  callable = lambda x: self._show_resource_show_resource(os.path.basename(item.source))
84  return [PopupMenuItem('Search Keywords', callable=callable)]
85  return []
86 
87  def _show_resource(self, resource):
88  self._dialog_dialog.show_search_with_criteria(source=resource)
89 
90  def show_search_for(self, pattern):
91  self._dialog_dialog.show_search_with_criteria(pattern=pattern)
92 
93  def disable(self):
94  self.unregister_actionsunregister_actions()
95  self.unsubscribe_allunsubscribe_all()
96 
98 
99  def __init__(self, pattern='', search_docs=True, source_filter=ALL_KEYWORDS):
100  self._pattern_pattern = pattern
101  self._search_docs_search_docs = search_docs
102  self._source_filter_source_filter = source_filter
103 
104  def matches(self, kw):
105  if not self._matches_source_filter_matches_source_filter(kw):
106  return False
107  if self._contains_contains(kw.name, self._pattern_pattern):
108  return True
109  return self._search_docs_search_docs and self._contains_contains(kw.doc, self._pattern_pattern)
110 
111  def _matches_source_filter(self, kw):
112  if self._source_filter_source_filter == ALL_KEYWORDS:
113  return True
114  if self._source_filter_source_filter == ALL_USER_KEYWORDS and kw.is_user_keyword():
115  return True
116  if self._source_filter_source_filter == ALL_LIBRARY_KEYWORDS and kw.is_library_keyword():
117  return True
118  return self._source_filter_source_filter == kw.source
119 
120  def _contains(self, string, pattern):
121  return utils.normalize(pattern) in utils.normalize(string)
122 
123 
124 class KeywordSearchDialog(RIDEDialog):
125 
126  def __init__(self, parent, searcher):
127  RIDEDialog.__init__(self, title="Search Keywords", parent=parent, size=(650, 400),
128  style=wx.DEFAULT_FRAME_STYLE | wx.FRAME_FLOAT_ON_PARENT)
129  # set Left to Right direction (while we don't have localization)
130  self.SetLayoutDirection(wx.Layout_LeftToRight)
131  self.SetBackgroundColour(Colour(self.color_background))
132  self.SetForegroundColour(Colour(self.color_foreground))
133  self._plugin_plugin = searcher
134  self._create_components_create_components()
135  self._make_bindings_make_bindings()
136  self._sort_order_sort_order = _SortOrder()
137  self._last_selected_kw_last_selected_kw = None
138  self.CenterOnParent()
139 
141  self.SetSizer(wx.BoxSizer(wx.VERTICAL))
142  self._add_search_control_add_search_control()
143  self._add_keyword_list_add_keyword_list()
144  self._add_keyword_details_add_keyword_details()
145  self.SetSize((700,500))
146 
148  line1 = self._horizontal_sizer_horizontal_sizer()
149  self._add_pattern_filter_add_pattern_filter(line1)
150  self._add_doc_filter_add_doc_filter(line1)
151  self.Sizer.Add(line1, 0, wx.ALL, 3)
152  line2 = self._horizontal_sizer_horizontal_sizer()
153  self._add_source_filter_add_source_filter(line2)
154  self.Sizer.Add(line2, 0, wx.ALL, 3)
155 
156  def _horizontal_sizer(self):
157  return wx.BoxSizer(wx.HORIZONTAL)
158 
159  def _add_pattern_filter(self, sizer):
160  sizer.Add(Label(self, label='Search term: '))
161  self._search_control_search_control = wx.SearchCtrl(self, size=(200,-1),
162  style=wx.TE_PROCESS_ENTER)
163  self._search_control_search_control.SetBackgroundColour(Colour(self.color_secondary_background))
164  self._search_control_search_control.SetForegroundColour(Colour(self.color_secondary_foreground))
165  sizer.Add(self._search_control_search_control)
166 
167  def _add_doc_filter(self, sizer):
168  self._use_doc_use_doc = wx.CheckBox(self, label='Search documentation')
169  self._use_doc_use_doc.SetValue(True)
170  sizer.Add(self._use_doc_use_doc)
171 
172  def _add_source_filter(self, sizer):
173  sizer.Add(Label(self, label='Source: '))
174  self._source_filter_source_filter = wx.ComboBox(self, value=ALL_KEYWORDS, size=(300, -1),
175  choices=self._get_sources_get_sources(), style=wx.CB_READONLY)
176  self._source_filter_source_filter.SetBackgroundColour(Colour(self.color_secondary_background))
177  self._source_filter_source_filter.SetForegroundColour(Colour(self.color_secondary_foreground))
178  sizer.Add(self._source_filter_source_filter)
179 
180  def _get_sources(self):
181  sources = []
182  for kw in self._plugin_plugin.all_keywords:
183  if kw.source not in sources:
184  sources.append(kw.source)
185  return [ALL_KEYWORDS, ALL_USER_KEYWORDS, ALL_LIBRARY_KEYWORDS] + sorted(sources)
186 
187  def _add_keyword_list(self):
188  self._list_list = _KeywordList(self, self._plugin_plugin)
189  self._list_list.SetSize(self.Size)
190  self._add_to_sizer_add_to_sizer(self._list_list)
191 
193  self._details_details = HtmlWindow(self)
194  self._add_to_sizer_add_to_sizer(self._details_details)
195  self._find_usages_button_find_usages_button = ButtonWithHandler(self, 'Find Usages')
196  self._find_usages_button_find_usages_button.SetBackgroundColour(Colour(self.color_secondary_background))
197  self._find_usages_button_find_usages_button.SetForegroundColour(Colour(self.color_secondary_foreground))
198  self.Sizer.Add(self._find_usages_button_find_usages_button, 0, wx.ALL, 3)
199 
200  def _add_to_sizer(self, component):
201  self.Sizer.Add(component, 1, wx.EXPAND | wx.ALL, 3)
202 
203  def OnFindUsages(self, event):
204  Usages(self._plugin_plugin.model, self._plugin_plugin.tree.highlight, self._last_selected_kw_last_selected_kw.name, self._last_selected_kw_last_selected_kw).show()
205 
206  def _make_bindings(self):
207  self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnItemSelectedOnItemSelected, self._list_list)
208  self.Bind(wx.EVT_SEARCHCTRL_SEARCH_BTN, self.OnSearchOnSearch,
209  self._search_control_search_control)
210  self.Bind(wx.EVT_TEXT_ENTER, self.OnSearchOnSearch, self._search_control_search_control)
211  self.Bind(wx.EVT_ACTIVATE, self.OnActivateOnActivate)
212  self.Bind(wx.EVT_CLOSE, self.OnCloseOnClose)
213  self.Bind(wx.EVT_CHECKBOX, self.OnUseDocChangeOnUseDocChange, self._use_doc_use_doc)
214  self.Bind(wx.EVT_COMBOBOX, self.OnSourceFilterChangeOnSourceFilterChange, self._source_filter_source_filter)
215  self.Bind(wx.EVT_LIST_COL_CLICK, self.OnColClickOnColClick)
216 
217  def OnColClick(self,event):
218  col = event.GetColumn()
219  if self._sort_order_sort_order.is_sortable_column(col):
220  self._sort_order_sort_order.sort(col)
221  self._populate_search_populate_search()
222  event.Skip()
223 
224  def OnActivate(self, event):
225  if self._plugin_plugin.have_keywords_changed():
226  self._update_sources_update_sources()
227  self._populate_search_populate_search()
228 
229  def OnUseDocChange(self, event):
230  self._populate_search_populate_search()
231 
232  def OnSearch(self, event):
233  self._sort_order_sort_order.searched(self._get_search_text_get_search_text())
234  self._populate_search_populate_search()
235 
236  def OnSourceFilterChange(self, event):
237  self._populate_search_populate_search()
238 
239  def OnKey(self, event):
240  # Needed for HtmlWindow callback
241  pass
242 
243  def OnItemSelected(self, event):
244  self._last_selected_kw = self._keywords[event.Index]
245  self._update_details()
246 
247  def _update_sources(self):
248  selection = self._source_filter_source_filter.GetValue()
249  self._source_filter_source_filter.Clear()
250  for source in self._get_sources_get_sources():
251  self._source_filter_source_filter.Append(source)
252  self._source_filter_source_filter.SetValue(selection)
253  if self._source_filter_source_filter.GetValue() != selection:
254  self._source_filter_source_filter.SetValue(ALL_KEYWORDS)
255 
256  def OnClose(self, event):
257  self.Hide()
258 
259  def _populate_search(self):
260  self._keywords_keywords = _KeywordData(self._plugin_plugin.search(*self._get_search_criteria_get_search_criteria()),
261  self._sort_order_sort_order, self._get_search_text_get_search_text())
262  self._update_keyword_selection_update_keyword_selection()
263  self._list_list.show_keywords(self._keywords_keywords, self._last_selected_kw_last_selected_kw)
264  self.Refresh()
265 
267  return self._get_search_text_get_search_text(), self._use_doc_use_doc.GetValue(), self._source_filter_source_filter.GetValue()
268 
269  def _get_search_text(self):
270  return self._search_control_search_control.GetValue().lower()
271 
273  if not self._last_selected_kw_last_selected_kw in self._keywords_keywords and self._keywords_keywords:
274  self._last_selected_kw_last_selected_kw = self._keywords_keywords[0]
275  self._update_details_update_details()
276 
277  def _update_details(self):
278  if self._last_selected_kw_last_selected_kw in self._keywords_keywords:
279  self._details_details.set_content(self._last_selected_kw_last_selected_kw.details)
280  self._find_usages_button_find_usages_button.Enable()
281  else:
282  self._details_details.clear()
283  self._find_usages_button_find_usages_button.Disable()
284 
285  def show_search_with_criteria(self, pattern='', search_docs=True, source=ALL_KEYWORDS):
286  self._update_widgets_update_widgets(pattern, search_docs, source)
287  self._populate_search_populate_search()
288  self._show_show()
289  self._search_control_search_control.SetFocus()
290 
291  def _update_widgets(self, pattern, search_docs, source):
292  self._search_control_search_control.SetValue(pattern)
293  self._use_doc_use_doc.SetValue(search_docs)
294  self._source_filter_source_filter.SetValue(source)
295 
296  def _show(self):
297  if not self.IsShown():
298  self.Show()
299  self.Raise()
300 
301 
302 class _SortOrder():
303 
304  def __init__(self):
305  self.sort_upsort_up = True
306  self.columncolumn = 0
307  self.default_orderdefault_order = False
308 
309  def searched(self, term):
310  self.__init____init__()
311  if term:
312  self.default_orderdefault_order = True
313 
314  def swap_direction(self):
315  self.sort_upsort_up = not self.sort_upsort_up
316 
317  def is_sortable_column(self, col):
318  return col < 2
319 
320  def sort(self, col):
321  if self._has_been_sorted_by_has_been_sorted_by(col):
322  self.swap_directionswap_direction()
323  else:
324  self.sort_upsort_up = True
325  self.columncolumn = col
326  self.default_orderdefault_order = False
327 
328  def _has_been_sorted_by(self, col):
329  return self.columncolumn == col and not self.default_orderdefault_order
330 
331 
332 class _KeywordData(list):
333  headers = ['Name', 'Source', 'Description']
334 
335  def __init__(self, keywords, sort_order, search_criteria=None):
336  self.extend(self._sort_sort(keywords, sort_order, search_criteria))
337 
338  def _sort(self, keywords, sort_order, search_criteria=None):
339  if sort_order.default_order:
340  return self._sort_by_search_sort_by_search(keywords, sort_order, search_criteria)
341  return self._sort_by_attr_sort_by_attr(keywords, sort_order)
342 
343  def _sort_by_search(self, keywords, sort_order, search_criteria):
344  search_criteria = search_criteria.lower()
345  starts_with = [kw for kw in keywords if kw.name.lower().startswith(search_criteria)]
346  name_contains = [kw for kw in keywords if (search_criteria in kw.name.lower()
347  and kw not in starts_with)]
348  doc_contains = [kw for kw in keywords if (search_criteria in kw.details.lower()
349  and kw not in starts_with
350  and kw not in name_contains)]
351  result = []
352  for to_sort in (starts_with, name_contains, doc_contains):
353  result.extend(self._sort_by_attr_sort_by_attr(to_sort, sort_order))
354  return result
355 
356  def _sort_by_attr(self, keywords, sort_order):
357  return sorted(keywords, key=cmp_to_key(self._get_comparator_for_get_comparator_for(
358  self.headersheaders[sort_order.column].lower())),
359  reverse=not sort_order.sort_up)
360 
361  @staticmethod
362  def m_cmp(a, b):
363  return (a > b) - (a < b)
364 
365  def _get_comparator_for(self, atrr_name):
366  return lambda kw, kw2: self.m_cmpm_cmp(self._value_lowerer_value_lowerer(kw, atrr_name),
367  self._value_lowerer_value_lowerer(kw2, atrr_name))
368 
369  def _value_lowerer(self, kw, attr_name):
370  return getattr(kw, attr_name).lower()
371 
372 
373 class _KeywordList(wx.ListCtrl, ListCtrlAutoWidthMixin):
374 
375  def __init__(self, parent, plugin):
376  style = wx.LC_REPORT|wx.NO_BORDER|wx.LC_SINGLE_SEL|wx.LC_HRULES|wx.LC_VIRTUAL
377  wx.ListCtrl.__init__(self, parent, style=style)
378  ListCtrlAutoWidthMixin.__init__(self)
379  self.SetBackgroundColour(Colour(parent.color_background))
380  self.SetForegroundColour(Colour(parent.color_foreground))
381  self._plugin_plugin = plugin
382  self._create_headers_create_headers()
383  self._link_attribute_link_attribute = self._create_link_attribute_create_link_attribute()
384  self._image_list_image_list = self._create_image_list_create_image_list()
385  self.Bind(wx.EVT_LEFT_UP, self.OnLeftUpOnLeftUp)
386 
387  def _create_headers(self):
388  for col, title in enumerate(_KeywordData.headers):
389  self.InsertColumn(col, title)
390  self.SetBackgroundColour(Colour(self.GetParent().color_background))
391  self.SetForegroundColour(Colour(self.GetParent().color_foreground))
392  self.SetColumnWidth(0, 250)
393 
395  if wx.VERSION < (4, 1, 0):
396  attr = wx.ListItemAttr()
397  else:
398  attr = wx.ItemAttr()
399  attr.SetTextColour(wx.BLUE)
400  attr.SetFont(Font().underlined)
401  return attr
402 
404  imglist = wx.ImageList(16, 16)
405  imglist.Add(wx.ArtProvider.GetBitmap(wx.ART_GO_UP, wx.ART_OTHER, (16, 16)))
406  self.SetImageList(imglist, wx.IMAGE_LIST_SMALL)
407  return imglist
408 
409  def show_keywords(self, keywords, kw_selection):
410  self._keywords_keywords = keywords
411  self.SetItemCount(len(self._keywords_keywords))
412  self.SetBackgroundColour(Colour(self.GetParent().color_secondary_background))
413  self.SetForegroundColour(Colour(self.GetParent().color_secondary_foreground))
414  if keywords:
415  index = self._keywords_keywords.index(kw_selection)
416  self.Select(index)
417  self.Focus(index)
418 
419  def OnLeftUp(self, event):
420  item, flags = self.HitTest(event.Position)
421  if item == wx.NOT_FOUND:
422  return
423  kw = self._keywords_keywords[item]
424  if kw.is_user_keyword() and (flags & wx.LIST_HITTEST_ONITEMICON):
425  self._plugin_plugin.select_user_keyword_node(kw.item)
426 
427  def OnGetItemText(self, row, col):
428  kw = self._keywords_keywords[row]
429  return [kw.name, kw.source, kw.shortdoc][col]
430 
431  def OnGetItemImage(self, item):
432  if self._keywords_keywords[item].is_user_keyword():
433  return 0 # index in self._image_list
434  return -1 # No image
Used to create menu entries, keyboard shortcuts and/or toolbar buttons.
Definition: actioninfo.py:176
Entry point to RIDE plugin API – all plugins must extend this class.
Definition: plugin.py:50
def subscribe(self, listener, *topics)
Start to listen to messages with the given topics.
Definition: plugin.py:396
def unsubscribe_all(self)
Stops to listen to all messages this plugin has subscribed to.
Definition: plugin.py:411
def register_action(self, action_info)
Registers a menu entry and optionally a shortcut and a toolbar icon.
Definition: plugin.py:205
def unregister_actions(self)
Unregisters all actions registered by this plugin.
Definition: plugin.py:230
def register_search_action(self, description, handler, icon, default=False)
Definition: plugin.py:226
def show_search_with_criteria(self, pattern='', search_docs=True, source=ALL_KEYWORDS)
def _update_widgets(self, pattern, search_docs, source)
A plugin for searching keywords based on name or documentation.
def enable(self)
This method is called by RIDE when the plugin is enabled.
def search(self, pattern, search_docs, source_filter)
def disable(self)
Called by RIDE when the plugin is disabled.
def _sort_by_search(self, keywords, sort_order, search_criteria)
def _get_comparator_for(self, atrr_name)
def __init__(self, keywords, sort_order, search_criteria=None)
def _sort_by_attr(self, keywords, sort_order)
def _sort(self, keywords, sort_order, search_criteria=None)
def _value_lowerer(self, kw, attr_name)
def __init__(self, parent, plugin)
def show_keywords(self, keywords, kw_selection)
def _contains(self, string, pattern)
def __init__(self, pattern='', search_docs=True, source_filter=ALL_KEYWORDS)