Jelajahi Sumber

wxGUI: module search GUI improved (#1194)

* filtering functionality moved to treemodel
* change search modules layout
* remove unused code in goutput
* change data catalog's search widget, add cancel button, add menu to data catalog search
* change behavior also for Extensions search
Anna Petrasova 4 tahun lalu
induk
melakukan
7275e780f7

+ 55 - 14
gui/wxpython/core/treemodel.py

@@ -17,6 +17,7 @@ This program is free software under the GNU General Public License
 """
 """
 import six
 import six
 import weakref
 import weakref
+import copy
 
 
 from grass.script.utils import naturally_sort
 from grass.script.utils import naturally_sort
 
 
@@ -153,6 +154,37 @@ class TreeModel(object):
         if node.children:
         if node.children:
             naturally_sort(node.children, key=lambda node: node.label)
             naturally_sort(node.children, key=lambda node: node.label)
 
 
+    def Filtered(self, **kwargs):
+        """Filters model based on parameters in kwargs
+        that are passed to node's match function.
+        Copies tree and returns a filtered copy."""
+        def _filter(node):
+            if node.children:
+                to_remove = []
+                for child in node.children:
+                    match = _filter(child)
+                    if not match:
+                        to_remove.append(child)
+                for child in reversed(to_remove):
+                    fmodel.RemoveNode(child)
+                if node.children:
+                    return True
+            return node.match(**kwargs)
+
+        fmodel = copy.deepcopy(self)
+        _filter(fmodel.root)
+
+        return fmodel
+
+    def GetLeafCount(self, node):
+        """Returns the number of leaves in a node."""
+        if node.children:
+            count = 0
+            for child in node.children:
+                count += self.GetLeafCount(child)
+            return count
+        return 1
+
     def __str__(self):
     def __str__(self):
         """Print tree."""
         """Print tree."""
         text = []
         text = []
@@ -225,21 +257,30 @@ class ModuleNode(DictNode):
         keywords or description."""
         keywords or description."""
         if not self.data:
         if not self.data:
             return False
             return False
-        if key not in ('command', 'keywords', 'description'):
-            return False
-        try:
-            text = self.data[key]
-        except KeyError:
-            return False
-        if not text:
-            return False
-        if case_sensitive:
-            # start supported but unused, so testing last
-            return value in text or value == '*'
+        if isinstance(key, str):
+            keys = [key]
         else:
         else:
-            # this works fully only for English and requires accents
-            # to be exact match (even Python 3 casefold() does not help)
-            return value.lower() in text.lower() or value == '*'
+            keys = key
+
+        for key in keys:
+            if key not in ('command', 'keywords', 'description'):
+                return False
+            try:
+                text = self.data[key]
+            except KeyError:
+                continue
+            if not text:
+                continue
+            if case_sensitive:
+                # start supported but unused, so testing last
+                if value in text or value == '*':
+                    return True
+            else:
+                # this works fully only for English and requires accents
+                # to be exact match (even Python 3 casefold() does not help)
+                if value.lower() in text.lower() or value == '*':
+                    return True
+        return False
 
 
 
 
 def main():
 def main():

+ 2 - 2
gui/wxpython/datacatalog/catalog.py

@@ -138,5 +138,5 @@ class DataCatalog(wx.Panel):
         """Allow editing other mapsets or restrict editing to current mapset"""
         """Allow editing other mapsets or restrict editing to current mapset"""
         self.tree.SetRestriction(restrict)
         self.tree.SetRestriction(restrict)
 
 
-    def Filter(self, text):
-        self.tree.Filter(text=text)
+    def Filter(self, text, element=None):
+        self.tree.Filter(text=text, element=element)

+ 32 - 7
gui/wxpython/datacatalog/toolbars.py

@@ -16,7 +16,7 @@ This program is free software under the GNU General Public License
 
 
 import wx
 import wx
 from gui_core.toolbars import BaseToolbar
 from gui_core.toolbars import BaseToolbar
-from gui_core.wrap import StaticText, TextCtrl
+from gui_core.wrap import StaticText, SearchCtrl
 from icons.icon import MetaIcon
 from icons.icon import MetaIcon
 
 
 icons = {
 icons = {
@@ -58,16 +58,28 @@ class DataCatalogToolbar(BaseToolbar):
         BaseToolbar.__init__(self, parent)
         BaseToolbar.__init__(self, parent)
 
 
         self.InitToolbar(self._toolbarData())
         self.InitToolbar(self._toolbarData())
-        self.filter = TextCtrl(parent=self)
-        self.filter.SetSize((120, self.filter.GetBestSize()[1]))
+        self.filter_element = None
+        self.filter = SearchCtrl(parent=self)
+        self.filter.SetDescriptiveText(_('Search'))
+        self.filter.ShowCancelButton(True)
+        self.filter.SetSize((150, self.filter.GetBestSize()[1]))
         self.filter.Bind(wx.EVT_TEXT,
         self.filter.Bind(wx.EVT_TEXT,
                          lambda event: self.parent.Filter(
                          lambda event: self.parent.Filter(
-                         self.filter.GetValue()))
-        self.AddControl(StaticText(self, label=_("Search:")))
+                         self.filter.GetValue(), self.filter_element))
+        self.filter.Bind(wx.EVT_SEARCHCTRL_CANCEL_BTN,
+                         lambda evt: self.parent.Filter(''))
         self.AddControl(self.filter)
         self.AddControl(self.filter)
+        filterMenu = wx.Menu()
+        item = filterMenu.AppendRadioItem(-1, "All")
+        self.Bind(wx.EVT_MENU, self.OnFilterMenu, item)
+        item = filterMenu.AppendRadioItem(-1, "Raster maps")
+        self.Bind(wx.EVT_MENU, self.OnFilterMenu, item)
+        item = filterMenu.AppendRadioItem(-1, "Vector maps")
+        self.Bind(wx.EVT_MENU, self.OnFilterMenu, item)
+        item = filterMenu.AppendRadioItem(-1, "3D raster maps")
+        self.Bind(wx.EVT_MENU, self.OnFilterMenu, item)
+        self.filter.SetMenu(filterMenu)
         help = _("Type to search database by map type or name. "
         help = _("Type to search database by map type or name. "
-                 "Use prefix 'r:', 'v:' and 'r3:'"
-                 "to show only raster, vector or 3D raster data, respectively. "
                  "Use Python regular expressions to refine your search.")
                  "Use Python regular expressions to refine your search.")
         self.SetToolShortHelp(self.filter.GetId(), help)
         self.SetToolShortHelp(self.filter.GetId(), help)
         # realize the toolbar
         # realize the toolbar
@@ -92,6 +104,19 @@ class DataCatalogToolbar(BaseToolbar):
                                      ("addMapset", icons['addMapset'],
                                      ("addMapset", icons['addMapset'],
                                       self.parent.OnCreateMapset)
                                       self.parent.OnCreateMapset)
                                      ))
                                      ))
