# Copyright 2007-2008 Nanorex, Inc. See LICENSE file for details. """ MoviePropertyManager.py @author: Ninad, Bruce, Mark, Huaicai @version: $Id$ @copyright: 2007-2008 Nanorex, Inc. All rights reserved. History: ninad20070507 : Converted movie dashboard into movie Property manager (authors: various) """ from PyQt4 import QtCore, QtGui from commands.PlayMovie.Ui_MoviePropertyManager import Ui_MoviePropertyManager from PyQt4.Qt import Qt, SIGNAL, QFileDialog, QString, QMessageBox import os, foundation.env as env from utilities.Log import redmsg, greenmsg from utilities.constants import filesplit from utilities.prefs_constants import workingDirectory_prefs_key _superclass = Ui_MoviePropertyManager class MoviePropertyManager(Ui_MoviePropertyManager): """ The MoviePropertyManager class provides the Property Manager for the B{Movie mode}. The UI is defined in L{Ui_MoviePropertyManager} """ #see self.connect_or_disconnect_signals for comment about this flag isAlreadyConnected = False def connect_or_disconnect_signals(self, connect): """ Connect the slots in the Property Manager. """ if connect: change_connect = self.w.connect else: change_connect = self.w.disconnect #TODO: This is a temporary fix for a bug. When you invoke a temp mode #such as Line_Command or PanMode, entering such a temporary mode keeps the #PM from the previous mode open (and thus keeps all its signals #connected) but while exiting that temporary mode and reentering the #previous mode, it actually reconnects the signal! This gives rise to #lots of bugs. This needs more general fix in Temporary mode API. # -- Ninad 2007-10-29 if connect and self.isAlreadyConnected: return self.isAlreadyConnected = connect change_connect(self.movieResetAction, SIGNAL("triggered()"), self.movieReset) change_connect(self.moviePlayRevAction, SIGNAL("triggered()"), self.moviePlayRev) change_connect(self.moviePauseAction, SIGNAL("triggered()"), self.moviePause) change_connect(self.moviePlayAction, SIGNAL("triggered()"), self.moviePlay) change_connect(self.movieMoveToEndAction, SIGNAL("triggered()"), self.movieMoveToEnd) change_connect(self.frameNumberSlider, SIGNAL("valueChanged(int)"), self.movieSlider) change_connect(self.frameNumberSpinBox, SIGNAL("valueChanged(int)"), self.moviePlayFrame) change_connect(self.fileOpenMovieAction, SIGNAL("triggered()"), self.fileOpenMovie) change_connect(self.fileSaveMovieAction, SIGNAL("triggered()"), self.fileSaveMovie) change_connect(self.movieInfoAction, SIGNAL("triggered()"), self.movieInfo) def updateMessage(self, msg = ''): """ Updates the message box with an informative message. """ if not msg: msg = "Use movie control buttons in the Property Manager to play " \ "current simulation movie (if it exists). You can also load a" \ "previously saved movie for this model using "\ "'Open Movie File...' option." self.MessageGroupBox.insertHtmlMessage( msg, minLines = 6, setAsDefault = True ) def getOpenMovieFileInfo(self): """ Updates the message groupbox message in the Movie Property Manager """ msg = '' movie = self.w.assy.current_movie if movie: if movie.filename: fileName, numOfFrames, numOfAtoms = movie.getMovieInfo() msg1 = "Movie File: [%s]
" % (fileName) msg2 = " Number Of Frames: %s
" % (str(numOfFrames)) msg3 = " Number Of Atoms: %s " % (str(numOfAtoms)) msg = msg1 + msg2 + msg3 else: msg1 = redmsg("No movie file opened.
") msg2 = " Use 'Open movie file' to load any existing"\ " movie for the current part." msg = msg1 + msg2 else: msg = redmsg("No movie file opened.") return msg def show(self): """ Overrides the Ui_MoviePropertyManager.show) method. Updates the message groupbox with movie file information """ msg = self.getOpenMovieFileInfo() self.updateMessage(msg) _superclass.show(self) # == # bruce 050428: these attrs say when we're processing a valueChanged # signal from slider or spinbox _moviePropMgr_in_valuechanged_SL = False _moviePropMgr_in_valuechanged_SB = False # bruce 050428: this says whether to ignore signals from slider and spinbox, # since they're being changed by the program rather than by the user. # (#e The Movie method that sets it should be made a method of this class.) _moviePropMgr_ignore_slider_and_spinbox = False def _moviePropMgr_reinit(self): self._moviePropMgr_ignore_slider_and_spinbox = True try: self.frameNumberSpinBox.setMaximum(999999) # in UI.py code self.frameNumberSlider.setMaximum(999999) # guess self.frameNumberSpinBox.setValue(0) # guess self.frameNumberSlider.setValue(0) # guess finally: self._moviePropMgr_ignore_slider_and_spinbox = False return def moviePlay(self): """ Play current movie foward from current position. """ if self.w.assy.current_movie: #bruce 050427 added this condition in all these slot methods self.w.assy.current_movie._play(1) def moviePause(self): """ Pause movie. """ if self.w.assy.current_movie: self.w.assy.current_movie._pause() def moviePlayRev(self): """ Play current movie in reverse from current position. """ if self.w.assy.current_movie: self.w.assy.current_movie._play(-1) def movieReset(self): """ Move current frame position to frame 0 (beginning) of the movie. """ if self.w.assy.current_movie: self.w.assy.current_movie._reset() def movieMoveToEnd(self): """ Move frame position to the last frame (end) of the movie. """ if self.w.assy.current_movie: self.w.assy.current_movie._moveToEnd() def moviePlayFrame(self, fnum): """ Show frame fnum in the current movie. This slot receives valueChanged(int) signal from self.frameNumberSpinBox. """ if not self.w.assy.current_movie: return if self._moviePropMgr_ignore_slider_and_spinbox: return ## next line is redundant, so I removed it [bruce 050428] ## if fnum == self.w.assy.current_movie.currentFrame: return self._moviePropMgr_in_valuechanged_SB = True try: self.w.assy.current_movie._playToFrame(fnum) finally: self._moviePropMgr_in_valuechanged_SB = False return def movieSlider(self, fnum): """ Show frame fnum in the current movie. This slot receives valueChanged(int) signal from self.frameNumberSlider. """ if not self.w.assy.current_movie: return if self._moviePropMgr_ignore_slider_and_spinbox: return ## next line is redundant, so I removed it [bruce 050428] ## if fnum == self.w.assy.current_movie.currentFrame: return self._moviePropMgr_in_valuechanged_SL = True try: self.w.assy.current_movie._playSlider(fnum) finally: self._moviePropMgr_in_valuechanged_SL = False return def movieInfo(self): """ Prints information about the current movie to the history widget. """ if not self.w.assy.current_movie: return env.history.message(greenmsg("Movie Information")) self.w.assy.current_movie._info() def fileOpenMovie(self): """ Open a movie file to play. """ # bruce 050327 comment: this is not yet updated for "multiple movie objects" # and bugfixing of bugs introduced by that is in progress (only done in a klugy # way so far). ####@@@@ env.history.message(greenmsg("Open Movie File:")) assert self.w.assy.current_movie # (since (as a temporary kluge) we create an empty one, if necessary, before entering # Movie Mode, of which this is a dashboard method [bruce 050328]) if self.w.assy.current_movie and self.w.assy.current_movie.currentFrame != 0: ###k bruce 060108 comment: I don't know if this will happen when currentFrame != 0 due to bug 1273 fix... #####@@@@@ env.history.message(redmsg("Current movie must be reset to frame 0 to load a new movie.")) return # Determine what directory to open. [bruce 050427 comment: if no moviefile, we should try assy.filename's dir next ###e] if self.w.assy.current_movie and self.w.assy.current_movie.filename: odir, fil, ext = filesplit(self.w.assy.current_movie.filename) del fil, ext #bruce 050413 else: odir = self.w.currentWorkingDirectory # Fixes bug 291 (comment #4). Mark 060729. fn = QFileDialog.getOpenFileName( self, "Differential Position Bytes Format (*.dpb)", odir) if not fn: env.history.message("Cancelled.") return fn = str(fn) # Check if file with name fn is a movie file which is valid for the current Part # [bruce 050324 made that a function and made it print the history messages # which I've commented out below.] from simulation.movie import _checkMovieFile r = _checkMovieFile(self.w.assy.part, fn) if r == 1: ## msg = redmsg("Cannot play movie file [" + fn + "]. It does not exist.") ## env.history.message(msg) return elif r == 2: ## msg = redmsg("Movie file [" + fn + "] not valid for the current part.") ## env.history.message(msg) if self.w.assy.current_movie and self.w.assy.current_movie.might_be_playable(): #bruce 050427 isOpen -> might_be_playable() msg = "(Previous movie file [" + self.w.assy.current_movie.filename + "] is still open.)" env.history.message(msg) return #bruce 050427 rewrote the following to use a new Movie object from simulation.movie import find_saved_movie new_movie = find_saved_movie( self.w.assy, fn ) if new_movie: new_movie.set_alist_from_entire_part(self.w.assy.part) # kluge? might need changing... if self.w.assy.current_movie: #bruce 050427 no longer checking isOpen here self.w.assy.current_movie._close() self.w.assy.current_movie = new_movie self.w.assy.current_movie.cueMovie(propMgr = self) #Make sure to enable movie control buttons! self.command.enableMovieControls(True) self.updateFrameInformation() self._updateMessageInModePM() else: # should never happen due to _checkMovieFile call, so this msg is ok # (but if someday we do _checkMovieFile inside find_saved_movie and not here, # then this will happen as an error return from find_saved_movie) msg = redmsg("Internal error in fileOpenMovie") self.command.enableMovieControls(False) self._updateMessageInModePM(msg) env.history.message(msg) return def _updateMessageInModePM(self, msg = ''): """ Updates the message box in the Movie Property Manager with the information about the opened movie file. @WARNING: This is a temporary method. Likely to be moved/ modified when movieMode.py is cleaned up. See bug 2428 for details. """ #@WARNING: Following updates the Movie property manager's message #groupbox. This should be done in a better way. At present there # is no obvious way to tell the Movie Property manager that the # movie has changed. So this is a kludge. # See bug 2428 comment 8 for further details -- Ninad 2007-10-02 currentCommand = self.win.commandSequencer.currentCommand if currentCommand.commandName == "MOVIE": if currentCommand.propMgr: if not msg: msg = currentCommand.propMgr.getOpenMovieFileInfo() currentCommand.propMgr.updateMessage(msg) def fileSaveMovie(self): """ Save a copy of the current movie file loaded in the Movie Player. """ # Make sure there is a moviefile to save. if not self.w.assy.current_movie or not self.w.assy.current_movie.filename \ or not os.path.exists(self.w.assy.current_movie.filename): msg = redmsg("Save Movie File: No movie file to save.") env.history.message(msg) msg = "To create a movie, click on the Simulator icon." #QMimeSourceFactory.defaultFactory().setPixmap( "simicon", # self.simSetupAction.iconSet().pixmap() ) env.history.message(msg) return env.history.message(greenmsg("Save Movie File:")) if self.w.assy.filename: sdir = self.w.assy.filename else: sdir = env.prefs[workingDirectory_prefs_key] sfilter = QString("Differential Position Bytes Format (*.dpb)") # Removed .xyz as an option in the sfilter since the section of code below # to save XYZ files never worked anyway. This also fixes bug 492. Mark 050816. fn = QFileDialog.getSaveFileName( self, "Save As", sdir, "Differential Position Bytes Format (*.dpb);;POV-Ray Series (*.pov)", sfilter) if not fn: env.history.message("Cancelled.") return else: fn = str(fn) dir, fil, ext2 = filesplit(fn) del ext2 #bruce 050413 ext = str(sfilter[-5:-1]) # Get "ext" from the sfilter. It *can* be different from "ext2"!!! - Mark #bruce 050413 comment: the above assumes that len(ext) is always 4 (.xxx). # I'm speculating that this is ok for now since it has to be one of the ones # provided to the getSaveFileName method above. # Changed os.path.join > os.path.normpath to partially fix bug #956. Mark 050911. safile = os.path.normpath(dir + "/" + fil + ext) # full path of "Save As" filename if os.path.exists(safile): # ...and if the "Save As" file exists... # ... confirm overwrite of the existing file. ret = QMessageBox.warning( self, "Confirm overwrite", "The file \"" + fil + ext + "\" already exists.\n"\ "Do you want to overwrite the existing file or cancel?", "&Overwrite", "&Cancel", "", 0, # Enter == button 0 1 ) # Escape == button 1 if ret == 1: # The user cancelled env.history.message( "Cancelled. File not saved." ) return # Cancel clicked or Alt+C pressed or Escape pressed if ext == '.dpb': self.w.assy.current_movie._close() import shutil shutil.copy(self.w.assy.current_movie.filename, safile) # Get the trace file name. tfile1 = self.w.assy.current_movie.get_trace_filename() # Copy the tracefile if os.path.exists(tfile1): fullpath, ext = os.path.splitext(safile) tfile2 = fullpath + "-trace.txt" shutil.copy(tfile1, tfile2) env.history.message("DPB movie file saved: " + safile) # note that we are still playing it from the old file and filename... does it matter? [bruce 050427 question] # Added "hflag=False" to suppress history msg, fixing bug #956. Mark 050911. self.w.assy.current_movie.cueMovie(propMgr = self, hflag = False) # Do not print info to history widget. elif ext == '.pov': self.w.assy.current_movie._write_povray_series( os.path.normpath(dir + "/" + fil)) else: #.xyz (or something unexpected) #bruce 051115 added warning and immediate return, to verify that this code is never called QMessageBox.warning(self, "ERROR", "internal error: unsupported file extension %r" % (ext,) ) # args are title, content return # from fileSaveMovie def updateFrameInformation(self): """ Update movie control widgets in PM to the current movie frames. """ movie = self.w.assy.current_movie self.frameNumberSlider.setMaximum(movie.getTotalFrames()) self.frameNumberSpinBox.setMaximum(movie.getTotalFrames()) self.frameSkipSpinBox.setMaximum(movie.getTotalFrames()) #ninad060928 fixed bug 2285 self.updateCurrentFrame() return def updateCurrentFrame(self): """ Update dashboard controls which show self.currentFrame, except for the ones being used to change it. """ #bruce 050428 split this out too, added all conditions/flags; ##e it should become a method of movieDashboardSlotsMixin if not self.w.assy.current_movie: return movie = self.w.assy.current_movie old = self._moviePropMgr_ignore_slider_and_spinbox # not sure if this is ever True here self._moviePropMgr_ignore_slider_and_spinbox = True try: dont_update_slider = self._moviePropMgr_in_valuechanged_SL # SL = Slider dont_update_spinbox = self._moviePropMgr_in_valuechanged_SB # SB = SpinBox if not dont_update_slider: self.frameNumberSlider.setValue(movie.getCurrentFrame()) currentFrameLbl = str(self.frameNumberSlider.value()) totalFrameLbl = str(movie.getTotalFrames()) flabel = "Current Frame: " + currentFrameLbl + "/" + \ totalFrameLbl self.movieFrameUpdateLabel.setText(flabel) if not dont_update_spinbox: self.frameNumberSpinBox.setValue(movie.getCurrentFrame()) finally: self._moviePropMgr_ignore_slider_and_spinbox = old return def enableMovieControls(self, enabled = True): """ Enable or disable movie control button. """ self.movieResetAction.setEnabled(enabled) self.moviePlayRevAction.setEnabled(enabled) self.moviePauseAction.setEnabled(enabled) self.moviePlayAction.setEnabled(enabled) self.movieMoveToEndAction.setEnabled(enabled) self.frameNumberSlider.setEnabled(enabled) self.frameNumberSpinBox.setEnabled(enabled) self.fileSaveMovieAction.setEnabled(enabled) return pass # end of class movieDashboardSlotsMixin