def fillOptions(self, frameLayout: QtWidgets.QVBoxLayout, fullState: FullState) -> None: frameLayout.addWidget(QtWidgets.QLabel("Options for " + self.name)) self.formParent = QtWidgets.QWidget(frameLayout.parentWidget()) self.formLayout = QtWidgets.QFormLayout(self.formParent) # One forced widget on all - whether or not to include in analysis. self.inclusionCheck = QtWidgets.QCheckBox(self.formParent) k = self._includeKey() currentState = fullState.projectOptions.analysisOptions self.inclusionCheck.setChecked(currentState[k] if k in currentState else True) self._addFormRow("Include in analysis?", self.inclusionCheck) self.fillOptionsInner(currentState, fullState, self.formParent) frameLayout.addWidget(self.formParent)
class ImageSearchScraperApp(QMainWindow): searchCountUpdated = pyqtSignal() clientCountUpdated = pyqtSignal() def __init__(self): super().__init__() self.title = __appname__ self.left = 100 self.top = 100 self.width = 1024 self.height = 768 self.defaultSaveDir = str(Path.home()).replace('\\', '/') self.searchCount = 0 self.clientCount = 0 self.initUI() def initUI(self): """ Initialize the user interface """ # Main layout centralWidget = QWidget(self) self.setCentralWidget(centralWidget) scrollAreaContent = QWidget() listLayout = QVBoxLayout() # Search Clients layout self.clientsLayout = QVBoxLayout() firstSearchClient = SearchClient(SupportedSearchClients.GOOGLE_API, self.defaultSaveDir) self.clientsLayout.addWidget(firstSearchClient) self.clientCount = 1 # update client count self.searchCount = 1 listLayout.addLayout(self.clientsLayout) # Layout to add additional API search clients apiPlusLayout = QHBoxLayout() apiPlusLayout.setAlignment(Qt.AlignRight) addGoogleAPI = ImageButton('add-google-api', 49, 32) addGoogleAPI.setToolTip('Add Google Custom Search Engine API Instance') apiPlusLayout.addWidget(addGoogleAPI) addBingAPI = ImageButton('add-bing-api', 49, 32) addBingAPI.setToolTip('Add Bing Image Search API Instance') apiPlusLayout.addWidget(addBingAPI) addGoogleScraper = ImageButton('add-google-scraper', 49, 32) addGoogleScraper.setToolTip('Add Google Image Scraper Instance') apiPlusLayout.addWidget(addGoogleScraper) # apiPlusLayout.addStretch(1) listLayout.addLayout(apiPlusLayout) listLayout.addStretch(1) # Scroll area scroll = QScrollArea() scroll.setWidget(scrollAreaContent) scroll.setWidgetResizable(True) scroll.setFrameShape(QFrame.NoFrame) scroll.setContentsMargins(0, 0, 0, 0) scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) scroll.setMinimumWidth(250) scrollLayout = QVBoxLayout() scrollLayout.addWidget(scroll) scrollAreaContent.setLayout(listLayout) centralWidget.setLayout(scrollLayout) centralWidget.layout().setContentsMargins(0, 0, 0, 0) # Toolbar self.toolbar = ToolBar() self.addToolBar(Qt.TopToolBarArea, self.toolbar) # Dock widgets self.downloadProgressDock = ProgressDock('Queries Download Progress') self.addDockWidget(Qt.RightDockWidgetArea, self.downloadProgressDock) # Connect signals and slots addGoogleScraper.clicked.connect(lambda state, x=SupportedSearchClients .GOOGLE: self.addSearchClient(x)) self.searchCountUpdated.connect(self.updateToolbar) self.clientCountUpdated.connect(self.updateToolbar) addGoogleAPI.clicked.connect(lambda state, x=SupportedSearchClients. GOOGLE_API: self.addSearchClient(x)) addBingAPI.clicked.connect(lambda state, x=SupportedSearchClients. BING_API: self.addSearchClient(x)) self.toolbar.setDefaultDirectory.connect(self.setDefaultSaveDirectory) self.toolbar.searchAll.connect(self.searchAllQueries) self.toolbar.deleteAll.connect(self.removeAllSearchClients) self.toolbar.clearCompleted.connect( self.downloadProgressDock.removeCompleted) self.downloadProgressDock.completed[bool].connect( lambda state: self.toolbar.setClearProgressDockEnabled(state)) firstSearchClient.delete.connect(self.decrementClientCount) firstSearchClient.search[str, str, str, str, int].connect(self.startGoogleAPISearchTask) firstSearchClient.searchCountUpdated[int, str].connect( self.updateSearchCount) # Window settings self.setWindowTitle(self.title) self.setGeometry(self.left, self.top, self.width, self.height) self.setWindowIcon(newIcon('app')) self.center() self.show() def startSearchTask(self, client: SupportedSearchClients, directory: str, query: str, numImages: int, apiKey: str = None, cseID: str = None): """ Start an image search and download in a worker thread, and start tracking its download progress """ self.downloadProgressDock.addProgressItem( reduceString(directory + '/' + query)) idx = self.downloadProgressDock.getItemCount() - 1 self.thread = QThread(self) self.searchTask = SearchTask(idx, client, directory, query, numImages, apiKey=apiKey, cseID=cseID) self.searchTask.moveToThread(self.thread) self.searchTask.finished.connect(self.thread.terminate) self.searchTask.updateProgress[int, float].connect( self.downloadProgressDock.setProgressItemValue) self.thread.started.connect(self.searchTask.executeSearchTask) self.thread.start() def center(self): """ Utility function to center the UI window on a user's screen """ qtRectangle = self.frameGeometry() centerPoint = QDesktopWidget().availableGeometry().center() qtRectangle.moveCenter(centerPoint) self.move(qtRectangle.topLeft()) def closeEvent(self, event): """ QMainWindow closeEvent handler """ mboxtitle = 'Exit' mboxmsg = 'Are you sure you want to quit?' reply = QMessageBox.warning(self, mboxtitle, mboxmsg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply == QMessageBox.Yes: event.accept() else: event.ignore() self.show() @pyqtSlot(str, str, int) def startGoogleSearchTask(self, directory: str, query: str, numImages: int): """ Start Google Image Scraper search task """ self.startSearchTask(SupportedSearchClients.GOOGLE, directory + '/google', query, numImages) @pyqtSlot(str, str, str, int) def startBingAPISearchTask(self, apiKey: str, directory: str, query: str, numImages: int): """ Start Bing Image Search API search task """ self.startSearchTask(SupportedSearchClients.BING_API, directory + '/bing-api', query, numImages, apiKey) @pyqtSlot(str, str, str, str, int) def startGoogleAPISearchTask(self, apiKey: str, cseID: str, directory: str, query: str, numImages: int): """ Start Google Custom Search JSON API search task """ self.startSearchTask(SupportedSearchClients.GOOGLE_API, directory + '/google-api', query, numImages, apiKey, cseID) @pyqtSlot() def removeAllSearchClients(self): """ Remove all search client instances in the UI """ mboxtitle = 'Delete All' mboxmsg = 'Are you sure you want to delete all search client instances and their corresponding queries?' reply = QMessageBox.warning(self, mboxtitle, mboxmsg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply == QMessageBox.Yes: for s in self.clientsLayout.parentWidget().findChildren( SearchClient): s.destroy() @pyqtSlot() def searchAllQueries(self): """ Start a search and download for all the non-empty queries """ for s in self.clientsLayout.parentWidget().findChildren(SearchBox): if s.searchQuery(): s.searchButton.click() @pyqtSlot() def setDefaultSaveDirectory(self): """ Set the default save root directory for all queries """ selected = QFileDialog.getExistingDirectory( self, 'Save images to the directory', self.defaultSaveDir, QFileDialog.ShowDirsOnly) if selected: self.defaultSaveDir = selected for s in self.clientsLayout.parentWidget().findChildren( SearchClient): s.setDefaultSaveDirectory(self.defaultSaveDir) @pyqtSlot(SupportedSearchClients) def addSearchClient(self, client: SupportedSearchClients): """ Add a search client widget to the UI """ self.clientCount += 1 self.searchCount += 1 clientWidget = SearchClient(client, self.defaultSaveDir) self.clientsLayout.addWidget(clientWidget) clientWidget.delete.connect(self.decrementClientCount) clientWidget.searchCountUpdated[int, str].connect(self.updateSearchCount) if client == SupportedSearchClients.GOOGLE_API: clientWidget.search[str, str, str, str, int].connect(self.startGoogleAPISearchTask) elif client == SupportedSearchClients.BING_API: clientWidget.search[str, str, str, int].connect(self.startBingAPISearchTask) elif client == SupportedSearchClients.GOOGLE: clientWidget.search[str, str, int].connect(self.startGoogleSearchTask) self.clientCountUpdated.emit() @pyqtSlot() def decrementClientCount(self): """ Decrement the client count """ self.clientCount -= 1 self.clientCountUpdated.emit() @pyqtSlot(int, str) def updateSearchCount(self, count: int, action: str): """ Update the search boxes count based on action (add/remove) """ if action == 'added': self.searchCount += count elif action == 'removed': self.searchCount -= count # print(f'Search count: {self.searchCount} ({action} {count})') self.searchCountUpdated.emit() @pyqtSlot() def updateToolbar(self): """ Update the toolbar buttons state (enabled or disabled) """ self.toolbar.setSearchAllEnabled(self.searchCount > 0) self.toolbar.setDeleteAllEnabled(self.clientCount > 0)
class SearchClient(QGroupBox): """ Search Client groupbox widget layout """ delete = pyqtSignal() searchCountUpdated = pyqtSignal(int, str) search = pyqtSignal([str, str, str, str, int], [str, str, str, int], [str, str, int]) _titles = { SupportedSearchClients.GOOGLE: 'Google Image Scraper', SupportedSearchClients.GOOGLE_API: 'Google Custom Search JSON API', SupportedSearchClients.BING_API: 'Bing Image Search API v7' } def __init__(self, client: SupportedSearchClients, saveDirectory: str, parent: QWidget = None): super().__init__(self._titles[client], parent) self.client = client self.searchCount = 1 self.maxResults = 25000 self.defaultSaveDirectory = saveDirectory self.setContentsMargins(11, 3, 0, 11) # Main vertical layout self.mainLayout = QVBoxLayout() self.mainLayout.setAlignment(Qt.AlignTop) self.mainLayout.setContentsMargins(0, 0, 0, 0) # Delete button and its layout deleteLayout = QHBoxLayout() deleteLayout.setAlignment(Qt.AlignRight) deleteLayout.setContentsMargins(0, 6, 0, 0) self.deleteButton = DeleteButton() deleteLayout.addWidget(self.deleteButton) self.mainLayout.addLayout(deleteLayout) # Client layout clientLayout = QVBoxLayout() clientLayout.setAlignment(Qt.AlignTop) clientLayout.setContentsMargins(0, 0, 11, 0) self.mainLayout.addLayout(clientLayout) # API keys text boxes if self.client == SupportedSearchClients.GOOGLE_API or self.client == SupportedSearchClients.BING_API: keysLayout = QHBoxLayout() self.apiKey = TextBox('Enter your API key here...') self.apiKey.textChanged.connect(self.updateSearchBoxEnabledState) keysLayout.addWidget(self.apiKey) if self.client == SupportedSearchClients.GOOGLE_API: self.cseID = TextBox('Google Custom Search Engine ID...') self.cseID.textChanged.connect( self.updateSearchBoxEnabledState) keysLayout.addWidget(self.cseID) self.maxResults = 100 keysLayout.addStretch(1) clientLayout.addLayout(keysLayout) # Initial search related widgets state self.initState = self.client != SupportedSearchClients.GOOGLE_API and self.client != SupportedSearchClients.BING_API # Search queries layout self.queriesLayout = QVBoxLayout() # Add search box searchBox = SearchBox(self.maxResults, f'Query #{self.searchCount}', self.defaultSaveDirectory) searchBox.setEnabled(self.initState) self.queriesLayout.addWidget(searchBox) clientLayout.addLayout(self.queriesLayout) # Add plus icon self.addQuery = PlusIcon('Query', size=20) self.addQuery.setEnabled(self.initState) clientLayout.addWidget(self.addQuery) clientLayout.addStretch(1) self.setLayout(self.mainLayout) # Connect signals and slots self.deleteButton.clicked.connect(self.destroy) searchBox.delete.connect(self.updateSearchBoxTitles) searchBox.search[str, str, int].connect(self.searchRequest) self.addQuery.add.connect(self.addSearchBox) def addSearchBox(self): self.searchCount += 1 searchBox = SearchBox(self.maxResults, f'Query #{self.searchCount}', self.defaultSaveDirectory) self.queriesLayout.addWidget(searchBox) searchBox.delete.connect(self.updateSearchBoxTitles) searchBox.search[str, str, int].connect(self.searchRequest) self.searchCountUpdated.emit(1, 'added') def setDefaultSaveDirectory(self, directory: str): self.defaultSaveDirectory = directory for s in self.queriesLayout.parentWidget().findChildren(SearchBox): s.setSaveDirectory(directory) def _clearLayout(self, layout): if layout is not None: while layout.count(): item = layout.takeAt(0) widget = item.widget() if widget is not None: widget.deleteLater() else: self._clearLayout(item.layout()) @pyqtSlot(str, str, int) def searchRequest(self, directory: str, query: str, numImages: int): if self.client == SupportedSearchClients.GOOGLE_API: self.search[str, str, str, str, int].emit(self.apiKey.toPlainText(), self.cseID.toPlainText(), directory, query, numImages) elif self.client == SupportedSearchClients.BING_API: self.search[str, str, str, int].emit(self.apiKey.toPlainText(), directory, query, numImages) elif self.client == SupportedSearchClients.GOOGLE: self.search[str, str, int].emit(directory, query, numImages) @pyqtSlot() def updateSearchBoxEnabledState(self): if self.client == SupportedSearchClients.GOOGLE_API: # print(len(self.apiKey.toPlainText()), len(self.cseID.toPlainText())) state = len(self.apiKey.toPlainText()) == 39 and len( self.cseID.toPlainText()) == 33 elif self.client == SupportedSearchClients.BING_API: state = len(self.apiKey.toPlainText()) == 39 for s in self.queriesLayout.parentWidget().findChildren(SearchBox): s.setEnabled(state) self.addQuery.setEnabled(state) @pyqtSlot() def updateSearchBoxTitles(self): self.searchCount -= 1 i = self.searchCount - 1 for s in self.queriesLayout.parentWidget().findChildren(SearchBox): if not s.deleteInProgress: s.setProperties(f'Query #{self.searchCount - i}') i -= 1 self.searchCountUpdated.emit(1, 'removed') # removed one search box @pyqtSlot() def destroy(self): # Delete all items in the main layout self._clearLayout(self.mainLayout) # Delete self self.deleteLater() self.delete.emit() self.searchCountUpdated.emit(self.searchCount, 'removed')