//***************************************************************************
//
//	Library:
//      FM_UTILS.DLL - version 1.1a
//
//	Purpose:
//		FM_UTILS is a File Manager extension DLL. An extension DLL adds
//		a menu to File Manager, contains an entry point that processes menu
//		commands and notification messages sent by File Manager, and
//		queries data and information about the File Manager windows. The 
//		purpose of an extension DLL is to add administration support 
//		features to File Manager, for example, file and disk utilities.
//		Up to five extension DLLs may be installed at any one time.
//
//		FM_UTILS adds a menu (called "Utilities" by default) to File Manager
//		and processes all the messages that are sent by File Manager to an 
//		extension DLL. In order to retrieve any information, it sends 
//		messages to File Manager.
//
//	Usage:
//		File Manager installs the extensions that have entries in the 
//		[AddOns] section of the WINFILE.INI initialization file. An entry 
//		consists of a tag and a value. To load FM_UTILS.DLL as a File 
//		Manager extension, add the following to WINFILE.INI (assuming the 
//		DLL resides in c:\win\system):
//
//			[AddOns]
//			FM_Utils Extension=fm_utils.dll
//
//		In addition, a section specific to this Extension DLL is added to
//		WINFILE.INI. There are several elements to this section:
//		For each utility to appear on the menu, there is a 'UserProgn=' tag,
//		and a 'UserDescn=' tag. Comments (preceded by semicolons) are optional.
//		You may have up to twenty separate utilities specified.
//		You can also specify up to five utilities to load at the same time as
//		FileMan, and exit when you close FileMan. You use the 'PreLoadn='
//		tag for this. All of the utilities are started with the SW_SHOWMINNOACTIVE
//		setting (minimized, but not activated).
//		And, you can specify the name of the added menu. If you choose not to,
//		the default is "&Utilities". This provides foreign language support,
//		as well as allowing you to use a very short name, to keep the menu bar
//		from growing too long. The tag to use is 'UserMenuName='.
//
//			[FM_Utils Extension]
//			;Name the menu
//			UserMenuName=&Util
//			;add Upper Deck Editor to menu
//			UserDesc1=&Edit File(s)
//			UserProg1=D:\UDE\UDE.EXE
//			;add V. Buerg's LIST to menu
//			UserDesc2=&View File(s)
//			UserProg2=LIST.PIF
//			;PreLoad Trash Can
//			PreLoad1=TRASH.EXE
//
//	Revision History:
//		1.0 - March '92 - initial release
//
//		1.1 - April '92 - added ability to PreLoad utilities. This seems even
//							more popular than the original DLL.
//
//		1.1a - August '92 - added ability to specify menu name; changed
//							SendMessage(...WM_CLOSE...) to PostMessage(...WM_CLOSE...)
//							since it seemed to make more sense.
//
//	Notes:
//		Portions of the base code for this utility is based on, and borrowed
//		from, the XTENSION sample code provided with the Microsoft Windows
//		3.1 SDK.
//
//		FM_UTILS attempts to close any auto-started utilities by posting a
//		WM_CLOSE to it's main window. There's no guarantee that this method
//		will work for *all* programs. If you know of a better way to handle
//		this situation, I'd be *really* happy to hear about it.
//
//		While it's not properly documented anywhere, it seems that the maximum
//		command line to pass to WinExec() is 128 characters. Since we have to
//		pass in complete pathnames for all files in order for this to work,
//		we may not be able to pass *all* selected file names.
//
//	Copyright (c) 1992 by Brad P. Smith - all rights reserved
//
//		snail:	R.R. #2 - 777 Crozier Rd.
//				Oxford Mills, Ontario, CANADA
//				K0G 1S0
//		E-Mail:	'B.P.Smith' on BIX, or 'smithb@cognos.com' on Internet
//
//		This utility DLL is FreeWare. Don't let *anybody* charge you
//		for it.
//		However, I would *really* appreciate hearing from you if you find
//		FM_UTILS useful. Drop me a postcard or E-mail. I could use the moral
//		support. :-)
//
//***************************************************************************
#include <windows.h>
#include <string.h>
#include <direct.h>
#include <wfext.h>

