Skip to content
This repository has been archived by the owner on Sep 2, 2021. It is now read-only.

Implement Windows Explorer "Open with..." option (no .msi) #299

Merged
93 changes: 90 additions & 3 deletions appshell/cefclient_win.cpp
Expand Up @@ -35,6 +35,9 @@

#define CLOSING_PROP L"CLOSING"

#define FIRST_INSTANCE_MUTEX_NAME L".Shell.Instance"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why no product or project identifier here?

"com.brackets.shell.instance" which I guess would be "com.,adobe.edge.code.shell.instance" for Edge Code. If Brackets and Edge Code are both installed it will get confused.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JeffryBooher I pre-pend APP_NAME to the string in the spots that I use this. That differentiates the mutex between EC and Brackets when they are both running. Having said that, I can move the pre-pending code directly into the #define to make it more readable. (I was doing this previously when I was using the static const TCHAR variable, but I should've moved it into the #define when I refactored it the last time for you.)

#define ID_WM_COPYDATA_SENDOPENFILECOMMAND (WM_USER+1001)

// Global Variables:
DWORD g_appStartupTime;
HINSTANCE hInst; // current instance
Expand Down Expand Up @@ -142,6 +145,32 @@ std::wstring GetFilenamesFromCommandLine() {
return result;
}

// EnumWindowsProc callback function
// - searches for an already running Brackets application window
BOOL CALLBACK FindSuitableBracketsInstance(HWND hwnd, LPARAM lParam)
{
ASSERT(lParam != NULL); // must be passed an HWND pointer to return, if found

// check for the Brackets application window by class name and title
WCHAR cName[MAX_PATH+1] = {0}, cTitle[MAX_PATH+1] = {0};
::GetClassName(hwnd, cName, MAX_PATH);
::GetWindowText(hwnd, cTitle, MAX_PATH);
if ((wcscmp(cName, szWindowClass) == 0) && (wcsstr(cTitle, APP_NAME) != 0)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should also check to see if there is a Modal Dialog open and not try to send it a message in that case. It would be great to check for a brackets modal dialog as well but I think we should at least check that a File Open / Save As, etc... dialog isn't open

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can do that by enumerating child windows and looking for a window with the class "#32770"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the check for the modal dialog by checking if the main window is disabled. As you noted, this won't catch the JS "modal" dialogs, like the About box. However, as those aren't really modal, the selected file still gets opened anyway. Consequently, I didn't bother iterating the child windows to watch for these. It still seems to work as expected. However, if you're really bothered by this, I can add it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bchintx yep, i thought about suggesting the disabled window check but wasn't sure if that was the only case. I think i t's fine to error on that side and it is much easier to test for a disabled window rather than enumerating the child windows.

// found an already running instance of Brackets. Now, check that that window
// isn't currently disabled (eg. modal dialog). If it is keep searching.
if ((::GetWindowLong(hwnd, GWL_STYLE) & WS_DISABLED) == 0) {
//return the window handle and stop searching
*(HWND*)lParam = hwnd;
return FALSE;
}
}

return TRUE; // otherwise, continue searching
}

// forward declaration; implemented in appshell_extensions_win.cpp
void ConvertToUnixPath(ExtensionString& filename);

