コード例 #1
0
class ModelGroupsTable(QWidget):
    EditableAttrs = [
        attr for attr in PlotStyles.StyleAttributes
        if attr in PlotStyles.StyleAttributeOptions
    ]
    ColList = 3
    ColPlot = 4
    ColApply = 5
    AttrByCol = dict([(i + 6, attr) for i, attr in enumerate(EditableAttrs)])

    def __init__(self, parent, *args):
        QWidget.__init__(self, parent, *args)
        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        lo = QVBoxLayout(self)
        lo.setContentsMargins(0, 0, 0, 0)
        lo1 = QHBoxLayout()
        lo.addLayout(lo1)
        lo1.setContentsMargins(0, 0, 0, 0)
        lbl = QLabel(QString("<nobr><b>Source groupings:</b></nobr>"), self)
        lo1.addWidget(lbl, 0)
        lo1.addStretch(1)
        # add show/hide button
        self._showattrbtn = QPushButton(self)
        self._showattrbtn.setMinimumWidth(256)
        lo1.addWidget(self._showattrbtn, 0)
        lo1.addStretch()
        QObject.connect(self._showattrbtn, SIGNAL("clicked()"),
                        self._togglePlotControlsVisibility)
        # add table
        self.table = QTableWidget(self)
        lo.addWidget(self.table)
        QObject.connect(self.table, SIGNAL("cellChanged(int,int)"),
                        self._valueChanged)
        self.table.setSelectionMode(QTableWidget.NoSelection)
        # setup basic columns
        self.table.setColumnCount(6 + len(self.EditableAttrs))
        for i, label in enumerate(
            ("grouping", "total", "selection", "list", "plot", "style")):
            self.table.setHorizontalHeaderItem(i, QTableWidgetItem(label))
        self.table.horizontalHeader().setSectionHidden(self.ColApply, True)
        # setup columns for editable grouping attributes
        for i, attr in self.AttrByCol.items():
            self.table.setHorizontalHeaderItem(
                i, QTableWidgetItem(PlotStyles.StyleAttributeLabels[attr]))
            self.table.horizontalHeader().setSectionHidden(i, True)
        self.table.verticalHeader().hide()
        # other internal init
        self._attrs_shown = False
        self._togglePlotControlsVisibility()
        self.model = None
        self._setting_model = False
        self._currier = PersistentCurrier()
        # row of 'selected' grouping
        self._irow_selgroup = 0

    def clear(self):
        self.table.setRowCount(0)
        self.model = None

    # setup mappings from the group.show_plot attribute to check state
    ShowAttrToCheckState = {
        PlotStyles.ShowNot: Qt.Unchecked,
        PlotStyles.ShowDefault: Qt.PartiallyChecked,
        PlotStyles.ShowAlways: Qt.Checked
    }
    CheckStateToShowAttr = dict([(val, key)
                                 for key, val in ShowAttrToCheckState.items()])

    def _makeCheckItem(self, name, group, attr):
        item = QTableWidgetItem(name)
        if group is self.model.defgroup:
            item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable)
            item.setCheckState(
                Qt.Checked if getattr(group.style, attr) else Qt.Unchecked)
        else:
            item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable
                          | Qt.ItemIsTristate)
            item.setCheckState(self.ShowAttrToCheckState[getattr(
                group.style, attr)])
        return item

    def _updateModel(self, what=SkyModel.UpdateAll, origin=None):
        if origin is self or not what & (SkyModel.UpdateTags
                                         | SkyModel.UpdateGroupStyle):
            return
        model = self.model
        self._setting_model = True
        # to ignore cellChanged() signals (in valueChanged())
        # _item_cb is a dict (with row,col keys) containing the widgets (CheckBoxes ComboBoxes) per each cell
        self._item_cb = {}
        # lists of "list" and "plot" checkboxes per each grouping (excepting the default grouping); each entry is an (row,col,item) tuple.
        # used as argument to self._showControls()
        self._list_controls = []
        self._plot_controls = []
        # list of selection callbacks (to which signals are connected)
        self._callbacks = []
        # set requisite number of rows,and start filling
        self.table.setRowCount(len(model.groupings))
        for irow, group in enumerate(model.groupings):
            self.table.setItem(irow, 0, QTableWidgetItem(group.name))
            if group is model.selgroup:
                self._irow_selgroup = irow
            # total # source in group: skip for "current"
            if group is not model.curgroup:
                self.table.setItem(irow, 1, QTableWidgetItem(str(group.total)))
            # selection controls: skip for current and selection
            if group not in (model.curgroup, model.selgroup):
                btns = QWidget()
                lo = QHBoxLayout(btns)
                lo.setContentsMargins(0, 0, 0, 0)
                lo.setSpacing(0)
                # make selector buttons (depending on which group we're in)
                if group is model.defgroup:
                    Buttons = (("+", lambda src, grp=group: True,
                                "select all sources"),
                               ("-", lambda src, grp=group: False,
                                "unselect all sources"))
                else:
                    Buttons = (
                        ("=", lambda src, grp=group: grp.func(src),
                         "select only this grouping"),
                        ("+",
                         lambda src, grp=group: src.selected or grp.func(src),
                         "add grouping to selection"),
                        ("-", lambda src, grp=group: src.selected and not grp.
                         func(src), "remove grouping from selection"),
                        ("&&",
                         lambda src, grp=group: src.selected and grp.func(src),
                         "intersect selection with grouping"))
                lo.addStretch(1)
                for label, predicate, tooltip in Buttons:
                    btn = QToolButton(btns)
                    btn.setText(label)
                    btn.setMinimumWidth(24)
                    btn.setMaximumWidth(24)
                    btn.setToolTip(tooltip)
                    lo.addWidget(btn)
                    # add callback
                    QObject.connect(
                        btn, SIGNAL("clicked()"),
                        self._currier.curry(self.selectSources, predicate))
                lo.addStretch(1)
                self.table.setCellWidget(irow, 2, btns)
            # "list" checkbox (not for current and selected groupings: these are always listed)
            if group not in (model.curgroup, model.selgroup):
                item = self._makeCheckItem("", group, "show_list")
                self.table.setItem(irow, self.ColList, item)
                item.setToolTip(
                    """<P>If checked, sources in this grouping will be listed in the source table. If un-checked, sources will be
            excluded from the table. If partially checked, then the default list/no list setting of "all sources" will be in effect.
            </P>""")
            # "plot" checkbox (not for the current grouping, since that's always plotted)
            if group is not model.curgroup:
                item = self._makeCheckItem("", group, "show_plot")
                self.table.setItem(irow, self.ColPlot, item)
                item.setToolTip(
                    """<P>If checked, sources in this grouping will be included in the plot. If un-checked, sources will be
            excluded from the plot. If partially checked, then the default plot/no plot setting of "all sources" will be in effect.
            </P>""")
            # custom style control
            # for default, current and selected, this is just a text label
            if group is model.defgroup:
                item = QTableWidgetItem("default:")
                item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
                item.setToolTip(
                    """<P>This is the default plot style used for all sources for which a custom grouping style is not selected.</P>"""
                )
                self.table.setItem(irow, self.ColApply, item)
            elif group is model.curgroup:
                item = QTableWidgetItem("")
                item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
                item.setToolTip(
                    """<P>This is the plot style used for the highlighted source, if any.</P>"""
                )
                self.table.setItem(irow, self.ColApply, item)
            elif group is model.selgroup:
                item = QTableWidgetItem("")
                item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
                item.setToolTip(
                    """<P>This is the plot style used for the currently selected sources.</P>"""
                )
                self.table.setItem(irow, self.ColApply, item)
            # for the rest, a combobox with custom priorities
            else:
                cb = QComboBox()
                cb.addItems(["default"] +
                            ["custom %d" % p for p in range(1, 10)])
                index = max(0, min(group.style.apply, 9))
                #        dprint(0,group.name,"apply",index)
                cb.setCurrentIndex(index)
                QObject.connect(
                    cb, SIGNAL("activated(int)"),
                    self._currier.xcurry(self._valueChanged,
                                         (irow, self.ColApply)))
                self.table.setCellWidget(irow, self.ColApply, cb)
                cb.setToolTip(
                    """<P>This controls whether sources within this group are plotted with a customized
            plot style. Customized styles have numeric priority; if a source belongs to multiple groups, then
            the style with the lowest priority takes precedence.<P>""")
            # attribute comboboxes
            for icol, attr in self.AttrByCol.items():
                # get list of options for this style attribute. If dealing with first grouping (i==0), which is
                # the "all sources" grouping, then remove the "default" option (which is always first in the list)
                options = PlotStyles.StyleAttributeOptions[attr]
                if irow == 0:
                    options = options[1:]
                # make combobox
                cb = QComboBox()
                cb.addItems(list(map(str, options)))
                # the "label" option is also editable
                if attr == "label":
                    cb.setEditable(True)
                try:
                    index = options.index(getattr(group.style, attr))
                    cb.setCurrentIndex(index)
                except ValueError:
                    cb.setEditText(str(getattr(group.style, attr)))
                slot = self._currier.xcurry(self._valueChanged, (irow, icol))
                QObject.connect(cb, SIGNAL("activated(int)"), slot)
                QObject.connect(cb, SIGNAL("editTextChanged(const QString &)"),
                                slot)
                cb.setEnabled(group is model.defgroup or group.style.apply)
                self.table.setCellWidget(irow, icol, cb)
                label = attr
                if irow:
                    cb.setToolTip(
                        """<P>This is the %s used to plot sources in this group, when a "custom" style for the group
          is enabled via the style control.<P>""" % label)
                else:
                    cb.setToolTip(
                        "<P>This is the default %s used for all sources for which a custom style is not specified below.<P>"
                        % label)
        self.table.resizeColumnsToContents()
        # re-enable processing of cellChanged() signals
        self._setting_model = False

    def setModel(self, model):
        self.model = model
        self.model.connect("updated", self._updateModel)
        self.model.connect("selected", self.updateModelSelection)
        self._updateModel(SkyModel.UpdateAll)

    def _valueChanged(self, row, col):
        """Called when a cell has been edited"""
        if self._setting_model:
            return
        group = self.model.groupings[row]
        item = self.table.item(row, col)
        if col == self.ColList:
            if group is not self.model.defgroup:
                # tri-state items go from unchecked to checked when user clicks them. Make them partially checked instead.
                if group.style.show_list == PlotStyles.ShowNot and item.checkState(
                ) == Qt.Checked:
                    item.setCheckState(Qt.PartiallyChecked)
            group.style.show_list = self.CheckStateToShowAttr[
                item.checkState()]
            self.model.emitChangeGroupingVisibility(group, origin=self)
            return
        elif col == self.ColPlot:
            if group is not self.model.defgroup:
                # tri-state items go from unchecked to checked by default. Make them partially checked instead.
                if group.style.show_plot == PlotStyles.ShowNot and item.checkState(
                ) == Qt.Checked:
                    item.setCheckState(Qt.PartiallyChecked)
            group.style.show_plot = self.CheckStateToShowAttr[
                item.checkState()]
        elif col == self.ColApply:
            group.style.apply = self.table.cellWidget(row, col).currentIndex()
            # enable/disable editable cells
            for j in list(self.AttrByCol.keys()):
                item1 = self.table.item(row, j)
                if item1:
                    fl = item1.flags() & ~Qt.ItemIsEnabled
                    if group.style.apply:
                        fl |= Qt.ItemIsEnabled
                    item1.setFlags(fl)
                cw = self.table.cellWidget(row, j)
                cw and cw.setEnabled(group.style.apply)
        elif col in self.AttrByCol:
            cb = self.table.cellWidget(row, col)
            txt = str(cb.currentText())
            attr = self.AttrByCol[col]
            if txt == "default":
                setattr(group.style, attr, PlotStyles.DefaultValue)
            else:
                setattr(group.style, attr,
                        PlotStyles.StyleAttributeTypes.get(attr, str)(txt))
        # all other columns: return so we don't emit a signal
        else:
            return
        # in all cases emit a signal
        self.model.emitChangeGroupingStyle(group, origin=self)

    def selectSources(self, predicate):
        """Selects sources according to predicate(src)"""
        busy = BusyIndicator()
        for src in self.model.sources:
            src.selected = predicate(src)
        self.model.emitSelection(origin=self)
        busy = None

    def updateModelSelection(self, nsel, origin=None):
        """This is called when some other widget changes the set of selected model sources"""
        self.table.clearSelection()
        if self.model:
            self.table.item(self._irow_selgroup, 1).setText(str(nsel))

    def _togglePlotControlsVisibility(self):
        if self._attrs_shown:
            self._attrs_shown = False
            self.table.hideColumn(self.ColApply)
            for col in self.AttrByCol.keys():
                self.table.hideColumn(col)
            self._showattrbtn.setText("Show plot styles >>")
        else:
            self._attrs_shown = True
            self.table.showColumn(self.ColApply)
            for col in self.AttrByCol.keys():
                self.table.showColumn(col)
            self._showattrbtn.setText("<< Hide plot styles")
