class CommitDateMaxFilterEditor(QWidget, Ui_CommitDateMaxFilter): def __init__(self, parent=None): super().__init__(parent) self.setupUi(self) self._data_mapper = QDataWidgetMapper() def setModel(self, model): self._model = model self._data_mapper.setModel(model) self._data_mapper.addMapping(self.uiCommitDateMax, 2) def setSelection(self, current: QModelIndex) -> None: parent = current.parent() self._data_mapper.setRootIndex(parent) self._data_mapper.setCurrentModelIndex(current)
class CommitDateDeltaMinFilterEditor(QWidget, Ui_CommitDateDeltaMinFilter): def __init__(self, parent=None): super().__init__(parent) self.setupUi(self) self._data_mapper = QDataWidgetMapper() self.uiHelp.clicked.connect(_showTimeDurationHelp) def setModel(self, model): self._model = model self._data_mapper.setModel(model) self._data_mapper.addMapping(self.uiCommitDateDeltaMin, 2) def setSelection(self, current: QModelIndex) -> None: parent = current.parent() self._data_mapper.setRootIndex(parent) self._data_mapper.setCurrentModelIndex(current)
class AuthorFilterEditor(QWidget, Ui_AuthorFilterProperties): def __init__(self, parent=None): super().__init__(parent) self.setupUi(self) self._data_mapper = QDataWidgetMapper() def setModel(self, model): self._model = model self._data_mapper.setModel(model) self._data_mapper.addMapping(self.uiAuthorName, 2) self._data_mapper.addMapping(self.uiAuthorEmail, 3) def setSelection(self, current: QModelIndex) -> None: parent = current.parent() self._data_mapper.setRootIndex(parent) self._data_mapper.setCurrentModelIndex(current)
class NodeEditor(QWidget, Ui_FilterNodeProperties): def __init__(self, parent=None): super().__init__(parent) self.setupUi(self) self._data_mapper = QDataWidgetMapper() def setModel(self, model): self._model = model self._data_mapper.setModel(model) self._data_mapper.addMapping(self.uiName, 0) self._data_mapper.addMapping(self.uiComment, 1) def setSelection(self, current: QModelIndex) -> None: parent = current.parent() self._data_mapper.setRootIndex(parent) self._data_mapper.setCurrentModelIndex(current)
def setMappings(mappings): """Set the mappings between the model and widgets. TODO: - Should this be extended to accept other columns? - Check if the already has the model. """ column = 1 mappers = list() for widget, obj in mappings: mapper = QDataWidgetMapper(widget) # logger.debug(obj.model()) mapper.setModel(obj.model()) mapper.addMapping(widget, column) delegate = Delegate(widget) mapper.setItemDelegate(delegate) mapper.setRootIndex(obj.parent().index()) mapper.setCurrentModelIndex(obj.index()) # QDataWidgetMapper needs a focus event to notice a change in the data. # To make sure the model is informed about the change, I connected the # stateChanged signal of the QCheckBox to the submit slot of the # QDataWidgetMapper. The same idea goes for the QComboBox. # https://bugreports.qt.io/browse/QTBUG-1818 if isinstance(widget, QCheckBox): signal = widget.stateChanged try: signal.disconnect() except TypeError: pass signal.connect(mapper.submit) elif isinstance(widget, QComboBox): signal = widget.currentTextChanged try: signal.disconnect() except TypeError: pass signal.connect(mapper.submit) mappers.append(mapper) return mappers
class RecordFormView(QMainWindow): ui_file = '' # type: str model = None def __init__(self, model=None, ui_file: str = None) -> None: super().__init__() loadUi(ui_file or self.ui_file, self) self.model = model or self.model self.data_mapper = QDataWidgetMapper() self.data_mapper.setModel(self.model) for field in model.fields: if hasattr(self, field.name): self.data_mapper.addMapping(getattr(self, field.name), field.index) if hasattr(self, 'setup_ui'): self.setup_ui() def set_record_index(self, index: QModelIndex) -> None: self.data_mapper.setCurrentModelIndex(index)
class ArmorEditor(ArmorEditorWidgetBase, ArmorEditorWidget): def __init__(self, parent=None): super().__init__(parent) self.setupUi(self) self.model = None self.parts_tree_model = ArmorSetTreeModel() self.skill_model = SkillTranslationModel() self.armor_item_mapper = QDataWidgetMapper(self) self.armor_item_mapper.setItemDelegate(ItemDelegate()) self.armor_item_mapper.setModel(self.parts_tree_model) self.parts_tree_view.setModel(self.parts_tree_model) self.parts_tree_view.activated.connect( self.handle_parts_tree_activated) self.import_export_manager = ImportExportManager(self.parts_tree_view) self.import_export_manager.connect_custom_context_menu() for it in ("set_skill1_value", "set_skill2_value", "skill1_value", "skill2_value", "skill3_value"): getattr(self, it).setModel(self.skill_model) mappings = [ (self.id_value, Column.id, b"text"), (self.name_value, Column.gmd_name_index, b"text"), (self.description_value, Column.gmd_desc_index, b"text"), (self.setid_value, Column.set_id), (self.set_group_value, Column.set_group), (self.type_value, Column.type, b"currentIndex"), (self.order_value, Column.order), (self.variant_value, Column.variant, b"currentIndex"), (self.equip_slot_value, Column.equip_slot, b"currentIndex"), (self.gender_value, Column.gender, b"currentIndex"), (self.mdl_main_id_value, Column.mdl_main_id), (self.mdl_secondary_id_value, Column.mdl_secondary_id), (self.icon_color_value, Column.icon_color), (self.defense_value, Column.defense), (self.rarity_value, Column.rarity), (self.cost_value, Column.cost), (self.fire_res_value, Column.fire_res), (self.water_res_value, Column.water_res), (self.thunder_res_value, Column.thunder_res), (self.ice_res_value, Column.ice_res), (self.dragon_res_value, Column.dragon_res), (self.set_skill1_value, Column.set_skill1), (self.set_skill1_lvl_value, Column.set_skill1_lvl), (self.set_skill2_value, Column.set_skill2), (self.set_skill2_lvl_value, Column.set_skill2_lvl), (self.skill1_value, Column.skill1), (self.skill1_lvl_value, Column.skill1_lvl), (self.skill2_value, Column.skill2), (self.skill2_lvl_value, Column.skill2_lvl), (self.skill3_value, Column.skill3), (self.skill3_lvl_value, Column.skill3_lvl), (self.num_gem_slots, Column.num_gem_slots), (self.gem_slot1_lvl_value, Column.gem_slot1_lvl), (self.gem_slot2_lvl_value, Column.gem_slot2_lvl), (self.gem_slot3_lvl_value, Column.gem_slot3_lvl), ] for mapping in mappings: self.armor_item_mapper.addMapping(*mapping) def handle_parts_tree_activated(self, qindex: QModelIndex): if isinstance(qindex.internalPointer(), ArmorSetNode): return self.armor_item_mapper.setRootIndex(qindex.parent()) self.armor_item_mapper.setCurrentModelIndex(qindex) entry = qindex.internalPointer().ref self.crafting_requirements_editor.set_current(entry.id) def set_model(self, model): self.model = model if self.model is None: self.parts_tree_model = None self.parts_tree_view.setModel(None) return self.skill_model.update(model.get_relation_data("t9n_skill_pt")) self.crafting_requirements_editor.set_model(model, None) self.parts_tree_model.update(model) self.configure_tree_view() def configure_tree_view(self): header = self.parts_tree_view.header() header.setSectionResizeMode(0, QHeaderView.Stretch) header.setSectionResizeMode(1, QHeaderView.ResizeToContents) header.setStretchLastSection(False) for i in range(2, self.parts_tree_model.columnCount(None)): header.hideSection(i)
class WpDatGEditor(WeaponGunEditorWidgetBase, WeaponGunEditorWidget): def __init__(self, parent=None): super().__init__(parent) self.setupUi(self) self.model = None self.item_model = WpDatGTableModel(self) self.weapon_tree_view.setModel(self.item_model) self.shell_table_model = ShellTableModel() self.shell_table_view.setModel(self.shell_table_model) self.bottle_table_model = BottleTableModel(self) self.mapper = QDataWidgetMapper(self) self.mapper.setItemDelegate(ItemDelegate()) self.mapper.setModel(self.item_model) self.bottle_mapper = QDataWidgetMapper(self) self.bottle_mapper.setItemDelegate(ItemDelegate()) self.bottle_mapper.setModel(self.bottle_table_model) self.weapon_tree_view.activated.connect( self.handle_weapon_tree_view_activated) self.skill_model = SkillTranslationModel() self.skill_id_value.setModel(self.skill_model) self.import_export_manager = ImportExportManager( self.weapon_tree_view, WpDatGPlugin.import_export.get("safe_attrs")) self.import_export_manager.connect_custom_context_menu() mappings = [ (self.id_value, WpDatGEntry.id.index, b"text"), (self.name_value, WpDatGEntry.gmd_name_index.index, b"text"), (self.description_value, WpDatGEntry.gmd_description_index.index, b"text"), (self.order_value, WpDatGEntry.order.index), (self.tree_id_value, WpDatGEntry.tree_id.index), (self.tree_position_value, WpDatGEntry.tree_position.index), (self.is_fixed_upgrade_value, WpDatGEntry.is_fixed_upgrade.index, b"checked"), (self.base_model_id_value, WpDatGEntry.base_model_id.index), (self.part1_id_value, WpDatGEntry.part1_id.index), (self.part2_id_value, WpDatGEntry.part2_id.index), (self.color_value, WpDatGEntry.color.index), (self.muzzle_type_value, WpDatGEntry.muzzle_type.index, b"currentIndex"), (self.barrel_type_value, WpDatGEntry.barrel_type.index, b"currentIndex"), (self.magazine_type_value, WpDatGEntry.magazine_type.index, b"currentIndex"), (self.scope_type_value, WpDatGEntry.scope_type.index, b"currentIndex"), (self.rarity_value, WpDatGEntry.rarity.index), (self.cost_value, WpDatGEntry.crafting_cost.index), (self.raw_damage_value, WpDatGEntry.raw_damage.index), (self.affinity_value, WpDatGEntry.affinity.index), (self.defense_value, WpDatGEntry.defense.index), (self.deviation_value, WpDatGEntry.deviation.index, b"currentIndex"), (self.special_ammo_type_value, WpDatGEntry.special_ammo_type.index, b"currentIndex"), (self.element_id_value, WpDatGEntry.element_id.index, b"currentIndex"), (self.element_damage_value, WpDatGEntry.element_damage.index), (self.hidden_element_id_value, WpDatGEntry.hidden_element_id.index, b"currentIndex"), (self.hidden_element_damage_value, WpDatGEntry.hidden_element_damage.index), (self.elderseal_value, WpDatGEntry.elderseal.index, b"currentIndex"), (self.num_gem_slots, WpDatGEntry.num_gem_slots.index), (self.gem_slot1_lvl_value, WpDatGEntry.gem_slot1_lvl.index), (self.gem_slot2_lvl_value, WpDatGEntry.gem_slot2_lvl.index), (self.gem_slot3_lvl_value, WpDatGEntry.gem_slot3_lvl.index), (self.skill_id_value, WpDatGEntry.skill_id.index), ] for mapping in mappings: self.mapper.addMapping(*mapping) mappings = [ (self.close_range_value, BbtblEntry.close_range.index, b"currentIndex"), (self.power_value, BbtblEntry.power.index, b"currentIndex"), (self.paralysis_value, BbtblEntry.paralysis.index, b"currentIndex"), (self.poison_value, BbtblEntry.poison.index, b"currentIndex"), (self.sleep_value, BbtblEntry.sleep.index, b"currentIndex"), (self.blast_value, BbtblEntry.blast.index, b"currentIndex"), ] for mapping in mappings: self.bottle_mapper.addMapping(*mapping) @property def is_bow_type(self): return self.model.attrs.get("equip_type") == WeaponType.Bow def handle_weapon_tree_view_activated(self, qindex: QModelIndex): self.mapper.setCurrentModelIndex(qindex) entry = self.item_model.entries[qindex.row()] self.tabs_weapon_details.setTabText( 0, self.item_model.data(qindex, Qt.DisplayRole)) self.crafting_requirements_editor.set_current(entry.id) if self.is_bow_type: index = self.bottle_table_model.index(entry.special_ammo_type, 0) self.bottle_mapper.setCurrentModelIndex(index) else: index = self.shell_table_model.index(entry.shell_table_id, 0, QModelIndex()) self.shell_table_view.setRootIndex(index) self.shell_table_view.expandAll() def set_model(self, model): self.model = model self.item_model.update(model) self.skill_model.update(model.get_relation_data("t9n_skill_pt")) self.crafting_requirements_editor\ .set_model(model, model.attrs.get("equip_type")) if self.is_bow_type: self.bottle_table_model.update(model) self.hide_tab(self.tab_shell_table) self.special_ammo_type_value.deleteLater() self.special_ammo_type_label.deleteLater() else: self.shell_table_model.update(model) self.hide_tab(self.tab_bottle_table) self.configure_tree_view() def configure_tree_view(self): for index, col in enumerate(self.item_model.fields): self.weapon_tree_view.hideColumn(index) self.weapon_tree_view.showColumn(WpDatGEntry.id.index) self.weapon_tree_view.showColumn(WpDatGEntry.gmd_name_index.index) header = self.weapon_tree_view.header() header.setSectionResizeMode(WpDatGEntry.gmd_name_index.index, QHeaderView.Stretch) header.setSectionResizeMode(WpDatGEntry.id.index, QHeaderView.ResizeToContents) def hide_tab(self, widget): index = self.tabs_weapon_details.indexOf(widget) self.tabs_weapon_details.removeTab(index)
class ItmEditor(ItmEditorWidgetBase, ItmEditorWidget): def __init__(self, parent=None): super().__init__(parent) self.setupUi(self) self.model = None self.itm_model = ItmTableModel(self) self.mapper = QDataWidgetMapper(self) self.mapper.setModel(self.itm_model) self.item_browser.setModel(self.itm_model) self.item_browser.activated.connect(self.handle_item_browser_activated) self.mapper.addMapping(self.name_value, Column.name, b"text") self.mapper.addMapping(self.id_value, Column.id, b"text") self.mapper.addMapping(self.description_value, Column.description, b"text") self.mapper.addMapping(self.subtype_value, Column.sub_type, b"currentIndex") self.mapper.addMapping(self.type_value, Column.type, b"currentIndex") self.mapper.addMapping(self.rarity_value, Column.rarity) self.mapper.addMapping(self.carry_limit_value, Column.carry_limit) self.mapper.addMapping(self.sort_order_value, Column.order) self.mapper.addMapping(self.icon_id_value, Column.icon_id) self.mapper.addMapping(self.icon_color_value, Column.icon_color) self.mapper.addMapping(self.sell_price_value, Column.sell_price) self.mapper.addMapping(self.buy_price_value, Column.buy_price) self.add_flag_mapping(self.flag_is_default_item, Column.flag_is_default_item) self.add_flag_mapping(self.flag_is_quest_only, Column.flag_is_quest_only) self.add_flag_mapping(self.flag_unknown1, Column.flag_unknown1) self.add_flag_mapping(self.flag_is_consumable, Column.flag_is_consumable) self.add_flag_mapping(self.flag_is_appraisal, Column.flag_is_appraisal) self.add_flag_mapping(self.flag_unknown2, Column.flag_unknown2) self.add_flag_mapping(self.flag_is_mega, Column.flag_is_mega) self.add_flag_mapping(self.flag_is_level_one, Column.flag_is_level_one) self.add_flag_mapping(self.flag_is_level_two, Column.flag_is_level_two) self.add_flag_mapping(self.flag_is_level_three, Column.flag_is_level_three) self.add_flag_mapping(self.flag_is_glitter, Column.flag_is_glitter) self.add_flag_mapping(self.flag_is_deliverable, Column.flag_is_deliverable) self.add_flag_mapping(self.flag_is_not_shown, Column.flag_is_not_shown) def handle_item_browser_activated(self, qindex): source_qindex = qindex.model().mapToSource(qindex) self.mapper.setCurrentModelIndex(source_qindex) def add_flag_mapping(self, widget, flag_column): self.mapper.addMapping(widget, flag_column) widget.released.connect(self.mapper.submit) def set_model(self, model): self.model = model self.itm_model.update(model) if model is not None: header = self.item_browser.header() # header = self.item_browser.horizontalHeader() header.hideSection(Column.description) header.setSectionResizeMode(Column.name, QHeaderView.Stretch) header.setSectionResizeMode(Column.id, QHeaderView.Fixed) header.resizeSection(Column.id, 50) header.setStretchLastSection(False) for i in range(3, self.itm_model.columnCount(None)): header.hideSection(i) self.item_browser.sortByColumn(Column.id, Qt.AscendingOrder)
class WpDatEditor(WeaponEditorWidgetBase, WeaponEditorWidget): def __init__(self, parent=None): super().__init__(parent) self.setupUi(self) self.model = None self.skill_model = SkillTranslationModel() self.table_model = WpDatTableModel(self) self.weapon_tree_view.activated.connect( self.handle_weapon_tree_view_activated) self.kire_widget.set_model(KireGaugeModelEntryAdapter()) self.mapper = QDataWidgetMapper(self) self.mapper.setItemDelegate(ItemDelegate()) self.mapper.setModel(self.table_model) self.skill_id_value.setModel(self.skill_model) self.import_export_manager = ImportExportManager(self.weapon_tree_view) self.import_export_manager.connect_custom_context_menu() mappings = [ (self.id_value, WpDatEntry.id.index, b"text"), (self.name_value, WpDatEntry.gmd_name_index.index, b"text"), (self.description_value, WpDatEntry.gmd_description_index.index, b"text"), (self.order_value, WpDatEntry.order.index), (self.tree_id_value, WpDatEntry.tree_id.index), (self.tree_position_value, WpDatEntry.tree_position.index), (self.is_fixed_upgrade_value, WpDatEntry.is_fixed_upgrade.index, b"checked"), (self.base_model_id_value, WpDatEntry.base_model_id.index), (self.part1_id_value, WpDatEntry.part1_id.index), (self.part2_id_value, WpDatEntry.part2_id.index), (self.color_value, WpDatEntry.color.index), (self.rarity_value, WpDatEntry.rarity.index), (self.cost_value, WpDatEntry.crafting_cost.index), (self.raw_damage_value, WpDatEntry.raw_damage.index), (self.affinity_value, WpDatEntry.affinity.index), (self.defense_value, WpDatEntry.defense.index), (self.handicraft_value, WpDatEntry.handicraft.index, b"currentIndex"), (self.element_id_value, WpDatEntry.element_id.index, b"currentIndex"), (self.element_damage_value, WpDatEntry.element_damage.index), (self.hidden_element_id_value, WpDatEntry.hidden_element_id.index, b"currentIndex"), (self.hidden_element_damage_value, WpDatEntry.hidden_element_damage.index), (self.elderseal_value, WpDatEntry.elderseal.index, b"currentIndex"), (self.num_gem_slots, WpDatEntry.num_gem_slots.index), (self.gem_slot1_lvl_value, WpDatEntry.gem_slot1_lvl.index), (self.gem_slot2_lvl_value, WpDatEntry.gem_slot2_lvl.index), (self.gem_slot3_lvl_value, WpDatEntry.gem_slot3_lvl.index), (self.skill_id_value, WpDatEntry.skill_id.index), (self.wep1_id_value, WpDatEntry.wep1_id.index), (self.wep2_id_value, WpDatEntry.wep2_id.index), (self.kire_widget, WpDatEntry.kire_id.index), ] for mapping in mappings: self.mapper.addMapping(*mapping) def handle_weapon_tree_view_activated(self, qindex: QModelIndex): self.mapper.setCurrentModelIndex(qindex) entry = self.table_model.entries[qindex.row()] self.crafting_requirements_editor.set_current(entry.id) def set_model(self, model): self.model = model self.skill_model.update(model.get_relation_data("t9n_skill_pt")) self.table_model.update(self.model) self.weapon_tree_view.setModel(self.table_model) self.crafting_requirements_editor.set_model(model, self.get_equip_type()) self.configure_tree_view() def configure_tree_view(self): for index, col in enumerate(self.table_model.columns): self.weapon_tree_view.hideColumn(index) self.weapon_tree_view.showColumn(WpDatEntry.id.index) self.weapon_tree_view.showColumn(WpDatEntry.gmd_name_index.index) header = self.weapon_tree_view.header() header.setSectionResizeMode(WpDatEntry.gmd_name_index.index, QHeaderView.Stretch) header.setSectionResizeMode(WpDatEntry.id.index, QHeaderView.ResizeToContents) def get_equip_type(self): return self.model.attrs.get("equip_type")
class RecordFormView(QMainWindow): """ Record form view - main view for editing individual records Params - ui_file - path to designer file data_model - database model subviews - subviews to insert into this view there must be a QWidget placeholder named [subview_name]_placeholder to insert the subview into window_title - obvious window_icon - obvious Events - pre_save - fired before saving the record to database post_save - fired after saving the record to database """ ui_file = '' # type: str data_model = None subviews = {} window_title = None window_icon = None pre_save = pyqtSignal(QSqlRecord) post_save = pyqtSignal() def __init__(self, model=None, ui_file: str = None) -> None: super().__init__() loadUi(ui_file or self.ui_file, self) self.row = None self.record = None self.new_record = False self.parent_view = None self.sub_views = [] if self.window_title: self.setWindowTitle(self.window_title) if self.window_icon: self.setWindowIcon(QIcon(self.window_icon)) # allow passing a model class or instances model = model or self.model if inspect.isclass(model): self.data_model = model() else: self.data_model = model # update subviews on row change self.data_mapper = QDataWidgetMapper() self.data_mapper.setModel(self.data_model) self.data_mapper.currentIndexChanged.connect(self._update_subviews) # auto map fields to db columns for field in self.data_model.fields: if hasattr(self, field.name): self.data_mapper.addMapping(getattr(self, field.name), field.index) # setup subviews for view_name, view_cls in self.subviews.items(): placeholder_name = '{0}_placeholder'.format(view_name) try: placeholder = getattr(self, placeholder_name) except AttributeError: raise ImproperlyConfigured( 'Unable to find subview placeholder ' + placeholder_name) view = view_cls() self.sub_views.append(view) view.set_parent_view(self) layout = placeholder.layout() if not layout: layout = QVBoxLayout(placeholder) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(view) setattr(self, view_name, view) if hasattr(self, 'setup_ui'): self.setup_ui() def set_read_only(self, read_only): """ Set form as read only """ for field in self.data_model.fields: if hasattr(self, field.name): widget = getattr(self, field.name) widget.setDisabled(read_only) def set_parent_view(self, view): """ Used by subviews to designate their parent - not usually called by user """ self.parent_view = view def set_record_index(self, model_index: QModelIndex) -> None: """ Set active record by model index """ if not model_index: self.setDisabled(True) return self.row = model_index.row() self.index = model_index self.record = self.data_model.record(self.row) self.setEnabled(True) self.data_mapper.setCurrentModelIndex(model_index) def save_record(self) -> bool: """ Save active record """ index = self.data_mapper.currentIndex() record = self.data_mapper.model().record(index) self.pre_save.emit(record) saved = self.data_model.submitAll() if not saved: QMessageBox.critical( self, 'Save record', 'Unable to save record\n{0}'.format( self.data_model.lastError().text())) return False self.post_save.emit() return True # row change event handler def _update_subviews(self, index: int): record = self.data_model.record(index) id = record.value(self.data_model.id_field_name) # refresh subviews for view_name, view_cls in self.subviews.items(): view = getattr(self, view_name) model = view.data_model model.set_related_id(id) model.select()
class MainWindow(QMainWindow): """ The main window class """ def __init__(self, parent=None): QMainWindow.__init__(self, parent) self.ui = uic.loadUi("gui/main_window.ui") self.timestamp_filename = None self.video_filename = None self.media_start_time = None self.media_end_time = None self.restart_needed = False self.timer_period = 100 self.is_full_screen = False self.media_started_playing = False self.media_is_playing = False self.original_geometry = None self.mute = False self.timestamp_model = TimestampModel(None, self) self.proxy_model = QSortFilterProxyModel(self) self.ui.list_timestamp.setModel(self.timestamp_model) self.ui.list_timestamp.doubleClicked.connect( lambda event: self.ui.list_timestamp.indexAt(event.pos()).isValid() and self.run() ) self.timer = QTimer() self.timer.timeout.connect(self.update_ui) self.timer.timeout.connect(self.timer_handler) self.timer.start(self.timer_period) self.vlc_instance = vlc.Instance() self.media_player = self.vlc_instance.media_player_new() # if sys.platform == "darwin": # for MacOS # self.ui.frame_video = QMacCocoaViewContainer(0) self.ui.frame_video.doubleClicked.connect(self.toggle_full_screen) self.ui.frame_video.wheel.connect(self.wheel_handler) self.ui.frame_video.keyPressed.connect(self.key_handler) # Set up buttons self.ui.button_run.clicked.connect(self.run) self.ui.button_timestamp_browse.clicked.connect( self.browse_timestamp_handler ) self.ui.button_video_browse.clicked.connect( self.browse_video_handler ) self.play_pause_model = ToggleButtonModel(None, self) self.play_pause_model.setStateMap( { True: { "text": "", "icon": qta.icon("fa.play", scale_factor=0.7) }, False: { "text": "", "icon": qta.icon("fa.pause", scale_factor=0.7) } } ) self.ui.button_play_pause.setModel(self.play_pause_model) self.ui.button_play_pause.clicked.connect(self.play_pause) self.mute_model = ToggleButtonModel(None, self) self.mute_model.setStateMap( { True: { "text": "", "icon": qta.icon("fa.volume-up", scale_factor=0.8) }, False: { "text": "", "icon": qta.icon("fa.volume-off", scale_factor=0.8) } } ) self.ui.button_mute_toggle.setModel(self.mute_model) self.ui.button_mute_toggle.clicked.connect(self.toggle_mute) self.ui.button_full_screen.setIcon( qta.icon("ei.fullscreen", scale_factor=0.6) ) self.ui.button_full_screen.setText("") self.ui.button_full_screen.clicked.connect(self.toggle_full_screen) self.ui.button_speed_up.clicked.connect(self.speed_up_handler) self.ui.button_speed_up.setIcon( qta.icon("fa.arrow-circle-o-up", scale_factor=0.8) ) self.ui.button_speed_up.setText("") self.ui.button_slow_down.clicked.connect(self.slow_down_handler) self.ui.button_slow_down.setIcon( qta.icon("fa.arrow-circle-o-down", scale_factor=0.8) ) self.ui.button_slow_down.setText("") self.ui.button_mark_start.setIcon( qta.icon("fa.quote-left", scale_factor=0.7) ) self.ui.button_mark_start.setText("") self.ui.button_mark_end.setIcon( qta.icon("fa.quote-right", scale_factor=0.7) ) self.ui.button_mark_end.setText("") self.ui.button_add_entry.clicked.connect(self.add_entry) self.ui.button_remove_entry.clicked.connect(self.remove_entry) self.ui.button_mark_start.clicked.connect( lambda: self.set_mark(start_time=int( self.media_player.get_position() * self.media_player.get_media().get_duration())) ) self.ui.button_mark_end.clicked.connect( lambda: self.set_mark(end_time=int( self.media_player.get_position() * self.media_player.get_media().get_duration())) ) self.ui.slider_progress.setTracking(False) self.ui.slider_progress.valueChanged.connect(self.set_media_position) self.ui.slider_volume.valueChanged.connect(self.set_volume) self.ui.entry_description.setReadOnly(True) # Mapper between the table and the entry detail self.mapper = QDataWidgetMapper() self.mapper.setSubmitPolicy(QDataWidgetMapper.ManualSubmit) self.ui.button_save.clicked.connect(self.mapper.submit) # Set up default volume self.set_volume(self.ui.slider_volume.value()) self.vlc_events = self.media_player.event_manager() self.vlc_events.event_attach( vlc.EventType.MediaPlayerTimeChanged, self.media_time_change_handler ) # Let our application handle mouse and key input instead of VLC self.media_player.video_set_mouse_input(False) self.media_player.video_set_key_input(False) self.ui.show() def add_entry(self): if not self.timestamp_filename: self._show_error("You haven't chosen a timestamp file yet") row_num = self.timestamp_model.rowCount() self.timestamp_model.insertRow(row_num) start_cell = self.timestamp_model.index(row_num, 0) end_cell = self.timestamp_model.index(row_num, 1) self.timestamp_model.setData(start_cell, TimestampDelta.from_string("")) self.timestamp_model.setData(end_cell, TimestampDelta.from_string("")) def remove_entry(self): if not self.timestamp_filename: self._show_error("You haven't chosen a timestamp file yet") selected = self.ui.list_timestamp.selectionModel().selectedIndexes() if len(selected) == 0: return self.proxy_model.removeRow(selected[0].row()) and self.mapper.submit() def set_media_position(self, position): percentage = position / 10000.0 self.media_player.set_position(percentage) absolute_position = percentage * \ self.media_player.get_media().get_duration() if absolute_position > self.media_end_time: self.media_end_time = -1 def set_mark(self, start_time=None, end_time=None): if len(self.ui.list_timestamp.selectedIndexes()) == 0: blankRowIndex = self.timestamp_model.blankRowIndex() if not blankRowIndex.isValid(): self.add_entry() else: index = self.proxy_model.mapFromSource(blankRowIndex) self.ui.list_timestamp.selectRow(index.row()) selectedIndexes = self.ui.list_timestamp.selectedIndexes() if start_time: self.proxy_model.setData(selectedIndexes[0], TimestampDelta.string_from_int( start_time)) if end_time: self.proxy_model.setData(selectedIndexes[1], TimestampDelta.string_from_int( end_time)) def update_ui(self): self.ui.slider_progress.blockSignals(True) self.ui.slider_progress.setValue( self.media_player.get_position() * 10000 ) # When the video finishes self.ui.slider_progress.blockSignals(False) if self.media_started_playing and \ self.media_player.get_media().get_state() == vlc.State.Ended: self.play_pause_model.setState(True) # Apparently we need to reset the media, otherwise the player # won't play at all self.media_player.set_media(self.media_player.get_media()) self.set_volume(self.ui.slider_volume.value()) self.media_is_playing = False self.media_started_playing = False self.run() def timer_handler(self): """ This is a workaround, because for some reason we can't call set_time() inside the MediaPlayerTimeChanged handler (as the video just stops playing) """ if self.restart_needed: self.media_player.set_time(self.media_start_time) self.restart_needed = False def key_handler(self, event): if event.key() == Qt.Key_Escape and self.is_full_screen: self.toggle_full_screen() if event.key() == Qt.Key_F: self.toggle_full_screen() if event.key() == Qt.Key_Space: self.play_pause() def wheel_handler(self, event): self.modify_volume(1 if event.angleDelta().y() > 0 else -1) def toggle_mute(self): self.media_player.audio_set_mute(not self.media_player.audio_get_mute()) self.mute = not self.mute self.mute_model.setState(not self.mute) def modify_volume(self, delta_percent): new_volume = self.media_player.audio_get_volume() + delta_percent if new_volume < 0: new_volume = 0 elif new_volume > 40: new_volume = 40 self.media_player.audio_set_volume(new_volume) self.ui.slider_volume.setValue(self.media_player.audio_get_volume()) def set_volume(self, new_volume): self.media_player.audio_set_volume(new_volume) def speed_up_handler(self): self.modify_rate(0.1) def slow_down_handler(self): self.modify_rate(-0.1) def modify_rate(self, delta_percent): new_rate = self.media_player.get_rate() + delta_percent if new_rate < 0.2 or new_rate > 2.0: return self.media_player.set_rate(new_rate) def media_time_change_handler(self, _): if self.media_end_time == -1: return if self.media_player.get_time() > self.media_end_time: self.restart_needed = True def update_slider_highlight(self): if self.ui.list_timestamp.selectionModel().hasSelection(): selected_row = self.ui.list_timestamp.selectionModel(). \ selectedRows()[0] self.media_start_time = self.ui.list_timestamp.model().data( selected_row.model().index(selected_row.row(), 0), Qt.UserRole ) self.media_end_time = self.ui.list_timestamp.model().data( selected_row.model().index(selected_row.row(), 1), Qt.UserRole ) duration = self.media_player.get_media().get_duration() self.media_end_time = self.media_end_time \ if self.media_end_time != 0 else duration if self.media_start_time > self.media_end_time: raise ValueError("Start time cannot be later than end time") if self.media_start_time > duration: raise ValueError("Start time not within video duration") if self.media_end_time > duration: raise ValueError("End time not within video duration") slider_start_pos = (self.media_start_time / duration) * \ (self.ui.slider_progress.maximum() - self.ui.slider_progress.minimum()) slider_end_pos = (self.media_end_time / duration) * \ (self.ui.slider_progress.maximum() - self.ui.slider_progress.minimum()) self.ui.slider_progress.setHighlight( int(slider_start_pos), int(slider_end_pos) ) else: self.media_start_time = 0 self.media_end_time = -1 def run(self): """ Execute the loop """ if self.timestamp_filename is None: self._show_error("No timestamp file chosen") return if self.video_filename is None: self._show_error("No video file chosen") return try: self.update_slider_highlight() self.media_player.play() self.media_player.set_time(self.media_start_time) self.media_started_playing = True self.media_is_playing = True self.play_pause_model.setState(False) except Exception as ex: self._show_error(str(ex)) print(traceback.format_exc()) def play_pause(self): """Toggle play/pause status """ if not self.media_started_playing: self.run() return if self.media_is_playing: self.media_player.pause() else: self.media_player.play() self.media_is_playing = not self.media_is_playing self.play_pause_model.setState(not self.media_is_playing) def toggle_full_screen(self): if self.is_full_screen: # TODO Artifacts still happen some time when exiting full screen # in X11 self.ui.frame_media.showNormal() self.ui.frame_media.restoreGeometry(self.original_geometry) self.ui.frame_media.setParent(self.ui.widget_central) self.ui.layout_main.addWidget(self.ui.frame_media, 2, 3, 3, 1) # self.ui.frame_media.ensurePolished() else: self.ui.frame_media.setParent(None) self.ui.frame_media.setWindowFlags(Qt.FramelessWindowHint | Qt.CustomizeWindowHint) self.original_geometry = self.ui.frame_media.saveGeometry() desktop = QApplication.desktop() rect = desktop.screenGeometry(desktop.screenNumber(QCursor.pos())) self.ui.frame_media.setGeometry(rect) self.ui.frame_media.showFullScreen() self.ui.frame_media.show() self.ui.frame_video.setFocus() self.is_full_screen = not self.is_full_screen def browse_timestamp_handler(self): """ Handler when the timestamp browser button is clicked """ tmp_name, _ = QFileDialog.getOpenFileName( self, "Choose Timestamp file", None, "Timestamp File (*.tmsp);;All Files (*)" ) if not tmp_name: return self.set_timestamp_filename(QDir.toNativeSeparators(tmp_name)) def _sort_model(self): self.ui.list_timestamp.sortByColumn(0, Qt.AscendingOrder) def _select_blank_row(self, parent, start, end): self.ui.list_timestamp.selectRow(start) def set_timestamp_filename(self, filename): """ Set the timestamp file name """ if not os.path.isfile(filename): self._show_error("Cannot access timestamp file " + filename) return try: self.timestamp_model = TimestampModel(filename, self) self.timestamp_model.timeParseError.connect( lambda err: self._show_error(err) ) self.proxy_model.setSortRole(Qt.UserRole) self.proxy_model.dataChanged.connect(self._sort_model) self.proxy_model.dataChanged.connect(self.update_slider_highlight) self.proxy_model.setSourceModel(self.timestamp_model) self.proxy_model.rowsInserted.connect(self._sort_model) self.proxy_model.rowsInserted.connect(self._select_blank_row) self.ui.list_timestamp.setModel(self.proxy_model) self.timestamp_filename = filename self.ui.entry_timestamp.setText(self.timestamp_filename) self.mapper.setModel(self.proxy_model) self.mapper.addMapping(self.ui.entry_start_time, 0) self.mapper.addMapping(self.ui.entry_end_time, 1) self.mapper.addMapping(self.ui.entry_description, 2) self.ui.list_timestamp.selectionModel().selectionChanged.connect( self.timestamp_selection_changed) self._sort_model() directory = os.path.dirname(self.timestamp_filename) basename = os.path.basename(self.timestamp_filename) timestamp_name_without_ext = os.path.splitext(basename)[0] for file_in_dir in os.listdir(directory): current_filename = os.path.splitext(file_in_dir)[0] found_video = (current_filename == timestamp_name_without_ext and file_in_dir != basename) if found_video: found_video_file = os.path.join(directory, file_in_dir) self.set_video_filename(found_video_file) break except ValueError as err: self._show_error("Timestamp file is invalid") def timestamp_selection_changed(self, selected, deselected): if len(selected) > 0: self.mapper.setCurrentModelIndex(selected.indexes()[0]) self.ui.button_save.setEnabled(True) self.ui.button_remove_entry.setEnabled(True) self.ui.entry_start_time.setReadOnly(False) self.ui.entry_end_time.setReadOnly(False) self.ui.entry_description.setReadOnly(False) else: self.mapper.setCurrentModelIndex(QModelIndex()) self.ui.button_save.setEnabled(False) self.ui.button_remove_entry.setEnabled(False) self.ui.entry_start_time.clear() self.ui.entry_end_time.clear() self.ui.entry_description.clear() self.ui.entry_start_time.setReadOnly(True) self.ui.entry_end_time.setReadOnly(True) self.ui.entry_description.setReadOnly(True) def set_video_filename(self, filename): """ Set the video filename """ if not os.path.isfile(filename): self._show_error("Cannot access video file " + filename) return self.video_filename = filename media = self.vlc_instance.media_new(self.video_filename) media.parse() if not media.get_duration(): self._show_error("Cannot play this media file") self.media_player.set_media(None) self.video_filename = None else: self.media_player.set_media(media) if sys.platform.startswith('linux'): # for Linux using the X Server self.media_player.set_xwindow(self.ui.frame_video.winId()) elif sys.platform == "win32": # for Windows self.media_player.set_hwnd(self.ui.frame_video.winId()) elif sys.platform == "darwin": # for MacOS self.media_player.set_nsobject(self.ui.frame_video.winId()) self.ui.entry_video.setText(self.video_filename) self.media_started_playing = False self.media_is_playing = False self.set_volume(self.ui.slider_volume.value()) self.play_pause_model.setState(True) def browse_video_handler(self): """ Handler when the video browse button is clicked """ tmp_name, _ = QFileDialog.getOpenFileName( self, "Choose Video file", None, "All Files (*)" ) if not tmp_name: return self.set_video_filename(QDir.toNativeSeparators(tmp_name)) def _show_error(self, message, title="Error"): QMessageBox.warning(self, title, message)
class MainWindow(QMainWindow): """ The main window class """ def __init__(self, parent=None): QMainWindow.__init__(self, parent) self.ui = uic.loadUi("gui/main_window.ui") self.timestamp_filename = None self.video_filename = None self.media_start_time = None self.media_end_time = None self.restart_needed = False self.timer_period = 100 self.is_full_screen = False self.media_started_playing = False self.media_is_playing = False self.original_geometry = None self.mute = False self.timestamp_model = TimestampModel(None, self) self.proxy_model = QSortFilterProxyModel(self) self.ui.list_timestamp.setModel(self.timestamp_model) self.ui.list_timestamp.doubleClicked.connect( lambda event: self.ui.list_timestamp.indexAt(event.pos()).isValid() and self.run() ) self.timer = QTimer() self.timer.timeout.connect(self.update_ui) self.timer.timeout.connect(self.timer_handler) self.timer.start(self.timer_period) self.vlc_instance = vlc.Instance() self.media_player = self.vlc_instance.media_player_new() # if sys.platform == "darwin": # for MacOS # self.ui.frame_video = QMacCocoaViewContainer(0) self.ui.frame_video.doubleClicked.connect(self.toggle_full_screen) self.ui.frame_video.wheel.connect(self.wheel_handler) self.ui.frame_video.keyPressed.connect(self.key_handler) # Set up Labels: # self.ui.lblVideoName. # self.displayed_video_title = bind("self.ui.lblVideoName", "text", str) self.ui.lblVideoName.setText(self.video_filename) self.ui.lblVideoSubtitle.setText("") self.ui.dateTimeEdit.setHidden(True) self.ui.lblCurrentFrame.setText("") self.ui.lblTotalFrames.setText("") self.ui.lblCurrentTime.setText("") self.ui.lblTotalDuration.setText("") self.ui.lblFileFPS.setText("") self.ui.spinBoxFrameJumpMultiplier.value = 1 # Set up buttons self.ui.button_run.clicked.connect(self.run) self.ui.button_timestamp_browse.clicked.connect( self.browse_timestamp_handler ) self.ui.button_timestamp_create.clicked.connect( self.create_timestamp_file_handler ) self.ui.button_video_browse.clicked.connect( self.browse_video_handler ) # Set up directional buttons self.ui.btnSkipLeft.clicked.connect(self.skip_left_handler) self.ui.btnSkipRight.clicked.connect(self.skip_right_handler) self.ui.btnLeft.clicked.connect(self.seek_left_handler) self.ui.btnRight.clicked.connect(self.seek_right_handler) self.play_pause_model = ToggleButtonModel(None, self) self.play_pause_model.setStateMap( { True: { "text": "", "icon": qta.icon("fa.play", scale_factor=0.7) }, False: { "text": "", "icon": qta.icon("fa.pause", scale_factor=0.7) } } ) self.ui.button_play_pause.setModel(self.play_pause_model) self.ui.button_play_pause.clicked.connect(self.play_pause) self.mute_model = ToggleButtonModel(None, self) self.mute_model.setStateMap( { True: { "text": "", "icon": qta.icon("fa.volume-up", scale_factor=0.8) }, False: { "text": "", "icon": qta.icon("fa.volume-off", scale_factor=0.8) } } ) self.ui.button_mute_toggle.setModel(self.mute_model) self.ui.button_mute_toggle.clicked.connect(self.toggle_mute) self.ui.button_full_screen.setIcon( qta.icon("ei.fullscreen", scale_factor=0.6) ) self.ui.button_full_screen.setText("") self.ui.button_full_screen.clicked.connect(self.toggle_full_screen) self.ui.button_speed_up.clicked.connect(self.speed_up_handler) self.ui.button_speed_up.setIcon( qta.icon("fa.arrow-circle-o-up", scale_factor=0.8) ) self.ui.button_speed_up.setText("") self.ui.button_slow_down.clicked.connect(self.slow_down_handler) self.ui.button_slow_down.setIcon( qta.icon("fa.arrow-circle-o-down", scale_factor=0.8) ) self.ui.button_slow_down.setText("") self.ui.button_mark_start.setIcon( qta.icon("fa.quote-left", scale_factor=0.7) ) self.ui.button_mark_start.setText("") self.ui.button_mark_end.setIcon( qta.icon("fa.quote-right", scale_factor=0.7) ) self.ui.button_mark_end.setText("") self.ui.button_add_entry.clicked.connect(self.add_entry) self.ui.button_remove_entry.clicked.connect(self.remove_entry) self.ui.button_mark_start.clicked.connect( lambda: self.set_mark(start_time=int( self.media_player.get_position() * self.media_player.get_media().get_duration())) ) self.ui.button_mark_end.clicked.connect( lambda: self.set_mark(end_time=int( self.media_player.get_position() * self.media_player.get_media().get_duration())) ) self.ui.slider_progress.setTracking(False) self.ui.slider_progress.valueChanged.connect(self.set_media_position) self.ui.slider_volume.valueChanged.connect(self.set_volume) self.ui.entry_description.setReadOnly(True) # Mapper between the table and the entry detail self.mapper = QDataWidgetMapper() self.mapper.setSubmitPolicy(QDataWidgetMapper.ManualSubmit) self.ui.button_save.clicked.connect(self.mapper.submit) # Set up default volume self.set_volume(self.ui.slider_volume.value()) self.vlc_events = self.media_player.event_manager() self.vlc_events.event_attach( vlc.EventType.MediaPlayerTimeChanged, self.media_time_change_handler ) # Let our application handle mouse and key input instead of VLC self.media_player.video_set_mouse_input(False) self.media_player.video_set_key_input(False) self.ui.show() def add_entry(self): if not self.timestamp_filename: self._show_error("You haven't chosen a timestamp file yet") row_num = self.timestamp_model.rowCount() self.timestamp_model.insertRow(row_num) start_cell = self.timestamp_model.index(row_num, 0) end_cell = self.timestamp_model.index(row_num, 1) self.timestamp_model.setData(start_cell, TimestampDelta.from_string("")) self.timestamp_model.setData(end_cell, TimestampDelta.from_string("")) def remove_entry(self): if not self.timestamp_filename: self._show_error("You haven't chosen a timestamp file yet") selected = self.ui.list_timestamp.selectionModel().selectedIndexes() if len(selected) == 0: return self.proxy_model.removeRow(selected[0].row()) and self.mapper.submit() def set_media_position(self, position): percentage = position / 10000.0 self.media_player.set_position(percentage) absolute_position = percentage * \ self.media_player.get_media().get_duration() if absolute_position > self.media_end_time: self.media_end_time = -1 def set_mark(self, start_time=None, end_time=None): if len(self.ui.list_timestamp.selectedIndexes()) == 0: blankRowIndex = self.timestamp_model.blankRowIndex() if not blankRowIndex.isValid(): self.add_entry() else: index = self.proxy_model.mapFromSource(blankRowIndex) self.ui.list_timestamp.selectRow(index.row()) selectedIndexes = self.ui.list_timestamp.selectedIndexes() if start_time: self.proxy_model.setData(selectedIndexes[0], TimestampDelta.string_from_int( start_time)) if end_time: self.proxy_model.setData(selectedIndexes[1], TimestampDelta.string_from_int( end_time)) def update_ui(self): self.ui.slider_progress.blockSignals(True) self.ui.slider_progress.setValue( self.media_player.get_position() * 10000 ) #print(self.media_player.get_position() * 10000) self.update_video_file_play_labels() # When the video finishes self.ui.slider_progress.blockSignals(False) if self.media_started_playing and \ self.media_player.get_media().get_state() == vlc.State.Ended: self.play_pause_model.setState(True) # Apparently we need to reset the media, otherwise the player # won't play at all self.media_player.set_media(self.media_player.get_media()) self.set_volume(self.ui.slider_volume.value()) self.media_is_playing = False self.media_started_playing = False self.run() def timer_handler(self): """ This is a workaround, because for some reason we can't call set_time() inside the MediaPlayerTimeChanged handler (as the video just stops playing) """ if self.restart_needed: self.media_player.set_time(self.media_start_time) self.restart_needed = False def key_handler(self, event): if event.key() == Qt.Key_Escape and self.is_full_screen: self.toggle_full_screen() if event.key() == Qt.Key_F: self.toggle_full_screen() if event.key() == Qt.Key_Space: self.play_pause() def wheel_handler(self, event): self.modify_volume(1 if event.angleDelta().y() > 0 else -1) def toggle_mute(self): self.media_player.audio_set_mute(not self.media_player.audio_get_mute()) self.mute = not self.mute self.mute_model.setState(not self.mute) def modify_volume(self, delta_percent): new_volume = self.media_player.audio_get_volume() + delta_percent if new_volume < 0: new_volume = 0 elif new_volume > 40: new_volume = 40 self.media_player.audio_set_volume(new_volume) self.ui.slider_volume.setValue(self.media_player.audio_get_volume()) def set_volume(self, new_volume): self.media_player.audio_set_volume(new_volume) def speed_up_handler(self): self.modify_rate(0.1) def slow_down_handler(self): self.modify_rate(-0.1) def modify_rate(self, delta_percent): new_rate = self.media_player.get_rate() + delta_percent if new_rate < 0.2 or new_rate > 2.0: return self.media_player.set_rate(new_rate) def media_time_change_handler(self, _): if self.media_end_time == -1: return if self.media_player.get_time() > self.media_end_time: self.restart_needed = True def update_slider_highlight(self): if self.ui.list_timestamp.selectionModel().hasSelection(): selected_row = self.ui.list_timestamp.selectionModel(). \ selectedRows()[0] self.media_start_time = self.ui.list_timestamp.model().data( selected_row.model().index(selected_row.row(), 0), Qt.UserRole ) self.media_end_time = self.ui.list_timestamp.model().data( selected_row.model().index(selected_row.row(), 1), Qt.UserRole ) duration = self.media_player.get_media().get_duration() self.media_end_time = self.media_end_time \ if self.media_end_time != 0 else duration if self.media_start_time > self.media_end_time: raise ValueError("Start time cannot be later than end time") if self.media_start_time > duration: raise ValueError("Start time not within video duration") if self.media_end_time > duration: raise ValueError("End time not within video duration") slider_start_pos = (self.media_start_time / duration) * \ (self.ui.slider_progress.maximum() - self.ui.slider_progress.minimum()) slider_end_pos = (self.media_end_time / duration) * \ (self.ui.slider_progress.maximum() - self.ui.slider_progress.minimum()) self.ui.slider_progress.setHighlight( int(slider_start_pos), int(slider_end_pos) ) else: self.media_start_time = 0 self.media_end_time = -1 def run(self): """ Execute the loop """ if self.timestamp_filename is None: self._show_error("No timestamp file chosen") return if self.video_filename is None: self._show_error("No video file chosen") return try: self.update_slider_highlight() self.media_player.play() self.media_player.set_time(self.media_start_time) self.media_started_playing = True self.media_is_playing = True self.play_pause_model.setState(False) except Exception as ex: self._show_error(str(ex)) print(traceback.format_exc()) def play_pause(self): """Toggle play/pause status """ if not self.media_started_playing: self.run() return if self.media_is_playing: self.media_player.pause() else: self.media_player.play() self.media_is_playing = not self.media_is_playing self.play_pause_model.setState(not self.media_is_playing) def update_video_file_play_labels(self): curr_total_fps = self.media_player.get_fps() curr_total_duration = self.media_player.get_length() totalNumFrames = int(curr_total_duration * curr_total_fps) if totalNumFrames > 0: self.ui.lblTotalFrames.setText(str(totalNumFrames)) else: self.ui.lblTotalFrames.setText("--") if curr_total_duration > 0: self.ui.lblTotalDuration.setText(str(curr_total_duration)) # Gets duration in [ms] else: self.ui.lblTotalDuration.setText("--") # Changing Values: Dynamically updated each time the playhead changes curr_percent_complete = self.media_player.get_position() # Current percent complete between 0.0 and 1.0 if curr_percent_complete >= 0: self.ui.lblPlaybackPercent.setText(str(curr_percent_complete)) else: self.ui.lblPlaybackPercent.setText("--") curr_frame = int(round(curr_percent_complete * totalNumFrames)) if curr_frame >= 0: self.ui.lblCurrentFrame.setText(str(curr_frame)) else: self.ui.lblCurrentFrame.setText("--") if self.media_player.get_time() >= 0: self.ui.lblCurrentTime.setText(str(self.media_player.get_time()) + "[ms]") # Gets time in [ms] else: self.ui.lblCurrentTime.setText("-- [ms]") # Gets time in [ms] # Called only when the video file changes: def update_video_file_labels_on_file_change(self): if self.video_filename is None: self.ui.lblVideoName.setText("") else: self.ui.lblVideoName.setText(self.video_filename) # Only updated when the video file is changed: curr_total_fps = self.media_player.get_fps() self.ui.lblFileFPS.setText(str(curr_total_fps)) curr_total_duration = self.media_player.get_length() totalNumFrames = int(curr_total_duration * curr_total_fps) if totalNumFrames > 0: self.ui.lblTotalFrames.setText(str(totalNumFrames)) else: self.ui.lblTotalFrames.setText("--") if curr_total_duration > 0: self.ui.lblTotalDuration.setText(str(curr_total_duration)) # Gets duration in [ms] else: self.ui.lblTotalDuration.setText("--") self.update_video_file_play_labels() def get_frame_multipler(self): return self.ui.spinBoxFrameJumpMultiplier.value # def compute_total_number_frames(self): # self.media_player.get_length() def seek_left_handler(self): print('seek: left') self.seek_frames(-10 * self.get_frame_multipler()) def skip_left_handler(self): print('skip: left') self.seek_frames(-1 * self.get_frame_multipler()) def seek_right_handler(self): print('seek: right') self.seek_frames(10 * self.get_frame_multipler()) def skip_right_handler(self): print('skip: right') self.seek_frames(1 * self.get_frame_multipler()) def seek_frames(self, relativeFrameOffset): """Jump a certain number of frames forward or back """ if self.video_filename is None: self._show_error("No video file chosen") return # if self.media_end_time == -1: # return curr_total_fps = self.media_player.get_fps() relativeSecondsOffset = relativeFrameOffset / curr_total_fps # Desired offset in seconds curr_total_duration = self.media_player.get_length() relative_percent_offset = relativeSecondsOffset / curr_total_duration # percent of the whole that we want to skip totalNumFrames = int(curr_total_duration * curr_total_fps) try: didPauseMedia = False if self.media_is_playing: self.media_player.pause() didPauseMedia = True newPosition = self.media_player.get_position() + relative_percent_offset # newTime = int(self.media_player.get_time() + relativeFrameOffset) # self.update_slider_highlight() # self.media_player.set_time(newTime) self.media_player.set_position(newPosition) if (didPauseMedia): self.media_player.play() # else: # # Otherwise, the media was already paused, we need to very quickly play the media to update the frame with the new time, and then immediately pause it again. # self.media_player.play() # self.media_player.pause() self.media_player.next_frame() print("Setting media playback time to ", newPosition) except Exception as ex: self._show_error(str(ex)) print(traceback.format_exc()) def toggle_full_screen(self): if self.is_full_screen: # TODO Artifacts still happen some time when exiting full screen # in X11 self.ui.frame_media.showNormal() self.ui.frame_media.restoreGeometry(self.original_geometry) self.ui.frame_media.setParent(self.ui.widget_central) self.ui.layout_main.addWidget(self.ui.frame_media, 2, 3, 3, 1) # self.ui.frame_media.ensurePolished() else: self.ui.frame_media.setParent(None) self.ui.frame_media.setWindowFlags(Qt.FramelessWindowHint | Qt.CustomizeWindowHint) self.original_geometry = self.ui.frame_media.saveGeometry() desktop = QApplication.desktop() rect = desktop.screenGeometry(desktop.screenNumber(QCursor.pos())) self.ui.frame_media.setGeometry(rect) self.ui.frame_media.showFullScreen() self.ui.frame_media.show() self.ui.frame_video.setFocus() self.is_full_screen = not self.is_full_screen def browse_timestamp_handler(self): """ Handler when the timestamp browser button is clicked """ tmp_name, _ = QFileDialog.getOpenFileName( self, "Choose Timestamp file", None, "Timestamp File (*.tmsp);;All Files (*)" ) if not tmp_name: return self.set_timestamp_filename(QDir.toNativeSeparators(tmp_name)) def create_timestamp_file_handler(self): """ Handler when the timestamp file create button is clicked """ tmp_name, _ = QFileDialog.getSaveFileName( self, "Create New Timestamp file", None, "Timestamp File (*.tmsp);;All Files (*)" ) if not tmp_name: return try: if (os.stat(QDir.toNativeSeparators(tmp_name)).st_size == 0): # File is empty, create a non-empty one: with open(QDir.toNativeSeparators(tmp_name), "w") as fh: fh.write("[]") # Write the minimal valid JSON string to the file to allow it to be used else: pass # with open(tmp_name, 'r') as fh: # if fh.__sizeof__()>0: # # File is not empty: # pass # else: # # File is empty, create a non-empty one: # fh.close() # with open(tmp_name, "w") as fh: # fh.write("[]") # Write the minimal valid JSON string to the file to allow it to be used except WindowsError: with open(tmp_name, "w") as fh: fh.write("[]") # Write the minimal valid JSON string to the file to allow it to be used # Create new file: self.set_timestamp_filename(QDir.toNativeSeparators(tmp_name)) def _sort_model(self): self.ui.list_timestamp.sortByColumn(0, Qt.AscendingOrder) def _select_blank_row(self, parent, start, end): self.ui.list_timestamp.selectRow(start) def set_timestamp_filename(self, filename): """ Set the timestamp file name """ if not os.path.isfile(filename): self._show_error("Cannot access timestamp file " + filename) return try: self.timestamp_model = TimestampModel(filename, self) self.timestamp_model.timeParseError.connect( lambda err: self._show_error(err) ) self.proxy_model.setSortRole(Qt.UserRole) self.proxy_model.dataChanged.connect(self._sort_model) self.proxy_model.dataChanged.connect(self.update_slider_highlight) self.proxy_model.setSourceModel(self.timestamp_model) self.proxy_model.rowsInserted.connect(self._sort_model) self.proxy_model.rowsInserted.connect(self._select_blank_row) self.ui.list_timestamp.setModel(self.proxy_model) self.timestamp_filename = filename self.ui.entry_timestamp.setText(self.timestamp_filename) self.mapper.setModel(self.proxy_model) self.mapper.addMapping(self.ui.entry_start_time, 0) self.mapper.addMapping(self.ui.entry_end_time, 1) self.mapper.addMapping(self.ui.entry_description, 2) self.ui.list_timestamp.selectionModel().selectionChanged.connect( self.timestamp_selection_changed) self._sort_model() directory = os.path.dirname(self.timestamp_filename) basename = os.path.basename(self.timestamp_filename) timestamp_name_without_ext = os.path.splitext(basename)[0] for file_in_dir in os.listdir(directory): current_filename = os.path.splitext(file_in_dir)[0] found_video = (current_filename == timestamp_name_without_ext and file_in_dir != basename) if found_video: found_video_file = os.path.join(directory, file_in_dir) self.set_video_filename(found_video_file) break except ValueError as err: self._show_error("Timestamp file is invalid") def timestamp_selection_changed(self, selected, deselected): if len(selected) > 0: self.mapper.setCurrentModelIndex(selected.indexes()[0]) self.ui.button_save.setEnabled(True) self.ui.button_remove_entry.setEnabled(True) self.ui.entry_start_time.setReadOnly(False) self.ui.entry_end_time.setReadOnly(False) self.ui.entry_description.setReadOnly(False) else: self.mapper.setCurrentModelIndex(QModelIndex()) self.ui.button_save.setEnabled(False) self.ui.button_remove_entry.setEnabled(False) self.ui.entry_start_time.clear() self.ui.entry_end_time.clear() self.ui.entry_description.clear() self.ui.entry_start_time.setReadOnly(True) self.ui.entry_end_time.setReadOnly(True) self.ui.entry_description.setReadOnly(True) def set_video_filename(self, filename): """ Set the video filename """ if not os.path.isfile(filename): self._show_error("Cannot access video file " + filename) return self.video_filename = filename media = self.vlc_instance.media_new(self.video_filename) media.parse() if not media.get_duration(): self._show_error("Cannot play this media file") self.media_player.set_media(None) self.video_filename = None else: self.media_player.set_media(media) if sys.platform.startswith('linux'): # for Linux using the X Server self.media_player.set_xwindow(self.ui.frame_video.winId()) elif sys.platform == "win32": # for Windows self.media_player.set_hwnd(self.ui.frame_video.winId()) elif sys.platform == "darwin": # for MacOS self.media_player.set_nsobject(self.ui.frame_video.winId()) self.ui.entry_video.setText(self.video_filename) self.update_video_file_labels_on_file_change() self.media_started_playing = False self.media_is_playing = False self.set_volume(self.ui.slider_volume.value()) self.play_pause_model.setState(True) def browse_video_handler(self): """ Handler when the video browse button is clicked """ tmp_name, _ = QFileDialog.getOpenFileName( self, "Choose Video file", None, "All Files (*)" ) if not tmp_name: return self.set_video_filename(QDir.toNativeSeparators(tmp_name)) def _show_error(self, message, title="Error"): QMessageBox.warning(self, title, message)