class EditorPlugin(Plugin, TreeAwarePluginMixin): """The default editor plugin. This plugin implements editors for the various items of Robot Framework test data. """ def __init__(self, application): Plugin.__init__(self, application) self._tab = None self._grid_popup_creator = PopupCreator() self._creator = EditorCreator(self.register_editor) self._editor = None def enable(self): self._creator.register_editors() self._show_editor() self.register_actions(ActionInfoCollection(_EDIT, self._tab, self._tab)) self.subscribe(self.OnTreeItemSelected, RideTreeSelection) self.subscribe(self.OnTabChanged, RideNotebookTabChanged) self.subscribe(self.OnTabChanging, RideNotebookTabChanging) self.subscribe(self.OnSaveToModel, RideSaving) self.subscribe(self.OnFileDeleted, RideDataFileRemoved) self.add_self_as_tree_aware_plugin() def disable(self): self.remove_self_from_tree_aware_plugins() self.unsubscribe_all() self.delete_tab(self._tab) wx.CallLater(500, self.unregister_actions()) self._tab = None self._editor = None def is_focused(self): return self.tab_is_visible(self._tab) def highlight_cell(self, obj, row, column): self.show() self._editor.highlight_cell(obj, row, column) def highlight(self, text): self.show() self._editor.highlight(text) def show(self): self.show_tab(self._tab) def register_context_menu_hook_to_grid(self, hook): """ Used to register own items to grid's right click context menu hook is called with current selection (list of list containing values) and it is expected to return list of PopupMenuItem. If user selects one of the returned PopupMenuItem, related function is called with one argument, the wx event. """ self._grid_popup_creator.add_hook(hook) def unregister_context_menu_hook_to_grid(self, hook): self._grid_popup_creator.remove_hook(hook) def _show_editor(self): if not self._tab: self._tab = _EditorTab(self) self.add_tab(self._tab, 'Edit', allow_closing=False) if self.is_focused(): self._editor = self._create_editor() self._tab.show_editor(self._editor) def _create_editor(self): return self._creator.editor_for(self, self._tab, self.tree) def OnTreeItemSelected(self, message): self._show_editor() if not self.is_focused() and \ not self.is_focus_on_tree_aware_plugin() and \ (not message or not message.silent): self._editor = self._create_editor() self._tab.show_editor(self._editor) self.show() if self._editor: self._editor.tree_item_selected(message.item) @overrides(Plugin) def get_selected_datafile(self): if self._editor and self._editor.controller: return self._editor.controller.datafile return Plugin.get_selected_datafile(self) def OnOpenEditor(self, event): self._show_editor() def OnTabChanged(self, message): self._show_editor() def OnTabChanging(self, message): if 'Edit' in message.oldtab: self._tab.save() def OnSaveToModel(self, message): if self._tab: self._tab.save() def OnFileDeleted(self, message): self._create_editor()
class Tree(treemixin.DragAndDrop, customtreectrl.CustomTreeCtrl, utils.RideEventHandler): _RESOURCES_NODE_LABEL = 'External Resources' def __init__(self, parent, action_registerer, settings=None): self._checkboxes_for_tests = False self._test_selection_controller = self._create_test_selection_controller() self._controller = TreeController(self, action_registerer, settings=settings, test_selection=self._test_selection_controller) treemixin.DragAndDrop.__init__(self, parent, **_TREE_ARGS) self._controller.register_tree_actions() self._bind_tree_events() self._images = TreeImageList() self._silent_mode = False self.SetImageList(self._images) self.label_editor = TreeLabelEditListener(self, action_registerer) self._controller.bind_keys() self._subscribe_to_messages() self._popup_creator = PopupCreator() self._dragging = False self._clear_tree_data() self._editor = None self._execution_results = None if not hasattr(self, 'OnCancelEdit'): self.OnCancelEdit = self._on_cancel_edit def _create_test_selection_controller(self): tsc = TestSelectionController() PUBLISHER.subscribe(tsc.clear_all, RideOpenSuite) PUBLISHER.subscribe(tsc.clear_all, RideNewProject) return tsc def _on_cancel_edit(self, item): le = customtreectrl.TreeEvent(customtreectrl.wxEVT_TREE_END_LABEL_EDIT, self.GetId()) le._item = item le.SetEventObject(self) le._label = "" le._editCancelled = True self.GetEventHandler().ProcessEvent(le) def _bind_tree_events(self): self.Bind(wx.EVT_LEFT_DCLICK, self.OnDoubleClick) self.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnSelChanged) self.Bind(wx.EVT_TREE_ITEM_EXPANDING, self.OnTreeItemExpanding) self.Bind(wx.EVT_TREE_ITEM_RIGHT_CLICK, self.OnRightClick) self.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self.OnItemActivated) self.Bind(customtreectrl.EVT_TREE_ITEM_CHECKED, self.OnTreeItemChecked) def OnDoubleClick(self, event): item, pos = self.HitTest(self.ScreenToClient(wx.GetMousePosition())) if item: handler = self._controller.get_handler(item) handler.double_clicked() event.Skip() def set_editor(self, editor): self._editor = editor def StartDragging(self): self._dragging = True treemixin.DragAndDrop.StartDragging(self) def OnEndDrag(self, event): self._dragging = False treemixin.DragAndDrop.OnEndDrag(self, event) def register_context_menu_hook(self, callable): self._popup_creator.add_hook(callable) def unregister_context_menu_hook(self, callable): self._popup_creator.remove_hook(callable) def _subscribe_to_messages(self): subscriptions = [ (self._item_changed, RideItem), (self._resource_added, RideOpenResource), (self._select_resource, RideSelectResource), (self._suite_added, RideSuiteAdded), (self._keyword_added, RideUserKeywordAdded), (self._test_added, RideTestCaseAdded), (self._variable_added, RideVariableAdded), (self._leaf_item_removed, RideUserKeywordRemoved), (self._leaf_item_removed, RideTestCaseRemoved), (self._leaf_item_removed, RideVariableRemoved), (self._datafile_removed, RideDataFileRemoved), (self._datafile_set, RideDataFileSet), (self._data_dirty, RideDataChangedToDirty), (self._data_undirty, RideDataDirtyCleared), (self._variable_moved_up, RideVariableMovedUp), (self._variable_moved_down, RideVariableMovedDown), (self._variable_updated, RideVariableUpdated), (self._filename_changed, RideFileNameChanged), (self._testing_started, RideTestExecutionStarted), (self._test_result, RideTestRunning), (self._test_result, RideTestPassed), (self._test_result, RideTestFailed), (self._handle_import_setting_message, RideImportSetting), (self._mark_excludes, RideExcludesChanged), (self._mark_excludes, RideIncludesChanged), ] for listener, topic in subscriptions: PUBLISHER.subscribe(listener, topic) def _mark_excludes(self, message): tree = self._controller.find_node_by_controller(message.old_controller) self._render_datafile(self.GetItemParent(tree), message.new_controller) self._remove_datafile_node(tree) def _set_item_excluded(self, node): self.SetItemTextColour(node, 'gray') self.SetItemItalic(node, True) self.SetItemText(node, "%s (excluded)" % self.GetItemText(node)) def _handle_import_setting_message(self, message): if message.is_resource(): self._set_resource_color(message.import_controller.get_imported_controller()) self._set_resource_color(message.import_controller.get_previous_imported_controller()) def _set_resource_color(self, resource_controller): if not resource_controller: return node = self._controller.find_node_by_controller(resource_controller) if node: self.SetItemTextColour(node, self._get_resource_text_color(resource_controller)) def _get_resource_text_color(self, resource_controller): return self.GetDefaultAttributes().colFg if resource_controller.is_used() else wx.LIGHT_GREY def _testing_started(self, message): self._for_all_drawn_tests(self._root, lambda t: self.SetItemImage(t, ROBOT_IMAGE_INDEX)) self._execution_results = message.results self._images.set_execution_results(message.results) def _test_result(self, message): wx.CallAfter(self._set_icon_from_execution_results, message.item) def _set_icon_from_execution_results(self, controller): node = self._controller.find_node_by_controller(controller) if not node: return self.SetItemImage(node, self._get_icon_index_for(controller)) def _get_icon_index_for(self, controller): if not self._execution_results: return ROBOT_IMAGE_INDEX if self._execution_results.is_running(controller): return RUNNING_IMAGE_INDEX if self._execution_results.has_passed(controller): return PASSED_IMAGE_INDEX if self._execution_results.has_failed(controller): return FAILED_IMAGE_INDEX return ROBOT_IMAGE_INDEX def populate(self, model): self._clear_tree_data() self._populate_model(model) self._refresh_view() self.SetFocus() # Needed for keyboard shortcuts def _clear_tree_data(self): self.DeleteAllItems() self._root = self.AddRoot('') self._resource_root = self._create_resource_root() self._datafile_nodes = [] def _create_resource_root(self): return self._create_node(self._root, self._RESOURCES_NODE_LABEL, self._images.directory) def _populate_model(self, model): handler = ResourceRootHandler(model, self, self._resource_root, self._controller.settings) self.SetPyData(self._resource_root, handler) if model.data: self._render_datafile(self._root, model.data, 0) for res in model.external_resources: if not res.parent: self._render_datafile(self._resource_root, res) def _resource_added(self, message): ctrl = message.datafile if self._controller.find_node_by_controller(ctrl): return parent = self._get_dir_node(ctrl.parent) if ctrl.parent else self._resource_root self._render_datafile(parent, ctrl) def _get_dir_node(self, ctrl): if ctrl is None: return self._root dir_node = self._get_datafile_node(ctrl.data) if dir_node is None: parent = self._get_dir_node(ctrl.parent) self._render_datafile(parent, ctrl) dir_node = self._get_datafile_node(ctrl.data) return dir_node def _select_resource(self, message): self.select_controller_node(message.item) def select_controller_node(self, controller): self.SelectItem(self._controller.find_node_by_controller(controller)) def _suite_added(self, message): self.add_datafile(message.parent, message.suite) def _refresh_view(self): self.Refresh() if self._resource_root: self.Expand(self._resource_root) if self._datafile_nodes: self.SelectItem(self._datafile_nodes[0]) self._expand_and_render_children(self._datafile_nodes[0]) def _render_datafile(self, parent_node, controller, index=None): node = self._create_node_with_handler(parent_node, controller, index) if controller.dirty: self._controller.mark_node_dirty(node) self._datafile_nodes.append(node) self.SetItemHasChildren(node, True) for child in controller.children: self._render_datafile(node, child) return node def _create_node_with_handler(self, parent_node, controller, index=None): handler_class = action_handler_class(controller) node = self._create_node(parent_node, controller.display_name, self._images[controller], index, with_checkbox=(handler_class == TestCaseHandler and self._checkboxes_for_tests)) if isinstance(controller, ResourceFileController): if not controller.is_used(): self.SetItemTextColour(node, wx.ColorRGB(0xA9A9A9)) self.SetPyData(node, handler_class(controller, self, node, self._controller.settings)) if controller.is_excluded(): self._set_item_excluded(node) return node def set_checkboxes_for_tests(self): self._checkboxes_for_tests = True def _expand_and_render_children(self, node): assert node is not None self._render_children(node) self.Expand(node) def _render_children(self, node): handler = self._controller.get_handler(node) if not handler or not handler.can_be_rendered: return self._create_child_nodes(node, handler, lambda item: item.is_test_suite) handler.set_rendered() def _create_child_nodes(self, node, handler, predicate): for childitem in self._children_of(handler): index = self._get_insertion_index(node, predicate) self._create_node_with_handler(node, childitem, index) def _children_of(self, handler): return [v for v in handler.variables if v.has_data()] + list(handler.tests) + \ list(handler.keywords) def _create_node(self, parent_node, label, img, index=None, with_checkbox=False): node = self._wx_node(parent_node, index, label, with_checkbox) self.SetItemImage(node, img.normal, wx.TreeItemIcon_Normal) self.SetItemImage(node, img.expanded, wx.TreeItemIcon_Expanded) return node def _wx_node(self, parent_node, index, label, with_checkbox): ct_type = 1 if with_checkbox else 0 if index is not None: # blame wxPython for this ugliness if isinstance(index, int): return self.InsertItemByIndex(parent_node, index, label, ct_type=ct_type) else: return self.InsertItem(parent_node, index, label, ct_type=ct_type) return self.AppendItem(parent_node, label, ct_type=ct_type) def add_datafile(self, parent, suite): snode = self._render_datafile(self._get_datafile_node(parent.data), suite) self.SelectItem(snode) def add_test(self, parent_node, test): self._add_dataitem(parent_node, test, lambda item: item.is_user_keyword) def add_keyword(self, parent_node, kw): self._add_dataitem(parent_node, kw, lambda item: item.is_test_suite) def _add_dataitem(self, parent_node, dataitem, predicate): node = self._get_or_create_node(parent_node, dataitem, predicate) self._select(node) self._controller.mark_node_dirty(parent_node) def _get_or_create_node(self, parent_node, dataitem, predicate): if not self.IsExpanded(parent_node): self._expand_and_render_children(parent_node) return self._controller.find_node_with_label(parent_node, dataitem.display_name) index = self._get_insertion_index(parent_node, predicate) return self._create_node_with_handler(parent_node, dataitem, index) def _select(self, node): if node: wx.CallAfter(self.SelectItem, node) def _get_insertion_index(self, parent_node, predicate): if not predicate: return None item, cookie = self.GetFirstChild(parent_node) while item: if predicate(self._controller.get_handler(item)): index = self.GetPrevSibling(item) if not index: index = 0 return index item, cookie = self.GetNextChild(parent_node, cookie) return None def _keyword_added(self, message): self.add_keyword(self._get_datafile_node(self.get_selected_datafile()), message.item) def _variable_added(self, message): self._get_or_create_node(self._get_datafile_node(self.get_selected_datafile()), message.item, lambda item: not item.is_variable or item.index > message.index) def _leaf_item_removed(self, message): node = self._controller.find_node_by_controller(message.item) self.delete_node(node) def _test_added(self, message): self.add_test(self._get_datafile_node(self.get_selected_datafile()), message.item) def _datafile_removed(self, message): dfnode = self._get_datafile_node(message.datafile.data) self._datafile_nodes.remove(dfnode) self.DeleteChildren(dfnode) self.Delete(dfnode) def _datafile_set(self, message): wx.CallAfter(self._refresh_datafile_when_file_set, message.item) def _filename_changed(self, message): df = message.datafile node = self._controller.find_node_by_controller(df) if not node: raise AssertionError('No node found with controller "%s"' % df) wx.CallAfter(self.SetItemText, node, df.display_name) def add_keyword_controller(self, controller): parent = self._get_datafile_node(self.get_selected_datafile()) self.add_keyword(parent, controller) def delete_node(self, node): if node is None: return parent = self.GetItemParent(node) self._controller.mark_node_dirty(parent) if self.IsSelected(node): wx.CallAfter(self.SelectItem, parent) wx.CallAfter(self.Delete, node) def _data_dirty(self, message): self._controller.mark_controller_dirty(message.datafile) def _data_undirty(self, message): self.unset_dirty() def unset_dirty(self): for node in self._datafile_nodes: text = self.GetItemText(node) handler = self._controller.get_handler(node) if text.startswith('*') and not handler.controller.dirty: self.SetItemText(node, text[1:]) def select_node_by_data(self, controller): '''Find and select the tree item associated with the given controller. Controller can be any of the controllers that are represented in the tree.''' parent_node = self._get_datafile_node(controller.datafile) if not parent_node: return None if not self.IsExpanded(parent_node): self._expand_and_render_children(parent_node) node = self._controller.find_node_by_controller(controller) if node != self.GetSelection(): self.SelectItem(node) return node def select_user_keyword_node(self, uk): parent_node = self._get_datafile_node(uk.parent.parent) if not parent_node: return if not self.IsExpanded(parent_node): self._expand_and_render_children(parent_node) node = self._controller.find_node_with_label(parent_node, utils.normalize(uk.name)) if node != self.GetSelection(): self.SelectItem(node) def _get_datafile_node(self, datafile): for node in self._datafile_nodes: if self._controller.get_handler(node).item == datafile: return node return None def get_selected_datafile(self): """Returns currently selected data file. If a test or user keyword node is selected, returns parent of that item. """ datafile = self._get_selected_datafile_node() if not datafile: return None return self._controller.get_handler(datafile).item def get_selected_datafile_controller(self): """Returns controller associated with currently active data file. If a test or user keyword node is selected, returns parent of that item. """ dfnode = self._get_selected_datafile_node() return self._controller.get_handler(dfnode).controller if dfnode else None def _get_selected_datafile_node(self): node = self.GetSelection() if not node or node in (self._resource_root, self._root): return None while node not in self._datafile_nodes: node = self.GetItemParent(node) return node def get_selected_item(self): """Returns model object associated with currently selected tree node.""" selection = self.GetSelection() if not selection: return None handler = self._controller.get_handler(selection) return handler and handler.controller or None def move_up(self, node): prev = self.GetPrevSibling(node) if prev.IsOk(): self._switch_items(prev, node, node) def move_down(self, node): next = self.GetNextSibling(node) if next.IsOk(): self._switch_items(node, next, node) def _switch_items(self, first, second, currently_selected): """Changes the order of given items, first is expected to be directly above the second""" selection = self.GetItemPyData(currently_selected).controller controller = self._controller.get_handler(first).controller self.Delete(first) self._create_node_with_handler(self.GetItemParent(second), controller, second) self.select_node_by_data(selection) def _refresh_datafile_when_file_set(self, controller): self._start_silent_mode() #Prevent tab selections based on tree item selected events current = self.get_selected_datafile_controller() if not current: # If tree is not yet in use - do not expand anything. self._end_silent_mode() return self._uncheck_tests(current) item = self.GetSelection() current_txt = self.GetItemText(item) if item.IsOk() else '' # after refresh current and current_txt might have been changed node = self._refresh_datafile(controller) if node is None: #TODO: Find out why this sometimes happens return self._expand_and_render_children(node) if current == controller: wx.CallAfter(self.SelectItem, self._controller.find_node_with_label(node, current_txt) or node) wx.CallAfter(self._end_silent_mode) else: self._end_silent_mode() def _uncheck_tests(self, controller): self._test_selection_controller.unselect_all(controller.tests) def _start_silent_mode(self): self._silent_mode = True def _end_silent_mode(self): self._silent_mode = False def refresh_datafile(self, controller, event): to_be_selected = self._get_pending_selection(event) new_node = self._refresh_datafile(controller) self._handle_pending_selection(to_be_selected, new_node) def _refresh_datafile(self, controller): self._uncheck_tests(controller) orig_node = self._get_data_controller_node(controller) if orig_node is not None: insertion_index = self._get_datafile_index(orig_node) parent = self._get_parent(orig_node) self._remove_datafile_node(orig_node) return self._render_datafile(parent, controller, insertion_index) def _get_pending_selection(self, event): if hasattr(event, 'Item'): item = event.GetItem() event.Veto() elif hasattr(event, 'Position'): item, flags = self.HitTest(event.Position) if not (item.IsOk() and self._click_on_item(flags)): return else: return return self.GetItemText(item) def _get_data_controller_node(self, controller): for node in self._datafile_nodes: if self.GetItemPyData(node).controller == controller: return node return None def _click_on_item(self, flags): return flags & wx.TREE_HITTEST_ONITEM def _get_datafile_index(self, node): insertion_index = self.GetPrevSibling(node) if not insertion_index: insertion_index = 0 return insertion_index def _get_parent(self, node): return self.GetItemParent(node) def _remove_datafile_node(self, node): for child in self.GetItemChildren(node): if child in self._datafile_nodes: self._remove_datafile_node(child) self._datafile_nodes.remove(node) self.Delete(node) def _handle_pending_selection(self, to_be_selected, parent_node): if to_be_selected: self._expand_and_render_children(parent_node) wx.CallAfter(self.SelectItem, self._controller.find_node_with_label(parent_node, to_be_selected)) def OnSelChanged(self, event): node = event.GetItem() if not node.IsOk() or self._dragging: event.Skip() return self._controller.add_to_history(node) handler = self._controller.get_handler(node) if handler and handler.item: RideTreeSelection(node=node, item=handler.controller, silent=self._silent_mode).publish() self.SetFocus() def OnTreeItemExpanding(self, event): node = event.GetItem() if node.IsOk(): self._render_children(node) def SelectAllTests(self, item): self._for_all_tests(item, lambda t: self.CheckItem(t)) def SelectTests(self, tests): def foo(t): if self.GetPyData(t).controller in tests: self.CheckItem(t) self._for_all_tests(self._root, foo) def ExpandAllSubNodes(self, item): self._expand_or_collapse_nodes(item, self.Expand) def CollapseAllSubNodes(self, item): self._expand_or_collapse_nodes(item, self.Collapse) def _expand_or_collapse_nodes(self, item, callback): if not self.HasAGWFlag(customtreectrl.TR_HIDE_ROOT) or item != self.GetRootItem(): callback(item) for child in item.GetChildren(): self._expand_or_collapse_nodes(child, callback) def _for_all_tests(self, item, func): if not self.HasAGWFlag(customtreectrl.TR_HIDE_ROOT) or item != self.GetRootItem(): if isinstance(item.GetData(), ResourceRootHandler or ResourceFileHandler): return self.Expand(item) if self._is_test_node(item): func(item) if not self.IsExpanded(item): return for child in item.GetChildren(): self._for_all_tests(child, func) def _for_all_drawn_tests(self, item, func): if self._is_test_node(item): func(item) for child in item.GetChildren(): self._for_all_drawn_tests(child, func) def _is_test_node(self, node): return node.GetType() == 1 def DeselectAllTests(self, item): self._for_all_tests(item, lambda t: self.CheckItem(t, checked=False)) def DeselectTests(self, tests): def foo(t): if self.GetPyData(t).controller in tests: self.CheckItem(t, checked=False) self._for_all_tests(self._root, foo) def SelectFailedTests(self, item): def func(t): # FIXME: This information should be in domain model! self.CheckItem(t, checked=(self.GetItemImage(t) == FAILED_IMAGE_INDEX)) self._for_all_tests(item, func) def OnTreeItemChecked(self, event): node = event.GetItem() handler = self._controller.get_handler(node=node) self._test_selection_controller.select(handler.controller, node.IsChecked()) def OnItemActivated(self, event): node = event.GetItem() if self.IsExpanded(node): self.Collapse(node) elif self.ItemHasChildren(node): self._expand_and_render_children(node) def OnLeftArrow(self, event): node = self.GetSelection() if self.IsExpanded(node): self.Collapse(node) else: event.Skip() def OnRightClick(self, event): handler = self._controller.get_handler(event.GetItem() if hasattr(event, 'GetItem') else None) if handler: if not self.IsExpanded(handler.node): self.Expand(handler.node) handler.show_popup() def OnNewTestCase(self, event): handler = self._controller.get_handler() if handler: handler.OnNewTestCase(event) def OnDrop(self, target, dragged): dragged = self._controller.get_handler(dragged) target = self._controller.get_handler(target) if target and target.accepts_drag(dragged): dragged.controller.execute(MoveTo(target.controller)) else: self.Refresh() def IsValidDragItem(self, item): return self._controller.get_handler(item).is_draggable def OnMoveUp(self, event): handler = self._controller.get_handler() if handler.is_draggable: handler.OnMoveUp(event) def OnMoveDown(self, event): handler = self._controller.get_handler() if handler.is_draggable: handler.OnMoveDown(event) def _item_changed(self, data): controller = data.item node = self._controller.find_node_by_controller(controller) if node: self.SetItemText(node, data.item.name) self._test_selection_controller.send_selection_changed_message() if controller.dirty: self._controller.mark_node_dirty(self._get_datafile_node(controller.datafile)) def _variable_moved_up(self, data): if self._should_update_variable_positions(data): self._do_action_if_datafile_node_is_expanded(self.move_up, data) def _variable_moved_down(self, data): if self._should_update_variable_positions(data): self._do_action_if_datafile_node_is_expanded(self.move_down, data) def _should_update_variable_positions(self, message): return message.item != message.other and message.item.has_data() and message.other.has_data() def _do_action_if_datafile_node_is_expanded(self, action, data): if self.IsExpanded(self._get_datafile_node(data.item.datafile)): node = self._controller.find_node_by_controller(data.item) action(node) def _variable_updated(self, data): self._item_changed(data) def highlight(self, data, text): self.select_node_by_data(data) self._editor.highlight(text) def node_is_resource_file(self, node): return self._controller.get_handler(node).__class__ == ResourceFileHandler
class EditorPlugin(Plugin, TreeAwarePluginMixin): """The default editor plugin. This plugin implements editors for the various items of Robot Framework test data. """ def __init__(self, application): Plugin.__init__(self, application) self._tab = None self._grid_popup_creator = PopupCreator() self._creator = EditorCreator(self.register_editor) self._editor = None def enable(self): self._creator.register_editors() self._show_editor() self.register_actions(ActionInfoCollection(_EDIT, self._tab, self._tab)) self.subscribe(self.OnTreeItemSelected, RideTreeSelection) self.subscribe(self.OnTabChanged, RideNotebookTabChanged) self.subscribe(self.OnTabChanging, RideNotebookTabChanging) self.subscribe(self.OnSaveToModel, RideSaving) self.add_self_as_tree_aware_plugin() def disable(self): self.remove_self_from_tree_aware_plugins() self.unregister_actions() self.unsubscribe_all() self.delete_tab(self._tab) self._tab = None def is_focused(self): return self.tab_is_visible(self._tab) def highlight_cell(self, obj, row, column): self.show() self._editor.highlight_cell(obj, row, column) def highlight(self, text): self.show() self._editor.highlight(text) def show(self): self.show_tab(self._tab) def register_context_menu_hook_to_grid(self, hook): """ Used to register own items to grid's right click context menu hook is called with current selection (list of list containing values) and it is expected to return list of PopupMenuItem. If user selects one of the returned PopupMenuItem, related function is called with one argument, the wx event. """ self._grid_popup_creator.add_hook(hook) def unregister_context_menu_hook_to_grid(self, hook): self._grid_popup_creator.remove_hook(hook) def _show_editor(self): if not self._tab: self._tab = _EditorTab(self) self.add_tab(self._tab, 'Edit', allow_closing=False) if self.is_focused(): self._editor = self._create_editor() self._tab.show_editor(self._editor) def _create_editor(self): return self._creator.editor_for(self, self._tab, self.tree) def OnTreeItemSelected(self, message=None): self._show_editor() if not self.is_focused() and \ not self.is_focus_on_tree_aware_plugin() and \ (not message or not message.silent): self._editor = self._create_editor() self._tab.show_editor(self._editor) self.show() if self._editor: self._editor.tree_item_selected(message.item) def OnOpenEditor(self, event): self._show_editor() def OnTabChanged(self, event): self._show_editor() def OnTabChanging(self, message): if 'Edit' in message.oldtab: self._tab.save() def OnSaveToModel(self, message): if self._tab: self._tab.save()
class Tree( with_metaclass(classmaker(), treemixin.DragAndDrop, customtreectrl.CustomTreeCtrl, utils.RideEventHandler)): _RESOURCES_NODE_LABEL = 'External Resources' def __init__(self, parent, action_registerer, settings=None): self._checkboxes_for_tests = False self._test_selection_controller = \ self._create_test_selection_controller() self._controller = TreeController( self, action_registerer, settings=settings, test_selection=self._test_selection_controller) treemixin.DragAndDrop.__init__(self, parent, **_TREE_ARGS) self._controller.register_tree_actions() self._bind_tree_events() self._images = TreeImageList() self._animctrl = None self._silent_mode = False self.SetImageList(self._images) self.label_editor = TreeLabelEditListener(self, action_registerer) self._controller.bind_keys() self._subscribe_to_messages() self._popup_creator = PopupCreator() self._dragging = False self._clear_tree_data() self._editor = None self._execution_results = None self._resources = [] self.SetBackgroundColour('white') # TODO get background color from def if not hasattr(self, 'OnCancelEdit'): self.OnCancelEdit = self._on_cancel_edit def _create_test_selection_controller(self): tsc = TestSelectionController() PUBLISHER.subscribe(tsc.clear_all, RideOpenSuite) PUBLISHER.subscribe(tsc.clear_all, RideNewProject) return tsc def _on_cancel_edit(self, item): le = customtreectrl.TreeEvent(customtreectrl.wxEVT_TREE_END_LABEL_EDIT, self.GetId()) le._item = item le.SetEventObject(self) le._label = "" le._editCancelled = True self.GetEventHandler().ProcessEvent(le) def _bind_tree_events(self): self.Bind(wx.EVT_LEFT_DCLICK, self.OnDoubleClick) self.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnSelChanged) self.Bind(wx.EVT_TREE_ITEM_EXPANDING, self.OnTreeItemExpanding) self.Bind(wx.EVT_TREE_ITEM_RIGHT_CLICK, self.OnRightClick) self.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self.OnItemActivated) self.Bind(customtreectrl.EVT_TREE_ITEM_CHECKED, self.OnTreeItemChecked) self.Bind(wx.EVT_TREE_ITEM_COLLAPSING, self.OnTreeItemCollapsing) self.Bind(wx.EVT_CLOSE, self.OnClose) def OnDoubleClick(self, event): item, pos = self.HitTest(self.ScreenToClient(wx.GetMousePosition())) if item: handler = self._controller.get_handler(item) handler.double_clicked() event.Skip() def set_editor(self, editor): self._editor = editor def StartDragging(self): self._dragging = True treemixin.DragAndDrop.StartDragging(self) def OnEndDrag(self, event): self._dragging = False treemixin.DragAndDrop.OnEndDrag(self, event) def register_context_menu_hook(self, callable): self._popup_creator.add_hook(callable) def unregister_context_menu_hook(self, callable): self._popup_creator.remove_hook(callable) def _subscribe_to_messages(self): subscriptions = [ (self._item_changed, RideItem), (self._resource_added, RideOpenResource), (self._select_resource, RideSelectResource), (self._suite_added, RideSuiteAdded), (self._keyword_added, RideUserKeywordAdded), (self._test_added, RideTestCaseAdded), (self._variable_added, RideVariableAdded), (self._leaf_item_removed, RideUserKeywordRemoved), (self._leaf_item_removed, RideTestCaseRemoved), (self._leaf_item_removed, RideVariableRemoved), (self._datafile_removed, RideDataFileRemoved), (self._datafile_set, RideDataFileSet), (self._data_dirty, RideDataChangedToDirty), (self._data_undirty, RideDataDirtyCleared), (self._variable_moved_up, RideVariableMovedUp), (self._variable_moved_down, RideVariableMovedDown), (self._variable_updated, RideVariableUpdated), (self._filename_changed, RideFileNameChanged), (self._testing_started, RideTestExecutionStarted), (self._test_result, RideTestRunning), (self._test_result, RideTestPaused), (self._test_result, RideTestPassed), (self._test_result, RideTestFailed), (self._handle_import_setting_message, RideImportSetting), (self._mark_excludes, RideExcludesChanged), (self._mark_excludes, RideIncludesChanged), ] for listener, topic in subscriptions: PUBLISHER.subscribe(listener, topic) def _mark_excludes(self, message): tree = self._controller.find_node_by_controller(message.old_controller) self._render_datafile(self.GetItemParent(tree), message.new_controller) self._remove_datafile_node(tree) def _set_item_excluded(self, node): self.SetItemTextColour(node, wx.TheColourDatabase.Find("GRAY")) self.SetItemItalic(node, True) self.SetItemText(node, "%s (excluded)" % self.GetItemText(node)) def _handle_import_setting_message(self, message): if message.is_resource(): self._set_resource_color( message.import_controller.get_imported_controller()) self._set_resource_color( message.import_controller.get_previous_imported_controller()) def _set_resource_color(self, resource_controller): if not resource_controller: return node = self._controller.find_node_by_controller(resource_controller) if node: self.SetItemTextColour( node, self._get_resource_text_color(resource_controller)) def _get_resource_text_color(self, resource_controller): if resource_controller.is_used(): return self.GetDefaultAttributes().colFg else: return wx.LIGHT_GREY def _testing_started(self, message): self._for_all_drawn_tests( self._root, lambda t: self.SetItemImage(t, ROBOT_IMAGE_INDEX)) self._execution_results = message.results self._images.set_execution_results(message.results) def _test_result(self, message): wx.CallAfter(self._set_icon_from_execution_results, message.item) def _set_icon_from_execution_results(self, controller): node = self._controller.find_node_by_controller(controller) if not node: return img_index = self._get_icon_index_for(controller) # Always set the static icon self.SetItemImage(node, img_index) if self._animctrl: self._animctrl.Stop() self._animctrl.Animation.Destroy() self._animctrl.Destroy() self._animctrl = None self.DeleteItemWindow(node) if img_index in (RUNNING_IMAGE_INDEX, PAUSED_IMAGE_INDEX): from wx.adv import Animation, AnimationCtrl import os _BASE = os.path.join(os.path.dirname(__file__), '..', 'widgets') if img_index == RUNNING_IMAGE_INDEX: img = os.path.join(_BASE, 'robot-running.gif') else: img = os.path.join(_BASE, 'robot-pause.gif') ani = Animation(img) obj = self rect = (node.GetX() + 20, node.GetY()) # Overlaps robot icon self._animctrl = AnimationCtrl(obj, -1, ani, rect) self._animctrl.SetBackgroundColour(obj.GetBackgroundColour()) self.SetItemWindow(node, self._animctrl, False) self._animctrl.Play() # Make visible the running or paused test self.EnsureVisible(node) def _get_icon_index_for(self, controller): if not self._execution_results: return ROBOT_IMAGE_INDEX if self._execution_results.is_paused(controller): return PAUSED_IMAGE_INDEX if self._execution_results.is_running(controller): return RUNNING_IMAGE_INDEX if self._execution_results.has_passed(controller): return PASSED_IMAGE_INDEX if self._execution_results.has_failed(controller): return FAILED_IMAGE_INDEX return ROBOT_IMAGE_INDEX def populate(self, model): self._clear_tree_data() self._populate_model(model) self._refresh_view() self.SetFocus() # Needed for keyboard shortcuts def _clear_tree_data(self): self.DeleteAllItems() self._root = self.AddRoot('') self._resource_root = self._create_resource_root() self._datafile_nodes = [] self._resources = [] def _create_resource_root(self): return self._create_node(self._root, self._RESOURCES_NODE_LABEL, self._images.directory) def _populate_model(self, model): handler = ResourceRootHandler(model, self, self._resource_root, self._controller.settings) self.SetPyData(self._resource_root, handler) if model.data: self._render_datafile(self._root, model.data, 0) for res in model.external_resources: if not res.parent: self._render_datafile(self._resource_root, res) def _resource_added(self, message): ctrl = message.datafile if self._controller.find_node_by_controller(ctrl): return parent = None if ctrl.parent: parent = self._get_dir_node(ctrl.parent) else: parent = self._resource_root self._render_datafile(parent, ctrl) def _get_dir_node(self, ctrl): if ctrl is None: return self._root dir_node = self._get_datafile_node(ctrl.data) if dir_node is None: parent = self._get_dir_node(ctrl.parent) self._render_datafile(parent, ctrl) dir_node = self._get_datafile_node(ctrl.data) return dir_node def _select_resource(self, message): self.select_controller_node(message.item) def select_controller_node(self, controller): self.SelectItem(self._controller.find_node_by_controller(controller)) def _suite_added(self, message): self.add_datafile(message.parent, message.suite) def _refresh_view(self): self.Refresh() if self._resource_root: self.Expand(self._resource_root) if self._datafile_nodes: self.SelectItem(self._datafile_nodes[0]) self._expand_and_render_children(self._datafile_nodes[0]) def _render_datafile(self, parent_node, controller, index=None): node = self._create_node_with_handler(parent_node, controller, index) if not node: return None if controller.dirty: self._controller.mark_node_dirty(node) self._datafile_nodes.append(node) self.SetItemHasChildren(node, True) for child in controller.children: self._render_datafile(node, child) return node def _normalize(self, path): return os.path.normcase(os.path.normpath(os.path.abspath(path))) def _create_node_with_handler(self, parent_node, controller, index=None): if IS_WINDOWS and isinstance(controller, ResourceFileController): resourcefile = self._normalize(controller.filename) pname = parent_node.GetText() self._resources.append((pname, resourcefile)) if IS_WINDOWS: count = 0 for (p, r) in self._resources: if (p, r) == (pname, resourcefile): count += 1 if count > 3: return None handler_class = action_handler_class(controller) with_checkbox = (handler_class == TestCaseHandler and self._checkboxes_for_tests) node = self._create_node(parent_node, controller.display_name, self._images[controller], index, with_checkbox=with_checkbox) if isinstance(controller, ResourceFileController) and not controller.is_used(): self.SetItemTextColour(node, TREETEXTCOLOUR) # wxPython3 hack action_handler = handler_class(controller, self, node, self._controller.settings) self.SetPyData(node, action_handler) # if we have a TestCase node we have to make sure that # we retain the checked state if (handler_class == TestCaseHandler and self._checkboxes_for_tests) \ and self._test_selection_controller.is_test_selected(controller): self.CheckItem(node, True) if controller.is_excluded(): self._set_item_excluded(node) return node def set_checkboxes_for_tests(self): self._checkboxes_for_tests = True def _expand_and_render_children(self, node): assert node is not None self._render_children(node) self.Expand(node) def _render_children(self, node): handler = self._controller.get_handler(node) if not handler or not handler.can_be_rendered: return self._create_child_nodes(node, handler, lambda item: item.is_test_suite) handler.set_rendered() def _create_child_nodes(self, node, handler, predicate): for childitem in self._children_of(handler): index = self._get_insertion_index(node, predicate) self._create_node_with_handler(node, childitem, index) def _children_of(self, handler): return [v for v in handler.variables if v.has_data()] + \ list(handler.tests) + list(handler.keywords) def _create_node(self, parent_node, label, img, index=None, with_checkbox=False): node = self._wx_node(parent_node, index, label, with_checkbox) self.SetItemImage(node, img.normal, wx.TreeItemIcon_Normal) self.SetItemImage(node, img.expanded, wx.TreeItemIcon_Expanded) return node def _wx_node(self, parent_node, index, label, with_checkbox): ct_type = 1 if with_checkbox else 0 if index is not None: # blame wxPython for this ugliness if isinstance(index, int): return self.InsertItemByIndex(parent_node, index, label, ct_type=ct_type) else: return self.InsertItem(parent_node, index, label, ct_type=ct_type) return self.AppendItem(parent_node, label, ct_type=ct_type) def add_datafile(self, parent, suite): snode = self._render_datafile(self._get_datafile_node(parent.data), suite) self.SelectItem(snode) def add_test(self, parent_node, test): self._add_dataitem(parent_node, test, lambda item: item.is_user_keyword) def add_keyword(self, parent_node, kw): self._add_dataitem(parent_node, kw, lambda item: item.is_test_suite) def _add_dataitem(self, parent_node, dataitem, predicate): node = self._get_or_create_node(parent_node, dataitem, predicate) self._select(node) self._controller.mark_node_dirty(parent_node) def _get_or_create_node(self, parent_node, dataitem, predicate): if not self.IsExpanded(parent_node): self._expand_and_render_children(parent_node) return self._controller.find_node_with_label( parent_node, dataitem.display_name) index = self._get_insertion_index(parent_node, predicate) return self._create_node_with_handler(parent_node, dataitem, index) def _select(self, node): if node: wx.CallAfter(self.SelectItem, node) def _get_insertion_index(self, parent_node, predicate): if not predicate: return None item, cookie = self.GetFirstChild(parent_node) while item: if predicate(self._controller.get_handler(item)): index = self.GetPrevSibling(item) if not index: index = 0 return index item, cookie = self.GetNextChild(parent_node, cookie) return None def _keyword_added(self, message): self.add_keyword(self._get_datafile_node(self.get_selected_datafile()), message.item) def _variable_added(self, message): self._get_or_create_node( self._get_datafile_node(self.get_selected_datafile()), message.item, lambda item: not item.is_variable or item.index > message.index) def _leaf_item_removed(self, message): node = self._controller.find_node_by_controller(message.item) parent_node = self._get_datafile_node(message.datafile) # DEBUG The below call causes not calling delete_node # self._test_selection_controller.select(message.item, False) self._controller.mark_node_dirty(parent_node) self.delete_node(node) def _test_added(self, message): self.add_test(self._get_datafile_node(self.get_selected_datafile()), message.item) def _datafile_removed(self, message): dfnode = self._get_datafile_node(message.datafile.data) self._datafile_nodes.remove(dfnode) self.DeleteChildren(dfnode) self.Delete(dfnode) def _datafile_set(self, message): wx.CallAfter(self._refresh_datafile_when_file_set, message.item) def _filename_changed(self, message): df = message.datafile node = self._controller.find_node_by_controller(df) if not node: raise AssertionError('No node found with controller "%s"' % df) wx.CallAfter(self.SetItemText, node, df.display_name) def add_keyword_controller(self, controller): parent = self._get_datafile_node(self.get_selected_datafile()) self.add_keyword(parent, controller) def delete_node(self, node): if node is None: return parent = self.GetItemParent(node) self._controller.mark_node_dirty(parent) if self.IsSelected(node): wx.CallAfter(self.SelectItem, parent) wx.CallAfter(self.Delete, node) def _data_dirty(self, message): self._controller.mark_controller_dirty(message.datafile) def _data_undirty(self, message): self.unset_dirty() def unset_dirty(self): for node in self._datafile_nodes: text = self.GetItemText(node) handler = self._controller.get_handler(node) if text.startswith('*') and not handler.controller.dirty: self.SetItemText(node, text[1:]) def select_node_by_data(self, controller): """Find and select the tree item associated with the given controller. Controller can be any of the controllers that are represented in the tree.""" parent_node = self._get_datafile_node(controller.datafile) if not parent_node: return None if not self.IsExpanded(parent_node): self._expand_and_render_children(parent_node) node = self._controller.find_node_by_controller(controller) if node != self.GetSelection(): self.SelectItem(node) return node def select_user_keyword_node(self, uk): parent_node = self._get_datafile_node(uk.parent.parent) if not parent_node: return if not self.IsExpanded(parent_node): self._expand_and_render_children(parent_node) node = self._controller.find_node_with_label(parent_node, utils.normalize(uk.name)) if node != self.GetSelection(): self.SelectItem(node) def _get_datafile_node(self, datafile): for node in self._datafile_nodes: if self._controller.get_handler(node).item == datafile: return node return None def get_selected_datafile(self): """Returns currently selected data file. If a test or user keyword node is selected, returns parent of that item.""" datafile = self._get_selected_datafile_node() if not datafile: return None return self._controller.get_handler(datafile).item def get_selected_datafile_controller(self): """Returns controller associated with currently active data file. If a test or user keyword node is selected, returns parent of that item.""" dfnode = self._get_selected_datafile_node() if dfnode: return self._controller.get_handler(dfnode).controller else: return None def _get_selected_datafile_node(self): node = self.GetSelection() if not node or node in (self._resource_root, self._root): return None while node not in self._datafile_nodes: node = self.GetItemParent(node) return node def get_selected_item(self): """Returns model object associated with currently selected tree node. """ selection = self.GetSelection() if not selection: return None handler = self._controller.get_handler(selection) return handler and handler.controller or None def move_up(self, node): prev = self.GetPrevSibling(node) if prev.IsOk(): self._switch_items(prev, node, node) def move_down(self, node): next = self.GetNextSibling(node) if next.IsOk(): self._switch_items(node, next, node) def _switch_items(self, first, second, currently_selected): """Changes the order of given items, first is expected to be directly above the second""" selection = self.GetItemPyData(currently_selected).controller controller = self._controller.get_handler(first).controller self.Delete(first) self._create_node_with_handler(self.GetItemParent(second), controller, second) self.select_node_by_data(selection) def _refresh_datafile_when_file_set(self, controller): # Prevent tab selections based on tree item selected events self._start_silent_mode() current = self.get_selected_datafile_controller() if not current: # If tree is not yet in use - do not expand anything. self._end_silent_mode() return item = self.GetSelection() current_txt = self.GetItemText(item) if item.IsOk() else '' # after refresh current and current_txt might have been changed node = self._refresh_datafile(controller) if node is None: # TODO: Find out why this sometimes happens return self._expand_and_render_children(node) if current == controller: select_item = self._controller.find_node_with_label( node, current_txt) if select_item is None: select_item = node wx.CallAfter(self.SelectItem, select_item) wx.CallAfter(self._end_silent_mode) else: self._end_silent_mode() def _uncheck_tests(self, controller): self._test_selection_controller.unselect_all(controller.tests) def _start_silent_mode(self): self._silent_mode = True def _end_silent_mode(self): self._silent_mode = False def refresh_datafile(self, controller, event): to_be_selected = self._get_pending_selection(event) new_node = self._refresh_datafile(controller) self._handle_pending_selection(to_be_selected, new_node) def _refresh_datafile(self, controller): orig_node = self._get_data_controller_node(controller) if orig_node is not None: insertion_index = self._get_datafile_index(orig_node) parent = self.GetItemParent(orig_node) self._remove_datafile_node(orig_node) return self._render_datafile(parent, controller, insertion_index) def _get_pending_selection(self, event): if hasattr(event, 'Item'): item = event.GetItem() event.Veto() elif hasattr(event, 'Position'): item, flags = self.HitTest(event.Position) if not self._click_on_item(item, flags): return else: return return self.GetItemText(item) def _get_data_controller_node(self, controller): for node in self._datafile_nodes: if self.GetItemPyData(node).controller == controller: return node return None def _click_on_item(self, item, flags): return item is not None and item.IsOk() and \ flags & wx.TREE_HITTEST_ONITEM def _get_datafile_index(self, node): insertion_index = self.GetPrevSibling(node) if not insertion_index: insertion_index = 0 return insertion_index def _remove_datafile_node(self, node): for child in self.GetItemChildren(node): if child in self._datafile_nodes: self._remove_datafile_node(child) self._datafile_nodes.remove(node) self.Delete(node) def _handle_pending_selection(self, to_be_selected, parent_node): if to_be_selected: self._expand_and_render_children(parent_node) select_item = self._controller.find_node_with_label( parent_node, to_be_selected) wx.CallAfter(self.SelectItem, select_item) def OnSelChanged(self, event): node = event.GetItem() if not node.IsOk() or self._dragging: event.Skip() return self._controller.add_to_history(node) handler = self._controller.get_handler(node) if handler and handler.item: RideTreeSelection(node=node, item=handler.controller, silent=self._silent_mode).publish() self.SetFocus() def OnTreeItemExpanding(self, event): node = event.GetItem() if node.IsOk(): self._render_children(node) # This exists because CustomTreeItem does not remove animations def OnTreeItemCollapsing(self, event): item = event.GetItem() self._hide_item(item) event.Skip() def _hide_item(self, item): for item in item.GetChildren(): itemwindow = item.GetWindow() if itemwindow: itemwindow.Hide() if self.ItemHasChildren(item): self._hide_item(item) def SelectAllTests(self, item): self._for_all_tests(item, lambda t: self.CheckItem(t)) def SelectTests(self, tests): def foo(t): if self.GetPyData(t).controller in tests: self.CheckItem(t) self._for_all_tests(self._root, foo) def ExpandAllSubNodes(self, item): self._expand_or_collapse_nodes(item, self.Expand) def CollapseAllSubNodes(self, item): self._expand_or_collapse_nodes(item, self.Collapse) def _expand_or_collapse_nodes(self, item, callback): if not self.HasAGWFlag(customtreectrl.TR_HIDE_ROOT) or \ item != self.GetRootItem(): callback(item) for child in item.GetChildren(): self._expand_or_collapse_nodes(child, callback) def _for_all_tests(self, item, func): item_was_expanded = self.IsExpanded(item) if not self.HasAGWFlag(customtreectrl.TR_HIDE_ROOT) or \ item != self.GetRootItem(): if isinstance(item.GetData(), ResourceRootHandler or ResourceFileHandler): return is_item_expanded = self.IsExpanded(item) if not is_item_expanded: self.Expand(item) if self._is_test_node(item): func(item) if not self.IsExpanded(item): return for child in item.GetChildren(): self._for_all_tests(child, func) if not item_was_expanded: self.Collapse(item) def _for_all_drawn_tests(self, item, func): if self._is_test_node(item): func(item) for child in item.GetChildren(): self._for_all_drawn_tests(child, func) def _is_test_node(self, node): return node.GetType() == 1 def DeselectAllTests(self, item): self._for_all_tests(item, lambda t: self.CheckItem(t, checked=False)) def DeselectTests(self, tests): def foo(t): if self.GetPyData(t).controller in tests: self.CheckItem(t, checked=False) self._for_all_tests(self._root, foo) def SelectFailedTests(self, item): def func(t): # FIXME: This information should be in domain model! is_checked = self.GetItemImage(t) == FAILED_IMAGE_INDEX self.CheckItem(t, checked=is_checked) self._for_all_tests(item, func) def SelectPassedTests(self, item): def func(t): is_checked = self.GetItemImage(t) == PASSED_IMAGE_INDEX self.CheckItem(t, checked=is_checked) self._for_all_tests(item, func) def OnClose(self, event): print("DEBUG: Tree OnClose hidding") self.Hide() def OnTreeItemChecked(self, event): node = event.GetItem() handler = self._controller.get_handler(node=node) self._test_selection_controller.select(handler.controller, node.IsChecked()) def OnItemActivated(self, event): node = event.GetItem() if self.IsExpanded(node): self.Collapse(node) elif self.ItemHasChildren(node): self._expand_and_render_children(node) def OnLeftArrow(self, event): node = self.GetSelection() if self.IsExpanded(node): self.Collapse(node) else: event.Skip() def OnRightClick(self, event): handler = None if hasattr(event, 'GetItem'): handler = self._controller.get_handler(event.GetItem()) if handler: if not self.IsExpanded(handler.node): self.Expand(handler.node) handler.show_popup() def OnNewTestCase(self, event): handler = self._controller.get_handler() if handler: handler.OnNewTestCase(event) def OnDrop(self, target, dragged): dragged = self._controller.get_handler(dragged) target = self._controller.get_handler(target) if target and target.accepts_drag(dragged): dragged.controller.execute(MoveTo(target.controller)) self.Refresh() # DEBUG Always refresh def IsValidDragItem(self, item): return self._controller.get_handler(item).is_draggable def OnMoveUp(self, event): handler = self._controller.get_handler() if handler.is_draggable: handler.OnMoveUp(event) def OnMoveDown(self, event): handler = self._controller.get_handler() if handler.is_draggable: handler.OnMoveDown(event) def _item_changed(self, data): controller = data.item node = self._controller.find_node_by_controller(controller) if node: self.SetItemText(node, data.item.name) self._test_selection_controller.send_selection_changed_message() if controller.dirty: self._controller.mark_node_dirty( self._get_datafile_node(controller.datafile)) def _variable_moved_up(self, data): if self._should_update_variable_positions(data): self._do_action_if_datafile_node_is_expanded(self.move_up, data) def _variable_moved_down(self, data): if self._should_update_variable_positions(data): self._do_action_if_datafile_node_is_expanded(self.move_down, data) def _should_update_variable_positions(self, message): return message.item != message.other and message.item.has_data() and \ message.other.has_data() def _do_action_if_datafile_node_is_expanded(self, action, data): if self.IsExpanded(self._get_datafile_node(data.item.datafile)): node = self._controller.find_node_by_controller(data.item) action(node) def _variable_updated(self, data): self._item_changed(data) def highlight(self, data, text): self.select_node_by_data(data) self._editor.highlight(text) def node_is_resource_file(self, node): return self._controller.get_handler(node).__class__ == \ ResourceFileHandler
class Tree(treemixin.DragAndDrop, wx.TreeCtrl, utils.RideEventHandler): _RESOURCES_NODE_LABEL = 'External Resources' def __init__(self, parent, action_registerer): style = wx.TR_DEFAULT_STYLE if IS_WINDOWS: style = style|wx.TR_EDIT_LABELS treemixin.DragAndDrop.__init__(self, parent, style=style) actions = ActionInfoCollection(tree_actions, self, self) action_registerer.register_actions(actions) self._find_node = _FindNode(self) self._bind_tree_events() self._images = TreeImageList() self._silent_mode = False self.SetImageList(self._images) self._history = _History() self._label_editor = TreeLabelEditListener(self) self._bind_keys() self._subscribe_to_messages() self._popup_creator = PopupCreator() self._dragging = False self._clear_tree_data() self._editor = None def _bind_tree_events(self): self.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnSelChanged) self.Bind(wx.EVT_TREE_ITEM_EXPANDING, self.OnTreeItemExpanding) self.Bind(wx.EVT_TREE_ITEM_RIGHT_CLICK, self.OnRightClick) self.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self.OnItemActivated) def set_editor(self, editor): self._editor = editor def StartDragging(self): self._dragging = True treemixin.DragAndDrop.StartDragging(self) def OnEndDrag(self, event): self._dragging = False treemixin.DragAndDrop.OnEndDrag(self, event) def register_context_menu_hook(self, callable): self._popup_creator.add_hook(callable) def unregister_context_menu_hook(self, callable): self._popup_creator.remove_hook(callable) def _subscribe_to_messages(self): subscriptions = [ (self._item_changed, RideItem), (self._resource_added, RideOpenResource), (self._select_resource, RideSelectResource), (self._suite_added, RideSuiteAdded), (self._keyword_added, RideUserKeywordAdded), (self._test_added, RideTestCaseAdded), (self._variable_added, RideVariableAdded), (self._leaf_item_removed, RideUserKeywordRemoved), (self._leaf_item_removed, RideTestCaseRemoved), (self._leaf_item_removed, RideVariableRemoved), (self._datafile_removed, RideDataFileRemoved), (self._datafile_set, RideDataFileSet), (self._data_dirty, RideDataChangedToDirty), (self._data_undirty, RideDataDirtyCleared), (self._variable_moved_up, RideVariableMovedUp), (self._variable_moved_down, RideVariableMovedDown), (self._variable_updated, RideVariableUpdated), (self._filename_changed, RideFileNameChanged), ] for listener, topic in subscriptions: PUBLISHER.subscribe(listener, topic) def _bind_keys(self): bind_keys_to_evt_menu(self, self._get_bind_keys()) def _get_bind_keys(self): bindings = [ (ctrl_or_cmd(), wx.WXK_UP, self.OnMoveUp), (ctrl_or_cmd(), wx.WXK_DOWN, self.OnMoveDown), (wx.ACCEL_NORMAL, wx.WXK_F2, self._label_editor.OnLabelEdit), (wx.ACCEL_NORMAL, wx.WXK_WINDOWS_MENU, self.OnRightClick), (ctrl_or_cmd() | wx.ACCEL_SHIFT, ord('d'), lambda event: self._expanded_handler().OnSafeDelete(event)), (ctrl_or_cmd() | wx.ACCEL_SHIFT, ord('f'), lambda event: self._expanded_handler().OnNewSuite(event)), (ctrl_or_cmd() | wx.ACCEL_SHIFT, ord('k'), lambda event: self._expanded_handler().OnNewUserKeyword(event)), (ctrl_or_cmd() | wx.ACCEL_SHIFT, ord('t'), lambda event: self._expanded_handler().OnNewTestCase(event)), (ctrl_or_cmd() | wx.ACCEL_SHIFT, ord('v'), lambda event: self._expanded_handler().OnNewScalar(event)), (ctrl_or_cmd() | wx.ACCEL_SHIFT, ord('l'), lambda event: self._expanded_handler().OnNewListVariable(event)), (ctrl_or_cmd() | wx.ACCEL_SHIFT, ord('c'), lambda event: self._expanded_handler().OnCopy(event)) ] if not IS_WINDOWS: bindings.append((wx.ACCEL_NORMAL, wx.WXK_LEFT, self.OnLeftArrow)) return bindings def _expanded_handler(self): handler = self._get_handler() if not self.IsExpanded(handler.node): self.Expand(handler.node) return handler def populate(self, model): self._clear_tree_data() self._populate_model(model) self._refresh_view() self.SetFocus() # Needed for keyboard shortcuts def _clear_tree_data(self): self.DeleteAllItems() self._root = self.AddRoot('') self._resource_root = self._create_resource_root() self._datafile_nodes = [] def _create_resource_root(self): return self._create_node(self._root, self._RESOURCES_NODE_LABEL, self._images.directory) def _populate_model(self, model): self.SetPyData(self._resource_root, ResourceRootHandler(model, self, self._resource_root)) if model.data: self._render_datafile(self._root, model.data, 0) for res in model.external_resources: if not res.parent: self._render_datafile(self._resource_root, res) def _resource_added(self, message): ctrl = message.datafile if self._find_node.by_controller(ctrl): return parent = self._get_dir_node(ctrl.parent) if ctrl.parent else self._resource_root self._render_datafile(parent, ctrl) def _get_dir_node(self, ctrl): if ctrl is None: return self._root dir_node = self._get_datafile_node(ctrl.data) if dir_node is None: parent = self._get_dir_node(ctrl.parent) self._render_datafile(parent, ctrl) dir_node = self._get_datafile_node(ctrl.data) return dir_node def _select_resource(self, message): self.select_controller_node(message.item) def select_controller_node(self, controller): self.SelectItem(self._find_node.by_controller(controller)) def _suite_added(self, message): self.add_datafile(message.parent, message.suite) def _refresh_view(self): self.Refresh() if self._resource_root: self.Expand(self._resource_root) if self._datafile_nodes: self.SelectItem(self._datafile_nodes[0]) self._expand_and_render_children(self._datafile_nodes[0]) def _render_datafile(self, parent_node, controller, index=None): node = self._create_node_with_handler(parent_node, controller, index) if controller.dirty: self._mark_dirty(node) self._datafile_nodes.append(node) self.SetItemHasChildren(node, True) for child in controller.children: self._render_datafile(node, child) return node def _create_node_with_handler(self, parent_node, controller, index=None): node = self._create_node(parent_node, controller.display_name, self._images[controller], index) self.SetPyData(node, action_handler(controller, self, node)) return node def _expand_and_render_children(self, node): assert node is not None self._render_children(node) self.Expand(node) def _render_children(self, node): handler = self._get_handler(node) if not handler or not handler.can_be_rendered: return self._create_child_nodes(node, handler, lambda item: item.is_test_suite) handler.set_rendered() def _create_child_nodes(self, node, handler, predicate): for childitem in self._children_of(handler): index = self._get_insertion_index(node, predicate) self._create_node_with_handler(node, childitem, index) def _children_of(self, handler): return list(handler.variables) + list(handler.tests) + \ list(handler.keywords) def _create_node(self, parent_node, label, img, index=None): if index is not None: # blame wxPython for this ugliness if isinstance(index, int): node = self.InsertItemBefore(parent_node, index, label) else: node = self.InsertItem(parent_node, index, label) else: node = self.AppendItem(parent_node, label) self.SetItemImage(node, img.normal, wx.TreeItemIcon_Normal) self.SetItemImage(node, img.expanded, wx.TreeItemIcon_Expanded) return node def add_datafile(self, parent, suite): snode = self._render_datafile(self._get_datafile_node(parent.data), suite) self.SelectItem(snode) def add_test(self, parent_node, test): self._add_dataitem(parent_node, test, lambda item: item.is_user_keyword) def add_keyword(self, parent_node, kw): self._add_dataitem(parent_node, kw, lambda item: item.is_test_suite) def _add_dataitem(self, parent_node, dataitem, predicate): node = self._get_or_create_node(parent_node, dataitem, predicate) self._select(node) self._mark_dirty(parent_node) def _get_or_create_node(self, parent_node, dataitem, predicate): if not self.IsExpanded(parent_node): self._expand_and_render_children(parent_node) return self._find_node.with_label(parent_node, dataitem.display_name) index = self._get_insertion_index(parent_node, predicate) return self._create_node_with_handler(parent_node, dataitem, index) def _select(self, node): if node: wx.CallAfter(self.SelectItem, node) def _get_insertion_index(self, parent_node, predicate): if not predicate: return None item, cookie = self.GetFirstChild(parent_node) while item: if predicate(self._get_handler(item)): index = self.GetPrevSibling(item) if not index.IsOk: index = 0 return index item, cookie = self.GetNextChild(parent_node, cookie) return None def _keyword_added(self, message): self.add_keyword(self._get_datafile_node(self.get_selected_datafile()), message.item) def _variable_added(self, message): self._get_or_create_node(self._get_datafile_node(self.get_selected_datafile()), message.item, lambda item: not item.is_variable) def _leaf_item_removed(self, message): node = self._find_node.by_controller(message.item) self.delete_node(node) def _test_added(self, message): self.add_test(self._get_datafile_node(self.get_selected_datafile()), message.item) def _datafile_removed(self, message): dfnode = self._get_datafile_node(message.datafile.data) self._datafile_nodes.remove(dfnode) self.DeleteChildren(dfnode) self.Delete(dfnode) def _datafile_set(self, message): wx.CallAfter(self._refresh_datafile_when_file_set, message.item) def _filename_changed(self, message): df = message.datafile node = self._find_node.by_controller(df) if not node: raise AssertionError('No node found with controller "%s"' % df) wx.CallAfter(self.SetItemText, node, df.display_name) def add_keyword_controller(self, controller): parent = self._get_datafile_node(self.get_selected_datafile()) self.add_keyword(parent, controller) def delete_node(self, node): if node is None: return parent = self.GetItemParent(node) self._mark_dirty(parent) if self.IsSelected(node): wx.CallAfter(self.SelectItem, parent) wx.CallAfter(self.Delete, node) def _data_dirty(self, message): self.mark_dirty(message.datafile) def _data_undirty(self, message): self.unset_dirty() def mark_dirty(self, controller): if not controller.dirty: return node = self._find_node.by_controller(controller) if node: self._mark_dirty(node) def _mark_dirty(self, node): text = self.GetItemText(node) if not text.startswith('*'): self.SetItemText(node, '*' + text) def unset_dirty(self): for node in self._datafile_nodes: text = self.GetItemText(node) handler = self._get_handler(node) if text.startswith('*') and not handler.controller.dirty: self.SetItemText(node, text[1:]) def select_node_by_data(self, controller): '''Find and select the tree item associated with the given controller. Controller can be any of the controllers that are represented in the tree.''' parent_node = self._get_datafile_node(controller.datafile) if not parent_node: return None if not self.IsExpanded(parent_node): self._expand_and_render_children(parent_node) node = self._find_node.by_controller(controller) if node != self.GetSelection(): self.SelectItem(node) return node def select_user_keyword_node(self, uk): parent_node = self._get_datafile_node(uk.parent.parent) if not parent_node: return if not self.IsExpanded(parent_node): self._expand_and_render_children(parent_node) node = self._find_node.with_label(parent_node, utils.normalize(uk.name)) if node != self.GetSelection(): self.SelectItem(node) def _get_datafile_node(self, datafile): for node in self._datafile_nodes: if self._get_handler(node).item == datafile: return node return None def get_selected_datafile(self): """Returns currently selected data file. If a test or user keyword node is selected, returns parent of that item. """ datafile = self._get_selected_datafile_node() if not datafile: return None return self._get_handler(datafile).item def get_selected_datafile_controller(self): """Returns controller associated with currently active data file. If a test or user keyword node is selected, returns parent of that item. """ dfnode = self._get_selected_datafile_node() return self._get_handler(dfnode).controller if dfnode else None def _get_selected_datafile_node(self): node = self.GetSelection() if not node or node in (self._resource_root, self._root): return None while node not in self._datafile_nodes: node = self.GetItemParent(node) return node def get_selected_item(self): """Returns model object associated with currently selected tree node.""" selection = self.GetSelection() if not selection: return None handler = self._get_handler(selection) return handler and handler.controller or None def move_up(self, node): prev = self.GetPrevSibling(node) if prev.IsOk(): self._switch_items(prev, node, node) def move_down(self, node): next = self.GetNextSibling(node) if next.IsOk(): self._switch_items(node, next, node) def _switch_items(self, first, second, currently_selected): """Changes the order of given items, first is expected to be directly above the second""" selection = self.GetItemPyData(currently_selected).controller controller = self._get_handler(first).controller self.Delete(first) self._create_node_with_handler(self.GetItemParent(second), controller, second) self.select_node_by_data(selection) def _refresh_datafile_when_file_set(self, controller): self._start_silent_mode() #Prevent tab selections based on tree item selected events current = self.get_selected_datafile_controller() if not current: # If tree is not yet in use - do not expand anything. self._end_silent_mode() return item = self.GetSelection() current_txt = self.GetItemText(item) if item.IsOk() else '' # after refresh current and current_txt might have been changed node = self._refresh_datafile(controller) if node is None: #TODO: Find out why this sometimes happens return self._expand_and_render_children(node) if current == controller: wx.CallAfter(self.SelectItem, self._find_node.with_label(node, current_txt) or node) wx.CallAfter(self._end_silent_mode) else: self._end_silent_mode() def _start_silent_mode(self): self._silent_mode = True def _end_silent_mode(self): self._silent_mode = False def refresh_datafile(self, controller, event): to_be_selected = self._get_pending_selection(event) new_node = self._refresh_datafile(controller) self._handle_pending_selection(to_be_selected, new_node) def _refresh_datafile(self, controller): orig_node = self._get_data_controller_node(controller) if orig_node is not None: insertion_index = self._get_datafile_index(orig_node) parent = self._get_parent(orig_node) self._remove_datafile_node(orig_node) return self._render_datafile(parent, controller, insertion_index) def _get_pending_selection(self, event): if hasattr(event, 'Item'): item= event.Item event.Veto() elif hasattr(event, 'Position'): item, flags = self.HitTest(event.Position) if not (item.IsOk() and self._click_on_item(flags)): return else: return return self.GetItemText(item) def _get_data_controller_node(self, controller): for node in self._datafile_nodes: if self.GetItemPyData(node).controller == controller: return node return None def _click_on_item(self, flags): return flags & wx.TREE_HITTEST_ONITEM def _get_datafile_index(self, node): insertion_index = self.GetPrevSibling(node) if not insertion_index.IsOk(): insertion_index = 0 return insertion_index def _get_parent(self, node): return self.GetItemParent(node) def _get_handler(self, node=None): return self.GetItemPyData(node or self.Selection) def _remove_datafile_node(self, node): for child in self.GetItemChildren(node): if child in self._datafile_nodes: self._remove_datafile_node(child) self._datafile_nodes.remove(node) self.Delete(node) def _handle_pending_selection(self, to_be_selected, parent_node): if to_be_selected: self._expand_and_render_children(parent_node) wx.CallAfter(self.SelectItem, self._find_node.with_label(parent_node, to_be_selected)) def OnGoBack(self, event): node = self._history.back() if node: self.SelectItem(node) def OnGoForward(self, event): node = self._history.forward() if node: self.SelectItem(node) def OnSelChanged(self, event): node = event.Item if not node.IsOk() or self._dragging: event.Skip() return self._history.change(node) handler = self._get_handler(node) if handler and handler.item: RideTreeSelection(node=node, item=handler.controller, silent=self._silent_mode).publish() self.SetFocus() def OnTreeItemExpanding(self, event): node = event.Item if node.IsOk(): self._render_children(node) def OnItemActivated(self, event): node = event.Item if self.IsExpanded(node): self.Collapse(node) elif self.ItemHasChildren(node): self._expand_and_render_children(node) def OnLeftArrow(self, event): node = self.GetSelection() if self.IsExpanded(node): self.Collapse(node) else: event.Skip() def OnRightClick(self, event): handler = self._get_handler(event.Item if hasattr(event, 'Item') else None) if handler: if not self.IsExpanded(handler.node): self.Expand(handler.node) handler.show_popup() def OnNewTestCase(self, event): handler = self._get_handler() if handler: handler.OnNewTestCase(event) def OnDrop(self, target, dragged): dragged = self._get_handler(dragged) target = self._get_handler(target) if target and target.accepts_drag(dragged): dragged.controller.execute(MoveTo(target.controller)) def IsValidDragItem(self, item): return self._get_handler(item).is_draggable def OnMoveUp(self, event): handler = self._get_handler() if handler.is_draggable: handler.OnMoveUp(event) def OnMoveDown(self, event): handler = self._get_handler() if handler.is_draggable: handler.OnMoveDown(event) def _item_changed(self, data): controller = data.item node = self._find_node.by_controller(controller) if node: self.SetItemText(node, data.item.name) if controller.dirty: self._mark_dirty(self._get_datafile_node(controller.datafile)) def _variable_moved_up(self, data): self._do_action_if_datafile_node_is_expanded(self.move_up, data) def _variable_moved_down(self, data): self._do_action_if_datafile_node_is_expanded(self.move_down, data) def _do_action_if_datafile_node_is_expanded(self, action, data): if self.IsExpanded(self._get_datafile_node(data.item.datafile)): node = self._find_node.by_controller(data.item) action(node) def _variable_updated(self, data): self._item_changed(data) def highlight(self, data, text): self.select_node_by_data(data) self._editor.highlight(text)