Coverage for src/robotide/pluginapi/plugin.py: 70%
171 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-06 10:40 +0100
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-06 10:40 +0100
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.
16import inspect 1ab
18import wx 1ab
20from .. import utils 1ab
21from ..action.actioninfo import ActionInfo 1ab
22from ..publish import PUBLISHER 1ab
25class Plugin(object): 1ab
26 """Entry point to RIDE plugin API -- all plugins must extend this class.
28 Plugins can use the helper methods implemented in this class to interact
29 with the core application. The methods and their arguments are kept stable
30 across the different RIDE releases to the extent that it is possible.
32 If the provided methods are not enough, plugins can also interact with the
33 core directly using properties `tree`, `menubar`, `toolbar`, `notebook` and
34 `model`. Although these attributes themselves are stable, the functionality
35 behind them may still change between releases. Users are thus recommended
36 to propose new helper methods, preferably with patches, for often needed
37 functionality that is only available through these properties.
39 :IVariables:
40 name
41 Plugin name. Set in `__init__` based on the given name or the class name.
42 doc
43 Plugin documentation. Set in `__init__` based on the given doc or
44 the class docstring.
45 metadata
46 Plugin metadata. Set in `__init__` based on the given metadata.
47 initially_enabled
48 Specifies should the plugin be enabled when first loaded.
49 Set in `__init__`.
50 """
51 tree = property(lambda self: self.__frame.tree, doc='Provides access to the suite and resource tree') 1abrCkDEFGHIJlmnoi
52 filemgr = property(lambda self: self.__frame.filemgr, doc='Provides access to the files and folders explorer') 1abi
53 menubar = property(lambda self: self.__frame.GetMenuBar(), doc='Provides access to the application menubar') 1abi
54 toolbar = property(lambda self: self.__frame.GetToolBar(), doc='Provides access to the application toolbar') 1abi
55 statusbar = property(lambda self: self.__frame.FindWindowByName("StatusBar", self.__frame), 1abqi
56 doc='Provides access to the application statusbar')
57 notebook = property(lambda self: self.__frame.notebook, doc='Provides access to the tabbed notebook') 1abqrklKi
58 model = property(lambda self: self.__app.model, doc='Provides access to the data model') 1abkmnoi
59 frame = property(lambda self: self.__frame, doc='Reference to the RIDE main frame') 1abi
60 datafile = property(lambda self: self.get_selected_datafile(), doc='Currently selected datafile') 1abki
61 global_settings = property(lambda self: self.__app.settings, doc='Settings read from settings.cfg') 1abi
63 def __init__(self, application, name=None, doc=None, metadata=None, default_settings=None, initially_enabled=True): 1ab
64 """Initialize the plugin with the provided data.
66 The provided information is mainly used by the plugin manager. Simple
67 plugins are often fine with the defaults. If this method is overridden,
68 the plugin must call it explicitly::
70 from robotide.pluginapi import Plugin
72 class MyCoolPluginExample(Plugin):
73 \"\"\"This extra cool docstring is used as the plugin doc.\"\"\"
74 def __init__(self, application):
75 Plugin.__init__(self, application, metadata={'version': '0.1'},
76 default_settings={'color': 'red', 'x': 42})
78 Plugins should not create any user interface elements at this point but
79 wait until the `enable` method is called.
81 :Parameters:
82 application
83 RIDE application reference.
84 name
85 Name of the plugin. If not specified, the name is got from the
86 plugin class name-dropping possible ``Plugin`` from the end.
87 doc
88 Plugin documentation. If not specified, the doc is got from the
89 plugin class docstring.
90 metadata
91 A dictionary of free metadata shown on the plugin manager. Values
92 containing URLs will be shown as links.
93 default_settings
94 A dictionary of settings and their default values. Settings are
95 automatically stored onto RIDE configuration file, can be
96 accessed using direct attribute access via `__getattr__`, and new
97 settings can be saved using `save_setting`.
98 initially_enabled
99 Specifies should the plugin be enabled when loaded for the first
100 time. Users can change the status later from the plugin manager.
101 """
102 self.name = name or utils.name_from_class(self, drop='Plugin') 1ajefcdgh
103 self.doc = self._get_doc(doc) 1ajefcdgh
104 self.metadata = metadata or {} 1ajefcdgh
105 self.initially_enabled = initially_enabled 1ajefcdgh
106 self._save_timer = None 1ajefcdgh
107 self.__app = application 1ajefcdgh
108 self.__frame = application.frame 1ajefcdgh
109 self.__namespace = application.namespace 1ajefcdgh
110 self.__settings = application.settings['Plugins'].add_section(self.name) 1ajefcdgh
111 self.__settings.set_defaults(default_settings) 1ajefcdgh
112 self.__actions = [] 1ajefcdgh
114 def _get_doc(self, given_doc): 1ab
115 if given_doc: 115 ↛ 116line 115 didn't jump to line 116 because the condition on line 115 was never true1ajefcdgh
116 return given_doc
117 if self.__doc__ == Plugin.__doc__: 1ajefcdgh
118 return '' 1efcdgh
119 return inspect.getdoc(self) or '' 1aj
121 def __getattr__(self, name): 1ab
122 """Provides convenient attribute access to saved settings.
124 For example, setting ``color`` can be accessed directly like
125 ``self.color``.
126 """
127 if name in ('_Plugin__settings', '_parent'): 1aqrkmnoiefcdgh
128 return 1ai
129 if '__settings' not in name and self.__settings.has_setting(name): 1aqrkmnoiefcdgh
130 return self.__settings[name] 1aecdgh
131 raise AttributeError("No attribute or settings with name '%s' found" % name) 1aqrkmnof
133 def save_setting(self, name, value, override=True, delay=0): 1ab
134 """Saves the specified setting into the RIDE configuration file.
136 ``override`` controls whether a possibly already existing value is
137 overridden or not. Saved settings can be accessed using direct attribute
138 access via `__getattr__`.
139 ``delay`` is number defining how many seconds is waited before setting
140 is saved. This can be used to prevent saving the value while user is
141 typing it.
142 """
143 self.__settings.set(name, value, autosave=delay == 0, override=override) 1acd
144 self._delay_saving(delay) 1acd
146 def _delay_saving(self, delay): 1ab
147 if not delay: 147 ↛ 149line 147 didn't jump to line 149 because the condition on line 147 was always true1acd
148 return 1acd
149 delay = delay * 1000
150 if not self._save_timer:
151 self._save_timer = wx.CallLater(delay, self._save_setting_after_delay)
152 else:
153 self._save_timer.Restart(delay)
155 def _save_setting_after_delay(self): 1ab
156 self.__settings.save()
157 self._save_timer = None
159 def enable(self): 1ab
160 """This method is called by RIDE when the plugin is enabled.
162 Possible integration to UI should be done in this method and removed
163 when the `disable` method is called.
164 """
165 pass 1p
167 def disable(self): 1ab
168 """Called by RIDE when the plugin is disabled.
170 Undo whatever was done in the `enable` method.
171 """
172 pass
174 def config_panel(self, parent): 1ab
175 """Called by RIDE to get the plugin configuration panel.
177 The panel returned will be integrated into the plugin manager UI, and
178 can be used e.g. to display configurable settings.
180 By default, there is no configuration panel.
181 """
182 _ = parent
183 return None
185 def register_action(self, action_info): 1ab
186 """Registers a menu entry and optionally a shortcut and a toolbar icon.
188 ``action_info`` is an instance of `ActionInfo` class containing needed
189 information to create menu entry, keyboard shortcut and/or toolbar
190 button for the action.
192 All registered actions can be un-registered using the
193 `unregister_actions` method.
195 If register action is used in menu event handler, and it modifies the
196 menu that triggered the event, it is safest to wrap register action
197 call inside wx.CallAfter function.
199 Returns created `Action` object.
200 """
201 action = self.__frame.actions.register_action(action_info) 1apl
202 self.__actions.append(action) 1apl
203 return action 1apl
205 def register_shortcut(self, shortcut, callback): 1ab
206 action_info = ActionInfo(None, None, action=callback, shortcut=shortcut) 1al
207 action = self.__frame.actions.register_shortcut(action_info) 1al
208 self.__actions.append(action) 1al
209 return action 1al
211 def register_actions(self, action_infos): 1ab
212 """Registers multiple menu entries and shortcuts/icons.
214 ``action_infos`` is a list of same `ActionInfo` objects that
215 `register_action` method accepts.
217 Returns list of created `Action` objects.
218 """
219 return [self.register_action(info) for info in action_infos] 1al
221 def register_search_action(self, description, handler, icon, default=False): 1ab
222 self.__frame.toolbar.register_search_handler(description, handler, icon, default=default)
224 def unregister_actions(self): 1ab
225 """Unregisters all actions registered by this plugin."""
226 for action in self.__actions: 1ap
227 action.unregister()
228 self.__actions = [] 1ap
230 def add_tab(self, tab, title, allow_closing=True): 1ab
231 """Adds the ``tab`` with the ``title`` to the tabbed notebook and shows it.
233 The ``tab`` can be any wxPython container. ``allow_closing`` defines
234 can users close the tab while the plugin is enabled.
235 """
236 self.notebook.add_tab(tab, title, allow_closing)
238 def show_tab(self, tab): 1ab
239 """Makes the ``tab`` added using `add_tab` visible."""
240 self.notebook.show_tab(tab)
242 def delete_tab(self, tab): 1ab
243 """Deletes the ``tab`` added using `add_tab`."""
244 self.notebook.delete_tab(tab)
246 def allow_tab_closing(self, tab): 1ab
247 """Allows closing a tab that has been created using allow_closing=False."""
248 self.notebook.allow_closing(tab)
250 def disallow_tab_closing(self, tab): 1ab
251 """Disallows closing a tab by user"""
252 self.notebook.disallow_closing(tab)
254 def tab_is_visible(self, tab): 1ab
255 """Returns is the ``tab`` added using `add_tab` visible or not."""
256 return self.notebook.tab_is_visible(tab)
258 def new_suite_can_be_opened(self): 1ab
259 """Checks are there modified files and asks user what to do if there are.
261 Returns False if there were modified files and user canceled the dialog,
262 otherwise returns True.
263 """
264 return self.__app.ok_to_open_new()
266 def open_suite(self, path): 1ab
267 """Opens a test suite specified by the ``path``.
269 No suite is opened if parsing the suite fails.
270 """
271 self.__frame.open_suite(path)
273 def get_selected_datafile(self): 1ab
274 """Returns the data file that is currently selected in the tree.
276 If a test case or a keyword is selected, the data file containing the
277 selected item is returned.
279 :rtype:
280 `InitFile`, `TestCaseFile` or `ResourceFile`
281 """
282 if not self.tree: 282 ↛ 283line 282 didn't jump to line 283 because the condition on line 282 was never true1aki
283 return
284 return self.tree.get_selected_datafile() 1ak
286 def save_selected_datafile(self): 1ab
287 """Saves the data file that is currently selected in the tree.
289 If a test case or a keyword is selected, the data file containing the
290 selected item is saved.
291 """
292 self.__frame.save(self.tree.get_selected_datafile_controller())
294 def is_unsaved_changes(self): 1ab
295 """Returns True if there is any unsaved changes, otherwise False"""
296 return self.__frame.has_unsaved_changes()
298 def save_all_unsaved_changes(self): 1ab
299 """Saves all the data files that are modified."""
300 self.__frame.save_all()
302 def get_selected_item(self): 1ab
303 """Returns the item that is currently selected in the tree.
305 The item can be a test suite, a resource file, a test case or a keyword.
307 :rtype:
308 `InitFile`, `TestCaseFile`, `ResourceFile`, `TestCase` or `UserKeyword`
309 """
310 if not self.tree: 310 ↛ 311line 310 didn't jump to line 311 because the condition on line 310 was never true1akmno
311 return
312 if hasattr(self, 'model'): 1akmno
313 return self.tree.get_selected_item() or self.model 1ak
314 else:
315 return self.tree.get_selected_item() 1mno
317 def content_assist_values(self, value=''): 1ab
318 """Returns content assist values for currently selected item."""
319 return self.__namespace.get_suggestions_for(self.get_selected_item(), value) 1km
321 def get_user_keyword(self, name): 1ab
322 """Returns user keyword instance whose name is ``name`` or None."""
323 keyword_info = self.__namespace.find_user_keyword(self.datafile, name)
324 return keyword_info.item if keyword_info else None
326 def select_user_keyword_node(self, uk): 1ab
327 """Selects node containing the given ``uk`` in the tree."""
328 if not self.tree:
329 return
330 self.tree.select_user_keyword_node(uk)
332 def get_keyword(self, name): 1ab
333 """Returns the keyword object with the given name or None"""
334 return self.__namespace.find_keyword(self.datafile, name)
336 def get_keyword_details(self, name): 1ab
337 """Returns details (documentation, source) of keyword with name ``name``.
339 Returns None if no matching keyword is found.
340 """
341 return self.__namespace.keyword_details(self.datafile, name)
343 def is_user_keyword(self, name): 1ab
344 """Returns whether ``name`` is a user keyword of current datafile.
346 Checks both the datafile's own and imported user keywords for match.
347 """
348 return self.__namespace.is_user_keyword(self.datafile, name)
350 def is_library_keyword(self, name): 1ab
351 """Returns whether ``name`` is a keyword imported by current datafile."""
352 return self.__namespace.is_library_keyword(self.datafile, name)
354 def all_testcases(self): 1ab
355 """Returns all test cases from all suites in one, unsorted list"""
356 return self.model.all_testcases()
358 def register_content_assist_hook(self, hook): 1ab
359 """Allows plugin to insert values in content assist dialog.
361 ``hook`` must be a callable, which should take two arguments and
362 return a list of instances of `ItemInfo` class. When content
363 assist is requested by user, ``hook`` will be called with the current
364 dataitem and current value of cell as parameters.
365 """
366 self.__namespace.register_content_assist_hook(hook) 1j
368 def get_plugins(self): 1ab
369 """Returns list containing plugin wrapper for every loaded plugin.
371 Wrapper is an instance of `PluginConnector` if the plugin has loaded
372 successfully, otherwise it's an instance of `BrokenPlugin`."""
373 return self.__app.get_plugins() 1L
375 def publish(self, topic, data): 1ab
376 """Publishes a message with given topic and client data.
378 Purpose of this method is to support inter-plugin communication which
379 is not possible to achieve using custom message classes.
381 `data` will be passed as an argument to registered listener methods.
382 """
383 PUBLISHER.publish(topic, data)
385 def subscribe(self, listener, *topics): 1ab
386 """Start to listen to messages with the given ``topics``.
388 See the documentation of the `robotide.publish` module for more
389 information about subscribing to messages and the messaging system
391 `unsubscribe` and `unsubscribe_all` can be used to stop listening to
392 certain or all messages.
393 """
394 for topic in topics: 1aplstu
395 PUBLISHER.subscribe(listener, topic) 1aplstu
397 def unsubscribe(self, listener, *topics): 1ab
398 """Stops listening to messages with the given ``topics``.
400 ``listener`` and ``topics`` have the same meaning as in `subscribe`
401 and a listener/topic combination is unsubscribed only when both of them
402 match.
403 """
404 for topic in topics: 1vwxyzAB
405 PUBLISHER.unsubscribe(listener, topic) 1vwxyzAB
407 def unsubscribe_all(self): 1ab
408 """Stops to listen to all messages this plugin has subscribed to."""
409 PUBLISHER.unsubscribe_all(self) 1ai
411 def register_editor(self, item_class, editor_class, activate=True): 1ab
412 """Register ``editor_class`` as an editor class for model items of type ``item_class``
414 If ``activate`` is True, the given editor is automatically activated
415 using `set_active_editor`.
416 """
417 self.__app.register_editor(item_class, editor_class, activate)
419 def unregister_editor(self, item_class, editor_class): 1ab
420 """Unregisters ``editor_class`` as an editor class for model items of type ``item_class``"""
421 self.__app.unregister_editor(item_class, editor_class)
423 def set_active_editor(self, item_class, editor_class): 1ab
424 """Activates the specified editor to be used with the specified model item.
426 The editor must have been registered first by using `register_editor`.
427 """
428 self.__app.activate_editor(item_class, editor_class)
430 def get_editors(self, item_class): 1ab
431 """Return all registered editors for the given model item class.
433 The last editor in the list is the currently active editor.
434 """
435 return self.__app.get_editors(item_class)
437 def get_editor(self, item_class): 1ab
438 """Return the current editor class for the given model item class"""
439 return self.__app.get_editor(item_class)
441 def highlight_cell(self, tcuk, obj=None, row=-1, column=-1): 1ab
442 """Highlight a specific row/column of a test case or user keyword"""
443 if not self.tree:
444 return
445 self.tree.select_node_by_data(tcuk)
446 self.__app.editor.highlight_cell(obj, row, column)
448 def highlight(self, data, text): 1ab
449 """Highlight a specific text of a given data's editor"""
450 if not self.tree:
451 return
452 self.tree.highlight(data, text)
454 def statusbar_message(self, text, ttl=0): 1ab
455 """Set a text message at Plugin area of StatusBar
456 :Parameters:
457 text
458 Message to show in StatusBar
459 ttl
460 Time to live in milliseconds, default = 0, means does not expire
461 """
462 if text is None: 462 ↛ 463line 462 didn't jump to line 463 because the condition on line 462 was never true1q
463 text = ''
464 self.statusbar.SetStatusText(text, 1) 1q
465 wx.CallAfter(self._delayed_clear, ttl)
467 def _delayed_clear(self, ttl): 1ab
468 if ttl >= 100:
469 wx.CallLater(ttl, self.statusbar_clear)
471 def statusbar_clear(self): 1ab
472 """Clears the message at Plugin area of StatusBar"""
473 self.statusbar.SetStatusText('', 1)