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 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 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