+    def OnFilterMenu(self, event):
+        """Decide the element to filter by"""
+        filterMenu = self.filter.GetMenu().GetMenuItems()
+        self.filter_element = None
+        if filterMenu[1].IsChecked():
+            self.filter_element = 'raster'
+        elif filterMenu[2].IsChecked():
+            self.filter_element = 'vector'
+        elif filterMenu[3].IsChecked():
+            self.filter_element = 'raster_3d'
+        # trigger filter on change
+        if self.filter.GetValue():
+            self.parent.Filter(self.filter.GetValue(), self.filter_element)
 
 
     def OnSetRestriction(self, event):
     def OnSetRestriction(self, event):
         if self.GetToolState(self.lock):
         if self.GetToolState(self.lock):

+ 30 - 72
gui/wxpython/datacatalog/tree.py

@@ -82,55 +82,6 @@ from grass.exceptions import CalledModuleError
 updateMapset, EVT_UPDATE_MAPSET = NewEvent()
 updateMapset, EVT_UPDATE_MAPSET = NewEvent()
 
 
 
 
-def filterModel(model, element=None, name=None):
-    """Filter tree model based on type or name of map using regular expressions.
-    Copies tree and remove nodes which don't match."""
-    fmodel = copy.deepcopy(model)
-    nodesToRemove = []
-    if name:
-        try:
-            regex = re.compile(name)
-        except:
-            return fmodel
-    for gisdbase in fmodel.root.children:
-        for location in gisdbase.children:
-            for mapset in location.children:
-                for layer in mapset.children:
-                    if element and layer.data['type'] != element:
-                        nodesToRemove.append(layer)
-                        continue
-                    if name and regex.search(layer.data['name']) is None:
-                        nodesToRemove.append(layer)
-
-    for node in reversed(nodesToRemove):
-        fmodel.RemoveNode(node)
-
-    cleanUpTree(fmodel)
-    return fmodel
-
-
-def cleanUpTree(model):
-    """Removes empty element/mapsets/locations nodes.
-    It first removes empty elements, then mapsets, then locations"""
-    # removes empty mapsets
-    nodesToRemove = []
-    for gisdbase in model.root.children:
-        for location in gisdbase.children:
-            for mapset in location.children:
-                if not mapset.children:
-                    nodesToRemove.append(mapset)
-    for node in reversed(nodesToRemove):
-        model.RemoveNode(node)
-    # removes empty locations
-    nodesToRemove = []
-    for gisdbase in model.root.children:
-        for location in gisdbase.children:
-            if not location.children:
-                nodesToRemove.append(location)
-    for node in reversed(nodesToRemove):
-        model.RemoveNode(node)
-
-
 def getLocationTree(gisdbase, location, queue, mapsets=None):
 def getLocationTree(gisdbase, location, queue, mapsets=None):
     """Creates dictionary with mapsets, elements, layers for given location.
     """Creates dictionary with mapsets, elements, layers for given location.
     Returns tuple with the dictionary and error (or None)"""
     Returns tuple with the dictionary and error (or None)"""
