Пример #1
0
    def __init__(self, parent, library_path, wait_time=2):
        QDialog.__init__(self, parent)
        self.l = QVBoxLayout()
        self.setLayout(self.l)
        self.l1 = QLabel('<b>'+_('Restoring database from backups, do not'
            ' interrupt, this will happen in three stages')+'...')
        self.setWindowTitle(_('Restoring database'))
        self.l.addWidget(self.l1)
        self.pb = QProgressBar(self)
        self.l.addWidget(self.pb)
        self.pb.setMaximum(0)
        self.pb.setMinimum(0)
        self.msg = QLabel('')
        self.l.addWidget(self.msg)
        self.msg.setWordWrap(True)
        self.bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Cancel)
        self.l.addWidget(self.bb)
        self.bb.rejected.connect(self.confirm_cancel)
        self.resize(self.sizeHint() + QSize(100, 50))
        self.error = None
        self.rejected = False
        self.library_path = library_path
        self.update_signal.connect(self.do_update, type=Qt.ConnectionType.QueuedConnection)

        from calibre.db.restore import Restore
        self.restorer = Restore(library_path, self)
        self.restorer.daemon = True

        # Give the metadata backup thread time to stop
        QTimer.singleShot(wait_time * 1000, self.start)
Пример #2
0
    def do_one_apply(self):
        if self.apply_current_idx >= len(self.apply_id_map):
            return self.finalize_apply()

        i, mi = self.apply_id_map[self.apply_current_idx]
        if self.gui.current_db.has_id(i):
            if isinstance(mi, tuple):
                opf, cover = mi
                if opf:
                    mi = OPF(open(opf, 'rb'),
                             basedir=os.path.dirname(opf),
                             populate_spine=False).to_book_metadata()
                    self.apply_mi(i, mi)
                if cover:
                    self.gui.current_db.set_cover(i,
                                                  open(cover, 'rb'),
                                                  notify=False,
                                                  commit=False)
                    self.applied_ids.add(i)
            else:
                self.apply_mi(i, mi)

        self.apply_current_idx += 1
        if self.apply_pd is not None:
            self.apply_pd.value += 1
        QTimer.singleShot(5, self.do_one_apply)
Пример #3
0
    def play(self, resume=False):
        self.status = "Switching to Player..."
        
        import player
        
        player.app = app
        struct, cells_by_id, columns_by_id = save(self.scene, resume=resume)
        
        window = player.MainWindow(playtest=True)
        window.setWindowModality(qt.ApplicationModal)
        window.setWindowState(self.windowState())
        window.setGeometry(self.geometry())

        windowcloseevent = window.closeEvent
        def closeevent(e):
            windowcloseevent(e)
            for it in window.scene.all(player.Cell):
                cells_by_id[it.id].revealed_resume = it.kind is not Cell.unknown
        window.closeEvent = closeevent

        def delayed():
            window.load(struct)
            window.view.setSceneRect(self.view.sceneRect())
            window.view.setTransform(self.view.transform())
            window.view.horizontalScrollBar().setValue(self.view.horizontalScrollBar().value())
            delta = window.view.mapTo(window.central_widget, QPoint(0, 0))
            window.view.verticalScrollBar().setValue(self.view.verticalScrollBar().value()+delta.y())
            self.status = "Done", 1
            
        window.show()
        QTimer.singleShot(0, delayed)
Пример #4
0
    def __init__(self, log, parent=None):
        QDialog.__init__(self, parent)
        self.log = log
        self.l = l = QVBoxLayout()
        self.setLayout(l)

        self.tb = QTextBrowser(self)
        l.addWidget(self.tb)

        self.bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
        l.addWidget(self.bb)
        self.copy_button = self.bb.addButton(
            _('Copy to clipboard'), QDialogButtonBox.ButtonRole.ActionRole)
        self.copy_button.clicked.connect(self.copy_to_clipboard)
        self.copy_button.setIcon(QIcon(I('edit-copy.png')))
        self.bb.rejected.connect(self.reject)
        self.bb.accepted.connect(self.accept)

        self.setWindowTitle(_('Download log'))
        self.setWindowIcon(QIcon(I('debug.png')))
        self.resize(QSize(800, 400))

        self.keep_updating = True
        self.last_html = None
        self.finished.connect(self.stop)
        QTimer.singleShot(100, self.update_log)

        self.show()
