%PDF- %PDF-
Direktori : /lib/calibre/calibre/gui2/ |
Current File : //lib/calibre/calibre/gui2/win_file_dialogs.py |
#!/usr/bin/env python3 # License: GPLv3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net> import os import struct import subprocess import sys from threading import Thread from uuid import uuid4 from contextlib import suppress from polyglot.builtins import string_or_bytes 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-dialog.exe') current_app_uid = None def set_app_uid(val=None): global current_app_uid current_app_uid = val def is_ok(): return os.path.exists(HELPER) try: from calibre.utils.config import dynamic except ImportError: dynamic = {} def get_hwnd(widget=None): ewid = None if widget is not None: ewid = widget.effectiveWinId() if ewid is None: return None return int(ewid) def serialize_hwnd(hwnd): if hwnd is None: return b'' return struct.pack('=B4s' + ('Q' if is64bit else 'I'), 4, b'HWND', int(hwnd)) def serialize_secret(secret): return struct.pack('=B6s32s', 6, b'SECRET', secret) def serialize_binary(key, val): key = key.encode('ascii') if not isinstance(key, bytes) else key return struct.pack('=B%ssB' % len(key), len(key), key, int(val)) def serialize_string(key, val): key = key.encode('ascii') if not isinstance(key, bytes) else key val = str(val).encode('utf-8') if len(val) > 2**16 - 1: raise ValueError('%s is too long' % key) return struct.pack('=B%dsH%ds' % (len(key), len(val)), len(key), key, len(val), val) def serialize_file_types(file_types): key = b"FILE_TYPES" buf = [struct.pack('=B%dsH' % len(key), len(key), key, len(file_types))] def add(x): x = x.encode('utf-8').replace(b'\0', b'') buf.append(struct.pack('=H%ds' % len(x), len(x), x)) for name, extensions in file_types: add(name or _('Files')) if isinstance(extensions, string_or_bytes): extensions = extensions.split() add('; '.join('*.' + ext.lower() for ext in extensions)) return b''.join(buf) class Helper(Thread): def __init__(self, process, data, callback): Thread.__init__(self, name='FileDialogHelper') self.process = process self.callback = callback self.data = data self.daemon = True self.rc = 1 self.stdoutdata = self.stderrdata = b'' def run(self): try: self.stdoutdata, self.stderrdata = self.process.communicate(b''.join(self.data)) self.rc = self.process.wait() finally: self.callback() def process_path(x): if isinstance(x, bytes): x = os.fsdecode(x) return os.path.abspath(os.path.expanduser(x)) def select_initial_dir(q): while q: c = os.path.dirname(q) if c == q: break if os.path.exists(c): return c q = c return os.path.expanduser('~') def run_file_dialog( parent=None, title=None, initial_folder=None, filename=None, save_path=None, allow_multiple=False, only_dirs=False, confirm_overwrite=True, save_as=False, no_symlinks=False, file_types=(), default_ext=None, app_uid=None ): from calibre.gui2 import sanitize_env_vars secret = os.urandom(32).replace(b'\0', b' ') pipename = '\\\\.\\pipe\\%s' % uuid4() data = [serialize_string('PIPENAME', pipename), serialize_secret(secret)] parent = parent or None if parent is not None: data.append(serialize_hwnd(get_hwnd(parent))) if title: data.append(serialize_string('TITLE', title)) if no_symlinks: data.append(serialize_binary('NO_SYMLINKS', no_symlinks)) if save_as: data.append(serialize_binary('SAVE_AS', save_as)) if confirm_overwrite: data.append(serialize_binary('CONFIRM_OVERWRITE', confirm_overwrite)) if save_path is not None: save_path = process_path(save_path) if os.path.exists(save_path): data.append(serialize_string('SAVE_PATH', save_path)) else: if not initial_folder: initial_folder = select_initial_dir(save_path) if not filename: filename = os.path.basename(save_path) else: if allow_multiple: data.append(serialize_binary('MULTISELECT', allow_multiple)) if only_dirs: data.append(serialize_binary('ONLY_DIRS', only_dirs)) if initial_folder is not None: initial_folder = process_path(initial_folder) if os.path.isdir(initial_folder): data.append(serialize_string('FOLDER', initial_folder)) if filename: if isinstance(filename, bytes): filename = os.fsdecode(filename) data.append(serialize_string('FILENAME', filename)) if only_dirs: file_types = () # file types not allowed for dir only dialogs elif not file_types: file_types = [(_('All files'), ('*',))] if file_types: data.append(serialize_file_types(file_types)) if default_ext: data.append(serialize_string('DEFAULT_EXTENSION', default_ext)) app_uid = app_uid or current_app_uid if app_uid: data.append(serialize_string('APP_UID', app_uid)) from qt.core import QEventLoop, Qt, pyqtSignal class Loop(QEventLoop): dialog_closed = pyqtSignal() def __init__(self): QEventLoop.__init__(self) self.dialog_closed.connect(self.exit, type=Qt.ConnectionType.QueuedConnection) loop = Loop() server = PipeServer(pipename) server.start() with sanitize_env_vars(): h = Helper(subprocess.Popen( [HELPER], stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE), data, loop.dialog_closed.emit) h.start() loop.exec(QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents) def decode(x): x = x or b'' try: x = x.decode('utf-8') except Exception: x = repr(x) return x def get_errors(): return decode(h.stdoutdata) + ' ' + decode(h.stderrdata) from calibre import prints from calibre.constants import DEBUG if DEBUG: prints('stdout+stderr from file dialog helper:', str([h.stdoutdata, h.stderrdata])) if h.rc != 0: raise Exception(f'File dialog failed (return code {h.rc}): {get_errors()}') server.join(2) if server.is_alive(): raise Exception('Timed out waiting for read from pipe to complete') if server.err_msg: raise Exception(server.err_msg) if not server.data: return () parts = list(filter(None, server.data.split(b'\0'))) if DEBUG: prints('piped data from file dialog helper:', str(parts)) if len(parts) < 2: return () if parts[0] != secret: raise Exception('File dialog failed, incorrect secret received: ' + get_errors()) from calibre_extensions.winutil import get_long_path_name def fix_path(x): u = os.path.abspath(x.decode('utf-8')) with suppress(Exception): try: return get_long_path_name(u) except FileNotFoundError: base, fn = os.path.split(u) return os.path.join(get_long_path_name(base), fn) return u ans = tuple(map(fix_path, parts[1:])) 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 = os.path.expanduser(default_dir) else: initial_folder = dynamic.get(name, os.path.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='~'): 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=None): if formats is None: from calibre.gui2.dnd import image_extensions formats = image_extensions() 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'), ['*'])) all_exts = [] for ftext, exts in file_types: for ext in exts: if '*' not in ext: all_exts.append(ext.lower()) default_ext = all_exts[0] if all_exts else None 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, default_ext=default_ext) if ans: ans = ans[0] if not no_save_dir: dynamic.set(name, ans) return ans class PipeServer(Thread): def __init__(self, pipename): Thread.__init__(self, name='PipeServer', daemon=True) from calibre_extensions import winutil self.client_connected = False self.pipe_handle = winutil.create_named_pipe( pipename, winutil.PIPE_ACCESS_INBOUND | winutil.FILE_FLAG_FIRST_PIPE_INSTANCE, winutil.PIPE_TYPE_BYTE | winutil.PIPE_READMODE_BYTE | winutil.PIPE_WAIT | winutil.PIPE_REJECT_REMOTE_CLIENTS, 1, 8192, 8192, 0) winutil.set_handle_information(self.pipe_handle, winutil.HANDLE_FLAG_INHERIT, 0) self.err_msg = None self.data = b'' def run(self): from calibre_extensions import winutil try: try: winutil.connect_named_pipe(self.pipe_handle) except Exception as err: self.err_msg = f'ConnectNamedPipe failed: {err}' return self.client_connected = True while True: try: data = winutil.read_file(self.pipe_handle, 64 * 1024) except OSError as err: if err.winerror == winutil.ERROR_BROKEN_PIPE: break # pipe was closed at the other end self.err_msg = f'ReadFile on pipe failed: {err}' if not data: break self.data += data finally: self.pipe_handle = None def test(helper=HELPER): pipename = '\\\\.\\pipe\\%s' % uuid4() echo = '\U0001f431 Hello world!' secret = os.urandom(32).replace(b'\0', b' ') data = serialize_string('PIPENAME', pipename) + serialize_string('ECHO', echo) + serialize_secret(secret) server = PipeServer(pipename) server.start() p = subprocess.Popen([helper], stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = p.communicate(data) if p.wait() != 0: raise Exception('File dialog failed: ' + stdout.decode('utf-8') + ' ' + stderr.decode('utf-8')) if server.err_msg is not None: raise RuntimeError(server.err_msg) server.join(2) parts = list(filter(None, server.data.split(b'\0'))) if parts[0] != secret: raise RuntimeError(f'Did not get back secret: {secret!r} != {parts[0]!r}') q = parts[1].decode('utf-8') if q != echo: raise RuntimeError('Unexpected response: %r' % server.data) if __name__ == '__main__': from calibre.gui2 import Application app = Application([]) print(choose_save_file(None, 'xxx', 'yyy')) del app