"""
@package iscatt.frame

@brief Main scatter plot widgets.

Classes:
 - frame::IClassIScattPanel
 - frame::IScattDialog
 - frame::MapDispIScattPanel
 - frame::ScatterPlotsPanel
 - frame::CategoryListCtrl

(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 Stepan Turek <stepan.turek seznam.cz> (mentor: Martin Landa)
"""

from __future__ import print_function

import os
import sys
import six

import wx
import wx.lib.scrolledpanel as scrolled
import wx.lib.mixins.listctrl as listmix

from core import globalvar
from core.gcmd import GException, GError, RunCommand

from gui_core.gselect import Select
from gui_core.dialogs import SetOpacityDialog
from gui_core.wrap import StaticBox, Menu, ListCtrl
from iscatt.controllers import ScattsManager
from iscatt.toolbars import MainToolbar, EditingToolbar, CategoryToolbar
from iscatt.iscatt_core import idScattToidBands
from iscatt.dialogs import ManageBusyCursorMixin, RenameClassDialog
from iscatt.plots import ScatterPlotWidget
from iclass.dialogs import ContrastColor

try:
    from agw import aui
except ImportError:
    import wx.lib.agw.aui as aui


class IClassIScattPanel(wx.Panel, ManageBusyCursorMixin):

    def __init__(self, parent, giface, iclass_mapwin=None,
                 id=wx.ID_ANY):

        # wx.SplitterWindow.__init__(self, parent = parent, id = id,
        #                           style = wx.SP_LIVE_UPDATE)
        wx.Panel.__init__(self, parent=parent, id=wx.ID_ANY)
        ManageBusyCursorMixin.__init__(self, window=self)

        self.scatt_mgr = self._createScattMgr(guiparent=parent, giface=giface,
                                              iclass_mapwin=iclass_mapwin)

        # toobars
        self.toolbars = {}
        self.toolbars['mainToolbar'] = self._createMainToolbar()
        self.toolbars['editingToolbar'] = EditingToolbar(
            parent=self, scatt_mgr=self.scatt_mgr)

        self._createCategoryPanel(self)

        self.plot_panel = ScatterPlotsPanel(self, self.scatt_mgr)

        self.mainsizer = wx.BoxSizer(wx.VERTICAL)
        self.mainsizer.Add(
            self.toolbars['mainToolbar'],
            proportion=0, flag=wx.EXPAND)
        self.mainsizer.Add(
            self.toolbars['editingToolbar'],
            proportion=0, flag=wx.EXPAND)
        self.mainsizer.Add(self.catsPanel, proportion=0,
                           flag=wx.EXPAND | wx.LEFT | wx.RIGHT, border=5)
        self.mainsizer.Add(self.plot_panel, proportion=1, flag=wx.EXPAND)

        self.catsPanel.Hide()
        self.toolbars['editingToolbar'].Hide()

        self.SetSizer(self.mainsizer)

        self.scatt_mgr.computingStarted.connect(
            lambda: self.UpdateCur(busy=True))
        self.scatt_mgr.renderingStarted.connect(
            lambda: self.UpdateCur(busy=True))
        self.scatt_mgr.renderingFinished.connect(
            lambda: self.UpdateCur(busy=False))

        # self.SetSashGravity(0.5)
        #self.SplitHorizontally(self.head_panel, self.plot_panel, -50)
        self.Layout()

    def CloseWindow(self):
        self.scatt_mgr.CleanUp()

    def UpdateCur(self, busy):
        self.plot_panel.SetBusy(busy)
        ManageBusyCursorMixin.UpdateCur(self, busy)

    def _selCatInIScatt(self):
        return False

    def _createMainToolbar(self):
        return MainToolbar(parent=self, scatt_mgr=self.scatt_mgr)

    def _createScattMgr(self, guiparent, giface, iclass_mapwin):
        return ScattsManager(guiparent=self, giface=giface,
                             iclass_mapwin=iclass_mapwin)

    def NewScatterPlot(self, scatt_id, transpose):
        return self.plot_panel.NewScatterPlot(scatt_id, transpose)

    def ShowPlotEditingToolbar(self, show):
        self.toolbars["editingToolbar"].Show(show)
        self.Layout()

    def ShowCategoryPanel(self, show):
        self.catsPanel.Show(show)

        # if show:
        #    self.SetSashSize(5)
        # else:
        #    self.SetSashSize(0)
        self.plot_panel.SetVirtualSize(self.plot_panel.GetBestVirtualSize())
        self.Layout()

    def _createCategoryPanel(self, parent):
        self.catsPanel = wx.Panel(parent=parent)
        self.cats_list = CategoryListCtrl(
            parent=self.catsPanel,
            cats_mgr=self.scatt_mgr.GetCategoriesManager(),
            sel_cats_in_iscatt=self._selCatInIScatt())

        self.catsPanel.SetMinSize((-1, 100))
        self.catsPanel.SetInitialSize((-1, 150))

        box_capt = StaticBox(parent=self.catsPanel, id=wx.ID_ANY,
                             label=' %s ' % _("Classes"),)
        catsSizer = wx.StaticBoxSizer(box_capt, wx.VERTICAL)

        self.toolbars['categoryToolbar'] = self._createCategoryToolbar(
            self.catsPanel)

        catsSizer.Add(
            self.cats_list,
            proportion=1,
            flag=wx.EXPAND | wx.TOP,
            border=5)
        if self.toolbars['categoryToolbar']:
            catsSizer.Add(self.toolbars['categoryToolbar'], proportion=0)

        self.catsPanel.SetSizer(catsSizer)

    def _createCategoryToolbar(self, parent):
        return CategoryToolbar(parent=parent,
                               scatt_mgr=self.scatt_mgr,
                               cats_list=self.cats_list)