Пример #5
0
def main():
    from calibre.gui2 import Application
    from qt.core import QMainWindow, QStatusBar, QTimer
    app = Application([])
    w = QMainWindow()
    s = QStatusBar(w)
    w.setStatusBar(s)
    s.showMessage('Testing ProceedQuestion')
    w.show()
    p = ProceedQuestion(w)

    def doit():
        p.dummy_question()
        p.dummy_question(
            action_label='A very long button for testing relayout (indeed)')
        p(lambda p: None,
          None,
          'ass2',
          'ass2',
          'testing2',
          'testing2',
          det_msg=
          'details shown first, with a long line to test wrapping of text and width layout',
          show_det=True,
          show_ok=True)

    QTimer.singleShot(10, doit)
    app.exec_()
Пример #6
0
    def do_one_isbn_add(self):
        try:
            db = self.gui.library_view.model().db

            try:
                x = self.isbn_books.pop(0)
            except IndexError:
                self.gui.library_view.model().books_added(
                    self.isbn_add_dialog.value)
                self.isbn_add_dialog.accept()
                self.gui.iactions['Edit Metadata'].download_metadata(
                    ids=self.add_by_isbn_ids,
                    ensure_fields=frozenset(['title', 'authors']))
                return

            mi = MetaInformation(None)
            mi.isbn = x['isbn']
            if self.isbn_add_tags:
                mi.tags = list(self.isbn_add_tags)
            fmts = [] if x['path'] is None else [x['path']]
            self.add_by_isbn_ids.add(db.import_book(mi, fmts))
            self.isbn_add_dialog.value += 1
            QTimer.singleShot(10, self.do_one_isbn_add)
        except:
            self.isbn_add_dialog.accept()
            raise
Пример #7
0
    def check_for_completions(self):
        from calibre.utils.filenames import retry_on_fail
        for job in tuple(self.jobs):
            started_path = job['path'] + '.started'
            result_path = job['path'] + '.result'
            if job['started'] and os.path.exists(result_path):
                self.jobs.remove(job)
                ret = -1

                def read(result_path):
                    nonlocal ret
                    with open(result_path) as f:
                        ret = int(f.read().strip())

                retry_on_fail(read, result_path)
                retry_on_fail(os.remove, result_path)
                if ret == 0:
                    db = self.gui.current_db
                    if db.new_api.library_id != job['library_id']:
                        error_dialog(self.gui, _('Library changed'), _(
                            'Cannot save changes made to {0} by the ToC editor as'
                            ' the calibre library has changed.').format(job['title']), show=True)
                    else:
                        db.new_api.add_format(job['book_id'], job['fmt'], job['path'], run_hooks=False)
                os.remove(job['path'])
            else:
                if monotonic() - job['start_time'] > 120:
                    self.jobs.remove(job)
                    continue
                if os.path.exists(started_path):
                    job['started'] = True
                    retry_on_fail(os.remove, started_path)
        if self.jobs:
            QTimer.singleShot(100, self.check_for_completions)
Пример #8
0
 def check(self):
     if self.rejected:
         return
     if self.thread.is_alive():
         QTimer.singleShot(100, self.check)
     else:
         self.accept()
Пример #9
0
    def play(self, resume=False):
        self.status = "Switching to Player..."
        
        import player
        
        player.app = app
        struct, cells_by_id, columns_by_id = save(self.scene, resume=resume)
        
        window = player.MainWindow(playtest=True)
        window.setWindowModality(qt.ApplicationModal)
        window.setWindowState(self.windowState())
        window.setGeometry(self.geometry())

        windowcloseevent = window.closeEvent
        def closeevent(e):
            windowcloseevent(e)
            for it in window.scene.all(player.Cell):
                cells_by_id[it.id].revealed_resume = it.kind is not Cell.unknown
        window.closeEvent = closeevent

        def delayed():
            window.load(struct)
            window.view.setSceneRect(self.view.sceneRect())
            window.view.setTransform(self.view.transform())
            window.view.horizontalScrollBar().setValue(self.view.horizontalScrollBar().value())
            delta = window.view.mapTo(window.central_widget, QPoint(0, 0))
            window.view.verticalScrollBar().setValue(self.view.verticalScrollBar().value()+delta.y())
            self.status = "Done", 1
            
        window.show()
        QTimer.singleShot(0, delayed)
