Esempio n. 1
0
    def __init__(self, parent):
        super(TextHighlighter, self).__init__(parent)

        self.__highlightingRules = []

        fmt = QtGui.QTextCharFormat()
        fmt.setForeground(QtCore.Qt.white)
        fmt.setBackground(QtCore.Qt.darkGreen)
        pattern = QtCore.QRegExp("", QtCore.Qt.CaseInsensitive)
        self.__foundMatchFormat = (fmt, pattern)

        fmt = QtGui.QTextCharFormat()
        fmt.setForeground(QtGui.QColor(QtCore.Qt.red).lighter(115))
        errors = ["error", "critical", "failed", "fail", "crashed", "crash"]
        for pattern in errors:
            rx = QtCore.QRegExp(r'\b%s\b' % pattern, QtCore.Qt.CaseInsensitive)
            rule = (fmt, rx)
            self.__highlightingRules.append(rule)

        fmt = QtGui.QTextCharFormat()
        fmt.setForeground(QtGui.QColor(255, 168, 0))
        for pattern in ("warning", "warn"):
            rx = QtCore.QRegExp(r'\b%s\b' % pattern, QtCore.Qt.CaseInsensitive)
            rule = (fmt, rx)
            self.__highlightingRules.append(rule)
Esempio n. 2
0
 def parent(self, index):
     if not index.isValid():
         return QtCore.QModelIndex()
     node = index.internalPointer()
     parent = node.parent()
     if parent is None:
         return QtCore.QModelIndex()
     else:
         return self.createIndex(parent.row, 0, parent)
Esempio n. 3
0
    def paintEvent(self, event):

        total_width = self.width() - self.Margins[2]
        total_height = self.height() - self.Margins[3]
        total_tasks = float(self.__totals.total)

        bar = []
        for i, v in enumerate(self.__values):
            if v == 0:
                continue
            bar.append((total_width * (v / total_tasks),
                        constants.COLOR_TASK_STATE[i + 1]))

        painter = QtGui.QPainter()
        painter.begin(self)
        painter.setRenderHints(painter.HighQualityAntialiasing
                               | painter.SmoothPixmapTransform
                               | painter.Antialiasing)
        painter.setPen(self.__PEN)

        move = 0
        for width, color in bar:
            painter.setBrush(color)
            rect = QtCore.QRectF(self.Margins[0], self.Margins[1], total_width,
                                 total_height)
            if move:
                rect.setLeft(move)
            move += width
            painter.drawRoundedRect(rect, 3, 3)
        painter.end()
        event.accept()
Esempio n. 4
0
    def paintEvent(self, event):

        total_width = self.width() - self.Margins[2]
        total_height = self.height() - self.Margins[3]
        total_tasks = float(self.__totals.total)

        vals = self.__values
        bar = [(total_width * (val / total_tasks), color)
               for val, color in vals if val != 0]

        painter = QtGui.QPainter()
        painter.begin(self)
        painter.setRenderHints(painter.HighQualityAntialiasing
                               | painter.SmoothPixmapTransform
                               | painter.Antialiasing)
        painter.setPen(self.__PEN)

        move = 0
        x, y = self.Margins[:2]
        for width, color in reversed(bar):
            painter.setBrush(color)
            rect = QtCore.QRectF(x, y, total_width, total_height)
            if move:
                rect.setLeft(move)
            move += width
            painter.drawRoundedRect(rect, 3, 3)
        painter.end()
        event.accept()
Esempio n. 5
0
    def itemsRect(self):
        """
        Return a QRect of the specific boundary of the items in the layout
        """
        count = self._itemLayout.count()
        if not count:
            return QtCore.QRect(0,0,0,0)

        first = self._itemLayout.itemAt(0).widget()

        if count == 1:
            rect = first.geometry()
            return QtCore.QRect(rect.topLeft(), rect.bottomRight())

        last = self._itemLayout.itemAt(count-1).widget()
        return QtCore.QRect(first.geometry().topLeft(), last.geometry().bottomRight())
Esempio n. 6
0
    def __init__(self, parent=None):
        super(FileWatcher, self).__init__(parent)

        self.__files = {}

        self.__timer = QtCore.QTimer(self)
        self.__timer.setInterval(5000)
        self.__timer.timeout.connect(self.checkFiles)
