Source code for steno.player

# -*- coding: utf-8 -*-
"""
.. module:: player
   :platform: Unix, Windows
   :synopsis: Main frame of the application "Steno"

.. moduleauthor:: Anton Konyshev <anton.konyshev@gmail.com>

"""
# License: wxWidgets (wxWindows Library Licence) 3.1

import os.path as op
import threading

import wx

import pysrt

from player_gui import PlayerGUI
from player_settings import PlayerSettingsMixin
from player_content import PlayerContentMixin
from verificator import Verificator
from droptargets import FileDropTarget
from translator import Translator
from dialogs import PreferencesDialog
from dialogs import UserManualDialog
from defaults import Defaults
import events as ev
import ids


[docs]class Player(PlayerGUI, PlayerSettingsMixin, PlayerContentMixin): """Main window of the application. """
[docs] def __init__(self, app, pargs=None): """:class:`Player` implements graphical user interface of the application. :param app: Application instance :type app: :class:`wx.App` :param pargs: Command line arguments :type pargs: :class:`argparse.Namespace` """ self.app = app super(Player, self).__init__(None, -1, "") self.SetMinSize(wx.Size(*Defaults.PLAYER_MIN_SIZE)) self.word_translate.SetMinSize( wx.Size(*Defaults.WORD_TRANSLATE_MIN_SIZE)) self.subtitle_translate.SetMinSize( wx.Size(*Defaults.SUBTITLE_TRANSLATE_MIN_SIZE)) self._video_state = self._subtitles_state = False self._aspect_ratio = None self._filters = self.app.load_filters() self.enable_menu_actions(play=False, pause=False, hint=False, repeat=False, stop=False) self.playback_timer = wx.Timer(self, id=ids.PLAYBACK_TICK) filedroptarget = FileDropTarget(self) self.SetDropTarget(filedroptarget) self.Bind(ev.LOAD_VIDEO, self.load_video) self.Bind(ev.LOAD_SUBTITLES, self.load_subtitles) self.Bind(ev.CONTENT_LOADING_STATE, self.on_content_loading_state_change) self.Bind(wx.EVT_TIMER, self.on_playback_timer, id=ids.PLAYBACK_TICK) self.Bind(ev.FRAGMENT_COMPLETE, self.on_fragment_completion) self.Bind(ev.FRAGMENT_STARTED, self.on_fragment_starting) self.Bind(wx.EVT_SIZE, self.on_resize) self.Bind(wx.EVT_MOVE, self.on_move) self.Bind(ev.SUBTITLES_SHIFT, self.on_subtitles_shift) self.Bind(ev.TRANSLATE_REQUEST, self.on_translate_request) self.Bind(ev.TRANSLATION_RESULT, self.on_translation_result) self.Bind(ev.CONFIG_UPDATE, self.on_config_update) self.Bind(ev.TRANSLATE_ANSWER, self.on_translate_answer) self.video.SetMinSize(self._get_video_slot_size()) if pargs: self._accept_args(pargs) wx.PostEvent(self, ev.ConfigUpdate(apply_all=True))
[docs] def on_config_update(self, event): """Stores user's configuration data and applies them to the graphical interface. :param event: Event which contains updated configuration values :type event: :class:`events.ConfigUpdate` """ params = getattr(event, 'params', {}) apply_updated = getattr(event, 'apply_updated', False) apply_all = getattr(event, 'apply_all', False) for name, value in params.iteritems(): if ( name is not None and value is not None and name in Defaults.SETTINGS.keys() ): try: if not isinstance(value, Defaults.SETTINGS.get(name)): value = Defaults.SETTINGS.get(name)(value) except (TypeError, ValueError): value = None if value is not None: self.app.set_setting(name, value) for group in set(par[1] for name, par in Defaults.SETTINGS.iteritems() if apply_updated and name in params.keys() or apply_all): getattr(self, u'apply_{0}_settings'.format(group))()
[docs] def on_preferences(self, event): """Creates and shows :class:`dialogs.PreferencesDialog` to edit the configuration parameters. :param event: Event entailed execution of the callback :type event: :class:`wx.CommandEvent` with type :const:`wx.EVT_MENU` """ dialog = PreferencesDialog(None, player=self) if dialog.ShowModal() == wx.ID_OK: wx.PostEvent( self, ev.ConfigUpdate(params=dialog.get_preferences(), apply_updated=True)) self._filters = dialog.get_filters() self.app.dump_filters(self._filters) dialog.Destroy()
[docs] def on_translate_request(self, event): """Creates and starts a thread in which the translation request will be performed. :param event: Event which contains details of the translation request :type event: :class:`events.TranslateRequest` """ src = getattr(event, 'src', None) if src: threading.Thread( target=self._translate_in_thread, args=(self.learning_language(), self.user_language(), src, getattr(event, u'details', False))).start()
[docs] def _translate_in_thread(self, src_lang, target_lang, text, details=False): """Performs the translation request in a thread. Language codes for `src_lang` and `target_lang` parameters can be found in :const:`defaults.Defaults.LANGUAGES`. :param src_lang: Source language of the text :type src_lang: unicode or str :param target_lang: Desired language for the text :type target_lang: unicode or str :param text: Original text for the translation :type text: unicode or str :param bool details: Verbosity of the translation result """ with self.translator.lock: result = getattr( self.translator, u'trans_{0}'.format( u'details' if details else u'sentence') )(src_lang, target_lang, text) if result: wx.CallAfter(wx.PostEvent, self, ev.TranslationResult(result=result, details=details))
[docs] def on_translation_result(self, event): """Handles the translation result. :param event: Event which contains a result of the translation :type event: :class:`events.TranslationResult` """ result = getattr(event, 'result', None) if result: if getattr(event, 'details', False): self.word_translate.SetValue(result) else: self.subtitle_translate.SetValue(result)
[docs] def on_playback_timer(self, event): """Supports actual values of widgets during playback. :param event: Event entailed execution of the callback :type event: :class:`wx.TimerEvent` """ if getattr(self, 'afterseek', False): self.afterseek = False self.video.Seek(self.video_slider.GetValue()) self.video.Play() else: self.video_slider.SetValue(self.video.Tell()) subtitle = self.find_subtitle() if subtitle and not self.is_learning(): self.show_subtitle(subtitle) if self.is_learning(): if self.verificator.set_subtitle(subtitle, replace=False): wx.PostEvent(self, ev.FragmentStarted()) wx.PostEvent(self, wx.PyCommandEvent(wx.EVT_TEXT.typeId, ids.ANSWER)) if self.verificator.is_passed(self.video.Tell() / 1000): if self.verificator.is_empty(): wx.PostEvent(self, ev.FragmentComplete()) else: wx.PostEvent(self, wx.PyCommandEvent(wx.EVT_BUTTON.typeId, ids.PAUSE)) wx.PostEvent(self, wx.PyCommandEvent(wx.EVT_TEXT.typeId, ids.ANSWER))
[docs] def on_repeat(self, event): """Rewinds a position in a video stream to the beginning of the current subtitle fragment. :param event: Event which entailed execution of the callback :type event: :class:`wx.ScrollEvent` """ if self.verificator.has_subtitle(): last = self.verificator.get_subtitle() self.video.Seek(int( (last.start.hours * 3600 + last.start.minutes * 60 + last.start.seconds) * 1000)) wx.PostEvent(self, wx.PyCommandEvent( wx.EVT_BUTTON.typeId, ids.PLAY))
[docs] def on_aspect_ratio(self, event): """Sets aspect ratio for video widget. :param event: Event which entailed execution of the callback :type event: :class:`wx.CommandEvent` with type :const:`wx.EVT_MENU` """ widget_id = event.GetId() aspect = None for width, height in Defaults.ASPECT_RATIO_LIST: if widget_id == getattr( ids, 'ASPECT_{0}x{1}'.format(width, height), None ): aspect = (width, height) self._aspect_ratio = aspect self.video.SetAspectRatio(self._aspect_ratio, slot_size=self._get_video_slot_size()) self.GetSizer().Layout()
[docs] def on_resize(self, event): """Handles frame resize event in order to save new sizes in configuration and to correctly resize the video widget. :param event: Event that entailed an execution of the callback :type event: :class:`wx.SizeEvent` """ event.Skip() self.video.SetAspectRatio(self._aspect_ratio, slot_size=self._get_video_slot_size()) self.GetSizer().Layout() self._after_resize_or_move()
[docs] def _after_resize_or_move(self): """Saves new position and sizes of the window after a short delay. Delay is necessary to write new values in configuration when user will stop their changing. Otherwise it would be required to save sizes and position coordinates many times while user resizes the frame. """ if hasattr(self, '_resize_params_save_held_call'): self._resize_params_save_held_call.Restart( Defaults.RESIZE_PARAMS_SAVE_DELAY * 1000) else: self._resize_params_save_held_call = wx.CallLater( Defaults.RESIZE_PARAMS_SAVE_DELAY * 1000, self._save_frame_size_and_position)
[docs] def on_move(self, event): """Handles frame move event in order to save new position coordinates in configuration. :param event: Event that entailed an execution of the callback :type event: :defaults:`wx.MoveEvent` """ event.Skip() self._after_resize_or_move()
[docs] def on_playback_control(self, event): """Controls the video playback. :param event: Event that entailed an execution of the callback :type event: :class:`wx.CommandEvent` with types :const:`wx.EVT_BUTTON` or :const:`wx.EVT_MENU` """ widget_id = event.GetId() if widget_id == wx.ID_STOP or widget_id == ids.PAUSE: if widget_id == ids.PAUSE: self.video.Pause() elif widget_id == wx.ID_STOP: self.video.Stop() self.video_slider.SetValue(0) if self.is_learning(): self.mode_choice.SetSelection(ids.PREVIEW) wx.PostEvent(self, wx.PyCommandEvent(wx.EVT_CHOICE.typeId, ids.MODE)) self.show_statistics_dialog() self.pause_button.Enable(False) self.play_button.Enable(True) self.enable_menu_actions(pause=False, play=True) if self.playback_timer.IsRunning(): self.playback_timer.Stop() elif widget_id == ids.PLAY: self.video.Play() self.pause_button.Enable(True) self.play_button.Enable(False) if not self.is_learning(): self.enable_menu_actions(pause=True, play=False) if not self.playback_timer.IsRunning(): self.video_slider.SetRange(0, self.video.Length()) self.playback_timer.Start(Defaults.PLAYBACK_TICK_INTERVAL)
[docs] def on_seek(self, event): """Rewinds the video stream position to. .. note:: Rewind procedure implemented in :meth:`Player.on_playback_timer`. :param event: Event that entailed an execution of the callback :type event: :class:`wx.ScrollEvent` """ self.video.Pause() self.afterseek = True
[docs] def on_hint(self, event): """Prints the hint requested by user in "learning" mode. :param event: Event that entailed an execution of the callback :type event: :class:`wx.CommandEvent` with types :const:`wx.EVT_BUTTON` or :const:`wx.EVT_MENU` """ if self.is_learning() and self.verificator.has_subtitle(): self.answer_edit.SetValue(self.verificator.hint( self.answer_edit.GetValue()))
[docs] def on_answer(self, event): """Checks the correctness of user's answer and its completeness. :param event: Event that entailed an execution of the callback :type event: :class:`wx.CommandEvent` with type :const:`wx.EVT_TEXT` """ if self.is_learning() and len(self.answer_edit.GetValue()): if self.verificator.has_subtitle(): self.edit_answer(self.verificator.verify_answer( self.answer_edit.GetValue())) if self.verificator.is_complete(self.answer_edit.GetValue()): wx.PostEvent(self, ev.FragmentComplete())
[docs] def on_translate_answer(self, event): """Generates a request for translation of an answer. :param event: Event contains the source text for translation :type event: :class:`events.TranslateAnswer` """ answer = getattr(event, 'answer', None) if answer: try: if answer[-1] == u' ': wx.PostEvent(self, ev.TranslateRequest(src=answer)) last_word = self.verificator.get_last_word() if last_word: wx.PostEvent(self, ev.TranslateRequest(src=last_word, details=True)) except IndexError: pass
[docs] def on_fragment_completion(self, event): """Clears the current subtitle and user's answer when current fragment was successfully complete. :param event: Event that entailed an execution of the callback :type event: :class:`events.FragmentComplete` """ self.progress_gauge.SetValue(self.verificator.fragment_length()) self.verificator.clear_subtitle(complete=True) self.answer_edit.ChangeValue(u'') wx.PostEvent(self, wx.PyCommandEvent( wx.EVT_BUTTON.typeId, ids.PLAY))
[docs] def on_fragment_starting(self, event): """Sets actual values for progress bar when new subtitle fragment starts. :param event: Event that entailed an execution of the callback :type event: :class:`events.FragmentStarted` """ if self.verificator.has_subtitle(): self.progress_gauge.SetRange(self.verificator.fragment_length()) self.progress_gauge.SetValue(0)
[docs] def on_close_all(self, event): """Closes video and subtitles files. :param event: Event that entailed an execution of the callback :type event: :class:`wx.CommandEvent` with type :const:`wx.EVT_MENU` """ wx.PostEvent(self, wx.PyCommandEvent(wx.EVT_BUTTON.typeId, wx.ID_STOP)) self.mode_choice.SetSelection(ids.PREVIEW) wx.PostEvent(self, wx.PyCommandEvent(wx.EVT_CHOICE.typeId, ids.MODE)) self.video.Close() self.video_slider.SetValue(0) self.progress_gauge.SetValue(0) del self.subtitles wx.PostEvent(self, ev.ContentLoadingState(video=False, subtitles=False))
[docs] def on_content_loading_state_change(self, event): """Handles loading/unloading of a content. :param event: Event that contains information about current availability of a content :type event: :class:`events.ContentLoadingState` """ video = getattr(event, 'video', None) subtitles = getattr(event, 'subtitles', None) if video is not None and video != self._video_state: self._video_state = video self.video_slider.SetRange(0, self.video.Length()) for widget in ('play_button', 'stop_button', 'video_slider'): getattr(self, widget).Enable(video) self.enable_menu_actions(play=video, stop=video) if subtitles is not None: self._subtitles_state = subtitles both = self._video_state and self._subtitles_state for widget in ('mode_choice', 'delay_spin', 'answer_edit'): getattr(self, widget).Enable(both) if both: self.translator = Translator() else: try: del self.translator except AttributeError: pass
[docs] def on_subtitles_shift(self, event): """Sets a shift value for subtitles. :param event: Event that contains new shift value :type event: :class:`events.SubtitlesShift` """ shift = getattr(event, 'shift', None) if shift: if getattr(self, '_applied_shift', None): self.subtitles.shift(milliseconds=(-1*self._applied_shift)) self._applied_shift = shift self.subtitles.shift(milliseconds=shift)
[docs] def on_content_open(self, event): """Provides an opportunity for user to choose content files for loading. :param event: Event that entailed an execution of the callback :type event: :class:`wx.CommandEvent` with type :const:`wx.EVT_MENU` """ widget_id = event.GetId() workdir = self.app.get_setting(u'working_directory', None) if not workdir: workdir = wx.StandardPaths.Get().GetDocumentsDir() dialog = wx.FileDialog( self, message=Defaults.VIDEO_OPEN_DIALOG_TITLE if widget_id == wx.ID_OPEN else Defaults.SUBTITLES_OPEN_DIALOG_TITLE, defaultDir=workdir, wildcard=Defaults.VIDEO_WILDCARD if widget_id == wx.ID_OPEN else Defaults.SUBTITLES_WILDCARD, style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) if dialog.ShowModal() == wx.ID_OK: path = dialog.GetPath() wx.PostEvent(self, ev.LoadVideo(filepath=path) if widget_id == wx.ID_OPEN else ev.LoadSubtitles(filepath=path)) newwd = op.split(path)[0] if op.isfile(path) else path if newwd != workdir: wx.PostEvent( self, ev.ConfigUpdate(params={u'working_directory': newwd}) ) dialog.Destroy()
[docs] def on_delay(self, event): """Handles editing of subtitles shift value. :param event: Event that entailed an execution of the callback :type event: :class:`wx.SpinEvent` """ if isinstance(getattr(self, 'subtitles', None), pysrt.srtfile.SubRipFile): shift = self.delay_spin.GetValue() wx.PostEvent(self, ev.SubtitlesShift(shift=shift)) wx.PostEvent(self, ev.ConfigUpdate(params={u'subtitles_shift': shift}))
[docs] def on_volume(self, event): """Applies new volume level. :param event: Event that entailed an execution of the callback :type event: :class:`wx.ScrollEvent` """ volume = event.GetPosition() self.video.SetVolume(volume / 100.0) ev.ConfigUpdate(params={u'volume': volume})
[docs] def on_mode(self, event): """Changes the application working mode. :param event: Event that entailed an execution of the callback :type event: :class:`wx.CommandEvent` with type :const:`wx.EVT_CHOICE` """ self._is_learning = None if self.is_learning(): wx.PostEvent(self, wx.PyCommandEvent(wx.EVT_BUTTON.typeId, ids.PLAY)) self.play_button.Hide() self.pause_button.Hide() self.repeat_button.Show() self.hint_button.Show() self.GetSizer().Layout() self.video_slider.Enable(False) self.enable_menu_actions(play=False, pause=False, repeat=True, hint=True) self.verificator = Verificator(self) if self.verificator.set_subtitle(self.find_subtitle(first=True)): wx.PostEvent(self, ev.FragmentStarted()) else: self.repeat_button.Hide() self.hint_button.Hide() self.play_button.Show() self.pause_button.Show() self.GetSizer().Layout() self.video_slider.Enable(True) self.enable_menu_actions(play=True, pause=True, repeat=False, hint=False) try: del self.verificator except AttributeError: pass self.answer_edit.ChangeValue(u'') self.word_translate.Clear() self.subtitle_translate.Clear()
[docs] def on_exit(self, event): """Prepares for an application termination. :param event: Event that entailed an execution of the callback :type event: :class:`wx.CommandEvent` with type :const:`wx.EVT_MENU` """ self.video.Close() for component in (u'translator', u'verificator', u'subtitles'): if getattr(self, component, None) is not None: delattr(self, component) self.Close()
[docs] def on_about(self, event): """Shows "About" dialog. :param event: Event that entailed an execution of the callback :type event: :class:`wx.CommandEvent` with type :const:`wx.EVT_MENU` """ info = wx.AboutDialogInfo() info.SetIcon(wx.Icon(self.app.get_img_path(Defaults.ABOUT_ICON), wx.BITMAP_TYPE_PNG)) for attr in (u'Name', u'Version', u'Description', u'License', u'WebSite', u'Copyright',): getattr(info, u'Set{0}'.format(attr))( getattr(Defaults, attr.upper(), u'')) for dev in Defaults.ABOUT_DEVELOPERS: info.AddDeveloper(dev) wx.AboutBox(info)
[docs] def on_user_manual(self, event): """Creates and shows "User Manual" dialog. :param event: Event that entailed an execution of the callback :type event: :class:`wx.CommandEvent` with type :const:`wx.EVT_MENU` """ dialog = UserManualDialog(self) dialog.ShowModal() dialog.Destroy()