Пример #10
0
 def do_one_block(self):
     try:
         start_cursor, end_cursor = self.requests[0]
     except IndexError:
         return
     self.ignore_requests = True
     try:
         block = start_cursor.block()
         if not block.isValid():
             self.requests.popleft()
             return
         formats, force_next_highlight = self.parse_single_block(block)
         self.apply_format_changes(block, formats)
         try:
             self.doc.markContentsDirty(block.position(), block.length())
         except AttributeError:
             self.requests.clear()
             return
         ok = start_cursor.movePosition(QTextCursor.MoveOperation.NextBlock)
         if not ok:
             self.requests.popleft()
             return
         next_block = start_cursor.block()
         if next_block.position() > end_cursor.position():
             if force_next_highlight:
                 end_cursor.setPosition(next_block.position() + 1)
             else:
                 self.requests.popleft()
             return
     finally:
         self.ignore_requests = False
         QTimer.singleShot(0, self.do_one_block)
Пример #11
0
 def closeEvent(self, ev):
     if self.shutdown_done:
         return
     if self.current_book_data and self.web_view.view_is_ready and not self.close_forced:
         ev.ignore()
         if not self.shutting_down:
             self.shutting_down = True
             QTimer.singleShot(2000, self.force_close)
             self.web_view.prepare_for_close()
         return
     self.shutting_down = True
     self.search_widget.shutdown()
     self.web_view.shutdown()
     try:
         self.save_state()
         self.save_annotations()
         if self.annotations_saver is not None:
             self.annotations_saver.shutdown()
             self.annotations_saver = None
     except Exception:
         import traceback
         traceback.print_exc()
     clean_running_workers()
     self.shutdown_done = True
     return MainWindow.closeEvent(self, ev)
Пример #12
0
 def drop_event(self, event, mime_data):
     mime = 'application/calibre+from_library'
     if mime_data.hasFormat(mime):
         self.dropped_ids = tuple(map(int, mime_data.data(mime).data().split()))
         QTimer.singleShot(1, self.do_drop)
         return True
     return False
    def perform_action(self, ac, loc):
        if ac in ('new', 'existing'):
            self.callback(loc, copy_structure=self.copy_structure.isChecked())
        else:
            # move library
            self.db.prefs.disable_setting = True
            abort_move = Event()
            pd = ProgressDialog(_('Moving library, please wait...'), _('Scanning...'), max=0, min=0, icon='lt.png', parent=self)
            pd.canceled_signal.connect(abort_move.set)
            self.parent().library_view.model().stop_metadata_backup()
            move_error = []

            def do_move():
                try:
                    self.db.new_api.move_library_to(loc, abort=abort_move, progress=pd.show_new_progress)
                except Exception:
                    import traceback
                    move_error.append(traceback.format_exc())
                finally:
                    pd.finished_moving.emit()

            t = Thread(name='MoveLibrary', target=do_move)
            QTimer.singleShot(0, t.start)
            pd.exec_()
            if abort_move.is_set():
                self.callback(self.db.library_path)
                return
            if move_error:
                error_dialog(self.parent(), _('Failed to move library'), _(
                    'There was an error while moving the library. The operation has been aborted. Click'
                    ' "Show details" for details.'), det_msg=move_error[0], show=True)
                self.callback(self.db.library_path)
                return
            self.callback(loc, library_renamed=True)
Пример #14
0
def run_gui(app, opts, args, internal_book_data, listener=None):
    acc = EventAccumulator(app)
    app.file_event_hook = acc
    app.load_builtin_fonts()
    app.setWindowIcon(QIcon(I('viewer.png')))
    migrate_previous_viewer_prefs()
    main = EbookViewer(open_at=opts.open_at,
                       continue_reading=opts.continue_reading,
                       force_reload=opts.force_reload,
                       calibre_book_data=internal_book_data)
    main.set_exception_handler()
    if len(args) > 1:
        acc.events.append(os.path.abspath(args[-1]))
    acc.got_file.connect(main.handle_commandline_arg)
    main.show()
    if listener is not None:
        listener.message_received.connect(
            main.message_from_other_instance,
            type=Qt.ConnectionType.QueuedConnection)
    QTimer.singleShot(0, acc.flush)
    if opts.raise_window:
        main.raise_()
    if opts.full_screen:
        main.set_full_screen(True)

    app.exec_()