コード例 #2
0
class PluginObject(object):

   tabName = 'Exchange Rates'
   maxVersion = '0.92'
   
   #############################################################################
   def __init__(self, main):

      self.main = main

      ##########################################################################
      ##### Display the conversion values based on the Coinbase API
      self.lastPriceFetch = 0
      self.lblHeader    = QRichLabel(tr("""<b>Tracking buy and sell prices on 
         Coinbase every 60 seconds</b>"""), doWrap=False)
      self.lblLastTime  = QRichLabel('', doWrap=False)
      self.lblSellLabel = QRichLabel(tr('Coinbase <b>Sell</b> Price (USD):'), doWrap=False)
      self.lblBuyLabel  = QRichLabel(tr('Coinbase <b>Buy</b> Price (USD):'),  doWrap=False)
      self.lblSellPrice = QRichLabel('<Not Available>')
      self.lblBuyPrice  = QRichLabel('<Not Available>')

      self.lastSellStr = ''
      self.lastBuyStr = ''

      self.btnUpdate = QPushButton(tr('Check Now'))
      self.main.connect(self.btnUpdate, SIGNAL('clicked()'), self.checkUpdatePrice)

      ##########################################################################
      ##### A calculator for converting prices between USD and BTC
      lblCalcTitle = QRichLabel(tr("""Convert between USD and BTC using 
         Coinbase sell price"""), hAlign=Qt.AlignHCenter, doWrap=False)
      self.edtEnterUSD = QLineEdit()
      self.edtEnterBTC = QLineEdit()
      self.lblEnterUSD1 = QRichLabel('$')
      self.lblEnterUSD2 = QRichLabel('USD')
      self.lblEnterBTC = QRichLabel('BTC')
      btnClear = QPushButton('Clear')

      self.main.connect(self.edtEnterUSD, SIGNAL('textEdited(QString)'), self.updateCalcBTC)
      self.main.connect(self.edtEnterBTC, SIGNAL('textEdited(QString)'), self.updateCalcUSD)

      def clearCalc():
         self.edtEnterUSD.setText('')
         self.edtEnterBTC.setText('')

      self.main.connect(btnClear, SIGNAL('clicked()'), clearCalc)

      frmCalcMid = makeHorizFrame( [self.lblEnterUSD1,
                                    self.edtEnterUSD,
                                    self.lblEnterUSD2,
                                    'Stretch',
                                    self.edtEnterBTC,
                                    self.lblEnterBTC])

      frmCalcClear = makeHorizFrame(['Stretch', btnClear, 'Stretch'])
      frmCalc = makeVertFrame([lblCalcTitle, frmCalcMid, frmCalcClear], STYLE_PLAIN)

      ##########################################################################
      ##### A table showing you the total balance of each wallet in USD and BTC
      lblWltTableTitle = QRichLabel(tr("Wallet balances converted to USD"), 
                                            doWrap=False, hAlign=Qt.AlignHCenter)
      numWallets = len(self.main.walletMap)
      self.wltTable = QTableWidget(self.main)
      self.wltTable.setRowCount(numWallets)
      self.wltTable.setColumnCount(4)
      self.wltTable.horizontalHeader().setStretchLastSection(True)
      self.wltTable.setMinimumWidth(600)


      ##########################################################################
      ##### Setup the main layout for the tab
      mainLayout = QGridLayout()
      i=0
      mainLayout.addWidget(self.lblHeader,      i,0,  1,3)
      i+=1
      mainLayout.addItem(QSpacerItem(15,15),    i,0)
      mainLayout.addWidget(self.lblSellLabel,   i,1)
      mainLayout.addWidget(self.lblSellPrice,   i,2)
      i+=1
      mainLayout.addItem(QSpacerItem(15,15),    i,0)
      mainLayout.addWidget(self.lblBuyLabel,    i,1)
      mainLayout.addWidget(self.lblBuyPrice,    i,2)
      i+=1
      mainLayout.addWidget(self.lblLastTime,    i,0,  1,2)
      mainLayout.addWidget(self.btnUpdate,      i,2)
      i+=1
      mainLayout.addItem(QSpacerItem(20,20),    i,0)
      i+=1
      mainLayout.addWidget(frmCalc,             i,0,  1,3)
      i+=1
      mainLayout.addItem(QSpacerItem(30,30),    i,0)
      i+=1
      mainLayout.addWidget(lblWltTableTitle,    i,0,  1,3)
      i+=1
      mainLayout.addWidget(self.wltTable,       i,0,  1,3)

      mainLayout.setColumnStretch(0,0)
      mainLayout.setColumnStretch(1,1)
      mainLayout.setColumnStretch(2,1)
      tabWidget = QWidget()
      tabWidget.setLayout(mainLayout)

      frmH = makeHorizFrame(['Stretch', tabWidget, 'Stretch'])
      frm  = makeVertFrame(['Space(20)', frmH, 'Stretch'])


      # Now set the scrollarea widget to the layout
      self.tabToDisplay = QScrollArea()
      self.tabToDisplay.setWidgetResizable(True)
      self.tabToDisplay.setWidget(frm)


   #############################################################################
   def getTabToDisplay(self):
      return self.tabToDisplay


   #############################################################################
   def addCommasToPrice(self, pstr):
      dispStr = pstr.strip().split('.')[0] 
      dispStr = ','.join([dispStr[::-1][3*i:3*(i+1)][::-1] \
                            for i in range((len(dispStr)-1)/3+1)][::-1])
      if '.' in pstr:
         dispStr = dispStr + '.' + pstr.split('.')[1]
      return dispStr


   #############################################################################
   def fetchFormattedPrice(self, url):
      sock = urllib2.urlopen(url)
      value = ast.literal_eval(sock.read())['subtotal']['amount']
      return self.addCommasToPrice(value)



   #############################################################################
   def checkUpdatePrice(self):

      urlBase = 'http://coinbase.com/api/v1/prices/'
      urlSell = urlBase + 'sell'
      urlBuy  = urlBase + 'buy'

      try:
         self.lastSellStr = self.fetchFormattedPrice(urlSell)
         self.lastBuyStr  = self.fetchFormattedPrice(urlBuy)
         
         self.lblSellPrice.setText('<b><font color="%s">$%s</font> / BTC</b>' % \
                                           (htmlColor('TextBlue'), self.lastSellStr))
         self.lblBuyPrice.setText( '<b><font color="%s">$%s</font> / BTC</b>' % \
                                           (htmlColor('TextBlue'), self.lastBuyStr))
      
         self.lastPriceFetch = RightNow()

         self.updateLastTimeStr()
         self.updateWalletTable()
         self.updateCalcUSD(self.edtEnterBTC.text())
      except:
         #LOGEXCEPT('Failed to fetch price data from %s' % urlBase)
         pass

   

   #############################################################################
   def updateLastTimeStr(self):
      secs = RightNow() - self.lastPriceFetch
      tstr = 'Less than 1 min'
      if secs > 60:
         tstr = secondsToHumanTime(secs)

      self.lblLastTime.setText(tr("""<font color="%s">Last updated:  
         %s ago</font>""") % (htmlColor('DisableFG'), tstr))

   #############################################################################
   def injectGoOnlineFunc(self, topBlock):
      self.checkUpdatePrice()

   #############################################################################
   def injectHeartbeatAlwaysFunc(self):
      # Check the price every 60 seconds, update widgets
      self.updateLastTimeStr()
      if RightNow() < self.lastPriceFetch+60:
         return

      self.lastPriceFetch = RightNow()
      self.checkUpdatePrice() 


   #############################################################################
   def updateCalcUSD(self, newBTCVal):
      try:
         convertVal = float(self.lastSellStr.replace(',',''))
         usdVal = convertVal * float(newBTCVal.replace(',',''))
         self.edtEnterUSD.setText(self.addCommasToPrice('%0.2f' % usdVal))
      except:
         self.edtEnterUSD.setText('')
         
   #############################################################################
   def updateCalcBTC(self, newUSDVal):
      try:
         convertVal = float(self.lastSellStr.replace(',',''))
         btcVal = float(newUSDVal.replace(',','')) / convertVal
         self.edtEnterBTC.setText(self.addCommasToPrice('%0.8f' % btcVal))
      except:
         self.edtEnterBTC.setText('')
      
      
   #############################################################################
   def updateWalletTable(self):
      numWallets = len(self.main.walletMap)
      self.wltTable.setRowCount(numWallets)
      self.wltTable.setColumnCount(4)

      row = 0
      for wltID,wltObj in self.main.walletMap.iteritems():
         wltValueBTC = '(...)'
         wltValueUSD = '(...)'
         if TheBDM.getBDMState()=='BlockchainReady':
            convertVal = float(self.lastSellStr.replace(',',''))
            wltBal = wltObj.getBalance('Total')
            wltValueBTC = coin2str(wltBal, maxZeros=2)
            wltValueUSD = '$' + self.addCommasToPrice('%0.2f' % (wltBal*convertVal/1e8))

         rowItems = []
         rowItems.append(QTableWidgetItem(wltID))
         rowItems.append(QTableWidgetItem(wltObj.labelName))
         rowItems.append(QTableWidgetItem(wltValueBTC))
         rowItems.append(QTableWidgetItem(wltValueUSD))

         rowItems[-2].setTextAlignment(Qt.AlignRight)
         rowItems[-1].setTextAlignment(Qt.AlignRight)
         rowItems[-2].setFont(GETFONT('Fixed', 10))
         rowItems[-1].setFont(GETFONT('Fixed', 10))

         for i,item in enumerate(rowItems):
            self.wltTable.setItem(row, i, item)
            item.setFlags(Qt.NoItemFlags)

         self.wltTable.setHorizontalHeaderItem(0, QTableWidgetItem(tr('Wallet ID')))
         self.wltTable.setHorizontalHeaderItem(1, QTableWidgetItem(tr('Wallet Name')))
         self.wltTable.setHorizontalHeaderItem(2, QTableWidgetItem(tr('BTC Balance')))
         self.wltTable.setHorizontalHeaderItem(3, QTableWidgetItem(tr('USD ($) Value')))
         self.wltTable.verticalHeader().hide()

         row += 1
