Files
sloth/sloth/gui/propertyeditor.py
T

342 lines
12 KiB
Python

from sloth.core.exceptions import ImproperlyConfigured
from sloth.annotations.model import AnnotationModelItem
from sloth.gui.floatinglayout import FloatingLayout
from sloth.utils.bind import bind
import sys
from PyQt4.QtCore import pyqtSignal, QSize, Qt
from PyQt4.QtGui import QApplication, QWidget, QGroupBox, QVBoxLayout, QPushButton, QScrollArea
import logging
LOG = logging.getLogger(__name__)
# This is really really ugly, but the QDockWidget for some reason does not notice when
# its child widget becomes smaller...
# Therefore we manually set its minimum size when our own minimum size changes
class MyVBoxLayout(QVBoxLayout):
def __init__(self, parent=None):
QVBoxLayout.__init__(self, parent)
self._last_size = QSize(0, 0)
def setGeometry(self, r):
QVBoxLayout.setGeometry(self, r)
try:
wid = self.parentWidget().parentWidget()
new_size = self.minimumSize()
if new_size == self._last_size: return
self._last_size = new_size
twid = wid.titleBarWidget()
if twid is not None:
theight = twid.sizeHint().height()
else:
theight = 0
new_size += QSize(0, theight)
wid.setMinimumSize(new_size)
except Exception:
pass
class AbstractAttributeHandler:
def defaults(self):
pass
def updateValues(self, values):
pass
def setItems(self, items, showItemClasses=False):
pass
class AttributeHandlerFactory:
def create(self, attribute, values):
# At the moment we always create a DefaultAttributeHandler
# But in the future this could also create user-defined attribute editors
# Class attribute cannot be changed
if attribute == 'class':
return None
# Just a value. No attribute editor needed, we just add it to the item to be inserted...
if isinstance(values, str) or isinstance(values, float) or isinstance(values, int):
return None
# If it's already a handler, just return it
elif isinstance(values, AbstractAttributeHandler):
return values
# Else, we create our own default handler
else:
return DefaultAttributeHandler(attribute, values)
class DefaultAttributeHandler(QGroupBox, AbstractAttributeHandler):
def __init__(self, attribute, values, parent=None):
QGroupBox.__init__(self, attribute, parent)
self._attribute = attribute
self._values = []
self._current_items = []
self._defaults = {}
# Setup GUI
self._layout = FloatingLayout()
self.setLayout(self._layout)
self._buttons = {}
# Add interface elements
self.updateValues(values)
def updateValues(self, values):
# TODO: Properly parse
for val in values:
if isinstance(val, str) or isinstance(val, float) or isinstance(val, int):
val = str(val)
if val not in self._values:
self.addValue(val)
def defaults(self):
return self._defaults
def addValue(self, v):
self._values.append(v)
button = QPushButton(v, self)
button.setFlat(True)
button.setCheckable(True)
self._buttons[v] = button
self._layout.addWidget(button)
button.clicked.connect(bind(self.onButtonClicked, v))
def reset(self):
self._current_items = []
for v, button in self._buttons.items():
button.setChecked(False)
button.setFlat(True)
def setItems(self, items, showItemClasses=False):
self.reset()
if showItemClasses:
title = ", ".join(set([item['class'] for item in items]))
self.setTitle(self._attribute + " (" + title + ")")
else:
self.setTitle(self._attribute)
selected_values = set([item[self._attribute] for item in items if self._attribute in item])
for val in selected_values:
if len(selected_values) > 1:
self._buttons[val].setFlat(False)
else:
self._buttons[val].setChecked(True)
self._current_items = items
def onButtonClicked(self, val):
attr = self._attribute
LOG.debug("Button %s: %s clicked" % (attr, val))
button = self._buttons[val]
# Unpress all other buttons
for v, but in self._buttons.items():
but.setFlat(True)
if but is not button:
but.setChecked(False)
# Update model item
for item in self._current_items:
if button.isChecked():
item[attr] = val
else:
item[attr] = None
class LabelEditor(QScrollArea):
def __init__(self, items, parent=None):
QScrollArea.__init__(self, parent)
self._editor = parent
self._items = items
# Find all classes
self._label_classes = set([item['class'] for item in items if 'class' in item])
n_classes = len(self._label_classes)
LOG.debug("Creating editor for %d item classes: %s" % (n_classes, ", ".join(list(self._label_classes))))
# Widget layout
self._layout = QVBoxLayout()
self._content = QWidget()
self._content.setLayout(self._layout)
attributes = set()
for lc in self._label_classes:
attributes |= set(self._editor.getLabelClassAttributes(lc))
for attr in attributes:
handler = self._editor.getHandler(attr)
if handler is not None:
if len(items) > 1:
valid_items = [item for item in items if attr in self._editor.getLabelClassAttributes(item['class'])]
handler.setItems(valid_items, True)
else:
handler.setItems(items)
self._layout.addWidget(handler)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setWidgetResizable(True)
self.setWidget(self._content)
def sizeHint(self):
minsz = self.minimumSize()
sz = self._layout.minimumSize()
left, top, right, bottom = self.getContentsMargins()
return QSize(max(minsz.width(), sz.width() + left + right), max(minsz.height(), sz.height() + top + bottom))
def labelClasses(self):
return self._label_classes
def currentProperties(self):
if len(self._items) > 1:
return {}
else:
return self._items[0]
class PropertyEditor(QWidget):
# Signals
insertionModeStarted = pyqtSignal(str)
insertionModeEnded = pyqtSignal()
insertionPropertiesChanged = pyqtSignal(object)
editPropertiesChanged = pyqtSignal(object)
def __init__(self, config, parent=None):
QWidget.__init__(self, parent)
self._class_config = {}
self._class_items = {}
self._class_prototypes = {}
self._attribute_handlers = {}
self._handler_factory = AttributeHandlerFactory()
self._setupGUI()
# Add label classes from config
for label in config:
self.addLabelClass(label)
def addLabelClass(self, label_config):
# Check label configuration
if 'attributes' not in label_config:
raise ImproperlyConfigured("Label with no 'attributes' dict found")
attrs = label_config['attributes']
if 'class' not in attrs:
raise ImproperlyConfigured("Labels must have an attribute 'class'")
label_class = attrs['class']
if label_class in self._class_config:
raise ImproperlyConfigured("Label with class '%s' defined more than once" % label_class)
# Store config
self._class_config[label_class] = label_config
# Parse configuration and create handlers and item
self.parseConfiguration(label_class, label_config)
# Add label class button
button = QPushButton(label_class, self)
button.setCheckable(True)
button.setFlat(True)
button.clicked.connect(self.onClassButtonPressed)
self._class_buttons[label_class] = button
self._classbox_layout.addWidget(button)
def parseConfiguration(self, label_class, label_config):
attrs = label_config['attributes']
# Add prototype item for insertion
self._class_items[label_class] = AnnotationModelItem({ 'class': label_class })
# Create attribute handler widgets or update their values
for attr, vals in attrs.items():
if attr in self._attribute_handlers:
self._attribute_handlers[attr].updateValues(vals)
else:
handler = self._handler_factory.create(attr, vals)
if handler is None:
self._class_items[label_class][attr] = vals
else:
self._attribute_handlers[attr] = handler
for attr in attrs:
if attr in self._attribute_handlers:
self._class_items[label_class].update(self._attribute_handlers[attr].defaults())
def getHandler(self, attribute):
if attribute in self._attribute_handlers:
return self._attribute_handlers[attribute]
else:
return None
def getLabelClassAttributes(self, label_class):
return self._class_config[label_class]['attributes'].keys()
def onClassButtonPressed(self):
if self.sender().isChecked():
self.startInsertionMode(str(self.sender().text()))
else:
self.endInsertionMode()
def startInsertionMode(self, label_class):
self.endInsertionMode(False)
for lc, button in self._class_buttons.items():
button.setChecked(lc == label_class)
LOG.debug("Starting insertion mode for %s" % label_class)
self._label_editor = LabelEditor([self._class_items[label_class]], self)
self._layout.insertWidget(1, self._label_editor, 0)
self.insertionModeStarted.emit(label_class)
def endInsertionMode(self, uncheck_buttons=True):
if self._label_editor is not None:
LOG.debug("Ending insertion mode")
self._label_editor.hide()
self._layout.removeWidget(self._label_editor)
self._label_editor = None
self.uncheckAllButtons()
self.insertionModeEnded.emit()
def uncheckAllButtons(self):
for lc, button in self._class_buttons.items():
button.setChecked(False)
def markEditButtons(self, label_classes):
for lc, button in self._class_buttons.items():
button.setFlat(lc not in label_classes)
def currentEditorProperties(self):
if self._label_editor is None:
return None
else:
return self._label_editor.currentProperties()
def startEditMode(self, model_items):
self.endInsertionMode()
LOG.debug("Starting edit mode for items: %s" % model_items)
self._label_editor = LabelEditor(model_items, self)
self.markEditButtons(self._label_editor.labelClasses())
self._layout.insertWidget(1, self._label_editor, 0)
def _setupGUI(self):
self._class_buttons = {}
self._label_editor = None
# Label class buttons
self._classbox = QGroupBox("Labels", self)
self._classbox_layout = FloatingLayout()
self._classbox.setLayout(self._classbox_layout)
# Global widget
self._layout = MyVBoxLayout()
self.setLayout(self._layout)
self._layout.addWidget(self._classbox, 0)
self._layout.addStretch(1)
def main():
from sloth.conf import config
config.update("/home/mfischer/videmo_config_new_simple")
app = QApplication(sys.argv)
ba = PropertyEditor(config.LABELS)
ba.show()
return app.exec_()
if __name__ == '__main__':
sys.exit(main())