#include "fm_utils.h"

//******************** Global Variables

HANDLE	ghDllInst;						// DLL's instance handle
HMENU	ghMenu;							// Extension's menu handle
WORD	gwMenuDelta;					// Delta for extension's menu items

USERPROG	UserProg[ NUM_USERPROGS ];
PRELOADPROG	PreLoadProg[ NUM_PRELOADPROGS ];
char		szTempBuffer[ PATH_NAME_LEN ];

//***************************************************************************
//
//	LibMain()
//
//	Purpose:
//		LibMain is called by LibEntry. LibEntry is called by Windows
//		when the DLL is loaded. The LibEntry routine is provided
//		in the LIBENTRY.OBJ in the SDK Link Libraries disk. (The
//		source LIBENTRY.ASM is also provided.)
//
//		LibEntry initializes the DLL's heap if a HEAPSIZE value is
//		specified in the DLL's DEF file. After this, LibEntry calls
//		LibMain. The LibMain function below satisfies that call.
//
//		The LibMain function should perform additional initialization
//		tasks required by the DLL. In this DLL, no initialization
//		tasks are required; only the DLL's instance handle is saved. 
//		LibMain should return a value of TRUE if the initialization is 
//		successful.
//
//	Parameters:
//		hLibInst		- DLLs instance handle
//		wDataSeg		- Data segment
//		cbHeapSize		- Size of the DLL's heap
//		lpszCmdLine 	- Command line
//
//	Return Value:
//		TRUE if the initialization is successful; FALSE otherwise.
//
//***************************************************************************
int CALLBACK LibMain( HANDLE hLibInst, WORD wDataSeg,
						WORD cbHeapSize, LPSTR lpszCmdLine )
{
	ghDllInst = hLibInst;
	return TRUE;
}

//***************************************************************************
//
//	WEP()
//
//	Purpose:
//		Performs cleanup tasks when the .DLL is unloaded. The WEP() is
//		called automatically by Windows when the DLL is unloaded.
//		
//		Make sure that the WEP() is @1 RESIDENTNAME in the EXPORTS
//		section of the .DEF file. This ensures that the WEP() can
//		be called as quickly as possible. Incidently, this is why
//		the WEP() is called the WEP() instead of WindowsExitProcedure().
//		It takes up the minimum amount of space and is quickly located.
//
//	Parameters:
//		bSystemExit 	- Type of exit
//
//	Return Value:
//		TRUE.
//
//***************************************************************************
int CALLBACK WEP( int bSystemExit )
{
	return TRUE;
}


//***************************************************************************
//
//	FMExtensionProc()
//
//	Purpose:
//		This is an application-defined callback function. It processes menu 
//		commands and messages sent to FM_UTILS.DLL.
//
//	Parameters:
//		hWndExtension	- Identifies the File Manager window
//		wMessage		- Message sent to extension DLL
//		lParam			- Message information
//
//	Return Value:
//		When the wMessage is FMEVENT_LOAD, the handle to extension's menu
//		should be returned; otherwise a NULL value.
//
//***************************************************************************
HMENU CALLBACK FMExtensionProc( HWND hWndExtension, WORD wMessage, LONG lParam )
{
	LPFMS_LOAD	lpload;
	FARPROC	lpDialogProc;

	switch( wMessage )
	{
		case FMEVENT_LOAD:
			lpload	= (LPFMS_LOAD) lParam;

		// Assign the menu handle from the DLL's resource

			ghMenu = LoadMenu( ghDllInst, "UtilsExtensionMenu" );
			lpload->hMenu = ghMenu;

		// This is the delta we are being assigned.

			gwMenuDelta = lpload->wMenuDelta;

			lpload->dwSize = sizeof( FMS_LOAD );
		
		// Assign the popup menu name for this extension

			NameMenu( lpload->szMenuName );

			InitializeMenu( ghMenu );
			PreLoadPrograms();
			return ghMenu;

	// Kill any utilities we started, and free up allocated memory

		case FMEVENT_UNLOAD:
			KillPreLoadedPrograms();
			CleanUpMenu();
			break;

		case IDM_ABOUT:
			lpDialogProc = (FARPROC)AboutDlgProc;
			DialogBoxParam( ghDllInst, "About", hWndExtension,
							lpDialogProc, (LONG)hWndExtension );
			break;

		default:

		// If one of our menu items chosen, process it, otherwise ignore

			if( wMessage >= IDM_USERPROG && wMessage <= IDM_MAXUSERPROG )
				UserProgram( wMessage-IDM_USERPROG, hWndExtension );
			break;
	}
	return NULL;
}

