⛏️ index : haiku.git

/*
 * Copyright 2004-2023, Axel Dörfler, axeld@pinc-software.de.
 * Copyright 2009, Philippe St-Pierre, stpere@gmail.com
 * Distributed under the terms of the MIT License.
 */


#include "FindWindow.h"

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#include <algorithm>

#include <Application.h>
#include <Autolock.h>
#include <AutoLocker.h>
#include <Beep.h>
#include <Button.h>
#include <Catalog.h>
#include <CheckBox.h>
#include <Clipboard.h>
#include <LayoutBuilder.h>
#include <Locale.h>
#include <MenuField.h>
#include <MenuItem.h>
#include <Mime.h>
#include <PopUpMenu.h>
#include <ScrollView.h>
#include <TextView.h>

#include "DataView.h"
#include "DiskProbe.h"


#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "FindWindow"

static const uint32 kMsgFindMode = 'FMde';
static const uint32 kMsgStartFind = 'SFnd';


class FindTextView : public BTextView {
public:
							FindTextView(const char* name);

	virtual void			MakeFocus(bool state);
	virtual void			TargetedByScrollView(BScrollView* view);

			find_mode		Mode() const { return fMode; }
			status_t		SetMode(find_mode mode);

			void			SetData(BMessage& message);
			void			GetData(BMessage& message);

	virtual void			KeyDown(const char* bytes, int32 numBytes);

	virtual bool			AcceptsPaste(BClipboard* clipboard);
	virtual void			Copy(BClipboard* clipboard);
	virtual void			Cut(BClipboard* clipboard);
	virtual void			Paste(BClipboard* clipboard);

protected:
	virtual	void			InsertText(const char* text, int32 length,
								int32 offset, const text_run_array* runs);

private:
			void			_HexReformat(int32 oldCursor, int32& newCursor);
			status_t		_GetHexFromData(const uint8* in, size_t inSize,
								char** _hex, size_t* _hexSize);
			status_t		_GetDataFromHex(const char* text, size_t textLength,
								uint8** _data, size_t* _dataSize);

			BScrollView*	fScrollView;
			find_mode		fMode;
};


FindTextView::FindTextView(const char* name)
	:
	BTextView(name),
	fScrollView(NULL),
	fMode(kAsciiMode)
{
}


void
FindTextView::MakeFocus(bool state)
{
	BTextView::MakeFocus(state);

	if (fScrollView != NULL)
		fScrollView->SetBorderHighlighted(state);
}


void
FindTextView::TargetedByScrollView(BScrollView* view)
{
	BTextView::TargetedByScrollView(view);
	fScrollView = view;
}


void
FindTextView::_HexReformat(int32 oldCursor, int32& newCursor)
{
	const char* text = Text();
	int32 textLength = TextLength();
	char* insert = (char*)malloc(textLength * 2);
	if (insert == NULL)
		return;

	newCursor = TextLength();
	int32 out = 0;
	for (int32 i = 0; i < textLength; i++) {
		if (i == oldCursor) {
			// this is the end of the inserted text
			newCursor = out;
		}

		char c = text[i];
		if (c >= 'A' && c <= 'F')
			c += 'a' - 'A';
		if ((c >= 'a' && c <= 'f') || (c >= '0' && c <= '9'))
			insert[out++] = c;

		if ((out % 48) == 47)
			insert[out++] = '\n';
		else if ((out % 3) == 2)
			insert[out++] = ' ';
	}
	insert[out] = '\0';

	DeleteText(0, textLength);

	// InsertText() does not work here, as we need the text
	// to be reformatted as well (newlines, breaks, whatever).
	// IOW the BTextView class is not very nicely done.
	//	BTextView::InsertText(insert, out, 0, NULL);
	fMode = kAsciiMode;
	Insert(0, insert, out);
	fMode = kHexMode;

	free(insert);
}


