/******************************************************************************
 JXPathInput.cc

	Input field for entering a file path.

	BASE CLASS = JXInputField

	Copyright © 1996 by Glenn W. Bach.
	Copyright © 1998 by John Lindal. All rights reserved.

 ******************************************************************************/

#include <JXPathInput.h>
#include <JXSelectionManager.h>
#include <JXDNDManager.h>
#include <JXColormap.h>
#include <jXGlobals.h>
#include <jXUtil.h>
#include <jDirUtil.h>
#include <jAssert.h>

/******************************************************************************
 Constructor

 ******************************************************************************/

JXPathInput::JXPathInput
	(
	JXContainer*		enclosure,
	const HSizingOption	hSizing,
	const VSizingOption	vSizing,
	const JCoordinate	x,
	const JCoordinate	y,
	const JCoordinate	w,
	const JCoordinate	h
	)
	:
	JXInputField(enclosure, hSizing, vSizing, x,y, w,h)
{
	itsAllowInvalidPathFlag = kFalse;
	itsRequireWriteFlag     = kFalse;
	itsExpectURLDropFlag    = kFalse;
	SetIsRequired();
	ListenTo(this);
}

/******************************************************************************
 Destructor

 ******************************************************************************/

JXPathInput::~JXPathInput()
{
}

/******************************************************************************
 HandleUnfocusEvent (virtual protected)

 ******************************************************************************/

void
JXPathInput::HandleUnfocusEvent()
{
	JXInputField::HandleUnfocusEvent();
	SetCaretLocation(GetTextLength() + 1);
}

/******************************************************************************
 ApertureResized (virtual protected)

 ******************************************************************************/

void
JXPathInput::ApertureResized
	(
	const JCoordinate dw,
	const JCoordinate dh
	)
{
	JXInputField::ApertureResized(dw,dh);
	if (!HasFocus())
		{
		SetCaretLocation(GetTextLength() + 1);
		}
}

/******************************************************************************
 Receive (virtual protected)

 ******************************************************************************/

void
JXPathInput::Receive
	(
	JBroadcaster*	sender,
	const Message&	message
	)
{
	if (sender == this && message.Is(JTextEditor::kTextSet))
		{
		SetCaretLocation(GetTextLength() + 1);
		}

	JXInputField::Receive(sender, message);
}

/******************************************************************************
 SetBasePath

	This is used if a relative path is entered.  It must be an absolute path.

 ******************************************************************************/

void
JXPathInput::SetBasePath
	(
	const JCharacter* path
	)
{
	assert( path != NULL && path[0] == '/' );
	itsBasePath = path;
	RecalcAll(kTrue);
}

/******************************************************************************
 GetPath

	Returns kTrue if the current path is valid.  In this case, *path is
	set to the full path, relative to the root directory.

	Use this instead of GetText(), because that may return a relative path.

 ******************************************************************************/

JBoolean
JXPathInput::GetPath
	(
	JString* path
	)
	const
{
	const JString& text = GetText();
	return JI2B(!text.IsEmpty() &&
				JConvertToAbsolutePath(text, itsBasePath, path) &&
				JDirectoryExists(*path) &&
				JDirectoryReadable(*path) &&
				JCanEnterDirectory(*path) &&
				(!itsRequireWriteFlag || JDirectoryWritable(*path)));
}

/******************************************************************************
 InputValid (virtual)

 ******************************************************************************/