//***************************************************************************
//
//	NameMenu()
//
//	Purpose:
//		We parse WINFILE.INI, looking for an entry 'UserMenuName='. If found
//		we use *that* name for our added menu, otherwise we use the default
//		"&Utilities"
//	
//	Parameters:
//		szName - the buffer to put the Menu Name into.
//
//	Return Value:
//		none.
//
//	Notes:
//		We don't perform *any* validity checking on the string, so it is quite
//		possible for the user to specify "&File" (for example) and totally
//		confuse him/herself (but not Windows, fortunately).
//
//***************************************************************************
void NameMenu( LPSTR szName )
{
	GetPrivateProfileString( (LPSTR)"FM_Utils Extension",
			(LPSTR)"UserMenuName", (LPSTR)"&Utilities", szName, MENU_TEXT_LEN,
			(LPSTR)"WINFILE.INI" );
}


//***************************************************************************
//
//	InitializeMenu()
//
//	Purpose:
//		We parse WINFILE.INI, looking for the list of User Programs and
//		descriptions to add to our menu. To save time, we dynamically
//		allocate buffers to store the program names & descriptions.
//		As we find each complete entry, we add it to the menu.
//	
//	Parameters:
//		none.
//
//	Return Value:
//		none.
//
//	Notes:
//		We really don't *need* to store the Description, because it's of
//		little use to us once it's added to the menu. However, we may
//		someday find use for it, so we may as well keep it.
//
//***************************************************************************
int InitializeMenu( HMENU hMenu )
{
	WORD wProgNum;
	int iNumItems = 0;
	char szUserProgEntry[ ENTRY_TAG_LENGTH ];

	for( wProgNum = 0; wProgNum < NUM_USERPROGS; ++wProgNum )
	{
		UserProg[wProgNum].fActive = FALSE;

	// Get the Utility's path

		wsprintf( szUserProgEntry, "UserProg%u", wProgNum+1 );
		GetPrivateProfileString( (LPSTR)"FM_Utils Extension",
				(LPSTR)szUserProgEntry, "", szTempBuffer, PATH_NAME_LEN,
				(LPSTR)"WINFILE.INI" );
		if( !( strlen( szTempBuffer )))
			break;
		else
		{
			UserProg[wProgNum].szPath=(NPSTR)LocalAlloc( LPTR, strlen( szTempBuffer )+1 );
			if( !UserProg[wProgNum].szPath )
			{
				MessageBox( NULL, "Memory Allocation Error\nCreating Extension Menu",
					"File Manager", MB_ICONEXCLAMATION | MB_OK );
				break;
			}
			strcpy( UserProg[wProgNum].szPath, szTempBuffer );
		}

	// Get the Description to add to the menu

		wsprintf( szUserProgEntry, "UserDesc%u", wProgNum+1 );
		GetPrivateProfileString( (LPSTR)"FM_Utils Extension",
				(LPSTR)szUserProgEntry, "", szTempBuffer, PROG_DESC_LENGTH,
				(LPSTR)"WINFILE.INI" );
		if( !( strlen( szTempBuffer )))
		{
			LocalFree( (HLOCAL)UserProg[wProgNum].szPath );
			break;
		}
		else
		{
			UserProg[wProgNum].szDescription=(NPSTR)LocalAlloc( LPTR, strlen( szTempBuffer )+1 );
			if( !UserProg[wProgNum].szDescription )
			{
				LocalFree( (HLOCAL)UserProg[wProgNum].szPath );
				MessageBox( NULL, "Memory Allocation Error\nCreating Extension Menu",
					"File Manager", MB_ICONEXCLAMATION | MB_OK );
				break;
			}
			strcpy( UserProg[wProgNum].szDescription, szTempBuffer );
		}

	// If all is okay, prepare to add this item to the menu

		UserProg[wProgNum].fActive = TRUE;
		AppendMenu( hMenu, MF_STRING | MF_ENABLED,
				IDM_USERPROG+wProgNum, (LPSTR)UserProg[wProgNum].szDescription );
		++iNumItems;
	}
	return iNumItems;
}