Esempio n. 7
0
    def mouseMoveEvent(self, event):
        startDrag = QtGui.QApplication.startDragDistance()

        if (event.pos() - self.__dragStartPos).manhattanLength() < startDrag:
            return

        mimeData = QtCore.QMimeData()
        data = cPickle.dumps(self.mapToParent(self.__dragStartPos))
        mimeData.setData("application/x-DragDropList", QtCore.QByteArray(data))

        pix = QtGui.QPixmap(self.size())
        self.render(pix)

        drag = QtGui.QDrag(self)
        drag.setMimeData(mimeData)
        drag.setPixmap(pix)
        drag.setHotSpot(event.pos())
        drag.exec_(QtCore.Qt.MoveAction)
Esempio n. 8
0
    def setRefreshTime(self, value):
        value = int(value)

        if self.__refreshTimer is None:
            self.__refreshTimer = QtCore.QTimer(self)
            self.__refreshTimer.timeout.connect(self.__refresh)

        self.__refreshTimer.stop()
        self.__refreshTimer.start(max(value, 1) * 1000)
Esempio n. 9
0
 def _lastIndex(self):
     """Index of the very last item in the tree.
     """
     currentIndex = QtCore.QModelIndex()
     rowCount = self.rowCount(currentIndex)
     while rowCount > 0:
         currentIndex = self.index(rowCount - 1, 0, currentIndex)
         rowCount = self.rowCount(currentIndex)
     return currentIndex
Esempio n. 10
0
class CheckableComboBox(QtGui.QWidget):
    """
    A combo box with selectable items.
    """

    optionSelected = QtCore.Signal(str)

    def __init__(self, title, options, selected=None, icons=None, parent=None):
        QtGui.QWidget.__init__(self, parent)
        layout = QtGui.QVBoxLayout(self)

        self.__btn = btn = QtGui.QPushButton(title)
        btn.setFocusPolicy(QtCore.Qt.NoFocus)
        btn.setMaximumHeight(22)
        btn.setFlat(True)
        btn.setContentsMargins(0, 0, 0, 0)

        self.__menu = menu = QtGui.QMenu(self)
        btn.setMenu(menu)

        self.setOptions(options, selected, icons)

        layout.addWidget(btn)

        btn.toggled.connect(btn.showMenu)
        menu.triggered.connect(
            lambda action: self.optionSelected.emit(action.text()))

    def options(self):
        return [a.text() for a in self.__menu.actions()]

    def setOptions(self, options, selected=None, icons=None):
        if selected and not isinstance(selected, (set, dict)):
            selected = set(selected)

        menu = self.__menu
        menu.clear()

        for opt, icon in izip_longest(options, icons or []):
            a = QtGui.QAction(menu)
            a.setText(opt)
            a.setCheckable(True)
            if selected and opt in selected:
                a.setChecked(True)
            if icon:
                a.setIcon(icons[i])
            menu.addAction(a)

    def setSelected(self, options):
        opts = set(options)
        for action in self.__menu.actions():
            checked = action.text() in opts
            action.setChecked(checked)

    def selectedOptions(self):
        return [a.text() for a in self.__menu.actions() if a.isChecked()]
Esempio n. 11
0
 def findIndex(self, rowPath):
     """Returns the QtCore.QModelIndex at `rowPath`
     
     `rowPath` is a sequence of node rows. For example, [1, 2, 1] is the 2nd child of the
     3rd child of the 2nd child of the root.
     """
     result = QtCore.QModelIndex()
     for row in rowPath:
         result = self.index(row, 0, result)
     return result
Esempio n. 12
0
    def highlightBlock(self, text):
        for fmt, pattern in self.__highlightingRules:
            expression = QtCore.QRegExp(pattern)
            index = expression.indexIn(text)
            while index >= 0:
                length = expression.matchedLength()
                self.setFormat(index, length, fmt)
                index = expression.indexIn(text, index + length)

        fmt, pattern = self.__foundMatchFormat
        if pattern.isEmpty():
            return

        expression = QtCore.QRegExp(pattern)
        index = expression.indexIn(text)
        while index >= 0:
            length = expression.matchedLength()
            self.setFormat(index, length, fmt)
            index = expression.indexIn(text, index + length)
Esempio n. 13
0
    def __init__(self, parent=None):
        QtCore.QAbstractTableModel.__init__(self, parent)
        self.__tasks = []
        self.__index = {}
        self.__jobId = None
        self.__lastUpdateTime = 0

        # A timer for refreshing duration column.
        self.__timer = QtCore.QTimer(self)
        self.__timer.setInterval(1000)
        self.__timer.timeout.connect(self.__durationRefreshTimer)