Пример #15
0
    def no_changes(self):
        self.any_changes = False

        def no_changes():
            self.any_changes = False

        QTimer.singleShot(0, no_changes)
Пример #16
0
    def __init__(self, parent=None, initial=None):
        QDialog.__init__(self, parent)
        self.setWindowTitle(_('Choose a texture'))

        self.l = l = QVBoxLayout()
        self.setLayout(l)

        self.tdir = texture_dir()

        self.images = il = QListWidget(self)
        il.itemDoubleClicked.connect(self.accept, type=Qt.ConnectionType.QueuedConnection)
        il.setIconSize(QSize(256, 256))
        il.setViewMode(QListView.ViewMode.IconMode)
        il.setFlow(QListView.Flow.LeftToRight)
        il.setSpacing(20)
        il.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
        il.itemSelectionChanged.connect(self.update_remove_state)
        l.addWidget(il)

        self.ad = ad = QLabel(_('The builtin textures come from <a href="{}">subtlepatterns.com</a>.').format(
            'https://www.toptal.com/designers/subtlepatterns/'))
        ad.setOpenExternalLinks(True)
        ad.setWordWrap(True)
        l.addWidget(ad)
        self.bb = bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok|QDialogButtonBox.StandardButton.Cancel)
        bb.accepted.connect(self.accept)
        bb.rejected.connect(self.reject)
        b = self.add_button = bb.addButton(_('Add texture'), QDialogButtonBox.ButtonRole.ActionRole)
        b.setIcon(QIcon(I('plus.png')))
        b.clicked.connect(self.add_texture)
        b = self.remove_button = bb.addButton(_('Remove texture'), QDialogButtonBox.ButtonRole.ActionRole)
        b.setIcon(QIcon(I('minus.png')))
        b.clicked.connect(self.remove_texture)
        l.addWidget(bb)

        images = [{
            'fname': ':'+os.path.basename(x),
            'path': x,
            'name': ' '.join(map(lambda s: s.capitalize(), os.path.splitext(os.path.basename(x))[0].split('_')))
        } for x in glob.glob(I('textures/*.png'))] + [{
            'fname': os.path.basename(x),
            'path': x,
            'name': os.path.splitext(os.path.basename(x))[0],
        } for x in glob.glob(os.path.join(self.tdir, '*')) if x.rpartition('.')[-1].lower() in {'jpeg', 'png', 'jpg'}]

        images.sort(key=lambda x:sort_key(x['name']))

        for i in images:
            self.create_item(i)
        self.update_remove_state()

        if initial:
            existing = {str(i.data(Qt.ItemDataRole.UserRole) or ''):i for i in (self.images.item(c) for c in range(self.images.count()))}
            item = existing.get(initial, None)
            if item is not None:
                item.setSelected(True)
                QTimer.singleShot(100, partial(il.scrollToItem, item))

        self.resize(QSize(950, 650))
Пример #17
0
    def check_exited(self):
        if getattr(self.server, 'is_running', False):
            QTimer.singleShot(20, self.check_exited)
            return

        self.gui.content_server = None
        self.main_tab.update_button_state()
        self.stopping_msg.accept()
Пример #18
0
 def update_log(self):
     if not self.keep_updating:
         return
     html = self.log.html
     if html != self.last_html:
         self.last_html = html
         self.tb.setHtml('<pre style="font-family:monospace">%s</pre>'%html)
     QTimer.singleShot(1000, self.update_log)
Пример #19
0
 def __init__(self, dock_action, parent=None):
     QWidget.__init__(self, parent=parent)
     self.view_to_debug = parent
     self.view = None
     self.layout = QHBoxLayout(self)
     self.layout.setContentsMargins(0, 0, 0, 0)
     self.dock_action = dock_action
     QTimer.singleShot(0, self.connect_to_dock)
Пример #20
0
 def check(self):
     if self.worker.is_alive() and not self.abort.is_set():
         QTimer.singleShot(50, self.check)
         try:
             self.process_result(self.worker.rq.get_nowait())
         except Empty:
             pass
     else:
         self.process_results()