//***************************************************************************
//
//	CleanUpMenu()
//
//	Purpose:
//		To be nice folks, we go through and free up all of our allocated
//		memory.
//	
//	Parameters:
//		none.
//
//	Return Value:
//		none.
//
//***************************************************************************
void CleanUpMenu()
{
	WORD wProgNum;

	for( wProgNum = 0; wProgNum < NUM_USERPROGS; ++wProgNum )
	{
		if( !UserProg[wProgNum].fActive )
			break;

		LocalFree( (HLOCAL)UserProg[wProgNum].szPath );
		LocalFree( (HLOCAL)UserProg[wProgNum].szDescription );
		UserProg[wProgNum].fActive = FALSE;
	}
}


//***************************************************************************
//
//	PreLoadPrograms()
//
//	Purpose:
//		Parse the name of a utility to run, and attempt to start it using
//		WinExec(). If we're successful, we determine and keep the window
//		handle of the main WndProc for that program. We'll need that later to
//		close the program.
//	
//	Parameters:
//		none.
//
//	Return Value:
//		none.
//
//***************************************************************************
void PreLoadPrograms()
{
	WORD wProgNum;
	char szPreLoadProgEntry[ ENTRY_TAG_LENGTH ];

	for( wProgNum = 0; wProgNum < NUM_PRELOADPROGS; ++wProgNum )
	{
		PreLoadProg[wProgNum].fActive = FALSE;

	// Get the Utility's path

		wsprintf( szPreLoadProgEntry, "PreLoad%u", wProgNum+1 );
		GetPrivateProfileString( (LPSTR)"FM_Utils Extension",
				(LPSTR)szPreLoadProgEntry, "", szTempBuffer, PATH_NAME_LEN,
				(LPSTR)"WINFILE.INI" );
		if( !( strlen( szTempBuffer )))
			break;
		else
		{
			PreLoadProg[wProgNum].szPath=(NPSTR)LocalAlloc( LPTR, strlen( szTempBuffer )+1 );
			if( !PreLoadProg[wProgNum].szPath )
			{
				MessageBox( NULL, "Memory Allocation Error\nPre-Loading Programs",
					"File Manager", MB_ICONEXCLAMATION | MB_OK );
				break;
			}
			strcpy( PreLoadProg[wProgNum].szPath, szTempBuffer );
		}

	// If all is okay, attempt start the program

		PreLoadProg[wProgNum].hInst =
				WinExec( (LPSTR)PreLoadProg[wProgNum].szPath, SW_SHOWMINNOACTIVE );

	// If we failed, don't bother to notify. Otherwise, determine the handle

		if( PreLoadProg[wProgNum].hInst >= 32 )
		{
			PreLoadProg[wProgNum].fActive = TRUE;
			PreLoadProg[wProgNum].hWnd =
					DetermineProgramHandle( PreLoadProg[wProgNum].hInst );
		}
	}
}


