diff --git a/setup/installer/windows/file_dialogs.cpp b/setup/installer/windows/file_dialogs.cpp new file mode 100644 index 0000000000..60574120ca --- /dev/null +++ b/setup/installer/windows/file_dialogs.cpp @@ -0,0 +1,79 @@ +/* + * file_dialogs.c + * Copyright (C) 2016 Kovid Goyal + * + * Distributed under terms of the GPL3 license. + */ + +#include +#include +#include +#include + +#define PRINTERR(x) fprintf(stderr, x); fflush(stderr); +#define REPORTERR(x) { PRINTERR(x); ret = 1; goto error; } +#define CALLCOM(x, err) hr = x; if(FAILED(hr)) REPORTERR(err) + +int show_dialog(HWND parent, bool save_dialog) { + int ret = 0; + IFileDialog *pfd = NULL; + IShellItem *psiResult = NULL; + DWORD dwFlags; + HRESULT hr = S_OK; + hr = CoInitialize(NULL); + if (FAILED(hr)) { PRINTERR("Failed to initialize COM"); return 1; } + + CALLCOM(CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_INPROC_SERVER, (save_dialog ? IID_IFileSaveDialog : IID_IFileOpenDialog), reinterpret_cast(&pfd)), "Failed to create COM object for file dialog") + CALLCOM(pfd->GetOptions(&dwFlags), "Failed to get options") + dwFlags |= FOS_FORCEFILESYSTEM; + CALLCOM(pfd->SetOptions(dwFlags), "Failed to set options") + hr = pfd->Show(parent); + if (hr == HRESULT_FROM_WIN32(ERROR_CANCELLED)) goto error; + if (FAILED(hr)) REPORTERR("Failed to show dialog") + + CALLCOM(pfd->GetResult(&psiResult), "Failed to get dialog result") + +error: + if(pfd) pfd->Release(); + CoUninitialize(); + return ret; +} + +bool read_bytes(size_t sz, char* buf, bool allow_incomplete=false) { + char *ptr = buf, *limit = buf + sz; + while(limit > ptr && !feof(stdin) && !ferror(stdin)) { + ptr += fread(ptr, 1, limit - ptr, stdin); + } + if (ferror(stdin)) { PRINTERR("Failed to read from stdin!"); return false; } + if (ptr - buf != sz) { if (!allow_incomplete) PRINTERR("Truncated input!"); return false; } + return true; +} + +#define READ(x, y) if (!read_bytes((x), (y))) return 1; + +int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow) { + char buf[257]; + size_t key_size = 0; + HWND parent = NULL; + bool save_dialog = false; + + while(!feof(stdin)) { + memset(buf, 0, sizeof(buf)); + if(!read_bytes(1, buf, true)) { if (feof(stdin)) break; return 1;} + key_size = (size_t)buf[0]; + READ(key_size, buf); + if (key_size == 4 && memcmp(buf, "HWND", 4) == 0) { + READ(sizeof(HWND), buf); + if (sizeof(HWND) == 8) parent = (HWND)*((__int64*)buf); + else if (sizeof(HWND) == 4) parent = (HWND)*((__int32*)buf); + else { fprintf(stderr, "Unknown pointer size: %d", sizeof(HWND)); return 1;} + } + + else { + PRINTERR("Unknown key"); + return 1; + } + } + + return show_dialog(parent, save_dialog); +} diff --git a/src/calibre/gui2/win_file_dialogs.py b/src/calibre/gui2/win_file_dialogs.py new file mode 100644 index 0000000000..6d72696de9 --- /dev/null +++ b/src/calibre/gui2/win_file_dialogs.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python2 +# vim:fileencoding=utf-8 +# License: GPLv3 Copyright: 2016, Kovid Goyal + +from __future__ import (unicode_literals, division, absolute_import, + print_function) +import sys, subprocess, struct, os +from threading import Thread + +from PyQt5.Qt import QMainWindow, QApplication, QPushButton, pyqtSignal, QEventLoop, Qt + +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 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(b'=' + (b'B4sQ' if is64bit else b'I'), 4, b'HWND', int(hwnd)) + +def serialize_string(key, val): + key = key.encode('ascii') if not isinstance(key, bytes) else key + val = type('')(val).encode('utf-8') + if len(val) > 2**16 - 1: + raise ValueError('%s is too long' % key) + return struct.pack(b'=B%dsH%ds' % (len(key), len(val)), len(key), key, len(val), val) + +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 = 0 + self.stdoutdata = None + + def run(self): + self.stdoutdata, self.stderrdata = self.process.communicate(b''.join(self.data)) + self.rc = self.process.wait() + self.callback() + +class Loop(QEventLoop): + + dialog_closed = pyqtSignal() + + def __init__(self): + QEventLoop.__init__(self) + self.dialog_closed.connect(self.exit, type=Qt.QueuedConnection) + +def run_file_dialog(parent=None, title=None): + data = [] + if parent is not None: + data.append(serialize_hwnd(get_hwnd(parent))) + if title is not None: + data.append(serialize_string('TITLE', title)) + loop = Loop() + h = Helper(subprocess.Popen( + [HELPER], stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE), + data, loop.dialog_closed.emit) + h.start() + loop.exec_(QEventLoop.ExcludeUserInputEvents) + if h.rc != 0: + raise Exception('File dialog failed: ' + h.stderrdata.decode('utf-8')) + if not h.stdoutdata: + return () + return tuple(x.decode('utf-8') for x in h.stdoutdata.split(b'\0')) + +if __name__ == '__main__': + HELPER = sys.argv[-1] + app = QApplication([]) + q = QMainWindow() + + def clicked(): + print(run_file_dialog(b)), sys.stdout.flush() + + b = QPushButton('click me') + b.clicked.connect(clicked) + q.setCentralWidget(b) + q.show() + app.exec_()