@@ -284,18 +235,32 @@ class DataCatalogNode(DictNode):
 
 
         return _("{name}").format(**data)
         return _("{name}").format(**data)
 
 
-    def match(self, **kwargs):
+    def match(self, method='exact', **kwargs):
         """Method used for searching according to given parameters.
         """Method used for searching according to given parameters.
 
 
-        :param value: dictionary value to be matched
-        :param key: data dictionary key
+        :param method: 'exact' for exact match or 'filtering' for filtering by type/name
+        :param kwargs key-value to be matched, filtering method uses 'type' and 'name'
+               where 'name' is compiled regex
         """
         """
         if not kwargs:
         if not kwargs:
             return False
             return False
 
 
-        for key in kwargs:
-            if not (key in self.data and self.data[key] == kwargs[key]):
-                return False
+        if method == 'exact':
+            for key, value in kwargs.items():
+                if not (key in self.data and self.data[key] == value):
+                    return False
+            return True
+        # for filtering            
+        if (
+            'type' in kwargs and 'type' in self.data
+            and kwargs['type'] != self.data['type']
+        ):
+            return False
+        if (
+            'name' in kwargs and 'name' in self.data
+            and not kwargs['name'].search(self.data['name'])
+        ):
+            return False
         return True
         return True
 
 
 
 
@@ -1685,28 +1650,21 @@ class DataCatalogTree(TreeView):
             wx.TheClipboard.SetData(do)
             wx.TheClipboard.SetData(do)
             wx.TheClipboard.Close()
             wx.TheClipboard.Close()
 
 
-    def Filter(self, text):
+    def Filter(self, text, element=None):
         """Filter tree based on name and type."""
         """Filter tree based on name and type."""
-        text = text.strip()
-        if len(text.split(':')) > 1:
-            name = text.split(':')[1].strip()
-            elem = text.split(':')[0].strip()
-            if 'r' == elem:
-                element = 'raster'
-            elif 'r3' == elem:
-                element = 'raster_3d'
-            elif 'v' == elem:
-                element = 'vector'
-            else:
-                element = None
+        name = text.strip()
+        if not name:
+            self._model = self._orig_model
         else:
         else:
-            element = None
-            name = text.strip()
-
-        self._model = filterModel(self._orig_model, name=name, element=element)
+            if element:
+                self._model = self._orig_model.Filtered(method='filtering', name=re.compile(name), type=element)
+            else:
+                self._model = self._orig_model.Filtered(method='filtering', name=re.compile(name))
         self.UpdateCurrentDbLocationMapsetNode()
         self.UpdateCurrentDbLocationMapsetNode()
         self.RefreshItems()
         self.RefreshItems()
         self.ExpandCurrentMapset()
         self.ExpandCurrentMapset()
+        if self._model.GetLeafCount(self._model.root) <= 50:
+            self.ExpandAll()
 
 
     def _getNewMapName(self, message, title, value, element, mapset, env):
     def _getNewMapName(self, message, title, value, element, mapset, env):
         """Dialog for simple text entry"""
         """Dialog for simple text entry"""

+ 4 - 55
gui/wxpython/gui_core/goutput.py