//***************************************************************************
//
//	DetermineProgramHandle()
//
//	Purpose:
//		Given the Instance Handle of a program, we want to find out the Window
//		handle of its main WndProc.
//	
//	Parameters:
//		hInst - Instance handle of the app we're trying to find the handle of.
//
//	Return Value:
//		hWnd - Window handle of app's main WndProc
//
//***************************************************************************
HWND DetermineProgramHandle( HINSTANCE hInst )
{
	HWND hWnd;
	HWND hWndParent;

// Find the first top-level window that matches our selected instance handle.

	for( hWnd = GetWindow( GetDesktopWindow(), GW_CHILD );
		hWnd;
		hWnd = GetWindow( hWnd, GW_HWNDNEXT ) )
	{
		if( GetWindowWord( hWnd, GWW_HINSTANCE ) == hInst )
			break;
	}

// Iconized programs can have *two* top-level window handles, the icon
// (or main window), and the icon title.
// In case we didn't get the top-most window of this task, loop through
// until GetParent returns NULL. Then, we have the window we want to send
// our WM_CLOSE to.

	while( hWndParent = GetParent( hWnd ) )
		hWnd = hWndParent;

	return hWnd;
}


//***************************************************************************
//
//	KillPreLoadedPrograms()
//
//	Purpose:
//		We step through the window handles of the preloaded apps, and send
//		WM_CLOSE messages to them. Naturally, we check first to ensure that
//		the window handle is still valid.
//	
//	Parameters:
//		none.
//
//	Return Value:
//		none.
//
//	Notes:
//		We have no guarantee that, when the main WndProc of the application
//		receives a WM_CLOSE message, that it will actually terminate the
//		app. If somebody knows of a better way to handle this, I'd be *real*
//		happy to hear about it.
//
//***************************************************************************
void KillPreLoadedPrograms()
{
	WORD wProgNum;

	for( wProgNum = 0; wProgNum < NUM_PRELOADPROGS; ++wProgNum )
	{
		if( !PreLoadProg[wProgNum].fActive )
			break;

	// Check to make sure the Window is still active, and still matches
	// the instance handle. If so, attempt to close it.

		if(( IsWindow( PreLoadProg[wProgNum].hWnd )) &&
			( GetWindowWord( PreLoadProg[wProgNum].hWnd, GWW_HINSTANCE ) ==
			PreLoadProg[wProgNum].hInst ))
		{
			PostMessage( PreLoadProg[wProgNum].hWnd, WM_CLOSE, 0, 0L );
		}
		LocalFree( (HLOCAL)PreLoadProg[wProgNum].szPath );
		PreLoadProg[wProgNum].fActive = FALSE;
	}
}


