summaryrefslogtreecommitdiff
path: root/src/python/app/debugpanel.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/python/app/debugpanel.py')
-rw-r--r--src/python/app/debugpanel.py697
1 files changed, 697 insertions, 0 deletions
diff --git a/src/python/app/debugpanel.py b/src/python/app/debugpanel.py
new file mode 100644
index 0000000..c786785
--- /dev/null
+++ b/src/python/app/debugpanel.py
@@ -0,0 +1,697 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# libavg - Media Playback Engine.
+# Copyright (C) 2003-2014 Ulrich von Zadow
+#
+# 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
+# OXullo Interecans <x at brainrapers dot org>
+# Richard Klemm <richy at coding-reality.de>
+
+from collections import defaultdict
+from collections import deque
+import math
+
+import libavg
+from libavg import avg
+from touchvisualization import DebugTouchVisualization
+from touchvisualization import TouchVisualizationOverlay as TouchVisOverlay
+
+
+import keyboardmanager as kbmgr
+
+g_fontsize = 10
+
+PANGO_ENTITIES_MAP = {
+ "&": "&amp;",
+ '"': "&quot;",
+ "'": "&apos;",
+ ">": "&gt;",
+ "<": "&lt;",
+}
+
+def subscribe(publisher, msgID, callable_):
+ publisher.subscribe(msgID, callable_)
+ return lambda: publisher.unsubscribe(msgID, callable_)
+
+
+class DebugWidgetFrame(avg.DivNode):
+
+ BORDER = 7
+ 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)
+
+ self.publish(DebugWidgetFrame.FRAME_HEIGHT_CHANGED)
+
+ 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 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
+ CAPTION = ''
+
+ 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)
+ if self.CAPTION:
+ self._caption = avg.WordsNode(text=self.CAPTION, pivot=(0, 0),
+ opacity=0.5, fontsize=14, parent=self)
+ self._caption.angle = math.pi / 2
+ self._caption.pos = (self.width, 0)
+
+ def syncSize(self, size):
+ self._caption.width = size[1]
+
+ def update(self):
+ pass
+
+ def onShow(self):
+ pass
+
+ def onHide(self):
+ pass
+
+ def kill(self):
+ pass
+
+
+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
+ self.liveColumn = avg.WordsNode(parent=self.columnContainer, fontsize=g_fontsize,
+ text="N/A - SPECIAL", size=(COL_WIDTH, ROW_HEIGHT), variant="bold")
+ 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+1) * COL_WIDTH, 0)),
+ avg.WordsNode(parent=self.columnContainer,
+ fontsize=g_fontsize,
+ text="(0)", size=(COL_WIDTH / 2.0, ROW_HEIGHT),
+ pos=((i+1) * COL_WIDTH + COL_WIDTH / 2, 0),
+ color="000000"))
+
+ self.rowData = deque([(0, 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):
+ prevValue = self.rowData[0][0]
+ self.rowData.appendleft([data, data-prevValue])
+ for i in xrange(0, len(self.rowData)-1):
+ val, diff = self.rowData[i]
+ column = self.cols[i]
+ column[0].text = str(val)
+ column[1].text = "({diff})".format(diff=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"
+
+ 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):
+ CAPTION = 'Objects count'
+
+ 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="Object count snapshot",
+ modifiers=libavg.KEYMOD_CTRL)
+
+ def onHide(self):
+ if self.intervalID:
+ libavg.player.clearInterval(self.intervalID)
+ self.intervalID = None
+ kbmgr.unbindKeyDown(keystring='i', modifiers=libavg.KEYMOD_CTRL)
+
+ def kill(self):
+ self.onHide()
+ self.tableDivs = None
+
+
+class GraphWidget(DebugWidget):
+ def __init__(self, **kwargs):
+ super(GraphWidget, self).__init__(**kwargs)
+ self.registerInstance(self, None)
+ self.__graph = None
+
+ def onShow(self):
+ if self.__graph:
+ self.__graph.active = True
+ else:
+ self.__graph = self._createGraph()
+
+ def onHide(self):
+ if self.__graph:
+ self.__graph.active = False
+
+ def kill(self):
+ self.__graph.unlink(True)
+
+ def _createGraph(self):
+ pass
+
+
+class MemoryGraphWidget(GraphWidget):
+ CAPTION = 'Memory usage'
+
+ def _createGraph(self):
+ return libavg.graph.AveragingGraph(parent=self, size=self.size,
+ getValue=avg.getMemoryUsage)
+
+
+class FrametimeGraphWidget(GraphWidget):
+ CAPTION = 'Time per frame'
+
+ def _createGraph(self):
+ return libavg.graph.SlidingBinnedGraph(parent=self,
+ getValue=libavg.player.getFrameTime,
+ binsThresholds=[0.0, 20.0, 40.0, 80.0, 160.0],
+ size=self.size)
+
+
+class GPUMemoryGraphWidget(GraphWidget):
+ CAPTION = 'GPU Memory usage'
+
+ def _createGraph(self):
+ try:
+ libavg.player.getVideoMemUsed()
+ except RuntimeError:
+ return avg.WordsNode(parent=self,
+ text='GPU memory graph is not supported on this hardware',
+ color='ff5555')
+ else:
+ return libavg.graph.AveragingGraph(parent=self, size=self.size,
+ getValue=libavg.player.getVideoMemUsed)
+
+
+class KeyboardManagerBindingsShower(DebugWidget):
+ CAPTION = 'Keyboard bindings'
+
+ def __init__(self, *args, **kwargs):
+ super(KeyboardManagerBindingsShower, self).__init__(**kwargs)
+ self.registerInstance(self, None)
+ self.keybindingWordNodes = []
+ kbmgr.publisher.subscribe(kbmgr.publisher.BINDINGS_UPDATED, self.update)
+
+ def clear(self):
+ for node in self.keybindingWordNodes:
+ node.unlink(True)
+ self.keybindingWordNodes = []
+
+ def update(self):
+ self.clear()
+ for binding in kbmgr.getCurrentBindings():
+ keystring = binding.keystring.decode('utf8')
+ modifiersStr = self.__modifiersToString(binding.modifiers)
+
+ if modifiersStr is not None:
+ key = '%s-%s' % (modifiersStr, keystring)
+ else:
+ key = keystring
+
+ if binding.type == libavg.avg.KEYDOWN:
+ key = '%s %s' % (unichr(8595), key)
+ else:
+ key = '%s %s' % (unichr(8593), key)
+
+ node = avg.WordsNode(
+ text='<span size="large"><b>%s</b></span>: %s' %
+ (key, binding.help),
+ fontsize=g_fontsize, parent=self)
+ self.keybindingWordNodes.append(node)
+
+ self._placeNodes()
+
+ def _placeNodes(self):
+ if not self.keybindingWordNodes:
+ return
+
+ maxWidth = max([node.width for node in self.keybindingWordNodes])
+ columns = int(self.parent.width / maxWidth)
+ rows = len(self.keybindingWordNodes) / columns
+ remainder = len(self.keybindingWordNodes) % columns
+
+ if remainder != 0:
+ rows += 1
+
+ colSize = self.parent.width / columns
+
+ currentColumn = 0
+ currentRow = 0
+ heights = [0] * columns
+ for node in self.keybindingWordNodes:
+ if currentRow == rows and currentColumn < columns - 1:
+ currentRow = 0
+ currentColumn += 1
+
+ node.pos = (currentColumn * colSize, heights[currentColumn])
+ heights[currentColumn] += node.height
+ currentRow += 1
+
+ finalHeight = max(heights)
+ if self.height != finalHeight:
+ self.notifySubscribers(self.WIDGET_HEIGHT_CHANGED, [finalHeight])
+
+ def __modifiersToString(self, modifiers):
+ def isSingleBit(number):
+ bitsSet = 0
+ for i in xrange(8):
+ if (1 << i) & number:
+ bitsSet += 1
+
+ return bitsSet == 1
+
+ if modifiers in (0, kbmgr.KEYMOD_ANY):
+ return None
+
+ allModifiers = []
+ for mod in dir(avg):
+ if 'KEYMOD_' in mod:
+ maskVal = int(getattr(avg, mod))
+ if isSingleBit(maskVal):
+ allModifiers.append((maskVal, mod))
+
+ modifiersStringsList = []
+ for modval, modstr in allModifiers:
+ if modifiers & modval:
+ modifiersStringsList.append(modstr.replace('KEYMOD_', ''))
+
+ for doubleMod in ['CTRL', 'META', 'SHIFT']:
+ left = 'L' + doubleMod
+ right = 'R' + doubleMod
+ if left in modifiersStringsList and right in modifiersStringsList:
+ modifiersStringsList.remove(left)
+ modifiersStringsList.remove(right)
+ modifiersStringsList.append(doubleMod)
+
+ return '/'.join(modifiersStringsList).lower()
+
+
+class DebugPanel(avg.DivNode):
+ def __init__(self, parent=None, fontsize=10, **kwargs):
+ super(DebugPanel, self).__init__(**kwargs)
+ self.registerInstance(self, parent)
+
+ avg.RectNode(size=self.size, opacity=0, fillopacity=0.3, fillcolor='ff0000',
+ parent=self)
+ avg.WordsNode(text='Debug panel', fontsize=fontsize,
+ pos=(0, self.height - fontsize - fontsize / 3),
+ parent=self)
+
+ self.sensitive = False
+ self.active = False
+ self.__panel = None
+ self.__callables = []
+ self.__fontsize = fontsize
+ self.__touchVisOverlay = None
+
+ def setupKeys(self):
+ kbmgr.bindKeyDown(keystring='g',
+ handler=lambda: self.toggleWidget(GPUMemoryGraphWidget),
+ help="GPU memory graph",
+ modifiers=libavg.avg.KEYMOD_CTRL)
+
+ kbmgr.bindKeyDown(keystring='m',
+ handler=lambda: self.toggleWidget(MemoryGraphWidget),
+ help="Memory graph",
+ modifiers=libavg.avg.KEYMOD_CTRL)
+
+ kbmgr.bindKeyDown(keystring='f',
+ handler=lambda: self.toggleWidget(FrametimeGraphWidget),
+ help="Frametime graph",
+ modifiers=libavg.avg.KEYMOD_CTRL)
+
+ kbmgr.bindKeyDown(keystring='?',
+ handler=lambda: self.toggleWidget(KeyboardManagerBindingsShower),
+ help="Show keyboard bindings",
+ modifiers=kbmgr.KEYMOD_ANY)
+
+ kbmgr.bindKeyDown(keystring='o',
+ handler=lambda: self.toggleWidget(ObjectDumpWidget),
+ help="Object count table",
+ modifiers=libavg.avg.KEYMOD_CTRL)
+
+ kbmgr.bindKeyDown(keystring='v', handler=self.toggleTouchVisualization,
+ help="Cursor visualization",
+ modifiers=libavg.avg.KEYMOD_CTRL)
+
+ def addWidget(self, widgetCls, *args, **kwargs):
+ callable_ = lambda: self.__panel.addWidget(widgetCls, *args, **kwargs)
+ if self.__panel:
+ callable_()
+ else:
+ self.__callables.append(callable_)
+
+ def toggleWidget(self, *args, **kwargs):
+ if not self.active:
+ self.show()
+ self.__panel.ensureWidgetWisible(*args, **kwargs)
+ else:
+ self.__panel.toggleWidget(*args, **kwargs)
+
+ if not self.__panel.activeWidgetClasses:
+ self.hide()
+
+ def hide(self):
+ if self.__panel and self.active:
+ self.__panel.hide()
+ self.active = False
+
+ def show(self):
+ if self.__panel:
+ if not self.active:
+ self.__panel.show()
+ else:
+ self.forceLoadPanel()
+
+ self.active = True
+
+ def toggleVisibility(self):
+ if self.active:
+ self.hide()
+ else:
+ self.show()
+
+ def toggleTouchVisualization(self):
+ if self.__touchVisOverlay is None:
+ self.__touchVisOverlay = TouchVisOverlay(
+ isDebug=True,
+ visClass=DebugTouchVisualization,
+ size=self.parent.size,
+ parent=self.parent)
+ else:
+ self.__touchVisOverlay.unlink(True)
+ self.__touchVisOverlay = None
+
+ def forceLoadPanel(self):
+ if self.__panel is None:
+ self.__panel = _DebugPanel(parent=self, size=self.size,
+ fontsize=self.__fontsize)
+ for callable_ in self.__callables:
+ callable_()
+
+
+class _DebugPanel(avg.DivNode):
+
+ def __init__(self, parent=None, fontsize=10, **kwargs):
+ super(_DebugPanel, self).__init__(**kwargs)
+ self.registerInstance(self, parent)
+
+ self.__slots = []
+
+ self.maxSize = self.size
+ self.size = (self.size[0], 0)
+ self.activeWidgetClasses = []
+ self.__selectedWidget = None
+
+ global g_fontsize
+ g_fontsize = fontsize
+
+ self.show()
+
+ def show(self):
+ for widgetFrame in self.__slots:
+ if widgetFrame:
+ widgetFrame.show()
+ self.updateWidgets()
+
+ def hide(self):
+ for widget in self.__slots:
+ if widget:
+ widget.hide()
+
+ def ensureWidgetWisible(self, widgetClass, *args, **kwargs):
+ if not widgetClass in self.activeWidgetClasses:
+ self.toggleWidget(widgetClass, *args, **kwargs)
+
+ def toggleWidget(self, widgetClass, *args, **kwargs):
+ if widgetClass in self.activeWidgetClasses:
+ self._removeWidgetByClass(widgetClass)
+ else:
+ self.addWidget(widgetClass, *args, **kwargs)
+
+ def addWidget(self, widgetClass, *args, **kwargs):
+ if widgetClass in self.activeWidgetClasses:
+ libavg.logger.warning("You can't add the same widget twice")
+ return
+
+ widgetFrame = DebugWidgetFrame((max(0, self.width), DebugWidget.SLOT_HEIGHT),
+ widgetClass)
+ height = 0
+ for frame in self.__slots:
+ if frame:
+ height += frame.height
+ height += widgetFrame.height
+
+ if height > self.maxSize[1]:
+ libavg.logger.warning("No vertical space left. "
+ "Delete a widget and try again")
+ return False
+
+ self.appendChild(widgetFrame)
+
+ widgetPlaced = False
+ for idx, slot in enumerate(self.__slots):
+ if slot is None:
+ self.__slots[idx] = widgetFrame
+ widgetPlaced = True
+ break
+ if not widgetPlaced:
+ self.__slots.append(widgetFrame)
+ widgetFrame.subscribe(widgetFrame.FRAME_HEIGHT_CHANGED, self._heightChanged)
+
+ self.reorderWidgets()
+ widgetFrame.show()
+ self.updateWidgets()
+ self.activeWidgetClasses.append(widgetClass)
+
+ def _removeWidgetByClass(self, widgetClass):
+ for frame in self.__slots:
+ if frame and frame.widget.__class__ == widgetClass:
+ self.removeWidgetFrame(frame)
+ return
+
+ def _heightChanged(self):
+ height = 0
+ for childID in xrange(0, self.getNumChildren()):
+ child = self.getChild(childID)
+ height += child.height
+ self.height = height
+ self.reorderWidgets()
+
+ def updateWidgets(self):
+ for childID in xrange(0, self.getNumChildren()):
+ self.getChild(childID).widget.update()
+
+ def selectWidget(self, id):
+ id = id % self.getNumChildren()
+ for childID in xrange(0, self.getNumChildren()):
+ self.getChild(childID).unselect()
+ self.getChild(id).select()
+ self.__selectedWidget = id
+
+ def selectPreviousWidget(self):
+ if self.__selectedWidget is None:
+ self.selectWidget(-1)
+ else:
+ self.selectWidget(self.__selectedWidget - 1)
+
+ def selectNextWidget(self):
+ if self.__selectedWidget is None:
+ self.selectWidget(0)
+ else:
+ self.selectWidget(self.__selectedWidget + 1)
+
+ def removeWidgetFrame(self, widgetFrame):
+ self.activeWidgetClasses.remove(widgetFrame.widget.__class__)
+ for idx, slot in enumerate(self.__slots):
+ if slot == widgetFrame:
+ self.__slots[idx] = None
+ break
+ widgetFrame.widget.kill()
+ widgetFrame.unlink(True)
+ self.reorderWidgets()
+ self.updateWidgets()
+
+ def removeSelectedWidgetFrames(self):
+ candidates = []
+ for childID in xrange(0, self.getNumChildren()):
+ child = self.getChild(childID)
+ if child.isSelected():
+ candidates.append(child)
+ for widgetFrame in candidates:
+ self.removeWidgetFrame(widgetFrame)
+ self.__selectedWidget = None
+
+ def reorderWidgets(self):
+ #TODO: This is no layout management, yet
+ count = 0
+ height = 0
+ for idx, widgetFrame in enumerate(self.__slots):
+ if widgetFrame:
+ widgetFrame.pos = (0, height)
+ count += 1
+ height += widgetFrame.height
+ self.size = (self.maxSize[0], height)