void
FindTextView::InsertText(const char* text, int32 length, int32 offset,
	const text_run_array* runs)
{
	if (fMode == kHexMode) {
		if (offset > TextLength())
			offset = TextLength();

		BTextView::InsertText(text, length, offset, runs);
			// lets add anything, and then start to filter out
			// (since we have to reformat the whole text)

		int32 start, end;
		GetSelection(&start, &end);

		int32 cursor;
		_HexReformat(offset, cursor);

		if (length == 1 && start == offset)
			Select(cursor + 1, cursor + 1);
	} else
		BTextView::InsertText(text, length, offset, runs);
}


void
FindTextView::KeyDown(const char* bytes, int32 numBytes)
{
	if (fMode == kHexMode) {
		// filter out invalid (for hex mode) characters
		if (numBytes > 1)
			return;

		switch (bytes[0]) {
			case B_RIGHT_ARROW:
			case B_LEFT_ARROW:
			case B_UP_ARROW:
			case B_DOWN_ARROW:
			case B_HOME:
			case B_END:
			case B_PAGE_UP:
			case B_PAGE_DOWN:
				break;

			case B_BACKSPACE:
			case B_DELETE:
			{
				int32 start, end;
				GetSelection(&start, &end);

				if (bytes[0] == B_BACKSPACE && --start < 0) {
					if (end == 0)
						return;
					start = 0;
				}

				if (ByteAt(start) == ' ')
					BTextView::KeyDown(bytes, numBytes);

				BTextView::KeyDown(bytes, numBytes);

				if (bytes[0] == B_BACKSPACE)
					GetSelection(&start, &end);

				_HexReformat(start, start);
				Select(start, start);
				return;
			}

			default:
			{
				if (!strchr("0123456789abcdefABCDEF", bytes[0]))
					return;

				// the original KeyDown() has severe cursor setting
				// problems with our InsertText().

				int32 start, end;
				GetSelection(&start, &end);
				InsertText(bytes, 1, start, NULL);
				return;
			}
		}
	}
	BTextView::KeyDown(bytes, numBytes);
}


bool
FindTextView::AcceptsPaste(BClipboard* clipboard)
{
	if (clipboard == NULL)
		return false;

	AutoLocker<BClipboard> _(clipboard);

	BMessage* clip = clipboard->Data();
	if (clip == NULL)
		return false;

	if (clip->HasData(B_FILE_MIME_TYPE, B_MIME_TYPE)
		|| clip->HasData("text/plain", B_MIME_TYPE))
		return true;

	return BTextView::AcceptsPaste(clipboard);
}


void
FindTextView::Copy(BClipboard* clipboard)
{
	if (fMode != kHexMode) {
		BTextView::Copy(clipboard);
		return;
	}

	int32 start, end;
	GetSelection(&start, &end);

	if (clipboard == NULL || start == end)
		return;

	AutoLocker<BClipboard> _(clipboard);

	BMessage* clip = clipboard->Data();
	if (clip == NULL)
		return;

	// convert hex-text to real data
	uint8* data;
	size_t dataSize;
	if (_GetDataFromHex(Text() + start, end - start, &data, &dataSize)
			!= B_OK)
		return;

	clip->AddData(B_FILE_MIME_TYPE, B_MIME_TYPE, data, dataSize);

	if (is_valid_utf8(data, dataSize))
		clip->AddData("text/plain", B_MIME_TYPE, data, dataSize);

	free(data);
}


void
FindTextView::Cut(BClipboard* clipboard)
{
	if (fMode != kHexMode) {
		BTextView::Cut(clipboard);
		return;
	}

	int32 start, end;
	GetSelection(&start, &end);
	if (clipboard == NULL || start == end)
		return;

	AutoLocker<BClipboard> _(clipboard);

	BMessage* clip = clipboard->Data();
	if (clip == NULL)
		return;

	Copy(clipboard);
	Clear();
}


void
FindTextView::Paste(BClipboard* clipboard)
{
	if (clipboard == NULL)
		return;

	AutoLocker<BClipboard> _(clipboard);

	BMessage* clip = clipboard->Data();
	if (clip == NULL)
		return;

	const uint8* data;
	ssize_t dataSize;
	if (clip->FindData(B_FILE_MIME_TYPE, B_MIME_TYPE, (const void**)&data,
			&dataSize) == B_OK) {
		if (fMode == kHexMode) {
			char* hex;
			size_t hexSize;
			if (_GetHexFromData(data, dataSize, &hex, &hexSize) < B_OK)
				return;

			Insert(hex, hexSize);
			free(hex);
		} else
			Insert((char*)data, dataSize);
		return;
	}

	BTextView::Paste(clipboard);
}


