class EncounterPreviewTable(QTableView): def __init__(self, parent): super(EncounterPreviewTable, self).__init__(parent) self.model = EncounterInfoModel() self.sortModel = QSortFilterProxyModel() self.model.dataChanged.connect(self.compact) self.horizontalHeader().sectionResized.connect(self.calcHeader) self.horizontalHeader().sectionClicked.connect(self.model.setSortColumn) def setup(self, path): self.model.setup(path) self.sortModel.setSourceModel(self.model) self.setModel(self.sortModel) sortPoint = self.model.headers.index(self.model.TXT_DPS) order = Qt.DescendingOrder self.sortModel.sort(sortPoint, order) self.horizontalHeader().setSortIndicator(sortPoint,order) def compact(self): self.resizeColumnsToContents() self.resizeRowsToContents() def calcHeader(self): v: QHeaderView = self.horizontalHeader() totalheader = 0 headerEnds = [] for c in range (0, v.count()): totalheader += v.sectionSize(c) headerEnds.append(totalheader) self.model.totalWidth = totalheader self.model.headerWidths = headerEnds
class SortFilterTableView(QTableView): def __init__(self, parent=None): super().__init__(parent) self._proxy_model = QSortFilterProxyModel(self) self._proxy_model.setDynamicSortFilter(True) super().setModel(self._proxy_model) header = FilterHeader(self) header.filter_changed.connect(self.set_filter) self.setHorizontalHeader(header) self.setSortingEnabled(True) self.setSelectionMode(QAbstractItemView.ContiguousSelection) self.import_export_manager = ImportExportManager(self) self.import_export_manager.connect_custom_context_menu() def set_filter(self, section, filter_text): log.debug("set_filter(section: %s, filter: %r)", section, filter_text) self._proxy_model.setFilterWildcard(filter_text) self._proxy_model.setFilterKeyColumn(section) def setModel(self, model): self.horizontalHeader().set_filter_boxes(model.columnCount()) self._proxy_model.setSourceModel(model) self._proxy_model.sort(0, Qt.AscendingOrder) super().setModel(self._proxy_model) font = model.data(0, Qt.FontRole) if font is None: font = self.font() metrics = QFontMetrics(font) self.verticalHeader().setDefaultSectionSize(metrics.lineSpacing() * 1.5) self.horizontalHeader().setDefaultSectionSize(metrics.maxWidth() * 5)
class SqlRelationalTableModel(QtSql.QSqlRelationalTableModel): def __init__(self, model, parent, db): super().__init__(parent, db) self.model = model self._proxyModel = QSortFilterProxyModel(self) self._proxyModel.setSortLocaleAware(True) self._proxyModel.setSourceModel(self) def relationModel(self, _column): return self.model def data(self, index, role=Qt.DisplayRole): if role == Qt.DecorationRole: if index.row() < 0: return None iconIndex = self.index(index.row(), self.fieldIndex('icon')) if not self.data(iconIndex) or self.data(iconIndex).isNull(): return None icon = QPixmap() icon.loadFromData(self.data(iconIndex)) return icon return super().data(index, role) def proxyModel(self): return self._proxyModel def sort(self, sort=True): if sort: self._proxyModel.sort(self.fieldIndex('value')) else: self._proxyModel.sort(-1)
class SqlRelationalTableModel(QtSql.QSqlRelationalTableModel): def __init__(self, model, parent, db): super().__init__(parent, db) self.model = model self._proxyModel = QSortFilterProxyModel(self) self._proxyModel.setSortLocaleAware(True) self._proxyModel.setSourceModel(self) def relationModel(self, _column): return self.model def data(self, index, role=Qt.DisplayRole): if role == Qt.DecorationRole: if index.row() < 0: return None iconIndex = self.index(index.row(), self.fieldIndex('icon')) if not self.data(iconIndex) or self.data(iconIndex).isNull(): return None icon = QPixmap() icon.loadFromData(self.data(iconIndex)) return icon return super().data(index, role) def proxyModel(self): return self._proxyModel def sort(self, sort=True): if sort: self._proxyModel.sort(self.fieldIndex('value')) else: self._proxyModel.sort(-1)
class MainInputWidget(QWidget): def __init__(self, parent, base_model): super(MainInputWidget, self).__init__(parent) self.resize(1024, 768) self.base_model = base_model self.fr_model = QSortFilterProxyModel() self.fr_model.setSourceModel(FirstRoundModel(self.base_model)) self.playoff_model = PlayoffInputModel(self.base_model) self.final_round_model = QSortFilterProxyModel() self.final_round_model.setSourceModel(FinalRoundModel(self.base_model)) layout = QHBoxLayout(self) self.setLayout(layout) layout.addWidget(FirstRoundInputWidget(self, self.fr_model)) layout.addWidget(PlayoffInputWidget(self, self.playoff_model)) layout.addWidget(FinalRoundInputWidget(self, self.final_round_model)) self.setWindowTitle(config.TITLE) self.game_state = 0 def finish_first_round(self): self.playoff_model.initiate_pairing() def finish_playoff(self): self.final_round_model.sort(1, QtCore.Qt.DescendingOrder) def finish(self): for t, r in self.base_model.get_results(): r.save()
def filterListAlphaUpdate(self): ''' Updates filterListAlpha for sorting values and to send rows to videoTable ''' pmodel = QSortFilterProxyModel() pmodel.setSourceModel(self.filterListAlphaModel) pmodel.sort(3, Qt.AscendingOrder) self.filterListAlpha.setModel(pmodel) self.filterListAlpha.setModelColumn(3) self.filterListAlpha.show()
def main(): """The main entry point, compatible with setuptools entry points.""" app = QApplication(sys.argv) with open(os.path.join(os.path.dirname(__file__), 'testgui.ui')) as fobj: window = loadUi(fobj) model = GameListModel(get_games()) model_sorted = QSortFilterProxyModel() model_sorted.setDynamicSortFilter(True) model_sorted.setSortCaseSensitivity(Qt.CaseInsensitive) model_sorted.setSourceModel(model) model_sorted.sort(0, Qt.AscendingOrder) window.view_games.setModel(model_sorted) window.show() sys.exit(app.exec_())
class Example(QWidget): def __init__(self): super().__init__() self.setGeometry(300, 300, 350, 200) self.setWindowTitle("Sorting") self.initData() self.initUI() def initData(self): names = ["Jack", "Tom", "Lucy", "Bill", "Jane"] self.model = QStringListModel(names) self.model = QStringListModel(names) self.filterModel = QSortFilterProxyModel(self) self.filterModel.setSourceModel(self.model) def initUI(self): sortB = QPushButton("Sort", self) sortB.move(250, 20) self.sortType = QCheckBox("Ascending", self) self.sortType.move(250, 60) sortB.clicked.connect(self.sortItems) self.lv = QListView(self) self.lv.setModel(self.filterModel) self.lv.setGeometry(20, 20, 200, 150) def sortItems(self): checked = self.sortType.isChecked() if checked: self.filterModel.sort(FIRST_COLUMN, Qt.AscendingOrder) else: self.filterModel.sort(FIRST_COLUMN, Qt.DescendingOrder)
class SelectionDialog(CustomStandardDialog, Ui_SelectionDialog): def __init__(self, source_table, selection_mode=QAbstractItemView.ExtendedSelection, selection_behavior=QAbstractItemView.SelectItems): CustomStandardDialog.__init__(self) self.setupUi(self) self.source_table = source_table self.resize(self.dataView.horizontalHeader().width(), self.height()) self.proxyModel = QSortFilterProxyModel(self) self.proxyModel.setSourceModel(source_table) self.proxyModel.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) self.proxyModel.setFilterKeyColumn(-1) self.dataView.setModel(self.proxyModel) self.dataView.setSelectionBehavior(selection_behavior) self.dataView.setSelectionMode(selection_mode) self.proxyModel.sort(0) self.dataView.selectionModel().selectionChanged.connect( self.activate_button) self.dataView.doubleClicked.connect(self.accept) self.restore_dialog_geometry() def selected_items(self): return [ self.source_table.item_from_row(x) for x in self.dataView.get_selected_rows() ] @QtCore.pyqtSlot() def activate_button(self): self.buttonBox.button(QDialogButtonBox.Ok).setEnabled( len(self.dataView.selectedIndexes())) @QtCore.pyqtSlot(str) def update_filter(self, new): self.proxyModel.setFilterFixedString(new)
class DataFrameDialog(QDialog, Ui_DataFrameDialog): def __init__(self, dataframe): super(DataFrameDialog, self).__init__() self.setupUi(self) def dataitem(value): item = QStandardItem() try: item.setData(float(value), 2) except ValueError: item.setText(str(value)) return item self.datatable = QStandardItemModel(self) self.datatable.setColumnCount(dataframe.shape[1]) for row in dataframe.itertuples(index=True): self.datatable.appendRow([dataitem(x) for x in row]) self.datatable.setHorizontalHeaderLabels( ["Index"] + [x.title() for x in dataframe.axes[1]]) self.proxymodel = QSortFilterProxyModel(self) self.proxymodel.setSourceModel(self.datatable) self.tableView.setModel(self.proxymodel) self.proxymodel.setDynamicSortFilter(True) self.proxymodel.sort(0)
class IPTagsSelectDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.app = QApplication.instance() self.destTags = [] self.allTagsModel = UneditableStringListModel(self) self.destTagsModel = UneditableStringListModel(self) self.allTagsProxyModel = QSortFilterProxyModel(self) self.allTagsProxyModel.setSourceModel(self.allTagsModel) self.ui = ui_iptagsmanager.Ui_IPTagsDialog() self.ui.setupUi(self) self.ui.destTagsView.setModel(self.destTagsModel) self.ui.destTagsView.setEditTriggers(QAbstractItemView.NoEditTriggers) self.ui.destTagsView.doubleClicked.connect(self.onTagDoubleClicked) self.ui.addTagButton.clicked.connect(lambda: ensure(self.addTag())) self.ui.lineEditTag.textChanged.connect(self.onTagEditChanged) self.ui.lineEditTag.setValidator( QRegExpValidator(QRegExp(r'[A-Za-z0-9-_@#]+'))) self.ui.lineEditTag.setMaxLength(128) self.ui.tagItButton.clicked.connect(self.onTagObject) self.ui.untagItButton.clicked.connect(self.untagObject) self.ui.okButton.clicked.connect(lambda: ensure(self.validate())) self.ui.noTagsButton.clicked.connect(self.reject) self.setMinimumSize(self.app.desktopGeometry.width() / 2, (2 * self.app.desktopGeometry.height()) / 3) def onTagEditChanged(self, text): self.allTagsProxyModel.setFilterRegExp(text) self.ui.allTagsView.clearSelection() def onTagDoubleClicked(self, idx): ensure(self.tagObject([idx])) def onTagObject(self): ensure(self.tagObject()) def untagObject(self): try: for idx in self.ui.destTagsView.selectedIndexes(): tag = self.destTagsModel.data(idx, Qt.DisplayRole) if tag: tagList = self.destTagsModel.stringList() tagList.remove(tag) self.destTagsModel.setStringList(tagList) except Exception: pass async def tagObject(self, indexes=None): if indexes is None: indexes = self.ui.allTagsView.selectedIndexes() for idx in indexes: tag = self.allTagsProxyModel.data(idx, Qt.DisplayRole) if tag and tag not in self.destTagsModel.stringList(): self.destTagsModel.setStringList( self.destTagsModel.stringList() + [tag]) async def initDialog(self): await self.updateAllTags() async def addTag(self): tagname = self.ui.lineEditTag.text() if not tagname: return await database.ipTagAdd(ipTagsFormat(tagname)) self.ui.lineEditTag.clear() await self.updateAllTags() async def updateAllTags(self): tags = [t.name for t in await database.ipTagsAll()] self.allTagsModel.setStringList(tags) self.ui.allTagsView.setModel(self.allTagsProxyModel) self.allTagsProxyModel.sort(0) async def validate(self): self.destTags = self.destTagsModel.stringList() self.done(1)
class TileStampsDock(QDockWidget): setStamp = pyqtSignal(TileStamp) def __init__(self, stampManager, parent=None): super().__init__(parent) self.mTileStampManager = stampManager self.mTileStampModel = stampManager.tileStampModel() self.mProxyModel = QSortFilterProxyModel(self.mTileStampModel) self.mFilterEdit = QLineEdit(self) self.mNewStamp = QAction(self) self.mAddVariation = QAction(self) self.mDuplicate = QAction(self) self.mDelete = QAction(self) self.mChooseFolder = QAction(self) self.setObjectName("TileStampsDock") self.mProxyModel.setSortLocaleAware(True) self.mProxyModel.setSortCaseSensitivity(Qt.CaseInsensitive) self.mProxyModel.setFilterCaseSensitivity(Qt.CaseInsensitive) self.mProxyModel.setSourceModel(self.mTileStampModel) self.mProxyModel.sort(0) self.mTileStampView = TileStampView(self) self.mTileStampView.setModel(self.mProxyModel) self.mTileStampView.setVerticalScrollMode( QAbstractItemView.ScrollPerPixel) self.mTileStampView.header().setStretchLastSection(False) self.mTileStampView.header().setSectionResizeMode( 0, QHeaderView.Stretch) self.mTileStampView.header().setSectionResizeMode( 1, QHeaderView.ResizeToContents) self.mTileStampView.setContextMenuPolicy(Qt.CustomContextMenu) self.mTileStampView.customContextMenuRequested.connect( self.showContextMenu) self.mNewStamp.setIcon(QIcon(":images/16x16/document-new.png")) self.mAddVariation.setIcon(QIcon(":/images/16x16/add.png")) self.mDuplicate.setIcon(QIcon(":/images/16x16/stock-duplicate-16.png")) self.mDelete.setIcon(QIcon(":images/16x16/edit-delete.png")) self.mChooseFolder.setIcon(QIcon(":images/16x16/document-open.png")) Utils.setThemeIcon(self.mNewStamp, "document-new") Utils.setThemeIcon(self.mAddVariation, "add") Utils.setThemeIcon(self.mDelete, "edit-delete") Utils.setThemeIcon(self.mChooseFolder, "document-open") self.mFilterEdit.setClearButtonEnabled(True) self.mFilterEdit.textChanged.connect( self.mProxyModel.setFilterFixedString) self.mTileStampModel.stampRenamed.connect(self.ensureStampVisible) self.mNewStamp.triggered.connect(self.newStamp) self.mAddVariation.triggered.connect(self.addVariation) self.mDuplicate.triggered.connect(self.duplicate) self.mDelete.triggered.connect(self.delete_) self.mChooseFolder.triggered.connect(self.chooseFolder) self.mDuplicate.setEnabled(False) self.mDelete.setEnabled(False) self.mAddVariation.setEnabled(False) widget = QWidget(self) layout = QVBoxLayout(widget) layout.setContentsMargins(5, 5, 5, 5) buttonContainer = QToolBar() buttonContainer.setFloatable(False) buttonContainer.setMovable(False) buttonContainer.setIconSize(QSize(16, 16)) buttonContainer.addAction(self.mNewStamp) buttonContainer.addAction(self.mAddVariation) buttonContainer.addAction(self.mDuplicate) buttonContainer.addAction(self.mDelete) stretch = QWidget() stretch.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) buttonContainer.addWidget(stretch) buttonContainer.addAction(self.mChooseFolder) listAndToolBar = QVBoxLayout() listAndToolBar.setSpacing(0) listAndToolBar.addWidget(self.mFilterEdit) listAndToolBar.addWidget(self.mTileStampView) listAndToolBar.addWidget(buttonContainer) layout.addLayout(listAndToolBar) selectionModel = self.mTileStampView.selectionModel() selectionModel.currentRowChanged.connect(self.currentRowChanged) self.setWidget(widget) self.retranslateUi() def changeEvent(self, e): super().changeEvent(e) x = e.type() if x == QEvent.LanguageChange: self.retranslateUi() else: pass def keyPressEvent(self, event): x = event.key() if x == Qt.Key_Delete or x == Qt.Key_Backspace: self.delete_() return super().keyPressEvent(event) def currentRowChanged(self, index): sourceIndex = self.mProxyModel.mapToSource(index) isStamp = self.mTileStampModel.isStamp(sourceIndex) self.mDuplicate.setEnabled(isStamp) self.mDelete.setEnabled(sourceIndex.isValid()) self.mAddVariation.setEnabled(isStamp) if (isStamp): self.setStamp.emit(self.mTileStampModel.stampAt(sourceIndex)) else: variation = self.mTileStampModel.variationAt(sourceIndex) if variation: # single variation clicked, use it specifically self.setStamp.emit(TileStamp(Map(variation.map))) def showContextMenu(self, pos): index = self.mTileStampView.indexAt(pos) if (not index.isValid()): return menu = QMenu() sourceIndex = self.mProxyModel.mapToSource(index) if (self.mTileStampModel.isStamp(sourceIndex)): addStampVariation = QAction(self.mAddVariation.icon(), self.mAddVariation.text(), menu) deleteStamp = QAction(self.mDelete.icon(), self.tr("Delete Stamp"), menu) deleteStamp.triggered.connect(self.delete_) addStampVariation.triggered.connect(self.addVariation) menu.addAction(addStampVariation) menu.addSeparator() menu.addAction(deleteStamp) else: removeVariation = QAction(QIcon(":/images/16x16/remove.png"), self.tr("Remove Variation"), menu) Utils.setThemeIcon(removeVariation, "remove") removeVariation.triggered.connect(self.delete_) menu.addAction(removeVariation) menu.exec(self.mTileStampView.viewport().mapToGlobal(pos)) def newStamp(self): stamp = self.mTileStampManager.createStamp() if (self.isVisible() and not stamp.isEmpty()): stampIndex = self.mTileStampModel.index(stamp) if (stampIndex.isValid()): viewIndex = self.mProxyModel.mapFromSource(stampIndex) self.mTileStampView.setCurrentIndex(viewIndex) self.mTileStampView.edit(viewIndex) def delete_(self): index = self.mTileStampView.currentIndex() if (not index.isValid()): return sourceIndex = self.mProxyModel.mapToSource(index) self.mTileStampModel.removeRow(sourceIndex.row(), sourceIndex.parent()) def duplicate(self): index = self.mTileStampView.currentIndex() if (not index.isValid()): return sourceIndex = self.mProxyModel.mapToSource(index) if (not self.mTileStampModel.isStamp(sourceIndex)): return stamp = self.mTileStampModel.stampAt = TileStamp(sourceIndex) self.mTileStampModel.addStamp(stamp.clone()) def addVariation(self): index = self.mTileStampView.currentIndex() if (not index.isValid()): return sourceIndex = self.mProxyModel.mapToSource(index) if (not self.mTileStampModel.isStamp(sourceIndex)): return stamp = self.mTileStampModel.stampAt(sourceIndex) self.mTileStampManager.addVariation(stamp) def chooseFolder(self): prefs = Preferences.instance() stampsDirectory = prefs.stampsDirectory() stampsDirectory = QFileDialog.getExistingDirectory( self.window(), self.tr("Choose the Stamps Folder"), stampsDirectory) if (not stampsDirectory.isEmpty()): prefs.setStampsDirectory(stampsDirectory) def ensureStampVisible(self, stamp): stampIndex = self.mTileStampModel.index(stamp) if (stampIndex.isValid()): self.mTileStampView.scrollTo( self.mProxyModel.mapFromSource(stampIndex)) def retranslateUi(self): self.setWindowTitle(self.tr("Tile Stamps")) self.mNewStamp.setText(self.tr("Add New Stamp")) self.mAddVariation.setText(self.tr("Add Variation")) self.mDuplicate.setText(self.tr("Duplicate Stamp")) self.mDelete.setText(self.tr("Delete Selected")) self.mChooseFolder.setText(self.tr("Set Stamps Folder")) self.mFilterEdit.setPlaceholderText(self.tr("Filter"))
def sorted(self): proxy_model = QSortFilterProxyModel(self) proxy_model.setSourceModel(self) proxy_model.setDynamicSortFilter(True) proxy_model.sort(0) return proxy_model
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 = []
def sorted(self): proxy_model = QSortFilterProxyModel(self) proxy_model.setSourceModel(self) proxy_model.setDynamicSortFilter(True) proxy_model.sort(0) return proxy_model
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 SortFilterTableView(QTableView): def __init__(self, parent=None): super().__init__(parent) self._proxy_model = QSortFilterProxyModel(self) self._proxy_model.setDynamicSortFilter(True) super().setModel(self._proxy_model) header = FilterHeader(self) header.filter_changed.connect(self.set_filter) self.setHorizontalHeader(header) self.setSortingEnabled(True) self.setSelectionMode(QAbstractItemView.ContiguousSelection) self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.show_context_menu) self.import_export_manager = ImportExportManager(self) self.copy_action = create_action( None, "Copy", self.copy_selection_to_clipboard) def show_context_menu(self, point): self.import_export_manager.set_model_index(self.indexAt(point)) context_menu = QMenu() context_menu.addAction(self.copy_action) context_menu.addAction(self.import_export_manager.export_action) context_menu.addAction(self.import_export_manager.import_action) context_menu.exec(self.mapToGlobal(point)) def keyPressEvent(self, event: QKeyEvent): if event.type() == QKeyEvent.KeyPress \ and event.matches(QKeySequence.Copy): self.copy_selection_to_clipboard() else: super().keyPressEvent(event) def copy_selection_to_clipboard(self): selected_indexes = self.selectionModel().selectedIndexes() if not selected_indexes or len(selected_indexes) == 0: return model = self.model() result = "\n".join( "\t".join(row) for row in self.selected_rows(model, selected_indexes) ) cp = QApplication.clipboard() cp.setText(result) def selected_rows(self, model, selected_indexes): row = [] last_row = selected_indexes[0].row() for current in selected_indexes: value = str(model.data(current, Qt.DisplayRole)) if last_row != current.row(): yield row row = [value, ] else: row.append(value) last_row = current.row() def set_filter(self, section, filter_text): log.debug("set_filter(section: %s, filter: %r)", section, filter_text) self._proxy_model.setFilterWildcard(filter_text) self._proxy_model.setFilterKeyColumn(section) def setModel(self, model): self.horizontalHeader().set_filter_boxes(model.columnCount()) self._proxy_model.setSourceModel(model) self._proxy_model.sort(0, Qt.AscendingOrder) super().setModel(self._proxy_model) font = model.data(0, Qt.FontRole) if font is None: font = self.font() metrics = QFontMetrics(font) self.verticalHeader().setDefaultSectionSize(metrics.lineSpacing() * 1.5) self.horizontalHeader().setDefaultSectionSize(metrics.maxWidth() * 5)
class TileStampsDock(QDockWidget): setStamp = pyqtSignal(TileStamp) def __init__(self, stampManager, parent = None): super().__init__(parent) self.mTileStampManager = stampManager self.mTileStampModel = stampManager.tileStampModel() self.mProxyModel = QSortFilterProxyModel(self.mTileStampModel) self.mFilterEdit = QLineEdit(self) self.mNewStamp = QAction(self) self.mAddVariation = QAction(self) self.mDuplicate = QAction(self) self.mDelete = QAction(self) self.mChooseFolder = QAction(self) self.setObjectName("TileStampsDock") self.mProxyModel.setSortLocaleAware(True) self.mProxyModel.setSortCaseSensitivity(Qt.CaseInsensitive) self.mProxyModel.setFilterCaseSensitivity(Qt.CaseInsensitive) self.mProxyModel.setSourceModel(self.mTileStampModel) self.mProxyModel.sort(0) self.mTileStampView = TileStampView(self) self.mTileStampView.setModel(self.mProxyModel) self.mTileStampView.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel) self.mTileStampView.header().setStretchLastSection(False) self.mTileStampView.header().setSectionResizeMode(0, QHeaderView.Stretch) self.mTileStampView.header().setSectionResizeMode(1, QHeaderView.ResizeToContents) self.mTileStampView.setContextMenuPolicy(Qt.CustomContextMenu) self.mTileStampView.customContextMenuRequested.connect(self.showContextMenu) self.mNewStamp.setIcon(QIcon(":images/16x16/document-new.png")) self.mAddVariation.setIcon(QIcon(":/images/16x16/add.png")) self.mDuplicate.setIcon(QIcon(":/images/16x16/stock-duplicate-16.png")) self.mDelete.setIcon(QIcon(":images/16x16/edit-delete.png")) self.mChooseFolder.setIcon(QIcon(":images/16x16/document-open.png")) Utils.setThemeIcon(self.mNewStamp, "document-new") Utils.setThemeIcon(self.mAddVariation, "add") Utils.setThemeIcon(self.mDelete, "edit-delete") Utils.setThemeIcon(self.mChooseFolder, "document-open") self.mFilterEdit.setClearButtonEnabled(True) self.mFilterEdit.textChanged.connect(self.mProxyModel.setFilterFixedString) self.mTileStampModel.stampRenamed.connect(self.ensureStampVisible) self.mNewStamp.triggered.connect(self.newStamp) self.mAddVariation.triggered.connect(self.addVariation) self.mDuplicate.triggered.connect(self.duplicate) self.mDelete.triggered.connect(self.delete_) self.mChooseFolder.triggered.connect(self.chooseFolder) self.mDuplicate.setEnabled(False) self.mDelete.setEnabled(False) self.mAddVariation.setEnabled(False) widget = QWidget(self) layout = QVBoxLayout(widget) layout.setContentsMargins(5, 5, 5, 5) buttonContainer = QToolBar() buttonContainer.setFloatable(False) buttonContainer.setMovable(False) buttonContainer.setIconSize(QSize(16, 16)) buttonContainer.addAction(self.mNewStamp) buttonContainer.addAction(self.mAddVariation) buttonContainer.addAction(self.mDuplicate) buttonContainer.addAction(self.mDelete) stretch = QWidget() stretch.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) buttonContainer.addWidget(stretch) buttonContainer.addAction(self.mChooseFolder) listAndToolBar = QVBoxLayout() listAndToolBar.setSpacing(0) listAndToolBar.addWidget(self.mFilterEdit) listAndToolBar.addWidget(self.mTileStampView) listAndToolBar.addWidget(buttonContainer) layout.addLayout(listAndToolBar) selectionModel = self.mTileStampView.selectionModel() selectionModel.currentRowChanged.connect(self.currentRowChanged) self.setWidget(widget) self.retranslateUi() def changeEvent(self, e): super().changeEvent(e) x = e.type() if x==QEvent.LanguageChange: self.retranslateUi() else: pass def keyPressEvent(self, event): x = event.key() if x==Qt.Key_Delete or x==Qt.Key_Backspace: self.delete_() return super().keyPressEvent(event) def currentRowChanged(self, index): sourceIndex = self.mProxyModel.mapToSource(index) isStamp = self.mTileStampModel.isStamp(sourceIndex) self.mDuplicate.setEnabled(isStamp) self.mDelete.setEnabled(sourceIndex.isValid()) self.mAddVariation.setEnabled(isStamp) if (isStamp): self.setStamp.emit(self.mTileStampModel.stampAt(sourceIndex)) else: variation = self.mTileStampModel.variationAt(sourceIndex) if variation: # single variation clicked, use it specifically self.setStamp.emit(TileStamp(Map(variation.map))) def showContextMenu(self, pos): index = self.mTileStampView.indexAt(pos) if (not index.isValid()): return menu = QMenu() sourceIndex = self.mProxyModel.mapToSource(index) if (self.mTileStampModel.isStamp(sourceIndex)): addStampVariation = QAction(self.mAddVariation.icon(), self.mAddVariation.text(), menu) deleteStamp = QAction(self.mDelete.icon(), self.tr("Delete Stamp"), menu) deleteStamp.triggered.connect(self.delete_) addStampVariation.triggered.connect(self.addVariation) menu.addAction(addStampVariation) menu.addSeparator() menu.addAction(deleteStamp) else : removeVariation = QAction(QIcon(":/images/16x16/remove.png"), self.tr("Remove Variation"), menu) Utils.setThemeIcon(removeVariation, "remove") removeVariation.triggered.connect(self.delete_) menu.addAction(removeVariation) menu.exec(self.mTileStampView.viewport().mapToGlobal(pos)) def newStamp(self): stamp = self.mTileStampManager.createStamp() if (self.isVisible() and not stamp.isEmpty()): stampIndex = self.mTileStampModel.index(stamp) if (stampIndex.isValid()): viewIndex = self.mProxyModel.mapFromSource(stampIndex) self.mTileStampView.setCurrentIndex(viewIndex) self.mTileStampView.edit(viewIndex) def delete_(self): index = self.mTileStampView.currentIndex() if (not index.isValid()): return sourceIndex = self.mProxyModel.mapToSource(index) self.mTileStampModel.removeRow(sourceIndex.row(), sourceIndex.parent()) def duplicate(self): index = self.mTileStampView.currentIndex() if (not index.isValid()): return sourceIndex = self.mProxyModel.mapToSource(index) if (not self.mTileStampModel.isStamp(sourceIndex)): return stamp = self.mTileStampModel.stampAt = TileStamp(sourceIndex) self.mTileStampModel.addStamp(stamp.clone()) def addVariation(self): index = self.mTileStampView.currentIndex() if (not index.isValid()): return sourceIndex = self.mProxyModel.mapToSource(index) if (not self.mTileStampModel.isStamp(sourceIndex)): return stamp = self.mTileStampModel.stampAt(sourceIndex) self.mTileStampManager.addVariation(stamp) def chooseFolder(self): prefs = Preferences.instance() stampsDirectory = prefs.stampsDirectory() stampsDirectory = QFileDialog.getExistingDirectory(self.window(), self.tr("Choose the Stamps Folder"), stampsDirectory) if (not stampsDirectory.isEmpty()): prefs.setStampsDirectory(stampsDirectory) def ensureStampVisible(self, stamp): stampIndex = self.mTileStampModel.index(stamp) if (stampIndex.isValid()): self.mTileStampView.scrollTo(self.mProxyModel.mapFromSource(stampIndex)) def retranslateUi(self): self.setWindowTitle(self.tr("Tile Stamps")) self.mNewStamp.setText(self.tr("Add New Stamp")) self.mAddVariation.setText(self.tr("Add Variation")) self.mDuplicate.setText(self.tr("Duplicate Stamp")) self.mDelete.setText(self.tr("Delete Selected")) self.mChooseFolder.setText(self.tr("Set Stamps Folder")) self.mFilterEdit.setPlaceholderText(self.tr("Filter"))
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 Explorer(QWidget): """ This class implements the diagram predicate node explorer. """ def __init__(self, mainwindow): """ Initialize the Explorer. :type mainwindow: MainWindow """ super().__init__(mainwindow) self.expanded = {} self.searched = {} self.scrolled = {} self.mainview = None self.iconA = QIcon(':/icons/treeview-icon-attribute') self.iconC = QIcon(':/icons/treeview-icon-concept') self.iconD = QIcon(':/icons/treeview-icon-datarange') self.iconI = QIcon(':/icons/treeview-icon-instance') self.iconR = QIcon(':/icons/treeview-icon-role') self.iconV = QIcon(':/icons/treeview-icon-value') self.search = StringField(self) self.search.setAcceptDrops(False) self.search.setClearButtonEnabled(True) self.search.setPlaceholderText('Search...') self.search.setFixedHeight(30) self.model = QStandardItemModel(self) self.proxy = QSortFilterProxyModel(self) self.proxy.setDynamicSortFilter(False) self.proxy.setFilterCaseSensitivity(Qt.CaseInsensitive) self.proxy.setSortCaseSensitivity(Qt.CaseSensitive) self.proxy.setSourceModel(self.model) self.view = ExplorerView(mainwindow, self) self.view.setModel(self.proxy) self.mainLayout = QVBoxLayout(self) self.mainLayout.setContentsMargins(0, 0, 0, 0) self.mainLayout.addWidget(self.search) self.mainLayout.addWidget(self.view) self.setContentsMargins(0, 0, 0, 0) self.setMinimumWidth(216) self.setMinimumHeight(160) connect(self.view.doubleClicked, self.itemDoubleClicked) connect(self.view.pressed, self.itemPressed) connect(self.view.collapsed, self.itemCollapsed) connect(self.view.expanded, self.itemExpanded) connect(self.search.textChanged, self.filterItem) #################################################################################################################### # # # EVENTS # # # #################################################################################################################### def paintEvent(self, paintEvent): """ This is needed for the widget to pick the stylesheet. :type paintEvent: QPaintEvent """ option = QStyleOption() option.initFrom(self) painter = QPainter(self) style = self.style() style.drawPrimitive(QStyle.PE_Widget, option, painter, self) #################################################################################################################### # # # SLOTS # # # #################################################################################################################### @pyqtSlot('QGraphicsItem') def add(self, item): """ Add a node in the tree view. :type item: AbstractItem """ if item.node and item.predicate: parent = self.parentFor(item) if not parent: parent = ParentItem(item) parent.setIcon(self.iconFor(item)) self.model.appendRow(parent) self.proxy.sort(0, Qt.AscendingOrder) child = ChildItem(item) child.setData(item) parent.appendRow(child) self.proxy.sort(0, Qt.AscendingOrder) @pyqtSlot(str) def filterItem(self, key): """ Executed when the search box is filled with data. :type key: str """ if self.mainview: self.proxy.setFilterFixedString(key) self.proxy.sort(Qt.AscendingOrder) self.searched[self.mainview] = key @pyqtSlot('QModelIndex') def itemCollapsed(self, index): """ Executed when an item in the tree view is collapsed. :type index: QModelIndex """ if self.mainview: if self.mainview in self.expanded: item = self.model.itemFromIndex(self.proxy.mapToSource(index)) expanded = self.expanded[self.mainview] expanded.remove(item.text()) @pyqtSlot('QModelIndex') def itemDoubleClicked(self, index): """ Executed when an item in the tree view is double clicked. :type index: QModelIndex """ item = self.model.itemFromIndex(self.proxy.mapToSource(index)) node = item.data() if node: self.selectNode(node) self.focusNode(node) @pyqtSlot('QModelIndex') def itemExpanded(self, index): """ Executed when an item in the tree view is expanded. :type index: QModelIndex """ if self.mainview: item = self.model.itemFromIndex(self.proxy.mapToSource(index)) if self.mainview not in self.expanded: self.expanded[self.mainview] = set() expanded = self.expanded[self.mainview] expanded.add(item.text()) @pyqtSlot('QModelIndex') def itemPressed(self, index): """ Executed when an item in the tree view is clicked. :type index: QModelIndex """ item = self.model.itemFromIndex(self.proxy.mapToSource(index)) node = item.data() if node: self.selectNode(node) @pyqtSlot('QGraphicsItem') def remove(self, item): """ Remove a node from the tree view. :type item: AbstractItem """ if item.node and item.predicate: parent = self.parentFor(item) if parent: child = self.childFor(parent, item) if child: parent.removeRow(child.index().row()) if not parent.rowCount(): self.model.removeRow(parent.index().row()) #################################################################################################################### # # # AUXILIARY METHODS # # # #################################################################################################################### @staticmethod def childFor(parent, node): """ Search the item representing this node among parent children. :type parent: QStandardItem :type node: AbstractNode """ key = ChildItem.key(node) for i in range(parent.rowCount()): child = parent.child(i) if child.text() == key: return child return None def parentFor(self, node): """ Search the parent element of the given node. :type node: AbstractNode :rtype: QStandardItem """ key = ParentItem.key(node) for i in self.model.findItems(key, Qt.MatchExactly): n = i.child(0).data() if node.item is n.item: return i return None #################################################################################################################### # # # INTERFACE # # # #################################################################################################################### def browse(self, view): """ Set the widget to inspect the given view. :type view: MainView """ self.reset() self.mainview = view if self.mainview: scene = self.mainview.scene() connect(scene.index.sgnItemAdded, self.add) connect(scene.index.sgnItemRemoved, self.remove) for item in scene.index.nodes(): self.add(item) if self.mainview in self.expanded: expanded = self.expanded[self.mainview] for i in range(self.model.rowCount()): item = self.model.item(i) index = self.proxy.mapFromSource( self.model.indexFromItem(item)) self.view.setExpanded(index, item.text() in expanded) key = '' if self.mainview in self.searched: key = self.searched[self.mainview] self.search.setText(key) if self.mainview in self.scrolled: rect = self.rect() item = first(self.model.findItems( self.scrolled[self.mainview])) for i in range(self.model.rowCount()): self.view.scrollTo( self.proxy.mapFromSource( self.model.indexFromItem(self.model.item(i)))) index = self.proxy.mapToSource( self.view.indexAt(rect.topLeft())) if self.model.itemFromIndex(index) is item: break def reset(self): """ Clear the widget from inspecting the current view. """ if self.mainview: rect = self.rect() item = self.model.itemFromIndex( self.proxy.mapToSource(self.view.indexAt(rect.topLeft()))) if item: node = item.data() key = ParentItem.key(node) if node else item.text() self.scrolled[self.mainview] = key else: self.scrolled.pop(self.mainview, None) try: scene = self.mainview.scene() disconnect(scene.index.sgnItemAdded, self.add) disconnect(scene.index.sgnItemRemoved, self.remove) except RuntimeError: pass finally: self.mainview = None self.model.clear() def flush(self, view): """ Flush the cache of the given mainview. :type view: MainView """ self.expanded.pop(view, None) self.searched.pop(view, None) self.scrolled.pop(view, None) def iconFor(self, node): """ Returns the icon for the given node. :type node: """ if node.item is Item.AttributeNode: return self.iconA if node.item is Item.ConceptNode: return self.iconC if node.item is Item.ValueDomainNode: return self.iconD if node.item is Item.ValueRestrictionNode: return self.iconD if node.item is Item.IndividualNode: if node.identity is Identity.Instance: return self.iconI if node.identity is Identity.Value: return self.iconV if node.item is Item.RoleNode: return self.iconR def focusNode(self, node): """ Focus the given node in the main view. :type node: AbstractNode """ if self.mainview: self.mainview.centerOn(node) def selectNode(self, node): """ Select the given node in the main view. :type node: AbstractNode """ if self.mainview: scene = self.mainview.scene() scene.clearSelection() node.setSelected(True)
class MainApp(QtWidgets.QMainWindow, design.Ui_MainWindow): def bind_copy_actions(self) -> None: def copy_list(model: QStandardItemModel): def func(_): copy_model_to_clipboard(model) return func self.db_created_at_label.mousePressEvent = copy_list(self.db_created_at_list_model) self.db_columns_label.mousePressEvent = copy_list(self.db_columns_list_model) self.db_partitions_label.mousePressEvent = copy_list(self.db_partitions_list_model) self.db_based_on_label.mousePressEvent = copy_list(self.db_based_on_list_model) self.db_updated_at_label.mousePressEvent = copy_list(self.db_updated_at_list_model) self.db_used_in_label.mousePressEvent = copy_list(self.db_used_in_list_model) self.wf_predecessors_label.mousePressEvent = copy_list(self.wf_predecessors_list_model) self.wf_descendants_label.mousePressEvent = copy_list(self.wf_predecessors_list_model) self.wf_source_label.mousePressEvent = copy_list(self.wf_source_list_model) self.wf_effected_label.mousePressEvent = copy_list(self.wf_effected_list_model) def context_menu_requested(self, view: QListView, reverse_target: bool = False): def func(position): menu: QMenu = QMenu(self) related_tables_action: QAction = menu.addAction('Show related tables') related_tables_action.triggered.connect(self.show_related_tables(view, reverse_target)) menu.exec_(view.viewport().mapToGlobal(position)) return func def create_list_view_dialog(self) -> (QDialog, QStandardItemModel): dialog: QDialog = QDialog(self) dialog.setWindowTitle('Related tables') dialog.resize(800, 600) dialog.setMinimumSize(600, 400) layout: QVBoxLayout = QVBoxLayout(dialog) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) list_view: QListView = QListView() list_view.resize(400, 400) list_view_model: QStandardItemModel = QStandardItemModel(list_view) list_view.setModel(list_view_model) save_button: QPushButton = QPushButton('Copy to clipboard') save_button.clicked.connect(lambda _: copy_model_to_clipboard(list_view_model)) layout.addWidget(list_view) layout.addWidget(save_button) dialog.setLayout(layout) return dialog, list_view_model def show_related_tables(self, view: QListView, reverse_target: bool): def func(): base_wf: str = view.selectionModel().selectedIndexes()[0].data(Qt.DisplayRole) target_wf: str = self.current_workflow.name if reverse_target: target_wf = base_wf base_wf = self.current_workflow.name related_tables: List[str] = self.store.get_related_tables(target_wf, base_wf) dialog, list_view_model = self.create_list_view_dialog() for s in related_tables: item: QStandardItem = QStandardItem(s) item.setEditable(False) list_view_model.appendRow(item) dialog.exec_() return func def sort_by_text_and_color(self, search_box: QLineEdit, color_filter: List[Color], proxy_model: QSortFilterProxyModel, watch_unplugged: bool = False): unplugged_tables: List[str] = [] if watch_unplugged: unplugged_tables: List[str] = self.store.get_tables('', only_unplugged=True, only_names=True) def func(row: int, _) -> bool: model: QStandardItemModel = proxy_model.sourceModel() search_text: str = search_box.text() item: QStandardItem = model.item(row) text: str = item.text() if watch_unplugged and self.only_unplugged['value']: if text not in unplugged_tables: return False color = Color.from_q_color(item.foreground().color()) return search_text in text and (not len(color_filter) or color in color_filter) return func def select_workflows_directory(self) -> None: dialog: QFileDialog = QFileDialog(self, caption='Select workflows directory') self.directory_path = str(dialog.getExistingDirectory(dialog, 'Select workflows directory')) dialog.close() if self.directory_path: try: self.loading_progress.setValue(0) self.stackedWidget.setCurrentIndex(1) table_id_name_pairs: List[Tuple[int, str]] = self.store.get_tables(id_name_pairs=True) gen = parse_workflows_coroutine(self.directory_path, table_id_name_pairs) while True: progress: int = next(gen) self.loading_progress.setValue(progress) except StopIteration as ret: sqooped_tables, workflows, table_based_on, table_created_in, table_partitions, table_updated_in, table_used_in = ret.value workflows = [Workflow(*w_n) for w_n in workflows] self.store.insert_sqooped_tables(sqooped_tables) self.store.insert_workflows(workflows) self.store.insert_table_based_on(table_based_on) self.store.insert_table_created_in(table_created_in) self.store.insert_table_used_in(table_used_in) self.store.insert_table_updated_in(table_updated_in) self.store.insert_table_partitions(table_partitions) finally: self.stackedWidget.setCurrentIndex(0) self.wf_filter_workflows() self.set_menu_state() def insert_tables_from_schema_coroutine(self, tables_list: List[str]) -> bool: progress: int = 0 length: int = len(tables_list) self.store.delete_tables() for table_name in tables_list: without_schema: str = table_name.split('.')[1] tables: List[Table] = self.store.get_tables_by_names([table_name, without_schema]) if len(tables) != 0: for table in tables: table.name = table_name self.store.update_table(table) else: self.store.insert_new_table(table_name) progress += 1 yield int((progress / length * 100) + 1) return True def update_tables_columns_from_schema_coroutine(self, tables_dict: Dict[str, List[Tuple[str, str]]]) -> bool: progress: int = 0 length: int = len(list(tables_dict.keys())) tables: List[Table] = self.store.get_tables_by_names(list(tables_dict.keys())) for table in tables: if table.name in tables_dict: self.store.insert_table_columns([(table.index, t[0], t[1]) for t in tables_dict[table.name]]) # del tables_dict[table.name] progress += 1 yield int((progress / length * 100) + 1) # for table_name in tables_dict: # table: Table = self.store.insert_new_table(table_name) # self.store.insert_table_columns([(table.index, t[0], t[1]) for t in tables_dict[table.name]]) # progress += 1 # yield int((progress / length * 100) + 1) return True def extract_hive_schema(self) -> None: dialog: QFileDialog = QFileDialog(self, caption='Select hive schema file') schema_filepath: str = str(dialog.getOpenFileName(dialog, 'Select hive schema file')[0]) dialog.close() if schema_filepath: tables_list: List[str] = [] with open(schema_filepath, 'r') as file: for line in file.readlines(): if 'schema_name,table_name' in line: continue else: table_name: str = line.replace(' ', '').replace(',', '.').strip() tables_list.append(table_name) try: self.loading_progress.setValue(0) self.stackedWidget.setCurrentIndex(1) gen = self.insert_tables_from_schema_coroutine(tables_list) while True: progress: int = next(gen) self.loading_progress.setValue(progress) except StopIteration as ret: pass finally: self.stackedWidget.setCurrentIndex(0) self.db_filter_tables() self.set_menu_state() def extract_impala_schema(self) -> None: schema_filepath: str = str(QFileDialog.getOpenFileName(None, 'Select impala schema file')[0]) if schema_filepath: tables_dict: Dict[str, List[Tuple[str, str]]] = {} with open(schema_filepath, 'r') as file: for line in file.readlines(): if 'schema_name,table_name,field_name,field_type' in line: continue else: table: List[str] = line.split(',') table_name: str = table[0].strip() + '.' + table[1].strip() if table_name not in tables_dict: tables_dict[table_name] = [(table[2].strip(), table[3].strip().strip('"'))] else: tables_dict[table_name].append((table[2].strip(), table[3].strip().strip('"'))) try: self.loading_progress.setValue(0) self.stackedWidget.setCurrentIndex(1) gen = self.update_tables_columns_from_schema_coroutine(tables_dict) while True: progress: int = next(gen) self.loading_progress.setValue(progress) except StopIteration as ret: pass finally: self.stackedWidget.setCurrentIndex(0) self.db_filter_tables() self.set_menu_state() def clear_database(self) -> None: self.store.create_db_tables(force=True) self.set_menu_state() self.db_filter_tables() self.wf_filter_workflows() def wf_fill_workflows(self) -> None: for workflow in self.store.get_workflows(): color: QColor = QColor(Color.to_q_color(workflow.color)) brush: QBrush = QBrush(color) item = QStandardItem(workflow.name) item.setForeground(brush) item.setEditable(False) self.wf_workflow_list_model.appendRow(item) def wf_filter_workflows(self) -> None: self.wf_workflow_proxy_model.invalidateFilter() self.wf_workflow_proxy_model.sort(0) def wf_select_workflows(self) -> None: try: workflow_name: str = self.wf_workflow_list.selectionModel().selectedIndexes()[0].data(Qt.DisplayRole) workflow: Workflow = self.store.get_workflows(workflow_name)[0] self.store.populate_workflow_data(workflow) self.current_workflow = workflow self.fill_wf_fields() except IndexError: pass def fill_wf_fields(self) -> None: if self.current_workflow: self.wf_set_color(self.current_workflow.color) self.wf_effected_list_model.clear() self.wf_source_list_model.clear() self.wf_predecessors_list_model.clear() self.wf_descendants_list_model.clear() for ef_t in sorted(self.current_workflow.effected_tables): item = QStandardItem(ef_t) item.setEditable(False) self.wf_effected_list_model.appendRow(item) for src_t in sorted(self.current_workflow.source_tables): item = QStandardItem(src_t) item.setEditable(False) self.wf_source_list_model.appendRow(item) for p_w in sorted(self.current_workflow.predecessors): item = QStandardItem(p_w) item.setEditable(False) self.wf_predecessors_list_model.appendRow(item) for d_w in sorted(self.current_workflow.descendants): item = QStandardItem(d_w) item.setEditable(False) self.wf_descendants_list_model.appendRow(item) def save_wf_fields(self) -> None: if self.current_workflow: self.current_workflow.color = self.wf_get_color() self.store.update_workflow(self.current_workflow) change_item_color(self.wf_workflow_list_model, self.current_workflow.name, self.current_workflow.color) def db_fill_tables(self) -> None: self.db_table_list_model.clear() for table in self.store.get_tables(): color: QColor = QColor(Color.to_q_color(table.color)) brush: QBrush = QBrush(color) item = QStandardItem(table.name) item.setEditable(False) item.setForeground(brush) self.db_table_list_model.appendRow(item) def db_filter_tables(self) -> None: self.db_table_proxy_model.invalidateFilter() self.db_table_proxy_model.sort(0) def fill_db_fields(self) -> None: if self.current_table: self.store.populate_table_data(self.current_table) self.db_description_input.setText(self.current_table.meaning) self.db_authors_input.setText(self.current_table.authors) self.db_set_color(self.current_table.color) self.db_based_on_list_model.clear() self.db_created_at_list_model.clear() self.db_updated_at_list_model.clear() self.db_used_in_list_model.clear() self.db_partitions_list_model.clear() self.db_columns_list_model.clear() for s in sorted(self.current_table.first_based_on_tables): item = QStandardItem(s) font: QFont = item.font() font.setBold(True) item.setFont(font) item.setEditable(False) self.db_based_on_list_model.appendRow(item) for s in sorted(self.current_table.based_on_tables): item = QStandardItem(s) item.setEditable(False) self.db_based_on_list_model.appendRow(item) for s in sorted(self.current_table.created_in_workflows): item = QStandardItem(s) item.setEditable(False) self.db_created_at_list_model.appendRow(item) for s in sorted(self.current_table.updated_in_workflows): item = QStandardItem(s) item.setEditable(False) self.db_updated_at_list_model.appendRow(item) for s in sorted(self.current_table.used_in_workflows): item = QStandardItem(s) item.setEditable(False) self.db_used_in_list_model.appendRow(item) for s in sorted(self.current_table.partitions): item = QStandardItem(s) item.setEditable(False) self.db_partitions_list_model.appendRow(item) for s in self.current_table.columns: item = QStandardItem(s) item.setEditable(False) self.db_columns_list_model.appendRow(item) def save_db_fields(self) -> None: if self.current_table: self.current_table.meaning = self.db_description_input.toPlainText() self.current_table.authors = self.db_authors_input.toPlainText() self.current_table.color = self.db_get_color() self.store.update_table(self.current_table) change_item_color(self.db_table_list_model, self.current_table.name, self.current_table.color) def db_set_color(self, color: Color): if color is Color.RED: self.db_red_color_button.setChecked(True) self.db_blue_color_button.setChecked(False) self.db_magenta_color_button.setChecked(False) self.db_green_color_button.setChecked(False) self.db_none_color_button.setChecked(False) elif color is Color.BLUE: self.db_red_color_button.setChecked(False) self.db_blue_color_button.setChecked(True) self.db_magenta_color_button.setChecked(False) self.db_green_color_button.setChecked(False) self.db_none_color_button.setChecked(False) elif color is Color.MAGENTA: self.db_red_color_button.setChecked(False) self.db_blue_color_button.setChecked(False) self.db_magenta_color_button.setChecked(True) self.db_green_color_button.setChecked(False) self.db_none_color_button.setChecked(False) elif color is Color.GREEN: self.db_red_color_button.setChecked(False) self.db_blue_color_button.setChecked(False) self.db_magenta_color_button.setChecked(False) self.db_green_color_button.setChecked(True) self.db_none_color_button.setChecked(False) else: self.db_red_color_button.setChecked(False) self.db_blue_color_button.setChecked(False) self.db_magenta_color_button.setChecked(False) self.db_green_color_button.setChecked(False) self.db_none_color_button.setChecked(True) def wf_set_color(self, color: Color): if color is Color.RED: self.wf_red_color_button.setChecked(True) self.wf_blue_color_button.setChecked(False) self.wf_magenta_color_button.setChecked(False) self.wf_green_color_button.setChecked(False) self.wf_none_color_button.setChecked(False) elif color is Color.BLUE: self.wf_red_color_button.setChecked(False) self.wf_blue_color_button.setChecked(True) self.wf_magenta_color_button.setChecked(False) self.wf_green_color_button.setChecked(False) self.wf_none_color_button.setChecked(False) elif color is Color.MAGENTA: self.wf_red_color_button.setChecked(False) self.wf_blue_color_button.setChecked(False) self.wf_magenta_color_button.setChecked(True) self.wf_green_color_button.setChecked(False) self.wf_none_color_button.setChecked(False) elif color is Color.GREEN: self.wf_red_color_button.setChecked(False) self.wf_blue_color_button.setChecked(False) self.wf_magenta_color_button.setChecked(False) self.wf_green_color_button.setChecked(True) self.wf_none_color_button.setChecked(False) else: self.wf_red_color_button.setChecked(False) self.wf_blue_color_button.setChecked(False) self.wf_magenta_color_button.setChecked(False) self.wf_green_color_button.setChecked(False) self.wf_none_color_button.setChecked(True) def db_get_color(self) -> Color: if self.db_red_color_button.isChecked(): return Color.RED elif self.db_blue_color_button.isChecked(): return Color.BLUE elif self.db_green_color_button.isChecked(): return Color.GREEN elif self.db_magenta_color_button.isChecked(): return Color.MAGENTA elif self.db_none_color_button.isChecked(): return Color.NONE def wf_get_color(self) -> Color: if self.wf_red_color_button.isChecked(): return Color.RED elif self.wf_blue_color_button.isChecked(): return Color.BLUE elif self.wf_green_color_button.isChecked(): return Color.GREEN elif self.wf_magenta_color_button.isChecked(): return Color.MAGENTA elif self.wf_none_color_button.isChecked(): return Color.NONE def db_select_tables(self) -> None: try: table_name: str = self.db_table_list.selectionModel().selectedIndexes()[0].data(Qt.DisplayRole) table: Table = self.store.get_tables(table_name)[0] self.current_table = table self.fill_db_fields() except IndexError: pass def db_change_tables_filter(self, v: bool): self.only_unplugged['value'] = v self.db_filter_tables() def db_toggle_color_filter(self, color: Color): def func(value): if value: self.db_color_filter.append(color) else: try: self.db_color_filter.remove(color) except ValueError: pass self.db_filter_tables() return func def wf_toggle_color_filter(self, color: Color): def func(value): if value: self.wf_color_filter.append(color) else: try: self.wf_color_filter.remove(color) except ValueError: pass self.wf_filter_workflows() return func def set_menu_state(self): status: str = self.store.get_db_status() if status == 'db_empty': self.action_extract_hive.setEnabled(True) self.action_exctract_impala.setEnabled(False) self.action_open_workflows.setEnabled(False) elif status == 'hive_extracted': self.action_extract_hive.setEnabled(False) self.action_exctract_impala.setEnabled(True) self.action_open_workflows.setEnabled(False) elif status == 'impala_extracted': self.action_extract_hive.setEnabled(False) self.action_exctract_impala.setEnabled(False) self.action_open_workflows.setEnabled(True) else: self.action_extract_hive.setEnabled(False) self.action_exctract_impala.setEnabled(False) self.action_open_workflows.setEnabled(False) def export_list(self): tab_id: int = self.tabWidget.currentIndex() if tab_id == 0: copy_model_to_clipboard(self.db_table_list_model) elif tab_id == 1: copy_model_to_clipboard(self.wf_workflow_list_model) def __init__(self): super().__init__() self.store: Store = Store('db.sqlite3') self.directory_path: str = None self.current_table: Table = None self.current_workflow: Workflow = None self.only_unplugged: Dict[str, bool] = {'value': False} self.db_color_filter: List[Color] = [] self.wf_color_filter: List[Color] = [] self.setupUi(self) self.set_menu_state() self.wf_workflow_list_model = QStandardItemModel(self) self.wf_workflow_proxy_model = QSortFilterProxyModel(self) self.wf_workflow_proxy_model.setSourceModel(self.wf_workflow_list_model) self.wf_workflow_list.setModel(self.wf_workflow_proxy_model) self.wf_workflow_proxy_model.filterAcceptsRow = self.sort_by_text_and_color(self.wf_workflow_search, self.wf_color_filter, self.wf_workflow_proxy_model ) self.wf_workflow_proxy_model.lessThan = less_than_name_color(self.wf_workflow_proxy_model) self.wf_fill_workflows() self.wf_workflow_proxy_model.sort(0) self.wf_save_button.clicked.connect(self.save_wf_fields) self.wf_source_list_model = QStandardItemModel(self.wf_source_list) self.wf_source_list.setModel(self.wf_source_list_model) self.wf_effected_list_model = QStandardItemModel(self.wf_effected_list) self.wf_effected_list.setModel(self.wf_effected_list_model) self.wf_predecessors_list_model = QStandardItemModel(self.wf_predecessors_list) self.wf_predecessors_list.setModel(self.wf_predecessors_list_model) self.wf_predecessors_list.setContextMenuPolicy(Qt.CustomContextMenu) self.wf_predecessors_list.customContextMenuRequested.connect( self.context_menu_requested(self.wf_predecessors_list)) self.wf_descendants_list_model = QStandardItemModel(self.wf_descendants_list) self.wf_descendants_list.setModel(self.wf_descendants_list_model) self.wf_descendants_list.setContextMenuPolicy(Qt.CustomContextMenu) self.wf_descendants_list.customContextMenuRequested.connect( self.context_menu_requested(self.wf_descendants_list, reverse_target=True)) self.action_open_workflows.triggered.connect(self.select_workflows_directory) self.action_extract_hive.triggered.connect(self.extract_hive_schema) self.action_exctract_impala.triggered.connect(self.extract_impala_schema) self.action_clear_database.triggered.connect(self.clear_database) self.action_copy_list_to_clipboard.triggered.connect(self.export_list) self.wf_workflow_search.textChanged.connect(self.wf_filter_workflows) self.wf_workflow_list.selectionModel().selectionChanged.connect(self.wf_select_workflows) self.wf_blue_color_filter.stateChanged.connect(self.wf_toggle_color_filter(Color.BLUE)) self.wf_green_color_filter.stateChanged.connect(self.wf_toggle_color_filter(Color.GREEN)) self.wf_red_color_filter.stateChanged.connect(self.wf_toggle_color_filter(Color.RED)) self.wf_magenta_color_filter.stateChanged.connect(self.wf_toggle_color_filter(Color.MAGENTA)) self.wf_none_color_filter.stateChanged.connect(self.wf_toggle_color_filter(Color.NONE)) self.db_table_list_model = QStandardItemModel(self) self.db_table_proxy_model = QSortFilterProxyModel(self) self.db_table_proxy_model.setSourceModel(self.db_table_list_model) self.db_table_list.setModel(self.db_table_proxy_model) self.db_table_proxy_model.filterAcceptsRow = self.sort_by_text_and_color(self.db_table_search, self.db_color_filter, self.db_table_proxy_model, watch_unplugged=True) self.db_table_proxy_model.lessThan = less_than_name_color(self.db_table_proxy_model) self.db_fill_tables() self.db_table_proxy_model.sort(0) self.db_created_at_list_model = QStandardItemModel(self.db_created_at_list) self.db_created_at_list.setModel(self.db_created_at_list_model) self.db_updated_at_list_model = QStandardItemModel(self.db_updated_at_list) self.db_updated_at_list.setModel(self.db_updated_at_list_model) self.db_based_on_list_model = QStandardItemModel(self.db_based_on_list) self.db_based_on_list.setModel(self.db_based_on_list_model) self.db_used_in_list_model = QStandardItemModel(self.db_used_in_list) self.db_used_in_list.setModel(self.db_used_in_list_model) self.db_partitions_list_model = QStandardItemModel(self.db_partitions_list) self.db_partitions_list.setModel(self.db_partitions_list_model) self.db_columns_list_model = QStandardItemModel(self.db_columns_list) self.db_columns_list.setModel(self.db_columns_list_model) self.db_table_search.textChanged.connect(self.db_filter_tables) self.db_table_list.selectionModel().selectionChanged.connect(self.db_select_tables) self.db_save_button.clicked.connect(self.save_db_fields) self.db_show_only_unplugged.stateChanged.connect(self.db_change_tables_filter) self.db_blue_color_filter.stateChanged.connect(self.db_toggle_color_filter(Color.BLUE)) self.db_green_color_filter.stateChanged.connect(self.db_toggle_color_filter(Color.GREEN)) self.db_red_color_filter.stateChanged.connect(self.db_toggle_color_filter(Color.RED)) self.db_magenta_color_filter.stateChanged.connect(self.db_toggle_color_filter(Color.MAGENTA)) self.db_none_color_filter.stateChanged.connect(self.db_toggle_color_filter(Color.NONE)) self.bind_copy_actions()