Bug 661991 - close file pickers when the parent window closes. r=ehsan
authorJim Mathies <jmathies@mozilla.com>
Wed, 14 Dec 2011 15:22:28 -0600
changeset 82588 7f5a60c89420338872650a5a9e2c9fbae04bad95
parent 82587 487e81eb1b6bc360ef5e5673419c0208ee770a4f
child 82589 77bcf367126074b279542b17f462ec0808f00566
push idunknown
push userunknown
push dateunknown
reviewersehsan
bugs661991
milestone11.0a1
Bug 661991 - close file pickers when the parent window closes. r=ehsan
widget/src/windows/nsFilePicker.cpp
widget/src/windows/nsFilePicker.h
widget/src/windows/nsWindow.h
--- a/widget/src/windows/nsFilePicker.cpp
+++ b/widget/src/windows/nsFilePicker.cpp
@@ -58,16 +58,20 @@
 #include "nsEnumeratorUtils.h"
 #include "nsCRT.h"
 #include "nsString.h"
 #include "nsToolkit.h"
 
 PRUnichar *nsFilePicker::mLastUsedUnicodeDirectory;
 char nsFilePicker::mLastUsedDirectory[MAX_PATH+1] = { 0 };
 
+static const PRUnichar kDialogPtrProp[] = L"DialogPtrProperty";
+static const DWORD kDialogTimerID = 9999;
+static const unsigned long kDialogTimerTimeout = 300;
+
 #define MAX_EXTENSION_LENGTH 10
 
 ///////////////////////////////////////////////////////////////////////////////
 // Helper classes
 
 // Manages matching SuppressBlurEvents calls on the parent widget.
 class AutoSuppressEvents
 {
@@ -153,21 +157,54 @@ private:
         mWindow->PickerOpen();
       else
         mWindow->PickerClosed();
     }
   }
   nsRefPtr<nsWindow> mWindow;
 };
 
+// Manages a simple callback timer
+class AutoTimerCallbackCancel
+{
+public:
+  AutoTimerCallbackCancel(nsFilePicker* aTarget,
+                          nsTimerCallbackFunc aCallbackFunc) {
+    Init(aTarget, aCallbackFunc);
+  }
+
+  ~AutoTimerCallbackCancel() {
+    if (mPickerCallbackTimer) {
+      mPickerCallbackTimer->Cancel();
+    }
+  }
+
+private:
+  void Init(nsFilePicker* aTarget,
+            nsTimerCallbackFunc aCallbackFunc) {
+    mPickerCallbackTimer = do_CreateInstance("@mozilla.org/timer;1");
+    if (!mPickerCallbackTimer) {
+      NS_WARNING("do_CreateInstance for timer failed??");
+      return;
+    }
+    mPickerCallbackTimer->InitWithFuncCallback(aCallbackFunc,
+                                               aTarget,
+                                               kDialogTimerTimeout,
+                                               nsITimer::TYPE_REPEATING_SLACK);
+  }
+  nsCOMPtr<nsITimer> mPickerCallbackTimer;
+    
+};
+
 ///////////////////////////////////////////////////////////////////////////////
 // nsIFilePicker
 
 nsFilePicker::nsFilePicker() :
   mSelectedType(1)
+  , mDlgWnd(NULL)
 #if MOZ_WINSDK_TARGETVER >= MOZ_NTDDI_LONGHORN
   , mFDECookie(0)
 #endif
 {
    CoInitialize(NULL);
 }
 
 nsFilePicker::~nsFilePicker()
@@ -176,16 +213,17 @@ nsFilePicker::~nsFilePicker()
     NS_Free(mLastUsedUnicodeDirectory);
     mLastUsedUnicodeDirectory = nsnull;
   }
   CoUninitialize();
 }
 
 NS_IMPL_ISUPPORTS1(nsFilePicker, nsIFilePicker)
 
+
 #if MOZ_WINSDK_TARGETVER >= MOZ_NTDDI_LONGHORN
 STDMETHODIMP nsFilePicker::QueryInterface(REFIID refiid, void** ppvResult)
 {
   *ppvResult = NULL;
   if (IID_IUnknown == refiid ||
       refiid == IID_IFileDialogEvents) {
     *ppvResult = this;
   }
@@ -233,53 +271,96 @@ EnsureWindowVisible(HWND hwnd)
                           parentRect.left, 
                           parentRect.top, 0, 0, 
                           SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOZORDER);
   }
 }
 
 // Callback hook which will ensure that the window is visible. Currently
 // only in use on os <= XP.