status_t
FindTextView::_GetHexFromData(const uint8* in, size_t inSize, char** _hex,
	size_t* _hexSize)
{
	char* hex = (char*)malloc(inSize * 3 + 1);
	if (hex == NULL)
		return B_NO_MEMORY;

	char* out = hex;
	for (uint32 i = 0; i < inSize; i++) {
		out += sprintf(out, "%02x", *(unsigned char*)(in + i));
	}
	out[0] = '\0';

	*_hex = hex;
	*_hexSize = out + 1 - hex;
	return B_OK;
}


status_t
FindTextView::_GetDataFromHex(const char* text, size_t textLength, uint8** _data,
	size_t* _dataSize)
{
	uint8* data = (uint8*)malloc(textLength);
	if (data == NULL)
		return B_NO_MEMORY;

	size_t dataSize = 0;
	uint8 hiByte = 0;
	bool odd = false;
	for (uint32 i = 0; i < textLength; i++) {
		char c = text[i];
		int32 number;
		if (c >= 'A' && c <= 'F')
			number = c + 10 - 'A';
		else if (c >= 'a' && c <= 'f')
			number = c + 10 - 'a';
		else if (c >= '0' && c <= '9')
			number = c - '0';
		else
			continue;

		if (!odd)
			hiByte = (number << 4) & 0xf0;
		else
			data[dataSize++] = hiByte | (number & 0x0f);

		odd = !odd;
	}
	if (odd)
		data[dataSize++] = hiByte;

	*_data = data;
	*_dataSize = dataSize;
	return B_OK;
}


status_t
FindTextView::SetMode(find_mode mode)
{
	if (fMode == mode)
		return B_OK;

	if (mode == kHexMode) {
		// convert text to hex mode

		char* hex;
		size_t hexSize;
		if (_GetHexFromData((const uint8*)Text(), TextLength(), &hex, &hexSize)
				< B_OK)
			return B_NO_MEMORY;

		fMode = mode;

		SetText(hex, hexSize);
		free(hex);
	} else {
		// convert hex to ascii

		uint8* data;
		size_t dataSize;
		if (_GetDataFromHex(Text(), TextLength(), &data, &dataSize) < B_OK)
			return B_NO_MEMORY;

		fMode = mode;

		SetText((const char*)data, dataSize);
		free(data);
	}

	return B_OK;
}


void
FindTextView::SetData(BMessage& message)
{
	const uint8* data;
	ssize_t dataSize;
	if (message.FindData("data", B_RAW_TYPE,
			(const void**)&data, &dataSize) != B_OK)
		return;

	if (fMode == kHexMode) {
		char* hex;
		size_t hexSize;
		if (_GetHexFromData(data, dataSize, &hex, &hexSize) < B_OK)
			return;

		SetText(hex, hexSize);
		free(hex);
	} else
		SetText((char*)data, dataSize);
}


void
FindTextView::GetData(BMessage& message)
{
	if (fMode == kHexMode) {
		// convert hex-text to real data
		uint8* data;
		size_t dataSize;
		if (_GetDataFromHex(Text(), TextLength(), &data, &dataSize) != B_OK)
			return;

		message.AddData("data", B_RAW_TYPE, data, dataSize);
		free(data);
	} else
		message.AddData("data", B_RAW_TYPE, Text(), TextLength());
}


//	#pragma mark -


