Laurent@814: #!/usr/bin/env python Laurent@814: # -*- coding: utf-8 -*- Laurent@814: andrej@1571: # This file is part of Beremiz, a Integrated Development Environment for andrej@1571: # programming IEC 61131-3 automates supporting plcopen standard and CanFestival. Laurent@814: # andrej@1571: # Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD Laurent@814: # andrej@1571: # See COPYING file for copyrights details. Laurent@814: # andrej@1571: # This program is free software; you can redistribute it and/or andrej@1571: # modify it under the terms of the GNU General Public License andrej@1571: # as published by the Free Software Foundation; either version 2 andrej@1571: # of the License, or (at your option) any later version. Laurent@814: # andrej@1571: # This program is distributed in the hope that it will be useful, andrej@1571: # but WITHOUT ANY WARRANTY; without even the implied warranty of andrej@1571: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the andrej@1571: # GNU General Public License for more details. Laurent@814: # andrej@1571: # You should have received a copy of the GNU General Public License andrej@1571: # along with this program; if not, write to the Free Software andrej@1571: # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Laurent@814: Laurent@814: import wx Laurent@814: Laurent@814: #------------------------------------------------------------------------------- Laurent@814: # Helpers Laurent@814: #------------------------------------------------------------------------------- Laurent@814: Laurent@814: [CATEGORY, BLOCK] = range(2) Laurent@814: Laurent@814: #------------------------------------------------------------------------------- Laurent@814: # Library Panel Laurent@814: #------------------------------------------------------------------------------- Laurent@814: Laurent@1230: Laurent@814: class LibraryPanel(wx.Panel): andrej@1736: """ andrej@1736: Class that implements a panel displaying a tree containing an hierarchical list andrej@1736: of functions and function blocks available in project an a search control for andrej@1736: quickly find one functions or function blocks in this list and a text control andrej@1736: displaying informations about selected functions or function blocks andrej@1736: """ andrej@1730: Laurent@814: def __init__(self, parent, enable_drag=False): Laurent@1230: """ Laurent@1230: Constructor Laurent@1230: @param parent: Parent wx.Window of LibraryPanel Laurent@1230: @param enable_drag: Flag indicating that function or function block can Laurent@1230: be drag'n drop from LibraryPanel (default: False) Laurent@1230: """ Laurent@814: wx.Panel.__init__(self, parent, style=wx.TAB_TRAVERSAL) andrej@1730: Laurent@1230: # Define LibraryPanel main sizer Laurent@814: main_sizer = wx.FlexGridSizer(cols=1, hgap=0, rows=2, vgap=0) Laurent@814: main_sizer.AddGrowableCol(0) Laurent@814: main_sizer.AddGrowableRow(1) andrej@1730: Laurent@1230: # Add SearchCtrl to main sizer Laurent@814: self.SearchCtrl = wx.SearchCtrl(self) Laurent@1230: # Add a button with a magnifying glass, essentially to show that this Laurent@1230: # control is for searching in tree Laurent@814: self.SearchCtrl.ShowSearchButton(True) Laurent@814: self.Bind(wx.EVT_TEXT, self.OnSearchCtrlChanged, self.SearchCtrl) andrej@1730: self.Bind(wx.EVT_SEARCHCTRL_SEARCH_BTN, Laurent@1230: self.OnSearchButtonClick, self.SearchCtrl) Laurent@1230: # Bind keyboard event on SearchCtrl text control to catch UP and DOWN Laurent@1230: # for search previous and next occurrence alexander@1528: alexander@1528: # This protects from fail to start when no children[0] available (possible for wxPython 3.0) alexander@1528: if self.SearchCtrl.GetChildren(): alexander@1528: search_textctrl = self.SearchCtrl.GetChildren()[0] alexander@1528: search_textctrl.Bind(wx.EVT_CHAR, self.OnKeyDown) alexander@1528: Laurent@814: main_sizer.AddWindow(self.SearchCtrl, flag=wx.GROW) andrej@1730: Laurent@1230: # Add Splitter window for tree and block comment to main sizer Laurent@814: splitter_window = wx.SplitterWindow(self) Laurent@814: splitter_window.SetSashGravity(1.0) Laurent@814: main_sizer.AddWindow(splitter_window, flag=wx.GROW) andrej@1730: Laurent@1230: # Add TreeCtrl for functions and function blocks library in splitter Laurent@1230: # window Laurent@814: self.Tree = wx.TreeCtrl(splitter_window, andrej@1730: size=wx.Size(0, 0), Laurent@814: style=wx.TR_HAS_BUTTONS| Laurent@814: wx.TR_SINGLE| Laurent@814: wx.SUNKEN_BORDER| Laurent@814: wx.TR_HIDE_ROOT| Laurent@814: wx.TR_LINES_AT_ROOT) Laurent@814: self.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnTreeItemSelected, self.Tree) Laurent@814: self.Tree.Bind(wx.EVT_CHAR, self.OnKeyDown) Laurent@1230: # If drag'n drop is enabled, bind event generated when a drag begins on Laurent@1230: # tree to start a drag'n drop Laurent@814: if enable_drag: Laurent@814: self.Bind(wx.EVT_TREE_BEGIN_DRAG, self.OnTreeBeginDrag, self.Tree) andrej@1730: Laurent@1230: # Add TextCtrl for function and function block informations andrej@1730: self.Comment = wx.TextCtrl(splitter_window, size=wx.Size(0, 80), Laurent@814: style=wx.TE_READONLY|wx.TE_MULTILINE) andrej@1730: Laurent@814: splitter_window.SplitHorizontally(self.Tree, self.Comment, -80) andrej@1730: Laurent@814: self.SetSizer(main_sizer) andrej@1730: Laurent@1230: # Reference to the project controller Laurent@814: self.Controller = None andrej@1730: Laurent@1230: # Variable storing functions and function blocks library to display Laurent@814: self.BlockList = None andrej@1730: Laurent@814: def __del__(self): Laurent@1230: """ Laurent@1230: Destructor Laurent@1230: """ Laurent@1230: # Remove reference to project controller Laurent@814: self.Controller = None andrej@1730: Laurent@814: def SetController(self, controller): Laurent@1230: """ Laurent@1230: Set reference to project controller Laurent@1230: @param controller: Reference to project controller Laurent@1230: """ Laurent@814: self.Controller = controller andrej@1730: Laurent@814: def SetBlockList(self, blocklist): Laurent@1230: """ Laurent@1230: Set function and function block library to display in TreeCtrl Laurent@1230: @param blocklist: Function and function block library Laurent@1230: """ Laurent@1230: # Save functions and function blocks library Laurent@814: self.BlockList = blocklist Laurent@1230: # Refresh TreeCtrl values Laurent@814: self.RefreshTree() andrej@1730: Laurent@814: def SetFocus(self): Laurent@1230: """ Laurent@1230: Called to give focus to LibraryPanel andrej@1730: Override wx.Window SetFocus method Laurent@1230: """ Laurent@1230: # Give focus to SearchCtrl Laurent@814: self.SearchCtrl.SetFocus() andrej@1730: Laurent@814: def ResetTree(self): Laurent@1230: """ Laurent@1230: Reset LibraryPanel values displayed in controls Laurent@1230: """ Laurent@1230: # Clear SearchCtrl, TreeCtrl and TextCtrl Laurent@814: self.SearchCtrl.SetValue("") Laurent@814: self.Tree.DeleteAllItems() Laurent@814: self.Comment.SetValue("") andrej@1730: Laurent@814: def RefreshTree(self): Laurent@1230: """ Laurent@1230: Refresh LibraryPanel values displayed in controls Laurent@1230: """ Laurent@1230: # Get function and function blocks library Laurent@1230: blocktypes = self.BlockList Laurent@1230: if blocktypes is None and self.Controller is not None: Laurent@1230: # Get library from project controller if not defined Laurent@1230: blocktypes = self.Controller.GetBlockTypes() andrej@1730: Laurent@1230: # Refresh TreeCtrl values if a library is defined Laurent@1230: if blocktypes is not None: Laurent@1230: # List that will contain tree items to be deleted when TreeCtrl Laurent@1230: # will be refreshed Laurent@1230: items_to_delete = [] andrej@1730: Laurent@1230: # Get current selected item for selected it when values refreshed Laurent@1230: selected_item = self.Tree.GetSelection() Laurent@1230: selected_pydata = (self.Tree.GetPyData(selected_item) Laurent@1230: if selected_item.IsOk() and Laurent@1230: selected_item != self.Tree.GetRootItem() Laurent@1230: else None) Laurent@1230: # Don't save selected item if it is a category Laurent@1230: selected_infos = ((self.Tree.GetItemText(selected_item), Laurent@1230: selected_pydata["inputs"]) Laurent@1230: if selected_pydata is not None and Laurent@1230: selected_pydata["type"] == BLOCK Laurent@1230: else (None, None)) andrej@1730: Laurent@1230: # Get TreeCtrl root item (hidden) Laurent@814: root = self.Tree.GetRootItem() Laurent@814: if not root.IsOk(): Laurent@1230: # Create root if not present Laurent@814: root = self.Tree.AddRoot("") andrej@1730: Laurent@1230: # Iterate over functions and function blocks library categories and Laurent@1230: # add a tree item to root item for each of them andrej@1730: Laurent@1230: # Get first child under root item Laurent@814: category_item, root_cookie = self.Tree.GetFirstChild(root) Laurent@814: for category in blocktypes: Laurent@1230: # Store category name in a local variable to prevent script Laurent@1230: # extracting translated strings for gettext to consider "name" Laurent@1230: # to be translated Laurent@814: category_name = category["name"] andrej@1730: Laurent@1230: # Tree item already exists, set item label Laurent@1230: if category_item.IsOk(): Laurent@1230: self.Tree.SetItemText(category_item, _(category_name)) andrej@1730: Laurent@1230: # Tree item doesn't exist, add new one to root Laurent@1230: else: Laurent@814: category_item = self.Tree.AppendItem(root, _(category_name)) Laurent@1230: # On Windows, needs to get next child of root to have a Laurent@1230: # reference to the newly added tree item Laurent@814: if wx.Platform != '__WXMSW__': Laurent@1230: category_item, root_cookie = \ Laurent@1230: self.Tree.GetNextChild(root, root_cookie) andrej@1730: andrej@1730: # Set data associated to tree item (only save that item is a Laurent@1230: # category) Laurent@814: self.Tree.SetPyData(category_item, {"type" : CATEGORY}) andrej@1730: Laurent@1230: # Iterate over functions and function blocks defined in library andrej@1730: # category add a tree item to category tree item for each of Laurent@1230: # them andrej@1730: Laurent@1230: # Get first child under category tree item Laurent@1230: blocktype_item, category_cookie = \ Laurent@1230: self.Tree.GetFirstChild(category_item) Laurent@814: for blocktype in category["list"]: andrej@1730: Laurent@1230: # Tree item already exists, set item label Laurent@1230: if blocktype_item.IsOk(): Laurent@1230: self.Tree.SetItemText(blocktype_item, blocktype["name"]) andrej@1730: Laurent@1230: # Tree item doesn't exist, add new one to category item Laurent@1230: else: Laurent@1230: blocktype_item = self.Tree.AppendItem( Laurent@1230: category_item, blocktype["name"]) Laurent@1230: # See comment when adding category Laurent@814: if wx.Platform != '__WXMSW__': Laurent@1230: blocktype_item, category_cookie = \ andrej@1730: self.Tree.GetNextChild(category_item, Laurent@1230: category_cookie) andrej@1730: Laurent@1230: # Define data to associate to block tree item Laurent@1230: comment = blocktype["comment"] andrej@1730: block_data = {"type" : BLOCK, andrej@1730: "block_type" : blocktype["type"], andrej@1730: "inputs" : tuple([type Laurent@1230: for name, type, modifier andrej@1730: in blocktype["inputs"]]), Laurent@1230: "extension" : (len(blocktype["inputs"]) Laurent@1230: if blocktype["extensible"] Laurent@1230: else None), andrej@1730: "comment": _(comment) + Laurent@1230: blocktype.get("usage", "")} Laurent@814: self.Tree.SetPyData(blocktype_item, block_data) andrej@1730: Laurent@1230: # Select block tree item in tree if it corresponds to Laurent@1230: # previously selected one andrej@1730: if selected_infos == (blocktype["name"], Laurent@1230: blocktype["inputs"]): Laurent@814: self.Tree.SelectItem(blocktype_item) andrej@1730: Laurent@1230: # Update TextCtrl value Laurent@1230: self.Comment.SetValue(block_data["comment"]) andrej@1730: Laurent@1230: # Get next block tree item under category tree item Laurent@1230: blocktype_item, category_cookie = \ Laurent@1230: self.Tree.GetNextChild(category_item, category_cookie) andrej@1730: Laurent@1230: # Add every remaining tree item under category tree item after Laurent@1230: # updating all block items to the list of items to delete Laurent@814: while blocktype_item.IsOk(): Laurent@1230: items_to_delete.append(blocktype_item) Laurent@1230: blocktype_item, category_cookie = \ Laurent@1230: self.Tree.GetNextChild(category_item, category_cookie) andrej@1730: Laurent@1230: # Get next category tree item under root item Laurent@1230: category_item, root_cookie = \ Laurent@1230: self.Tree.GetNextChild(root, root_cookie) andrej@1730: andrej@1730: # Add every remaining tree item under root item after updating all Laurent@1230: # category items to the list of items to delete Laurent@814: while category_item.IsOk(): Laurent@1230: items_to_delete.append(category_item) Laurent@1230: category_item, root_cookie = \ Laurent@1230: self.Tree.GetNextChild(root, root_cookie) andrej@1730: Laurent@1230: # Remove all items in list of items to delete from TreeCtrl Laurent@1230: for item in items_to_delete: Laurent@814: self.Tree.Delete(item) andrej@1730: Laurent@814: def GetSelectedBlock(self): Laurent@1230: """ Laurent@1230: Get selected block informations Laurent@1230: @return: {"type": block_type_name, "inputs": [input_type,...]} or None Laurent@1230: if no block selected Laurent@1230: """ Laurent@1230: # Get selected item associated data in tree Laurent@1230: selected_item = self.Tree.GetSelection() Laurent@1230: selected_pydata = (self.Tree.GetPyData(selected_item) Laurent@1230: if selected_item.IsOk() and Laurent@1230: selected_item != self.Tree.GetRootItem() Laurent@1230: else None) andrej@1730: Laurent@1230: # Return value is None if selected tree item is root or a category andrej@1730: return ({"type": self.Tree.GetItemText(selected_item), Laurent@1230: "inputs": selected_pydata["inputs"]} andrej@1730: if selected_pydata is not None and Laurent@1230: selected_pydata["type"] == BLOCK Laurent@1230: else None) andrej@1730: Laurent@814: def SelectTreeItem(self, name, inputs): Laurent@1230: """ andrej@1730: Select Tree item corresponding to block informations given Laurent@1230: @param name: Block type name Laurent@1230: @param inputs: List of block inputs type [input_type,...] Laurent@1230: """ Laurent@1230: # Find tree item corresponding to block informations Laurent@814: item = self.FindTreeItem(self.Tree.GetRootItem(), name, inputs) Laurent@814: if item is not None and item.IsOk(): Laurent@1230: # Select tree item found Laurent@814: self.Tree.SelectItem(item) Laurent@814: self.Tree.EnsureVisible(item) andrej@1730: Laurent@1230: def FindTreeItem(self, item, name, inputs = None): Laurent@1230: """ Laurent@1230: Find Tree item corresponding to block informations given Laurent@1230: Function is recursive Laurent@1230: @param item: Item to test Laurent@1230: @param name: Block type name Laurent@1230: @param inputs: List of block inputs type [input_type,...] Laurent@1230: """ Laurent@1230: # Return immediately if item isn't valid Laurent@1230: if not item.IsOk(): Laurent@1230: return None andrej@1730: Laurent@1230: # Get data associated to item to test Laurent@1230: item_pydata = self.Tree.GetPyData(item) Laurent@1230: if item_pydata is not None and item_pydata["type"] == BLOCK: Laurent@1230: # Only test item corresponding to block andrej@1730: Laurent@1230: # Test if block inputs type are the same than those given Laurent@1230: type_inputs = item_pydata.get("inputs", None) Laurent@1230: type_extension = item_pydata.get("extension", None) Laurent@1230: if inputs is not None and type_inputs is not None: Laurent@1230: same_inputs = reduce( Laurent@1230: lambda x, y: x and y, Laurent@1230: map( Laurent@1230: lambda x: x[0]==x[1] or x[0]=='ANY' or x[1]=='ANY', Laurent@1230: zip(type_inputs, Laurent@1230: (inputs[:type_extension] Laurent@1230: if type_extension is not None Laurent@1230: else inputs))), Laurent@1230: True) Laurent@814: else: Laurent@1230: same_inputs = True andrej@1730: Laurent@1230: # Return item if block data corresponds to informations given Laurent@1230: if self.Tree.GetItemText(item) == name and same_inputs: Laurent@1230: return item andrej@1730: Laurent@1230: # Test item children if item doesn't correspond Laurent@1230: child, child_cookie = self.Tree.GetFirstChild(item) Laurent@1230: while child.IsOk(): Laurent@1230: result = self.FindTreeItem(child, name, inputs) Laurent@1230: if result: Laurent@1230: return result Laurent@1230: child, child_cookie = self.Tree.GetNextChild(item, child_cookie) andrej@1730: Laurent@814: return None andrej@1730: Laurent@814: def SearchInTree(self, value, mode="first"): Laurent@1230: """ Laurent@1230: Search in Tree and select item that name contains string given Laurent@1230: @param value: String contained in block name to find Laurent@1230: @param mode: Search mode ('first', 'previous' or 'next') Laurent@1230: (default: 'first') Laurent@1230: @return: True if an item was found Laurent@1230: """ Laurent@1230: # Return immediately if root isn't valid Laurent@814: root = self.Tree.GetRootItem() Laurent@814: if not root.IsOk(): Laurent@814: return False andrej@1730: Laurent@1230: # Set function to navigate in Tree item sibling according to search andrej@1730: # mode defined Laurent@1230: sibling_function = (self.Tree.GetPrevSibling Laurent@1230: if mode == "previous" Laurent@1230: else self.Tree.GetNextSibling) andrej@1730: Laurent@1230: # Get current selected item (for next and previous mode) Laurent@1230: item = self.Tree.GetSelection() Laurent@1230: if not item.IsOk() or mode == "first": Laurent@814: item, item_cookie = self.Tree.GetFirstChild(root) Laurent@814: selected = None Laurent@814: else: Laurent@814: selected = item andrej@1730: Laurent@1230: # Navigate through tree items until one matching found or reach tree Laurent@1230: # starting or ending Laurent@814: while item.IsOk(): andrej@1730: Laurent@1230: # Get item data to get item type Laurent@814: item_pydata = self.Tree.GetPyData(item) andrej@1730: Laurent@1230: # Item is a block category andrej@1501: if (item == root) or item_pydata["type"] == CATEGORY: andrej@1730: andrej@1730: # Get category first or last child according to search mode Laurent@1230: # defined Laurent@1230: child = (self.Tree.GetLastChild(item) Laurent@1230: if mode == "previous" Laurent@1230: else self.Tree.GetFirstChild(item)[0]) andrej@1730: Laurent@1230: # If category has no child, go to sibling category Laurent@1230: item = (child if child.IsOk() else sibling_function(item)) andrej@1730: Laurent@1230: # Item is a block Laurent@814: else: andrej@1730: Laurent@1230: # Extract item block name Laurent@814: name = self.Tree.GetItemText(item) Laurent@1230: # Test if block name contains string given Laurent@1068: if name.upper().find(value.upper()) != -1 and item != selected: Laurent@1230: # Select block and collapse all categories other than block Laurent@1230: # category Laurent@1235: child, child_cookie = self.Tree.GetFirstChild(root) Laurent@1235: while child.IsOk(): Laurent@1235: self.Tree.CollapseAllChildren(child) Laurent@1235: child, child_cookie = self.Tree.GetNextChild(root, child_cookie) Laurent@814: self.Tree.SelectItem(item) Laurent@814: self.Tree.EnsureVisible(item) Laurent@814: return True andrej@1730: Laurent@1230: # Go to next item sibling if block not found Laurent@1230: next = sibling_function(item) andrej@1730: Laurent@1230: # If category has no other child, go to next category sibling Laurent@1230: item = (next Laurent@1230: if next.IsOk() Laurent@1230: else sibling_function(self.Tree.GetItemParent(item))) andrej@1730: Laurent@814: return False andrej@1730: Laurent@814: def OnSearchCtrlChanged(self, event): Laurent@1230: """ Laurent@1230: Called when SearchCtrl text control value changed Laurent@1230: @param event: TextCtrl change event Laurent@1230: """ Laurent@1230: # Search for block containing SearchCtrl value in 'first' mode Laurent@814: self.SearchInTree(self.SearchCtrl.GetValue()) Laurent@814: event.Skip() andrej@1730: Laurent@814: def OnSearchButtonClick(self, event): Laurent@1230: """ Laurent@1230: Called when SearchCtrl search button was clicked Laurent@1230: @param event: Button clicked event Laurent@1230: """ Laurent@1230: # Search for block containing SearchCtrl value in 'next' mode Laurent@814: self.SearchInTree(self.SearchCtrl.GetValue(), "next") Laurent@814: event.Skip() andrej@1730: Laurent@814: def OnTreeItemSelected(self, event): Laurent@1230: """ Laurent@1230: Called when tree item is selected Laurent@1230: @param event: wx.TreeEvent Laurent@1230: """ Laurent@1230: # Update TextCtrl value with block selected usage Laurent@1230: item_pydata = self.Tree.GetPyData(event.GetItem()) Laurent@1230: self.Comment.SetValue( Laurent@1230: item_pydata["comment"] Laurent@1230: if item_pydata is not None and item_pydata["type"] == BLOCK Laurent@1230: else "") andrej@1730: Laurent@1230: # Call extra function defined when tree item is selected Laurent@814: if getattr(self, "_OnTreeItemSelected", None) is not None: Laurent@814: self._OnTreeItemSelected(event) andrej@1730: Laurent@814: event.Skip() andrej@1730: Laurent@814: def OnTreeBeginDrag(self, event): Laurent@1230: """ Laurent@1230: Called when a drag is started in tree Laurent@1230: @param event: wx.TreeEvent Laurent@1230: """ Laurent@1230: selected_item = event.GetItem() Laurent@1230: item_pydata = self.Tree.GetPyData(selected_item) andrej@1730: Laurent@1230: # Item dragged is a block Laurent@1230: if item_pydata is not None and item_pydata["type"] == BLOCK: Laurent@1230: # Start a drag'n drop Laurent@1230: data = wx.TextDataObject(str( andrej@1730: (self.Tree.GetItemText(selected_item), andrej@1730: item_pydata["block_type"], andrej@1730: "", Laurent@1230: item_pydata["inputs"]))) Laurent@814: dragSource = wx.DropSource(self.Tree) Laurent@814: dragSource.SetData(data) Laurent@814: dragSource.DoDragDrop() andrej@1730: Laurent@814: def OnKeyDown(self, event): Laurent@1230: """ Laurent@1230: Called when key is pressed in SearchCtrl text control Laurent@1230: @param event: wx.KeyEvent Laurent@1230: """ Laurent@1230: # Get event keycode and value in SearchCtrl Laurent@814: keycode = event.GetKeyCode() Laurent@814: search_value = self.SearchCtrl.GetValue() andrej@1730: Laurent@1230: # Up key was pressed and SearchCtrl isn't empty, search for block in andrej@1730: # 'previous' mode Laurent@814: if keycode == wx.WXK_UP and search_value != "": Laurent@814: self.SearchInTree(search_value, "previous") andrej@1730: Laurent@1230: # Down key was pressed and SearchCtrl isn't empty, search for block in andrej@1730: # 'next' mode Laurent@814: elif keycode == wx.WXK_DOWN and search_value != "": Laurent@814: self.SearchInTree(search_value, "next") andrej@1730: Laurent@1230: # Handle key normally Laurent@814: else: Laurent@814: event.Skip()