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.
This commit is contained in:
Kovid Goyal 2016-05-11 11:05:49 +05:30
parent 9c96e5f877
commit c1ef4b0294
2 changed files with 141 additions and 71 deletions

View File

@ -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.

View File

@ -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!'