-static UINT_PTR CALLBACK
-FilePickerHook(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) 
+UINT_PTR CALLBACK
+nsFilePicker::FilePickerHook(HWND hwnd,
+                             UINT msg,
+                             WPARAM wParam,
+                             LPARAM lParam) 
 {
-  if (msg == WM_NOTIFY) {
-    LPOFNOTIFYW lpofn = (LPOFNOTIFYW) lParam;
-    if (!lpofn || !lpofn->lpOFN) {
-      return 0;
-    }
-    
-    if (CDN_INITDONE == lpofn->hdr.code) {
-      // The Window will be automatically moved to the last position after
-      // CDN_INITDONE.  We post a message to ensure the window will be visible
-      // so it will be done after the automatic last position window move.
-      PostMessage(hwnd, MOZ_WM_ENSUREVISIBLE, 0, 0);
-    }
-  } else if (msg == MOZ_WM_ENSUREVISIBLE) {
-    EnsureWindowVisible(GetParent(hwnd));
+  switch(msg) {
+    case WM_NOTIFY:
+      {
+        LPOFNOTIFYW lpofn = (LPOFNOTIFYW) lParam;
+        if (!lpofn || !lpofn->lpOFN) {
+          return 0;
+        }
+        
+        if (CDN_INITDONE == lpofn->hdr.code) {
+          // The Window will be automatically moved to the last position after
+          // CDN_INITDONE.  We post a message to ensure the window will be visible
+          // so it will be done after the automatic last position window move.
+          PostMessage(hwnd, MOZ_WM_ENSUREVISIBLE, 0, 0);
+        }
+      }
+      break;
+    case MOZ_WM_ENSUREVISIBLE:
+      EnsureWindowVisible(GetParent(hwnd));
+      break;
+    case WM_INITDIALOG:
+      {
+        OPENFILENAMEW* pofn = reinterpret_cast<OPENFILENAMEW*>(lParam);
+        SetProp(hwnd, kDialogPtrProp, (HANDLE)pofn->lCustData);
+        nsFilePicker* picker = reinterpret_cast<nsFilePicker*>(pofn->lCustData);
+        if (picker) {
+          picker->SetDialogHandle(hwnd);
+          SetTimer(hwnd, kDialogTimerID, kDialogTimerTimeout, NULL);
+        }
+      }
+      break;
+    case WM_TIMER:
+      {
+        // Check to see if our parent has been torn down, if so, we close too.
+        if (wParam == kDialogTimerID) {
+          nsFilePicker* picker = 
+            reinterpret_cast<nsFilePicker*>(GetProp(hwnd, kDialogPtrProp));
+          if (picker && picker->ClosePickerIfNeeded(true)) {
+            KillTimer(hwnd, kDialogTimerID);
+          }
+        }
+      }
+      break;
   }
   return 0;
 }
 
 
 // Callback hook which will dynamically allocate a buffer large enough
 // for the file picker dialog.  Currently only in use on  os <= XP.
-static UINT_PTR CALLBACK
-MultiFilePickerHook(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
+UINT_PTR CALLBACK
+nsFilePicker::MultiFilePickerHook(HWND hwnd,
+                                  UINT msg,
+                                  WPARAM wParam,
+                                  LPARAM lParam)
 {
   switch (msg) {
     case WM_INITDIALOG:
       {
         // Finds the child drop down of a File Picker dialog and sets the 
         // maximum amount of text it can hold when typed in manually.
         // A wParam of 0 mean 0x7FFFFFFE characters.
         HWND comboBox = FindWindowEx(GetParent(hwnd), NULL, 
                                      L"ComboBoxEx32", NULL );
         if(comboBox)
           SendMessage(comboBox, CB_LIMITTEXT, 0, 0);
+        // Store our nsFilePicker ptr for future use
+        OPENFILENAMEW* pofn = reinterpret_cast<OPENFILENAMEW*>(lParam);
+        SetProp(hwnd, kDialogPtrProp, (HANDLE)pofn->lCustData);
+        nsFilePicker* picker =
+          reinterpret_cast<nsFilePicker*>(pofn->lCustData);
+        if (picker) {
+          picker->SetDialogHandle(hwnd);
+          SetTimer(hwnd, kDialogTimerID, kDialogTimerTimeout, NULL);
+        }
       }
       break;
     case WM_NOTIFY:
       {
         LPOFNOTIFYW lpofn = (LPOFNOTIFYW) lParam;
         if (!lpofn || !lpofn->lpOFN) {
           return 0;
         }
@@ -322,16 +403,28 @@ MultiFilePickerHook(HWND hwnd, UINT msg,
             ZeroMemory(filesBuffer, newBufLength * sizeof(PRUnichar));
 
             lpofn->lpOFN->lpstrFile = filesBuffer;
             lpofn->lpOFN->nMaxFile  = newBufLength;
           }
         }
       }
       break;
+    case WM_TIMER:
+      {
+        // Check to see if our parent has been torn down, if so, we close too.
+        if (wParam == kDialogTimerID) {
+          nsFilePicker* picker =
+            reinterpret_cast<nsFilePicker*>(GetProp(hwnd, kDialogPtrProp));
+          if (picker && picker->ClosePickerIfNeeded(true)) {
+            KillTimer(hwnd, kDialogTimerID);
+          }
+        }
+      }
+      break;
   }
 
   return FilePickerHook(hwnd, msg, wParam, lParam);
 }
 
 /*
  * Vista+ callbacks
  */
@@ -369,30 +462,93 @@ nsFilePicker::OnShareViolation(IFileDial
                                FDE_SHAREVIOLATION_RESPONSE *pResponse)
 {
   return S_OK;
 }
 
 HRESULT
 nsFilePicker::OnTypeChange(IFileDialog *pfd)
 {
+  // Failures here result in errors due to security concerns.
+  nsRefPtr<IOleWindow> win;
+  pfd->QueryInterface(IID_IOleWindow, getter_AddRefs(win));
+  if (!win) {
+    NS_ERROR("Could not retrieve the IOleWindow interface for IFileDialog.");
+    return S_OK;
+  }
+  HWND hwnd = NULL;
+  win->GetWindow(&hwnd);
+  if (!hwnd) {
+    NS_ERROR("Could not retrieve the HWND for IFileDialog.");
+    return S_OK;
+  }
+  
+  SetDialogHandle(hwnd);
   return S_OK;
 }
 
 HRESULT
 nsFilePicker::OnOverwrite(IFileDialog *pfd,
                           IShellItem *psi,
                           FDE_OVERWRITE_RESPONSE *pResponse)
 {
   return S_OK;
 }
 
 #endif // MOZ_NTDDI_LONGHORN
 
 /*
+ * Close on parent close logic
+ */
+
+bool
+nsFilePicker::ClosePickerIfNeeded(bool aIsXPDialog)
+{
+  if (!mParentWidget || !mDlgWnd)
+    return false;
+
+  nsWindow *win = static_cast<nsWindow *>(mParentWidget.get());
+  // Note, the xp callbacks hand us an inner window, so we have to step up
+  // one to get the actual dialog.
+  HWND dlgWnd;
+  if (aIsXPDialog)
+    dlgWnd = GetParent(mDlgWnd);
+  else
+    dlgWnd = mDlgWnd;
+  if (IsWindow(dlgWnd) && IsWindowVisible(dlgWnd) && win->DestroyCalled()) {
+    PRUnichar className[64];
+    // Make sure we have the right window
+    if (GetClassNameW(dlgWnd, className, mozilla::ArrayLength(className)) &&
+        !wcscmp(className, L"#32770") &&
+        DestroyWindow(dlgWnd)) {
+      mDlgWnd = NULL;
+      return true;
+    }
+  }
+  return false;
+}
+
+void
+nsFilePicker::PickerCallbackTimerFunc(nsITimer *aTimer, void *aCtx)
+{
+  nsFilePicker* picker = (nsFilePicker*)aCtx;
+  if (picker->ClosePickerIfNeeded(false)) {
+    aTimer->Cancel();
+  }
+}
+
+void
+nsFilePicker::SetDialogHandle(HWND aWnd)
+{
+  if (!aWnd || mDlgWnd)
+    return;
+  mDlgWnd = aWnd;
+}
+
+/*
  * Folder picker invocation
  */
 
 // Open the older XP style folder picker dialog. We end up in this call
 // on XP systems or when platform is built without the longhorn SDK.
 bool
 nsFilePicker::ShowXPFolderPicker(const nsString& aInitialDir)
 {
@@ -407,16 +563,17 @@ nsFilePicker::ShowXPFolderPicker(const n
 
   BROWSEINFOW browserInfo = {0};
   browserInfo.pidlRoot       = nsnull;
   browserInfo.pszDisplayName = (LPWSTR)dirBuffer;
   browserInfo.lpszTitle      = mTitle.get();
   browserInfo.ulFlags        = BIF_USENEWUI | BIF_RETURNONLYFSDIRS;
   browserInfo.hwndOwner      = adtw.get(); 
   browserInfo.iImage         = nsnull;
+  browserInfo.lParam         = reinterpret_cast<LPARAM>(this);
 
   if (!aInitialDir.IsEmpty()) {
     // the dialog is modal so that |initialDir.get()| will be valid in 
     // BrowserCallbackProc. Thus, we don't need to clone it.
     browserInfo.lParam = (LPARAM) aInitialDir.get();
     browserInfo.lpfn   = &BrowseCallbackProc;
   } else {
     browserInfo.lParam = nsnull;
@@ -531,16 +688,17 @@ nsFilePicker::ShowXPFilePicker(const nsS
     mParentWidget->GetNativeData(NS_NATIVE_TMP_WINDOW) : NULL));
 
   ofn.lpstrTitle   = (LPCWSTR)mTitle.get();
   ofn.lpstrFilter  = (LPCWSTR)filterBuffer.get();
   ofn.nFilterIndex = mSelectedType;
   ofn.lpstrFile    = fileBuffer;
   ofn.nMaxFile     = FILE_BUFFER_SIZE;
   ofn.hwndOwner    = adtw.get();
+  ofn.lCustData    = reinterpret_cast<LPARAM>(this);
   ofn.Flags = OFN_SHAREAWARE | OFN_LONGNAMES | OFN_OVERWRITEPROMPT |
               OFN_HIDEREADONLY | OFN_PATHMUSTEXIST | OFN_ENABLESIZING | 
               OFN_EXPLORER;
 
   // Windows Vista and up won't allow you to use the new looking dialogs with
   // a hook procedure.  The hook procedure fixes a problem on XP dialogs for
   // file picker visibility.  Vista and up automatically ensures the file 
   // picker is always visible.
@@ -792,25 +950,28 @@ nsFilePicker::ShowFilePicker(const nsStr
   // filter types and the default index
   if (!mComFilterList.IsEmpty()) {
     dialog->SetFileTypes(mComFilterList.Length(), mComFilterList.get());
     dialog->SetFileTypeIndex(mSelectedType);
   }
 
   // display
 
-  AutoDestroyTmpWindow adtw((HWND)(mParentWidget.get() ?
-    mParentWidget->GetNativeData(NS_NATIVE_TMP_WINDOW) : NULL));
+  {
+    AutoDestroyTmpWindow adtw((HWND)(mParentWidget.get() ?
+      mParentWidget->GetNativeData(NS_NATIVE_TMP_WINDOW) : NULL));
+    AutoTimerCallbackCancel atcc(this, PickerCallbackTimerFunc);
+    AutoWidgetPickerState awps(mParentWidget);
 
-  AutoWidgetPickerState awps(mParentWidget);
-  if (FAILED(dialog->Show(adtw.get()))) {
+    if (FAILED(dialog->Show(adtw.get()))) {
+      dialog->Unadvise(mFDECookie);
+      return false;
+    }
     dialog->Unadvise(mFDECookie);
-    return false;
   }
-  dialog->Unadvise(mFDECookie);
 
   // results
 
   // single selection
   if (mMode != modeOpenMultiple) {
     nsRefPtr<IShellItem> item;
     if (FAILED(dialog->GetResult(getter_AddRefs(item))) || !item) {
       return false;
--- a/widget/src/windows/nsFilePicker.h
+++ b/widget/src/windows/nsFilePicker.h
@@ -52,16 +52,17 @@
 #define _WIN32_WINNT _WIN32_WINNT_VISTA
 #define _WIN32_IE_bak _WIN32_IE
 #undef _WIN32_IE
 #define _WIN32_IE _WIN32_IE_IE70
 #endif
 #endif
 
 #include "nsILocalFile.h"
+#include "nsITimer.h"
 #include "nsISimpleEnumerator.h"
 #include "nsCOMArray.h"
 #include "nsAutoPtr.h"
 #include "nsICharsetConverterManager.h"
 #include "nsBaseFilePicker.h"
 #include "nsString.h"
 #include "nsdefs.h"
 #include <commdlg.h>
@@ -129,63 +130,69 @@ protected:
   bool ShowXPFolderPicker(const nsString& aInitialDir);
   bool ShowFilePicker(const nsString& aInitialDir);
   bool ShowXPFilePicker(const nsString& aInitialDir);
   void AppendXPFilter(const nsAString& aTitle, const nsAString& aFilter);
   void RememberLastUsedDirectory();
   bool IsPrivacyModeEnabled();
   bool IsDefaultPathLink();
   bool IsDefaultPathHtml();
+  void SetDialogHandle(HWND aWnd);
+  bool ClosePickerIfNeeded(bool aIsXPDialog);
+  static void PickerCallbackTimerFunc(nsITimer *aTimer, void *aPicker);
+  static UINT_PTR CALLBACK MultiFilePickerHook(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
+  static UINT_PTR CALLBACK FilePickerHook(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
 
   nsCOMPtr<nsIWidget>    mParentWidget;
   nsString               mTitle;
   PRInt16                mMode;
   nsCString              mFile;
   nsString               mDefaultFilePath;
   nsString               mDefaultFilename;
   nsString               mDefaultExtension;
   nsString               mFilterList;
   PRInt16                mSelectedType;
   nsCOMArray<nsILocalFile> mFiles;
   static char            mLastUsedDirectory[];
   nsString               mUnicodeFile;
   static PRUnichar      *mLastUsedUnicodeDirectory;
-  DWORD                  mFDECookie;
+  HWND                   mDlgWnd;
 
 #if MOZ_WINSDK_TARGETVER >= MOZ_NTDDI_LONGHORN
   class ComDlgFilterSpec
   {
   public:
     ComDlgFilterSpec() :
       mSpecList(nsnull),
       mLength(0) {}
     ~ComDlgFilterSpec() {
       free(mSpecList);
     }
     
-    PRUint32 Length() {
+    const PRUint32 Length() {
       return mLength;
     }
 
-    bool IsEmpty() {
+    const bool IsEmpty() {
       return (mLength == 0);
     }
 
     const COMDLG_FILTERSPEC* get() {
       return mSpecList;
     }
     
     void Append(const nsAString& aTitle, const nsAString& aFilter);
   private:
     COMDLG_FILTERSPEC* mSpecList;
     nsAutoTArray<nsString, 2> mStrings;
     PRUint32 mLength;
   };
 
-  ComDlgFilterSpec mComFilterList;
+  ComDlgFilterSpec       mComFilterList;
+  DWORD                  mFDECookie;
 #endif
 };
 
 #if MOZ_WINSDK_TARGETVER >= MOZ_NTDDI_LONGHORN
 #if defined(_WIN32_WINNT_bak)
 #undef _WIN32_WINNT
 #define _WIN32_WINNT _WIN32_WINNT_bak
 #undef _WIN32_IE
--- a/widget/src/windows/nsWindow.h
+++ b/widget/src/windows/nsWindow.h
@@ -305,16 +305,17 @@ public:
 #endif
 
   NS_IMETHOD              ReparentNativeWidget(nsIWidget* aNewParent);
 
   // Open file picker tracking
   void                    PickerOpen();
   void                    PickerClosed();
 
+  bool                    const DestroyCalled() { return mDestroyCalled; }
 protected:
 
   // A magic number to identify the FAKETRACKPOINTSCROLLABLE window created
   // when the trackpoint hack is enabled.
   enum { eFakeTrackPointScrollableID = 0x46545053 };
 
   /**
    * Callbacks