Start work on helper process for windows file dialogs

This commit is contained in:
Kovid Goyal 2016-05-03 18:26:51 +05:30
parent f451b173ed
commit d8fe21d156
2 changed files with 169 additions and 0 deletions

View File

@ -0,0 +1,79 @@
/*
* file_dialogs.c
* Copyright (C) 2016 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#include <Windows.h>
#include <Shobjidl.h>
#include <stdio.h>
#include <string.h>
#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<LPVOID*>(&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);
}

View File

@ -0,0 +1,90 @@
#!/usr/bin/env python2
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
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_()