Esempio n. 14
0
    def _openPanelSettingsDialog(self):
        w = self.widget()
        if not w:
            return

        pos = QtGui.QCursor.pos()
        menu = QtGui.QMenu(w)
        action = menu.addAction("Multi-Tab Mode")
        action.setCheckable(True)
        action.setChecked(int(self.getAttr("multiTabMode")))
        action.toggled.connect(self.__multiTabModeChanged)
        menu.popup(pos + QtCore.QPoint(5,5))
Esempio n. 15
0
class AlnumSortProxyModel(QtGui.QSortFilterProxyModel):

    RX_ALNUMS = QtCore.QRegExp('(\d+|\D+)')

    def __init__(self, *args, **kwargs):
        super(AlnumSortProxyModel, self).__init__(*args, **kwargs)
        self.setSortRole(DATA_ROLE)

    def lessThan(self, left, right):
        sortRole = self.sortRole()
        leftData = left.data(sortRole)
        if isinstance(leftData, (str, unicode)):
            rightData = right.data(sortRole)
            return self.lessThanAlphaNumeric(leftData, rightData)

        return super(AlnumSortProxyModel, self).lessThan(left, right)

    def lessThanAlphaNumeric(self, left, right):
        if left == right:
            return False

        alnums = self.RX_ALNUMS
        leftList = []
        rightList = []

        pos = 0
        while True:
            pos = alnums.indexIn(left, pos)
            if pos == -1:
                break

            leftList.append(alnums.cap(1))
            pos += alnums.matchedLength()

        pos = 0
        while True:
            pos = alnums.indexIn(right, pos)
            if pos == -1:
                break

            rightList.append(alnums.cap(1))
            pos += alnums.matchedLength()

        for leftItem, rightItem in zip(leftList, rightList):
            if leftItem != rightItem and leftItem.isdigit(
            ) and rightItem.isdigit():
                return int(leftItem) < int(rightItem)

            if leftItem != rightItem:
                return leftItem < rightItem

        return left < right
Esempio n. 16
0
    def refresh(self):
        updated = set()
        to_add = set()
        object_ids = set()

        rows = self._index
        columnCount = self.columnCount()
        parent = QtCore.QModelIndex()

        objects = self.fetchObjects()

        # Update existing
        for obj in objects:
            object_ids.add(obj.id)

            try:
                idx = self._index[obj.id]
                self._items[idx] = obj
                updated.add(obj.id)
                self.dataChanged.emit(self.index(idx, 0),
                                      self.index(idx, columnCount - 1))

            except (IndexError, KeyError):
                to_add.add(obj)

        # Add new
        if to_add:
            size = len(to_add)
            start = len(self._items)
            end = start + size - 1
            self.beginInsertRows(parent, start, end)
            self._items.extend(to_add)
            self.endInsertRows()
            LOGGER.debug("adding %d new objects", size)

        # Remove missing
        if self.refreshShouldRemove:
            to_remove = set(self._index.iterkeys()).difference(object_ids)
            if to_remove:
                row_ids = ((rows[old_id], old_id) for old_id in to_remove)

                for row, old_id in sorted(row_ids, reverse=True):

                    self.beginRemoveRows(parent, row, row)
                    obj = self._items.pop(row)
                    self.endRemoveRows()

                    LOGGER.debug("removing %s %s", old_id, obj.name)

        # reindex the items
        self._index = dict(
            ((item.id, i) for i, item in enumerate(self._items)))
Esempio n. 17
0
    def checkFiles(self):
        if not self.__files:
            return

        info = QtCore.QFileInfo()

        for path, mtime in self.__files.iteritems():
            info.setFile(path)
            test_mtime = info.lastModified()
            if mtime != test_mtime:
                self.__files[path] = test_mtime
                LOGGER.debug("Log file modified: (%r) '%s'", test_mtime, path)
                self.fileChanged.emit(path)
Esempio n. 18
0
 def index(self, row, column, parent):
     if not self.subnodes:
         return QtCore.QModelIndex()
     node = parent.internalPointer() if parent.isValid() else self
     try:
         return self.createIndex(row, column, node.subnodes[row])
     except IndexError:
         logging.debug(
             "Wrong tree index called (%r, %r, %r). Returning DummyNode",
             row, column, node)
         parentNode = parent.internalPointer() if parent.isValid() else None
         dummy = self._createDummyNode(parentNode, row)
         self._dummyNodes.add(dummy)
         return self.createIndex(row, column, dummy)