// Program entry point function.
int APIENTRY wWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
Expand All @@ -167,6 +196,43 @@ int APIENTRY wWinMain(HINSTANCE hInstance,
// Parse command line arguments. The passed in values are ignored on Windows.
AppInitCommandLine(0, NULL);

// Initialize global strings
LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadString(hInstance, IDC_CEFCLIENT, szWindowClass, MAX_LOADSTRING);

// Determine if we should use an already running instance of Brackets.
HANDLE hMutex = ::OpenMutex(MUTEX_ALL_ACCESS, FALSE, APP_NAME FIRST_INSTANCE_MUTEX_NAME);
if ((hMutex != NULL) && AppGetCommandLine()->HasArguments() && (lpCmdLine != NULL)) {
// for subsequent instances, re-use an already running instance if we're being called to
// open an existing file on the command-line (eg. Open With.. from Windows Explorer)
HWND hFirstInstanceWnd = NULL;
::EnumWindows(FindSuitableBracketsInstance, (LPARAM)&hFirstInstanceWnd);
if (hFirstInstanceWnd != NULL) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like the indentation got messed up here.

::SetForegroundWindow(hFirstInstanceWnd);
if (::IsIconic(hFirstInstanceWnd))
::ShowWindow(hFirstInstanceWnd, SW_RESTORE);

// message the other Brackets instance to actually open the given filename
std::wstring wstrFilename = lpCmdLine;
ConvertToUnixPath(wstrFilename);
// note: WM_COPYDATA will manage passing the string across process space
COPYDATASTRUCT data;
data.dwData = ID_WM_COPYDATA_SENDOPENFILECOMMAND;
data.cbData = (wstrFilename.length() + 1) * sizeof(WCHAR);
data.lpData = (LPVOID)wstrFilename.c_str();
::SendMessage(hFirstInstanceWnd, WM_COPYDATA, (WPARAM)(HWND)hFirstInstanceWnd, (LPARAM)(LPVOID)&data);

// exit this instance
return 0;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like maybe you have convert tabs to spaces turned on for C++ I think maybe turn that off and fix the block above and we're good to go.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. Lemme redo this again. I had re-indented using Brackets per an earlier comment. Lemme go back to VS2010 to indent.

// otherwise, fall thru and launch a new instance
}

if (hMutex == NULL) {
// first instance of this app, so create the mutex and continue execution of this instance.
hMutex = ::CreateMutex(NULL, FALSE, APP_NAME FIRST_INSTANCE_MUTEX_NAME);
}

CefSettings settings;

// Populate the settings based on command line arguments.
Expand All @@ -180,9 +246,7 @@ int APIENTRY wWinMain(HINSTANCE hInstance,
// Initialize CEF.
CefInitialize(main_args, settings, app.get());

// Initialize global strings
LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadString(hInstance, IDC_CEFCLIENT, szWindowClass, MAX_LOADSTRING);
// Register window class
MyRegisterClass(hInstance, *(app->GetCurrentLanguage().GetStruct()));

CefRefPtr<CefCommandLine> cmdLine = AppGetCommandLine();
Expand Down Expand Up @@ -272,6 +336,10 @@ int APIENTRY wWinMain(HINSTANCE hInstance,
// Shut down CEF.
CefShutdown();

// release the first instance mutex
if (hMutex != NULL)
ReleaseMutex(hMutex);

return result;
}

Expand Down Expand Up @@ -867,6 +935,25 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam,
PostQuitMessage(0);
return 0;

case WM_COPYDATA:
// handle the interprocess communication request from another Brackets running instance
if (lParam != NULL) {
PCOPYDATASTRUCT data = (PCOPYDATASTRUCT)lParam;
if ((data->dwData == ID_WM_COPYDATA_SENDOPENFILECOMMAND) && (data->cbData > 0)) {
// another Brackets instance requests that we open the given filename
std::wstring wstrFilename = (LPCWSTR)data->lpData;
// Windows Explorer might enclose the filename in double-quotes. We need to strip these off.
if ((wstrFilename.front() == '\"') && wstrFilename.back() == '\"')
wstrFilename = wstrFilename.substr(1, wstrFilename.length() - 2);
ASSERT(g_handler != NULL);
CefRefPtr<CefBrowser> browser = g_handler->GetBrowser();
// call into Javascript code to handle the open file command
ASSERT(browser != NULL);
g_handler->SendOpenFileCommand(browser, CefString(wstrFilename.c_str()));
}
}
break;

case WM_INITMENUPOPUP:
// Notify before popping up
g_handler->SendJSCommand(g_handler->GetBrowser(), APP_BEFORE_MENUPOPUP);
Expand Down
25 changes: 25 additions & 0 deletions installer/win/Brackets.wxs
Expand Up @@ -42,6 +42,29 @@ xmlns:fire="http://schemas.microsoft.com/wix/FirewallExtension">
</Component>
</DirectoryRef>

<Component Id="FileAssociations" Guid="{D0195E8D-0881-42B6-9B4F-DA84D9396506}" Directory="INSTALLDIR" KeyPath="yes">
<!-- Capabilities keys for Vista/7 "Set Program Access and Defaults" -->
<RegistryValue Root="HKLM" Key="SOFTWARE\$(var.RegistryRoot)\Capabilities" Name="ApplicationIcon" Value="[INSTALLDIR]$(var.ExeName).exe,0" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\$(var.RegistryRoot)\Capabilities" Name="ApplicationName" Value="!(loc.ProductName) $(var.ProductVersionName)" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\RegisteredApplications" Name="!(loc.ProductName) $(var.ProductVersionName)" Value="SOFTWARE\$(var.RegistryRoot)\Capabilities" Type="string" />

<!-- File associations -->
<?define SupportedFiletypes=txt;groovy;ini;properties;css;scss;html;htm;shtm;shtml;xhtml;cfm;cfm1;cfc;dhtml;xht;tpl;twig;hbs;handlebars;kit;jsp;aspx;ejs;js;jsx;json;svg;xml;wxs;wxl;wsdl;rss;atom;rdf;xslt;xul;xbl;mathml;config;php;php3;php4;php5;phtm;phtml;ctp;c;h;i;cc;cp;cpp;c++;cxx;hh;hpp;hxx;h++;ii;cs;cshtml;asax;ashx;java;scala;sbt;coffee;cson;cf;clj;pl;pm;rb;ru;gemspec;rake;py;pyw;wsgi;sass;less;lua;sql;diff;patch;md;markdown;yaml;yml;hx;sh?>

<?foreach filetype in $(var.SupportedFiletypes)?>
<!-- associate program with file type -->
<RegistryValue Root="HKLM" Key="SOFTWARE\$(var.RegistryRoot)\Capabilities\FileAssociations" Value="!(loc.ProductName) $(var.ProductVersionName) FileExt" Name=".$(var.filetype)" Type="string" />

<!-- associate each supported filetype with application -->
<RegistryValue Root="HKCR" Key=".$(var.filetype)" Value="text" Name="PerceivedType" Type="string" />
<RegistryValue Root="HKCR" Key=".$(var.filetype)\OpenWithProgids" Value="" Name="!(loc.ProductName) $(var.ProductVersionName) FileExt" Type="string" />
<?endforeach?>

<!-- create ProgId entry -->
<RegistryValue Root="HKLM" Key="SOFTWARE\Classes\!(loc.ProductName) $(var.ProductVersionName) FileExt\shell\open\command" Value="&quot;[INSTALLDIR]$(var.ExeName).exe&quot; &quot;%1&quot;" Type="string" />
<RegistryValue Root="HKLM" Key="SOFTWARE\Classes\Applications\$(var.ExeName).exe\shell\open" Name="FriendlyAppName" Value="!(loc.ProductName) $(var.ProductVersionName)" Type="string" />

</Component>
<!-- Start Menu Shortcuts-->
<UIRef Id="WixUI_MyInstallDir" />
<UIRef Id="WixUI_ErrorProgressText" />
Expand Down Expand Up @@ -78,6 +101,8 @@ xmlns:fire="http://schemas.microsoft.com/wix/FirewallExtension">
<ComponentGroupRef Id='BRACKETSHARVESTMANAGER'/>

<ComponentRef Id='StartMenuShortcut' />

<ComponentRef Id='FileAssociations' />
</Feature>
</Product>
</Wix>
Expand Down
2 changes: 1 addition & 1 deletion installer/win/brackets-win-install-build.xml
Expand Up @@ -16,7 +16,7 @@ default="build.mul">
<property name="product.version.number" value="0.${product.sprint.number}"/>
<property name="product.version.name" value="Sprint ${product.sprint.number}"/>
<property name="product.manufacturer" value="brackets.io"/>
<property name="product.registry.root" value="${product.shortname}"/>
<property name="product.registry.root" value="${product.shortname} ${product.version.name}"/>


<!-- Installer properties -->
Expand Down