summaryrefslogtreecommitdiff
path: root/cad/src/commands/PlayMovie/movieMode.py
blob: a85d7c345dbce9cf3fc37d32b93208cad9d51b04 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
# Copyright 2005-2009 Nanorex, Inc.  See LICENSE file for details.
"""
movieMode.py -- movie player mode.

@author: Mark
@version: $Id$
@copyright: 2005-2009 Nanorex, Inc.  See LICENSE file for details.

History:

By Mark.

bruce 050426 revising it, for Alpha5, mostly to fix bugs and perhaps to generalize it.
Adding various comments and revising docstrings; perhaps not signing every such change.
(Most movie code revisions will be in other files, and most revisions here will
 probably just be to adapt this file to the external changes.)

ninad20070507: moved Movie Player dashboard to Movie Property Manager.
ninad2008-08-21: Cleanup: a) to introduce command_enter/exit_* methods in the
                  new Command API b) Moved flyouttoolbar related code in its own
                  module (Ui_PlayMovieFlyout)
"""

import os

from PyQt4.Qt import Qt
from PyQt4.Qt import QDialog, QGridLayout, QPushButton, QTextBrowser, SIGNAL, QCursor

from utilities.Log import redmsg, orangemsg

import foundation.env as env
import foundation.changes as changes
import foundation.undo_manager as undo_manager

from command_support.modes import basicMode

from commands.PlayMovie.MoviePropertyManager import MoviePropertyManager

from ne1_ui.toolbars.Ui_PlayMovieFlyout import PlayMovieFlyout

# ==

class _MovieRewindDialog(QDialog):
    """
    Warn the user that a given movie is not rewound,
    explain why that matters, and offer to rewind it
    (by calling its _reset method).
    """
    def __init__(self, movie):
        self.movie = movie
        QDialog.__init__(self, None)
        self.setObjectName("movie_warning")
        self.text_browser = QTextBrowser(self)
        self.text_browser.setObjectName("movie_warning_textbrowser")
        self.text_browser.setMinimumSize(400, 40)
        self.setWindowTitle('Rewind your movie?')
        self.text_browser.setPlainText( #bruce 080827 revised text
            "You may want to rewind the movie now. The atoms move as the movie "
            "progresses, and saving the part without rewinding will save the "
            "current positions, which is sometimes useful, but will make the "
            "movie invalid, because .dpb files only store deltas relative to "
            "the initial atom positions, and don't store the initial positions "
            "themselves." )
        self.ok_button = QPushButton(self)
        self.ok_button.setObjectName("ok_button")
        self.ok_button.setText("Rewind movie")
        self.cancel_button = QPushButton(self)
        self.cancel_button.setObjectName("cancel_button")
        self.cancel_button.setText("Exit command without rewinding") #bruce 080827 revised text
            # Note: this is not, in fact, a cancel button --
            # there is no option in the caller to prevent exiting the command.
            # There is also no option to "forward to final position",
            # though for a minimize movie, that might be most useful.
            # [bruce 080827 comment]
        layout = QGridLayout(self)
        layout.addWidget(self.text_browser, 0, 0, 0, 1)
        layout.addWidget(self.ok_button, 1, 0)
        layout.addWidget(self.cancel_button, 1, 1)
        self.connect(self.ok_button, SIGNAL("clicked()"), self.rewindMovie)
        self.connect(self.cancel_button, SIGNAL("clicked()"), self.noThanks)
    def rewindMovie(self):
        self.movie._reset()
        self.accept()
    def noThanks(self):
        self.accept()
    pass

# ==

_superclass = basicMode

