class OptimizerView(QTableView): def __init__(self, parent=None): super().__init__() self.sorter = None def set_model(self, data): self.sorter = QSortFilterProxyModel() self.sorter.setDynamicSortFilter(True) self.sorter.setSortRole(Qt.EditRole) self.sorter.setSourceModel(OptimizerModel(data)) self.setModel(self.sorter) self.resizeColumnsToContents() self.setSortingEnabled(True)
class PopUpView(QTableView): def __init__(self, parent=None): super().__init__() self.sorter = None def set_model(self, data): self.sorter = QSortFilterProxyModel() self.sorter.setDynamicSortFilter(True) self.sorter.setSortRole(Qt.EditRole) self.sorter.setSourceModel(PopUpModel(data)) self.setModel(self.sorter) self.resizeColumnsToContents() self.setSortingEnabled(True) self.setSelectionBehavior(QAbstractItemView.SelectRows)
class CompilerView(QTableView): def __init__(self, parent=None): super().__init__() self.sorter = None def set_model(self, data): self.sorter = QSortFilterProxyModel() self.sorter.setDynamicSortFilter(True) self.sorter.setSortRole(Qt.EditRole) self.sorter.setFilterKeyColumn(1) self.sorter.setSourceModel(CompilerModel(data)) self.setModel(self.sorter) self.resizeColumnsToContents() self.setColumnWidth(0, max(100, self.columnWidth(0))) self.setColumnWidth(1, max(100, self.columnWidth(1))) for x in range (4, 8): self.setColumnWidth(x, max(self.columnWidth(x), 100)) self.setSortingEnabled(True) self.setSelectionBehavior(QAbstractItemView.SelectRows)
class MainWidget(QWidget): def __init__(self, parent): ## Initiating MainWidget super(MainWidget, self).__init__( parent) ## I have no idea what kind of magic going on here self.init_variables() ## This signal_count counts how many comics user opened. This is needed to disconnect "self.comic_file_table_model.itemChanged.connect(self.comic_file_table_cell_changed)" self.signal_count = 0 self.init_UI() def init_UI(self): ## Function that makes UI for the program layout = QGridLayout() # Setting QWidget layout to be grid self.choose_comic_file_directory = expanduser( "~" ) # This initiaded here instead of init_variables, because it will be changed during usage of the program to remember last place user chose a file and it doesn't need to be reset every time user choose another file. ## All UI elemnts in order they are added ## Variable names are self explanatory self.label_filename = QLabel("No comic is selected") self.label_filename.setAlignment(Qt.AlignCenter) self.button_select_comic = QPushButton("Select Comic") self.button_select_comic.clicked.connect(self.choose_comic_file) page_filename_groupbox = QGroupBox("Page Filename") page_filename_groupbox_layout = QVBoxLayout() self.page_filename_remove_checkbox = QCheckBox("Remove") self.page_filename_remove_checkbox.stateChanged.connect( self.page_filename_remove_checkbox_state_changed) self.page_filename_remove_line_edit = QLineEdit() self.page_filename_remove_line_edit.setEnabled(False) self.page_filename_remove_line_edit.setPlaceholderText( "Text you want to remove.") self.page_filename_replace_checkbox = QCheckBox("Replace with") self.page_filename_replace_checkbox.stateChanged.connect( self.page_filename_replace_checkbox_state_changed) self.page_filename_replace_checkbox.setEnabled(False) self.page_filename_replace_line_edit = QLineEdit() self.page_filename_replace_line_edit.setEnabled(False) self.page_filename_replace_line_edit.setPlaceholderText( "Text you want to replace with") page_filename_groupbox_layout.addWidget( self.page_filename_remove_checkbox) page_filename_groupbox_layout.addWidget( self.page_filename_remove_line_edit) page_filename_groupbox_layout.addWidget( self.page_filename_replace_checkbox) page_filename_groupbox_layout.addWidget( self.page_filename_replace_line_edit) page_filename_groupbox.setLayout(page_filename_groupbox_layout) # Setting up table model for table that will contain items inside comic's archive self.comic_file_table_model = QStandardItemModel() self.comic_file_table_model.setColumnCount(2) self.comic_file_table_proxy_model = QSortFilterProxyModel() self.comic_file_table_proxy_model.setSourceModel( self.comic_file_table_model) self.comic_file_table_proxy_model.setSortRole(10) self.comic_file_table_proxy_model.sort(0, Qt.AscendingOrder) # Setting up view for the table that will display items inside comic's archive self.comic_file_table = QTableView() self.comic_file_table.setModel(self.comic_file_table_proxy_model) self.comic_file_table.horizontalHeader().hide() self.comic_file_table.verticalHeader().hide() self.comic_file_table.resizeColumnToContents(0) self.comic_file_table.horizontalHeader().setStretchLastSection(True) self.button_convert_to_cbz = QPushButton("Convert to CBZ") self.button_convert_to_cbz.setToolTip( "Will not change content of the archive.") self.button_convert_to_cbz.setEnabled(False) self.button_convert_to_cbz.clicked.connect(self.convert_to_cbz_clicked) self.button_remove_subfolder_thumbs = QPushButton("Remove Trash") self.button_remove_subfolder_thumbs.setToolTip( "Only removes subfolder and all extra files. Will not remove pages." ) self.button_remove_subfolder_thumbs.setEnabled(False) self.button_remove_subfolder_thumbs.clicked.connect( self.button_remove_subfolder_thumbs_clicked) self.button_fix_comic = QPushButton("Fix Comic") self.button_fix_comic.setToolTip( "Removes selected images, subfolder, thumbs.db and renames pages if chosen." ) self.button_fix_comic.setEnabled(False) self.button_fix_comic.clicked.connect(self.button_fix_comic_clicked) self.label_message = QLabel() self.label_message.setAlignment(Qt.AlignCenter) # Adding all UI elements to the layout layout.addWidget(self.label_filename, 0, 0, 1, 6) layout.addWidget(self.button_select_comic, 0, 6, 1, 1) layout.addWidget(page_filename_groupbox, 1, 0, 3, 7) layout.addWidget(self.comic_file_table, 5, 0, 8, 6) layout.addWidget(self.button_convert_to_cbz, 9, 6, 1, 1) layout.addWidget(self.button_remove_subfolder_thumbs, 10, 6, 1, 1) layout.addWidget(self.button_fix_comic, 12, 6, 1, 1) layout.addWidget(self.label_message, 13, 0, 1, 7) self.setLayout(layout) # Setting layout the QMainWindow. def choose_comic_file(self): ## Prompts user to select a file and checks selected file. Set's variables used by other functions later. chosen_file = QFileDialog.getOpenFileName( self, "Choose Comic File", self.choose_comic_file_directory, "Comics (*.cbr *.cbz)" )[0] # Prompts user to select comic file and saves result to variable if chosen_file != "": # Checks if user actually selected a file self.choose_comic_file_directory = split(chosen_file)[0] # Resetting all variables for new file self.init_variables() # Disabling all buttons for new file self.button_convert_to_cbz.setEnabled(False) self.button_remove_subfolder_thumbs.setEnabled(False) self.button_fix_comic.setEnabled(False) # Disabling connection to the table if there is one. if self.comic_file_table_model.receivers( self.comic_file_table_model.itemChanged) > 0: self.comic_file_table_model.itemChanged.disconnect( self.comic_file_table_cell_changed) chosen_file_exte = split(chosen_file)[1][-4:].lower( ) # Saves user's chosen's file extention to a variable if chosen_file_exte == ".cbz" or chosen_file_exte == ".cbr": ## Checks if user's selected file actually ends with. ## Set's variables, shows message to user with file name and enables buttons if True self.comic_file = chosen_file self.comic_file_name = split(self.comic_file)[1][:-4] self.comic_file_exte = chosen_file_exte # Printin file that is being worked on. self.label_filename.setText(self.comic_file_name + self.comic_file_exte) # Removing all # from filename if user passed -s as an argument when launching program. if len(argv) > 1: if argv[1] == "-s": self.comic_file_name = self.comic_file_name.replace( "#", "") self.label_message.clear( ) # Clears message in case user selected a not comic file previously or working with multiple file in a row. # Checking if comic arhcive is rar file. If true enabling "Convert to CBZ button" if self.comic_file_exte == ".cbr": self.button_convert_to_cbz.setEnabled(True) # Getting more variables that will be used to by other functions. For more info check engine.check_comic. self.sorted_filename_length_dict, self.sub_folder_toggle, self.thumbs_db = engine.check_comic( self.comic_file, self.comic_file_name, self.comic_file_exte) # Enabling button "Remove Trash" if toggles are switched by check_engine function if self.sub_folder_toggle == 1 or self.thumbs_db[0] == 1: self.button_remove_subfolder_thumbs.setEnabled(True) # Enabling "Fix Comic" button self.button_fix_comic.setEnabled(True) self.label_message.clear( ) # Clears message in case user selected a not comic file previously or working with multiple file in a row. self.display_comic_files() self.comic_file_table.scrollToTop() else: ## Prints a message to user if he selected not comic file. self.label_message.setText( "You have to select cbr or cbz file.") def display_comic_files(self): ## Adds all comic archive files to the QtableWidget and checkmarks as suggestion based on sorted_file_length_dict ignore_file_exte = [".jpg", ".png", ".xml" ] # extension that will be not marked for deletion self.comic_file_list = engine.archive_file_list( self.comic_file, self.comic_file_name, self.comic_file_exte) # Getting archive's file list from engine. self.comic_file_table_model.setRowCount( len(self.comic_file_list) ) # Setting tables row count to the count of files inside archive ## Prints a message if a subfolder is detected. if self.sub_folder_toggle == 1: self.label_message.setText("There is a subfolder!") ## This makes two presumptions. First, is that first (shortest) file in dictionary will be the one that needs to be removed. Second, that it will be mention just once. Dictionary is already sorted by key (filename length) and this if statement checks if this length one found just once. Adding it to the delete_files list if true. if self.sorted_filename_length_dict[0][1][1] == 1: self.delete_files.append(self.sorted_filename_length_dict[0][1][0]) ## If thumbs_db toggle is switched adding it to the delete file list. if self.thumbs_db[0] == 1: self.delete_files.append(self.thumbs_db[1]) for item in range(len(self.comic_file_list)): ## Checkicg if file extentions isn't *.jpg or *.xml. If not it goes to delete_list. if self.comic_file_list[item][-4:].lower( ) not in ignore_file_exte and self.comic_file_list[ item] not in self.delete_files: self.delete_files.append(self.comic_file_list[item]) ## Adding every item from archive to the table item_checkbox_detele = QStandardItem() if self.comic_file_list[item] in self.delete_files: item_checkbox_detele.setCheckState( Qt.Unchecked ) # Setting checkmark as Unchecked. Files is marked for deletion else: item_checkbox_detele.setCheckState(Qt.Checked) item_checkbox_detele.setFlags( Qt.ItemIsUserCheckable | Qt.ItemIsEnabled ) # Checkmark's cell not editable, but state still can be changed. item_filename = QStandardItem( split(self.comic_file_list[item])[1] ) # Getting just a filename, without a path to the file in archive. item_filename.setFlags( Qt.ItemIsEnabled) # Filename's cell not editable self.comic_file_table_model.setItem(item, 0, item_checkbox_detele) self.comic_file_table_model.setItem(item, 1, item_filename) self.comic_file_table_model.itemChanged.connect( self.comic_file_table_cell_changed) def convert_to_cbz_clicked(self): ## This fuction exists because it is not possible to pass variables using connect self.disable_buttons() engine.convert_to_cbz(self.comic_file, self.comic_file_name) self.enable_buttons() self.label_message.setText("Converted " + self.comic_file_name + " to cbz") def button_remove_subfolder_thumbs_clicked(self): ## This function removes only subfolder and thumbs.db if they exists in the archive. ## Even if any other file will be selected for deletion it won't be deleted. self.disable_buttons() local_delete_files = [ ] # Instead of global delete file, local one will be used. ## Adding thumbs.db to the delete list if it exists. if self.thumbs_db[0] == 1: local_delete_files.append(self.thumbs_db[1]) engine.write_comic( self.comic_file, self.comic_file_name, self.comic_file_exte, local_delete_files, [] ) # Passing empty list for remove_from_filename. This function do not change filenames. self.enable_buttons() self.label_message.setText("Extra file removed!") def comic_file_table_cell_changed(self, clicked_checkbox): ## Function triggred when user toggles checkmark in table. clicked_checkbox_location = clicked_checkbox.index() clicked_item_state = clicked_checkbox.checkState() clicked_item_filename = self.comic_file_list[ clicked_checkbox_location.row( )] # Gets filename for the checkmark from comic file list based on checkmark's row. ## Depending of the state of the checkmark checks if filename is marked for deletion. Depending on the that removes or adds filename to delete_list if clicked_item_state == Qt.Checked: if clicked_item_filename in self.delete_files: self.delete_files.remove(clicked_item_filename) elif clicked_item_state == Qt.Unchecked: if clicked_item_filename not in self.delete_files: self.delete_files.append(clicked_item_filename) def page_filename_remove_checkbox_state_changed(self): ## Enables/disables page_filename_remove_line_edit depending on page_filename_remove_checkbox_state if self.page_filename_remove_checkbox.checkState() == Qt.Checked: self.page_filename_remove_line_edit.setEnabled(True) self.page_filename_replace_checkbox.setEnabled( True) # Enables "Replace With" checkbox elif self.page_filename_remove_checkbox.checkState() == Qt.Unchecked: self.page_filename_remove_line_edit.setEnabled(False) self.page_filename_remove_line_edit.clear() if self.page_filename_replace_checkbox.checkState() == Qt.Checked: ## Checks status of "Replace with" checkbox. If it's checked - removes the checkbox and disables it. self.page_filename_replace_checkbox.setChecked(False) self.page_filename_replace_checkbox.setEnabled(False) self.remove_from_filename = [ ] # Resets remove_from_filename, otherwise there will be BUGS. def page_filename_replace_checkbox_state_changed(self): ## Enables/disables page_filename_remove_line_edit depending on page_filename_remove_checkbox_state if self.page_filename_replace_checkbox.checkState() == Qt.Checked: self.page_filename_replace_line_edit.setEnabled(True) elif self.page_filename_replace_checkbox.checkState() == Qt.Unchecked: if self.page_filename_replace_line_edit.text( ) in self.remove_from_filename: ## Removes text that user planned to replace with removed text. self.remove_from_filename.remove( self.page_filename_replace_line_edit.text()) self.page_filename_replace_line_edit.setEnabled(False) self.page_filename_replace_line_edit.clear() def button_fix_comic_clicked(self): ## This functions inplements main funcction of this program. To actually remove page from comc archive, rename files if chosen. self.disable_buttons() self.remove_from_filename = [ ] # Resets the list, otherwise it would add the same items in the list to infinity. if self.page_filename_remove_checkbox.checkState() == Qt.Checked: # If remove checkbox is marked appends text from remove_line_edit to remove_from_filename. self.remove_from_filename.append( self.page_filename_remove_line_edit.text()) if self.page_filename_replace_checkbox.checkState() == Qt.Checked: # If rename checkbox marked appends what's written in replace_line_edit to remove_from_filename. self.remove_from_filename.append( self.page_filename_replace_line_edit.text()) else: # If rename checkbox isn't marked appends empty string to the remove_from_filename list self.remove_from_filename.append("") engine.write_comic(self.comic_file, self.comic_file_name, self.comic_file_exte, self.delete_files, self.remove_from_filename) self.enable_buttons() self.label_message.setText( "Fixed comic: " + self.comic_file_name + ".cbz") # prints messages to user what file was fixed. def disable_buttons(self): ## To Disable buttons before program starts working archive. self.button_select_comic.setEnabled(False) self.button_convert_to_cbz.setEnabled(False) self.button_remove_subfolder_thumbs.setEnabled(False) self.button_fix_comic.setEnabled(False) def enable_buttons(self): ## To Enable buttons after program finishes saving archive. self.button_select_comic.setEnabled(True) self.button_convert_to_cbz.setEnabled(True) self.button_remove_subfolder_thumbs.setEnabled(True) self.button_fix_comic.setEnabled(True) def init_variables(self): ## Variables needed for Engine Functions and other GUI elements that need to be reset before loading new file. self.comic_file = "" self.comic_file_name = "" self.comic_file_exte = "" self.sorted_filename_length_dict = dict() self.sub_folder_toggle = 0 self.thumbs_db = (0, "") self.comic_file_list = [] self.delete_files = [] self.remove_from_filename = []
class ListWidget(QWidget): deviceSelected = pyqtSignal(TasmotaDevice) openRulesEditor = pyqtSignal() openConsole = pyqtSignal() openTelemetry = pyqtSignal() openWebUI = pyqtSignal() def __init__(self, parent, *args, **kwargs): super(ListWidget, self).__init__(*args, **kwargs) self.setWindowTitle("Devices list") self.setWindowState(Qt.WindowMaximized) self.setLayout(VLayout(margin=0, spacing=0)) self.mqtt = parent.mqtt self.env = parent.env self.device = None self.idx = None self.nam = QNetworkAccessManager() self.backup = bytes() self.settings = QSettings("{}/TDM/tdm.cfg".format(QDir.homePath()), QSettings.IniFormat) views_order = self.settings.value("views_order", []) self.views = {} self.settings.beginGroup("Views") views = self.settings.childKeys() if views and views_order: for view in views_order.split(";"): view_list = self.settings.value(view).split(";") self.views[view] = base_view + view_list else: self.views = default_views self.settings.endGroup() self.tb = Toolbar(Qt.Horizontal, 24, Qt.ToolButtonTextBesideIcon) self.tb_relays = Toolbar(Qt.Horizontal, 24, Qt.ToolButtonIconOnly) # self.tb_filter = Toolbar(Qt.Horizontal, 24, Qt.ToolButtonTextBesideIcon) self.tb_views = Toolbar(Qt.Horizontal, 24, Qt.ToolButtonTextBesideIcon) self.pwm_sliders = [] self.layout().addWidget(self.tb) self.layout().addWidget(self.tb_relays) # self.layout().addWidget(self.tb_filter) self.device_list = TableView() self.device_list.setIconSize(QSize(24, 24)) self.model = parent.device_model self.model.setupColumns(self.views["Home"]) self.sorted_device_model = QSortFilterProxyModel() self.sorted_device_model.setFilterCaseSensitivity(Qt.CaseInsensitive) self.sorted_device_model.setSourceModel(parent.device_model) self.sorted_device_model.setSortRole(Qt.InitialSortOrderRole) self.sorted_device_model.setFilterKeyColumn(-1) self.device_list.setModel(self.sorted_device_model) self.device_list.setupView(self.views["Home"]) self.device_list.setSortingEnabled(True) self.device_list.setWordWrap(True) self.device_list.setItemDelegate(DeviceDelegate()) self.device_list.sortByColumn(self.model.columnIndex("FriendlyName"), Qt.AscendingOrder) self.device_list.setContextMenuPolicy(Qt.CustomContextMenu) self.device_list.verticalHeader().setSectionResizeMode(QHeaderView.ResizeToContents) self.layout().addWidget(self.device_list) self.layout().addWidget(self.tb_views) self.device_list.clicked.connect(self.select_device) self.device_list.customContextMenuRequested.connect(self.show_list_ctx_menu) self.ctx_menu = QMenu() self.ctx_menu_relays = None self.create_actions() self.create_view_buttons() # self.create_view_filter() self.device_list.doubleClicked.connect(lambda: self.openConsole.emit()) def create_actions(self): self.ctx_menu_cfg = QMenu("Configure") self.ctx_menu_cfg.setIcon(QIcon("GUI/icons/settings.png")) self.ctx_menu_cfg.addAction("Module", self.configureModule) self.ctx_menu_cfg.addAction("GPIO", self.configureGPIO) self.ctx_menu_cfg.addAction("Template", self.configureTemplate) # self.ctx_menu_cfg.addAction("Wifi", self.ctx_menu_teleperiod) # self.ctx_menu_cfg.addAction("Time", self.cfgTime.emit) # self.ctx_menu_cfg.addAction("MQTT", self.ctx_menu_teleperiod) # self.ctx_menu_cfg.addAction("Logging", self.ctx_menu_teleperiod) self.ctx_menu.addMenu(self.ctx_menu_cfg) self.ctx_menu.addSeparator() self.ctx_menu.addAction(QIcon("GUI/icons/refresh.png"), "Refresh", self.ctx_menu_refresh) self.ctx_menu.addSeparator() self.ctx_menu.addAction(QIcon("GUI/icons/clear.png"), "Clear retained", self.ctx_menu_clear_retained) self.ctx_menu.addAction("Clear Backlog", self.ctx_menu_clear_backlog) self.ctx_menu.addSeparator() self.ctx_menu.addAction(QIcon("GUI/icons/copy.png"), "Copy", self.ctx_menu_copy) self.ctx_menu.addSeparator() self.ctx_menu.addAction(QIcon("GUI/icons/restart.png"), "Restart", self.ctx_menu_restart) self.ctx_menu.addAction(QIcon(), "Reset", self.ctx_menu_reset) self.ctx_menu.addSeparator() self.ctx_menu.addAction(QIcon("GUI/icons/delete.png"), "Delete", self.ctx_menu_delete_device) console = self.tb.addAction(QIcon("GUI/icons/console.png"), "Console", self.openConsole.emit) console.setShortcut("Ctrl+E") rules = self.tb.addAction(QIcon("GUI/icons/rules.png"), "Rules", self.openRulesEditor.emit) rules.setShortcut("Ctrl+R") self.tb.addAction(QIcon("GUI/icons/timers.png"), "Timers", self.configureTimers) buttons = self.tb.addAction(QIcon("GUI/icons/buttons.png"), "Buttons", self.configureButtons) buttons.setShortcut("Ctrl+B") switches = self.tb.addAction(QIcon("GUI/icons/switches.png"), "Switches", self.configureSwitches) switches.setShortcut("Ctrl+S") power = self.tb.addAction(QIcon("GUI/icons/power.png"), "Power", self.configurePower) power.setShortcut("Ctrl+P") # setopts = self.tb.addAction(QIcon("GUI/icons/setoptions.png"), "SetOptions", self.configureSO) # setopts.setShortcut("Ctrl+S") self.tb.addSpacer() telemetry = self.tb.addAction(QIcon("GUI/icons/telemetry.png"), "Telemetry", self.openTelemetry.emit) telemetry.setShortcut("Ctrl+T") webui = self.tb.addAction(QIcon("GUI/icons/web.png"), "WebUI", self.openWebUI.emit) webui.setShortcut("Ctrl+U") # self.tb.addAction(QIcon(), "Multi Command", self.ctx_menu_webui) self.agAllPower = QActionGroup(self) self.agAllPower.addAction(QIcon("GUI/icons/P_ON.png"), "All ON") self.agAllPower.addAction(QIcon("GUI/icons/P_OFF.png"), "All OFF") self.agAllPower.setEnabled(False) self.agAllPower.setExclusive(False) self.agAllPower.triggered.connect(self.toggle_power_all) self.tb_relays.addActions(self.agAllPower.actions()) self.agRelays = QActionGroup(self) self.agRelays.setVisible(False) self.agRelays.setExclusive(False) for a in range(1, 9): act = QAction(QIcon("GUI/icons/P{}_OFF.png".format(a)), "") act.setShortcut("F{}".format(a)) self.agRelays.addAction(act) self.agRelays.triggered.connect(self.toggle_power) self.tb_relays.addActions(self.agRelays.actions()) self.tb_relays.addSeparator() self.actColor = self.tb_relays.addAction(QIcon("GUI/icons/color.png"), "Color", self.set_color) self.actColor.setEnabled(False) self.actChannels = self.tb_relays.addAction(QIcon("GUI/icons/sliders.png"), "Channels") self.actChannels.setEnabled(False) self.mChannels = QMenu() self.actChannels.setMenu(self.mChannels) self.tb_relays.widgetForAction(self.actChannels).setPopupMode(QToolButton.InstantPopup) def create_view_buttons(self): self.tb_views.addWidget(QLabel("View mode: ")) ag_views = QActionGroup(self) ag_views.setExclusive(True) for v in self.views.keys(): a = QAction(v) a.triggered.connect(self.change_view) a.setCheckable(True) ag_views.addAction(a) self.tb_views.addActions(ag_views.actions()) ag_views.actions()[0].setChecked(True) stretch = QWidget() stretch.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)) self.tb_views.addWidget(stretch) # actEditView = self.tb_views.addAction("Edit views...") # def create_view_filter(self): # # self.tb_filter.addWidget(QLabel("Show devices: ")) # # self.cbxLWT = QComboBox() # # self.cbxLWT.addItems(["All", "Online"d, "Offline"]) # # self.cbxLWT.currentTextChanged.connect(self.build_filter_regex) # # self.tb_filter.addWidget(self.cbxLWT) # # self.tb_filter.addWidget(QLabel(" Search: ")) # self.leSearch = QLineEdit() # self.leSearch.setClearButtonEnabled(True) # self.leSearch.textChanged.connect(self.build_filter_regex) # self.tb_filter.addWidget(self.leSearch) # # def build_filter_regex(self, txt): # query = self.leSearch.text() # # if self.cbxLWT.currentText() != "All": # # query = "{}|{}".format(self.cbxLWT.currentText(), query) # self.sorted_device_model.setFilterRegExp(query) def change_view(self, a=None): view = self.views[self.sender().text()] self.model.setupColumns(view) self.device_list.setupView(view) def ctx_menu_copy(self): if self.idx: string = dumps(self.model.data(self.idx)) if string.startswith('"') and string.endswith('"'): string = string[1:-1] QApplication.clipboard().setText(string) def ctx_menu_clear_retained(self): if self.device: relays = self.device.power() if relays and len(relays.keys()) > 0: for r in relays.keys(): self.mqtt.publish(self.device.cmnd_topic(r), retain=True) QMessageBox.information(self, "Clear retained", "Cleared retained messages.") def ctx_menu_clear_backlog(self): if self.device: self.mqtt.publish(self.device.cmnd_topic("backlog"), "") QMessageBox.information(self, "Clear Backlog", "Backlog cleared.") def ctx_menu_restart(self): if self.device: self.mqtt.publish(self.device.cmnd_topic("restart"), payload="1") for k in list(self.device.power().keys()): self.device.p.pop(k) def ctx_menu_reset(self): if self.device: reset, ok = QInputDialog.getItem(self, "Reset device and restart", "Select reset mode", resets, editable=False) if ok: self.mqtt.publish(self.device.cmnd_topic("reset"), payload=reset.split(":")[0]) for k in list(self.device.power().keys()): self.device.p.pop(k) def ctx_menu_refresh(self): if self.device: for k in list(self.device.power().keys()): self.device.p.pop(k) for c in initial_commands(): cmd, payload = c cmd = self.device.cmnd_topic(cmd) self.mqtt.publish(cmd, payload, 1) def ctx_menu_delete_device(self): if self.device: if QMessageBox.question(self, "Confirm", "Do you want to remove the following device?\n'{}' ({})" .format(self.device.p['FriendlyName1'], self.device.p['Topic'])) == QMessageBox.Yes: self.model.deleteDevice(self.idx) def ctx_menu_teleperiod(self): if self.device: teleperiod, ok = QInputDialog.getInt(self, "Set telemetry period", "Input 1 to reset to default\n[Min: 10, Max: 3600]", self.device.p['TelePeriod'], 1, 3600) if ok: if teleperiod != 1 and teleperiod < 10: teleperiod = 10 self.mqtt.publish(self.device.cmnd_topic("teleperiod"), teleperiod) def ctx_menu_config_backup(self): if self.device: self.backup = bytes() self.dl = self.nam.get(QNetworkRequest(QUrl("http://{}/dl".format(self.device.p['IPAddress'])))) self.dl.readyRead.connect(self.get_dump) self.dl.finished.connect(self.save_dump) def ctx_menu_ota_set_url(self): if self.device: url, ok = QInputDialog.getText(self, "Set OTA URL", '100 chars max. Set to "1" to reset to default.', text=self.device.p['OtaUrl']) if ok: self.mqtt.publish(self.device.cmnd_topic("otaurl"), payload=url) def ctx_menu_ota_set_upgrade(self): if self.device: if QMessageBox.question(self, "OTA Upgrade", "Are you sure to OTA upgrade from\n{}".format(self.device.p['OtaUrl']), QMessageBox.Yes | QMessageBox.No) == QMessageBox.Yes: self.mqtt.publish(self.device.cmnd_topic("upgrade"), payload="1") def show_list_ctx_menu(self, at): self.select_device(self.device_list.indexAt(at)) self.ctx_menu.popup(self.device_list.viewport().mapToGlobal(at)) def select_device(self, idx): self.idx = self.sorted_device_model.mapToSource(idx) self.device = self.model.deviceAtRow(self.idx.row()) self.deviceSelected.emit(self.device) relays = self.device.power() self.agAllPower.setEnabled(len(relays) >= 1) for i, a in enumerate(self.agRelays.actions()): a.setVisible(len(relays) > 1 and i < len(relays)) color = self.device.color().get("Color", False) has_color = bool(color) self.actColor.setEnabled(has_color and not self.device.setoption(68)) self.actChannels.setEnabled(has_color) if has_color: self.actChannels.menu().clear() max_val = 100 if self.device.setoption(15) == 0: max_val = 1023 for k, v in self.device.pwm().items(): channel = SliderAction(self, k) channel.slider.setMaximum(max_val) channel.slider.setValue(int(v)) self.mChannels.addAction(channel) channel.slider.valueChanged.connect(self.set_channel) dimmer = self.device.color().get("Dimmer") if dimmer: saDimmer = SliderAction(self, "Dimmer") saDimmer.slider.setValue(int(dimmer)) self.mChannels.addAction(saDimmer) saDimmer.slider.valueChanged.connect(self.set_channel) def toggle_power(self, action): if self.device: idx = self.agRelays.actions().index(action) relay = list(self.device.power().keys())[idx] self.mqtt.publish(self.device.cmnd_topic(relay), "toggle") def toggle_power_all(self, action): if self.device: idx = self.agAllPower.actions().index(action) for r in self.device.power().keys(): self.mqtt.publish(self.device.cmnd_topic(r), str(not bool(idx))) def set_color(self): if self.device: color = self.device.color().get("Color") if color: dlg = QColorDialog() new_color = dlg.getColor(QColor("#{}".format(color))) if new_color.isValid(): new_color = new_color.name() if new_color != color: self.mqtt.publish(self.device.cmnd_topic("color"), new_color) def set_channel(self, value=0): cmd = self.sender().objectName() if self.device: self.mqtt.publish(self.device.cmnd_topic(cmd), str(value)) def configureSO(self): if self.device: dlg = SetOptionsDialog(self.device) dlg.sendCommand.connect(self.mqtt.publish) dlg.exec_() def configureModule(self): if self.device: dlg = ModuleDialog(self.device) dlg.sendCommand.connect(self.mqtt.publish) dlg.exec_() def configureGPIO(self): if self.device: dlg = GPIODialog(self.device) dlg.sendCommand.connect(self.mqtt.publish) dlg.exec_() def configureTemplate(self): if self.device: dlg = TemplateDialog(self.device) dlg.sendCommand.connect(self.mqtt.publish) dlg.exec_() def configureTimers(self): if self.device: self.mqtt.publish(self.device.cmnd_topic("timers")) timers = TimersDialog(self.device) self.mqtt.messageSignal.connect(timers.parseMessage) timers.sendCommand.connect(self.mqtt.publish) timers.exec_() def configureButtons(self): if self.device: backlog = [] buttons = ButtonsDialog(self.device) if buttons.exec_() == QDialog.Accepted: for c, cw in buttons.command_widgets.items(): current_value = self.device.p.get(c) new_value = "" if isinstance(cw.input, SpinBox): new_value = cw.input.value() if isinstance(cw.input, QComboBox): new_value = cw.input.currentIndex() if current_value != new_value: backlog.append("{} {}".format(c, new_value)) for so, sow in buttons.setoption_widgets.items(): current_value = self.device.setoption(so) new_value = -1 if isinstance(sow.input, SpinBox): new_value = sow.input.value() if isinstance(sow.input, QComboBox): new_value = sow.input.currentIndex() if current_value != new_value: backlog.append("SetOption{} {}".format(so, new_value)) if backlog: backlog.append("status 3") self.mqtt.publish(self.device.cmnd_topic("backlog"), "; ".join(backlog)) def configureSwitches(self): if self.device: backlog = [] switches = SwitchesDialog(self.device) if switches.exec_() == QDialog.Accepted: for c, cw in switches.command_widgets.items(): current_value = self.device.p.get(c) new_value = "" if isinstance(cw.input, SpinBox): new_value = cw.input.value() if isinstance(cw.input, QComboBox): new_value = cw.input.currentIndex() if current_value != new_value: backlog.append("{} {}".format(c, new_value)) for so, sow in switches.setoption_widgets.items(): current_value = self.device.setoption(so) new_value = -1 if isinstance(sow.input, SpinBox): new_value = sow.input.value() if isinstance(sow.input, QComboBox): new_value = sow.input.currentIndex() if current_value != new_value: backlog.append("SetOption{} {}".format(so, new_value)) for sw, sw_mode in enumerate(self.device.p['SwitchMode']): new_value = switches.sm.inputs[sw].currentIndex() if sw_mode != new_value: backlog.append("switchmode{} {}".format(sw+1, new_value)) if backlog: backlog.append("status") backlog.append("status 3") self.mqtt.publish(self.device.cmnd_topic("backlog"), "; ".join(backlog)) def configurePower(self): if self.device: backlog = [] power = PowerDialog(self.device) if power.exec_() == QDialog.Accepted: for c, cw in power.command_widgets.items(): current_value = self.device.p.get(c) new_value = "" if isinstance(cw.input, SpinBox): new_value = cw.input.value() if isinstance(cw.input, QComboBox): new_value = cw.input.currentIndex() if current_value != new_value: backlog.append("{} {}".format(c, new_value)) for so, sow in power.setoption_widgets.items(): new_value = -1 if isinstance(sow.input, SpinBox): new_value = sow.input.value() if isinstance(sow.input, QComboBox): new_value = sow.input.currentIndex() if new_value != self.device.setoption(so): backlog.append("SetOption{} {}".format(so, new_value)) new_interlock_value = power.ci.input.currentData() new_interlock_grps = " ".join([grp.text().replace(" ", "") for grp in power.ci.groups]).rstrip() if new_interlock_value != self.device.p.get("Interlock", "OFF"): backlog.append("interlock {}".format(new_interlock_value)) if new_interlock_grps != self.device.p.get("Groups", ""): backlog.append("interlock {}".format(new_interlock_grps)) for i, pt in enumerate(power.cpt.inputs): ptime = "PulseTime{}".format(i+1) current_ptime = self.device.p.get(ptime) if current_ptime: current_value = list(current_ptime.keys())[0] new_value = str(pt.value()) if new_value != current_value: backlog.append("{} {}".format(ptime, new_value)) if backlog: backlog.append("status") backlog.append("status 3") self.mqtt.publish(self.device.cmnd_topic("backlog"), "; ".join(backlog)) def get_dump(self): self.backup += self.dl.readAll() def save_dump(self): fname = self.dl.header(QNetworkRequest.ContentDispositionHeader) if fname: fname = fname.split('=')[1] save_file = QFileDialog.getSaveFileName(self, "Save config backup", "{}/TDM/{}".format(QDir.homePath(), fname))[0] if save_file: with open(save_file, "wb") as f: f.write(self.backup) def check_fulltopic(self, fulltopic): fulltopic += "/" if not fulltopic.endswith('/') else '' return "%prefix%" in fulltopic and "%topic%" in fulltopic def closeEvent(self, event): event.ignore()
class SearchFileWidget(QWidget): language_filter_change = pyqtSignal(list) def __init__(self): QWidget.__init__(self) self._refreshing = False self.fileModel = None self.proxyFileModel = None self.videoModel = None self._state = None self.timeLastSearch = QTime.currentTime() self.ui = Ui_SearchFileWidget() self.setup_ui() def set_state(self, state): self._state = state self._state.login_status_changed.connect(self.on_login_state_changed) self._state.interface_language_changed.connect( self.on_interface_language_changed) def get_state(self): return self._state def setup_ui(self): self.ui.setupUi(self) settings = QSettings() self.ui.splitter.setSizes([600, 1000]) self.ui.splitter.setChildrenCollapsible(False) # Set up folder view lastDir = settings.value("mainwindow/workingDirectory", QDir.homePath()) log.debug('Current directory: {currentDir}'.format(currentDir=lastDir)) self.fileModel = QFileSystemModel(self) self.fileModel.setFilter(QDir.AllDirs | QDir.Dirs | QDir.Drives | QDir.NoDotAndDotDot | QDir.Readable | QDir.Executable | QDir.Writable) self.fileModel.iconProvider().setOptions( QFileIconProvider.DontUseCustomDirectoryIcons) self.fileModel.setRootPath(QDir.rootPath()) self.fileModel.directoryLoaded.connect(self.onFileModelDirectoryLoaded) self.proxyFileModel = QSortFilterProxyModel(self) self.proxyFileModel.setSortRole(Qt.DisplayRole) self.proxyFileModel.setSourceModel(self.fileModel) self.proxyFileModel.sort(0, Qt.AscendingOrder) self.proxyFileModel.setSortCaseSensitivity(Qt.CaseInsensitive) self.ui.folderView.setModel(self.proxyFileModel) self.ui.folderView.setHeaderHidden(True) self.ui.folderView.hideColumn(3) self.ui.folderView.hideColumn(2) self.ui.folderView.hideColumn(1) index = self.fileModel.index(lastDir) proxyIndex = self.proxyFileModel.mapFromSource(index) self.ui.folderView.scrollTo(proxyIndex) self.ui.folderView.expanded.connect(self.onFolderViewExpanded) self.ui.folderView.clicked.connect(self.onFolderTreeClicked) self.ui.buttonFind.clicked.connect(self.onButtonFind) self.ui.buttonRefresh.clicked.connect(self.onButtonRefresh) # Set up introduction self.showInstructions() # Set up video view self.ui.filterLanguageForVideo.set_unknown_text(_('All languages')) self.ui.filterLanguageForVideo.selected_language_changed.connect( self.on_language_combobox_filter_change) # self.ui.filterLanguageForVideo.selected_language_changed.connect(self.onFilterLanguageVideo) self.videoModel = VideoModel(self) self.ui.videoView.setHeaderHidden(True) self.ui.videoView.setModel(self.videoModel) self.ui.videoView.activated.connect(self.onClickVideoTreeView) self.ui.videoView.clicked.connect(self.onClickVideoTreeView) self.ui.videoView.customContextMenuRequested.connect(self.onContext) self.videoModel.dataChanged.connect(self.subtitlesCheckedChanged) self.language_filter_change.connect( self.videoModel.on_filter_languages_change) self.ui.buttonSearchSelectVideos.clicked.connect( self.onButtonSearchSelectVideos) self.ui.buttonSearchSelectFolder.clicked.connect( self.onButtonSearchSelectFolder) self.ui.buttonDownload.clicked.connect(self.onButtonDownload) self.ui.buttonPlay.clicked.connect(self.onButtonPlay) self.ui.buttonIMDB.clicked.connect(self.onViewOnlineInfo) self.ui.videoView.setContextMenuPolicy(Qt.CustomContextMenu) # Drag and Drop files to the videoView enabled self.ui.videoView.__class__.dragEnterEvent = self.dragEnterEvent self.ui.videoView.__class__.dragMoveEvent = self.dragEnterEvent self.ui.videoView.__class__.dropEvent = self.dropEvent self.ui.videoView.setAcceptDrops(1) # FIXME: ok to drop this connect? # self.ui.videoView.clicked.connect(self.onClickMovieTreeView) self.retranslate() def retranslate(self): introduction = '<p align="center"><h2>{title}</h2></p>' \ '<p><b>{tab1header}</b><br/>{tab1content}</p>' \ '<p><b>{tab2header}</b><br/>{tab2content}</p>'\ '<p><b>{tab3header}</b><br/>{tab3content}</p>'.format( title=_('How To Use {title}').format(title=PROJECT_TITLE), tab1header=_('1st Tab:'), tab2header=_('2nd Tab:'), tab3header=_('3rd Tab:'), tab1content=_('Select, from the Folder Tree on the left, the folder which contains the videos ' 'that need subtitles. {project} will then try to automatically find available ' 'subtitles.').format(project=PROJECT_TITLE), tab2content=_('If you don\'t have the videos in your machine, you can search subtitles by ' 'introducing the title/name of the video.').format(project=PROJECT_TITLE), tab3content=_('If you have found some subtitle somewhere else that is not in {project}\'s database, ' 'please upload those subtitles so next users will be able to ' 'find them more easily.').format(project=PROJECT_TITLE)) self.ui.introductionHelp.setHtml(introduction) @pyqtSlot(Language) def on_interface_language_changed(self, language): self.ui.retranslateUi(self) self.retranslate() @pyqtSlot(str) def onFileModelDirectoryLoaded(self, path): settings = QSettings() lastDir = settings.value('mainwindow/workingDirectory', QDir.homePath()) qDirLastDir = QDir(lastDir) qDirLastDir.cdUp() if qDirLastDir.path() == path: index = self.fileModel.index(lastDir) proxyIndex = self.proxyFileModel.mapFromSource(index) self.ui.folderView.scrollTo(proxyIndex) self.ui.folderView.setCurrentIndex(proxyIndex) @pyqtSlot(int, str) def on_login_state_changed(self, state, message): log.debug( 'on_login_state_changed(state={state}, message={message}'.format( state=state, message=message)) if state in (State.LOGIN_STATUS_LOGGED_OUT, State.LOGIN_STATUS_BUSY): self.ui.buttonSearchSelectFolder.setEnabled(False) self.ui.buttonSearchSelectVideos.setEnabled(False) self.ui.buttonFind.setEnabled(False) elif state == State.LOGIN_STATUS_LOGGED_IN: self.ui.buttonSearchSelectFolder.setEnabled(True) self.ui.buttonSearchSelectVideos.setEnabled(True) self.ui.buttonFind.setEnabled( self.get_current_selected_folder() is not None) else: log.warning('unknown state') @pyqtSlot(Language) def on_language_combobox_filter_change(self, language): if language.is_generic(): self.language_filter_change.emit( self.get_state().get_permanent_language_filter()) else: self.language_filter_change.emit([language]) def on_permanent_language_filter_change(self, languages): selected_language = self.ui.filterLanguageForVideo.get_selected_language( ) if selected_language.is_generic(): self.language_filter_change.emit(languages) @pyqtSlot() def subtitlesCheckedChanged(self): subs = self.videoModel.get_checked_subtitles() if subs: self.ui.buttonDownload.setEnabled(True) else: self.ui.buttonDownload.setEnabled(False) def showInstructions(self): self.ui.stackedSearchResult.setCurrentWidget(self.ui.pageIntroduction) def hideInstructions(self): self.ui.stackedSearchResult.setCurrentWidget(self.ui.pageSearchResult) @pyqtSlot(QModelIndex) def onFolderTreeClicked(self, proxyIndex): """What to do when a Folder in the tree is clicked""" if not proxyIndex.isValid(): return index = self.proxyFileModel.mapToSource(proxyIndex) settings = QSettings() folder_path = self.fileModel.filePath(index) settings.setValue('mainwindow/workingDirectory', folder_path) # self.ui.buttonFind.setEnabled(self.get_state().) def get_current_selected_folder(self): proxyIndex = self.ui.folderView.currentIndex() index = self.proxyFileModel.mapToSource(proxyIndex) folder_path = self.fileModel.filePath(index) if not folder_path: return None return folder_path def get_current_selected_item_videomodel(self): current_index = self.ui.videoView.currentIndex() return self.videoModel.getSelectedItem(current_index) @pyqtSlot() def onButtonFind(self): now = QTime.currentTime() if now < self.timeLastSearch.addMSecs(500): return folder_path = self.get_current_selected_folder() settings = QSettings() settings.setValue('mainwindow/workingDirectory', folder_path) self.search_videos([folder_path]) self.timeLastSearch = QTime.currentTime() @pyqtSlot() def onButtonRefresh(self): currentPath = self.get_current_selected_folder() if not currentPath: settings = QSettings() currentPath = settings.value('mainwindow/workingDirectory', QDir.homePath()) self._refreshing = True self.ui.folderView.collapseAll() currentPath = self.get_current_selected_folder() if not currentPath: settings = QSettings() currentPath = settings.value('mainwindow/workingDirectory', QDir.homePath()) index = self.fileModel.index(currentPath) self.ui.folderView.scrollTo(self.proxyFileModel.mapFromSource(index)) @pyqtSlot(QModelIndex) def onFolderViewExpanded(self, proxyIndex): if self._refreshing: expandedPath = self.fileModel.filePath( self.proxyFileModel.mapToSource(proxyIndex)) if expandedPath == QDir.rootPath(): currentPath = self.get_current_selected_folder() if not currentPath: settings = QSettings() currentPath = settings.value('mainwindow/workingDirectory', QDir.homePath()) index = self.fileModel.index(currentPath) self.ui.folderView.scrollTo( self.proxyFileModel.mapFromSource(index)) self._refreshing = False @pyqtSlot() def onButtonSearchSelectFolder(self): settings = QSettings() path = settings.value('mainwindow/workingDirectory', QDir.homePath()) folder_path = QFileDialog.getExistingDirectory( self, _('Select the directory that contains your videos'), path) if folder_path: settings.setValue('mainwindow/workingDirectory', folder_path) self.search_videos([folder_path]) @pyqtSlot() def onButtonSearchSelectVideos(self): settings = QSettings() currentDir = settings.value('mainwindow/workingDirectory', QDir.homePath()) fileNames, t = QFileDialog.getOpenFileNames( self, _('Select the video(s) that need subtitles'), currentDir, SELECT_VIDEOS) if fileNames: settings.setValue('mainwindow/workingDirectory', QFileInfo(fileNames[0]).absolutePath()) self.search_videos(fileNames) def search_videos(self, paths): if not self.get_state().connected(): QMessageBox.about( self, _("Error"), _('You are not connected to the server. Please reconnect first.' )) return self.ui.buttonFind.setEnabled(False) self._search_videos_raw(paths) self.ui.buttonFind.setEnabled(True) def _search_videos_raw(self, paths): # FIXME: must pass mainwindow as argument to ProgressCallbackWidget callback = ProgressCallbackWidget(self) callback.set_title_text(_("Scanning...")) callback.set_label_text(_("Scanning files")) callback.set_finished_text(_("Scanning finished")) callback.set_block(True) try: local_videos, local_subs = scan_videopaths(paths, callback=callback, recursive=True) except OSError: callback.cancel() QMessageBox.warning(self, _('Error'), _('Some directories are not accessible.')) if callback.canceled(): return callback.finish() log.debug("Videos found: %s" % local_videos) log.debug("Subtitles found: %s" % local_subs) self.hideInstructions() QCoreApplication.processEvents() if not local_videos: QMessageBox.about(self, _("Scan Results"), _("No video has been found!")) return total = len(local_videos) # FIXME: must pass mainwindow as argument to ProgressCallbackWidget # callback = ProgressCallbackWidget(self) # callback.set_title_text(_("Asking Server...")) # callback.set_label_text(_("Searching subtitles...")) # callback.set_updated_text(_("Searching subtitles ( %d / %d )")) # callback.set_finished_text(_("Search finished")) callback.set_block(True) callback.set_range(0, total) callback.show() callback.set_range(0, 2) download_callback = callback.get_child_progress(0, 1) # videoSearchResults = self.get_state().get_OSDBServer().SearchSubtitles("", videos_piece) remote_subs = self.get_state().get_OSDBServer().search_videos( videos=local_videos, callback=download_callback) self.videoModel.set_videos(local_videos) # self.onFilterLanguageVideo(self.ui.filterLanguageForVideo.get_selected_language()) if remote_subs is None: QMessageBox.about( self, _("Error"), _("Error contacting the server. Please try again later")) callback.finish() # TODO: CHECK if our local subtitles are already in the server, otherwise suggest to upload # self.OSDBServer.CheckSubHash(sub_hashes) @pyqtSlot() def onButtonPlay(self): settings = QSettings() programPath = settings.value('options/VideoPlayerPath', '') parameters = settings.value('options/VideoPlayerParameters', '') if programPath == '': QMessageBox.about( self, _('Error'), _('No default video player has been defined in Settings.')) return selected_subtitle = self.get_current_selected_item_videomodel() if isinstance(selected_subtitle, SubtitleFileNetwork): selected_subtitle = selected_subtitle.get_subtitles()[0] if isinstance(selected_subtitle, LocalSubtitleFile): subtitle_file_path = selected_subtitle.get_filepath() elif isinstance(selected_subtitle, RemoteSubtitleFile): subtitle_file_path = QDir.temp().absoluteFilePath( 'subdownloader.tmp.srt') log.debug( 'Temporary subtitle will be downloaded into: {temp_path}'. format(temp_path=subtitle_file_path)) # FIXME: must pass mainwindow as argument to ProgressCallbackWidget callback = ProgressCallbackWidget(self) callback.set_title_text(_('Playing video + sub')) callback.set_label_text(_('Downloading files...')) callback.set_finished_text(_('Downloading finished')) callback.set_block(True) callback.set_range(0, 100) callback.show() try: subtitle_stream = selected_subtitle.download( self.get_state().get_OSDBServer(), callback=callback) except ProviderConnectionError: log.debug('Unable to download subtitle "{}"'.format( selected_subtitle.get_filename()), exc_info=True) QMessageBox.about( self, _('Error'), _('Unable to download subtitle "{subtitle}"').format( subtitle=selected_subtitle.get_filename())) callback.finish() return callback.finish() write_stream(subtitle_stream, subtitle_file_path) video = selected_subtitle.get_parent().get_parent().get_parent() def windows_escape(text): return '"{text}"'.format(text=text.replace('"', '\\"')) params = [windows_escape(programPath)] for param in parameters.split(' '): param = param.format(video.get_filepath(), subtitle_file_path) if platform.system() in ('Windows', 'Microsoft'): param = windows_escape(param) params.append(param) pid = None log.info('Running this command: {params}'.format(params=params)) try: log.debug('Trying os.spawnvpe ...') pid = os.spawnvpe(os.P_NOWAIT, programPath, params, os.environ) log.debug('... SUCCESS. pid={pid}'.format(pid=pid)) except AttributeError: log.debug('... FAILED', exc_info=True) except Exception as e: log.debug('... FAILED', exc_info=True) if pid is None: try: log.debug('Trying os.fork ...') pid = os.fork() if not pid: log.debug('... SUCCESS. pid={pid}'.format(pid=pid)) os.execvpe(os.P_NOWAIT, programPath, params, os.environ) except: log.debug('... FAIL', exc_info=True) if pid is None: QMessageBox.about(self, _('Error'), _('Unable to launch videoplayer')) @pyqtSlot(QModelIndex) def onClickVideoTreeView(self, index): data_item = self.videoModel.getSelectedItem(index) if isinstance(data_item, SubtitleFile): self.ui.buttonPlay.setEnabled(True) else: self.ui.buttonPlay.setEnabled(False) if isinstance(data_item, VideoFile): video = data_item if True: # video.getMovieInfo(): self.ui.buttonIMDB.setEnabled(True) self.ui.buttonIMDB.setIcon(QIcon(':/images/info.png')) self.ui.buttonIMDB.setText(_('Movie Info')) elif isinstance(data_item, RemoteSubtitleFile): self.ui.buttonIMDB.setEnabled(True) self.ui.buttonIMDB.setIcon( QIcon(':/images/sites/opensubtitles.png')) self.ui.buttonIMDB.setText(_('Subtitle Info')) else: self.ui.buttonIMDB.setEnabled(False) def onContext(self, point): # FIXME: code duplication with Main.onContext and/or SearchNameWidget and/or SearchFileWidget menu = QMenu('Menu', self) listview = self.ui.videoView index = listview.currentIndex() data_item = listview.model().getSelectedItem(index) if data_item is not None: if isinstance(data_item, VideoFile): video = data_item movie_info = video.getMovieInfo() if movie_info: subWebsiteAction = QAction(QIcon(":/images/info.png"), _("View IMDB info"), self) subWebsiteAction.triggered.connect(self.onViewOnlineInfo) else: subWebsiteAction = QAction(QIcon(":/images/info.png"), _("Set IMDB info..."), self) subWebsiteAction.triggered.connect(self.onSetIMDBInfo) menu.addAction(subWebsiteAction) elif isinstance(data_item, SubtitleFile): downloadAction = QAction(QIcon(":/images/download.png"), _("Download"), self) # Video tab, TODO:Replace me with a enum downloadAction.triggered.connect(self.onButtonDownload) playAction = QAction(QIcon(":/images/play.png"), _("Play video + subtitle"), self) playAction.triggered.connect(self.onButtonPlay) menu.addAction(playAction) subWebsiteAction = QAction( QIcon(":/images/sites/opensubtitles.png"), _("View online info"), self) menu.addAction(downloadAction) subWebsiteAction.triggered.connect(self.onViewOnlineInfo) menu.addAction(subWebsiteAction) elif isinstance(data_item, Movie): subWebsiteAction = QAction(QIcon(":/images/info.png"), _("View IMDB info"), self) subWebsiteAction.triggered.connect(self.onViewOnlineInfo) menu.addAction(subWebsiteAction) # Show the context menu. menu.exec_(listview.mapToGlobal(point)) def onButtonDownload(self): # We download the subtitle in the same folder than the video subs = self.videoModel.get_checked_subtitles() replace_all = False skip_all = False if not subs: QMessageBox.about(self, _("Error"), _("No subtitles selected to be downloaded")) return total_subs = len(subs) answer = None success_downloaded = 0 # FIXME: must pass mainwindow as argument to ProgressCallbackWidget callback = ProgressCallbackWidget(self) callback.set_title_text(_("Downloading...")) callback.set_label_text(_("Downloading files...")) callback.set_updated_text(_("Downloading subtitle {0} ({1}/{2})")) callback.set_finished_text( _("{0} from {1} subtitles downloaded successfully")) callback.set_block(True) callback.set_range(0, total_subs) callback.show() for i, sub in enumerate(subs): if callback.canceled(): break destinationPath = self.get_state().getDownloadPath(self, sub) if not destinationPath: break log.debug("Trying to download subtitle '%s'" % destinationPath) callback.update(i, QFileInfo(destinationPath).fileName(), i + 1, total_subs) # Check if we have write permissions, otherwise show warning window while True: # If the file and the folder don't have write access. if not QFileInfo( destinationPath).isWritable() and not QFileInfo( QFileInfo(destinationPath).absoluteDir().path() ).isWritable(): warningBox = QMessageBox( _("Error write permission"), _("%s cannot be saved.\nCheck that the folder exists and you have write-access permissions." ) % destinationPath, QMessageBox.Warning, QMessageBox.Retry | QMessageBox.Default, QMessageBox.Discard | QMessageBox.Escape, QMessageBox.NoButton, self) saveAsButton = warningBox.addButton( _("Save as..."), QMessageBox.ActionRole) answer = warningBox.exec_() if answer == QMessageBox.Retry: continue elif answer == QMessageBox.Discard: break # Let's get out from the While true # If we choose the SAVE AS elif answer == QMessageBox.NoButton: fileName, t = QFileDialog.getSaveFileName( self, _("Save subtitle as..."), destinationPath, 'All (*.*)') if fileName: destinationPath = fileName else: # If we have write access we leave the while loop. break # If we have chosen Discard subtitle button. if answer == QMessageBox.Discard: continue # Continue the next subtitle optionWhereToDownload = QSettings().value( "options/whereToDownload", "SAME_FOLDER") # Check if doesn't exists already, otherwise show fileExistsBox # dialog if QFileInfo(destinationPath).exists( ) and not replace_all and not skip_all and optionWhereToDownload != "ASK_FOLDER": # The "remote filename" below is actually not the real filename. Real name could be confusing # since we always rename downloaded sub to match movie # filename. fileExistsBox = QMessageBox( QMessageBox.Warning, _("File already exists"), _("Local: {local}\n\nRemote: {remote}\n\nHow would you like to proceed?" ).format(local=destinationPath, remote=QFileInfo(destinationPath).fileName()), QMessageBox.NoButton, self) skipButton = fileExistsBox.addButton(_("Skip"), QMessageBox.ActionRole) # skipAllButton = fileExistsBox.addButton(_("Skip all"), QMessageBox.ActionRole) replaceButton = fileExistsBox.addButton( _("Replace"), QMessageBox.ActionRole) replaceAllButton = fileExistsBox.addButton( _("Replace all"), QMessageBox.ActionRole) saveAsButton = fileExistsBox.addButton(_("Save as..."), QMessageBox.ActionRole) cancelButton = fileExistsBox.addButton(_("Cancel"), QMessageBox.ActionRole) fileExistsBox.exec_() answer = fileExistsBox.clickedButton() if answer == replaceAllButton: # Don't ask us again (for this batch of files) replace_all = True elif answer == saveAsButton: # We will find a uniqiue filename and suggest this to user. # add .<lang> to (inside) the filename. If that is not enough, start adding numbers. # There should also be a preferences setting "Autorename # files" or similar ( =never ask) FIXME suggBaseName, suggFileExt = os.path.splitext( destinationPath) fNameCtr = 0 # Counter used to generate a unique filename suggestedFileName = suggBaseName + '.' + \ sub.get_language().xxx() + suggFileExt while (os.path.exists(suggestedFileName)): fNameCtr += 1 suggestedFileName = suggBaseName + '.' + \ sub.get_language().xxx() + '-' + \ str(fNameCtr) + suggFileExt fileName, t = QFileDialog.getSaveFileName( None, _("Save subtitle as..."), suggestedFileName, 'All (*.*)') if fileName: destinationPath = fileName else: # Skip this particular file if no filename chosen continue elif answer == skipButton: continue # Skip this particular file # elif answer == skipAllButton: # count += percentage # skip_all = True # Skip all files already downloaded # continue elif answer == cancelButton: break # Break out of DL loop - cancel was pushed QCoreApplication.processEvents() # FIXME: redundant update? callback.update(i, QFileInfo(destinationPath).fileName(), i + 1, total_subs) try: if not skip_all: log.debug("Downloading subtitle '%s'" % destinationPath) download_callback = ProgressCallback() # FIXME data_stream = sub.download( provider_instance=self.get_state().get_OSDBServer(), callback=download_callback, ) write_stream(data_stream, destinationPath) except Exception as e: log.exception('Unable to Download subtitle {}'.format( sub.get_filename())) QMessageBox.about( self, _("Error"), _("Unable to download subtitle %s") % sub.get_filename()) callback.finish(success_downloaded, total_subs) def onViewOnlineInfo(self): # FIXME: code duplication with Main.onContext and/or SearchNameWidget and/or SearchFileWidget # Tab for SearchByHash TODO:replace this 0 by an ENUM value listview = self.ui.videoView index = listview.currentIndex() data_item = self.videoModel.getSelectedItem(index) if isinstance(data_item, VideoFile): video = data_item movie_info = video.getMovieInfo() if movie_info: imdb = movie_info["IDMovieImdb"] if imdb: webbrowser.open("http://www.imdb.com/title/tt%s" % imdb, new=2, autoraise=1) elif isinstance(data_item, RemoteSubtitleFile): sub = data_item webbrowser.open(sub.get_link(), new=2, autoraise=1) elif isinstance(data_item, Movie): movie = data_item imdb = movie.IMDBId if imdb: webbrowser.open("http://www.imdb.com/title/tt%s" % imdb, new=2, autoraise=1) @pyqtSlot() def onSetIMDBInfo(self): #FIXME: DUPLICATED WITH SEARCHNAMEWIDGET QMessageBox.about(self, _("Info"), "Not implemented yet. Sorry...")
class QItemCollectionSelector(QObject): selectedChanged = pyqtSignal(AmountItem) def __init__(self, tableView, actor, allowContext): QObject.__init__(self) self.actor = actor actor.ownedItems.aItemChanged.connect(self.actorsItemsChanged) self.preFilter = None self.filter = None # table self.tableView = tableView self.tableView.setSortingEnabled(True) self.tableView.selectionChanged = self.selectedItemChanged self.tableModel = ItemTableModel(self.actor.ownedItems) self.proxyModel = QSortFilterProxyModel() self.proxyModel.setSourceModel(self.tableModel) self.proxyModel.setSortRole(Qt.EditRole) self.tableView.setModel(self.proxyModel) resizeColumns(self.tableView, self.tableModel.columnCount()) # bunch func exec if allowContext: self.tableView.setContextMenuPolicy(Qt.CustomContextMenu) self.tableView.customContextMenuRequested.connect(self.itemTableViewContextMenu) self.cachedFuncs = None self.cachedIndex = None def setConstantPreFilter(self, preFilter): self.preFilter = preFilter self.updateFilter(self.filter) def getCurrentSelected(self): index = self.proxyModel.mapToSource(self.tableView.selectionModel().currentIndex()) if not index.isValid(): return AmountItem(None, None) return self.tableModel.filteredAItemList[index.row()] def actorsItemsChanged(self, batch): if batch[0] == AmountList.CHANGE_COMPLETELY: self.tableModel.reapplyFilter() return for i in batch: code = i[1] aItem = i[0] if code == AmountList.CHANGE_AMOUNT: #dataChanged i = self.tableModel.filteredAItemList.getIndexById(aItem.item.id) if i is None: continue index = self.tableModel.index(i, 0) self.tableModel.dataChanged.emit(index, index) elif code == AmountList.CHANGE_REMOVED: i = self.tableModel.filteredAItemList.getIndexById(aItem.item.id) if i == None: continue self.tableModel.layoutAboutToBeChanged.emit() del self.tableModel.filteredAItemList.aItemList[i] self.tableModel.layoutChanged.emit() elif code == AmountList.CHANGE_APPEND: if not self.tableModel.cachedFilter.isValidItem(aItem): continue self.tableModel.layoutAboutToBeChanged.emit() self.tableModel.filteredAItemList.append(aItem) self.tableModel.layoutChanged.emit() def itemTableViewContextMenu(self, pos): menu = QMenu() funcs = self.getFuncsAllSelectedItemsHave() if funcs == None: return for k, v in funcs.iteritems(): menu.addAction(tStr(k), lambda func=v: func(self.getCurrentSelected(), self.actor)) menu.exec_(self.tableView.mapToGlobal(pos)) def getFuncsAllSelectedItemsHave(self): index = self.tableView.selectedIndexes() if index == self.cachedIndex: return self.cachedFuncs if len(index) == 0: return None funcs = {} for i in index: f = self.getCurrentSelected().item.getExtraFunctionality() if funcs == {}: funcs = f else: newFuncs = {} for k in funcs.iterkeys(): if k in f: newFuncs[k] = f[k] funcs = newFuncs self.cachedIndex = index self.cachedFuncs = funcs return funcs def updateFilter(self, filter): self.filter = filter if self.preFilter != None: if filter == None: filter = self.preFilter else: filter = ItemFilterJoin(self.preFilter, ItemFilterJoin.AND, filter) self.tableModel.setFilter(filter) def selectedItemChanged(self, selected, deselected): QTableView.selectionChanged(self.tableView, selected, deselected) self.selectedChanged.emit(self.getCurrentSelected()) def resizeColumns(self): resizeColumns(self.tableView, self.tableModel.columnCount())
class GuiHandViewer(QSplitter): def __init__(self, config, querylist, mainwin): QSplitter.__init__(self, mainwin) self.config = config self.main_window = mainwin self.sql = querylist self.replayer = None self.db = Database.Database(self.config, sql=self.sql) filters_display = { "Heroes": True, "Sites": True, "Games": True, "Currencies": False, "Limits": True, "LimitSep": True, "LimitType": True, "Positions": True, "Type": True, "Seats": False, "SeatSep": False, "Dates": True, "Cards": True, "Groups": False, "GroupsAll": False, "Button1": True, "Button2": False } self.filters = Filters.Filters(self.db, display=filters_display) self.filters.registerButton1Name(_("Load Hands")) self.filters.registerButton1Callback(self.loadHands) self.filters.registerCardsCallback(self.filter_cards_cb) scroll = QScrollArea() scroll.setWidget(self.filters) self.handsFrame = QFrame() self.handsVBox = QVBoxLayout() self.handsFrame.setLayout(self.handsVBox) self.addWidget(scroll) self.addWidget(self.handsFrame) self.setStretchFactor(0, 0) self.setStretchFactor(1, 1) self.deck_instance = Deck.Deck(self.config, height=42, width=30) self.cardImages = self.init_card_images() # Dict of colnames and their column idx in the model/ListStore self.colnum = { 'Stakes': 0, 'Pos': 1, 'Street0': 2, 'Action0': 3, 'Street1-4': 4, 'Action1-4': 5, 'Won': 6, 'Bet': 7, 'Net': 8, 'Game': 9, 'HandId': 10, } self.view = QTableView() self.view.setSelectionBehavior(QTableView.SelectRows) self.handsVBox.addWidget(self.view) self.model = QStandardItemModel(0, len(self.colnum), self.view) self.filterModel = QSortFilterProxyModel() self.filterModel.setSourceModel(self.model) self.filterModel.setSortRole(Qt.UserRole) self.view.setModel(self.filterModel) self.view.verticalHeader().hide() self.model.setHorizontalHeaderLabels([ 'Stakes', 'Pos', 'Street0', 'Action0', 'Street1-4', 'Action1-4', 'Won', 'Bet', 'Net', 'Game', 'HandId' ]) self.view.doubleClicked.connect(self.row_activated) self.view.contextMenuEvent = self.contextMenu self.filterModel.rowsInserted.connect( lambda index, start, end: [self.view.resizeRowToContents(r) for r in xrange(start, end + 1)]) self.filterModel.filterAcceptsRow = lambda row, sourceParent: self.is_row_in_card_filter( row) self.view.resizeColumnsToContents() self.view.setSortingEnabled(True) def init_card_images(self): suits = ('s', 'h', 'd', 'c') ranks = (14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2) card_images = [0] * 53 for j in range(0, 13): for i in range(0, 4): loc = Card.cardFromValueSuit(ranks[j], suits[i]) card_image = self.deck_instance.card(suits[i], ranks[j]) card_images[loc] = card_image back_image = self.deck_instance.back() card_images[0] = back_image return card_images def loadHands(self, checkState): hand_ids = self.get_hand_ids_from_date_range( self.filters.getDates()[0], self.filters.getDates()[1]) self.reload_hands(hand_ids) def get_hand_ids_from_date_range(self, start, end): q = self.db.sql.query['handsInRange'] q = q.replace('<datetest>', "between '" + start + "' and '" + end + "'") q = self.filters.replace_placeholders_with_filter_values(q) c = self.db.get_cursor() c.execute(q) return [r[0] for r in c.fetchall()] def rankedhand(self, hand, game): ranks = { '0': 0, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, 'T': 10, 'J': 11, 'Q': 12, 'K': 13, 'A': 14 } suits = {'x': 0, 's': 1, 'c': 2, 'd': 3, 'h': 4} if game == 'holdem': card1 = ranks[hand[0]] card2 = ranks[hand[3]] suit1 = suits[hand[1]] suit2 = suits[hand[4]] if card1 < card2: (card1, card2) = (card2, card1) (suit1, suit2) = (suit2, suit1) if suit1 == suit2: suit1 += 4 return card1 * 14 * 14 + card2 * 14 + suit1 else: return 0 def reload_hands(self, handids): self.hands = {} self.model.removeRows(0, self.model.rowCount()) if len(handids) == 0: return progress = QProgressDialog("Loading hands", "Abort", 0, len(handids), self) progress.setValue(0) progress.show() for idx, handid in enumerate(handids): if progress.wasCanceled(): break self.hands[handid] = self.importhand(handid) self.addHandRow(handid, self.hands[handid]) progress.setValue(idx + 1) if idx % 10 == 0: QCoreApplication.processEvents() self.view.resizeColumnsToContents() self.view.resizeColumnsToContents() def addHandRow(self, handid, hand): hero = self.filters.getHeroes()[hand.sitename] won = 0 if hero in hand.collectees.keys(): won = hand.collectees[hero] bet = 0 if hero in hand.pot.committed.keys(): bet = hand.pot.committed[hero] net = won - bet pos = hand.get_player_position(hero) gt = hand.gametype['category'] row = [] if hand.gametype['base'] == 'hold': board = [] board.extend(hand.board['FLOP']) board.extend(hand.board['TURN']) board.extend(hand.board['RIVER']) pre_actions = hand.get_actions_short(hero, 'PREFLOP') post_actions = '' if 'F' not in pre_actions: #if player hasen't folded preflop post_actions = hand.get_actions_short_streets( hero, 'FLOP', 'TURN', 'RIVER') row = [ hand.getStakesAsString(), pos, hand.join_holecards(hero), pre_actions, ' '.join(board), post_actions, str(won), str(bet), str(net), gt, str(handid) ] elif hand.gametype['base'] == 'stud': third = " ".join( hand.holecards['THIRD'][hero][0]) + " " + " ".join( hand.holecards['THIRD'][hero][1]) # ugh - fix the stud join_holecards function so we can retrieve sanely later_streets = [] later_streets.extend(hand.holecards['FOURTH'][hero][0]) later_streets.extend(hand.holecards['FIFTH'][hero][0]) later_streets.extend(hand.holecards['SIXTH'][hero][0]) later_streets.extend(hand.holecards['SEVENTH'][hero][0]) pre_actions = hand.get_actions_short(hero, 'THIRD') post_actions = '' if 'F' not in pre_actions: post_actions = hand.get_actions_short_streets( hero, 'FOURTH', 'FIFTH', 'SIXTH', 'SEVENTH') row = [ hand.getStakesAsString(), pos, third, pre_actions, ' '.join(later_streets), post_actions, str(won), str(bet), str(net), gt, str(handid) ] elif hand.gametype['base'] == 'draw': row = [ hand.getStakesAsString(), pos, hand.join_holecards(hero, street='DEAL'), hand.get_actions_short(hero, 'DEAL'), None, None, str(won), str(bet), str(net), gt, str(handid) ] modelrow = [QStandardItem(r) for r in row] for index, item in enumerate(modelrow): item.setEditable(False) if index in (self.colnum['Street0'], self.colnum['Street1-4']): cards = item.data(Qt.DisplayRole) item.setData(self.render_cards(cards), Qt.DecorationRole) item.setData("", Qt.DisplayRole) item.setData(cards, Qt.UserRole + 1) if index in (self.colnum['Bet'], self.colnum['Net'], self.colnum['Won']): item.setData(float(item.data(Qt.DisplayRole)), Qt.UserRole) self.model.appendRow(modelrow) def copyHandToClipboard(self, checkState, hand): handText = StringIO() hand.writeHand(handText) QApplication.clipboard().setText(handText.getvalue()) def contextMenu(self, event): index = self.view.currentIndex() if index.row() < 0: return hand = self.hands[int( index.sibling(index.row(), self.colnum['HandId']).data())] m = QMenu() copyAction = m.addAction('Copy to clipboard') copyAction.triggered.connect( partial(self.copyHandToClipboard, hand=hand)) m.move(event.globalPos()) m.exec_() def filter_cards_cb(self, card): if hasattr(self, 'hands'): self.filterModel.invalidateFilter() def is_row_in_card_filter(self, rownum): """ Returns true if the cards of the given row are in the card filter """ # Does work but all cards that should NOT be displayed have to be clicked. card_filter = self.filters.getCards() hcs = self.model.data(self.model.index(rownum, self.colnum['Street0']), Qt.UserRole + 1).split(' ') if '0x' in hcs: #if cards are unknown return True return True gt = self.model.data(self.model.index(rownum, self.colnum['Game'])) if gt not in ('holdem', 'omahahi', 'omahahilo'): return True # Holdem: Compare the real start cards to the selected filter (ie. AhKh = AKs) value1 = Card.card_map[hcs[0][0]] value2 = Card.card_map[hcs[1][0]] idx = Card.twoStartCards(value1, hcs[0][1], value2, hcs[1][1]) abbr = Card.twoStartCardString(idx) return card_filter[abbr] def row_activated(self, index): handlist = list(sorted(self.hands.keys())) self.replayer = GuiReplayer.GuiReplayer(self.config, self.sql, self.main_window, handlist) self.replayer.play_hand( handlist.index( int(index.sibling(index.row(), self.colnum['HandId']).data()))) def importhand(self, handid=1): # Fetch hand info # We need at least sitename, gametype, handid # for the Hand.__init__ h = Hand.hand_factory(handid, self.config, self.db) # Set the hero for this hand using the filter for the sitename of this hand h.hero = self.filters.getHeroes()[h.sitename] return h def render_cards(self, cardstring): card_width = 30 card_height = 42 if cardstring is None or cardstring == '': cardstring = "0x" cardstring = cardstring.replace("'", "") cardstring = cardstring.replace("[", "") cardstring = cardstring.replace("]", "") cardstring = cardstring.replace("'", "") cardstring = cardstring.replace(",", "") cards = [Card.encodeCard(c) for c in cardstring.split(' ')] n_cards = len(cards) pixbuf = QPixmap(card_width * n_cards, card_height) painter = QPainter(pixbuf) x = 0 # x coord where the next card starts in pixbuf for card in cards: painter.drawPixmap(x, 0, self.cardImages[card]) x += card_width return pixbuf
class SearchFileWidget(QWidget): language_filter_change = pyqtSignal(list) def __init__(self): QWidget.__init__(self) self._refreshing = False self.fileModel = None self.proxyFileModel = None self.videoModel = None self._state = None self.timeLastSearch = QTime.currentTime() self.ui = Ui_SearchFileWidget() self.setup_ui() QTimer.singleShot(0, self.on_event_loop_started) @pyqtSlot() def on_event_loop_started(self): lastDir = self._state.get_video_path() index = self.fileModel.index(str(lastDir)) proxyIndex = self.proxyFileModel.mapFromSource(index) self.ui.folderView.scrollTo(proxyIndex) def set_state(self, state): self._state = state self._state.signals.interface_language_changed.connect( self.on_interface_language_changed) self._state.signals.login_status_changed.connect( self.on_login_state_changed) def setup_ui(self): self.ui.setupUi(self) self.ui.splitter.setSizes([600, 1000]) self.ui.splitter.setChildrenCollapsible(False) # Set up folder view self.fileModel = QFileSystemModel(self) self.fileModel.setFilter(QDir.AllDirs | QDir.Dirs | QDir.Drives | QDir.NoDotAndDotDot | QDir.Readable | QDir.Executable | QDir.Writable) self.fileModel.iconProvider().setOptions( QFileIconProvider.DontUseCustomDirectoryIcons) self.fileModel.setRootPath(QDir.rootPath()) self.fileModel.directoryLoaded.connect(self.onFileModelDirectoryLoaded) self.proxyFileModel = QSortFilterProxyModel(self) self.proxyFileModel.setSortRole(Qt.DisplayRole) self.proxyFileModel.setSourceModel(self.fileModel) self.proxyFileModel.sort(0, Qt.AscendingOrder) self.proxyFileModel.setSortCaseSensitivity(Qt.CaseInsensitive) self.ui.folderView.setModel(self.proxyFileModel) self.ui.folderView.setHeaderHidden(True) self.ui.folderView.hideColumn(3) self.ui.folderView.hideColumn(2) self.ui.folderView.hideColumn(1) self.ui.folderView.expanded.connect(self.onFolderViewExpanded) self.ui.folderView.clicked.connect(self.onFolderTreeClicked) self.ui.buttonFind.clicked.connect(self.onButtonFind) self.ui.buttonRefresh.clicked.connect(self.onButtonRefresh) # Setup and disable buttons self.ui.buttonFind.setEnabled(False) self.ui.buttonSearchSelectFolder.setEnabled(False) self.ui.buttonSearchSelectVideos.setEnabled(False) # Set up introduction self.showInstructions() # Set unknown text here instead of `retranslate()` because widget translates itself self.ui.filterLanguageForVideo.set_unknown_text(_('All languages')) self.ui.filterLanguageForVideo.set_selected_language( UnknownLanguage.create_generic()) self.ui.filterLanguageForVideo.selected_language_changed.connect( self.on_language_combobox_filter_change) # Set up video view self.videoModel = VideoModel(self) self.videoModel.connect_treeview(self.ui.videoView) self.ui.videoView.setHeaderHidden(True) self.ui.videoView.clicked.connect(self.onClickVideoTreeView) self.ui.videoView.selectionModel().currentChanged.connect( self.onSelectVideoTreeView) self.ui.videoView.customContextMenuRequested.connect(self.onContext) self.ui.videoView.setUniformRowHeights(True) self.videoModel.dataChanged.connect(self.subtitlesCheckedChanged) self.language_filter_change.connect( self.videoModel.on_filter_languages_change) self.ui.buttonSearchSelectVideos.clicked.connect( self.onButtonSearchSelectVideos) self.ui.buttonSearchSelectFolder.clicked.connect( self.onButtonSearchSelectFolder) self.ui.buttonDownload.clicked.connect(self.onButtonDownload) self.ui.buttonPlay.clicked.connect(self.onButtonPlay) self.ui.buttonIMDB.clicked.connect(self.onViewOnlineInfo) self.ui.videoView.setContextMenuPolicy(Qt.CustomContextMenu) self.ui.buttonPlay.setEnabled(False) # Drag and Drop files to the videoView enabled # FIXME: enable drag events for videoView (and instructions view) self.ui.videoView.__class__.dragEnterEvent = self.dragEnterEvent self.ui.videoView.__class__.dragMoveEvent = self.dragEnterEvent self.ui.videoView.__class__.dropEvent = self.dropEvent self.ui.videoView.setAcceptDrops(1) self.retranslate() def retranslate(self): introduction = '<p align="center"><h2>{title}</h2></p>' \ '<p><b>{tab1header}</b><br/>{tab1content}</p>' \ '<p><b>{tab2header}</b><br/>{tab2content}</p>'\ '<p><b>{tab3header}</b><br/>{tab3content}</p>'.format( title=_('How To Use {title}').format(title=PROJECT_TITLE), tab1header=_('1st Tab:'), tab2header=_('2nd Tab:'), tab3header=_('3rd Tab:'), tab1content=_('Select, from the Folder Tree on the left, the folder which contains the videos ' 'that need subtitles. {project} will then try to automatically find available ' 'subtitles.').format(project=PROJECT_TITLE), tab2content=_('If you don\'t have the videos in your machine, you can search subtitles by ' 'introducing the title/name of the video.').format(project=PROJECT_TITLE), tab3content=_('If you have found some subtitle somewhere else that is not in {project}\'s database, ' 'please upload those subtitles so next users will be able to ' 'find them more easily.').format(project=PROJECT_TITLE)) self.ui.introductionHelp.setHtml(introduction) @pyqtSlot(Language) def on_interface_language_changed(self, language): self.ui.retranslateUi(self) self.retranslate() @pyqtSlot(str) def onFileModelDirectoryLoaded(self, path): lastDir = str(self._state.get_video_path()) qDirLastDir = QDir(lastDir) qDirLastDir.cdUp() if qDirLastDir.path() == path: index = self.fileModel.index(lastDir) proxyIndex = self.proxyFileModel.mapFromSource(index) self.ui.folderView.scrollTo(proxyIndex) self.ui.folderView.setCurrentIndex(proxyIndex) @pyqtSlot() def on_login_state_changed(self): log.debug('on_login_state_changed()') nb_connected = self._state.providers.get_number_connected_providers() if nb_connected: self.ui.buttonSearchSelectFolder.setEnabled(True) self.ui.buttonSearchSelectVideos.setEnabled(True) self.ui.buttonFind.setEnabled( self.get_current_selected_folder() is not None) else: self.ui.buttonSearchSelectFolder.setEnabled(False) self.ui.buttonSearchSelectVideos.setEnabled(False) self.ui.buttonFind.setEnabled(False) @pyqtSlot(Language) def on_language_combobox_filter_change(self, language): if language.is_generic(): self.language_filter_change.emit( self._state.get_download_languages()) else: self.language_filter_change.emit([language]) def on_permanent_language_filter_change(self, languages): selected_language = self.ui.filterLanguageForVideo.get_selected_language( ) if selected_language.is_generic(): self.language_filter_change.emit(languages) @pyqtSlot() def subtitlesCheckedChanged(self): subs = self.videoModel.get_checked_subtitles() if subs: self.ui.buttonDownload.setEnabled(True) else: self.ui.buttonDownload.setEnabled(False) def showInstructions(self): self.ui.stackedSearchResult.setCurrentWidget(self.ui.pageIntroduction) def hideInstructions(self): self.ui.stackedSearchResult.setCurrentWidget(self.ui.pageSearchResult) @pyqtSlot(QModelIndex) def onFolderTreeClicked(self, proxyIndex): """What to do when a Folder in the tree is clicked""" if not proxyIndex.isValid(): return index = self.proxyFileModel.mapToSource(proxyIndex) folder_path = self.fileModel.filePath(index) self._state.set_video_paths([folder_path]) def get_current_selected_folder(self): proxyIndex = self.ui.folderView.currentIndex() index = self.proxyFileModel.mapToSource(proxyIndex) folder_path = Path(self.fileModel.filePath(index)) if not folder_path: return None return folder_path def get_current_selected_item_videomodel(self): current_index = self.ui.videoView.currentIndex() return self.videoModel.getSelectedItem(current_index) @pyqtSlot() def onButtonFind(self): now = QTime.currentTime() if now < self.timeLastSearch.addMSecs(500): return folder_path = self.get_current_selected_folder() self._state.set_video_paths([folder_path]) self.search_videos([folder_path]) self.timeLastSearch = QTime.currentTime() @pyqtSlot() def onButtonRefresh(self): currentPath = self.get_current_selected_folder() self._refreshing = True self.ui.folderView.collapseAll() currentPath = self.get_current_selected_folder() if not currentPath: self._state.set_video_paths([currentPath]) index = self.fileModel.index(str(currentPath)) self.ui.folderView.scrollTo(self.proxyFileModel.mapFromSource(index)) @pyqtSlot(QModelIndex) def onFolderViewExpanded(self, proxyIndex): if self._refreshing: expandedPath = self.fileModel.filePath( self.proxyFileModel.mapToSource(proxyIndex)) if expandedPath == QDir.rootPath(): currentPath = self.get_current_selected_folder() if not currentPath: currentPath = self._state.get_video_path() index = self.fileModel.index(str(currentPath)) self.ui.folderView.scrollTo( self.proxyFileModel.mapFromSource(index)) self._refreshing = False @pyqtSlot() def onButtonSearchSelectFolder(self): paths = self._state.get_video_paths() path = paths[0] if paths else Path() selected_path = QFileDialog.getExistingDirectory( self, _('Select the directory that contains your videos'), str(path)) if selected_path: selected_paths = [Path(selected_path)] self._state.set_video_paths(selected_paths) self.search_videos(selected_paths) @pyqtSlot() def onButtonSearchSelectVideos(self): paths = self._state.get_video_paths() path = paths[0] if paths else Path() selected_files, t = QFileDialog.getOpenFileNames( self, _('Select the video(s) that need subtitles'), str(path), get_select_videos()) if selected_files: selected_files = list(Path(f) for f in selected_files) selected_dirs = list(set(p.parent for p in selected_files)) self._state.set_video_paths(selected_dirs) self.search_videos(selected_files) def search_videos(self, paths): if not self._state.providers.get_number_connected_providers(): QMessageBox.about( self, _('Error'), _('You are not connected to a server. Please connect first.')) return self.ui.buttonFind.setEnabled(False) self._search_videos_raw(paths) self.ui.buttonFind.setEnabled(True) def _search_videos_raw(self, paths): # FIXME: must pass mainwindow as argument to ProgressCallbackWidget callback = ProgressCallbackWidget(self) callback.set_title_text(_('Scanning...')) callback.set_label_text(_('Scanning files')) callback.set_finished_text(_('Scanning finished')) callback.set_block(True) callback.show() try: local_videos, local_subs = scan_videopaths(paths, callback=callback, recursive=True) except OSError: callback.cancel() QMessageBox.warning(self, _('Error'), _('Some directories are not accessible.')) if callback.canceled(): return callback.finish() log.debug('Videos found: {}'.format(local_videos)) log.debug('Subtitles found: {}'.format(local_subs)) self.hideInstructions() if not local_videos: QMessageBox.information(self, _('Scan Results'), _('No video has been found.')) return total = len(local_videos) # FIXME: must pass mainwindow as argument to ProgressCallbackWidget # callback = ProgressCallbackWidget(self) # callback.set_title_text(_('Asking Server...')) # callback.set_label_text(_('Searching subtitles...')) # callback.set_updated_text(_('Searching subtitles ( %d / %d )')) # callback.set_finished_text(_('Search finished')) callback.set_block(True) callback.set_range(0, total) callback.show() try: remote_subs = self._state.search_videos(local_videos, callback) except ProviderConnectionError: log.debug( 'Unable to search for subtitles of videos: videos={}'.format( list(v.get_filename() for v in local_videos))) QMessageBox.about(self, _('Error'), _('Unable to search for subtitles')) callback.finish() return self.videoModel.set_videos(local_videos) if remote_subs is None: QMessageBox.about( self, _('Error'), _('Error contacting the server. Please try again later')) callback.finish() # TODO: CHECK if our local subtitles are already in the server, otherwise suggest to upload @pyqtSlot() def onButtonPlay(self): selected_item = self.get_current_selected_item_videomodel() log.debug('Trying to play selected item: {}'.format(selected_item)) if selected_item is None: QMessageBox.warning(self, _('No subtitle selected'), _('Select a subtitle and try again')) return if isinstance(selected_item, SubtitleFileNetwork): selected_item = selected_item.get_subtitles()[0] if isinstance(selected_item, VideoFile): subtitle_file_path = None video = selected_item elif isinstance(selected_item, LocalSubtitleFile): subtitle_file_path = selected_item.get_filepath() video = selected_item.get_super_parent(VideoFile) elif isinstance(selected_item, RemoteSubtitleFile): video = selected_item.get_super_parent(VideoFile) subtitle_file_path = Path( tempfile.gettempdir()) / 'subdownloader.tmp.srt' log.debug('tmp path is {}'.format(subtitle_file_path)) log.debug( 'Temporary subtitle will be downloaded into: {temp_path}'. format(temp_path=subtitle_file_path)) # FIXME: must pass mainwindow as argument to ProgressCallbackWidget callback = ProgressCallbackWidget(self) callback.set_title_text(_('Playing video + sub')) callback.set_label_text(_('Downloading files...')) callback.set_finished_text(_('Downloading finished')) callback.set_block(True) callback.set_range(0, 100) callback.show() try: selected_item.download( subtitle_file_path, self._state.providers.get(selected_item.get_provider), callback) except ProviderConnectionError: log.debug('Unable to download subtitle "{}"'.format( selected_item.get_filename()), exc_info=sys.exc_info()) QMessageBox.about( self, _('Error'), _('Unable to download subtitle "{subtitle}"').format( subtitle=selected_item.get_filename())) callback.finish() return callback.finish() else: QMessageBox.about( self, _('Error'), '{}\n{}'.format(_('Unknown Error'), _('Please submit bug report'))) return # video = selected_item.get_parent().get_parent().get_parent() # FIXME: download subtitle with provider + use returned localSubtitleFile instead of creating one here if subtitle_file_path: local_subtitle = LocalSubtitleFile(subtitle_file_path) else: local_subtitle = None try: player = self._state.get_videoplayer() player.play_video(video, local_subtitle) except RuntimeError as e: QMessageBox.about(self, _('Error'), e.args[0]) @pyqtSlot(QModelIndex) def onClickVideoTreeView(self, index): data_item = self.videoModel.getSelectedItem(index) if isinstance(data_item, VideoFile): video = data_item if True: # video.getMovieInfo(): self.ui.buttonIMDB.setEnabled(True) self.ui.buttonIMDB.setIcon(QIcon(':/images/info.png')) self.ui.buttonIMDB.setText(_('Movie Info')) elif isinstance(data_item, RemoteSubtitleFile): self.ui.buttonIMDB.setEnabled(True) self.ui.buttonIMDB.setIcon( QIcon(':/images/sites/opensubtitles.png')) self.ui.buttonIMDB.setText(_('Subtitle Info')) else: self.ui.buttonIMDB.setEnabled(False) @pyqtSlot(QModelIndex) def onSelectVideoTreeView(self, index): data_item = self.videoModel.getSelectedItem(index) self.ui.buttonPlay.setEnabled(True) # if isinstance(data_item, SubtitleFile): # self.ui.buttonPlay.setEnabled(True) # else: # self.ui.buttonPlay.setEnabled(False) def onContext(self, point): # FIXME: code duplication with Main.onContext and/or SearchNameWidget and/or SearchFileWidget menu = QMenu('Menu', self) listview = self.ui.videoView index = listview.currentIndex() data_item = listview.model().getSelectedItem(index) if data_item is not None: if isinstance(data_item, VideoFile): video = data_item video_identities = video.get_identities() if any(video_identities.iter_imdb_identity()): online_action = QAction(QIcon(":/images/info.png"), _("View IMDb info"), self) online_action.triggered.connect(self.onViewOnlineInfo) else: online_action = QAction(QIcon(":/images/info.png"), _("Set IMDb info..."), self) online_action.triggered.connect(self.on_set_imdb_info) menu.addAction(online_action) elif isinstance(data_item, SubtitleFile): play_action = QAction(QIcon(":/images/play.png"), _("Play video + subtitle"), self) play_action.triggered.connect(self.onButtonPlay) menu.addAction(play_action) if isinstance(data_item, RemoteSubtitleFile): download_action = QAction(QIcon(":/images/download.png"), _("Download"), self) download_action.triggered.connect(self.onButtonDownload) menu.addAction(download_action) online_action = QAction( QIcon(":/images/sites/opensubtitles.png"), _("View online info"), self) online_action.triggered.connect(self.onViewOnlineInfo) menu.addAction(online_action) # Show the context menu. menu.exec_(listview.mapToGlobal(point)) def _create_choose_target_subtitle_path_cb(self): def callback(path, filename): selected_path = QFileDialog.getSaveFileName( self, _('Choose the target filename'), str(path / filename)) return selected_path return callback def onButtonDownload(self): # We download the subtitle in the same folder than the video rsubs = self.videoModel.get_checked_subtitles() sub_downloader = SubtitleDownloadProcess(parent=self.parent(), rsubtitles=rsubs, state=self._state, parent_add=True) sub_downloader.download_all() new_subs = sub_downloader.downloaded_subtitles() self.videoModel.uncheck_subtitles(new_subs) def onViewOnlineInfo(self): # FIXME: code duplication with Main.onContext and/or SearchNameWidget and/or SearchFileWidget # Tab for SearchByHash TODO:replace this 0 by an ENUM value listview = self.ui.videoView index = listview.currentIndex() data_item = self.videoModel.getSelectedItem(index) if isinstance(data_item, VideoFile): video = data_item video_identities = video.get_identities() if any(video_identities.iter_imdb_identity()): imdb_identity = next(video_identities.iter_imdb_identity()) webbrowser.open(imdb_identity.get_imdb_url(), new=2, autoraise=1) else: QMessageBox.information(self.parent(), _('imdb unknown'), _('imdb is unknown')) elif isinstance(data_item, RemoteSubtitleFile): sub = data_item webbrowser.open(sub.get_link(), new=2, autoraise=1) @pyqtSlot() def on_set_imdb_info(self): # FIXME: DUPLICATED WITH SEARCHNAMEWIDGET QMessageBox.about(self, _("Info"), "Not implemented yet. Sorry...")
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)