class IScattDialog(wx.Dialog):

    def __init__(
            self, parent, giface,
            title=_("GRASS GIS Interactive Scatter Plot Tool"),
            id=wx.ID_ANY, style=wx.DEFAULT_FRAME_STYLE, **kwargs):
        wx.Dialog.__init__(
            self,
            parent,
            id,
            style=style,
            title=title,
            **kwargs)
        self.SetIcon(
            wx.Icon(
                os.path.join(
                    globalvar.ICONDIR,
                    'grass.ico'),
                wx.BITMAP_TYPE_ICO))

        self.iscatt_panel = MapDispIScattPanel(self, giface)

        mainsizer = wx.BoxSizer(wx.VERTICAL)
        mainsizer.Add(self.iscatt_panel, proportion=1, flag=wx.EXPAND)

        self.SetSizer(mainsizer)

        self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)

        self.SetMinSize((300, 300))

    def OnCloseWindow(self, event):
        event.Skip()
        # self.


class MapDispIScattPanel(IClassIScattPanel):

    def __init__(self, parent, giface,
                 id=wx.ID_ANY, **kwargs):
        IClassIScattPanel.__init__(self, parent=parent, giface=giface,
                                   id=id, **kwargs)

    def _createScattMgr(self, guiparent, giface, iclass_mapwin):
        return ScattsManager(guiparent=self, giface=giface)

    def _createMainToolbar(self):
        return MainToolbar(
            parent=self, scatt_mgr=self.scatt_mgr, opt_tools=['add_group'])

    def _selCatInIScatt(self):
        return True


