From 4aeb5e138864e2a12c2d05d3715f7c1e56d43be2 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Fri, 3 May 2013 19:31:24 +0800 Subject: [PATCH] Add dialog API. Supported APIs: * openFile * openMultiFiles * openFolder * saveAs Each API is asynchronous and accepts a callback. --- atom.gyp | 4 + browser/api/atom_api_app.h | 2 +- browser/api/atom_api_file_dialog.cc | 191 ++++++++++++++++++++++++++++ browser/api/atom_api_file_dialog.h | 47 +++++++ browser/api/lib/dialog.coffee | 51 ++++++++ browser/native_window.cc | 3 +- browser/native_window.h | 4 +- browser/native_window_mac.h | 1 + browser/native_window_mac.mm | 4 + common/api/atom_extensions.h | 1 + renderer/api/lib/dialog.coffee | 41 ++++++ 11 files changed, 344 insertions(+), 5 deletions(-) create mode 100644 browser/api/atom_api_file_dialog.cc create mode 100644 browser/api/atom_api_file_dialog.h create mode 100644 browser/api/lib/dialog.coffee create mode 100644 renderer/api/lib/dialog.coffee diff --git a/atom.gyp b/atom.gyp index 3627a3d51fc0..cfd188091dd2 100644 --- a/atom.gyp +++ b/atom.gyp @@ -8,6 +8,7 @@ 'coffee_sources': [ 'browser/api/lib/app.coffee', 'browser/api/lib/atom.coffee', + 'browser/api/lib/dialog.coffee', 'browser/api/lib/ipc.coffee', 'browser/api/lib/window.coffee', 'browser/atom/atom.coffee', @@ -16,6 +17,7 @@ 'common/api/lib/clipboard.coffee', 'common/api/lib/id_weak_map.coffee', 'common/api/lib/shell.coffee', + 'renderer/api/lib/dialog.coffee', 'renderer/api/lib/ipc.coffee', 'renderer/api/lib/remote.coffee', ], @@ -30,6 +32,8 @@ 'browser/api/atom_api_event.h', 'browser/api/atom_api_event_emitter.cc', 'browser/api/atom_api_event_emitter.h', + 'browser/api/atom_api_file_dialog.cc', + 'browser/api/atom_api_file_dialog.h', 'browser/api/atom_api_window.cc', 'browser/api/atom_api_window.h', 'browser/api/atom_browser_bindings.cc', diff --git a/browser/api/atom_api_app.h b/browser/api/atom_api_app.h index f2b45eea0264..06b650c9a070 100644 --- a/browser/api/atom_api_app.h +++ b/browser/api/atom_api_app.h @@ -14,7 +14,7 @@ namespace atom { namespace api { class App : public EventEmitter, - public BrowserObserver{ + public BrowserObserver { public: virtual ~App(); diff --git a/browser/api/atom_api_file_dialog.cc b/browser/api/atom_api_file_dialog.cc new file mode 100644 index 000000000000..baebfe76a3b6 --- /dev/null +++ b/browser/api/atom_api_file_dialog.cc @@ -0,0 +1,191 @@ +// Copyright (c) 2013 GitHub, Inc. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "browser/api/atom_api_file_dialog.h" + +#include + +#include "base/utf_string_conversions.h" +#include "base/values.h" +#include "browser/native_window.h" + +namespace atom { + +namespace api { + +namespace { + +base::FilePath V8ValueToFilePath(v8::Handle path) { + std::string path_string(*v8::String::Utf8Value(path)); + return base::FilePath::FromUTF8Unsafe(path_string); +} + +ui::SelectFileDialog::Type IntToDialogType(int type) { + switch (type) { + case ui::SelectFileDialog::SELECT_FOLDER: + return ui::SelectFileDialog::SELECT_FOLDER; + case ui::SelectFileDialog::SELECT_SAVEAS_FILE: + return ui::SelectFileDialog::SELECT_SAVEAS_FILE; + case ui::SelectFileDialog::SELECT_OPEN_FILE: + return ui::SelectFileDialog::SELECT_OPEN_FILE; + case ui::SelectFileDialog::SELECT_OPEN_MULTI_FILE: + return ui::SelectFileDialog::SELECT_OPEN_MULTI_FILE; + default: + return ui::SelectFileDialog::SELECT_NONE; + } +} + +} // namespace + +FileDialog::FileDialog(v8::Handle wrapper) + : EventEmitter(wrapper), + dialog_(ui::SelectFileDialog::Create(this, NULL)) { +} + +FileDialog::~FileDialog() { +} + +void FileDialog::FileSelected(const base::FilePath& path, + int index, void* params) { + int* id = static_cast(params); + + base::ListValue args; + args.AppendInteger(*id); + args.AppendString(path.value()); + + Emit("selected", &args); + + delete id; +} + +void FileDialog::MultiFilesSelected(const std::vector& files, + void* params) { + int* id = static_cast(params); + + base::ListValue args; + args.AppendInteger(*id); + for (size_t i = 0; i < files.size(); i++) + args.AppendString(files[i].value()); + + Emit("selected", &args); + + delete id; +} + +void FileDialog::FileSelectionCanceled(void* params) { + int* id = static_cast(params); + + base::ListValue args; + args.AppendInteger(*id); + + Emit("cancelled", &args); + + delete id; +} + +// static +v8::Handle FileDialog::New(const v8::Arguments &args) { + v8::HandleScope scope; + + if (!args.IsConstructCall()) + return node::ThrowError("Require constructor call"); + + new FileDialog(args.This()); + + return args.This(); +} + +// static +v8::Handle FileDialog::SelectFile(const v8::Arguments &args) { + FileDialog* self = Unwrap(args.This()); + if (!self) + return node::ThrowError("The FileDialog object is corrupted"); + + if (!args[0]->IsNumber() || // process_id + !args[1]->IsNumber() || // routing_id + !args[2]->IsNumber() || // type + !args[3]->IsString() || // title + !args[4]->IsString() || // default_path + !args[5]->IsArray() || // file_types + !args[6]->IsNumber() || // file_type_index + !args[7]->IsString() || // default_extension + !args[8]->IsNumber()) // callback_id + return node::ThrowTypeError("Bad argument"); + + int process_id = args[0]->IntegerValue(); + int routing_id = args[1]->IntegerValue(); + NativeWindow* window = NativeWindow::FromRenderView(process_id, routing_id); + if (!window) + return node::ThrowError("Window not found"); + + gfx::NativeWindow owning_window = window->GetNativeWindow(); + + int type = args[2]->IntegerValue(); + std::string title(*v8::String::Utf8Value(args[3])); + base::FilePath default_path(V8ValueToFilePath(args[4])); + + ui::SelectFileDialog::FileTypeInfo file_types; + FillTypeInfo(&file_types, v8::Handle::Cast(args[5])); + + int file_type_index = args[6]->IntegerValue(); + std::string default_extension(*v8::String::Utf8Value(args[7])); + int callback_id = args[8]->IntegerValue(); + + self->dialog_->SelectFile( + IntToDialogType(type), + UTF8ToUTF16(title), + default_path, + file_types.extensions.size() > 0 ? &file_types : NULL, + file_type_index, + default_extension, + owning_window, + new int(callback_id)); + + return v8::Undefined(); +} + +// static +void FileDialog::FillTypeInfo(ui::SelectFileDialog::FileTypeInfo* file_types, + v8::Handle v8_file_types) { + file_types->include_all_files = true; + file_types->support_drive = true; + + for (uint32_t i = 0; i < v8_file_types->Length(); ++i) { + v8::Handle element = v8_file_types->Get(i)->ToObject(); + + std::string description(*v8::String::Utf8Value( + element->Get(v8::String::New("description")))); + file_types->extension_description_overrides.push_back( + UTF8ToUTF16(description)); + + std::vector extensions; + v8::Handle v8_extensions = v8::Handle::Cast( + element->Get(v8::String::New("extensions"))); + + for (uint32_t j = 0; j < v8_extensions->Length(); ++j) { + std::string extension(*v8::String::Utf8Value(v8_extensions->Get(j))); + extensions.push_back(extension); + } + file_types->extensions.push_back(extensions); + } +} + +// static +void FileDialog::Initialize(v8::Handle target) { + v8::HandleScope scope; + + v8::Local t(v8::FunctionTemplate::New(FileDialog::New)); + t->InstanceTemplate()->SetInternalFieldCount(1); + t->SetClassName(v8::String::NewSymbol("FileDialog")); + + NODE_SET_PROTOTYPE_METHOD(t, "selectFile", SelectFile); + + target->Set(v8::String::NewSymbol("FileDialog"), t->GetFunction()); +} + +} // namespace api + +} // namespace atom + +NODE_MODULE(atom_browser_file_dialog, atom::api::FileDialog::Initialize) diff --git a/browser/api/atom_api_file_dialog.h b/browser/api/atom_api_file_dialog.h new file mode 100644 index 000000000000..4ca4e822a4ff --- /dev/null +++ b/browser/api/atom_api_file_dialog.h @@ -0,0 +1,47 @@ +// Copyright (c) 2013 GitHub, Inc. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef ATOM_BROWSER_API_ATOM_API_FILE_DIALOG_H_ +#define ATOM_BROWSER_API_ATOM_API_FILE_DIALOG_H_ + +#include "browser/api/atom_api_event_emitter.h" +#include "ui/shell_dialogs/select_file_dialog.h" + +namespace atom { + +namespace api { + +class FileDialog : public EventEmitter, + public ui::SelectFileDialog::Listener { + public: + virtual ~FileDialog(); + + static void Initialize(v8::Handle target); + + // ui::SelectFileDialog::Listener implementations: + virtual void FileSelected(const base::FilePath& path, + int index, void* params) OVERRIDE; + virtual void MultiFilesSelected( + const std::vector& files, void* params) OVERRIDE; + virtual void FileSelectionCanceled(void* params) OVERRIDE; + + private: + explicit FileDialog(v8::Handle wrapper); + + static void FillTypeInfo(ui::SelectFileDialog::FileTypeInfo* file_types, + v8::Handle v8_file_types); + + static v8::Handle New(const v8::Arguments &args); + static v8::Handle SelectFile(const v8::Arguments &args); + + scoped_refptr dialog_; + + DISALLOW_COPY_AND_ASSIGN(FileDialog); +}; + +} // namespace api + +} // namespace atom + +#endif // ATOM_BROWSER_API_ATOM_API_FILE_DIALOG_H_ diff --git a/browser/api/lib/dialog.coffee b/browser/api/lib/dialog.coffee new file mode 100644 index 000000000000..250ada4d15ee --- /dev/null +++ b/browser/api/lib/dialog.coffee @@ -0,0 +1,51 @@ +EventEmitter = require('events').EventEmitter +ipc = require 'ipc' + +FileDialog = process.atomBinding('file_dialog').FileDialog +FileDialog.prototype.__proto__ = EventEmitter.prototype + +callbacksInfo = {} + +fileDialog = new FileDialog + +onSelected = (event, id, paths...) -> + {processId, routingId} = callbacksInfo[id] + delete callbacksInfo[id] + + ipc.sendChannel processId, routingId, 'ATOM_RENDERER_DIALOG', id, paths... + +fileDialog.on 'selected', onSelected +fileDialog.on 'cancelled', onSelected + +validateOptions = (options) -> + return false unless typeof options is 'object' + + options.fileTypes = [] unless Array.isArray options.fileTypes + for type in options.fileTypes + return false unless typeof type is 'object' and + typeof type.description is 'string' + Array.isArray type.extensions + + options.defaultPath = '' unless options.defaultPath? + options.fileTypeIndex = 0 unless options.fileTypeIndex? + options.defaultExtension = '' unless options.defaultExtension? + + true + +ipc.on 'ATOM_BROWSER_FILE_DIALOG', (processId, routingId, callbackId, type, title, options) -> + options = {} unless options? + options.type = type + options.title = title unless options.title? + + throw new TypeError('Bad arguments') unless validateOptions options + + callbacksInfo[callbackId] = processId: processId, routingId: routingId + + fileDialog.selectFile processId, routingId, + options.type, + options.title, + options.defaultPath, + options.fileTypes, + options.fileTypeIndex, + options.defaultExtension, + callbackId diff --git a/browser/native_window.cc b/browser/native_window.cc index eb912695de21..cbea4a75fb74 100644 --- a/browser/native_window.cc +++ b/browser/native_window.cc @@ -60,8 +60,7 @@ NativeWindow* NativeWindow::Create(base::DictionaryValue* options) { } // static -NativeWindow* NativeWindow::FromProcessIDAndRoutingID(int process_id, - int routing_id) { +NativeWindow* NativeWindow::FromRenderView(int process_id, int routing_id) { // Stupid iterating. WindowList& window_list = *WindowList::GetInstance(); for (auto window : window_list) { diff --git a/browser/native_window.h b/browser/native_window.h index 06095b9b5b65..1182a706f721 100644 --- a/browser/native_window.h +++ b/browser/native_window.h @@ -55,8 +55,7 @@ class NativeWindow : public content::WebContentsDelegate, static NativeWindow* Create(base::DictionaryValue* options); // Find a window from its process id and routing id. - static NativeWindow* FromProcessIDAndRoutingID(int process_id, - int routing_id); + static NativeWindow* FromRenderView(int process_id, int routing_id); void InitFromOptions(base::DictionaryValue* options); @@ -90,6 +89,7 @@ class NativeWindow : public content::WebContentsDelegate, virtual void FlashFrame(bool flash) = 0; virtual void SetKiosk(bool kiosk) = 0; virtual bool IsKiosk() = 0; + virtual gfx::NativeWindow GetNativeWindow() = 0; virtual bool IsClosed() const { return is_closed_; } virtual void ShowDevTools(); diff --git a/browser/native_window_mac.h b/browser/native_window_mac.h index 5f583802b915..b302f9bacb46 100644 --- a/browser/native_window_mac.h +++ b/browser/native_window_mac.h @@ -49,6 +49,7 @@ class NativeWindowMac : public NativeWindow { virtual void FlashFrame(bool flash) OVERRIDE; virtual void SetKiosk(bool kiosk) OVERRIDE; virtual bool IsKiosk() OVERRIDE; + virtual gfx::NativeWindow GetNativeWindow() OVERRIDE; NSWindow*& window() { return window_; } diff --git a/browser/native_window_mac.mm b/browser/native_window_mac.mm index 48d324fb4edf..a586e8b792fb 100644 --- a/browser/native_window_mac.mm +++ b/browser/native_window_mac.mm @@ -360,6 +360,10 @@ bool NativeWindowMac::IsKiosk() { return is_kiosk_; } +gfx::NativeWindow NativeWindowMac::GetNativeWindow() { + return window(); +} + void NativeWindowMac::HandleKeyboardEvent( content::WebContents*, const content::NativeWebKeyboardEvent& event) { diff --git a/common/api/atom_extensions.h b/common/api/atom_extensions.h index 71c63f640b0a..4faacb02cdb3 100644 --- a/common/api/atom_extensions.h +++ b/common/api/atom_extensions.h @@ -10,6 +10,7 @@ NODE_EXT_LIST_START // Module names start with `atom_browser_` can only be used by browser process. NODE_EXT_LIST_ITEM(atom_browser_app) +NODE_EXT_LIST_ITEM(atom_browser_file_dialog) NODE_EXT_LIST_ITEM(atom_browser_ipc) NODE_EXT_LIST_ITEM(atom_browser_window) diff --git a/renderer/api/lib/dialog.coffee b/renderer/api/lib/dialog.coffee new file mode 100644 index 000000000000..6ac381f9b6e3 --- /dev/null +++ b/renderer/api/lib/dialog.coffee @@ -0,0 +1,41 @@ +ipc = require 'ipc' + +callbackId = 0 +callbacks = {} + +storeCallback = (callback) -> + throw new TypeError('Bad argument') unless typeof callback is 'function' + + ++callbackId + callbacks[callbackId] = callback + callbackId + +makeCallback = (id, args...) -> + callbacks[id].call global, args... + delete callbacks[id] + +# Force loading dialog code in browser. +remote.require 'dialog' + +ipc.on 'ATOM_RENDERER_DIALOG', (id, args...) -> + makeCallback(id, args...) + +callFileDialogs = (options, callback, args...) -> + if typeof options is 'function' + callback = options + options = {} + + ipc.sendChannel 'ATOM_BROWSER_FILE_DIALOG', storeCallback(callback), args..., options + +module.exports = + openFolder: (options, callback) -> + callFileDialogs options, callback, 1, 'Open Folder' + + saveAs: (options, callback) -> + callFileDialogs options, callback, 2, 'Save As' + + openFile: (options, callback) -> + callFileDialogs options, callback, 3, 'Open File' + + openMultiFiles: (options, callback) -> + callFileDialogs options, callback, 4, 'Open Files'