<commit_before>#!/usr/bin/env python
# -*- coding: utf-8 -*-

# libavg - Media Playback Engine.
# Copyright (C) 2003-2013 <NAME>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Current versions can be found at www.libavg.de
#
# Original authors of this file are
#   <NAME> <x at brainrapers dot org>
#   <NAME> <richy at coding-reality.de>
# Sponsored by Archimedes Exhibitions GmbH ( http://www.archimedes-exhibitions.de )

from collections import defaultdict
from collections import deque
from math import cos

import libavg
from libavg import avg
from libavg import graph
from libavg.widget.scrollarea import ScrollArea
from libavg.widget.button import TextButton
from libavg.apphelpers import DebugTouchVisualization
from libavg.apphelpers import TouchVisualizationOverlay as TouchVisOverlay


import keyboardmanager as kbmgr

g_fontsize = 10

PANGO_ENTITIES_MAP = {
    "&": "&amp;",
    '"': "&quot;",
    "'": "&apos;",
    ">": "&gt;",
    "<": "&lt;",
}

PADDING_LEFT = 10
PADDING_RIGHT = 5
PADDING_TOP = 2
PADDING_BOTTOM = 2

def subscribe(publisher, msgID, callable_):
    publisher.subscribe(msgID, callable_)
    return lambda: publisher.unsubscribe(msgID, callable_)


class DebugWidgetFrame(avg.DivNode):

    BORDER = 7
    REMOVE_WIDGET_FRAME = avg.Publisher.genMessageID()
    FRAME_HEIGHT_CHANGED = avg.Publisher.genMessageID()

    def __init__(self, size, widgetCls, *args, **kwargs):
        super(DebugWidgetFrame, self).__init__(size=size, *args, **kwargs)
        self.registerInstance(self, None)
        self.setup(widgetCls)
        self.subscribe(self.SIZE_CHANGED, self._onSizeChanged)
        self.size = size
        self._onSizeChanged(size)

    def setup(self, widgetCls):
        self.__background = avg.RectNode(parent=self, opacity=0.8,
                                         fillcolor='000000', fillopacity=0.8)
        self.__widget = widgetCls(parent=self,
                size=(max(0, self.width - self.BORDER * 2), 0),
                pos=(self.BORDER, self.BORDER))
        self.__selectHighlight = avg.RectNode(parent=self, color="35C0CD",
                strokewidth=self.BORDER, opacity=0.8,
                pos=(self.BORDER / 2, self.BORDER / 2), active=False, sensitive=False)
        self.__boundary = avg.RectNode(parent=self, sensitive=False)
        removeBtn = TextButton(parent=self, size=(20, 20),
                pos=(self.width - 40, 10), text="X")

        self.publish(self.REMOVE_WIDGET_FRAME)
        self.publish(DebugWidgetFrame.FRAME_HEIGHT_CHANGED)

        #self._subscriptions.append(subscribe(self, self.CURSOR_DOWN, self.toggleSelect))
        removeBtn.subscribe(removeBtn.CLICKED, self.remove)
        self.__widget.subscribe(self.__widget.WIDGET_HEIGHT_CHANGED,
                self.adjustWidgetHeight)
        self.__widget.update()

    def _onSizeChanged(self, size):
        self.__boundary.size = size
        self.__background.size = size
        childSize = (max(0, size[0] - self.BORDER * 2), max(0, size[1] - self.BORDER * 2))
        self.__selectHighlight.size = (max(0, size[0] - self.BORDER),
                max(0, size[1] - self.BORDER))
        self.__widget.size = childSize
        self.__widget.syncSize(childSize)

    def adjustWidgetHeight(self, height):
        self.size = (max(0, self.width), height + 2 * self.BORDER)
        self.notifySubscribers(DebugWidgetFrame.FRAME_HEIGHT_CHANGED, [])

    def remove(self):
        self.notifySubscribers(self.REMOVE_WIDGET_FRAME, [self])

    def toggleSelect(self, event=None):
        self.__selectHighlight.active = not(self.__selectHighlight.active)

    def isSelected(self):
        return self.__selectHighlight.active

    def select(self):
        self.__selectHighlight.active = True

    def unselect(self):
        self.__selectHighlight.active = False

    def show(self):
        self.active = True
        self.__widget.onShow()
        self.__widget.update()

    def hide(self):
        self.active = False
        self.__widget.onHide()

    @property
    def widget(self):
        return self.__widget


