Source code for schrodinger.trajectory.trajectory_gui_dir.export_movie

import os
from enum import Enum

from schrodinger.Qt import QtGui
from schrodinger.Qt import QtWidgets
from schrodinger.ui import maestro_ui
from schrodinger.ui.qt import filedialog
from schrodinger.ui.qt import utils

from . import export_movie_ui
from . import stylesheet


[docs]class TrajectoryMovieDoubleValidator(QtGui.QDoubleValidator): """ Override base class because QtGui.QDoubleValidator returns Intermediate if input contains a double that is outside of the range or in the wrong format. """
[docs] def validate(self, in_str, pos): """ Provides a range checking of floating point numbers. :type in_str: str :param in_str: Input string entered by user. :type pos: int :param pos: Cursor position in the input editor. :rtype: enum(QtGui.QValidator.State) :return: State defined by QtGui.QValidator.State enum. """ state, in_str, pos = super(TrajectoryMovieDoubleValidator, self).validate(in_str, pos) if state != QtGui.QValidator.Intermediate: return (state, in_str, pos) if in_str in (".", "0", "0.", "0.0"): return (state, in_str, pos) decimal_pt = self.decimals() decimal_pos = in_str.find('.') if decimal_pos >= 0 and (len(in_str[decimal_pos + 1:len(in_str)]) > decimal_pt): return (QtGui.QValidator.Invalid, in_str, pos) try: value = float(in_str) except ValueError: return (state, in_str, pos) if self.bottom() <= value <= self.top(): return (QtGui.QValidator.Acceptable, in_str, pos) else: return (QtGui.QValidator.Invalid, in_str, pos)
[docs]class Quality(Enum): """ Enums for quality options. """ LOW = maestro_ui.MovieData.LOW_QUALITY MEDIUM = maestro_ui.MovieData.MEDIUM_QUALITY HIGH = maestro_ui.MovieData.HIGH_QUALITY
[docs]class Resolution(Enum): """ Enums for resolution options. """ R_352X288 = maestro_ui.MovieData.LOW_RESOLUTION R_720X480 = maestro_ui.MovieData.MEDIUM_RESOLUTION R_1280X1024 = maestro_ui.MovieData.HIGH_RESOLUTION
[docs]class DurationType(Enum): """ Enums for movie options. """ PER_FRAME = maestro_ui.MovieData.PER_FRAME_DURATION FULL_MOVIE = maestro_ui.MovieData.FULL_MOVIE_DURATION
[docs]class ExportMovieDialog(QtWidgets.QDialog): """ Export Movie dialog class for exporting movies from trajectory viewer gui. """ PER_FRAME_MIN_VALUE = 0.04 PER_FRAME_MAX_VALUE = 5.00 PER_FRAME_ALLOWED_DECIMALS = 2 FULL_MOVIE_MIN_VALUE = 1 FULL_MOVIE_MAX_VALUE = 300
[docs] def __init__(self, parent=None): """ Construct export movie dialog. :type parent: QtWidgets.QWidget :param parent: Parent widget. """ super(QtWidgets.QDialog, self).__init__(parent) self.ui = export_movie_ui.Ui_Dialog() self.ui.setupUi(self) self.setStyleSheet(stylesheet.EXPORT_MOVIE_DIALOG_STYLESHEET) self.ui.cancel_button.clicked.connect(self.reject) self.ui.export_button.clicked.connect(self.accept) self._orig_start_frame = None self._orig_end_frame = None self._step_size = None self._total_frame = None self._last_start_frame = None self._last_end_frame = None self._last_dir = None self._start_range_validator = QtGui.QIntValidator(self) self._end_range_validator = QtGui.QIntValidator(self) self.ui.start_range_line_edit.setValidator(self._start_range_validator) self.ui.end_range_line_edit.setValidator(self._end_range_validator) self.ui.start_range_line_edit.editingFinished.connect( self.startRangeModified) self.ui.end_range_line_edit.editingFinished.connect( self.endRangeModified) self.ui.start_range_line_edit.textChanged.connect( self.startRangeModified) self.ui.end_range_line_edit.textChanged.connect(self.endRangeModified) for w in (self.ui.start_range_line_edit, self.ui.end_range_line_edit): w.lostFocus.connect(self.updateExportButton) w.lostFocus.connect(self.updateRangeLineEditStyle) self.ui.limit_checkbox.stateChanged.connect(self.updateLimitFrame) self.ui.reset_button.clicked.connect(self.resetLimitRange) # Insert quality option items. self.ui.quality_options.insertItems( Quality.LOW.value, [quality.name.title() for quality in Quality]) self.ui.quality_options.currentIndexChanged.connect( self.updateRecommendationLabel) self.ui.quality_options.setCurrentIndex(Quality.MEDIUM.value) # Insert duration_option items. self.ui.duration_options.insertItems(DurationType.PER_FRAME.value, [ movie_type.name.title().replace("_", " ") for movie_type in DurationType ]) self.duration_full_movie_validator = QtGui.QIntValidator( self.FULL_MOVIE_MIN_VALUE, self.FULL_MOVIE_MAX_VALUE, self) self.duration_per_frame_validator = TrajectoryMovieDoubleValidator( self.PER_FRAME_MIN_VALUE, self.PER_FRAME_MAX_VALUE, self.PER_FRAME_ALLOWED_DECIMALS, self) movie_type = DurationType.PER_FRAME self.ui.duration_options.setCurrentIndex(movie_type.value) self.setDurationValidator(movie_type) self.setDurationTextEditBox(self.duration_per_frame_validator.bottom()) self.ui.duration_options.currentIndexChanged.connect( self.durationOptionChanged) #Insert movie duration option items. self.ui.resolution_options.insertItems(Resolution.R_352X288.value, [ resolution.name.title().replace("R_", "").replace("X", " X ") for resolution in Resolution ]) self.ui.resolution_options.setCurrentIndex(Resolution.R_720X480.value) # Duration line edit self.ui.duration_line_edit.editingFinished.connect(self.durationEdited) self.ui.duration_line_edit.textChanged.connect(self.durationEdited) self.ui.buttonGroup.buttonClicked.connect(self.frameOptionChanged) self.ui.help_button.clicked.connect(self.showHelp) self.adjustSize()
[docs] def showHelp(self): """ Shows 'Export Movie' dialog help """ utils.help_dialog("TRAJECTORY_EXPORT_MOVIE", parent=self)
[docs] def setDurationTextEditBox(self, value): """ Set given value in the duration text edit box. """ if self.ui.duration_options.currentIndex( ) == DurationType.PER_FRAME.value: self.ui.duration_line_edit.setText("{0:.2f}".format(value)) else: self.ui.duration_line_edit.setText("{0:d}".format(int(value)))
[docs] def setDurationValidator(self, duration_type): """ Set duration text edit box validator based on duration type. :type duration_type: enum(DurationType) :param duration_type: One of the value specified in the DurationType enum. """ if duration_type == DurationType.PER_FRAME: self.ui.duration_line_edit.setValidator( self.duration_per_frame_validator) else: self.ui.duration_line_edit.setValidator( self.duration_full_movie_validator)
[docs] def frameOptionChanged(self, state): """ Slot which gets called whenever user changes frame option. It updates movie total time label and edit box. """ self.updateMovieDurationTime()
[docs] def durationEdited(self): """ Slot which gets called whenever use edits duration in the duration text edit box. :type: text: str :param: text: Text value entered in the text edit box. """ self.updateMovieDurationTime()
[docs] def startRangeModified(self): """ This signal is emitted whenever start frame edit box value changes. If the value differs from the default, Reset button is displayed. Frames label is updated accordingly. """ start_frame = self.getStartFrameValue() checked = self.ui.limit_checkbox.isChecked() if checked and start_frame != self._orig_start_frame and start_frame > 0: self._last_start_frame = start_frame self.updateGUI()
[docs] def endRangeModified(self): """ This signal is emitted whenever end frame edit box value changes. If the value differs from the default, Reset button is displayed. Frames label is updated accordingly. """ end_frame = self.getEndFrameValue() checked = self.ui.limit_checkbox.isChecked() if checked and end_frame != self._orig_end_frame and end_frame > 0: self._last_end_frame = end_frame self.updateGUI()
[docs] def updateGUI(self): """ Update all relevant gui component of dialog. """ self.updateFramesOptionAndResetButton() self.updateMovieDurationTime() self.updateExportButton() self.updateRangeLineEditStyle()
[docs] def updateFramesOptionAndResetButton(self): """ Update frames label based on start, end and step size and update Reset button visibility. """ start_frame = self.getStartFrameValue() end_frame = self.getEndFrameValue() total_frames = self.getFrameTotal(1) frame_count_by_step = self.getFrameTotal(self._step_size) self.ui.one_per_step_label.setText(f"({frame_count_by_step})") self.ui.all_range_label.setText(f"({total_frames})") checked = self.ui.limit_checkbox.isChecked() self.ui.reset_button.setVisible(checked and self.isRangeModifiedByUser())
[docs] def updateLimitFrame(self): """ Update limit frames gui components based on start, end, and total frames. """ checked = self.ui.limit_checkbox.isChecked() start_frame = (self._last_start_frame if self._last_start_frame else self._orig_start_frame) end_frame = (self._last_end_frame if self._last_end_frame else self._orig_end_frame) valid_range = self.isRangeModifiedByUser() and end_frame > start_frame show_reset_button = False if checked: if self._last_start_frame and valid_range: start = self._last_start_frame show_reset_button = True else: start = self._orig_start_frame if self._last_end_frame and valid_range: end = self._last_end_frame show_reset_button = True else: end = self._orig_end_frame else: start = 1 end = self._total_frame self.setStartFrameValue(start) self.setEndFrameValue(end) self.updateStartAndEndFrames(checked) self.updateMovieDurationTime() self.ui.reset_button.setVisible(show_reset_button)
[docs] def updateStartAndEndFrames(self, enable): """ Enable or disable start and end range text boxes. :type enable: bool :param enable: Enable start and end frame label and tex box. """ for obj in (self.ui.start_range_line_edit, self.ui.start_range_label, self.ui.end_range_line_edit, self.ui.end_range_label): obj.setEnabled(enable)
[docs] def showDlg(self, start_frame, end_frame, step_size, total_frame): """ Update dialog gui components based on given frame values. :type start_frame: int :param start_frame: Default start frame. :type end_frame: int :param end_frame: Default end frame. :type step_size: int :param step_size: Default step size :type total_frame: int :param total_frame: Total frames :rtype: str :return: Return a path to a file to be used to export movie """ old_resolution = self.ui.resolution_options.currentIndex() old_quality = self.ui.quality_options.currentIndex() old_duration = self.ui.duration_options.currentIndex() old_duration_time = self.getDuration() self._orig_start_frame = start_frame self._orig_end_frame = end_frame self._step_size = step_size self._total_frame = total_frame self._start_range_validator.setRange(1, self._total_frame - 1) self._end_range_validator.setRange(2, self._total_frame) self.resetLimitRange() self.ui.one_per_step_label.setEnabled(self._step_size > 1) self.ui.one_per_step_radio.setEnabled(self._step_size > 1) if self._step_size == 1: self.ui.all_in_range_radio.setChecked(True) else: self.ui.one_per_step_radio.setChecked(True) self.updateGUI() # Show dialog if self.exec(): file_path = filedialog.get_save_file_name( self, dir=self._last_dir, caption="Export Movie File", filter='MPEG (*.mpeg)') if file_path: self._last_dir = os.path.dirname(file_path) return file_path else: # Restore old values if user cancels operation. self.ui.resolution_options.setCurrentIndex(old_resolution) self.ui.quality_options.setCurrentIndex(old_quality) self.setDurationValidator(DurationType(old_duration)) self.setDurationTextEditBox(old_duration_time) self.ui.duration_options.setCurrentIndex(old_duration)
[docs] def getStartFrameValue(self): """ Return start frame value from line edit text box. :rtype: int :return: Start frame value. """ return self._getIntFromLabelText(self.ui.start_range_line_edit.text())
[docs] def getEndFrameValue(self): """ Return end frame value from line edit text box. :rtype: int :return: End frame value. """ return self._getIntFromLabelText(self.ui.end_range_line_edit.text())
[docs] def setStartFrameValue(self, start_frame): """ Set start frame value in the start line edit text box. :type start_frame: int :param start_frame: Frame value to be set in the start range field. """ self.ui.start_range_line_edit.setText(str(start_frame))
[docs] def setEndFrameValue(self, end_frame): """ Set end frame value in the end line edit text box. :type end_frame: int :param end_frame: Frame value to be set in the end range field. """ self.ui.end_range_line_edit.setText(str(end_frame))
[docs] def isRangeModifiedByUser(self): """ Check if start or end range were modified by user. rtype: bool return: True if range is modified by user in the session. """ start_frame = self._last_start_frame end_frame = self._last_end_frame return ((self._orig_start_frame != start_frame and start_frame is not None) or (self._orig_end_frame != end_frame and end_frame is not None))
[docs] def isFullRange(self, start_frame, end_frame): """ Return true if entire range is included. :type start_frame: int :param start_frame: Starting frame number in the range. :type end_frame: int :param end_frame: End frame number in the range. :rtype: bool :return: True if entire range is included, otherwise False. """ return start_frame == 1 and end_frame == self._total_frame
[docs] def resetLimitRange(self): """ Reset start and end frame values using default start and end. """ self._last_start_frame = None self._last_end_frame = None full_range = self.isFullRange(self._orig_start_frame, self._orig_end_frame) checked = not full_range self.ui.limit_checkbox.setChecked(checked) self.setStartFrameValue(self._orig_start_frame) self.setEndFrameValue(self._orig_end_frame) self.updateStartAndEndFrames(checked)
[docs] def updateRecommendationLabel(self, index): """ Update recommendation label based on current selected quality. :type index: int :param index: Selected option index in the quality option menu. """ recommendation = { Quality.HIGH.value: "(large file) ", Quality.LOW.value: "(small file) ", Quality.MEDIUM.value: "(recommended)" } self.ui.recommendation_label.setText(recommendation[index])
[docs] def durationOptionChanged(self, index): """ Slot which is called whenever duration option changes. Update movie total time label, movie duration text box and duration text box validator. :type index: int :param index: Selected option index int the duration option menu. """ total_movie_frames = self.getTotalMovieFrames() duration = self.getDuration() self.setDurationValidator(DurationType(index)) # If current movie option is per-frame, then it implies previous value # was full movie, so we have to calculate per frame time value and set # it. Similarly vice versa. if index == DurationType.PER_FRAME.value: per_frame_time = duration / total_movie_frames min_time = self.duration_per_frame_validator.bottom() max_time = self.duration_per_frame_validator.top() if per_frame_time < min_time: per_frame_time = min_time elif per_frame_time > max_time: per_frame_time = max_time self.setDurationTextEditBox(per_frame_time) else: total_movie_time = duration * total_movie_frames min_time = self.duration_full_movie_validator.bottom() max_time = self.duration_full_movie_validator.top() if total_movie_time < min_time: total_movie_time = min_time elif total_movie_time > max_time: total_movie_time = max_time self.setDurationTextEditBox(int(total_movie_time)) self.updateMovieDurationTime()
[docs] def getDuration(self): """ Return duration value set in the duration edit box. """ value = self.ui.duration_line_edit.text() try: return float(value) except ValueError: if self.ui.duration_options.currentIndex( ) == DurationType.PER_FRAME.value: return float(self.duration_per_frame_validator.bottom()) else: return float(self.duration_full_movie_validator.bottom())
[docs] def getTotalMovieFrames(self): """ Returns total movie frames based on user selection in the frames radio group box. """ if self.ui.one_per_step_radio.isChecked(): return self.getFrameTotal(self._step_size) else: return self.getFrameTotal(1)
[docs] def getFrameTotal(self, step_size): """ Return total frames in the range based on given step size. :type step_size: int :param step_size: Step to be used to calculate frame count. :rtype: int :return: Frame count in the given range based on given step size. """ start_frame = self.getStartFrameValue() end_frame = self.getEndFrameValue() return (end_frame - start_frame + 1) // step_size
[docs] def updateMovieDurationTime(self): """ Update movie total time label. Label is visible only if duration option is per frame. """ index = self.ui.duration_options.currentIndex() if index == DurationType.PER_FRAME.value: total_time = self.getDuration() * self.getTotalMovieFrames() self.ui.movie_total_label.setText( "Movie total: ~{0:.2f}".format(total_time)) self.ui.movie_total_label.setVisible( index == DurationType.PER_FRAME.value)
def _getIntFromLabelText(self, value): """ Return integer value from a given string value. :type value: str :param value: Value to be converted into integer :rtype: int :return: Integer converted from value. """ try: val = int(value) except ValueError: val = 0 return val
[docs] def updateRangeLineEditStyle(self): """ Update range edit boxes style sheet. """ value = "false" if self.isValidFrameRange() else "true" for le in (self.ui.start_range_line_edit, self.ui.end_range_line_edit): le.setProperty("invalid", value) utils.update_widget_style(le)
[docs] def updateExportButton(self): """ Enable/disable Export button. """ self.ui.export_button.setEnabled(self.isValidFrameRange())
[docs] def isValidFrameRange(self): """ Check if start and end range are specified in correctly. :rtype: bool :return: True if end_frame > start_frame """ start_frame = self.getStartFrameValue() end_frame = self.getEndFrameValue() return end_frame > start_frame and start_frame > 0 and end_frame > 0
[docs] def getResolution(self): """ Return resolution of image. :rtype: QtCore.QSize :return: Image resolution (w,h). """ curr_index = self.ui.resolution_options.currentIndex() return maestro_ui.mm_get_movie_resolution(curr_index)
[docs] def getMovieData(self): """ :rtype: maestro_ui.MovieData :return: Movie data according to user choice. """ duration_index = self.ui.duration_options.currentIndex() data = maestro_ui.MovieData(self.ui.resolution_options.currentIndex(), self.ui.quality_options.currentIndex(), duration_index, self.getTotalMovieFrames()) if duration_index == DurationType.PER_FRAME.value: data.setFrameDuration(self.getDuration()) else: data.setMovieDuration(int(self.getDuration())) return data