class movieMode(basicMode):
    """
    This class is used to play movie files.
    Users know it as "Movie mode".
    When entered, it might start playing a recently-made movie,
    or continue playing the last movie it was playing,
    or do nothing and wait for the user to open a new movie.
    The movie it is working with (if any) is always stored in assy.current_movie.
    In general, if assy.current_movie is playable when the mode is entered,
    it will start playing it. [I don't know the extent to which it will start
    from where it left off, in not only frame number but direction, etc. - bruce 050426]
    """
    # class constants
    commandName = 'MOVIE'
    featurename = "Movie Player Mode"
    from utilities.constants import CL_MISC_TOPLEVEL
    command_level = CL_MISC_TOPLEVEL

    PM_class = MoviePropertyManager #bruce 080909

    FlyoutToolbar_class = PlayMovieFlyout

    flyoutToolbar = None

    def command_entered(self):
        """
        Extends superclass method.
        @see: baseCommand.command_entered() for documentation.
        """
        _superclass.command_entered(self)
        #UPDATE 2008-09-18: Copying old code from self.Enter()
        # [bruce 050427 comment: I'm skeptical of this effect on selection,
        #  since I can't think of any good reason for it [maybe rendering speed optimization??],
        #  and once we have movies as nodes in the MT it will be a problem [why? #k],
        #  but for now I'll leave it in.]
        self.o.assy.unpickall_in_GLPane() # was: unpickparts, unpickatoms [bruce 060721]
        self.o.assy.permit_pick_atoms()


    def command_will_exit(self):
        """
        Extends superclass method, to offer to rewind the movie
        if it's not at the beginning. (Doesn't offer to prevent
        exit, only to rewind or not when exit is done.)
        """
        ask = not self.commandSequencer.exit_is_forced
            # It's not safe to rewind if exit is forced,
            # since this might happen *after* the check for whether
            # to offer to save changes in an old file being closed,
            # but it creates such changes.
            #
            # A possible fix is for Open to first exit all current commands
            # (by implicit Done, as when changing to some unrelated command),
            # before even doing the check. There are better, more complex fixes,
            # e.g. checking for changes to ask about saving (or for the need to
            # ask other questions before exit) by asking all commands on the stack.
            #
            # Note: a related necessary change is calling exit_all_commands when
            # closing a file, but I think it doesn't fix the same issue mentioned
            # above.
            #
            # [bruce 080806/080908 comments]

        if ask:
            self._offer_to_rewind_if_necessary()

        #bruce 050426 added this [originally in a method called
        # restore_patches_by_Command, no longer part of command API]
        # , to hold the side effect formerly
        # done illegally by haveNontrivialState.
        # ... but why do we need to do this at all?
        # the only point of what we'd do here would be to stop
        # having that movie optimize itself for rapid playing....
        movie = self.o.assy.current_movie
        if movie:
            movie._close()
            # note: this assumes this is the only movie which might be "open",
            # and that redundant _close is ok.

        _superclass.command_will_exit(self)
        return

    def _offer_to_rewind_if_necessary(self): #bruce 080806 split this out
        # TODO: add an option to the PM to always say yes or no to this,
        # e.g. a 3-choice combobox for what to do if not rewound on exit
        # (rewind, don't rewind, or ask). [bruce 080806 suggestion]
        movie = self.o.assy.current_movie
        if movie and movie.currentFrame != 0:
            # note: if the movie file stores absolute atom positions,
            # there is no need to call this. Currently, we only support
            # .dpb files, which don't store them.
            # note: if we entered this on a nonzero frame, it might
            # be more useful to compare to and offer to rewind to
            # that frame (perhaps in addition to frame 0).
            # [bruce 080827 comments]
            mrd = _MovieRewindDialog(movie)
                # rewind (by calling movie._reset()), if user desires it
                # (see text and comments in that class)
            mrd.exec_()
        return


    # see also command_will_exit, elsewhere in this file

    def command_enter_PM(self):
        """
        Extends superclass method.
        @see: baseCommand.command_enter_PM()  for documentation
        """

        _superclass.command_enter_PM(self) #bruce 080909 call this instead of inlining it

        #@WARNING: The following code in command_enter_PM was originally in
        #def init_gui method. Its copied 'as is' from there.-- Ninad 2008-08-21

        self.enableMovieControls(False)
            #bruce 050428 precaution (has no noticable effect but seems safer in theory)
        #bruce 050428, working on bug 395: I think some undesirable state is left in the dashboard, so let's reinit it
        # (and down below we might like to init it from the movie if possible, but it's not always possible).

        self.propMgr._moviePropMgr_reinit() ###e could pass frameno? is max frames avail yet in all playable movies? not sure.
        # [bruce 050426 comment: probably this should just be a call of an update method, also used during the mode ###e]
        movie = self.o.assy.current_movie # might be None, but might_be_playable() true implies it's not
        if self.might_be_playable(): #bruce 050426 added condition
            frameno = movie.currentFrame
        else:
            frameno = 0 #bruce 050426 guessed value

        self.propMgr.frameNumberSpinBox.setValue(frameno, blockSignals = True)
        self.propMgr.moviePlayActiveAction.setVisible(0)
        self.propMgr.moviePlayRevActiveAction.setVisible(0)

        if self.might_be_playable(): # We have a movie file ready.  It's showtime! [bruce 050426 changed .filename -> .might_be_playable()]
            movie.cueMovie(propMgr = self.propMgr) # Cue movie.
            self.enableMovieControls(True)
            self.propMgr.updateFrameInformation()
            if movie.filename: #k not sure this cond is needed or what to do if not true [bruce 050510]
                env.history.message( "Movie file ready to play: %s" % movie.filename) #bruce 050510 added this message
        else:
            self.enableMovieControls(False)
        return


    def command_enter_misc_actions(self):
        """
        Overrides superclass method.

        @see: baseCommand.command_enter_misc_actions()  for documentation
        """
        #@WARNING: The following code in  was originally in
        #def init_gui method. Its copied 'as is' from there.-- Ninad 2008-08-21

        self.w.simMoviePlayerAction.setChecked(1) # toggle on the Movie Player icon+

        # Disable some action items in the main window.
        # [bruce 050426 comment: I'm skeptical of disabling the ones marked #k
        #  and suggest for some others (especially "simulator") that they
        #  auto-exit the mode rather than be disabled,
        #  but I won't revise these for now.]
        self.w.disable_QActions_for_movieMode(True)
            #  Actions marked #k are now in disable_QActions_for_movieMode(). mark 060314)

        # Disable Undo/Redo actions, and undo checkpoints, during this mode (they *must* be reenabled in restore_gui).
        # We do this last, so as not to do it if there are exceptions in the rest of the method,
        # since if it's done and never undone, Undo/Redo won't work for the rest of the session.
        # [bruce 060414; same thing done in some other modes]

        undo_manager.disable_undo_checkpoints('Movie Player')
        undo_manager.disable_UndoRedo('Movie Player', "in Movie Player") # optimizing this for shortness in menu text
            # this makes Undo menu commands and tooltips look like "Undo (not permitted in Movie Player)" (and similarly for Redo)


    def command_exit_misc_actions(self):
        """
        Overrides superclass method.

        @see: baseCommand.command_exit_misc_actions()  for documentation
        """
        #@WARNING: The following code in  was originally in
        #def restore_gui method. Its copied 'as is' from there.-- Ninad 2008-08-21

        # Reenable Undo/Redo actions, and undo checkpoints
        #(disabled in command_entered);
        # do it first to protect it from exceptions in the rest of this method
        # (since if it never happens, Undo/Redo won't work for the rest of the session)
        # [bruce 060414; same thing done in some other modes]
        undo_manager.reenable_undo_checkpoints('Movie Player')
        undo_manager.reenable_UndoRedo('Movie Player')

        self.w.simMoviePlayerAction.setChecked(0) # toggle on the Movie Player icon
        self.set_cmdname('Movie Player') # this covers all changes while we were in the mode
            # (somewhat of a kluge, and whether this is the best place to do it is unknown;
            #  without this the cmdname is "Done")
        self.w.disable_QActions_for_movieMode(False)


    #END new command API methods =============================================

    def enableMovieControls(self, enabled = True):
        self.propMgr.enableMovieControls(enabled)

    def might_be_playable(self):
        """
        Do we have a current movie which is worth trying to play?
        """
        movie = self.o.assy.current_movie
        return movie and movie.might_be_playable()

    def makeMenus(self):
        self.Menu_spec = [
            ('Cancel', self.command_Cancel),
            ('Reset Movie', self.ResetMovie),
            ('Done', self.command_Done),
            None,
            ('Edit Color Scheme...', self.w.colorSchemeCommand),
        ]

    def ResetMovie(self):
        #bruce 050325 revised or made this, since .current_movie can change
        if self.o.assy.current_movie:
            self.o.assy.current_movie._reset()

    def Draw_model(self):
        _superclass.Draw_model(self)
        self.o.assy.draw(self.o)

    # mouse and key events

    def keyPress(self, key):

        # Disable delete key
        if key == Qt.Key_Delete:
            return

        movie = self.o.assy.current_movie
        if not movie:
            return

        # Left or Down arrow keys - advance back one frame
        if key == Qt.Key_Left or key == Qt.Key_Down:
            movie._playToFrame(movie.currentFrame - 1)

        # Right or Up arrow keys - advance forward one frame
        if key == Qt.Key_Right or key == Qt.Key_Up:
            movie._playToFrame(movie.currentFrame + 1)

        _superclass.keyPress(self, key) # So F1 Help key works. mark 060321

        return

    def update_cursor_for_no_MB(self): # Fixes bug 1693. mark 060321
        """
        Update the cursor for 'Movie Player' mode.
        """
        self.o.setCursor(QCursor(Qt.ArrowCursor))

    pass