class DebugWidget(avg.DivNode):
    SLOT_HEIGHT = 200

    WIDGET_HEIGHT_CHANGED = avg.Publisher.genMessageID()

    def __init__(self, parent=None, **kwargs):
        super(DebugWidget, self).__init__(**kwargs)
        self.registerInstance(self, parent)
        self.publish(DebugWidget.WIDGET_HEIGHT_CHANGED)

    def syncSize(self, size):
        pass

    def update(self):
        pass

    def onShow(self):
        pass

    def onHide(self):
        pass

    def kill(self):
        pass


class ScrollableTextWidget(DebugWidget):

    def __init__(self, parent=None, **kwargs):
        super(ScrollableTextWidget, self).__init__(**kwargs)
        self.registerInstance(self, parent)
        self.autoscroll = True
        self.__maxLines = 100
        self.__lines = avg.DivNode()
        self.__scrollableArea = ScrollArea(self.__lines, (self.width, self.SLOT_HEIGHT),
                                           parent=self)
        self.__scrollableArea.subscribe(self.__scrollableArea.PRESSED,
                                        self.disableAutoscroll)
        self.__scrollableArea.subscribe(self.__scrollableArea.RELEASED,
                                        self.enableAutoscroll)

    def syncSize(self, size):
        paddingSize = avg.Point2D(PADDING_LEFT + PADDING_RIGHT,
                                  PADDING_BOTTOM + PADDING_TOP)

        self.__scrollableArea.size = avg.Point2D(size) - paddingSize
        self.__scrollableArea.pos = (PADDING_LEFT, PADDING_TOP)
        self.__lines.width = size[0] - PADDING_LEFT - PADDING_RIGHT

    def disableAutoscroll(self):
        self.autoscroll = False

    def enableAutoscroll(self):
        self.autoscroll = True
        self.scrollToEnd()

    def scrollToEnd(self):
        # Early escape, there are less lines then screen estate
        if self.__scrollableArea.contentsize.y <= self.__scrollableArea.size.y:
            return
        childrenRemoved = 0
        removedHeight = 0
        while self.__lines.getNumChildren() > self.__maxLines:
            removedHeight += self.__lines.getChild(0).height
            self.__lines.removeChild(0)
            childrenRemoved += 1
            # Remove only some at a time to reduce framerate spikes when
            # releasing the mouse after reading the logs
            if childrenRemoved > 150:
                break
        if childrenRemoved > 0:
            for childID in xrange(0, self.__lines.getNumChildren()):
                child = self.__lines.getChild(childID)
                child.pos = child.pos - (0, removedHeight)
            self.__lines.height -= removedHeight

        distance = (self.__scrollableArea.contentpos.y /
                   (self.__scrollableArea.contentsize.y - self.__scrollableArea.size.y))
        slowdown = 500
        duration = (cos(distance) - cos(1.0)) * slowdown
        avg.LinearAnim(self.__scrollableArea,
                       'contentpos',
                       int(duration),
                       self.__scrollableArea.contentpos,
                       (0, self.__scrollableArea.contentsize.y -
                           self.__scrollableArea.size.y)).start()

    def appendLine(self, line, escape=True):
        if escape:
            line = ''.join(PANGO_ENTITIES_MAP.get(char, char) for char in line)

        node = avg.WordsNode(fontsize=g_fontsize, text=line, font='monospace',
                             pos=(0, self.__lines.height), rawtextmode=True)
        self.__lines.height += node.height
        if node.width > self.__lines.width:
            self.__lines.width = node.width
        self.__lines.appendChild(node)

        if self.autoscroll:
            self.scrollToEnd()

