#! /usr/bin/env python
#-******************************************************************************
#
# Copyright (c) 2012-2013,
# Sony Pictures Imageworks Inc. and
# Industrial Light & Magic, a division of Lucasfilm Entertainment Company Ltd.
#
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
# * Neither the name of Sony Pictures Imageworks, nor
# Industrial Light & Magic, nor the names of their contributors may be used
# to endorse or promote products derived from this software without specific
# prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
#-******************************************************************************
import os
import re
import sys
import math
import time
import logging
import traceback
from functools import wraps
from copy import deepcopy
from PyQt4 import QtCore
from PyQt4 import QtGui
from PyQt4 import uic
import imath
import alembic
import abcview
from abcview import log, style, config
from abcview.io import Session, Scene, Camera, ICamera
from abcview.gl import GLCamera, GLICamera, GLScene
from abcview.gl import get_final_matrix
from abcview.widget.console_widget import AbcConsoleWidget
from abcview.widget.viewer_widget import GLWidget
from abcview.widget.time_slider import TimeSlider
from abcview.widget.tree_widget import *
from abcview.utils import json
__all__ = ['create_app', 'AbcView', 'io2gl', 'version_check', ]
def make_dirty(func):
"""make abcview session dirty decorator"""
@wraps(func)
def with_wrapped_func(*args, **kwargs):
return func(*args, **kwargs)
if args[0].session:
args[0].session.make_dirty()
return with_wrapped_func
def make_clean(func):
"""make abcview session clean decorator"""
@wraps(func)
def with_wrapped_func(*args, **kwargs):
return func(*args, **kwargs)
if args[0].session:
args[0].session.make_clean()
return with_wrapped_func
def wait(func):
"""wait/arrow mouse cursor decorator"""
@wraps(func)
def with_wrapped_func(*args, **kwargs):
args[0].setCursor(QtCore.Qt.WaitCursor)
ret = func(*args, **kwargs)
args[0].setCursor(QtCore.Qt.ArrowCursor)
return ret
return with_wrapped_func
def message(info):
dialog = QtGui.QMessageBox()
dialog.setStyleSheet(style.DIALOG)
dialog.setText(info)
dialog.exec_()
# global file load counter (for default scene colors)
global COUNT; COUNT = 0
[docs]def io2gl(item, viewer=None):
"""
Recursively recasts an IO-module object to a GL-module object.
Used for loading session items into the GL viewer.
:param item: Session, Scene, Camera or ICamera object
:param viewer: GLWidget object
"""
global COUNT; COUNT += 1
if item.type() == Session.type():
for child in item.walk():
io2gl(child, viewer)
return
elif item.type() == Scene.type():
if not os.path.isfile(item.filepath):
log.warn("file not found %s" % item.filepath)
return
item.__class__ = GLScene
item.init()
elif item.type() == Camera.type():
_translation = item.translation
_rotation = item.rotation
_scale = item.scale
_center = item.center
_near = item.near
_far = item.far
_ratio = item.aspect_ratio
item.__class__ = GLCamera
item.init(viewer,
translation=_translation,
rotation=_rotation,
scale=_scale,
center=_center,
near=_near,
far=_far,
aspect_ratio=_ratio,
)
elif item.type() == ICamera.type():
item.__class__ = GLICamera
item.init(viewer)
else:
return
class QScriptAction(QtGui.QGroupBox):
def __init__(self, name, filepath, action, doc="", version="", author=""):
"""
:param name: name of the script
:param action: QWidgetAction object
:param doc: doc string
:param version: version string
:param author: author string
"""
super(QScriptAction, self).__init__()
self.action = action
self.layout = QtGui.QHBoxLayout()
self.layout.setMargin(0)
self.layout.setSpacing(0)
self.label = QtGui.QPushButton(os.path.basename(name))
self.button = QtGui.QPushButton()
self.button.setFixedSize(12, 12)
self.button.setObjectName("edit_button")
self.button.setIcon(QtGui.QIcon("%s/edit.png" % config.ICON_DIR))
self.button.setIconSize(self.button.size())
self.button.setFocusPolicy(QtCore.Qt.NoFocus)
self.layout.addWidget(self.label)
self.layout.addWidget(self.button)
self.name = name
self.filepath = filepath
self.setLayout(self.layout)
self.label.pressed.connect(self.handle_clicked)
self.button.pressed.connect(self.handle_edit)
self.setToolTip("%s %s\n(author: %s)\n\n%s" %(name, version, author, doc))
def handle_clicked(self):
"""script click handler"""
self.action.trigger()
def handle_edit(self):
"""script edit button handler"""
cmd = " ".join([config.SCRIPT_EDITOR, self.filepath])
log.debug(cmd)
os.system(cmd)
class AbcMenuBar(QtGui.QMenuBar):
def __init__(self, parent, main):
super(AbcMenuBar, self).__init__(parent)
self.main = main
self.setFocusPolicy(QtCore.Qt.ClickFocus)
self.file_menu = QtGui.QMenu("File")
self.widget_menu = QtGui.QMenu("Widgets")
self.script_menu = QtGui.QMenu("Scripts")
self.help_menu = QtGui.QMenu("Help")
self.file_menu.setStyleSheet(style.MAIN)
self.widget_menu.setStyleSheet(style.MAIN)
self.script_menu.setStyleSheet(style.MAIN)
self.help_menu.setStyleSheet(style.MAIN)
self.setup_file_menu()
self.setup_wid_menu()
self.setup_script_menu()
self.setup_help_menu()
def setup_file_menu(self):
self.file_menu.addAction("New", self.main.handle_new, "Ctrl+N")
self.file_menu.addAction("Open", self.main.handle_open, "Ctrl+O")
self.file_menu.addAction("Import", self.main.handle_import, "Ctrl+I")
self.file_menu.addAction("Reload", self.main.handle_reload, "Ctrl+R")
self.file_menu.addSeparator()
self.file_menu.addAction("Save", self.main.handle_save, "Ctrl+S")
self.file_menu.addAction("Save As..", self.main.handle_save_as, "Ctrl+Shift+S")
self.file_menu.addSeparator()
self.file_menu.addAction("Save Layout", self.main.save_settings, "Ctrl+Alt+S")
self.file_menu.addAction("Reset Layout", self.main.reset_settings)
self.file_menu.addAction("Review Mode", self.main.review_settings)
self.file_menu.addSeparator()
self.file_menu.addAction("Quit", self.main.close, "Ctrl+Q")
self.addMenu(self.file_menu)
def setup_wid_menu(self):
self.console_action = QtGui.QAction("Console", self)
self.console_action.setShortcut("Ctrl+Shift+C")
self.console_action.setCheckable(True)
self.console_action.setChecked(True)
self.connect(self.console_action, QtCore.SIGNAL("toggled (bool)"), self.handle_show_console)
self.widget_menu.addAction(self.console_action)
self.object_action = QtGui.QAction("Objects", self)
self.object_action.setShortcut("Ctrl+Shift+O")
self.object_action.setCheckable(True)
self.object_action.setChecked(True)
self.connect(self.object_action, QtCore.SIGNAL("toggled (bool)"), self.handle_show_objects)
self.widget_menu.addAction(self.object_action)
self.props_action = QtGui.QAction("Properties", self)
self.props_action.setShortcut("Ctrl+Shift+P")
self.props_action.setCheckable(True)
self.props_action.setChecked(True)
self.connect(self.props_action, QtCore.SIGNAL("toggled (bool)"), self.handle_show_props)
self.widget_menu.addAction(self.props_action)
self.time_slider_action = QtGui.QAction("Timeline", self)
self.time_slider_action.setShortcut("Ctrl+Shift+T")
self.time_slider_action.setCheckable(True)
self.time_slider_action.setChecked(True)
self.connect(self.time_slider_action, QtCore.SIGNAL("toggled (bool)"), self.handle_show_timeline)
self.widget_menu.addAction(self.time_slider_action)
self.viewer_action = QtGui.QAction("Viewer", self)
self.viewer_action.setShortcut("Ctrl+Shift+V")
self.viewer_action.setCheckable(True)
self.viewer_action.setChecked(True)
self.connect(self.viewer_action, QtCore.SIGNAL("toggled (bool)"), self.handle_show_viewer)
self.widget_menu.addAction(self.viewer_action)
self.addMenu(self.widget_menu)
def setup_script_menu(self):
self.addMenu(self.script_menu)
self.handle_refresh_scripts()
def setup_help_menu(self):
self.help_menu.addAction("About Alembic", self.handle_about_abc)
self.help_menu.addAction("About AbcView", self.handle_about_abcview)
self.addMenu(self.help_menu)
def handle_refresh_scripts(self):
self.script_menu.clear()
self.script_menu.addAction("Refresh", self.handle_refresh_scripts)
self.script_menu.addSeparator()
def get_docs(script):
"""
parses a python file looking for docstring, __name__ and __author__.
:param script: filepath to .py file
"""
import ast
doc = ""
name = os.path.basename(script)
version = ""
author = ""
try:
m = ast.parse(''.join(open(script)))
doc = ast.get_docstring(m)
assigns = [node for node in m.body if type(node) == ast.Assign]
names = [a.value.s for a in assigns if getattr(a.targets[0], 'id', None) == '__name__']
authors = [a.value.s for a in assigns if getattr(a.targets[0],'id', None) == '__author__']
versions = [a.value.s for a in assigns if getattr(a.targets[0], 'id', None) == '__version__']
if names:
name = names[0]
else:
name = os.path.basename(script)
if authors:
author = authors[0]
if versions:
version = versions[0]
except Exception, e:
log.error(e)
return doc, name, version, author
def find_scripts(path):
"""
walks scripts dir looking for .py files
:param path: directory containing scripts
"""
_scripts = {}
if not path or not os.path.isdir(path):
return
for r, d, f in os.walk(path):
for files in f:
name = os.path.basename(files)
if name == "__init__.py":
continue
if files.endswith(".py"):
script = os.path.join(r, files)
doc, name, version, author = get_docs(script)
_scripts[name] = {
"name": name,
"filepath": script,
"doc": doc,
"version": version,
"author": author
}
return _scripts
def build_scripts(scripts):
"""
builds up the script menu from a dict of found scripts
"""
for name, meta in scripts.items():
doc = meta.get("doc")
version = meta.get("version")
author = meta.get("author")
filepath = meta.get("filepath")
script_act = QtGui.QWidgetAction(self.script_menu)
script_act.setDefaultWidget(QScriptAction(name, filepath, script_act, doc, version, author))
script_act.setData(filepath)
script_act.triggered.connect(self.handle_run_script)
self.script_menu.addAction(script_act)
# built-in scripts that ship with abcview
scripts = find_scripts(config.SCRIPT_DIR)
# user-defined script paths
for path in config.USER_SCRIPT_DIR:
scripts.update(find_scripts(path) or {})
# build the scripts menu
build_scripts(scripts)
def handle_run_script(self):
script_path = str(self.sender().data().toString())
self.main.load_script(script_path)
def handle_show_console(self, toggled):
self.main.toggle_widget(self.main.console)
def handle_show_objects(self):
self.main.toggle_widget(self.main.objects_group)
def handle_show_props(self):
self.main.toggle_widget(self.main.properties_splitter)
def handle_show_timeline(self):
self.main.toggle_widget(self.main.time_slider_toolbar)
def handle_show_viewer(self):
self.main.toggle_widget(self.main.viewer_group)
def handle_about_abc(self):
message("Using Alembic %s" % alembic.Abc.GetLibraryVersionShort())
def handle_about_abcview(self):
_v = " ".join([config.__prog__, config.__version__])
message("\n".join([_v, abcview.__doc__]))
class FindLineEdit(QtGui.QLineEdit):
"""
Auto-unfocus line editor used for search bar.
"""
def __init__(self, parent):
super(FindLineEdit, self).__init__(parent)
self._parent = parent
self.setSizePolicy(QtGui.QSizePolicy.Expanding,
QtGui.QSizePolicy.Maximum)
self.setFocusPolicy(QtCore.Qt.ClickFocus)
def leaveEvent(self, event):
self._parent.setFocus()
super(FindLineEdit, self).leaveEvent(event)
class Splash(QtGui.QSplashScreen):
"""
AbcView splash screen.
"""
def __init__(self, parent):
super(Splash, self).__init__(parent)
self._parent = parent
self.setStyleSheet(style.SPLASH)
self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
# logo
self.logo = QtGui.QLabel()
self.logo.setPixmap(QtGui.QPixmap("%s/logo.png" % config.ICON_DIR))
self.resize(600, 350)
self.move(0, 0)
# layout
layout = QtGui.QVBoxLayout()
layout.setSpacing(0)
layout.setMargin(0)
layout.addWidget(self.logo)
self.text = QtGui.QLineEdit()
self.progress = QtGui.QProgressBar()
self.progress.setMaximum(100)
self.progress.setMinimum(0)
self.progress.setValue(0)
self.progress.setTextVisible(False)
layout.addWidget(self.progress)
layout.addWidget(self.text)
self.setLayout(layout)
def updateProgress(self, value):
"""
Updates the splash screen progress bar.
:param value: new value (must be b/w min and max values)
"""
self.progress.setValue(value)
def setMessage(self, message):
"""
Sets the message to be displayed in the splash screen.
:param message: message as string
"""
self.text.setText(message)
## MAIN -----------------------------------------------------------------------
[docs]class AbcView(QtGui.QMainWindow):
'''
Main application. The best way to instantiate this class is to use
the :py:func:`.create_app` function.
'''
TITLE = " ".join([config.__prog__, config.__version__])
def __init__(self, filepath=None):
"""
Creates an instance of the AbcView Main Window.
:param filepath: file to load (.io or .abc)
"""
QtGui.QMainWindow.__init__(self)
self.setWindowState(QtCore.Qt.WindowActive)
self.setWindowFlags(QtCore.Qt.Window)
self.setWindowTitle(self.TITLE)
self.setStyle(QtGui.QStyleFactory.create('cleanlooks'))
self.setStyleSheet(style.MAIN)
self.setMinimumSize(200, 200)
self.setFocusPolicy(QtCore.Qt.StrongFocus)
# deferred load list, files loaded after event loop starts
self._load_files = []
# overrides are used for deferring frame range overrides
self._overrides = {}
# for storing session data
self.settings = QtCore.QSettings("Alembic",
"-".join([config.__prog__, config.__version__]))
self.session = Session(filepath)
# main menu
self.main_menu = AbcMenuBar(self, main=self)
self.setMenuBar(self.main_menu)
self.objects_group = QtGui.QGroupBox(self)
self.objects_group.setLayout(QtGui.QVBoxLayout())
#TODO: refactor these signals
self.objects_tree = ObjectTreeWidget(self, main=self)
self.objects_tree.signal_item_removed.connect(self.handle_item_removed)
self.objects_tree.signal_view_camera.connect(self.handle_view_camera)
self.objects_tree.itemSelectionChanged.connect(self.handle_object_selection)
self.objects_tree.itemClicked.connect(self.handle_item_selected)
self.find_line_edit = FindLineEdit(self)
self.objects_group.layout().setSpacing(0)
self.objects_group.layout().setMargin(0)
self.objects_group.layout().addWidget(self.find_line_edit)
self.objects_group.layout().addWidget(self.objects_tree)
# tree widgets
self.properties_tree = PropertyTreeWidget(self, main=self)
self.samples_tree = SampleTreeWidget(self, main=self)
self.array_tree = ArrayTreeWidget(self, main=self)
# console widget
self.console = AbcConsoleWidget(self)
self.console.updateNamespace({
'exit': self.console.exit,
'find': self.find,
'self': self, # legacy, don't use
'app': self,
'objects': self.objects_tree,
'properties': self.properties_tree,
'samples': self.samples_tree,
'alembic': alembic,
'abcview': abcview
})
# viewer
self.viewer = GLWidget(self)
self.viewer_group = QtGui.QGroupBox(self)
self.viewer_group.setLayout(QtGui.QVBoxLayout())
self.viewer_group.layout().setSpacing(0)
self.viewer_group.layout().setMargin(0)
self.viewer_group.layout().addWidget(self.viewer)
self.main_menu.setFocusProxy(self.viewer)
# viewer/state connections
self.viewer.signal_scene_error.connect(self.handle_viewer_error)
self.viewer.signal_scene_opened.connect(self.handle_scene_opened)
self.viewer.signal_set_camera.connect(self.handle_set_camera)
self.viewer.signal_new_camera.connect(self.handle_new_camera)
self.viewer.state.signal_current_frame.connect(self.handle_update_frame)
self.viewer.state.signal_play_fwd.connect(self.handle_state_play_fwd)
self.viewer.state.signal_play_stop.connect(self.handle_state_play_stop)
self.viewer.signal_scene_selected.connect(self.handle_scene_selected)
self.viewer.signal_object_selected.connect(self.handle_object_selected)
self.viewer.signal_clear_selection.connect(self.objects_tree.clearSelection)
self.viewer.signal_undrawable_scene.connect(self.handle_bad_scene)
# time slider
self.time_slider = TimeSlider(self)
self.time_slider.setFocus(True)
self.time_slider.signal_play_fwd.connect(self.handle_play)
self.time_slider.signal_play_stop.connect(self.handle_stop)
#self.time_slider.signal_frame_changed.connect(self.handle_time_slider_change)
self.time_slider.signal_first_frame_changed.connect(self.handle_first_frame_change)
self.time_slider.signal_last_frame_changed.connect(self.handle_last_frame_change)
self.time_slider_toolbar = QtGui.QToolBar(self)
self.time_slider_toolbar.setObjectName("time_slider_toolbar")
self.time_slider_toolbar.addWidget(self.time_slider)
self.time_slider_toolbar.setMovable(False)
self.addToolBar(QtCore.Qt.BottomToolBarArea, self.time_slider_toolbar)
# splitters
self.main_splitter = QtGui.QSplitter(QtCore.Qt.Vertical, self)
self.console_splitter = QtGui.QSplitter(QtCore.Qt.Vertical, self)
self.objects_splitter = QtGui.QSplitter(QtCore.Qt.Horizontal, self)
self.properties_splitter = QtGui.QSplitter(QtCore.Qt.Vertical, self)
self.properties_splitter.addWidget(self.properties_tree)
self.properties_splitter.addWidget(self.samples_tree)
self.properties_splitter.addWidget(self.array_tree)
self.objects_splitter.addWidget(self.objects_group)
self.objects_splitter.addWidget(self.viewer_group)
self.objects_splitter.addWidget(self.properties_splitter)
self.main_splitter.addWidget(self.objects_splitter)
self.console_splitter.addWidget(self.main_splitter)
self.console_splitter.addWidget(self.console)
self.setCentralWidget(self.console_splitter)
# TODO: refactor signals to new signal object method
self.properties_tree.connect(self.objects_tree, QtCore.SIGNAL("itemSelectionChanged()"),
self.properties_tree.clear)
self.samples_tree.connect(self.properties_tree, QtCore.SIGNAL("itemSelectionChanged()"),
self.samples_tree.clear)
self.array_tree.connect(self.samples_tree, QtCore.SIGNAL("itemSelectionChanged()"),
self.array_tree.clear)
self.connect(self.objects_tree, QtCore.SIGNAL("itemLoaded (PyQt_PyObject)"),
self.handle_item_loaded)
self.connect(self.objects_tree, QtCore.SIGNAL("itemUnloaded (PyQt_PyObject)"),
self.handle_item_unloaded)
self.properties_tree.connect(self.objects_tree, QtCore.SIGNAL("itemClicked (PyQt_PyObject)"),
self.handle_object_clicked)
self.samples_tree.connect(self.properties_tree, QtCore.SIGNAL("itemClicked (PyQt_PyObject)"),
self.handle_property_clicked)
self.array_tree.connect(self.samples_tree, QtCore.SIGNAL("itemClicked (PyQt_PyObject)"),
self.array_tree.show_values)
self.connect(self.find_line_edit, QtCore.SIGNAL("returnPressed ()"),
self.handle_find)
# wait for main event loop to start
QtGui.QApplication.instance().signal_starting_up.connect(self._start)
# create the splash screen
self.splash = Splash(self)
# open a session
if filepath and filepath.endswith(Session.EXT):
self.open_file(filepath)
def __repr__(self):
return "<%s %s>" % (self.__class__.__name__, id(self))
@make_clean
[docs] def clear(self):
"""
Clears session and viewer.
"""
self.session.clear()
self.viewer.clear()
self.objects_tree.clear()
self.properties_tree.clear()
self.samples_tree.clear()
self.array_tree.clear()
[docs] def confirm_close(self, message):
"""
Window close confirmation.
:param message: text to display
"""
msg = QtGui.QMessageBox()
msg.setStyleSheet(style.DIALOG)
msg.setText(message);
msg.setInformativeText("Do you want to save your changes?");
msg.setStandardButtons(QtGui.QMessageBox.Save | QtGui.QMessageBox.Discard | QtGui.QMessageBox.Cancel);
msg.setDefaultButton(QtGui.QMessageBox.Save);
return msg.exec_()
## settings
def _settings(self, width, height):
# default position settings
self.setGeometry(200, 200, width, height)
# default splitter locations
self.objects_splitter.setSizes([50, 200, 50])
# default widgets settings
self.main_menu.object_action.setChecked(False)
self.main_menu.props_action.setChecked(False)
self.main_menu.console_action.setChecked(False)
self.main_menu.viewer_action.setChecked(True)
self.main_menu.time_slider_action.setChecked(True)
# default viewer settings
self.viewer.resize(self.width(), self.height())
self.viewer.draw_hud = False
[docs] def reset_settings(self, width=550, height=500):
"""
Resets the AbcView layout settings to default values.
"""
self.settings.clear()
self._settings(width, height)
self.viewer.camera.draw_grid = True
self.viewer.camera.draw_hud = False
self.viewer.camera.draw_normals = False
self.viewer.camera.fixed = False
self.viewer.camera.visible = True
[docs] def review_settings(self):
"""
Loads "review" display settings. Does not affect saved settings.
"""
self._settings(1200, 600)
self.viewer.camera.draw_grid = False
self.viewer.camera.draw_hud = True
self.viewer.camera.draw_normals = False
self.viewer.camera.fixed = True
self.viewer.camera.visible = True
[docs] def load_settings(self):
"""
Loads AbcView layout settings from a PyQt settings file.
"""
geom = self.settings.value('geometry')
if geom.isNull() or not geom.isValid():
self.reset_settings()
return
# general settings
self.restoreGeometry(geom.toByteArray())
self.restoreState(self.settings.value('window_state').toByteArray())
# layout settings
self.settings.beginGroup("layout")
self.main_splitter.restoreState(
self.settings.value('main_splitter').toByteArray())
self.objects_splitter.restoreState(
self.settings.value('objects_splitter').toByteArray())
self.properties_splitter.restoreState(
self.settings.value('properties_splitter').toByteArray())
self.console_splitter.restoreState(
self.settings.value('console_splitter').toByteArray())
self.main_menu.object_action.setChecked(not self.settings.value('objects_hidden').toBool())
self.objects_group.setHidden(self.settings.value('objects_hidden').toBool())
self.main_menu.props_action.setChecked(not self.settings.value('properties_hidden').toBool())
self.properties_splitter.setHidden(self.settings.value('properties_hidden').toBool())
self.main_menu.console_action.setChecked(not self.settings.value('console_hidden').toBool())
self.console.setHidden(self.settings.value('console_hidden').toBool())
self.main_menu.time_slider_action.setChecked(not self.settings.value('time_slider_hidden').toBool())
self.time_slider_toolbar.setHidden(self.settings.value('time_slider_hidden').toBool())
self.main_menu.viewer_action.setChecked(not self.settings.value('viewer_hidden').toBool())
self.viewer_group.setHidden(self.settings.value('viewer_hidden').toBool())
self.settings.endGroup()
# restore viewer settings
self.settings.beginGroup("viewer")
self.viewer.restoreGeometry(self.settings.value("geometry").toByteArray())
# drawing mode
mode, found = self.settings.value("draw_mode").toInt()
if found:
self.viewer.mode = mode
self.settings.endGroup()
[docs] def save_settings(self, settings=None):
"""
Saves layout settings to a PyQt settings file.
"""
if settings is None:
settings = self.settings
# general settings
settings.setValue("geometry", self.saveGeometry())
settings.setValue("window_state", self.saveState())
# layout settings
settings.beginGroup("layout")
settings.setValue("main_splitter", self.main_splitter.saveState())
settings.setValue("objects_splitter", self.objects_splitter.saveState())
settings.setValue("properties_splitter", self.properties_splitter.saveState())
settings.setValue("console_splitter", self.console_splitter.saveState())
settings.setValue("objects_hidden", self.objects_group.isHidden())
settings.setValue("properties_hidden", self.properties_splitter.isHidden())
settings.setValue("console_hidden", self.console.isHidden())
settings.setValue("time_slider_hidden", self.time_slider_toolbar.isHidden())
settings.setValue("viewer_hidden", self.viewer_group.isHidden())
settings.endGroup()
# save viewer settings
settings.beginGroup("viewer")
settings.setValue("geometry", self.saveGeometry())
# save timeslider settings
settings.endGroup()
@make_dirty
[docs] def get_selected(self):
"""
Returns the actively selected AbcView object.
"""
items = self.objects_tree.selectedItems()
if not items:
return
return items[0].object
## file io
[docs] def set_default_mode(self, mode):
"""
Sets the default display mode that overrides anything in the
session file(s). Disable by setting mode to None.
:param mode: alembic.io.Mode value, or None
"""
log.debug("[%s].set_default_mode: %s" % (self, mode))
self._overrides["mode"] = mode
def set_first_frame(self, frame):
log.debug("[%s.set_first_frame] %s" % (self, frame))
self._overrides["first_frame"] = frame
self.viewer.state.min_time = frame / self.viewer.state.frames_per_second
self.time_slider.set_minimum(frame)
self.time_slider.slider.setMinimum(frame)
self.viewer.state.min_frame = frame
def set_last_frame(self, frame):
log.debug("[%s.set_last_frame] %s" % (self, frame))
self._overrides["last_frame"] = frame
self.viewer.state.max_time = frame / self.viewer.state.frames_per_second
self.time_slider.set_maximum(frame)
self.time_slider.slider.setMaximum(frame)
self.viewer.state.max_frame = frame
def set_current_frame(self, frame):
log.debug("[%s.set_current_frame] %s" % (self, frame))
self._overrides["current_frame"] = frame
self.viewer.state.current_frame = frame
self.time_slider.set_value(frame)
self.time_slider.slider.setValue(frame)
self.viewer.state.current_frame = frame
[docs] def set_load_files(self, filepaths):
"""
Sets the deferred load list. Loads files after the main window and
event loop is up and running.
:param filepath: list of files to load
:param mode: Override display mode (abcview.io.Mode)
"""
self._load_files = deepcopy(filepaths)
self.splash.progress.setMaximum(len(filepaths))
[docs] def set_frames_from_session(self, session):
"""
Updates the time slider's frame range according to the min/max
frames of the given session.
:param session: GLScene object.
"""
assert session.type() == Session.type(), "Invalid session"
self.viewer.state.min_time = session.min_time
self.viewer.state.max_time = session.max_time
self.viewer.state.current_time = session.current_time
self.time_slider.set_minimum(session.min_time * session.frames_per_second)
self.time_slider.set_maximum(session.max_time * session.frames_per_second)
self.time_slider.set_value(session.current_time * session.frames_per_second)
[docs] def set_frames_from_scene(self, scene):
"""
Updates the time slider's frame range according to the min/max
frames of the given scene.
:param scene: GLScene object.
"""
assert scene.type() == GLScene.type(), "Invalid scene"
self.viewer.state.min_time = scene.min_time()
self.viewer.state.max_time = scene.max_time()
self.time_slider.set_minimum(scene.min_time() * self.session.frames_per_second)
self.time_slider.set_maximum(scene.max_time() * self.session.frames_per_second)
def _start(self):
"""
This is the startup callback function for when the QApplication starts
its event loop. This handles deferred file loading.
"""
log.debug("[%s]._start" % self)
self.viewer.setDisabled(True)
self.splash.setMessage("starting up")
self._wait()
start = time.time()
self.splash.show()
# build the abcview session
try:
self._load()
except Exception, e:
traceback.print_exc()
log.error(e)
log.debug("session loaded in %.2fs" % (time.time() - start))
self.splash.close()
self.time_slider.signal_frame_changed.connect(self.handle_time_slider_change)
self.viewer.setEnabled(True)
def _load(self):
"""
Loads all the files and cameras in the load list, sets display
overrides and creates tree widget items.
"""
self.viewer.setDisabled(True)
# track files that can't be loaded
_bad_files = []
# if just one session file, replace current session
if len(self._load_files) == 1 and self._load_files[0].endswith(Session.EXT):
self.session = Session(self._load_files[0])
# otherwise, add each file to current session
else:
for filepath in self._load_files:
if not os.path.isfile(filepath):
_bad_files.append(filepath)
continue
try:
self.session.add_file(filepath)
except abcview.io.AbcViewError, e:
log.debug(str(e))
_bad_files.append(filepath)
# display a warning for bad files, bail if all are bad
if len(_bad_files) > 0:
message("The follow files could not be loaded:\n%s" \
%("\n".join(_bad_files)))
if len(_bad_files) == len(self._load_files):
self.viewer.setDisabled(False)
return
# convert the session items to gl objects
io2gl(self.session, self.viewer)
# item color range
COLORS = style.gen_colors(COUNT)
self.splash.progress.setMaximum(COUNT)
# walk session looking for cameras and load them
for index, item in enumerate(self.session.walk()):
self.splash.setMessage("loading %s" % item.name)
if item.type() in [GLCamera.type(), GLICamera.type()]:
self.viewer.add_camera(item)
if item.loaded:
self.viewer.set_camera(item)
# set display overrides
else:
if self._overrides.get("mode") is not None:
item.mode = self._overrides.get("mode")
if not item.properties.get("color", None):
item.color = COLORS[index]
# default frame range
self.viewer.state.min_frame = 0
self.viewer.state.max_frame = 100
self.time_slider.set_minimum(0)
self.time_slider.set_maximum(100)
# set the frame range from loaded files
if len(self._load_files) == 1:
if self._load_files[0].endswith(Session.EXT):
self.set_frames_from_session(self.session)
elif self._load_files[0].endswith(Scene.EXT) and \
self._overrides.get("mode") != Mode.OFF:
self.set_frames_from_scene(self.session.items[0])
# frame range overrides
if self._overrides.get("first_frame"):
first_frame = self._overrides.get("first_frame")
self.viewer.state.min_frame = first_frame
self.time_slider.set_minimum(first_frame)
if self._overrides.get("last_frame"):
last_frame = self._overrides.get("last_frame")
self.viewer.state.max_frame = last_frame
self.time_slider.set_maximum(last_frame)
if self._overrides.get("current_frame"):
curr_frame = int(self._overrides.get("current_frame"))
self.viewer.state.current_frame = curr_frame
self.time_slider.set_value(curr_frame)
# frame boundary adjustments
if self.viewer.state.current_time < self.viewer.state.min_time:
self.viewer.state.current_time = self.viewer.state.min_time
elif self.viewer.state.current_time > self.viewer.state.max_time:
self.viewer.state.current_time = self.viewer.state.max_time
# create trees for top-level session items, load items last
for item in self.session.items:
tree = None
if item.type() == Session.type():
tree = SessionTreeWidgetItem(self.objects_tree, item)
elif item.type() == GLScene.type():
tree = SceneTreeWidgetItem(self.objects_tree, item)
if item.loaded and tree is not None:
tree.load()
item.tree = tree
# frame the viewer if we're just loading one abc file
if len(self._load_files) == 1 and self._load_files[0].endswith(Scene.EXT):
self.viewer.frame()
self.viewer.setDisabled(False)
def _wait(self):
"""
Waits for the window to be drawn before proceeding.
"""
app = QtGui.QApplication.instance()
while QtGui.QApplication.instance().startingUp():
pass
while not self.isVisible():
pass
[docs] def open_file(self, filepath):
"""
File loader
:param filepath: file path to load, replaces session
"""
self.set_load_files([filepath])
self._start()
[docs] def import_file(self, filepath, overrides=None):
"""
File importer
:param filepath: file path to import, adds to session
:param overrides: parameter overrides dict
"""
self.viewer.setDisabled(True)
if filepath.endswith(Session.EXT):
item = Session(filepath)
num_items = len(item.items)
io2gl(item, self.viewer)
elif filepath.endswith(Scene.EXT):
item = GLScene(filepath)
num_items = 1
# update item from params override
if overrides:
item.name = overrides.get("name", item.name)
item.loaded = overrides.get("loaded", item.loaded)
item.color = overrides.get("color", item.color)
item.properties.update(overrides.get("properties"))
self.session.add_item(item)
COLORS = style.gen_colors(num_items)
if self._overrides.get("mode") is not None:
item.mode = self._overrides.get("mode")
if item.type() == Session.type():
item = SessionTreeWidgetItem(self.objects_tree, item)
elif item.type() == GLScene.type():
item = SceneTreeWidgetItem(self.objects_tree, item)
if item.object.loaded:
item.load()
self.viewer.setDisabled(False)
@make_clean
[docs] def save_session(self, filepath=None):
"""
Saves the current AbcView session to a given filepath, or
to the current filepath if filepath is None.
:param filepath: save to filepath (passing no args or None
overrwrites current session file.
"""
try:
self.session.min_time = self.viewer.state.min_time
self.session.max_time = self.viewer.state.max_time
self.session.current_time = self.viewer.state.current_time
self.session.frames_per_second = self.viewer.state.frames_per_second
self.session.save(filepath)
except Exception, e:
traceback.print_exc()
message("Error saving file:\n\n%s" % str(e))
[docs] def load_script(self, script):
"""
Loads and executes a python AbcView script.
:param script: code or path to file containing python code.
"""
log.debug("[%s.load_script] %s" % (self, script))
if os.path.exists(script):
self.console.runScript(script)
else:
self.console.setCommand(script)
self.console.runCommand()
## event handlers
[docs] def handle_play(self):
"""
Playback handler, starts playing the viewer.
"""
self.viewer.state.play()
[docs] def handle_stop(self):
"""
Viewer playback stop handler, stops the viewer playback.
"""
self.viewer.state.stop()
@make_dirty
def handle_item_removed(self, item=None):
self.viewer.state.stop()
if item is None:
item = self.objects_tree.selectedItems()[0]
log.debug("[%s].handle_item_removed: %s" %(self, item))
self.viewer.remove_scene(item.object)
self.session.remove_item(item.object)
[docs] def handle_scene_opened(self, scene):
"""
Scene open signal handler.
"""
frame_range = self.viewer.state.frame_range()
self.time_slider.set_minimum(frame_range[0])
self.time_slider.set_maximum(frame_range[1])
def handle_viewer_error(self, msg):
message(msg)
[docs] def handle_time_slider_change(self, frame):
"""
handles frame changes coming from time slider
:param frame: frame number
"""
self.viewer.state.current_frame = frame
[docs] def handle_first_frame_change(self, frame):
"""
handles first frame change from time slider
:param frame: frame number
"""
self.viewer.state.min_frame = frame
if self.session.frames_per_second > 0:
self.session.min_time = frame / self.session.frames_per_second
[docs] def handle_last_frame_change(self, frame):
"""
handles last frame change from time slider
:param frame: frame number
"""
self.viewer.state.max_frame = frame
if self.session.frames_per_second > 0:
self.session.max_time = frame / self.session.frames_per_second
[docs] def handle_state_play_fwd(self):
"""
viewer state play foward signal handler
"""
self.time_slider.playing = True
[docs] def handle_state_play_stop(self):
"""
viewer state play stop signal handler
"""
self.time_slider.playing = False
[docs] def handle_update_frame(self, frame):
"""
handles frame changes coming from viewer
"""
self.time_slider.set_value(frame)
# update the samples tree selected item
if not self.properties_splitter.isHidden() and \
self.samples_tree.topLevelItemCount() > 0:
p = self.properties_tree.selected()
ts = p.getTimeSampling()
t = self.time_slider.value() \
/ float(self.viewer.state.frames_per_second)
index = ts.getNearIndex(t, len(p.samples))
item = self.samples_tree.topLevelItem(index)
self.samples_tree.clearSelection()
self.samples_tree.setItemSelected(item, True)
self.samples_tree.scrollToItem(item)
[docs] def handle_bad_scene(self, scene, frame):
"""
Undrawabe scene signal handler.
:param scene: GLScene
"""
scene.tree.set_bad(True)
@make_dirty
[docs] def handle_new_camera(self, camera):
"""
handles adding a new camera to the session
:param camera: GLCamera
"""
log.debug("[%s.handle_new_camera] %s" % (self, camera))
if camera.name != "interactive":
self.session.add_camera(camera)
@make_dirty
[docs] def handle_set_camera(self, camera):
"""
handles setting the camera name on the session
:param camera: GLCamera
"""
if camera.name != "interactive":
self.session.set_camera(camera)
@make_dirty
[docs] def handle_view_camera(self, item):
"""
object tree 'view through selected' menu handler
:param item: CameraTreeWidgetItem
"""
icamera = item.camera()
if icamera.getName() not in [c.name for c in self.viewer.state.cameras]:
camera = GLICamera(self.viewer, icamera)
self.viewer.add_camera(camera)
self.viewer.set_camera(camera.name)
else:
self.viewer.set_camera(icamera.getName())
[docs] def handle_scene_selected(self, scene):
"""
GLWidget scene selected handler.
:param scene: GLScene
"""
log.debug("[%s.handle_scene_selected] %s" % (self, scene))
self.objects_tree.clearSelection()
if scene:
scene.tree.treeWidget().scrollToItem(scene.tree,
QtGui.QAbstractItemView.PositionAtCenter)
scene.tree.treeWidget().setItemSelected(scene.tree, True)
self.handle_object_clicked(scene.tree)
@wait
[docs] def handle_object_selected(self, name):
"""
GLWidget object selected handler.
:param name: name of object
"""
log.debug("[%s.handle_object_selected] %s" %(self, name))
self.objects_tree.find(str(name.toAscii()))
@wait
[docs] def find(self, name):
"""
searches for objects in the tree matching name
"""
self.setFocus()
self.objects_tree.find(name)
@wait
[docs] def handle_find(self, text=None):
"""
handles input from the search box
"""
if text is None:
text = self.find_line_edit.text()
if text:
self.find(str(text.toAscii()))
def handle_object_selection(self):
for scene in self.viewer.state.scenes:
scene.selected = False
self.viewer.updateGL()
[docs] def handle_item_selected(self, item):
"""
item click handler
:param item: SceneTreeWidgetItem, SessionTreeWidgetItem
"""
if type(item) == SceneTreeWidgetItem:
item.object.selected = True
self.viewer.updateGL()
@wait
[docs] def handle_item_loaded(self, item):
"""
load checkbox click handler
:param item: SceneTreeWidgetItem, SessionTreeWidgetItem
"""
def load(item):
if type(item) == SceneTreeWidgetItem:
self.viewer.add_scene(item.object)
elif type(item) == SessionTreeWidgetItem:
for child in item.children():
load(child)
log.debug("[%s.handle_item_loaded] %s" % (self, item.object))
self.splash.setMessage("loading %s" % item.object.name)
self.splash.updateProgress(self.splash.progress.value() + 1)
load(item)
@wait
[docs] def handle_item_unloaded(self, item):
"""
Item unloaded handler.
:param item: ObjectTreeWidgetItem
"""
self.viewer.remove_scene(item.object)
[docs] def handle_object_clicked(self, item):
"""
Object tree item clicked handler.
:param item: ObjectTreeWidgetItem
"""
self.samples_tree.clear()
self.array_tree.clear()
self.properties_tree.show_properties(item)
@wait
[docs] def handle_property_clicked(self, item):
"""
Property tree item clicked handler.
:param item: PropertyTreeWidgetItem
"""
self.array_tree.clear()
self.samples_tree.show_samples(item)
@wait
[docs] def handle_new(self):
"""
File->New menu handler
"""
self.clear()
self.setWindowTitle(self.TITLE)
@make_clean
[docs] def handle_open(self):
"""
File->Open menud handler
"""
filepath = QtGui.QFileDialog.getOpenFileName(self, "Open File",
os.getcwd(), ("Alembic Files (*.%s *.%s)"
% (Scene.EXT, Session.EXT)))
if filepath:
self.clear()
self.set_load_files([str(filepath.toAscii())])
self._load()
@make_dirty
[docs] def handle_import(self):
"""
File->Import menud handler
"""
filepath = QtGui.QFileDialog.getOpenFileName(self, "Open File",
os.getcwd(), ("Alembic Files (*.%s *.%s)"
% (Scene.EXT, Session.EXT)))
if filepath:
self.import_file(str(filepath.toAscii()))
@make_clean
[docs] def handle_reload(self):
"""
File->Reload menu handler, reloads session
"""
self.clear()
self._load()
[docs] def handle_frame_all(self):
"""
Frame all.
"""
log.debug("[%s.handle_frame_all]" % self)
self.viewer.frame()
[docs] def handle_frame_scene(self, item):
"""
Scene level framing handler.
:param item: SceneTreeWidgetItem
"""
log.debug("[%s.handle_frame_scene] %s" % (self, item))
bounds = item.object.bounds(self.viewer.state.current_time)
self.viewer.frame(bounds)
[docs] def handle_frame_object(self, item=None):
"""
Object level framing handler. Called when a user types 'f' while
the object tree widget has focus.
:param item: ObjectTreeWidgetItem
"""
log.debug("[%s.handle_frame_object] %s" % (self, item))
if item is None:
obj = self.objects_tree.selected()
else:
obj = item.object
if obj.getFullName() == "/":
self.handle_frame_scene()
md = obj.getMetaData()
if alembic.AbcGeom.IPolyMesh.matches(md) or \
alembic.AbcGeom.ISubD.matches(md):
meshObj = alembic.AbcGeom.IPolyMesh(obj.getParent(), obj.getName())
mesh = meshObj.getSchema()
bounds_prop = mesh.getSelfBoundsProperty()
elif alembic.AbcGeom.IXform.matches(md):
meshObj = alembic.AbcGeom.IXform(obj.getParent(), obj.getName())
mesh = meshObj.getSchema()
bounds_prop = mesh.getChildBoundsProperty()
else:
return
# get the final accumulated xform values
xf = get_final_matrix(meshObj)
#iss = alembic.Abc.ISampleSelector(0)
if not bounds_prop.valid():
message("Object has invalid or no bounds set.")
return
# get the bounds value from the bounds property
ts = bounds_prop.getTimeSampling()
index = ts.getNearIndex(self.viewer.state.current_time,
bounds_prop.getNumSamples())
bounds = bounds_prop.getValue(index) #iss)
log.debug("bounds at index %s: %s" % (index, bounds))
#TODO: apply local transforms when framing
if 0:
min = bounds.min()
max = bounds.max()
item = self.objects_tree.selectedItems()[0]
scene = item.scene().object
if scene.properties.get("translate"):
max = max * imath.V3d(*scene.translate)
min = min * imath.V3d(*scene.translate)
if scene.properties.get("scale"):
max = max * imath.V3d(*scene.scale)
min = min * imath.V3d(*scene.scale)
bounds = imath.Box3d(min, max)
self.viewer.frame(bounds * xf)
def handle_save(self):
if not self.session.filepath:
self.handle_save_as()
else:
self.save_session()
def handle_save_as(self):
filepath = QtGui.QFileDialog.getSaveFileName(self, "Save File",
os.getcwd(), ("Alembic Files (*.%s)" % Session.EXT))
if filepath:
self.save_session(str(filepath.toAscii()))
## base class overrides
def resizeEvent(self, event):
self.splash.move((event.size().width() / 2.0) - (self.splash.width() / 2.0),
(event.size().height() / 2.0) - (self.splash.height() / 2.0))
super(AbcView, self).resizeEvent(event)
[docs] def keyPressEvent(self, event):
"""
Handles key press events for the main application. If nothing
matches, it defers to the viewer.
"""
if event.key() == QtCore.Qt.Key_F:
if len(self.objects_tree.selectedItems()) == 0:
self.handle_frame_all()
else:
item = self.objects_tree.selectedItems()[0]
if type(item) == SceneTreeWidgetItem:
self.handle_frame_scene(item)
elif type(item) == ObjectTreeWidgetItem:
self.handle_frame_object(item)
else:
log.debug("Can't frame this object: %s" % item)
elif event.key() == QtCore.Qt.Key_Space:
if self.time_slider.playing:
self.handle_stop()
else:
self.handle_play()
elif event.key() == QtCore.Qt.Key_Backspace:
item = self.objects_tree.selectedItems()[0]
self.handle_item_removed(item)
else:
self.viewer.keyPressEvent(event)
def closeEvent(self, event):
if self.session and self.session.is_dirty():
resp = self.confirm_close("This session has changed.")
if resp == QtGui.QMessageBox.Cancel:
event.ignore()
return
elif resp == QtGui.QMessageBox.Save:
self.handle_save()
super(AbcView, self).closeEvent(event)
class App(QtGui.QApplication):
"""
QApplication subclass that emits a "startup" signal after
the event loop has started. This may trigger deferred
file loading, among other things.
"""
signal_starting_up = QtCore.pyqtSignal()
def __init__(self, args):
super(App, self).__init__(args)
# trigger the callback after the main event loop starts
QtCore.QTimer.singleShot(1, self.starting)
def starting(self):
"""
Callback function for Timer timeout.
"""
self.signal_starting_up.emit()
[docs]def version_check():
"""
Validates that alembic and alembicgl can be imported and
that they meet the minimum requires versions.
Current minimum version requirements for Alembic are 1.5.0.
:return: 1 or 0
"""
# Alembic minimum version requirement
MAJOR = 1
MINOR = 5
MAINT = 0
# validate the Alembic version
version = alembic.Abc.GetLibraryVersionShort()
major, minor, maint = version.split('.')
if int(major) >= MAJOR and int(minor) >= MINOR:
return 1
else:
print "%s %s requires Alembic %s.%s.%s or greater" \
% (config.__prog__, config.__version__, \
MAJOR, MINOR, MAINT)
return 0
[docs]def create_app(files = None,
first_frame = None,
last_frame = None,
current_frame = None,
fps = 24.0,
script = None,
bounds = False,
mode = None,
review = False,
reset = False,
verbose = False
):
"""
Creates a new instance of an :py:class:`.AbcView` application.
:param files: list of files to load
:param first_frame: set first frame
:param last_frame: set last frame
:param current_frame: set current frame
:param fps: frames per second (default 24)
:param script: python script to load
:param bounds: force scene bounding box mode
:param mode: default object draw mode
:param review: use review settings
:param reset: reset layout settings
:param verbose: verbose standard out
:return: exit code
"""
assert version_check()
assert fps > 0.0, "fps must be greater than 0"
# create application and widget
app = App(sys.argv)
win = AbcView()
# verbosity
if verbose:
log.setLevel(logging.DEBUG)
# settings
if reset:
win.reset_settings()
elif review:
win.review_settings()
else:
win.load_settings()
# show widgets (Qt oddity this is done before event loop)
win.show()
win.raise_()
# force scene bounds
if bounds:
win.set_default_mode(Mode.OFF)
win.viewer.camera.mode = Mode.OFF
win.viewer.camera.draw_bounds = 1
elif mode is not None:
win.set_default_mode(mode)
# defer file loading until event loop starts
win.set_load_files(files)
# set frame range overrides
if first_frame is not None:
win.set_first_frame(first_frame)
if last_frame is not None:
win.set_last_frame(last_frame)
if current_frame is not None:
win.set_current_frame(current_frame)
# execute some python
if script:
win.load_script(script)
# start event loop
return app.exec_()