コード例 #3
0
ファイル: SkyModelTreeWidget.py プロジェクト: ska-sa/tigger
class ModelGroupsTable(QWidget):
    EditableAttrs = [attr for attr in PlotStyles.StyleAttributes if attr in PlotStyles.StyleAttributeOptions]
    ColList = 3
    ColPlot = 4
    ColApply = 5
    AttrByCol = dict([(i + 6, attr) for i, attr in enumerate(EditableAttrs)])

    def __init__(self, parent, *args):
        QWidget.__init__(self, parent, *args)
        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        lo = QVBoxLayout(self)
        lo.setContentsMargins(0, 0, 0, 0)
        lo1 = QHBoxLayout()
        lo.addLayout(lo1)
        lo1.setContentsMargins(0, 0, 0, 0)
        lbl = QLabel(QString("<nobr><b>Source groupings:</b></nobr>"), self)
        lo1.addWidget(lbl, 0)
        lo1.addStretch(1)
        # add show/hide button
        self._showattrbtn = QPushButton(self)
        self._showattrbtn.setMinimumWidth(256)
        lo1.addWidget(self._showattrbtn, 0)
        lo1.addStretch()
        QObject.connect(self._showattrbtn, SIGNAL("clicked()"), self._togglePlotControlsVisibility)
        # add table
        self.table = QTableWidget(self)
        lo.addWidget(self.table)
        QObject.connect(self.table, SIGNAL("cellChanged(int,int)"), self._valueChanged)
        self.table.setSelectionMode(QTableWidget.NoSelection)
        # setup basic columns
        self.table.setColumnCount(6 + len(self.EditableAttrs))
        for i, label in enumerate(("grouping", "total", "selection", "list", "plot", "style")):
            self.table.setHorizontalHeaderItem(i, QTableWidgetItem(label))
        self.table.horizontalHeader().setSectionHidden(self.ColApply, True)
        # setup columns for editable grouping attributes
        for i, attr in self.AttrByCol.items():
            self.table.setHorizontalHeaderItem(i, QTableWidgetItem(PlotStyles.StyleAttributeLabels[attr]))
            self.table.horizontalHeader().setSectionHidden(i, True)
        self.table.verticalHeader().hide()
        # other internal init
        self._attrs_shown = False
        self._togglePlotControlsVisibility()
        self.model = None
        self._setting_model = False
        self._currier = PersistentCurrier()
        # row of 'selected' grouping
        self._irow_selgroup = 0

    def clear(self):
        self.table.setRowCount(0)
        self.model = None

    # setup mappings from the group.show_plot attribute to check state
    ShowAttrToCheckState = {PlotStyles.ShowNot: Qt.Unchecked,
                            PlotStyles.ShowDefault: Qt.PartiallyChecked,
                            PlotStyles.ShowAlways: Qt.Checked}
    CheckStateToShowAttr = dict([(val, key) for key, val in ShowAttrToCheckState.items()])

    def _makeCheckItem(self, name, group, attr):
        item = QTableWidgetItem(name)
        if group is self.model.defgroup:
            item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable)
            item.setCheckState(Qt.Checked if getattr(group.style, attr) else Qt.Unchecked)
        else:
            item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable | Qt.ItemIsTristate)
            item.setCheckState(self.ShowAttrToCheckState[getattr(group.style, attr)])
        return item

    def _updateModel(self, what=SkyModel.UpdateAll, origin=None):
        if origin is self or not what & (SkyModel.UpdateTags | SkyModel.UpdateGroupStyle):
            return
        model = self.model
        self._setting_model = True;  # to ignore cellChanged() signals (in valueChanged())
        # _item_cb is a dict (with row,col keys) containing the widgets (CheckBoxes ComboBoxes) per each cell
        self._item_cb = {}
        # lists of "list" and "plot" checkboxes per each grouping (excepting the default grouping); each entry is an (row,col,item) tuple.
        # used as argument to self._showControls()
        self._list_controls = []
        self._plot_controls = []
        # list of selection callbacks (to which signals are connected)
        self._callbacks = []
        # set requisite number of rows,and start filling
        self.table.setRowCount(len(model.groupings))
        for irow, group in enumerate(model.groupings):
            self.table.setItem(irow, 0, QTableWidgetItem(group.name))
            if group is model.selgroup:
                self._irow_selgroup = irow
            # total # source in group: skip for "current"
            if group is not model.curgroup:
                self.table.setItem(irow, 1, QTableWidgetItem(str(group.total)))
            # selection controls: skip for current and selection
            if group not in (model.curgroup, model.selgroup):
                btns = QWidget()
                lo = QHBoxLayout(btns)
                lo.setContentsMargins(0, 0, 0, 0)
                lo.setSpacing(0)
                # make selector buttons (depending on which group we're in)
                if group is model.defgroup:
                    Buttons = (
                        ("+", lambda src, grp=group: True, "select all sources"),
                        ("-", lambda src, grp=group: False, "unselect all sources"))
                else:
                    Buttons = (
                        ("=", lambda src, grp=group: grp.func(src), "select only this grouping"),
                        ("+", lambda src, grp=group: src.selected or grp.func(src), "add grouping to selection"),
                        ("-", lambda src, grp=group: src.selected and not grp.func(src),
                         "remove grouping from selection"),
                        ("&&", lambda src, grp=group: src.selected and grp.func(src),
                         "intersect selection with grouping"))
                lo.addStretch(1)
                for label, predicate, tooltip in Buttons:
                    btn = QToolButton(btns)
                    btn.setText(label)
                    btn.setMinimumWidth(24)
                    btn.setMaximumWidth(24)
                    btn.setToolTip(tooltip)
                    lo.addWidget(btn)
                    # add callback
                    QObject.connect(btn, SIGNAL("clicked()"), self._currier.curry(self.selectSources, predicate))
                lo.addStretch(1)
                self.table.setCellWidget(irow, 2, btns)
            # "list" checkbox (not for current and selected groupings: these are always listed)
            if group not in (model.curgroup, model.selgroup):
                item = self._makeCheckItem("", group, "show_list")
                self.table.setItem(irow, self.ColList, item)
                item.setToolTip("""<P>If checked, sources in this grouping will be listed in the source table. If un-checked, sources will be
            excluded from the table. If partially checked, then the default list/no list setting of "all sources" will be in effect.
            </P>""")
            # "plot" checkbox (not for the current grouping, since that's always plotted)
            if group is not model.curgroup:
                item = self._makeCheckItem("", group, "show_plot")
                self.table.setItem(irow, self.ColPlot, item)
                item.setToolTip("""<P>If checked, sources in this grouping will be included in the plot. If un-checked, sources will be
            excluded from the plot. If partially checked, then the default plot/no plot setting of "all sources" will be in effect.
            </P>""")
            # custom style control
            # for default, current and selected, this is just a text label
            if group is model.defgroup:
                item = QTableWidgetItem("default:")
                item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
                item.setToolTip(
                    """<P>This is the default plot style used for all sources for which a custom grouping style is not selected.</P>""")
                self.table.setItem(irow, self.ColApply, item)
            elif group is model.curgroup:
                item = QTableWidgetItem("")
                item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
                item.setToolTip("""<P>This is the plot style used for the highlighted source, if any.</P>""")
                self.table.setItem(irow, self.ColApply, item)
            elif group is model.selgroup:
                item = QTableWidgetItem("")
                item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
                item.setToolTip("""<P>This is the plot style used for the currently selected sources.</P>""")
                self.table.setItem(irow, self.ColApply, item)
            # for the rest, a combobox with custom priorities
            else:
                cb = QComboBox()
                cb.addItems(["default"] + ["custom %d" % p for p in range(1, 10)])
                index = max(0, min(group.style.apply, 9))
                #        dprint(0,group.name,"apply",index)
                cb.setCurrentIndex(index)
                QObject.connect(cb, SIGNAL("activated(int)"),
                                self._currier.xcurry(self._valueChanged, (irow, self.ColApply)))
                self.table.setCellWidget(irow, self.ColApply, cb)
                cb.setToolTip("""<P>This controls whether sources within this group are plotted with a customized
            plot style. Customized styles have numeric priority; if a source belongs to multiple groups, then
            the style with the lowest priority takes precedence.<P>""")
            # attribute comboboxes
            for icol, attr in self.AttrByCol.items():
                # get list of options for this style attribute. If dealing with first grouping (i==0), which is
                # the "all sources" grouping, then remove the "default" option (which is always first in the list)
                options = PlotStyles.StyleAttributeOptions[attr]
                if irow == 0:
                    options = options[1:]
                # make combobox
                cb = QComboBox()
                cb.addItems(list(map(str, options)))
                # the "label" option is also editable
                if attr == "label":
                    cb.setEditable(True)
                try:
                    index = options.index(getattr(group.style, attr))
                    cb.setCurrentIndex(index)
                except ValueError:
                    cb.setEditText(str(getattr(group.style, attr)))
                slot = self._currier.xcurry(self._valueChanged, (irow, icol))
                QObject.connect(cb, SIGNAL("activated(int)"), slot)
                QObject.connect(cb, SIGNAL("editTextChanged(const QString &)"), slot)
                cb.setEnabled(group is model.defgroup or group.style.apply)
                self.table.setCellWidget(irow, icol, cb)
                label = attr
                if irow:
                    cb.setToolTip("""<P>This is the %s used to plot sources in this group, when a "custom" style for the group
          is enabled via the style control.<P>""" % label)
                else:
                    cb.setToolTip(
                        "<P>This is the default %s used for all sources for which a custom style is not specified below.<P>" % label)
        self.table.resizeColumnsToContents()
        # re-enable processing of cellChanged() signals
        self._setting_model = False

    def setModel(self, model):
        self.model = model
        self.model.connect("updated", self._updateModel)
        self.model.connect("selected", self.updateModelSelection)
        self._updateModel(SkyModel.UpdateAll)

    def _valueChanged(self, row, col):
        """Called when a cell has been edited"""
        if self._setting_model:
            return
        group = self.model.groupings[row]
        item = self.table.item(row, col)
        if col == self.ColList:
            if group is not self.model.defgroup:
                # tri-state items go from unchecked to checked when user clicks them. Make them partially checked instead.
                if group.style.show_list == PlotStyles.ShowNot and item.checkState() == Qt.Checked:
                    item.setCheckState(Qt.PartiallyChecked)
            group.style.show_list = self.CheckStateToShowAttr[item.checkState()]
            self.model.emitChangeGroupingVisibility(group, origin=self)
            return
        elif col == self.ColPlot:
            if group is not self.model.defgroup:
                # tri-state items go from unchecked to checked by default. Make them partially checked instead.
                if group.style.show_plot == PlotStyles.ShowNot and item.checkState() == Qt.Checked:
                    item.setCheckState(Qt.PartiallyChecked)
            group.style.show_plot = self.CheckStateToShowAttr[item.checkState()]
        elif col == self.ColApply:
            group.style.apply = self.table.cellWidget(row, col).currentIndex()
            # enable/disable editable cells
            for j in list(self.AttrByCol.keys()):
                item1 = self.table.item(row, j)
                if item1:
                    fl = item1.flags() & ~Qt.ItemIsEnabled
                    if group.style.apply:
                        fl |= Qt.ItemIsEnabled
                    item1.setFlags(fl)
                cw = self.table.cellWidget(row, j)
                cw and cw.setEnabled(group.style.apply)
        elif col in self.AttrByCol:
            cb = self.table.cellWidget(row, col)
            txt = str(cb.currentText())
            attr = self.AttrByCol[col]
            if txt == "default":
                setattr(group.style, attr, PlotStyles.DefaultValue)
            else:
                setattr(group.style, attr, PlotStyles.StyleAttributeTypes.get(attr, str)(txt))
        # all other columns: return so we don't emit a signal
        else:
            return
        # in all cases emit a signal
        self.model.emitChangeGroupingStyle(group, origin=self)

    def selectSources(self, predicate):
        """Selects sources according to predicate(src)"""
        busy = BusyIndicator()
        for src in self.model.sources:
            src.selected = predicate(src)
        self.model.emitSelection(origin=self)
        busy = None

    def updateModelSelection(self, nsel, origin=None):
        """This is called when some other widget changes the set of selected model sources"""
        self.table.clearSelection()
        if self.model:
            self.table.item(self._irow_selgroup, 1).setText(str(nsel))

    def _togglePlotControlsVisibility(self):
        if self._attrs_shown:
            self._attrs_shown = False
            self.table.hideColumn(self.ColApply)
            for col in self.AttrByCol.keys():
                self.table.hideColumn(col)
            self._showattrbtn.setText("Show plot styles >>")
        else:
            self._attrs_shown = True
            self.table.showColumn(self.ColApply)
            for col in self.AttrByCol.keys():
                self.table.showColumn(col)
            self._showattrbtn.setText("<< Hide plot styles")