NUM_COLS = 10
COL_WIDTH = 60
ROW_HEIGHT = g_fontsize + 2


class TableRow(avg.DivNode):
    COL_POS_X = 0
    ROW_ID = 0

    def __init__(self, parent=None, **kwargs):
        super(TableRow, self).__init__(**kwargs)
        self.registerInstance(self, parent)
        global NUM_COLS
        NUM_COLS = int((self.parent.width - COL_WIDTH * 4) / COL_WIDTH)
        self._initRow()
        TableRow.ROW_ID += 1

    def _initRow(self):
        self.columnBackground = avg.RectNode(parent=self, fillcolor="222222",
                                             fillopacity=0.6, opacity=0)
        self.columnContainer = avg.DivNode(parent=self)
        if TableRow.ROW_ID % 2 != 0:
            self.columnBackground.fillopacity = 0
        self.cols = [0] * NUM_COLS
        for i in xrange(0, NUM_COLS):
            self.cols[i] = (avg.WordsNode(parent=self.columnContainer,
                                          fontsize=g_fontsize,
                                          text="0", size=(COL_WIDTH / 2.0, ROW_HEIGHT),
                                          pos=(i * COL_WIDTH, 0)),
                            avg.WordsNode(parent=self.columnContainer,
                                          fontsize=g_fontsize,
                                          text="(0)", size=(COL_WIDTH / 2.0, ROW_HEIGHT),
                                          pos=(i * COL_WIDTH + COL_WIDTH / 2, 0),
                                          color="000000"))

        self.liveColumn = avg.WordsNode(parent=self.columnContainer, fontsize=g_fontsize,
                                        text="N/A - SPECIAL",
                                        size=(COL_WIDTH, ROW_HEIGHT), variant="bold",
                                        pos=(NUM_COLS * COL_WIDTH, 0))
        self.rowData = deque([0] * (NUM_COLS + 1), maxlen=NUM_COLS + 1)
        self.label = avg.WordsNode(parent=self, fontsize=g_fontsize, variant="bold")
        self.setLabel("NONE")

    @property
    def height(self):
        return self.label.height

    def setLabel(self, label):
        if self.label.text == label + ":":
            return
        self.label.text = label + ":"
        TableRow.COL_POS_X = max(TableRow.COL_POS_X, self.label.width)
        if self.label.width < TableRow.COL_POS_X:
            self.parent.labelColumnSizeChanged()

    def resizeLabelColumn(self):
        self.columnContainer.pos = (TableRow.COL_POS_X + 10, 0)
        self.columnBackground.size = (self.columnContainer.x + self.liveColumn.x +
                                      self.liveColumn.width, g_fontsize)

    def insertValue(self, data):
        self.rowData.append(data)
        prevValue = 0
        for i in xrange(0, len(self.rowData)):
            if i >= 0:
                data = self.rowData[i]
                column = self.cols[i - 1]
                column[0].text = str(data)
                diff = data - prevValue
                column[1].text = "(%s)" % diff
                column[1].pos = (column[0].x + column[0].getLineExtents(0)[0] + 2,
                                 column[0].y)
                if diff == 0:
                    column[1].color = "000000"
                elif diff < 0:
                    column[1].color = "00FF00"
                else:
                    column[1].color = "FF0000"
            prevValue = self.rowData[i]

    def updateLiveColumn(self, value):
        self.liveColumn.text = str(value)


class Table(avg.DivNode):
    def __init__(self, parent=None, **kwargs):
        super(Table, self).__init__(**kwargs)
        self.registerInstance(self, parent)

    def labelColumnSizeChanged(self):
        for childID in xrange(0, self.getNumChildren()):
            child = self.getChild(childID)
            child.resizeLabelColumn()