JBoolean
JXPathInput::InputValid()
{
	if (itsAllowInvalidPathFlag)
		{
		return kTrue;
		}
	else if (!JXInputField::InputValid())
		{
		return kFalse;
		}

	const JString& text = GetText();
	if (text.IsEmpty())
		{
		return JNegate(IsRequired());
		}

	JString path;
	if (!JConvertToAbsolutePath(text, itsBasePath, &path))
		{
		(JGetUserNotification())->ReportError("This is not a valid directory.");
		return kFalse;
		}

	const JString currDir = JGetCurrentDirectory();
	const JError err      = JChangeDirectory(path);
	JChangeDirectory(currDir);

	if (err.OK())
		{
		if (!JDirectoryReadable(path))
			{
			(JGetUserNotification())->ReportError("This directory is unreadable.");
			return kFalse;
			}
		else if (itsRequireWriteFlag && !JDirectoryWritable(path))
			{
			(JGetUserNotification())->ReportError(
				"You don't have permission to write to this directory.");
			return kFalse;
			}
		else
			{
			return kTrue;
			}
		}

	const JCharacter* errorStr;
	if (err == kJAccessDenied)
		{
		errorStr = "You don't have access to this directory.";
		}
	else if (err == kJBadPath)
		{
		errorStr = "This directory does not exist.";
		}
	else if (err == kJComponentNotDirectory)
		{
		errorStr = "Some part of this is not a directory.";
		}
	else
		{
		errorStr = "This is not a valid directory.";
		}

	(JGetUserNotification())->ReportError(errorStr);
	return kFalse;
}

/******************************************************************************
 AdjustStylesBeforeRecalc (virtual protected)

	Draw the invalid portion of the path in red.

 ******************************************************************************/

void
JXPathInput::AdjustStylesBeforeRecalc
	(
	const JString&		buffer,
	JRunArray<Font>*	styles,
	JIndexRange*		recalcRange,
	JIndexRange*		redrawRange,
	const JBoolean		deletion
	)
{
	const JColormap* colormap = GetColormap();
	const JSize totalLength   = buffer.GetLength();

	JString fullPath = buffer;
	if (buffer.GetFirstCharacter() != '/' &&
		buffer.GetFirstCharacter() != '~' &&
		!JConvertToAbsolutePath(buffer, itsBasePath, &fullPath))
		{
		if (!itsBasePath.IsEmpty())
			{
			fullPath = JCombinePathAndName(itsBasePath, buffer);
			}
		else
			{
			fullPath.Clear();
			}
		}

	JSize errLength;
	if (fullPath.IsEmpty())
		{
		errLength = totalLength;
		}
	else
		{
		const JString closestDir = JGetClosestDirectory(fullPath, itsRequireWriteFlag);
		if (fullPath.BeginsWith(closestDir))
			{
			errLength = fullPath.GetLength() - closestDir.GetLength();
			}
		else
			{
			errLength = totalLength;
			}
		}

	if (errLength > 0 && buffer.EndsWith("/."))
		{
		errLength++;	// trailing . is trimmed
		}

	Font f = styles->GetFirstElement();

	styles->RemoveAll();
	if (errLength >= totalLength)
		{
		f.style.color = colormap->GetRedColor();
		styles->AppendElements(f, totalLength);
		}
	else
		{
		f.style.color = colormap->GetBlackColor();
		styles->AppendElements(f, totalLength - errLength);

		if (errLength > 0)
			{
			f.style.color = colormap->GetRedColor();
			styles->AppendElements(f, errLength);
			}
		}

	*redrawRange += JIndexRange(1, totalLength);
}

/******************************************************************************
 GetTextColor (static)

	Draw the entire text red if the path is invalid.  This is provided
	so tables can draw the text the same way as the input field.

	base can be NULL.  If you use JXPathInput for relative paths, base
	should be the path passed to SetBasePath().

 ******************************************************************************/

JColorIndex
JXPathInput::GetTextColor
	(
	const JCharacter*	path,
	const JCharacter*	base,
	const JBoolean		requireWrite,
	const JColormap*	colormap
	)
{
	if (JStringEmpty(path))
		{
		return colormap->GetBlackColor();
		}

	JString fullPath;
	if (JConvertToAbsolutePath(path, base, &fullPath) &&
		JDirectoryReadable(fullPath) &&
		JCanEnterDirectory(fullPath) &&
		(!requireWrite || JDirectoryWritable(fullPath)))
		{
		return colormap->GetBlackColor();
		}
	else
		{
		return colormap->GetRedColor();
		}
}

