From c1ef4b0294a586834989d7ebc144b6ab4b7bc1c7 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 11 May 2016 11:05:49 +0530 Subject: [PATCH] Windows: Use a helper process to display file open/save dialogs. This should fix most crashes caused by poorly designed shell extensions The helper process only depends on the C runtime, no Qt/Python DLLs are loaded. --- src/calibre/gui2/__init__.py | 147 ++++++++++++++------------- src/calibre/gui2/win_file_dialogs.py | 65 +++++++++++- 2 files changed, 141 insertions(+), 71 deletions(-) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 338a4f0ad3..1248f30aa2 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -678,15 +678,84 @@ class FileDialog(QObject): return tuple(os.path.abspath(unicode(i)) for i in self.fd.selectedFiles()) return tuple(self.selected_files) +has_windows_file_dialog_helper = False +if iswindows and 'CALIBRE_NO_NATIVE_FILEDIALOGS' not in os.environ: + from calibre.gui2.win_file_dialogs import is_ok as has_windows_file_dialog_helper + has_windows_file_dialog_helper = has_windows_file_dialog_helper() +if has_windows_file_dialog_helper: + from calibre.gui2.win_file_dialogs import choose_files, choose_images, choose_dir, choose_save_file +else: -def choose_dir(window, name, title, default_dir='~', no_save_dir=False): - fd = FileDialog(title=title, filters=[], add_all_files_filter=False, - parent=window, name=name, mode=QFileDialog.Directory, - default_dir=default_dir, no_save_dir=no_save_dir) - dir = fd.get_files() - fd.setParent(None) - if dir: - return dir[0] + def choose_dir(window, name, title, default_dir='~', no_save_dir=False): + fd = FileDialog(title=title, filters=[], add_all_files_filter=False, + parent=window, name=name, mode=QFileDialog.Directory, + default_dir=default_dir, no_save_dir=no_save_dir) + dir = fd.get_files() + fd.setParent(None) + if dir: + return dir[0] + + def choose_files(window, name, title, + filters=[], all_files=True, select_only_single_file=False, default_dir=u'~'): + ''' + Ask user to choose a bunch of files. + :param name: Unique dialog name used to store the opened directory + :param title: Title to show in dialogs titlebar + :param filters: list of allowable extensions. Each element of the list + must be a 2-tuple with first element a string describing + the type of files to be filtered and second element a list + of extensions. + :param all_files: If True add All files to filters. + :param select_only_single_file: If True only one file can be selected + ''' + mode = QFileDialog.ExistingFile if select_only_single_file else QFileDialog.ExistingFiles + fd = FileDialog(title=title, name=name, filters=filters, default_dir=default_dir, + parent=window, add_all_files_filter=all_files, mode=mode, + ) + fd.setParent(None) + if fd.accepted: + return fd.get_files() + return None + + def choose_save_file(window, name, title, filters=[], all_files=True, initial_path=None, initial_filename=None): + ''' + Ask user to choose a file to save to. Can be a non-existent file. + :param filters: list of allowable extensions. Each element of the list + must be a 2-tuple with first element a string describing + the type of files to be filtered and second element a list + of extensions. + :param all_files: If True add All files to filters. + :param initial_path: The initially selected path (does not need to exist). Cannot be used with initial_filename. + :param initial_filename: If specified, the initially selected path is this filename in the previously used directory. Cannot be used with initial_path. + ''' + kwargs = dict(title=title, name=name, filters=filters, + parent=window, add_all_files_filter=all_files, mode=QFileDialog.AnyFile) + if initial_path is not None: + kwargs['no_save_dir'] = True + kwargs['default_dir'] = initial_path + elif initial_filename is not None: + kwargs['combine_file_and_saved_dir'] = True + kwargs['default_dir'] = initial_filename + fd = FileDialog(**kwargs) + fd.setParent(None) + ans = None + if fd.accepted: + ans = fd.get_files() + if ans: + ans = ans[0] + return ans + + def choose_images(window, name, title, select_only_single_file=True, + formats=('png', 'gif', 'jpg', 'jpeg', 'svg')): + mode = QFileDialog.ExistingFile if select_only_single_file else QFileDialog.ExistingFiles + fd = FileDialog(title=title, name=name, + filters=[(_('Images'), list(formats))], + parent=window, add_all_files_filter=False, mode=mode, + ) + fd.setParent(None) + if fd.accepted: + return fd.get_files() + return None def choose_osx_app(window, name, title, default_dir='/Applications'): fd = FileDialog(title=title, parent=window, name=name, mode=QFileDialog.ExistingFile, @@ -696,68 +765,6 @@ def choose_osx_app(window, name, title, default_dir='/Applications'): if app: return app -def choose_files(window, name, title, - filters=[], all_files=True, select_only_single_file=False, default_dir=u'~'): - ''' - Ask user to choose a bunch of files. - :param name: Unique dialog name used to store the opened directory - :param title: Title to show in dialogs titlebar - :param filters: list of allowable extensions. Each element of the list - must be a 2-tuple with first element a string describing - the type of files to be filtered and second element a list - of extensions. - :param all_files: If True add All files to filters. - :param select_only_single_file: If True only one file can be selected - ''' - mode = QFileDialog.ExistingFile if select_only_single_file else QFileDialog.ExistingFiles - fd = FileDialog(title=title, name=name, filters=filters, default_dir=default_dir, - parent=window, add_all_files_filter=all_files, mode=mode, - ) - fd.setParent(None) - if fd.accepted: - return fd.get_files() - return None - -def choose_save_file(window, name, title, filters=[], all_files=True, initial_path=None, initial_filename=None): - ''' - Ask user to choose a file to save to. Can be a non-existent file. - :param filters: list of allowable extensions. Each element of the list - must be a 2-tuple with first element a string describing - the type of files to be filtered and second element a list - of extensions. - :param all_files: If True add All files to filters. - :param initial_path: The initially selected path (does not need to exist). Cannot be used with initial_filename. - :param initial_filename: If specified, the initially selected path is this filename in the previously used directory. Cannot be used with initial_path. - ''' - kwargs = dict(title=title, name=name, filters=filters, - parent=window, add_all_files_filter=all_files, mode=QFileDialog.AnyFile) - if initial_path is not None: - kwargs['no_save_dir'] = True - kwargs['default_dir'] = initial_path - elif initial_filename is not None: - kwargs['combine_file_and_saved_dir'] = True - kwargs['default_dir'] = initial_filename - fd = FileDialog(**kwargs) - fd.setParent(None) - ans = None - if fd.accepted: - ans = fd.get_files() - if ans: - ans = ans[0] - return ans - -def choose_images(window, name, title, select_only_single_file=True, - formats=('png', 'gif', 'jpg', 'jpeg', 'svg')): - mode = QFileDialog.ExistingFile if select_only_single_file else QFileDialog.ExistingFiles - fd = FileDialog(title=title, name=name, - filters=[('Images', list(formats))], - parent=window, add_all_files_filter=False, mode=mode, - ) - fd.setParent(None) - if fd.accepted: - return fd.get_files() - return None - def pixmap_to_data(pixmap, format='JPEG', quality=90): ''' Return the QPixmap pixmap as a string saved in the specified format. diff --git a/src/calibre/gui2/win_file_dialogs.py b/src/calibre/gui2/win_file_dialogs.py index 8e91343cd6..a2474891c4 100644 --- a/src/calibre/gui2/win_file_dialogs.py +++ b/src/calibre/gui2/win_file_dialogs.py @@ -13,12 +13,17 @@ is64bit = sys.maxsize > (1 << 32) base = sys.extensions_location if hasattr(sys, 'new_app_layout') else os.path.dirname(sys.executable) HELPER = os.path.join(base, 'calibre-file-dialogs.exe') +def is_ok(): + return os.path.exists(HELPER) + try: from calibre.constants import filesystem_encoding from calibre.utils.filenames import expanduser + from calibre.utils.config import dynamic except ImportError: filesystem_encoding = 'utf-8' expanduser = os.path.expanduser + dynamic = {} def get_hwnd(widget=None): ewid = None @@ -52,6 +57,8 @@ def serialize_file_types(file_types): buf.append(struct.pack(b'=H%ds' % len(x), len(x), x)) for name, extensions in file_types: add(name or _('Files')) + if isinstance(extensions, basestring): + extensions = extensions.split() add('; '.join('*.' + ext.lower() for ext in extensions)) return b''.join(buf) @@ -100,9 +107,10 @@ def run_file_dialog( file_types=() ): data = [] + parent = parent or None if parent is not None: data.append(serialize_hwnd(get_hwnd(parent))) - if title is not None: + if title: data.append(serialize_string('TITLE', title)) if no_symlinks: data.append(serialize_binary('NO_SYMLINKS', no_symlinks)) @@ -151,6 +159,61 @@ def run_file_dialog( ans = tuple((os.path.abspath(x.decode('utf-8')) for x in h.stdoutdata.split(b'\0') if x)) return ans +def get_initial_folder(name, title, default_dir='~', no_save_dir=False): + name = name or 'dialog_' + title + if no_save_dir: + initial_folder = expanduser(default_dir) + else: + initial_folder = dynamic.get(name, expanduser(default_dir)) + if not initial_folder or not os.path.isdir(initial_folder): + initial_folder = select_initial_dir(initial_folder) + return name, initial_folder + +def choose_dir(window, name, title, default_dir='~', no_save_dir=False): + name, initial_folder = get_initial_folder(name, title, default_dir, no_save_dir) + ans = run_file_dialog(window, title, only_dirs=True, initial_folder=initial_folder) + if ans: + ans = ans[0] + if not no_save_dir: + dynamic.set(name, ans) + return ans + +def choose_files(window, name, title, + filters=(), all_files=True, select_only_single_file=False, default_dir=u'~'): + name, initial_folder = get_initial_folder(name, title, default_dir) + file_types = list(filters) + if all_files: + file_types.append((_('All files'), ['*'])) + ans = run_file_dialog(window, title, allow_multiple=not select_only_single_file, initial_folder=initial_folder, file_types=file_types) + if ans: + dynamic.set(name, os.path.dirname(ans[0])) + return ans + return None + +def choose_images(window, name, title, select_only_single_file=True, + formats=('png', 'gif', 'jpg', 'jpeg', 'svg')): + file_types = [(_('Images'), list(formats))] + return choose_files(window, name, title, select_only_single_file=select_only_single_file, filters=file_types) + +def choose_save_file(window, name, title, filters=[], all_files=True, initial_path=None, initial_filename=None): + no_save_dir = False + default_dir = '~' + filename = initial_filename + if initial_path is not None: + no_save_dir = True + default_dir = select_initial_dir(initial_path) + filename = os.path.basename(initial_path) + file_types = list(filters) + if all_files: + file_types.append((_('All files'), ['*'])) + name, initial_folder = get_initial_folder(name, title, default_dir, no_save_dir) + ans = run_file_dialog(window, title, save_as=True, initial_folder=initial_folder, filename=filename, file_types=file_types) + if ans: + ans = ans[0] + if not no_save_dir: + dynamic.set(name, ans) + return ans + def test(): p = subprocess.Popen([HELPER], stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) echo = '\U0001f431 Hello world!'