class ObjectDumpWidget(DebugWidget):
    def __init__(self, parent=None, **kwargs):
        super(ObjectDumpWidget, self).__init__(**kwargs)
        self.registerInstance(self, parent)
        self.tableContainer = Table(parent=self, size=(self.width, self.SLOT_HEIGHT))
        #self.tableDivs = defaultdict(lambda: TableRow(parent=self.tableContainer))

    def update(self):
        objDump = libavg.player.getTestHelper().getObjectCount()
        """
        pos = (0, 0)
        for key in sorted(objDump.iterkeys()):
            val = objDump[key]
            self.tableDivs[key].updateLiveColumn(val)
            self.tableDivs[key].setLabel(key)
            self.tableDivs[key].pos = pos
            pos = (0, pos[1] + self.tableDivs[key].height)
        height = len(objDump) * self.tableDivs[key].height
        if self.height != height:
            self.notifySubscribers(DebugWidget.WIDGET_HEIGHT_CHANGED, [height])
        """

    def persistColumn(self):
        pass
        """
        objDump = libavg.player.getTestHelper().getObjectCount()
        for key, val in objDump.iteritems():
            self.tableDivs[key].insertValue(val)
        """

    def syncSize(self, size):
        #self.tableContainer.size = (size[0], size[1] - (g_fontsize + 2))
        pass

    def onShow(self):
        self.intervalID = libavg.player.setInterval(1000, self.update)
        kbmgr.bindKeyDown(keystring='i',
                                    handler=self.persistColumn,
                                    help="Persist live column to object dump table")

    def onHide(self):
        self.kill()

    def kill(self):
        if self.intervalID:
            libavg.player.clearInterval(self.intervalID)
            self.intervalID = None
        kbmgr.unbindKeyDown(keystring='i')



class GraphWidget(DebugWidget):<commit_msg>libavg_app: Fixed leak in oject dump widget<commit_after>#!/usr/bin/env python
# -*- coding: utf-8 -*-

# libavg - Media Playback Engine.
# Copyright (C) 2003-2013 <NAME>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Current versions can be found at www.libavg.de
#
# Original authors of this file are
#   <NAME> <x at brainrapers dot org>
#   <NAME> <richy at coding-reality.de>
# Sponsored by Archimedes Exhibitions GmbH ( http://www.archimedes-exhibitions.de )

from collections import defaultdict
from collections import deque
from math import cos

import libavg
from libavg import avg
from libavg import graph
from libavg.widget.scrollarea import ScrollArea
from libavg.widget.button import TextButton
from libavg.apphelpers import DebugTouchVisualization
from libavg.apphelpers import TouchVisualizationOverlay as TouchVisOverlay


import keyboardmanager as kbmgr

g_fontsize = 10

PANGO_ENTITIES_MAP = {
    "&": "&amp;",
    '"': "&quot;",
    "'": "&apos;",
    ">": "&gt;",
    "<": "&lt;",
}

PADDING_LEFT = 10
PADDING_RIGHT = 5
PADDING_TOP = 2
PADDING_BOTTOM = 2

def subscribe(publisher, msgID, callable_):
    publisher.subscribe(msgID, callable_)
    return lambda: publisher.unsubscribe(msgID, callable_)