Esempio n. 19
0
 def refreshData(self):
     """Updates the data on all nodes, but without having to perform a full reset.
     
     A full reset on a tree makes us lose selection and expansion states. When all we ant to do
     is to refresh the data on the nodes without adding or removing a node, a call on
     dataChanged() is better. But of course, Qt makes our life complicated by asking us topLeft
     and bottomRight indexes. This is a convenience method refreshing the whole tree.
     """
     columnCount = self.columnCount()
     topLeft = self.index(0, 0, QtCore.QModelIndex())
     bottomLeft = self._lastIndex()
     bottomRight = self.sibling(bottomLeft.row(), columnCount - 1,
                                bottomLeft)
     self.dataChanged.emit(topLeft, bottomRight)
Esempio n. 20
0
    def __init__(self, parent=None):
        super(TaskModel, self).__init__(parent)

        self.__jobId = None
        self.__lastUpdateTime = 0

        # Tasks are updated incrementally, so don't
        # remove missing ones
        self.refreshShouldRemove = False

        # A timer for refreshing duration column.
        self.__timer = QtCore.QTimer(self)
        self.__timer.setInterval(1000)
        self.__timer.timeout.connect(self.__durationRefreshTimer)
Esempio n. 21
0
    def paint(self, painter, opts, index):
        job = index.data(self._role)
        if not job:
            super(JobProgressDelegate, self).paint(painter, opts, index)
            return

        state = plow.client.TaskState
        colors = constants.COLOR_TASK_STATE
        totals = job.totals

        values = [
            (totals.dead, colors[state.DEAD]),
            (totals.eaten, colors[state.EATEN]),
            (totals.waiting, colors[state.WAITING]),
            (totals.depend, colors[state.DEPEND]),
            (totals.running, colors[state.RUNNING]),
            (totals.succeeded, colors[state.SUCCEEDED]),
        ]

        rect = opts.rect
        total_width = rect.width() - self.MARGINS[2]
        total_height = rect.height() - self.MARGINS[3]
        total_tasks = float(totals.total)

        bar = [(total_width * (val / total_tasks), color)
               for val, color in values if val != 0]

        painter.setRenderHints(painter.HighQualityAntialiasing
                               | painter.SmoothPixmapTransform
                               | painter.Antialiasing)

        # self.drawBackground(painter, opt, index)

        painter.setPen(self.PEN)

        x, y = rect.x(), rect.y()
        x += self.MARGINS[0]
        y += self.MARGINS[1]

        move = 0

        for width, color in reversed(bar):
            painter.setBrush(color)
            rect = QtCore.QRectF(x, y, total_width, total_height)
            if move:
                rect.setLeft(x + move)
            move += width
            painter.drawRoundedRect(rect, 3, 3)
Esempio n. 22
0
    def __init__(self, value, parent=None):
        QtGui.QWidget.__init__(self, parent)
        layout = QtGui.QGridLayout(self)
        layout.setSpacing(0)
        layout.setContentsMargins(0, 0, 0, 0)
        self._widget = None

        self.__status = QtGui.QLabel(self)
        self.__status.setContentsMargins(5, 0, 0, 0)
        layout.addWidget(self.__status, 0, 2)

        if not FormWidget.__LOCKED_PIX:
            FormWidget.__LOCKED_PIX = QtGui.QPixmap(":/images/locked.png")
            FormWidget.__LOCKED_PIX = FormWidget.__LOCKED_PIX.scaled(
                QtCore.QSize(12, 12), QtCore.Qt.KeepAspectRatio,
                QtCore.Qt.SmoothTransformation)