/******************************************************************************
 WillAcceptDrop (virtual protected)

	Accept text/uri-list.

 ******************************************************************************/

JBoolean
JXPathInput::WillAcceptDrop
	(
	const JArray<Atom>&	typeList,
	Atom*				action,
	const Time			time,
	const JXWidget*		source
	)
{
	itsExpectURLDropFlag = kFalse;

	const Atom urlXAtom = (GetSelectionManager())->GetURLXAtom();

	JString dirName;
	const JSize typeCount = typeList.GetElementCount();
	for (JIndex i=1; i<=typeCount; i++)
		{
		if (typeList.GetElement(i) == urlXAtom &&
			GetDroppedDirectory(time, &dirName))
			{
			*action = (GetDNDManager())->GetDNDActionPrivateXAtom();
			itsExpectURLDropFlag = kTrue;
			return kTrue;
			}
		}

	return JXInputField::WillAcceptDrop(typeList, action, time, source);
}

/******************************************************************************
 HandleDNDEnter (virtual protected)

	This is called when the mouse enters the widget.

 ******************************************************************************/

void
JXPathInput::HandleDNDEnter()
{
	if (!itsExpectURLDropFlag)
		{
		JXInputField::HandleDNDEnter();
		}
}

/******************************************************************************
 HandleDNDHere (virtual protected)

	This is called while the mouse is inside the widget.

 ******************************************************************************/

void
JXPathInput::HandleDNDHere
	(
	const JPoint&	pt,
	const JXWidget*	source
	)
{
	if (!itsExpectURLDropFlag)
		{
		JXInputField::HandleDNDHere(pt, source);
		}
}

/******************************************************************************
 HandleDNDLeave (virtual protected)

	This is called when the mouse leaves the widget without dropping data.

 ******************************************************************************/

void
JXPathInput::HandleDNDLeave()
{
	if (!itsExpectURLDropFlag)
		{
		JXInputField::HandleDNDLeave();
		}
}

/******************************************************************************
 HandleDNDDrop (virtual protected)

	This is called when the data is dropped.  The data is accessed via
	the selection manager, just like Paste.

	Since we only accept text/uri-list, we don't bother to check typeList.

 ******************************************************************************/

void
JXPathInput::HandleDNDDrop
	(
	const JPoint&		pt,
	const JArray<Atom>&	typeList,
	const Atom			action,
	const Time			time,
	const JXWidget*		source
	)
{
	JString dirName;
	if (!itsExpectURLDropFlag)
		{
		JXInputField::HandleDNDDrop(pt, typeList, action, time, source);
		}
	else if (Focus() && GetDroppedDirectory(time, &dirName))
		{
		SetText(dirName);
		}
}

/******************************************************************************
 GetDroppedDirectory (private)

 ******************************************************************************/

JBoolean
JXPathInput::GetDroppedDirectory
	(
	const Time	time,
	JString*	dirName
	)
{
	dirName->Clear();

	JXSelectionManager* selMgr = GetSelectionManager();

	Atom returnType;
	unsigned char* data;
	JSize dataLength;
	JXSelectionManager::DeleteMethod delMethod;
	if (selMgr->GetSelectionData((GetDNDManager())->GetDNDSelectionName(),
								 time, GetWindow(), selMgr->GetURLXAtom(),
								 &returnType, &data, &dataLength, &delMethod))
		{
		if (returnType == selMgr->GetURLXAtom())
			{
			JPtrArray<JString> fileNameList, urlList;
			JXUnpackFileNames((char*) data, dataLength, &fileNameList, &urlList);
			if (fileNameList.GetElementCount() == 1 &&
				JDirectoryExists(*(fileNameList.FirstElement())))
				{
				*dirName = *(fileNameList.FirstElement());
				}
			fileNameList.DeleteAll();
			urlList.DeleteAll();
			}

		selMgr->DeleteSelectionData(&data, delMethod);
		}

	return JNegate( dirName->IsEmpty() );
}