@@ -41,12 +41,10 @@ from gui_core.prompt import GPromptSTC
 from gui_core.wrap import Button, ClearButton, ToggleButton, StaticText, \
 from gui_core.wrap import Button, ClearButton, ToggleButton, StaticText, \
     StaticBox
     StaticBox
 from core.settings import UserSettings
 from core.settings import UserSettings
-from gui_core.widgets import SearchModuleWidget
 
 
 
 
 GC_EMPTY = 0
 GC_EMPTY = 0
-GC_SEARCH = 1
-GC_PROMPT = 2
+GC_PROMPT = 1
 
 
 
 
 class GConsoleWindow(wx.SplitterWindow):
 class GConsoleWindow(wx.SplitterWindow):
@@ -64,8 +62,7 @@ class GConsoleWindow(wx.SplitterWindow):
         :param margin: use margin in output pane (GStc)
         :param margin: use margin in output pane (GStc)
         :param style: wx.SplitterWindow style
         :param style: wx.SplitterWindow style
         :param gcstyle: GConsole style
         :param gcstyle: GConsole style
-                        (GC_EMPTY, GC_PROMPT to show command prompt,
-                        GC_SEARCH to show search widget)
+                        (GC_EMPTY, GC_PROMPT to show command prompt)
         """
         """
         wx.SplitterWindow.__init__(
         wx.SplitterWindow.__init__(
             self, parent, id=wx.ID_ANY, style=style, **kwargs)
             self, parent, id=wx.ID_ANY, style=style, **kwargs)
@@ -113,7 +110,7 @@ class GConsoleWindow(wx.SplitterWindow):
             margin=margin,
             margin=margin,
             wrap=None)
             wrap=None)
 
 