class ScatterPlotsPanel(scrolled.ScrolledPanel):

    def __init__(self, parent, scatt_mgr, id=wx.ID_ANY):

        scrolled.ScrolledPanel.__init__(self, parent)
        self.SetupScrolling(scroll_x=False, scroll_y=True, scrollToTop=False)

        self.scatt_mgr = scatt_mgr

        self.mainPanel = wx.Panel(parent=self, id=wx.ID_ANY)

        # self._createCategoryPanel()
        # Fancy gui
        self._mgr = aui.AuiManager(self.mainPanel)
        # self._mgr.SetManagedWindow(self)

        self._mgr.Update()

        self._doLayout()
        self.Bind(wx.EVT_SCROLLWIN, self.OnScroll)
        self.Bind(wx.EVT_SCROLL_CHANGED, self.OnScrollChanged)

        self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPlotPaneClosed)

        dlgSize = (-1, 400)
        # self.SetBestSize(dlgSize)
        # self.SetInitialSize(dlgSize)
        self.SetAutoLayout(1)
        # fix goutput's pane size (required for Mac OSX)
        # if self.gwindow:
        #    self.gwindow.SetSashPosition(int(self.GetSize()[1] * .75))
        self.ignore_scroll = 0
        self.Bind(wx.EVT_MOUSEWHEEL, self.OnMouseWheel)

        self.scatt_i = 1
        self.scatt_id_scatt_i = {}
        self.transpose = {}
        self.scatts = {}

        self.Bind(wx.EVT_CLOSE, self.OnClose)

        self.scatt_mgr.cursorPlotMove.connect(self.CursorPlotMove)

    def SetBusy(self, busy):
        for scatt in six.itervalues(self.scatts):
            scatt.UpdateCur(busy)

    def CursorPlotMove(self, x, y, scatt_id):

        try:
            x = int(round(x))
            y = int(round(y))
            coords = True
        except:
            coords = False

        pane = self._getPane(scatt_id)
        caption = self._creteCaption(scatt_id)
        if coords:
            caption += "   %d, %d" % (x, y)

        pane.Caption(caption)
        self._mgr.RefreshCaptions()

    def _getPane(self, scatt_id):
        scatt_i = self.scatt_id_scatt_i[scatt_id]
        name = self._getScatterPlotName(scatt_i)
        return self._mgr.GetPane(name)

    def ScatterPlotClosed(self, scatt_id):

        scatt_i = self.scatt_id_scatt_i[scatt_id]

        name = self._getScatterPlotName(scatt_i)
        pane = self._mgr.GetPane(name)

        del self.scatt_id_scatt_i[scatt_id]
        del self.scatts[scatt_id]

        if pane.IsOk():
            self._mgr.ClosePane(pane)
        self._mgr.Update()

    def OnMouseWheel(self, event):
        # TODO very ugly find some better solution
        self.ignore_scroll = 3
        event.Skip()

    def ScrollChildIntoView(self, child):
        # For aui manager it does not work and returns position always to the
        # top -> deactivated.
        pass

    def OnPlotPaneClosed(self, event):
        if isinstance(event.pane.window, ScatterPlotWidget):
            event.pane.window.CleanUp()

    def OnScrollChanged(self, event):
        wx.CallAfter(self.Layout)

    def OnScroll(self, event):
        if self.ignore_scroll > 0:
            self.ignore_scroll -= 1
        else:
            event.Skip()

        # wx.CallAfter(self._mgr.Update)
        # wx.CallAfter(self.Layout)

    def _doLayout(self):

        mainsizer = wx.BoxSizer(wx.VERTICAL)
        mainsizer.Add(self.mainPanel, proportion=1, flag=wx.EXPAND)
        self.SetSizer(mainsizer)

        self.Layout()
        self.SetupScrolling()

    def OnClose(self, event):
        """Close dialog"""
        # TODO: why todo here, why print? just delete?
        print("closed")
        self.scatt_mgr.CleanUp()
        self.Destroy()

    def OnSettings(self, event):
        pass

    def _newScatterPlotName(self, scatt_id):
        name = self._getScatterPlotName(self.scatt_i)
        self.scatt_id_scatt_i[scatt_id] = self.scatt_i
        self.scatt_i += 1
        return name

    def _getScatterPlotName(self, i):
        name = "scatter plot %d" % i
        return name

    def NewScatterPlot(self, scatt_id, transpose):
        # TODO needs to be resolved (should be in this class)

        scatt = ScatterPlotWidget(parent=self.mainPanel,
                                  scatt_mgr=self.scatt_mgr,
                                  scatt_id=scatt_id,
                                  transpose=transpose)
        scatt.plotClosed.connect(self.ScatterPlotClosed)
        self.transpose[scatt_id] = transpose

        caption = self._creteCaption(scatt_id)
        self._mgr.AddPane(scatt,
                          aui.AuiPaneInfo().Dockable(True).Floatable(True).
                          Name(self._newScatterPlotName(scatt_id)).MinSize((-1, 300)).
                          Caption(caption).
                          Center().Position(1).MaximizeButton(True).
                          MinimizeButton(True).CaptionVisible(True).
                          CloseButton(True).Layer(0))

        self._mgr.Update()

        self.SetVirtualSize(self.GetBestVirtualSize())
        self.Layout()

        self.scatts[scatt_id] = scatt

        return scatt

    def _creteCaption(self, scatt_id):

        transpose = self.transpose[scatt_id]
        bands = self.scatt_mgr.GetBands()

        # TODO too low level
        b1_id, b2_id = idScattToidBands(scatt_id, len(bands))

        x_b = bands[b1_id].split('@')[0]
        y_b = bands[b2_id].split('@')[0]

        if transpose:
            tmp = x_b
            x_b = y_b
            y_b = tmp

        return "%s x: %s y: %s" % (_("scatter plot"), x_b, y_b)

    def GetScattMgr(self):
        return self.scatt_mgr


