1/*
2 * Copyright 2004-2023, Axel D��rfler, axeld@pinc-software.de.
3 * Copyright 2009, Philippe St-Pierre, stpere@gmail.com
4 * Distributed under the terms of the MIT License.
5 */
6
7
8#include "FindWindow.h"
9
10#include <stdlib.h>
11#include <stdio.h>
12#include <string.h>
13
14#include <algorithm>
15
16#include <Application.h>
17#include <Autolock.h>
18#include <AutoLocker.h>
19#include <Beep.h>
20#include <Button.h>
21#include <Catalog.h>
22#include <CheckBox.h>
23#include <Clipboard.h>
24#include <LayoutBuilder.h>
25#include <Locale.h>
26#include <MenuField.h>
27#include <MenuItem.h>
28#include <Mime.h>
29#include <PopUpMenu.h>
30#include <ScrollView.h>
31#include <TextView.h>
32
33#include "DataView.h"
34#include "DiskProbe.h"
35
36
37#undef B_TRANSLATION_CONTEXT
38#define B_TRANSLATION_CONTEXT "FindWindow"
39
40static const uint32 kMsgFindMode = 'FMde';
41static const uint32 kMsgStartFind = 'SFnd';
42
43
44class FindTextView : public BTextView {
45public:
46							FindTextView(const char* name);
47
48	virtual void			MakeFocus(bool state);
49	virtual void			TargetedByScrollView(BScrollView* view);
50
51			find_mode		Mode() const { return fMode; }
52			status_t		SetMode(find_mode mode);
53
54			void			SetData(BMessage& message);
55			void			GetData(BMessage& message);
56
57	virtual void			KeyDown(const char* bytes, int32 numBytes);
58
59	virtual bool			AcceptsPaste(BClipboard* clipboard);
60	virtual void			Copy(BClipboard* clipboard);
61	virtual void			Cut(BClipboard* clipboard);
62	virtual void			Paste(BClipboard* clipboard);
63
64protected:
65	virtual	void			InsertText(const char* text, int32 length,
66								int32 offset, const text_run_array* runs);
67
68private:
69			void			_HexReformat(int32 oldCursor, int32& newCursor);
70			status_t		_GetHexFromData(const uint8* in, size_t inSize,
71								char** _hex, size_t* _hexSize);
72			status_t		_GetDataFromHex(const char* text, size_t textLength,
73								uint8** _data, size_t* _dataSize);
74
75			BScrollView*	fScrollView;
76			find_mode		fMode;
77};
78
79
80FindTextView::FindTextView(const char* name)
81	:
82	BTextView(name),
83	fScrollView(NULL),
84	fMode(kAsciiMode)
85{
86}
87
88
89void
90FindTextView::MakeFocus(bool state)
91{
92	BTextView::MakeFocus(state);
93
94	if (fScrollView != NULL)
95		fScrollView->SetBorderHighlighted(state);
96}
97
98
99void
100FindTextView::TargetedByScrollView(BScrollView* view)
101{
102	BTextView::TargetedByScrollView(view);
103	fScrollView = view;
104}
105
106
107void
108FindTextView::_HexReformat(int32 oldCursor, int32& newCursor)
109{
110	const char* text = Text();
111	int32 textLength = TextLength();
112	char* insert = (char*)malloc(textLength * 2);
113	if (insert == NULL)
114		return;
115
116	newCursor = TextLength();
117	int32 out = 0;
118	for (int32 i = 0; i < textLength; i++) {
119		if (i == oldCursor) {
120			// this is the end of the inserted text
121			newCursor = out;
122		}
123
124		char c = text[i];
125		if (c >= 'A' && c <= 'F')
126			c += 'a' - 'A';
127		if ((c >= 'a' && c <= 'f') || (c >= '0' && c <= '9'))
128			insert[out++] = c;
129
130		if ((out % 48) == 47)
131			insert[out++] = '\n';
132		else if ((out % 3) == 2)
133			insert[out++] = ' ';
134	}
135	insert[out] = '\0';
136
137	DeleteText(0, textLength);
138
139	// InsertText() does not work here, as we need the text
140	// to be reformatted as well (newlines, breaks, whatever).
141	// IOW the BTextView class is not very nicely done.
142	//	BTextView::InsertText(insert, out, 0, NULL);
143	fMode = kAsciiMode;
144	Insert(0, insert, out);
145	fMode = kHexMode;
146
147	free(insert);
148}
149
150
151void
152FindTextView::InsertText(const char* text, int32 length, int32 offset,
153	const text_run_array* runs)
154{
155	if (fMode == kHexMode) {
156		if (offset > TextLength())
157			offset = TextLength();
158
159		BTextView::InsertText(text, length, offset, runs);
160			// lets add anything, and then start to filter out
161			// (since we have to reformat the whole text)
162
163		int32 start, end;
164		GetSelection(&start, &end);
165
166		int32 cursor;
167		_HexReformat(offset, cursor);
168
169		if (length == 1 && start == offset)
170			Select(cursor + 1, cursor + 1);
171	} else
172		BTextView::InsertText(text, length, offset, runs);
173}
174
175
176void
177FindTextView::KeyDown(const char* bytes, int32 numBytes)
178{
179	if (fMode == kHexMode) {
180		// filter out invalid (for hex mode) characters
181		if (numBytes > 1)
182			return;
183
184		switch (bytes[0]) {
185			case B_RIGHT_ARROW:
186			case B_LEFT_ARROW:
187			case B_UP_ARROW:
188			case B_DOWN_ARROW:
189			case B_HOME:
190			case B_END:
191			case B_PAGE_UP:
192			case B_PAGE_DOWN:
193				break;
194
195			case B_BACKSPACE:
196			case B_DELETE:
197			{
198				int32 start, end;
199				GetSelection(&start, &end);
200
201				if (bytes[0] == B_BACKSPACE && --start < 0) {
202					if (end == 0)
203						return;
204					start = 0;
205				}
206
207				if (ByteAt(start) == ' ')
208					BTextView::KeyDown(bytes, numBytes);
209
210				BTextView::KeyDown(bytes, numBytes);
211
212				if (bytes[0] == B_BACKSPACE)
213					GetSelection(&start, &end);
214
215				_HexReformat(start, start);
216				Select(start, start);
217				return;
218			}
219
220			default:
221			{
222				if (!strchr("0123456789abcdefABCDEF", bytes[0]))
223					return;
224
225				// the original KeyDown() has severe cursor setting
226				// problems with our InsertText().
227
228				int32 start, end;
229				GetSelection(&start, &end);
230				InsertText(bytes, 1, start, NULL);
231				return;
232			}
233		}
234	}
235	BTextView::KeyDown(bytes, numBytes);
236}
237
238
239bool
240FindTextView::AcceptsPaste(BClipboard* clipboard)
241{
242	if (clipboard == NULL)
243		return false;
244
245	AutoLocker<BClipboard> _(clipboard);
246
247	BMessage* clip = clipboard->Data();
248	if (clip == NULL)
249		return false;
250
251	if (clip->HasData(B_FILE_MIME_TYPE, B_MIME_TYPE)
252		|| clip->HasData("text/plain", B_MIME_TYPE))
253		return true;
254
255	return BTextView::AcceptsPaste(clipboard);
256}
257
258
259void
260FindTextView::Copy(BClipboard* clipboard)
261{
262	if (fMode != kHexMode) {
263		BTextView::Copy(clipboard);
264		return;
265	}
266
267	int32 start, end;
268	GetSelection(&start, &end);
269
270	if (clipboard == NULL || start == end)
271		return;
272
273	AutoLocker<BClipboard> _(clipboard);
274
275	BMessage* clip = clipboard->Data();
276	if (clip == NULL)
277		return;
278
279	// convert hex-text to real data
280	uint8* data;
281	size_t dataSize;
282	if (_GetDataFromHex(Text() + start, end - start, &data, &dataSize)
283			!= B_OK)
284		return;
285
286	clip->AddData(B_FILE_MIME_TYPE, B_MIME_TYPE, data, dataSize);
287
288	if (is_valid_utf8(data, dataSize))
289		clip->AddData("text/plain", B_MIME_TYPE, data, dataSize);
290
291	free(data);
292}
293
294
295void
296FindTextView::Cut(BClipboard* clipboard)
297{
298	if (fMode != kHexMode) {
299		BTextView::Cut(clipboard);
300		return;
301	}
302
303	int32 start, end;
304	GetSelection(&start, &end);
305	if (clipboard == NULL || start == end)
306		return;
307
308	AutoLocker<BClipboard> _(clipboard);
309
310	BMessage* clip = clipboard->Data();
311	if (clip == NULL)
312		return;
313
314	Copy(clipboard);
315	Clear();
316}
317
318
319void
320FindTextView::Paste(BClipboard* clipboard)
321{
322	if (clipboard == NULL)
323		return;
324
325	AutoLocker<BClipboard> _(clipboard);
326
327	BMessage* clip = clipboard->Data();
328	if (clip == NULL)
329		return;
330
331	const uint8* data;
332	ssize_t dataSize;
333	if (clip->FindData(B_FILE_MIME_TYPE, B_MIME_TYPE, (const void**)&data,
334			&dataSize) == B_OK) {
335		if (fMode == kHexMode) {
336			char* hex;
337			size_t hexSize;
338			if (_GetHexFromData(data, dataSize, &hex, &hexSize) < B_OK)
339				return;
340
341			Insert(hex, hexSize);
342			free(hex);
343		} else
344			Insert((char*)data, dataSize);
345		return;
346	}
347
348	BTextView::Paste(clipboard);
349}
350
351
352status_t
353FindTextView::_GetHexFromData(const uint8* in, size_t inSize, char** _hex,
354	size_t* _hexSize)
355{
356	char* hex = (char*)malloc(inSize * 3 + 1);
357	if (hex == NULL)
358		return B_NO_MEMORY;
359
360	char* out = hex;
361	for (uint32 i = 0; i < inSize; i++) {
362		out += sprintf(out, "%02x", *(unsigned char*)(in + i));
363	}
364	out[0] = '\0';
365
366	*_hex = hex;
367	*_hexSize = out + 1 - hex;
368	return B_OK;
369}
370
371
372status_t
373FindTextView::_GetDataFromHex(const char* text, size_t textLength, uint8** _data,
374	size_t* _dataSize)
375{
376	uint8* data = (uint8*)malloc(textLength);
377	if (data == NULL)
378		return B_NO_MEMORY;
379
380	size_t dataSize = 0;
381	uint8 hiByte = 0;
382	bool odd = false;
383	for (uint32 i = 0; i < textLength; i++) {
384		char c = text[i];
385		int32 number;
386		if (c >= 'A' && c <= 'F')
387			number = c + 10 - 'A';
388		else if (c >= 'a' && c <= 'f')
389			number = c + 10 - 'a';
390		else if (c >= '0' && c <= '9')
391			number = c - '0';
392		else
393			continue;
394
395		if (!odd)
396			hiByte = (number << 4) & 0xf0;
397		else
398			data[dataSize++] = hiByte | (number & 0x0f);
399
400		odd = !odd;
401	}
402	if (odd)
403		data[dataSize++] = hiByte;
404
405	*_data = data;
406	*_dataSize = dataSize;
407	return B_OK;
408}
409
410
411status_t
412FindTextView::SetMode(find_mode mode)
413{
414	if (fMode == mode)
415		return B_OK;
416
417	if (mode == kHexMode) {
418		// convert text to hex mode
419
420		char* hex;
421		size_t hexSize;
422		if (_GetHexFromData((const uint8*)Text(), TextLength(), &hex, &hexSize)
423				< B_OK)
424			return B_NO_MEMORY;
425
426		fMode = mode;
427
428		SetText(hex, hexSize);
429		free(hex);
430	} else {
431		// convert hex to ascii
432
433		uint8* data;
434		size_t dataSize;
435		if (_GetDataFromHex(Text(), TextLength(), &data, &dataSize) < B_OK)
436			return B_NO_MEMORY;
437
438		fMode = mode;
439
440		SetText((const char*)data, dataSize);
441		free(data);
442	}
443
444	return B_OK;
445}
446
447
448void
449FindTextView::SetData(BMessage& message)
450{
451	const uint8* data;
452	ssize_t dataSize;
453	if (message.FindData("data", B_RAW_TYPE,
454			(const void**)&data, &dataSize) != B_OK)
455		return;
456
457	if (fMode == kHexMode) {
458		char* hex;
459		size_t hexSize;
460		if (_GetHexFromData(data, dataSize, &hex, &hexSize) < B_OK)
461			return;
462
463		SetText(hex, hexSize);
464		free(hex);
465	} else
466		SetText((char*)data, dataSize);
467}
468
469
470void
471FindTextView::GetData(BMessage& message)
472{
473	if (fMode == kHexMode) {
474		// convert hex-text to real data
475		uint8* data;
476		size_t dataSize;
477		if (_GetDataFromHex(Text(), TextLength(), &data, &dataSize) != B_OK)
478			return;
479
480		message.AddData("data", B_RAW_TYPE, data, dataSize);
481		free(data);
482	} else
483		message.AddData("data", B_RAW_TYPE, Text(), TextLength());
484}
485
486
487//	#pragma mark -
488
489
490FindWindow::FindWindow(BRect _rect, BMessage& previous, BMessenger& target,
491		const BMessage* settings)
492	:
493	BWindow(_rect, B_TRANSLATE("Find"), B_TITLED_WINDOW,
494		B_ASYNCHRONOUS_CONTROLS | B_CLOSE_ON_ESCAPE | B_AUTO_UPDATE_SIZE_LIMITS),
495	fTarget(target)
496{
497	int8 mode = kAsciiMode;
498	if (previous.FindInt8("find_mode", &mode) != B_OK && settings != NULL)
499		settings->FindInt8("find_mode", &mode);
500
501	fMenu = new BPopUpMenu("mode");
502	BMessage* message;
503	BMenuItem* item;
504	fMenu->AddItem(item = new BMenuItem(B_TRANSLATE("Text"),
505		message = new BMessage(kMsgFindMode)));
506	message->AddInt8("mode", kAsciiMode);
507	if (mode == kAsciiMode)
508		item->SetMarked(true);
509	fMenu->AddItem(item = new BMenuItem(B_TRANSLATE_COMMENT("Hexadecimal",
510		"A menu item, as short as possible, noun is recommended if it is "
511		"shorter than adjective."), message = new BMessage(kMsgFindMode)));
512	message->AddInt8("mode", kHexMode);
513	if (mode == kHexMode)
514		item->SetMarked(true);
515
516	BMenuField* menuField = new BMenuField(B_TRANSLATE("Mode:"), fMenu);
517
518	fTextView = new FindTextView(B_EMPTY_STRING);
519	fTextView->SetWordWrap(true);
520	fTextView->SetMode((find_mode)mode);
521	fTextView->SetData(previous);
522
523	BScrollView* scrollView = new BScrollView("scroller", fTextView,
524		B_WILL_DRAW | B_FRAME_EVENTS, false, false);
525
526	BButton* button = new BButton(B_TRANSLATE("Find"),
527		new BMessage(kMsgStartFind));
528	button->MakeDefault(true);
529
530	fCaseCheckBox = new BCheckBox(B_TRANSLATE("Case sensitive"), NULL);
531	bool caseSensitive;
532	if (previous.FindBool("case_sensitive", &caseSensitive) != B_OK
533		&& (settings == NULL
534			|| settings->FindBool("case_sensitive", &caseSensitive) != B_OK)) {
535		caseSensitive = true;
536	}
537	fCaseCheckBox->SetValue(caseSensitive);
538
539	BLayoutBuilder::Group<>(this, B_VERTICAL)
540		.SetInsets(B_USE_WINDOW_SPACING)
541		.AddGroup(B_HORIZONTAL)
542			.Add(menuField->CreateLabelLayoutItem())
543			.Add(menuField->CreateMenuBarLayoutItem())
544			.AddGlue()
545			.End()
546		.Add(scrollView)
547		.AddGroup(B_HORIZONTAL)
548			.Add(fCaseCheckBox)
549			.AddGlue()
550			.Add(button);
551
552	ResizeTo(std::max(Bounds().Width() / 2, 300.f),
553		button->Frame().Height() * 3 + 30);
554}
555
556
557FindWindow::~FindWindow()
558{
559}
560
561
562void
563FindWindow::WindowActivated(bool active)
564{
565	fTextView->MakeFocus(active);
566}
567
568
569void
570FindWindow::MessageReceived(BMessage* message)
571{
572	switch (message->what) {
573		case kMsgFindMode:
574		{
575			int8 mode;
576			if (message->FindInt8("mode", &mode) != B_OK)
577				break;
578
579			if (fTextView->SetMode((find_mode)mode) != B_OK) {
580				// activate other item
581				fMenu->ItemAt(mode == kAsciiMode ? 1 : 0)->SetMarked(true);
582				beep();
583			}
584			fTextView->MakeFocus(true);
585			break;
586		}
587
588		case kMsgStartFind:
589		{
590			BMessage find(kMsgFind);
591			fTextView->GetData(find);
592			find.AddBool("case_sensitive", fCaseCheckBox->Value() != 0);
593			find.AddInt8("find_mode", fTextView->Mode());
594			fTarget.SendMessage(&find);
595
596			PostMessage(B_QUIT_REQUESTED);
597			break;
598		}
599
600		default:
601			BWindow::MessageReceived(message);
602	}
603}
604
605
606bool
607FindWindow::QuitRequested()
608{
609	// update the application's settings
610	BMessage update(kMsgSettingsChanged);
611	update.AddBool("case_sensitive", fCaseCheckBox->Value() != 0);
612	update.AddInt8("find_mode", fTextView->Mode());
613	be_app_messenger.SendMessage(&update);
614
615	be_app_messenger.SendMessage(kMsgFindWindowClosed);
616	return true;
617}
618
619
620void
621FindWindow::Show()
622{
623	fTextView->SelectAll();
624	BWindow::Show();
625}
626
627
628void
629FindWindow::SetTarget(BMessenger& target)
630{
631	fTarget = target;
632}
633
634