-        # search & command prompt
+        # command prompt
         # move to the if below
         # move to the if below
         # search depends on cmd prompt
         # search depends on cmd prompt
         self.cmdPrompt = GPromptSTC(
         self.cmdPrompt = GPromptSTC(
@@ -126,26 +123,6 @@ class GConsoleWindow(wx.SplitterWindow):
         if not self._gcstyle & GC_PROMPT:
         if not self._gcstyle & GC_PROMPT:
             self.cmdPrompt.Hide()
             self.cmdPrompt.Hide()
 
 
-        if self._gcstyle & GC_SEARCH:
-            self.infoCollapseLabelExp = _(
-                "Click here to show search module engine")
-            self.infoCollapseLabelCol = _(
-                "Click here to hide search module engine")
-            self.searchPane = wx.CollapsiblePane(
-                parent=self.panelOutput, label=self.infoCollapseLabelExp,
-                style=wx.CP_DEFAULT_STYLE | wx.CP_NO_TLW_RESIZE | wx.EXPAND)
-            self.MakeSearchPaneContent(
-                self.searchPane.GetPane(), self._menuModel)
-            self.searchPane.Collapse(True)
-            self.Bind(
-                wx.EVT_COLLAPSIBLEPANE_CHANGED,
-                self.OnSearchPaneChanged,
-                self.searchPane)
-            self.search.moduleSelected.connect(
-                lambda name: self.cmdPrompt.SetTextAndFocus(name + ' '))
-        else:
-            self.search = None
-
         if self._gcstyle & GC_PROMPT:
         if self._gcstyle & GC_PROMPT:
             cmdLabel = _("Command prompt")
             cmdLabel = _("Command prompt")
             self.outputBox = StaticBox(
             self.outputBox = StaticBox(
@@ -214,9 +191,6 @@ class GConsoleWindow(wx.SplitterWindow):
             promptSizer.Add(helpText,
             promptSizer.Add(helpText,
                             proportion=0, flag=wx.EXPAND | wx.LEFT, border=5)
                             proportion=0, flag=wx.EXPAND | wx.LEFT, border=5)
 
 
-        if self._gcstyle & GC_SEARCH:
-            self.outputSizer.Add(self.searchPane, proportion=0,
-                                 flag=wx.EXPAND | wx.ALL, border=3)
         self.outputSizer.Add(self.cmdOutput, proportion=1,
         self.outputSizer.Add(self.cmdOutput, proportion=1,
                              flag=wx.EXPAND | wx.ALL, border=3)
                              flag=wx.EXPAND | wx.ALL, border=3)
         if self._gcstyle & GC_PROMPT:
         if self._gcstyle & GC_PROMPT:
@@ -288,31 +262,6 @@ class GConsoleWindow(wx.SplitterWindow):
         self.SetAutoLayout(True)
         self.SetAutoLayout(True)
         self.Layout()
         self.Layout()
 
 
-    def MakeSearchPaneContent(self, pane, model):
-        """Create search pane"""
-        border = wx.BoxSizer(wx.VERTICAL)
-
-        self.search = SearchModuleWidget(parent=pane,
-                                         model=model)
-
-        self.search.showNotification.connect(self.showNotification)
-
-        border.Add(self.search, proportion=0,
-                   flag=wx.EXPAND | wx.ALL, border=1)
-
-        pane.SetSizer(border)
-        border.Fit(pane)
-
-    def OnSearchPaneChanged(self, event):
-        """Collapse search module box"""
-        if self.searchPane.IsExpanded():
-            self.searchPane.SetLabel(self.infoCollapseLabelCol)
-        else:
-            self.searchPane.SetLabel(self.infoCollapseLabelExp)
-
-        self.panelOutput.Layout()
-        self.panelOutput.SendSizeEvent()
-
     def GetPanel(self, prompt=True):
     def GetPanel(self, prompt=True):
         """Get panel
         """Get panel
 
 
@@ -822,7 +771,7 @@ class GConsoleFrame(wx.Frame):
         self.gconsole = GConsole(guiparent=self)
         self.gconsole = GConsole(guiparent=self)
         self.goutput = GConsoleWindow(parent=panel, gconsole=self.gconsole,
         self.goutput = GConsoleWindow(parent=panel, gconsole=self.gconsole,
                                       menuModel=menuTreeBuilder.GetModel(),
                                       menuModel=menuTreeBuilder.GetModel(),
-                                      gcstyle=GC_SEARCH | GC_PROMPT)
+                                      gcstyle=GC_PROMPT)
 
 
         mainSizer = wx.BoxSizer(wx.VERTICAL)
         mainSizer = wx.BoxSizer(wx.VERTICAL)
         mainSizer.Add(
         mainSizer.Add(

+ 43 - 54
gui/wxpython/gui_core/menu.py

@@ -26,9 +26,8 @@ import wx
 from core import globalvar
 from core import globalvar
 from core import utils
 from core import utils
 from core.gcmd import EncodeString
 from core.gcmd import EncodeString
-from gui_core.widgets import SearchModuleWidget
 from gui_core.treeview import CTreeView
 from gui_core.treeview import CTreeView
-from gui_core.wrap import Button, StaticText
+from gui_core.wrap import Button, StaticText, SearchCtrl
 from gui_core.wrap import Menu as MenuWidget
 from gui_core.wrap import Menu as MenuWidget
 from icons.icon import MetaIcon
 from icons.icon import MetaIcon
 
 
@@ -143,50 +142,39 @@ class SearchModuleWindow(wx.Panel):
         self.parent = parent
         self.parent = parent
         self._handlerObj = handlerObj
         self._handlerObj = handlerObj
         self._giface = giface
         self._giface = giface
+        self._model = model
 
 
         self.showNotification = Signal('SearchModuleWindow.showNotification')
         self.showNotification = Signal('SearchModuleWindow.showNotification')
         wx.Panel.__init__(self, parent=parent, id=id, **kwargs)
         wx.Panel.__init__(self, parent=parent, id=id, **kwargs)
 
 
+        # search widget
+        self._search = SearchCtrl(self)
+        self._search.SetDescriptiveText(_('Search'))
+        self._search.ShowCancelButton(True)
+        self._btnAdvancedSearch = Button(self, id=wx.ID_ANY,
+                                         label=_("Adva&nced search..."))
+        self._btnAdvancedSearch.SetToolTip(
+            _("Do advanced search using %s module") % 'g.search.module')
         # tree
         # tree
         self._tree = CTreeView(model=model, parent=self)
         self._tree = CTreeView(model=model, parent=self)
         self._tree.SetToolTip(
         self._tree.SetToolTip(
-            _("Double-click or Ctrl-Enter to run selected module"))
-
-#        self._dataBox = wx.StaticBox(parent = self, id = wx.ID_ANY,
-#                                     label = " %s " % _("Module tree"))
-
-        # search widget
-        self._search = SearchModuleWidget(parent=self,
-                                          model=model,
-                                          showChoice=False)
-        self._search.showSearchResult.connect(
-            lambda result: self._tree.Select(result))
-        self._search.showNotification.connect(self.showNotification)
-
-        self._helpText = StaticText(
-            parent=self, id=wx.ID_ANY,
-            label="Press Enter for next match, Ctrl+Enter to run command")
-        self._helpText.SetForegroundColour(
-            wx.SystemSettings.GetColour(
-                wx.SYS_COLOUR_GRAYTEXT))
+            _("Double-click to run selected module"))
 
 
         # buttons
         # buttons
-        self._btnRun = Button(self, id=wx.ID_OK, label=_("&Run"))
+        self._btnRun = Button(self, id=wx.ID_OK, label=_("&Run..."))
         self._btnRun.SetToolTip(_("Run selected module from the tree"))
         self._btnRun.SetToolTip(_("Run selected module from the tree"))
         self._btnHelp = Button(self, id=wx.ID_ANY, label=_("H&elp"))
         self._btnHelp = Button(self, id=wx.ID_ANY, label=_("H&elp"))
         self._btnHelp.SetToolTip(
         self._btnHelp.SetToolTip(
             _("Show manual for selected module from the tree"))
             _("Show manual for selected module from the tree"))
-        self._btnAdvancedSearch = Button(self, id=wx.ID_ANY,
-                                         label=_("Adva&nced search..."))
-        self._btnAdvancedSearch.SetToolTip(
-            _("Do advanced search using %s module") % 'g.search.module')
 
 
         # bindings
         # bindings
+        self._search.Bind(wx.EVT_TEXT, lambda evt: self.Filter(evt.GetString()))
+        self._search.Bind(wx.EVT_SEARCHCTRL_CANCEL_BTN,
+                          lambda evt: self.Filter(''))
         self._btnRun.Bind(wx.EVT_BUTTON, lambda evt: self.Run())
         self._btnRun.Bind(wx.EVT_BUTTON, lambda evt: self.Run())
         self._btnHelp.Bind(wx.EVT_BUTTON, lambda evt: self.Help())
         self._btnHelp.Bind(wx.EVT_BUTTON, lambda evt: self.Help())
         self._btnAdvancedSearch.Bind(wx.EVT_BUTTON,
         self._btnAdvancedSearch.Bind(wx.EVT_BUTTON,
                                      lambda evt: self.AdvancedSearch())
                                      lambda evt: self.AdvancedSearch())
-        self.Bind(wx.EVT_KEY_UP, self.OnKeyUp)
 
 
         self._tree.selectionChanged.connect(self.OnItemSelected)
         self._tree.selectionChanged.connect(self.OnItemSelected)
         self._tree.itemActivated.connect(lambda node: self.Run(node))
         self._tree.itemActivated.connect(lambda node: self.Run(node))
@@ -199,39 +187,41 @@ class SearchModuleWindow(wx.Panel):
         """Do dialog layout"""
         """Do dialog layout"""
         sizer = wx.BoxSizer(wx.VERTICAL)
         sizer = wx.BoxSizer(wx.VERTICAL)
 
 
+        # search
+        searchSizer = wx.BoxSizer(wx.HORIZONTAL)
+        searchSizer.Add(self._search, proportion=1,
+                        flag=wx.EXPAND | wx.RIGHT, border=5)
+        searchSizer.Add(self._btnAdvancedSearch, proportion=0,
+                        flag=wx.EXPAND)
+        sizer.Add(searchSizer, proportion=0,
+                  flag=wx.EXPAND | wx.ALL, border=5)
         # body
         # body
-        dataSizer = wx.BoxSizer(wx.HORIZONTAL)
-        dataSizer.Add(self._tree, proportion=1,
-                      flag=wx.EXPAND)
+        sizer.Add(self._tree, proportion=1,
+                  flag=wx.EXPAND | wx.LEFT | wx.RIGHT, border=5)
 
 
         # buttons
         # buttons
         btnSizer = wx.BoxSizer(wx.HORIZONTAL)
         btnSizer = wx.BoxSizer(wx.HORIZONTAL)
-        btnSizer.Add(self._btnAdvancedSearch, proportion=0)
         btnSizer.AddStretchSpacer()
         btnSizer.AddStretchSpacer()
-        btnSizer.Add(self._btnHelp, proportion=0)
+        btnSizer.Add(self._btnHelp, proportion=0,
+                     flag=wx.EXPAND | wx.RIGHT, border=5)
         btnSizer.Add(self._btnRun, proportion=0)
         btnSizer.Add(self._btnRun, proportion=0)
 
 
-        sizer.Add(dataSizer, proportion=1,
-                  flag=wx.EXPAND | wx.ALL, border=5)
-
-        sizer.Add(self._search, proportion=0,
-                  flag=wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, border=5)
-
         sizer.Add(btnSizer, proportion=0,
         sizer.Add(btnSizer, proportion=0,
-                  flag=wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, border=5)
-
-        sizer.Add(self._helpText,
-                  proportion=0, flag=wx.EXPAND | wx.LEFT, border=5)
-
-        sizer.Fit(self)
-        sizer.SetSizeHints(self)
-
-        self.SetSizer(sizer)
+                  flag=wx.EXPAND | wx.ALL, border=5)
 
 
-        self.Fit()
+        self.SetSizerAndFit(sizer)
         self.SetAutoLayout(True)
         self.SetAutoLayout(True)
         self.Layout()
         self.Layout()
 
 
+    def Filter(self, text):
+        if text:
+            model = self._model.Filtered(key=['command', 'keywords', 'description'],
+                                         value=text)
+            self._tree.SetModel(model)
+            self._tree.ExpandAll()
+        else:
+            self._tree.SetModel(self._model)
+
     def _GetSelectedNode(self):
     def _GetSelectedNode(self):
         selection = self._tree.GetSelected()
         selection = self._tree.GetSelected()
         if not selection:
         if not selection:
@@ -251,6 +241,11 @@ class SearchModuleWindow(wx.Panel):
         data = node.data
         data = node.data
         # non-leaf nodes
         # non-leaf nodes
         if not data:
         if not data:
+            # expand/collapse location/mapset...
+            if self._tree.IsNodeExpanded(node):
+                self._tree.CollapseNode(node, recursive=False)
+            else:
+                self._tree.ExpandNode(node, recursive=False)
             return
             return
 
 
         # extract name of the handler and create a new call
         # extract name of the handler and create a new call
@@ -287,12 +282,6 @@ class SearchModuleWindow(wx.Panel):
         """Show advanced search window"""
         """Show advanced search window"""
         self._handlerObj.RunMenuCmd(cmd=['g.search.modules'])
         self._handlerObj.RunMenuCmd(cmd=['g.search.modules'])
 
 
-    def OnKeyUp(self, event):
-        """Key or key combination pressed"""
-        if event.ControlDown() and \
-                event.GetKeyCode() in (wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER):
-            self.Run()
-
     def OnItemSelected(self, node):
     def OnItemSelected(self, node):
         """Item selected"""
         """Item selected"""
         data = node.data
         data = node.data

+ 14 - 0
gui/wxpython/gui_core/treeview.py

@@ -135,6 +135,20 @@ class AbstractTreeViewMixin(VirtualTree):
             self.Expand(item)
             self.Expand(item)
         self.EnsureVisible(item)
         self.EnsureVisible(item)
 
 
+    def ExpandAll(self):
+        """Expand all items.
+        """
+        def _expand(item, root=False):
+            if not root:
+                self.Expand(item)
+            child, cookie = self.GetFirstChild(item)
+            while child:
+                _expand(child)
+                child, cookie = self.GetNextChild(item, cookie)
+
+        item = self.GetRootItem()
+        _expand(item, True)
+
     def IsNodeExpanded(self, node):
     def IsNodeExpanded(self, node):
         """Check if node is expanded"""
         """Check if node is expanded"""
         index = self._model.GetIndexOfNode(node)
         index = self._model.GetIndexOfNode(node)

+ 19 - 15
gui/wxpython/modules/extensions.py

@@ -30,10 +30,10 @@ from core.gcmd import GError, RunCommand, GException, GMessage
 from core.utils import SetAddOnPath
 from core.utils import SetAddOnPath
 from core.gthread import gThread
 from core.gthread import gThread
 from core.menutree import TreeModel, ModuleNode
 from core.menutree import TreeModel, ModuleNode
-from gui_core.widgets import GListCtrl, SearchModuleWidget
+from gui_core.widgets import GListCtrl
 from gui_core.treeview import CTreeView
 from gui_core.treeview import CTreeView
 from core.toolboxes import toolboxesOutdated
 from core.toolboxes import toolboxesOutdated
-from gui_core.wrap import Button, StaticBox, TextCtrl, Menu, NewId
+from gui_core.wrap import Button, StaticBox, TextCtrl, Menu, NewId, SearchCtrl
 
 
 
 
 class InstallExtensionWindow(wx.Frame):
 class InstallExtensionWindow(wx.Frame):
@@ -70,15 +70,9 @@ class InstallExtensionWindow(wx.Frame):
             parent=self.panel,
             parent=self.panel,
             model=self.modelBuilder.GetModel())
             model=self.modelBuilder.GetModel())
 
 
-        self.search = SearchModuleWidget(
-            parent=self.panel,
-            model=self.modelBuilder.GetModel(),
-            showChoice=False)
-        self.search.showSearchResult.connect(
-            lambda result: self.tree.Select(result))
-        # show text in statusbar when notification appears
-        self.search.showNotification.connect(
-            lambda message: self.SetStatusText(message))
+        self.search = SearchCtrl(self.panel)
+        self.search.SetDescriptiveText(_('Search'))
+        self.search.ShowCancelButton(True)
         # load data in different thread
         # load data in different thread
         self.thread = gThread()
         self.thread = gThread()
 
 
@@ -127,6 +121,9 @@ class InstallExtensionWindow(wx.Frame):
         # self.btnFetch.Bind(wx.EVT_BUTTON, self.OnFetch)
         # self.btnFetch.Bind(wx.EVT_BUTTON, self.OnFetch)
         self.btnInstall.Bind(wx.EVT_BUTTON, self.OnInstall)
         self.btnInstall.Bind(wx.EVT_BUTTON, self.OnInstall)
         self.btnHelp.Bind(wx.EVT_BUTTON, self.OnHelp)
         self.btnHelp.Bind(wx.EVT_BUTTON, self.OnHelp)
+        self.search.Bind(wx.EVT_TEXT, lambda evt: self.Filter(evt.GetString()))
+        self.search.Bind(wx.EVT_SEARCHCTRL_CANCEL_BTN,
+                         lambda evt: self.Filter(''))
         self.tree.selectionChanged.connect(self.OnItemSelected)
         self.tree.selectionChanged.connect(self.OnItemSelected)
         self.tree.itemActivated.connect(self.OnItemActivated)
         self.tree.itemActivated.connect(self.OnItemActivated)
         self.tree.contextMenu.connect(self.OnContextMenu)
         self.tree.contextMenu.connect(self.OnContextMenu)
@@ -147,8 +144,7 @@ class InstallExtensionWindow(wx.Frame):
         # repoSizer.Add(repo1Sizer,
         # repoSizer.Add(repo1Sizer,
         #               flag=wx.EXPAND)
         #               flag=wx.EXPAND)
 
 
-        findSizer = wx.BoxSizer(wx.HORIZONTAL)
-        findSizer.Add(self.search, proportion=1)
+        sizer.Add(self.search, proportion=0, flag=wx.EXPAND | wx.ALL, border=3)
 
 
         treeSizer = wx.StaticBoxSizer(self.treeBox, wx.HORIZONTAL)
         treeSizer = wx.StaticBoxSizer(self.treeBox, wx.HORIZONTAL)
         treeSizer.Add(self.tree, proportion=1,
         treeSizer.Add(self.tree, proportion=1,
@@ -168,8 +164,6 @@ class InstallExtensionWindow(wx.Frame):
 
 
         # sizer.Add(repoSizer, proportion=0,
         # sizer.Add(repoSizer, proportion=0,
         #           flag=wx.ALL | wx.EXPAND, border=3)
         #           flag=wx.ALL | wx.EXPAND, border=3)
-        sizer.Add(findSizer, proportion=0,
-                  flag=wx.ALL | wx.EXPAND, border=3)
         sizer.Add(treeSizer, proportion=1,
         sizer.Add(treeSizer, proportion=1,
                   flag=wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.EXPAND, border=3)
                   flag=wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.EXPAND, border=3)
         sizer.Add(optionSizer, proportion=0,
         sizer.Add(optionSizer, proportion=0,
@@ -227,6 +221,16 @@ class InstallExtensionWindow(wx.Frame):
         self.SetStatusText(_("%d extensions loaded") % nitems, 0)
         self.SetStatusText(_("%d extensions loaded") % nitems, 0)
         wx.EndBusyCursor()
         wx.EndBusyCursor()
 
 
+    def Filter(self, text):
+        model = self.modelBuilder.GetModel()
+        if text:
+            model = model.Filtered(key=['command', 'keywords', 'description'],
+                                   value=text)
+            self.tree.SetModel(model)
+            self.tree.ExpandAll()
+        else:
+            self.tree.SetModel(model)
+
     def OnContextMenu(self, node):
     def OnContextMenu(self, node):
         if not hasattr(self, "popupID"):
         if not hasattr(self, "popupID"):
             self.popupID = dict()
             self.popupID = dict()