//***************************************************************************
//
//	UserProgram()
//
//	Purpose:
//		This is where we select which User Program has been selected to
//		run. If there are one or more files selected, we generate a command
//		line, and then execute it using WinExec().
//	
//	Parameters:
//		wProgNum		- Identifies the index of the user program to run
//		hWndExtension	- Identifies the File Manager window
//
//	Return Value:
//		none.
//
//	Notes:
//		While it doesn't seem to be specifically documented anywhere, the
//		maximum number of characters in a command line can't exceed 128.
//		Since it's possible to select any number of files, it's pretty easy
//		to blow this limit. In order to maximize our use of this limited
//		space, we perform some trickery.
//		Since we know that File Manager (currently) doesn't allow selection
//		of multiple files across different directories or drives, we know
//		that the path portion of all selected files should be the same. So,
//		we determine the path portion, and actually make that directory our
//		current directory, and chop the path portion from all of the selected
//		files (WinExec() will search for the utility, if its full path is
//		not specified, using the standard Windows search mechanism).
//		If we still try to blow the length limit, a warning MessageBox will
//		pop up, and the command line will be truncated at the last possble
//		file addition. While actually *changing* the file selections in
//		File Manager, it looks like this isn't feasible.
//
//***************************************************************************
void UserProgram( WORD wProgNum, HWND hWndExtension )
{
	static FMS_GETFILESEL fmsFileInfo;
	NPSTR szCmdLine;
	WORD wSelFileCount;
	WORD wProgIndex;
	WORD wWinExecReturn;
	WORD wCmdLineLength;
	char *szFileName;

	if( !UserProg[wProgNum].fActive )
		return;

	wSelFileCount = (WORD)SendMessage( hWndExtension, FM_GETSELCOUNTLFN, 0, 0L );
//	if( !wSelFileCount )
//	{
//		MessageBox( NULL, "No Files Selected!", "File Manager",
//				MB_ICONEXCLAMATION | MB_OK );
//		return;
//	}

	szCmdLine = (NPSTR)LocalAlloc( LPTR, strlen( UserProg[wProgNum].szPath )+1 );
	if( !szCmdLine )
	{
		MessageBox( NULL, "Unable to create command line!", "File Manager",
				MB_ICONEXCLAMATION | MB_OK );
		return;
	}
	strcpy( szCmdLine, UserProg[wProgNum].szPath );

	for( wProgIndex = 0; wProgIndex < wSelFileCount; wProgIndex++ )
	{
		SendMessage( hWndExtension, FM_GETFILESELLFN, wProgIndex,
					(LONG)(LPFMS_GETFILESEL)&fmsFileInfo );

	//	We know that all of the selected files will be in the same directory,
	//	because File Manager doesn't allow selection across drives or
	//	directories. We can use this to our advantage by determining the
	//	selected directory, making it the current directory, and only putting
	//	the actual file names on the command line. This maximizes our use of
	//	the command line (which, remember, is limited to 128 characters).

		if( wProgIndex == 0 )
		{
			strcpy( szTempBuffer, fmsFileInfo.szName );
			szFileName = strrchr( szTempBuffer, '\\' );

		// We don't chop the trailing '\' on root directory

			if( (WORD)( szFileName - szTempBuffer ) > 3 )
				szFileName[0] = 0;
			else
				szFileName[1] = 0;

			_chdrive( szTempBuffer[0] - 'A' + 1 );
			_chdir( szTempBuffer );
//			OutputDebugString( (LPSTR)szTempBuffer );
//			OutputDebugString( (LPSTR)"\n" );
		}

		szFileName = (char *)( strrchr( fmsFileInfo.szName, '\\' ) + 1 );

		wCmdLineLength = strlen( szCmdLine ) + strlen( szFileName ) + 2;
		if( wCmdLineLength <= MAX_CMDLINE_LENGTH )
		{
			szCmdLine = (NPSTR)LocalReAlloc( (HLOCAL)szCmdLine,
					wCmdLineLength, LMEM_ZEROINIT );
			if( !szCmdLine )
			{
				LocalFree( (HLOCAL)szCmdLine );
				MessageBox( NULL, "Unable to construct command line!", "File Manager",
						MB_ICONEXCLAMATION | MB_OK );
				return;
			}

			strcat( szCmdLine, " " );
			strcat( szCmdLine, szFileName );
		}
		else
		{
			wsprintf( szTempBuffer,
"Adding file path\n%s\nwould make command line too long.\nTruncating command line.",
					(LPSTR)fmsFileInfo.szName );
			MessageBox( NULL, szTempBuffer, "File Manager", MB_ICONEXCLAMATION | MB_OK );
			break;
		}
	}

//	OutputDebugString( (LPSTR)szCmdLine );
//	OutputDebugString( (LPSTR)"\n" );

	wWinExecReturn = WinExec( szCmdLine, SW_SHOWNORMAL );
	LocalFree( (HLOCAL)szCmdLine );
	if( wWinExecReturn < 32 )
	{
		wsprintf( szTempBuffer, "Unable to Execute\n%s", UserProg[wProgNum].szPath );
		MessageBox( NULL, szTempBuffer, "File Manager", MB_ICONEXCLAMATION | MB_OK );
	}
}


//***************************************************************************
//
//	AboutDlgProc()
//
//	Purpose:
//		Your typical "About" box, in case you forget who wrote this amazing
//		piece of code.    :-)
//	
//	Parameters:
//		The usual DLGPROC parameters
//
//	Return Value:
//		The typical DLGPROC BOOL return
//
//***************************************************************************
BOOL CALLBACK AboutDlgProc( HWND hDlg, UINT wMsg, WPARAM wParam, LPARAM lParam )
{
	switch( wMsg )
	{
		case WM_COMMAND:
			switch( wParam )
			{
				case IDOK:
				case IDCANCEL:
					EndDialog( hDlg, TRUE );
					break;

				default:
					return FALSE;
			}
			return TRUE;

		default:
			break;
	}
	return FALSE;
}