class DebugWidgetFrame(avg.DivNode):

    BORDER = 7
    REMOVE_WIDGET_FRAME = avg.Publisher.genMessageID()
    FRAME_HEIGHT_CHANGED = avg.Publisher.genMessageID()

    def __init__(self, size, widgetCls, *args, **kwargs):
        super(DebugWidgetFrame, self).__init__(size=size, *args, **kwargs)
        self.registerInstance(self, None)
        self.setup(widgetCls)
        self.subscribe(self.SIZE_CHANGED, self._onSizeChanged)
        self.size = size
        self._onSizeChanged(size)

    def setup(self, widgetCls):
        self.__background = avg.RectNode(parent=self, opacity=0.8,
                                         fillcolor='000000', fillopacity=0.8)
        self.__widget = widgetCls(parent=self,
                size=(max(0, self.width - self.BORDER * 2), 0),
                pos=(self.BORDER, self.BORDER))
        self.__selectHighlight = avg.RectNode(parent=self, color="35C0CD",
                strokewidth=self.BORDER, opacity=0.8,
                pos=(self.BORDER / 2, self.BORDER / 2), active=False, sensitive=False)
        self.__boundary = avg.RectNode(parent=self, sensitive=False)
        removeBtn = TextButton(parent=self, size=(20, 20),
                pos=(self.width - 40, 10), text="X")

        self.publish(self.REMOVE_WIDGET_FRAME)
        self.publish(DebugWidgetFrame.FRAME_HEIGHT_CHANGED)

        #self._subscriptions.append(subscribe(self, self.CURSOR_DOWN, self.toggleSelect))
        removeBtn.subscribe(removeBtn.CLICKED, self.remove)
        self.__widget.subscribe(self.__widget.WIDGET_HEIGHT_CHANGED,
                self.adjustWidgetHeight)
        self.__widget.update()

    def _onSizeChanged(self, size):
        self.__boundary.size = size
        self.__background.size = size
        childSize = (max(0, size[0] - self.BORDER * 2), max(0, size[1] - self.BORDER * 2))
        self.__selectHighlight.size = (max(0, size[0] - self.BORDER),
                max(0, size[1] - self.BORDER))
        self.__widget.size = childSize
        self.__widget.syncSize(childSize)

    def adjustWidgetHeight(self, height):
        self.size = (max(0, self.width), height + 2 * self.BORDER)
        self.notifySubscribers(DebugWidgetFrame.FRAME_HEIGHT_CHANGED, [])

    def remove(self):
        self.notifySubscribers(self.REMOVE_WIDGET_FRAME, [self])

    def toggleSelect(self, event=None):
        self.__selectHighlight.active = not(self.__selectHighlight.active)

    def isSelected(self):
        return self.__selectHighlight.active

    def select(self):
        self.__selectHighlight.active = True

    def unselect(self):
        self.__selectHighlight.active = False

    def show(self):
        self.active = True
        self.__widget.onShow()
        self.__widget.update()

    def hide(self):
        self.active = False
        self.__widget.onHide()

    @property
    def widget(self):
        return self.__widget


class DebugWidget(avg.DivNode):
    SLOT_HEIGHT = 200

    WIDGET_HEIGHT_CHANGED = avg.Publisher.genMessageID()

    def __init__(self, parent=None, **kwargs):
        super(DebugWidget, self).__init__(**kwargs)
        self.registerInstance(self, parent)
        self.publish(DebugWidget.WIDGET_HEIGHT_CHANGED)

    def syncSize(self, size):
        pass

    def update(self):
        pass

    def onShow(self):
        pass

    def onHide(self):
        pass

    def kill(self):
        pass


