""" @package gui_core.simplelmgr @brief GUI class for simple layer management. Classes: - simplelmgr::SimpleLayerManager - simplelmgr::SimpleLmgrToolbar (C) 2013 by the GRASS Development Team This program is free software under the GNU General Public License (>=v2). Read the file COPYING that comes with GRASS for details. @author Anna Petrasova (kratochanna gmail.com) """ import wx import wx.aui from grass.pydispatch.signal import Signal # needed just for testing if __name__ == "__main__": from grass.script.setup import set_gui_path set_gui_path() from gui_core.toolbars import BaseToolbar, BaseIcons from icons.icon import MetaIcon from gui_core.forms import GUI from gui_core.dialogs import SetOpacityDialog from gui_core.wrap import CheckListBox, Menu, NewId from core.utils import GetLayerNameFromCmd from core.gcmd import GError from core.layerlist import LayerList SIMPLE_LMGR_RASTER = 1 SIMPLE_LMGR_VECTOR = 2 SIMPLE_LMGR_RASTER3D = 4 SIMPLE_LMGR_RGB = 8 SIMPLE_LMGR_TB_TOP = 16 SIMPLE_LMGR_TB_BOTTOM = 32 SIMPLE_LMGR_TB_LEFT = 64 SIMPLE_LMGR_TB_RIGHT = 128 class SimpleLayerManager(wx.Panel): """Simple layer manager class provides similar functionality to Layertree, but it's just list, not tree.""" def __init__( self, parent, layerList, lmgrStyle=SIMPLE_LMGR_RASTER | SIMPLE_LMGR_VECTOR | SIMPLE_LMGR_TB_LEFT, toolbarCls=None, modal=False, ): wx.Panel.__init__(self, parent=parent, name="SimpleLayerManager") self._style = lmgrStyle self._layerList = layerList self._checkList = CheckListBox(self, style=wx.LB_EXTENDED) if not toolbarCls: toolbarCls = SimpleLmgrToolbar self._toolbar = toolbarCls(self, lmgrStyle=self._style) self._auimgr = wx.aui.AuiManager(self) self._modal = modal # d.* dialogs are recreated each time, attempt to hide it resulted # in completely mysterious memory corruption and crash when opening # any dialog with stock labels (wx.ID_OK and so on) # needed in order not to change selection when moving layers self._blockSelectionChanged = False self._checkList.Bind(wx.EVT_LISTBOX, lambda evt: self._selectionChanged()) self._checkList.Bind(wx.EVT_LISTBOX_DCLICK, self.OnLayerChangeProperties) self._checkList.Bind(wx.EVT_CHECKLISTBOX, self.OnLayerChecked) self._checkList.Bind(wx.EVT_CONTEXT_MENU, self.OnContextMenu) # signal emitted when somethin in layer list changes self.opacityChanged = Signal("SimpleLayerManager.opacityChanged") self.cmdChanged = Signal("SimpleLayerManager.cmdChanged") self.layerAdded = Signal("SimpleLayerManager.layerAdded") self.layerRemoved = Signal("SimpleLayerManager.layerRemoved") self.layerActivated = Signal("SimpleLayerManager.layerActivated") self.layerMovedUp = Signal("SimpleLayerManager.layerMovedUp") self.layerMovedDown = Signal("SimpleLayerManager.layerMovedDown") # emitted by any change (e.g. for rerendering) self.anyChange = Signal("SimpleLayerManager.layerChange") self._layout() self.SetMinSize((200, -1)) self._update() def _layout(self): self._auimgr.AddPane( self._checkList, wx.aui.AuiPaneInfo() .Name("checklist") .CenterPane() .CloseButton(False) .BestSize((self._checkList.GetBestSize())), ) paneInfo = ( wx.aui.AuiPaneInfo() .Name("toolbar") .Caption(_("Toolbar")) .ToolbarPane() .CloseButton(False) .Layer(1) .Gripper(False) .BestSize((self._toolbar.GetBestSize())) ) if self._style & SIMPLE_LMGR_TB_LEFT: paneInfo.Left() elif self._style & SIMPLE_LMGR_TB_RIGHT: paneInfo.Right() elif self._style & SIMPLE_LMGR_TB_TOP: paneInfo.Top() else: paneInfo.Bottom() self._auimgr.AddPane(self._toolbar, paneInfo) self._auimgr.Update() def _selectionChanged(self): """Selection was changed externally, updates selection info in layers.""" if self._blockSelectionChanged: return selected = self._checkList.GetSelections() for i, layer in enumerate(self._layerList): layer.Select(i in selected) def UnInit(self): """Needs to be called before destroying this window""" self._auimgr.UnInit() def OnContextMenu(self, event): """Show context menu. So far offers only copying layer list to clipboard """ if len(self._layerList) < 1: event.Skip() return menu = Menu() llist = [layer.name for layer in self._layerList] texts = [",".join(llist), ",".join(reversed(llist))] labels = [ _("Copy map names to clipboard (top to bottom)"), _("Copy map names to clipboard (bottom to top)"), ] for label, text in zip(labels, texts): id = NewId() self.Bind(wx.EVT_MENU, lambda evt, t=text, id=id: self._copyText(t), id=id) menu.Append(id, label) # show the popup menu self.PopupMenu(menu) menu.Destroy() event.Skip() def _copyText(self, text): """Helper function for copying TODO: move to utils? """ if wx.TheClipboard.Open(): do = wx.TextDataObject() do.SetText(text) wx.TheClipboard.SetData(do) wx.TheClipboard.Close() def OnLayerChecked(self, event): """Layer was (un)checked, update layer's info.""" checkedIdxs = self._checkList.GetCheckedItems() for i, layer in enumerate(self._layerList): if i in checkedIdxs and not layer.IsActive(): layer.Activate() self.layerActivated.emit(index=i, layer=layer) elif i not in checkedIdxs and layer.IsActive(): layer.Activate(False) self.layerActivated.emit(index=i, layer=layer) self.anyChange.emit() event.Skip() def OnAddRaster(self, event): """Opens d.rast dialog and adds layer. Dummy layer is added first.""" cmd = ["d.rast"] layer = self.AddRaster(name="", cmd=cmd, hidden=True, dialog=None) GUI(parent=self, giface=None, modal=self._modal).ParseCommand( cmd=cmd, completed=(self.GetOptData, layer, "") ) event.Skip() def OnAddVector(self, event): """Opens d.vect dialog and adds layer. Dummy layer is added first.""" cmd = ["d.vect"] layer = self.AddVector(name="", cmd=cmd, hidden=True, dialog=None) GUI(parent=self, giface=None, modal=self._modal).ParseCommand( cmd=cmd, completed=(self.GetOptData, layer, "") ) event.Skip() def OnAddRast3d(self, event): """Opens d.rast3d dialog and adds layer. Dummy layer is added first.""" cmd = ["d.rast3d"] layer = self.AddRast3d(name="", cmd=cmd, hidden=True, dialog=None) GUI(parent=self, giface=None, modal=self._modal).ParseCommand( cmd=cmd, completed=(self.GetOptData, layer, "") ) event.Skip() def OnAddRGB(self, event): """Opens d.rgb dialog and adds layer. Dummy layer is added first.""" cmd = ["d.rgb"] layer = self.AddRGB(name="", cmd=cmd, hidden=True, dialog=None) GUI(parent=self, giface=None, modal=self._modal).ParseCommand( cmd=cmd, completed=(self.GetOptData, layer, "") ) event.Skip() def OnRemove(self, event): """Removes selected layers from list.""" layers = self._layerList.GetSelectedLayers(activeOnly=False) for layer in layers: self.layerRemoved.emit( index=self._layerList.GetLayerIndex(layer), layer=layer ) self._layerList.RemoveLayer(layer) self._update() self.anyChange.emit() event.Skip() def OnLayerUp(self, event): """Moves selected layers one step up. Note: not completely correct for multiple layers.""" layers = self._layerList.GetSelectedLayers() self._blockSelectionChanged = True for layer in layers: idx = self._layerList.GetLayerIndex(layer) if idx > 0: self.layerMovedUp.emit(index=idx, layer=layer) self._layerList.MoveLayerUp(layer) self._update() self._blockSelectionChanged = False self.anyChange.emit() event.Skip() def OnLayerDown(self, event): """Moves selected layers one step down. Note: not completely correct for multiple layers.""" layers = self._layerList.GetSelectedLayers() self._blockSelectionChanged = True for layer in layers: idx = self._layerList.GetLayerIndex(layer) if idx < len(self._layerList) - 1: self.layerMovedDown.emit( index=self._layerList.GetLayerIndex(layer), layer=layer ) self._layerList.MoveLayerDown(layer) self._update() self._blockSelectionChanged = False self.anyChange.emit() event.Skip() def OnLayerChangeProperties(self, event): """Opens module dialog to edit layer command.""" layers = self._layerList.GetSelectedLayers() if not layers or len(layers) > 1: return self._layerChangeProperties(layers[0]) event.Skip() def _layerChangeProperties(self, layer): """Opens new module dialog or recycles it.""" GUI(parent=self, giface=None, modal=self._modal).ParseCommand( cmd=layer.cmd, completed=(self.GetOptData, layer, "") ) def OnLayerChangeOpacity(self, event): """Opacity of a layer is changing.""" layers = self._layerList.GetSelectedLayers() if not layers or len(layers) > 1: return layer = layers[0] dlg = SetOpacityDialog( self, opacity=layer.opacity, title=_("Set opacity of <%s>") % layer.name ) dlg.applyOpacity.connect(lambda value: self._setLayerOpacity(layer, value)) dlg.CentreOnParent() if dlg.ShowModal() == wx.ID_OK: self._setLayerOpacity(layer, dlg.GetOpacity()) dlg.Destroy() event.Skip() def _setLayerOpacity(self, layer, value): """Sets layer's opacity.'""" layer.opacity = value self._update() self.opacityChanged.emit( index=self._layerList.GetLayerIndex(layer), layer=layer ) self.anyChange.emit() def _update(self): """Updates checklistbox according to layerList structure.""" items = [] active = [] selected = [] # remove hidden (temporary) layers first for layer in reversed(self._layerList): if layer.hidden: self._layerList.RemoveLayer(layer) for layer in self._layerList: if layer.opacity < 1: items.append( "{name} (opacity {opacity}%)".format( name=layer.name, opacity=int(layer.opacity * 100) ) ) else: items.append(layer.name) active.append(layer.IsActive()) selected.append(layer.IsSelected()) self._checkList.SetItems(items) for i, check in enumerate(active): self._checkList.Check(i, check) for i, layer in enumerate(self._layerList): if selected[i]: self._checkList.Select(i) else: self._checkList.Deselect(i) def GetOptData(self, dcmd, layer, params, propwin): """Handler for module dialogs.""" if dcmd: layer.cmd = dcmd layer.selected = True mapName, found = GetLayerNameFromCmd(dcmd) if found: try: if layer.hidden: layer.hidden = False signal = self.layerAdded else: signal = self.cmdChanged layer.name = mapName signal.emit(index=self._layerList.GetLayerIndex(layer), layer=layer) except ValueError as e: self._layerList.RemoveLayer(layer) GError(parent=self, message=str(e), showTraceback=False) self._update() self.anyChange.emit() def AddRaster(self, name, cmd, hidden, dialog): """Ads new raster layer.""" layer = self._layerList.AddNewLayer( name=name, mapType="raster", active=True, cmd=cmd, hidden=hidden ) return layer def AddRast3d(self, name, cmd, hidden, dialog): """Ads new raster3d layer.""" layer = self._layerList.AddNewLayer( name=name, mapType="raster_3d", active=True, cmd=cmd, hidden=hidden ) return layer def AddVector(self, name, cmd, hidden, dialog): """Ads new vector layer.""" layer = self._layerList.AddNewLayer( name=name, mapType="vector", active=True, cmd=cmd, hidden=hidden ) return layer def AddRGB(self, name, cmd, hidden, dialog): """Ads new vector layer.""" layer = self._layerList.AddNewLayer( name=name, mapType="rgb", active=True, cmd=cmd, hidden=hidden ) return layer def GetLayerInfo(self, layer, key): """Just for compatibility, should be removed in the future""" value = getattr(layer, key) # hack to return empty list, required in OnCancel in forms # not sure why it should be empty if key == "cmd" and len(value) == 1: return [] return value def Delete(self, layer): """Just for compatibility, should be removed in the future""" self._layerList.RemoveLayer(layer) class SimpleLmgrToolbar(BaseToolbar): """Toolbar of simple layer manager. Style of the toolbar can be changed (horizontal, vertical, which map types to include). """ def __init__(self, parent, lmgrStyle): """Toolbar constructor""" self._style = lmgrStyle if lmgrStyle & (SIMPLE_LMGR_TB_LEFT | SIMPLE_LMGR_TB_RIGHT): direction = wx.TB_VERTICAL else: direction = wx.TB_HORIZONTAL BaseToolbar.__init__(self, parent, style=wx.NO_BORDER | direction) self.InitToolbar(self._getToolbarData(self._toolbarData())) # realize the toolbar self.Realize() def _toolbarData(self): """Toolbar data""" data = [ ("edit", icons["edit"], self.parent.OnLayerChangeProperties), ("remove", icons["remove"], self.parent.OnRemove), (None,), ("up", icons["up"], self.parent.OnLayerUp), ("down", icons["down"], self.parent.OnLayerDown), (None,), ("opacity", icons["opacity"], self.parent.OnLayerChangeOpacity), ] if self._style & SIMPLE_LMGR_RASTER3D: data.insert(0, ("addRaster3d", icons["addRast3d"], self.parent.OnAddRast3d)) if self._style & SIMPLE_LMGR_RGB: data.insert(0, ("addRGB", icons["addRGB"], self.parent.OnAddRGB)) if self._style & SIMPLE_LMGR_VECTOR: data.insert(0, ("addVector", BaseIcons["addVect"], self.parent.OnAddVector)) if self._style & SIMPLE_LMGR_RASTER: data.insert(0, ("addRaster", BaseIcons["addRast"], self.parent.OnAddRaster)) return data icons = { "remove": MetaIcon( img="layer-remove", label=_("Remove"), desc=_("Remove selected map(s) from list"), ), "up": MetaIcon( img="layer-up", label=_("Layer up"), desc=_("Move selected layer(s) up") ), "down": MetaIcon( img="layer-down", label=_("Layer down"), desc=_("Move selected layer(s) down") ), "edit": MetaIcon( img="layer-edit", label=_("Edit layer properties"), desc=_("Edit layer properties"), ), "opacity": MetaIcon( img="layer-opacity", label=_("Change opacity"), desc=_("Change layer opacity") ), "addRast3d": MetaIcon( img="layer-raster3d-add", label=_("Add 3D raster map layer"), desc=_("Add 3D raster map layer"), ), "addRGB": MetaIcon(img="layer-rgb-add", label=_("Add RGB map layer")), } class TestFrame(wx.Frame): def __init__(self, parent): wx.Frame.__init__(self, parent=parent, title="Simple layer manager test") SimpleLayerManager(parent=self, layerList=LayerList()) def test(): app = wx.App() frame = TestFrame(None) frame.Show() app.MainLoop() if __name__ == "__main__": test()