class Example_Window(QWidgets.QWidget): def __init__(self): super(QWidgets.QWidget, self).__init__() self.initUI() def initUI(self): self.button = QWidgets.QPushButton("Start Spinner") # + self.button.clicked.connect(self.toggle_spinner) self.spinner = QtWaitingSpinner(self, centerOnParent=False) self.grid = QWidgets.QGridLayout() self.grid.addWidget(self.button, 0, 0) # self.grid.addWidget(self.spinner,0,1) # --- self.grid.addWidget(self.spinner, 0, 1, 1, 2) # +++ <--- self.setLayout(self.grid) self.show() def toggle_spinner(self): if self.spinner.isSpinning(): self.spinner.stop() self.button.setText("Start Spinner") # + else: self.spinner.start() self.button.setText("Stop Spinner") # +
class Dialog(QDialog): def __init__(self, *args, **kwargs): QDialog.__init__(self, *args, **kwargs) self.setLayout(QVBoxLayout()) btn = QPushButton("Submit", self) btn.clicked.connect(self.submit) self.spinner = QtWaitingSpinner(self) self.layout().addWidget(btn) self.layout().addWidget(self.spinner) def submit(self): self.spinner.start() runnable = RequestRunnable("https://api.github.com/some/endpoint", {'some': 'data'}, self) QThreadPool.globalInstance().start(runnable) @pyqtSlot(str) def setData(self, data): print(data) self.spinner.stop() self.adjustSize()
class Demo(QWidget): sb_roundness = None sb_opacity = None sb_fadeperc = None sb_lines = None sb_line_length = None sb_line_width = None sb_inner_radius = None sb_rev_s = None btn_start = None btn_stop = None btn_pick_color = None spinner = None def __init__(self): super().__init__() self.init_ui() def init_ui(self): grid = QGridLayout() groupbox1 = QGroupBox() groupbox1_layout = QHBoxLayout() groupbox2 = QGroupBox() groupbox2_layout = QGridLayout() button_hbox = QHBoxLayout() self.setLayout(grid) self.setWindowTitle("QtWaitingSpinner Demo") self.setWindowFlags(Qt.Dialog) # SPINNER self.spinner = QtWaitingSpinner(self) # Spinboxes self.sb_roundness = QDoubleSpinBox() self.sb_opacity = QDoubleSpinBox() self.sb_fadeperc = QDoubleSpinBox() self.sb_lines = QSpinBox() self.sb_line_length = QDoubleSpinBox() self.sb_line_width = QDoubleSpinBox() self.sb_inner_radius = QDoubleSpinBox() self.sb_rev_s = QDoubleSpinBox() # set spinbox default values self.sb_roundness.setValue(70) self.sb_roundness.setRange(0, 9999) self.sb_opacity.setValue(15) self.sb_opacity.setRange(0, 9999) self.sb_fadeperc.setValue(70) self.sb_fadeperc.setRange(0, 9999) self.sb_lines.setValue(12) self.sb_lines.setRange(1, 9999) self.sb_line_length.setValue(10) self.sb_line_length.setRange(0, 9999) self.sb_line_width.setValue(5) self.sb_line_width.setRange(0, 9999) self.sb_inner_radius.setValue(10) self.sb_inner_radius.setRange(0, 9999) self.sb_rev_s.setValue(1) self.sb_rev_s.setRange(0.1, 9999) # Buttons self.btn_start = QPushButton("Start") self.btn_stop = QPushButton("Stop") self.btn_pick_color = QPushButton("Pick Color") # Connects self.sb_roundness.valueChanged.connect(self.set_roundness) self.sb_opacity.valueChanged.connect(self.set_opacity) self.sb_fadeperc.valueChanged.connect(self.set_fadeperc) self.sb_lines.valueChanged.connect(self.set_lines) self.sb_line_length.valueChanged.connect(self.set_line_length) self.sb_line_width.valueChanged.connect(self.set_line_width) self.sb_inner_radius.valueChanged.connect(self.set_inner_radius) self.sb_rev_s.valueChanged.connect(self.set_rev_s) self.btn_start.clicked.connect(self.spinner_start) self.btn_stop.clicked.connect(self.spinner_stop) self.btn_pick_color.clicked.connect(self.show_color_picker) # Layout adds groupbox1_layout.addWidget(self.spinner) groupbox1.setLayout(groupbox1_layout) groupbox2_layout.addWidget(QLabel("Roundness:"), *(1, 1)) groupbox2_layout.addWidget(self.sb_roundness, *(1, 2)) groupbox2_layout.addWidget(QLabel("Opacity:"), *(2, 1)) groupbox2_layout.addWidget(self.sb_opacity, *(2, 2)) groupbox2_layout.addWidget(QLabel("Fade Perc:"), *(3, 1)) groupbox2_layout.addWidget(self.sb_fadeperc, *(3, 2)) groupbox2_layout.addWidget(QLabel("Lines:"), *(4, 1)) groupbox2_layout.addWidget(self.sb_lines, *(4, 2)) groupbox2_layout.addWidget(QLabel("Line Length:"), *(5, 1)) groupbox2_layout.addWidget(self.sb_line_length, *(5, 2)) groupbox2_layout.addWidget(QLabel("Line Width:"), *(6, 1)) groupbox2_layout.addWidget(self.sb_line_width, *(6, 2)) groupbox2_layout.addWidget(QLabel("Inner Radius:"), *(7, 1)) groupbox2_layout.addWidget(self.sb_inner_radius, *(7, 2)) groupbox2_layout.addWidget(QLabel("Rev/s:"), *(8, 1)) groupbox2_layout.addWidget(self.sb_rev_s, *(8, 2)) groupbox2.setLayout(groupbox2_layout) button_hbox.addWidget(self.btn_start) button_hbox.addWidget(self.btn_stop) button_hbox.addWidget(self.btn_pick_color) grid.addWidget(groupbox1, *(1, 1)) grid.addWidget(groupbox2, *(1, 2)) grid.addLayout(button_hbox, *(2, 1)) self.spinner.start() self.show() def set_roundness(self): self.spinner.setRoundness(self.sb_roundness.value()) def set_opacity(self): self.spinner.setMinimumTrailOpacity(self.sb_opacity.value()) def set_fadeperc(self): self.spinner.setTrailFadePercentage(self.sb_fadeperc.value()) def set_lines(self): self.spinner.setNumberOfLines(self.sb_lines.value()) def set_line_length(self): self.spinner.setLineLength(self.sb_line_length.value()) def set_line_width(self): self.spinner.setLineWidth(self.sb_line_width.value()) def set_inner_radius(self): self.spinner.setInnerRadius(self.sb_inner_radius.value()) def set_rev_s(self): self.spinner.setRevolutionsPerSecond(self.sb_rev_s.value()) def spinner_start(self): self.spinner.start() def spinner_stop(self): self.spinner.stop() def show_color_picker(self): self.spinner.setColor(QColorDialog.getColor())
class Window(QWidget): sig_abort_workers = pyqtSignal() def __init__(self, val, screenwidth, screenheight, parent=None): super().__init__(parent) self.setWindowTitle("qtube") app_exit_shortcuts = ["Ctrl+Q", "Ctrl+W"] for sc in app_exit_shortcuts: exitshortcut = QShortcut(QKeySequence(sc), self) exitshortcut.activated.connect(self.exit_seq) backshortcut = QShortcut(QKeySequence('Alt+Left'), self) backshortcut.activated.connect(self.on_back_clicked) self.setStyleSheet("background-color: " + BACKGROUND_COLOR + ";") self.mygroupbox = QGroupBox('') self.mygroupbox.setStyleSheet("color: " + FOREGROUND_COLOR + "; font-family: " + FONT + "; font-style: italic") self.myform = QFormLayout() labellist = [] combolist = [] self.mygroupbox.setLayout(self.myform) self.scroll = QScrollArea() self.scroll.setWidget(self.mygroupbox) self.scroll.setWidgetResizable(True) self.scroll.setStyleSheet("color: " + FOREGROUND_COLOR + ";") self.history = { 'urls': [], 'title_boxes': [], 'data': [], 'page_numbers': [] } self.downloaded_videos = {'paths': [], 'short_titles': []} self.search = '' self.spinner = QtWaitingSpinner(self, False) self.spinner.setRoundness(70.0) self.spinner.setMinimumTrailOpacity(15.0) self.spinner.setTrailFadePercentage(70.0) self.spinner.setNumberOfLines(10) self.spinner.setLineLength(10) self.spinner.setLineWidth(4) self.spinner.setInnerRadius(4) self.spinner.setRevolutionsPerSecond(1.5) self.spinner.setColor(QColor(FOREGROUND_COLOR)) # multi-threading QThread.currentThread().setObjectName( 'main') # threads can be named, useful for log output self.__workers_done = [] self.__threads = [] self.line = QLineEdit(self) self.line.returnPressed.connect(self.clickMethod) self.line.setStyleSheet("color: " + FOREGROUND_COLOR + "; background-color: " + BACKGROUND_COLOR + "; border: 1px solid " + FOREGROUND_COLOR + "; font-family: " + FONT + ";") active_buttons = [] self.inactive_buttons = [] self.search_button = QPushButton() self.search_button.setText('Search') self.search_button.clicked.connect(self.clickMethod) active_buttons.append(self.search_button) self.home_button = QPushButton() self.home_button.setText('Home') self.home_button.clicked.connect(self.on_home_clicked) self.inactive_buttons.append(self.home_button) self.play_playlist_button = QPushButton() self.play_playlist_button.setText('Play All') self.play_playlist_button.clicked.connect( self.on_play_playlist_clicked) active_buttons.append(self.play_playlist_button) self.back_button = QPushButton() self.back_button.setText('Back') self.back_button.clicked.connect(self.on_back_clicked) self.inactive_buttons.append(self.back_button) for b in active_buttons: b.setStyleSheet("color: " + FOREGROUND_COLOR + "; background-color: " + BACKGROUND_COLOR + "; border: 1px solid " + FOREGROUND_COLOR + "; font-family: " + FONT + ";") b.setCursor(Qt.PointingHandCursor) for b in self.inactive_buttons: b.setStyleSheet("color: " + INACTIVE_COLOR + "; background-color: " + BACKGROUND_COLOR + "; border: 1px solid " + INACTIVE_COLOR + "; font-family: " + FONT + ";") self.download_label = QLabel() self.download_label.setText('0 downloads') self.download_label.setMaximumSize(QSize(110, 20)) self.download_label.setStyleSheet("color: " + INACTIVE_COLOR + "; background-color: " + BACKGROUND_COLOR + "; font-family: " + FONT + ";") self.download_selector = QComboBox() self.download_selector.setStyleSheet("color: " + INACTIVE_COLOR + "; background-color: " + BACKGROUND_COLOR + "; font-family: " + FONT + ";") self.download_selector.currentIndexChanged.connect( self.select_download) self.download_to_play = '' self.play_downloaded_button = QPushButton() self.play_downloaded_button.setText('Play') self.play_downloaded_button.clicked.connect(self.on_play_downloaded) self.play_downloaded_button.setMaximumSize(QSize(50, 20)) self.play_downloaded_button.setStyleSheet("color: " + INACTIVE_COLOR + "; background-color: " + BACKGROUND_COLOR + "; border: 1px solid " + INACTIVE_COLOR + "; font-family: " + FONT + ";") self.container = VideoContainer(PLAYER_SIZE) self.container.setAttribute(Qt.WA_DontCreateNativeAncestors) self.container.setAttribute(Qt.WA_NativeWindow) #self.container.sig_height.connect(self.resizeWindow) self.player = mpv.MPV( wid=str(int(self.container.winId())), ytdl=True, input_default_bindings=True, input_vo_keyboard=True, keep_open=True, reset_on_next_file='pause', osd_bar=True, ) script_dir = str(Path.home()) + '/.config/mpv/scripts/' [ self.player.command('load-script', script_dir + script) for script in os.listdir(script_dir) ] player_exit_shortcuts = ['q', 'ctrl+q', 'ctrl+w'] for sc in player_exit_shortcuts: self.player.register_key_binding(sc, self.exit_seq) self.player.register_key_binding('f', self.fullscreen) self.player.register_key_binding('esc', self.fullscreen_off) self.isFullScreen = False self.player.register_key_binding('WHEEL_LEFT', 'seek 1') self.player.register_key_binding('WHEEL_RIGHT', 'seek -1') searchbarlayout = QHBoxLayout() searchbarlayout.addWidget(self.line) searchbarlayout.addWidget(self.search_button) searchbarlayout.addWidget(self.spinner) searchbar = QWidget() searchbar.setLayout(searchbarlayout) buttonrowlayout = QHBoxLayout() buttonrowlayout.addWidget(self.back_button) buttonrowlayout.addWidget(self.home_button) buttonrowlayout.addWidget(self.play_playlist_button) buttonrow = QWidget() buttonrow.setLayout(buttonrowlayout) downloadrowlayout = QHBoxLayout() downloadrowlayout.addWidget(self.download_label) downloadrowlayout.addWidget(self.download_selector) downloadrowlayout.addWidget(self.play_downloaded_button) downloadrow = QWidget() downloadrow.setLayout(downloadrowlayout) sublayout = QVBoxLayout() sublayout.addWidget(searchbar) sublayout.addWidget(buttonrow) sublayout.addWidget(self.scroll) sublayout.addWidget(downloadrow) self.left = QWidget() self.left.setLayout(sublayout) self.left.setFixedWidth(LIST_WIDTH) biglayout = QHBoxLayout(self) biglayout.addWidget(self.left) biglayout.addWidget(self.container) biglayout.setContentsMargins(0, 0, 0, 0) self.move(int((screenwidth - PLAYER_SIZE.width() - LIST_WIDTH) / 2), int((screenheight - PLAYER_SIZE.height()) / 2)) # load home page data self.spinner.start() idx = 'Home' worker = Worker(idx, HOME_URL, search=False) thread = QThread() thread.setObjectName('thread_' + idx) worker.moveToThread(thread) worker.sig_data.connect(self.on_click_data_received) self.sig_abort_workers.connect(worker.abort) thread.started.connect(worker.grabData) thread.start() self.__threads.append((thread, worker)) def fullscreen(self, blank, blank2): if not self.isFullScreen: self.left.setFixedWidth(0) self.showFullScreen() self.isFullScreen = True time.sleep(.5) def fullscreen_off(self, blank, blank2): if self.isFullScreen: self.showNormal() self.left.setFixedWidth(LIST_WIDTH) self.isFullScreen = False time.sleep(.5) # @pyqtSlot(int) # def resizeWindow(self, newheight): # self.setFixedHeight(newheight) def clickMethod(self): self.spinner.start() self.search = self.line.text() print('searching "' + self.search + '"...') idx = 'clickMethod' worker = Worker(idx, self.search) thread = QThread() thread.setObjectName('thread_' + idx) self.__threads.append((thread, worker)) worker.moveToThread(thread) worker.sig_data.connect(self.on_click_data_received) self.sig_abort_workers.connect(worker.abort) thread.started.connect(worker.grabData) thread.start() @pyqtSlot(dict) def on_click_data_received(self, data): self.data = sort_dict_lists_by_list(data, 'titles') search_term = self.search if len(self.history['data']) == 0: title_box = data['page_title'] elif len(search_term) > 25: title_box = 'results: "' + search_term[:22] + '..."' else: title_box = 'results: "' + search_term + '"' if len(self.history['data']) > 0: for b in self.inactive_buttons: b.setStyleSheet("color: " + FOREGROUND_COLOR + "; background-color: " + BACKGROUND_COLOR + "; border: 1px solid " + FOREGROUND_COLOR + "; font-family: " + FONT + ";") b.setCursor(Qt.PointingHandCursor) self.history['data'].append(self.data) self.history['title_boxes'].append(title_box) self.history['urls'].append(self.data['playlist_url']) self.history['page_numbers'].append(1) self.populate() groupbox = QGroupBox(title_box) groupbox.setLayout(self.myform) groupbox.setStyleSheet("color: " + FOREGROUND_COLOR + "; font-family: " + FONT + ";font-style: italic") self.scroll.setWidget(groupbox) self.spinner.stop() def populate(self): labellist = [] combolist = [] form = QFormLayout() for i, img in enumerate(self.data['thumb_paths']): if self.data['titles'][i] is not None: title = '\n'.join( textwrap.wrap(self.data['titles'][i], TEXT_LENGTH)[:2]) if len(self.data['titles'][i]) > TEXT_LENGTH * 2: title = title + '...' else: # catch errors from youtube-dl failing to capture video title title = '[TITLE MISSING]' text = title + '\n' + self.data['durations'][ i] + ' | ' + self.data['dates'][i] + '\n' + self.data['views'][ i] + ' views | ' + self.data['ratings'][i] + ' likes' descLabel = DescriptionLabel(self.data['urls'][i], self.data['titles'][i]) descLabel.setText(text) descLabel.setMaximumWidth(TEXT_WIDTH) descLabel.video_clicked.connect(self.on_video_clicked) descLabel.download_clicked.connect(self.on_download_clicked) labellist.append(descLabel) imagelabel = ImageLabel(self.data['urls'][i], self.data['titles'][i]) pixmap = QPixmap(img) pixmap = pixmap.scaled(THUMB_SIZE, FLAGS) imagelabel.setPixmap(pixmap) imagelabel.video_clicked.connect(self.on_video_clicked) imagelabel.download_clicked.connect(self.on_download_clicked) combolist.append(imagelabel) # form.addRow(combolist[i], labellist[i]) number_of_pages = math.ceil(self.data['total_videos'] / NUM_RESULTS) if number_of_pages > 1: current_page = self.history['page_numbers'][-1] pages = [] if current_page <= 3 or number_of_pages <= 5: page_range = ['<'] page_range.extend([i for i in range(1, number_of_pages + 1)]) page_range.append('>') else: page_range = ['<', 1] if number_of_pages - current_page < 3: page_range.extend([ i for i in range(number_of_pages - 3, number_of_pages + 1) ]) else: page_range.extend([i + current_page - 1 for i in range(4)]) page_range.append('>') for i in page_range: active = (i != current_page ) and not (i == '<' and current_page == 1) and not ( i == '>' and current_page == number_of_pages) page = PageLabel(i, active) page.page_clicked.connect(self.get_next_page) pages.append(page) layout = QHBoxLayout() for p in pages: layout.addWidget(p) page_selector = QWidget() page_selector.setLayout(layout) form.addRow(QLabel('Pages: '), page_selector) self.myform = form def exit_seq(self, blank=None, blank2=None): app.quit() def on_video_clicked(self): label = self.sender() self.url = label.url self.player.play(self.url) #self.player.command('show-text', label.title) def on_download_clicked(self): label = self.sender() idx = label.title worker = Worker(idx, label.url, label=self.download_label) thread = QThread() thread.setObjectName('thread_' + idx) self.__threads.append( (thread, worker)) # need to store worker too otherwise will be gc'd worker.moveToThread(thread) worker.sig_msg.connect(self.on_download_complete) self.sig_abort_workers.connect(worker.abort) # get read to start worker: thread.started.connect(worker.download) thread.start( ) # this will emit 'started' and start thread's event loop @pyqtSlot(str) def on_download_complete(self, title): title_short = title[:20] vid_path = [ DOWNLOAD_LOCATION + file for file in os.listdir(DOWNLOAD_LOCATION) if file.startswith(title) ][0] self.downloaded_videos['short_titles'].append(title_short) self.downloaded_videos['paths'].append(vid_path) self.download_label.setText( str(len(self.downloaded_videos['paths'])) + ' downloads') self.download_label.setStyleSheet("color: " + FOREGROUND_COLOR + "; background-color: " + BACKGROUND_COLOR + "; font-family: " + FONT + ";") self.download_selector.insertItem(0, title_short, vid_path) self.download_selector.setStyleSheet("color: " + FOREGROUND_COLOR + "; background-color: " + BACKGROUND_COLOR + "; font-family: " + FONT + ";") self.download_selector.setCurrentIndex(0) self.play_downloaded_button.setStyleSheet("color: " + FOREGROUND_COLOR + "; background-color: " + BACKGROUND_COLOR + "; border: 1px solid " + FOREGROUND_COLOR + "; font-family: " + FONT + ";") self.play_downloaded_button.setCursor(Qt.PointingHandCursor) def on_home_clicked(self): if not (HOME_URL in self.history['urls'][-1] and self.history['page_numbers'][-1] == 1): print('loading homepage...') self.search = '' self.data = self.history['data'][0] self.history['data'].append(self.data) self.history['title_boxes'].append(self.data['page_title']) self.history['urls'].append(self.data['playlist_url']) self.history['page_numbers'].append(1) self.populate() groupbox = QGroupBox(self.data['page_title']) groupbox.setLayout(self.myform) groupbox.setStyleSheet("color: " + FOREGROUND_COLOR + "; font-family: " + FONT + ";font-style: italic") self.scroll.setWidget(groupbox) self.home_button.setStyleSheet("color: " + INACTIVE_COLOR + "; background-color: " + BACKGROUND_COLOR + "; border: 1px solid " + INACTIVE_COLOR + "; font-family: " + FONT + ";") self.home_button.setCursor(Qt.ArrowCursor) else: print('already home') def on_play_playlist_clicked(self): self.url = self.history['urls'][-1] self.player.play(self.url) #TODO: add mpv options to limit playlist items to number of search results def on_back_clicked(self): if len(self.history['urls']) > 1: #self.search = '' self.history['urls'].pop(-1) self.history['page_numbers'].pop(-1) self.history['data'].pop(-1) self.data = self.history['data'][-1] self.populate() self.history['title_boxes'].pop(-1) groupbox = QGroupBox(self.history['title_boxes'][-1]) groupbox.setLayout(self.myform) groupbox.setStyleSheet("color: " + FOREGROUND_COLOR + "; font-family: " + FONT + ";font-style: italic") self.scroll.setWidget(groupbox) print('returning to page ' + self.history['urls'][-1] + '...') if len(self.history['urls']) == 1: for b in self.inactive_buttons: b.setStyleSheet("color: " + INACTIVE_COLOR + "; background-color: " + BACKGROUND_COLOR + "; border: 1px solid " + INACTIVE_COLOR + "; font-family: " + FONT + ";") b.setCursor(Qt.ArrowCursor) elif HOME_URL not in self.history['urls'][-1]: self.home_button.setStyleSheet("color: " + FOREGROUND_COLOR + "; background-color: " + BACKGROUND_COLOR + "; border: 1px solid " + FOREGROUND_COLOR + "; font-family: " + FONT + ";") self.home_button.setCursor(Qt.PointingHandCursor) else: self.home_button.setStyleSheet("color: " + INACTIVE_COLOR + "; background-color: " + BACKGROUND_COLOR + "; border: 1px solid " + INACTIVE_COLOR + "; font-family: " + FONT + ";") self.home_button.setCursor(Qt.ArrowCursor) else: print('could not go back') def on_play_downloaded(self): if len(self.downloaded_videos['paths']) > 0: self.player.play(self.download_to_play) else: print('no videos downloaded yet') def select_download(self, index): print('queued ' + self.download_selector.itemData(index)) self.download_to_play = self.download_selector.itemData(index) def get_next_page(self): self.spinner.start() search_term = self.search try: sender = self.sender() if sender.page == '<': next_page_number = self.history['page_numbers'][-1] - 1 elif sender.page == '>': next_page_number = self.history['page_numbers'][-1] + 1 else: next_page_number = sender.page except: next_page_number = self.history['page_numbers'][-1] + 1 self.history['page_numbers'].append(next_page_number) url = self.history['urls'][-1] self.history['urls'].append(url) title_box = re.sub(r' page \d+$', '', self.history['title_boxes'][-1]) if next_page_number > 1: title_box = title_box[:29] + ' page ' + str(next_page_number) self.history['title_boxes'].append(title_box) data_limits = [ NUM_RESULTS * (next_page_number - 1), NUM_RESULTS * next_page_number ] idx = 'get_next_page' worker = Worker(idx, url, search=False, limit=data_limits) thread = QThread() thread.setObjectName('thread_' + idx) self.__threads.append((thread, worker)) worker.moveToThread(thread) worker.sig_data.connect(self.on_next_page_received) self.sig_abort_workers.connect(worker.abort) thread.started.connect(worker.grabData) thread.start() @pyqtSlot(dict) def on_next_page_received(self, data): search_term = self.search self.data = data self.history['data'].append(self.data) for b in self.inactive_buttons: b.setStyleSheet("color: " + FOREGROUND_COLOR + "; background-color: " + BACKGROUND_COLOR + "; border: 1px solid " + FOREGROUND_COLOR + "; font-family: " + FONT + ";") b.setCursor(Qt.PointingHandCursor) self.populate() groupbox = QGroupBox(self.history['title_boxes'][-1]) groupbox.setLayout(self.myform) groupbox.setStyleSheet("color: " + FOREGROUND_COLOR + "; font-family: " + FONT + ";font-style: italic") self.scroll.setWidget(groupbox) self.spinner.stop() @pyqtSlot() def abort_workers(self): self.sig_abort_workers.emit() for thread, worker in self.__threads: # note nice unpacking by Python, avoids indexing thread.quit( ) # this will quit **as soon as thread event loop unblocks** thread.wait() # <- so you need to wait for it to *actually* quit # even though threads have exited, there may still be messages on the main thread's # queue (messages that threads emitted before the abort): self.log.append('All threads exited')