class ScrollableTextWidget(DebugWidget):

    def __init__(self, parent=None, **kwargs):
        super(ScrollableTextWidget, self).__init__(**kwargs)
        self.registerInstance(self, parent)
        self.autoscroll = True
        self.__maxLines = 100
        self.__lines = avg.DivNode()
        self.__scrollableArea = ScrollArea(self.__lines, (self.width, self.SLOT_HEIGHT),
                                           parent=self)
        self.__scrollableArea.subscribe(self.__scrollableArea.PRESSED,
                                        self.disableAutoscroll)
        self.__scrollableArea.subscribe(self.__scrollableArea.RELEASED,
                                        self.enableAutoscroll)

    def syncSize(self, size):
        paddingSize = avg.Point2D(PADDING_LEFT + PADDING_RIGHT,
                                  PADDING_BOTTOM + PADDING_TOP)

        self.__scrollableArea.size = avg.Point2D(size) - paddingSize
        self.__scrollableArea.pos = (PADDING_LEFT, PADDING_TOP)
        self.__lines.width = size[0] - PADDING_LEFT - PADDING_RIGHT

    def disableAutoscroll(self):
        self.autoscroll = False

    def enableAutoscroll(self):
        self.autoscroll = True
        self.scrollToEnd()

    def scrollToEnd(self):
        # Early escape, there are less lines then screen estate
        if self.__scrollableArea.contentsize.y <= self.__scrollableArea.size.y:
            return
        childrenRemoved = 0
        removedHeight = 0
        while self.__lines.getNumChildren() > self.__maxLines:
            removedHeight += self.__lines.getChild(0).height
            self.__lines.removeChild(0)
            childrenRemoved += 1
            # Remove only some at a time to reduce framerate spikes when
            # releasing the mouse after reading the logs
            if childrenRemoved > 150:
                break
        if childrenRemoved > 0:
            for childID in xrange(0, self.__lines.getNumChildren()):
                child = self.__lines.getChild(childID)
                child.pos = child.pos - (0, removedHeight)
            self.__lines.height -= removedHeight

        distance = (self.__scrollableArea.contentpos.y /
                   (self.__scrollableArea.contentsize.y - self.__scrollableArea.size.y))
        slowdown = 500
        duration = (cos(distance) - cos(1.0)) * slowdown
        avg.LinearAnim(self.__scrollableArea,
                       'contentpos',
                       int(duration),
                       self.__scrollableArea.contentpos,
                       (0, self.__scrollableArea.contentsize.y -
                           self.__scrollableArea.size.y)).start()

    def appendLine(self, line, escape=True):
        if escape:
            line = ''.join(PANGO_ENTITIES_MAP.get(char, char) for char in line)

        node = avg.WordsNode(fontsize=g_fontsize, text=line, font='monospace',
                             pos=(0, self.__lines.height), rawtextmode=True)
        self.__lines.height += node.height
        if node.width > self.__lines.width:
            self.__lines.width = node.width
        self.__lines.appendChild(node)

        if self.autoscroll:
            self.scrollToEnd()

NUM_COLS = 10
COL_WIDTH = 60
ROW_HEIGHT = g_fontsize + 2


class TableRow(avg.DivNode):
    COL_POS_X = 0
    ROW_ID = 0

    def __init__(self, parent=None, **kwargs):
        super(TableRow, self).__init__(**kwargs)
        self.registerInstance(self, parent)
        global NUM_COLS
        NUM_COLS = int((self.parent.width - COL_WIDTH * 4) / COL_WIDTH)
        self._initRow()
        TableRow.ROW_ID += 1

    def _initRow(self):
        self.columnBackground = avg.RectNode(parent=self, fillcolor="222222",
                                             fillopacity=0.6, opacity=0)
        self.columnContainer = avg.DivNode(parent=self)
        if TableRow.ROW_ID % 2 != 0:
            self.columnBackground.fillopacity = 0
        self.cols = [0] * NUM_COLS
        for i in xrange(0, NUM_COLS):
            self.cols[i] = (avg.WordsNode(parent=self.columnContainer,
                                          fontsize=g_fontsize,
                                          text="0", size=(COL_WIDTH / 2.0, ROW_HEIGHT),
                                          pos=(i * COL_WIDTH, 0)),
                            avg.WordsNode(parent=self.columnContainer,
                                          fontsize=g_fontsize,
                                          text="(0)", size=(COL_WIDTH / 2.0, ROW_HEIGHT),
                                          pos=(i * COL_WIDTH + COL_WIDTH / 2, 0),
                                          color="000000"))

        self.liveColumn = avg.WordsNode(parent=self.columnContainer, fontsize=g_fontsize,
                                        text="N/A - SPECIAL",
                                        size=(COL_WIDTH, ROW_HEIGHT), variant="bold",
                                        pos=(NUM_COLS * COL_WIDTH, 0))
        self.rowData = deque([0] * (NUM_COLS + 1), maxlen=NUM_COLS + 1)
        self.label = avg.WordsNode(parent=self, fontsize=g_fontsize, variant="bold")
        self.setLabel("NONE")

    @property
    def height(self):
        return self.label.height

    def setLabel(self, label):
        if self.label.text == label + ":":
            return
        self.label.text = label + ":"
        TableRow.COL_POS_X = max(TableRow.COL_POS_X, self.label.width)
        if self.label.width < TableRow.COL_POS_X:
            self.parent.labelColumnSizeChanged()

    def resizeLabelColumn(self):
        self.columnContainer.pos = (TableRow.COL_POS_X + 10, 0)
        self.columnBackground.size = (self.columnContainer.x + self.liveColumn.x +
                                      self.liveColumn.width, g_fontsize)

    def insertValue(self, data):
        self.rowData.append(data)
        prevValue = 0
        for i in xrange(0, len(self.rowData)):
            if i >= 0:
                data = self.rowData[i]
                column = self.cols[i - 1]
                column[0].text = str(data)
                diff = data - prevValue
                column[1].text = "(%s)" % diff
                column[1].pos = (column[0].x + column[0].getLineExtents(0)[0] + 2,
                                 column[0].y)
                if diff == 0:
                    column[1].color = "000000"
                elif diff < 0:
                    column[1].color = "00FF00"
                else:
                    column[1].color = "FF0000"
            prevValue = self.rowData[i]

    def updateLiveColumn(self, value):
        self.liveColumn.text = str(value)