# ==

def simMoviePlayer(assy):
    """
    Plays a DPB movie file created by the simulator,
    either the current movie if any, or a previously saved
    dpb file with the same name as the current part, if one can be found.
    """
    from simulation.movie import find_saved_movie, Movie #bruce 050329 precaution (in case of similar bug to bug 499)
    win = assy.w
    if not assy.molecules: # No model, so no movie could be valid for current part.
        # bruce 050327 comment: even so, a movie file might be valid for some other Part...
        # not yet considered here. [050427 addendum: note that user can't yet autoload a new Part
        # just by opening a movie file, so there's no point in going into the mode -- it's only meant
        # for playing a movie for the *current contents of the current part*, for now.]
        env.history.message(redmsg("Movie Player: Need a model."))
        win.simMoviePlayerAction.setChecked(0) # toggle on the Movie Player icon ninad 061113
        return

    if assy.current_movie and assy.current_movie.might_be_playable():
        win.commandSequencer.userEnterCommand('MOVIE', always_update = True)
        return

    # no valid current movie, look for saved one with same name as assy
    ## env.history.message("Plot Tool: No simulation has been run yet.")
    if assy.filename:
        if assy.part is not assy.tree.part:
            msg = "Movie Player: Warning: Looking for saved movie for main part, not for displayed clipboard item."
            env.history.message(orangemsg(msg))
        errorcode, partdir = assy.find_or_make_part_files_directory()
        if not errorcode: # filename could be an MMP or PDB file.
            dir, fil = os.path.split(assy.filename)
            fil, ext = os.path.splitext(fil)
            mfile = os.path.join(partdir, fil + '.dpb')
        else:
            mfile = os.path.splitext(assy.filename)[0] + ".dpb"
        movie = find_saved_movie( assy, mfile)
            # checks existence -- should also check validity for current part or main part, but doesn't yet ###e
            # (neither did the pre-030527 code for this function, unless that's done in moviePlay, which it might be)
        if movie:
            # play this movie, and make it the current movie.
            assy.current_movie = movie
            #e should we switch to the part for which this movie was made? [might be done in moviePlay; if not:]
            # No current way to tell how to do that, and this might be done even if it's not valid
            # for any loaded Part. So let's not... tho we might presume (from filename choice we used)
            # it was valid for Main Part. Maybe print warning for clip item, and for not valid? #e
            env.history.message("Movie Player: %s previously saved movie for this part." % ("playing" or "loading"))
            win.commandSequencer.userEnterCommand('MOVIE', always_update = True)
            return
    # else if no assy.filename or no movie found from that:
    # bruce 050327 comment -- do what the old code did, except for the moviePlay
    # which seems wrong and tracebacks now.
    assy.current_movie = Movie(assy)
        # temporary kluge until bugs in movieMode for no assy.current_movie are fixed
    win.commandSequencer.userEnterCommand('MOVIE', always_update = True)
    return

# end