FindWindow::FindWindow(BRect _rect, BMessage& previous, BMessenger& target,
		const BMessage* settings)
	:
	BWindow(_rect, B_TRANSLATE("Find"), B_TITLED_WINDOW,
		B_ASYNCHRONOUS_CONTROLS | B_CLOSE_ON_ESCAPE | B_AUTO_UPDATE_SIZE_LIMITS),
	fTarget(target)
{
	int8 mode = kAsciiMode;
	if (previous.FindInt8("find_mode", &mode) != B_OK && settings != NULL)
		settings->FindInt8("find_mode", &mode);

	fMenu = new BPopUpMenu("mode");
	BMessage* message;
	BMenuItem* item;
	fMenu->AddItem(item = new BMenuItem(B_TRANSLATE("Text"),
		message = new BMessage(kMsgFindMode)));
	message->AddInt8("mode", kAsciiMode);
	if (mode == kAsciiMode)
		item->SetMarked(true);
	fMenu->AddItem(item = new BMenuItem(B_TRANSLATE_COMMENT("Hexadecimal",
		"A menu item, as short as possible, noun is recommended if it is "
		"shorter than adjective."), message = new BMessage(kMsgFindMode)));
	message->AddInt8("mode", kHexMode);
	if (mode == kHexMode)
		item->SetMarked(true);

	BMenuField* menuField = new BMenuField(B_TRANSLATE("Mode:"), fMenu);

	fTextView = new FindTextView(B_EMPTY_STRING);
	fTextView->SetWordWrap(true);
	fTextView->SetMode((find_mode)mode);
	fTextView->SetData(previous);

	BScrollView* scrollView = new BScrollView("scroller", fTextView,
		B_WILL_DRAW | B_FRAME_EVENTS, false, false);

	BButton* button = new BButton(B_TRANSLATE("Find"),
		new BMessage(kMsgStartFind));
	button->MakeDefault(true);

	fCaseCheckBox = new BCheckBox(B_TRANSLATE("Case sensitive"), NULL);
	bool caseSensitive;
	if (previous.FindBool("case_sensitive", &caseSensitive) != B_OK
		&& (settings == NULL
			|| settings->FindBool("case_sensitive", &caseSensitive) != B_OK)) {
		caseSensitive = true;
	}
	fCaseCheckBox->SetValue(caseSensitive);

	BLayoutBuilder::Group<>(this, B_VERTICAL)
		.SetInsets(B_USE_WINDOW_SPACING)
		.AddGroup(B_HORIZONTAL)
			.Add(menuField->CreateLabelLayoutItem())
			.Add(menuField->CreateMenuBarLayoutItem())
			.AddGlue()
			.End()
		.Add(scrollView)
		.AddGroup(B_HORIZONTAL)
			.Add(fCaseCheckBox)
			.AddGlue()
			.Add(button);

	ResizeTo(std::max(Bounds().Width() / 2, 300.f),
		button->Frame().Height() * 3 + 30);
}


FindWindow::~FindWindow()
{
}


void
FindWindow::WindowActivated(bool active)
{
	fTextView->MakeFocus(active);
}


void
FindWindow::MessageReceived(BMessage* message)
{
	switch (message->what) {
		case kMsgFindMode:
		{
			int8 mode;
			if (message->FindInt8("mode", &mode) != B_OK)
				break;

			if (fTextView->SetMode((find_mode)mode) != B_OK) {
				// activate other item
				fMenu->ItemAt(mode == kAsciiMode ? 1 : 0)->SetMarked(true);
				beep();
			}
			fTextView->MakeFocus(true);
			break;
		}

		case kMsgStartFind:
		{
			BMessage find(kMsgFind);
			fTextView->GetData(find);
			find.AddBool("case_sensitive", fCaseCheckBox->Value() != 0);
			find.AddInt8("find_mode", fTextView->Mode());
			fTarget.SendMessage(&find);

			PostMessage(B_QUIT_REQUESTED);
			break;
		}

		default:
			BWindow::MessageReceived(message);
	}
}


bool
FindWindow::QuitRequested()
{
	// update the application's settings
	BMessage update(kMsgSettingsChanged);
	update.AddBool("case_sensitive", fCaseCheckBox->Value() != 0);
	update.AddInt8("find_mode", fTextView->Mode());
	be_app_messenger.SendMessage(&update);

	be_app_messenger.SendMessage(kMsgFindWindowClosed);
	return true;
}


void
FindWindow::Show()
{
	fTextView->SelectAll();
	BWindow::Show();
}


void
FindWindow::SetTarget(BMessenger& target)
{
	fTarget = target;
}