class Table(avg.DivNode):
    def __init__(self, parent=None, **kwargs):
        super(Table, self).__init__(**kwargs)
        self.registerInstance(self, parent)

    def labelColumnSizeChanged(self):
        for childID in xrange(0, self.getNumChildren()):
            child = self.getChild(childID)
            child.resizeLabelColumn()


class ObjectDumpWidget(DebugWidget):
    def __init__(self, parent=None, **kwargs):
        super(ObjectDumpWidget, self).__init__(**kwargs)
        self.registerInstance(self, parent)
        self.tableContainer = Table(parent=self, size=(self.width, self.SLOT_HEIGHT))
        self.tableDivs = defaultdict(lambda: TableRow(parent=self.tableContainer))

    def update(self):
        objDump = libavg.player.getTestHelper().getObjectCount()
        pos = (0, 0)
        for key in sorted(objDump.iterkeys()):
            val = objDump[key]
            self.tableDivs[key].updateLiveColumn(val)
            self.tableDivs[key].setLabel(key)
            self.tableDivs[key].pos = pos
            pos = (0, pos[1] + self.tableDivs[key].height)
        height = len(objDump) * self.tableDivs[key].height
        if self.height != height:
            self.notifySubscribers(DebugWidget.WIDGET_HEIGHT_CHANGED, [height])

    def persistColumn(self):
        objDump = libavg.player.getTestHelper().getObjectCount()
        for key, val in objDump.iteritems():
            self.tableDivs[key].insertValue(val)

    def syncSize(self, size):
        self.tableContainer.size = (size[0], size[1] - (g_fontsize + 2))

    def onShow(self):
        self.intervalID = libavg.player.setInterval(1000, self.update)
        kbmgr.bindKeyDown(keystring='i',
                                    handler=self.persistColumn,
                                    help="Persist live column to object dump table")

    def onHide(self):
        if self.intervalID:
            libavg.player.clearInterval(self.intervalID)
            self.intervalID = None
        kbmgr.unbindKeyDown(keystring='i')

    def kill(self):
        self.onHide()
        self.tableDivs = None


class GraphWidget(DebugWidget):
{'fraction_non_alphanumeric': 0.10489012358870249, 'fraction_numerical': 0.013391744132207856, 'mean_word_length': 2.151290684624018, 'pattern_counts': {'":': 14, '<': 21, '<?xml version=': 0, '>': 25, 'https://': 0, 'lorem ipsum': 0, 'www.': 4, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 60, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7561763', 'n_tokens_mistral': 8494, 'n_tokens_neox': 8029, 'n_words': 1737}