Esempio n. 23
0
    def __init__(self, name, ptype, parent=None):
        QtGui.QDockWidget.__init__(self, parent)

        # Add the standard dock action buttons in.
        # TODO: hook up signals
        self.__label = QtGui.QLabel(self)
        self.__label.setIndent(10)
        self.__name = None
        self.__ptype = ptype
        self.setName(name)

        self.attrs = {}

        self.__refreshTimer = None

        # Note: the widet in the panel adds more buttons
        # to this toolbar.
        titleBar = QtGui.QWidget(self)
        barLayout = QtGui.QHBoxLayout(titleBar)
        barLayout.setSpacing(0)
        barLayout.setContentsMargins(0, 0, 0, 0)

        self.__toolbar = toolbar = QtGui.QToolBar(self)
        toolbar.setIconSize(QtCore.QSize(18, 18))
        toolbar.addAction(QtGui.QIcon(":/images/close.png"), "Close",
                          self.__close)

        float_action = QtGui.QAction(QtGui.QIcon(":/images/float.png"),
                                     "Float", self)
        float_action.toggled.connect(self.__floatingChanged)
        float_action.setCheckable(True)
        toolbar.addAction(float_action)

        config_action = QtGui.QAction(QtGui.QIcon(":/images/config.png"),
                                      "Configure Panel", self)
        config_action.triggered.connect(self._openPanelSettingsDialog)
        toolbar.addAction(config_action)

        toolbar.addSeparator()

        barLayout.addWidget(toolbar)
        barLayout.addStretch()
        barLayout.addWidget(self.__label)
        barLayout.addSpacing(4)

        self.setTitleBarWidget(titleBar)
        self.init()
Esempio n. 24
0
    def refresh(self):
        if not self.__items:
            self.reload()
            return

        rows = self.__index
        colCount = self.columnCount()
        parent = QtCore.QModelIndex()

        nodes = plow.client.get_nodes()
        nodes_ids = set()
        to_add = set()

        # Update
        for node in nodes:
            nodes_ids.add(node.id)
            if node.id in self.__index:
                row = rows[node.id]
                self.__items[row] = node
                start = self.index(row, 0)
                end = self.index(row, colCount - 1)
                self.dataChanged.emit(start, end)
                LOGGER.debug("updating %s %s", node.id, node.name)
            else:
                to_add.add(node)

        # Add new
        if to_add:
            size = len(to_add)
            start = len(self.__items)
            end = start + size - 1
            self.beginInsertRows(parent, start, end)
            self.__items.extend(to_add)
            self.endInsertRows()
            LOGGER.debug("adding %d new nodes", size)

        # Remove
        to_remove = set(self.__index.iterkeys()).difference(nodes_ids)
        for row, old_id in sorted(
            ((rows[old_id], old_id) for old_id in to_remove), reverse=True):
            self.beginRemoveRows(parent, row, row)
            node = self.__items.pop(row)
            self.endRemoveRows()
            LOGGER.debug("removing %s %s", old_id, node.name)

        self.__index = dict((n.id, row) for row, n in enumerate(self.__items))
Esempio n. 25
0
    def __initDefaultWorkspaces(self):
        for space in self.DEFAULTS:

            name = self.__getWorkspaceConfName(space)
            settings = util.getSettings(name)

            if not settings.contains("main::windowState"):

                src_name = os.path.join(DEFAULTS_DIR, '%s.ini' % name)
                if not os.path.exists(src_name):
                    LOGGER.warn("Could not locate default layout file: %r",
                                src_name)
                    continue

                src = QtCore.QSettings(src_name, QtCore.QSettings.IniFormat)

                for key in self._DEFAULT_KEYS:
                    settings.setValue(key, src.value(key))

                settings.sync()
                settings.deleteLater()
Esempio n. 26
0
    def __init__(self, *args, **kwargs):
        super(DragDropItem, self).__init__(*args, **kwargs)

        self.__dragStartPos = QtCore.QPoint(0,0)

        self.setMinimumHeight(24)
        self.setMaximumHeight(50)

        self.setCheckable(True)
        self.setFocusPolicy(QtCore.Qt.NoFocus)
        self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred)

        wrapperLayout = QtGui.QHBoxLayout(self)
        wrapperLayout.setSpacing(0)
        wrapperLayout.setContentsMargins(0,0,0,0)

        self._widgetLayout = layout = QtGui.QHBoxLayout()
        layout.setSpacing(self.ITEM_SPACING)
        layout.setContentsMargins(8,1,8,1)

        wrapperLayout.addLayout(layout)
        wrapperLayout.addStretch()
