Source code for abcview.app

#! /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 toggle_widget(self, widget): """ Toggles visibilty of given widget. :param widget: AbcView widget object. """ if widget.isHidden(): widget.show() else: widget.hide()
[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_()