class CategoryListCtrl(ListCtrl,
                       listmix.ListCtrlAutoWidthMixin):
                       # listmix.TextEditMixin):

    def __init__(self, parent, cats_mgr, sel_cats_in_iscatt, id=wx.ID_ANY):

        ListCtrl.__init__(
            self, parent, id, style=wx.LC_REPORT | wx.LC_VIRTUAL | wx.LC_HRULES |
            wx.LC_VRULES | wx.LC_SINGLE_SEL | wx.LC_NO_HEADER)
        self.columns = ((_('Class name'), 'name'), )
        #(_('Color'), 'color'))

        self.sel_cats_in_iscatt = sel_cats_in_iscatt

        self.Populate(columns=self.columns)

        self.cats_mgr = cats_mgr
        self.SetItemCount(len(self.cats_mgr.GetCategories()))

        self.rightClickedItemIdx = wx.NOT_FOUND

        listmix.ListCtrlAutoWidthMixin.__init__(self)

        # listmix.TextEditMixin.__init__(self)

        self.Bind(wx.EVT_COMMAND_RIGHT_CLICK, self.OnCategoryRightUp)  # wxMSW
        self.Bind(wx.EVT_RIGHT_UP, self.OnCategoryRightUp)  # wxGTK

        #self.Bind(wx.EVT_LIST_BEGIN_LABEL_EDIT, self.OnEdit)
        self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnSel)

        self.cats_mgr.setCategoryAttrs.connect(self.Update)
        self.cats_mgr.deletedCategory.connect(self.Update)
        self.cats_mgr.addedCategory.connect(self.Update)

    def Update(self, **kwargs):
        self.SetItemCount(len(self.cats_mgr.GetCategories()))
        if len(self.cats_mgr.GetCategories()):
            self.RefreshItems(0, len(self.cats_mgr.GetCategories()) - 1)

    def SetVirtualData(self, row, column, text):
        attr = self.columns[column][1]
        if attr == 'name':
            try:
                text.encode('ascii')
            except UnicodeEncodeError:
                GMessage(parent=self, message=_(
                    "Please use only ASCII characters."))
                return

        cat_id = self.cats_mgr.GetCategories()[row]

        self.cats_mgr.setCategoryAttrs.disconnect(self.Update)
        self.cats_mgr.SetCategoryAttrs(cat_id, {attr: text})
        self.cats_mgr.setCategoryAttrs.connect(self.Update)

        self.Select(row)

    def Populate(self, columns):
        for i, col in enumerate(columns):
            self.InsertColumn(i, col[0])  # wx.LIST_FORMAT_RIGHT

        #self.SetColumnWidth(0, 100)
        #self.SetColumnWidth(1, 100)

    def AddCategory(self):

        self.cats_mgr.addedCategory.disconnect(self.Update)
        cat_id = self.cats_mgr.AddCategory()
        self.cats_mgr.addedCategory.connect(self.Update)

        if cat_id < 0:
            GError(_("Maximum limit of categories number was reached."))
            return
        self.SetItemCount(len(self.cats_mgr.GetCategories()))

    def DeleteCategory(self):
        indexList = sorted(self.GetSelectedIndices(), reverse=True)
        cats = []
        for i in indexList:
            # remove temporary raster
            cat_id = self.cats_mgr.GetCategories()[i]

            cats.append(cat_id)

            self.cats_mgr.deletedCategory.disconnect(self.Update)
            self.cats_mgr.DeleteCategory(cat_id)
            self.cats_mgr.deletedCategory.connect(self.Update)

        self.SetItemCount(len(self.cats_mgr.GetCategories()))

    def OnSel(self, event):
        if self.sel_cats_in_iscatt:
            indexList = self.GetSelectedIndices()
            sel_cats = []
            cats = self.cats_mgr.GetCategories()
            for i in indexList:
                sel_cats.append(cats[i])

            if sel_cats:
                self.cats_mgr.SetSelectedCat(sel_cats[0])
        event.Skip()

    def GetSelectedIndices(self, state=wx.LIST_STATE_SELECTED):
        indices = []
        lastFound = -1
        while True:
            index = self.GetNextItem(lastFound, wx.LIST_NEXT_ALL, state)
            if index == -1:
                break
            else:
                lastFound = index
                indices.append(index)
        return indices

    def DeselectAll(self):
        """Deselect all items"""
        indexList = self.GetSelectedIndices()
        for i in indexList:
            self.Select(i, on=0)

        # no highlight
        self.OnCategorySelected(None)

    def OnGetItemText(self, item, col):
        attr = self.columns[col][1]
        cat_id = self.cats_mgr.GetCategories()[item]

        return self.cats_mgr.GetCategoryAttrs(cat_id)[attr]

    def OnGetItemImage(self, item):
        return -1

    def OnGetItemAttr(self, item):
        cat_id = self.cats_mgr.GetCategories()[item]

        cattr = self.cats_mgr.GetCategoryAttrs(cat_id)

        if cattr['show']:
            c = cattr['color']

            back_c = wx.Colour(*map(int, c.split(':')))
            text_c = wx.Colour(*ContrastColor(back_c))
        else:
            back_c = wx.SystemSettings.GetColour(wx.SYS_COLOUR_INACTIVECAPTION)
            text_c = wx.SystemSettings.GetColour(
                wx.SYS_COLOUR_INACTIVECAPTIONTEXT)

        # if it is in scope of the method, gui falls, using self solved it
        self.l = wx.ListItemAttr()
        self.l.SetBackgroundColour(back_c)
        self.l.SetTextColour(text_c)
        return self.l

    def OnCategoryRightUp(self, event):
        """Show context menu on right click"""
        item, flags = self.HitTest((event.GetX(), event.GetY()))
        if item != wx.NOT_FOUND and flags & wx.LIST_HITTEST_ONITEM:
            self.rightClickedItemIdx = item

        # generate popup-menu
        cat_idx = self.rightClickedItemIdx

        cats = self.cats_mgr.GetCategories()
        cat_id = cats[cat_idx]
        showed = self.cats_mgr.GetCategoryAttrs(cat_id)['show']

        menu = Menu()

        item = menu.Append(wx.ID_ANY, _("Rename class"))
        self.Bind(wx.EVT_MENU, self.OnRename, item)

        item = menu.Append(wx.ID_ANY, _("Set color"))
        self.Bind(wx.EVT_MENU, self.OnSetColor, item)

        item = menu.Append(item_id, _("Change opacity level"))
        self.Bind(wx.EVT_MENU, self.OnPopupOpacityLevel, item)

        if showed:
            text = _("Hide")
        else:
            text = _("Show")

        item = menu.Append(wx.ID_ANY, text)
        self.Bind(
            wx.EVT_MENU,
            lambda event: self._setCatAttrs(
                cat_id=cat_id,
                attrs={
                    'show': not showed}),
            item)

        menu.AppendSeparator()

        item = menu.Append(wx.ID_ANY, _("Move to top"))
        self.Bind(wx.EVT_MENU, self.OnMoveTop, item)
        if cat_idx == 0:
            menu.Enable(item.GetId(), False)

        item = menu.Append(wx.ID_ANY, _("Move to bottom"))
        self.Bind(wx.EVT_MENU, self.OnMoveBottom, item)
        if cat_idx == len(cats) - 1:
            menu.Enable(item.GetId(), False)

        menu.AppendSeparator()

        item = menu.Append(wx.ID_ANY, _("Move category up"))
        self.Bind(wx.EVT_MENU, self.OnMoveUp, item)
        if cat_idx == 0:
            menu.Enable(item.GetId(), False)

        item = menu.Append(wx.ID_ANY, _("Move category down"))
        self.Bind(wx.EVT_MENU, self.OnMoveDown, item)
        if cat_idx == len(cats) - 1:
            menu.Enable(item.GetId(), False)

        menu.AppendSeparator()

        item = menu.Append(wx.ID_ANY, _("Export class raster"))
        self.Bind(wx.EVT_MENU, self.OnExportCatRast, item)

        self.PopupMenu(menu)
        menu.Destroy()

    def OnExportCatRast(self, event):
        """Export training areas"""
        # TODO
        #   GMessage(parent=self, message=_("No class raster to export."))
        #    return

        cat_idx = self.rightClickedItemIdx
        cat_id = self.cats_mgr.GetCategories()[cat_idx]

        self.cats_mgr.ExportCatRast(cat_id)

    def OnMoveUp(self, event):
        cat_idx = self.rightClickedItemIdx
        cat_id = self.cats_mgr.GetCategories()[cat_idx]
        self.cats_mgr.ChangePosition(cat_id, cat_idx - 1)
        self.RefreshItems(0, len(self.cats_mgr.GetCategories()))

    def OnMoveDown(self, event):
        cat_idx = self.rightClickedItemIdx
        cat_id = self.cats_mgr.GetCategories()[cat_idx]
        self.cats_mgr.ChangePosition(cat_id, cat_idx + 1)
        self.RefreshItems(0, len(self.cats_mgr.GetCategories()))

    def OnMoveTop(self, event):
        cat_idx = self.rightClickedItemIdx
        cat_id = self.cats_mgr.GetCategories()[cat_idx]
        self.cats_mgr.ChangePosition(cat_id, 0)
        self.RefreshItems(0, len(self.cats_mgr.GetCategories()))

    def OnMoveBottom(self, event):
        cat_idx = self.rightClickedItemIdx
        cats = self.cats_mgr.GetCategories()
        cat_id = cats[cat_idx]
        self.cats_mgr.ChangePosition(cat_id, len(cats) - 1)
        self.RefreshItems(0, len(self.cats_mgr.GetCategories()))

    def OnSetColor(self, event):
        """Popup opacity level indicator"""
        cat_idx = self.rightClickedItemIdx
        cat_id = self.cats_mgr.GetCategories()[cat_idx]

        col = self.cats_mgr.GetCategoryAttrs(cat_id)['color']
        col = map(int, col.split(':'))

        col_data = wx.ColourData()
        col_data.SetColour(wx.Colour(*col))

        dlg = wx.ColourDialog(self, col_data)
        dlg.GetColourData().SetChooseFull(True)

        if dlg.ShowModal() == wx.ID_OK:
            color = dlg.GetColourData().GetColour().Get()
            color = ':'.join(map(str, color))
            self.cats_mgr.SetCategoryAttrs(cat_id, {"color": color})

        dlg.Destroy()

    def OnPopupOpacityLevel(self, event):
        """Popup opacity level indicator"""

        cat_id = self.cats_mgr.GetCategories()[self.rightClickedItemIdx]
        cat_attrs = self.cats_mgr.GetCategoryAttrs(cat_id)
        value = cat_attrs['opacity'] * 100
        name = cat_attrs['name']

        dlg = SetOpacityDialog(self, opacity=value,
                               title=_("Change opacity of class <%s>" % name))

        dlg.applyOpacity.connect(
            lambda value: self._setCatAttrs(
                cat_id=cat_id, attrs={
                    'opacity': value}))
        dlg.CentreOnParent()

        if dlg.ShowModal() == wx.ID_OK:
            self._setCatAttrs(cat_id=cat_id, attrs={'opacity': value})

        dlg.Destroy()

    def OnRename(self, event):
        cat_id = self.cats_mgr.GetCategories()[self.rightClickedItemIdx]
        cat_attrs = self.cats_mgr.GetCategoryAttrs(cat_id)

        dlg = RenameClassDialog(self, old_name=cat_attrs['name'])
        dlg.CentreOnParent()

        while True:
            if dlg.ShowModal() == wx.ID_OK:
                name = dlg.GetNewName().strip()
                if not name:
                    GMessage(parent=self, message=_(
                        "Empty name was inserted."))
                else:
                    self.cats_mgr.SetCategoryAttrs(cat_id, {"name": name})
                    break
            else:
                break

        dlg.Destroy()

    def _setCatAttrs(self, cat_id, attrs):
        self.cats_mgr.setCategoryAttrs.disconnect(self.Update)
        self.cats_mgr.SetCategoryAttrs(cat_id, attrs)
        self.cats_mgr.setCategoryAttrs.connect(self.Update)