Esempio n. 27
0
    def __init__(self, attrs, parent=None):
        super(JobWranglerWidget, self).__init__(parent)
        self.__attrs = attrs

        layout = QtGui.QVBoxLayout(self)
        layout.setContentsMargins(4,0,4,4)

        # DEBUG
        if not "projects" in attrs:
            attrs['projects'] = plow.client.get_projects()

        self.__model = model = JobModel(attrs, self)
        self.__proxy = proxy = models.AlnumSortProxyModel(self)
        proxy.setSourceModel(model)

        self.__view = view = TreeWidget(self)
        view.setModel(proxy)
        view.sortByColumn(4, QtCore.Qt.DescendingOrder)

        for i, width in enumerate(JobNode.HEADER_WIDTHS):
            view.setColumnWidth(i, width)

        view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        view.customContextMenuRequested.connect(self.__showContextMenu)

        layout.addWidget(view)

        #
        # Connections
        #
        view.doubleClicked.connect(self.__itemDoubleClicked)
        view.activated.connect(self.__itemClicked)

        model.modelReset.connect(view.expandAll)

        self.__refreshTimer = timer = QtCore.QTimer(self)
        timer.setSingleShot(True)
        timer.setInterval(1500)
        timer.timeout.connect(self.refresh)
Esempio n. 28
0
class AlnumSortProxyModel(QtGui.QSortFilterProxyModel):

    RX_ALNUMS = QtCore.QRegExp('(\d+|\D+)')

    def __init__(self, *args, **kwargs):
        super(AlnumSortProxyModel, self).__init__(*args, **kwargs)

        self.setSortRole(DATA_ROLE)
        self.__validAlnum = (str, unicode)

    def lessThan(self, left, right):
        sortRole = self.sortRole()
        leftData = left.data(sortRole)

        if isinstance(leftData, self.__validAlnum):

            rightData = right.data(sortRole)

            if leftData == rightData:
                return False

            return alphaNumericKey(leftData) < alphaNumericKey(rightData)

        return super(AlnumSortProxyModel, self).lessThan(left, right)
Esempio n. 29
0
    def paintEvent(self, event):

        total_width = self.width()
        total_height = self.height()

        painter = QtGui.QPainter()
        painter.begin(self)
        painter.setRenderHints(painter.HighQualityAntialiasing
                               | painter.SmoothPixmapTransform
                               | painter.Antialiasing)

        if self.__hasErrors:
            painter.setBrush(constants.RED)
        else:
            painter.setBrush(constants.COLOR_JOB_STATE[self.__state])

        painter.setPen(painter.brush().color().darker())

        rect = QtCore.QRect(0, 0, total_width, total_height)
        painter.drawRoundedRect(rect, 5, 5)
        painter.setPen(QtCore.Qt.black)
        painter.drawText(rect, QtCore.Qt.AlignCenter,
                         constants.JOB_STATES[self.__state])
        painter.end()
Esempio n. 30
0
    def refresh(self):
        projects = self.__attrs.get('projects', [])

        if not projects:
            self.reset()
            return 

        if not self.__folders:
            self.reload()
            return

        rows = self.__folder_index
        colCount = self.columnCount()
        parent = QtCore.QModelIndex()

        folder_ids = set()
        to_add = set()
        folderNodes = dict((f.ref.id, f) for f in self.subnodes)

        # pull the job board
        folders = chain.from_iterable(imap(plow.client.get_job_board, projects))

        # Update
        for folder in folders:

            folder_ids.add(folder.id)
            row = rows.get(folder.id)
            
            if row is None:
                to_add.add(folder)

            else:
                oldFolder = self.__folders[row]
                folderNode = folderNodes[folder.id]

                self.__updateJobs(folderNode, folder)
                folderNode.ref = folder

                self.__folders[row] = folder
                start = self.index(row, 0)
                end = self.index(row, colCount-1)
                self.dataChanged.emit(start, end)
                LOGGER.debug("updating %s %s", folder.id, folder.name)

        # Add new
        if to_add:
            size = len(to_add)
            start = len(self.__folders)
            end = start + size - 1
            self.beginInsertRows(parent, start, end)
            self.__folders.extend(to_add)
            self.invalidate()
            self.endInsertRows()
            LOGGER.debug("adding %d new folders", size)

        # Remove
        to_remove = ((rows[f_id], f_id) for f_id in set(rows).difference(folder_ids))
        for row, f_id in sorted(to_remove, reverse=True):
            self.beginRemoveRows(parent, row, row)
            folder = self.__folders.pop(row)
            self.subnodes.remove(folderNodes[f_id])
            self.endRemoveRows()
            LOGGER.debug("removing %s %s", f_id, folder.name)

        # re-index the rows
        self.__folder_index = dict((f.id, row) for row, f in enumerate(self.__folders))