Пример #21
0
 def start_download(self):
     self.worker.start()
     QTimer.singleShot(50, self.update)
     self.exec_()
     if self.worker.err is not None:
         error_dialog(self.parent(), _('Download failed'),
             _('Failed to download from %(url)r with error: %(err)s')%dict(
                 url=self.worker.url, err=self.worker.err),
             det_msg=self.worker.tb, show=True)
Пример #22
0
 def show_dialog(self, restrict_to_book_ids=None):
     if self.parent() is None:
         self.browse_panel.effective_query_changed()
         self.exec()
     else:
         self.reinitialize(restrict_to_book_ids)
         self.show()
         self.raise_()
         QTimer.singleShot(80, self.browse_panel.effective_query_changed)
Пример #23
0
 def stop_server(self):
     self.server.stop()
     self.stopping_msg = info_dialog(
         self,
         _('Stopping'),
         _('Stopping server, this could take up to a minute, please wait...'),
         show_copy_button=False
     )
     QTimer.singleShot(500, self.check_exited)
     self.stopping_msg.exec_()
Пример #24
0
 def toggle_content_server(self):
     if self.gui.content_server is None:
         self.gui.start_content_server()
     else:
         self.gui.content_server.stop()
         self.stopping_msg = info_dialog(self.gui, _('Stopping'),
                 _('Stopping server, this could take up to a minute, please wait...'),
                 show_copy_button=False)
         QTimer.singleShot(1000, self.check_exited)
         self.stopping_msg.exec_()
Пример #25
0
 def load_finished(self, ok):
     self.load_complete = True
     self.load_hang_check_timer.stop()
     if not ok:
         self.working = False
         self.work_done.emit(self, 'Load of {} failed'.format(self.url().toString()))
         return
     if self.wait_for_title and self.title() != self.wait_for_title:
         self.log(self.log_prefix, 'Load finished, waiting for title to change to:', self.wait_for_title)
         return
     QTimer.singleShot(int(1000 * self.settle_time), self.print_to_pdf)
Пример #26
0
def compare_books(path1, path2, revert_msg=None, revert_callback=None, parent=None, names=None):
    d = Diff(parent=parent, revert_button_msg=revert_msg)
    if revert_msg is not None:
        d.revert_requested.connect(revert_callback)
    QTimer.singleShot(0, partial(d.ebook_diff, path1, path2, names=names))
    d.exec()
    try:
        d.revert_requested.disconnect()
    except:
        pass
    d.break_cycles()
Пример #27
0
 def queue_files(self):
     self.tdir = PersistentTemporaryDirectory('_queue_polish')
     self.jobs = []
     if len(self.book_id_map) <= 5:
         for i, (book_id, formats) in enumerate(iteritems(self.book_id_map)):
             self.do_book(i+1, book_id, formats)
     else:
         self.queue = [(i+1, id_) for i, id_ in enumerate(self.book_id_map)]
         self.pd = ProgressDialog(_('Queueing books for polishing'),
                                  max=len(self.queue), parent=self)
         QTimer.singleShot(0, self.do_one)
         self.pd.exec_()
Пример #28
0
 def __init__(self, parent, book_ids, output_format, queue, db, user_recs,
         args, use_saved_single_settings=True):
     QProgressDialog.__init__(self, '',
             None, 0, len(book_ids), parent)
     self.setWindowTitle(_('Queueing books for bulk conversion'))
     self.book_ids, self.output_format, self.queue, self.db, self.args, self.user_recs = \
             book_ids, output_format, queue, db, args, user_recs
     self.parent = parent
     self.use_saved_single_settings = use_saved_single_settings
     self.i, self.bad, self.jobs, self.changed = 0, [], [], False
     QTimer.singleShot(0, self.do_book)
     self.exec_()
Пример #29
0
    def finalize_layout(self):
        self.status_bar.initialize(self.system_tray_icon)
        self.book_details.show_book_info.connect(
            self.iactions['Show Book Details'].show_book_info)
        self.book_details.files_dropped.connect(
            self.iactions['Add Books'].files_dropped_on_book)
        self.book_details.cover_changed.connect(
            self.bd_cover_changed, type=Qt.ConnectionType.QueuedConnection)
        self.book_details.open_cover_with.connect(
            self.bd_open_cover_with, type=Qt.ConnectionType.QueuedConnection)
        self.book_details.open_fmt_with.connect(
            self.bd_open_fmt_with, type=Qt.ConnectionType.QueuedConnection)
        self.book_details.edit_book.connect(
            self.bd_edit_book, type=Qt.ConnectionType.QueuedConnection)
        self.book_details.cover_removed.connect(
            self.bd_cover_removed, type=Qt.ConnectionType.QueuedConnection)
        self.book_details.remote_file_dropped.connect(
            self.iactions['Add Books'].remote_file_dropped_on_book,
            type=Qt.ConnectionType.QueuedConnection)
        self.book_details.open_containing_folder.connect(
            self.iactions['View'].view_folder_for_id)
        self.book_details.view_specific_format.connect(
            self.iactions['View'].view_format_by_id)
        self.book_details.search_requested.connect(
            self.set_search_string_with_append)
        self.book_details.remove_specific_format.connect(
            self.iactions['Remove Books'].remove_format_by_id)
        self.book_details.remove_metadata_item.connect(
            self.iactions['Edit Metadata'].remove_metadata_item)
        self.book_details.save_specific_format.connect(
            self.iactions['Save To Disk'].save_library_format_by_ids)
        self.book_details.restore_specific_format.connect(
            self.iactions['Remove Books'].restore_format)
        self.book_details.set_cover_from_format.connect(
            self.iactions['Edit Metadata'].set_cover_from_format)
        self.book_details.copy_link.connect(
            self.bd_copy_link, type=Qt.ConnectionType.QueuedConnection)
        self.book_details.view_device_book.connect(
            self.iactions['View'].view_device_book)
        self.book_details.manage_category.connect(
            self.manage_category_triggerred)
        self.book_details.find_in_tag_browser.connect(
            self.find_in_tag_browser_triggered)
        self.book_details.edit_identifiers.connect(
            self.edit_identifiers_triggerred)
        self.book_details.compare_specific_format.connect(self.compare_format)

        m = self.library_view.model()
        if m.rowCount(None) > 0:
            QTimer.singleShot(0, self.library_view.set_current_row)
            m.current_changed(self.library_view.currentIndex(),
                              self.library_view.currentIndex())
        self.library_view.setFocus(Qt.FocusReason.OtherFocusReason)
Пример #30
0
 def add_isbns(self, books, add_tags=[]):
     self.isbn_books = list(books)
     self.add_by_isbn_ids = set()
     self.isbn_add_tags = add_tags
     QTimer.singleShot(10, self.do_one_isbn_add)
     self.isbn_add_dialog = ProgressDialog(
         _('Adding'),
         _('Creating book records from ISBNs'),
         max=len(books),
         cancelable=False,
         parent=self.gui)
     self.isbn_add_dialog.exec_()
Пример #31
0
 def cover_browser_shown(self):
     self.cover_flow.setFocus(Qt.FocusReason.OtherFocusReason)
     if CoverFlow is not None:
         if self.db_images.ignore_image_requests:
             self.db_images.ignore_image_requests = False
             self.db_images.dataChanged.emit()
         self.cover_flow.setCurrentSlide(self.library_view.currentIndex().row())
         self.cover_flow_syncing_enabled = True
         QTimer.singleShot(500, self.cover_flow_do_sync)
     self.library_view.setCurrentIndex(
             self.library_view.currentIndex())
     self.library_view.scroll_to_row(self.library_view.currentIndex().row())
Пример #32
0
def main(f=None):
    global window

    window = MainWindow()
    window.show()

    if not f and len(sys.argv[1:]) == 1:
        f = sys.argv[1]
    if f:
        f = os.path.abspath(f)
        QTimer.singleShot(50, lambda: window.load_file(f))
    
    app.exec_()
Пример #33
0
def category_click(category):
    open_url(category.url)
    QTimer.singleShot(8000, web.update)
Пример #34
0
def comment_click(comment):
    open_url(comment.url)
    comments.remove(comment)
    categories.comments.count -= 1
    update()
    QTimer.singleShot(5000, web.update)
Пример #35
0
 def no_changes(self):
     self.any_changes = False
     def no_changes():
         self.any_changes = False
     QTimer.singleShot(0, no_changes)