⛏️ index : haiku.git

/*
 * Copyright 2002-2012, Haiku, Inc. All Rights Reserved.
 * Distributed under the terms of the MIT License.
 *
 * Authors:
 *		Mattias Sundblad
 *		Andrew Bachmann
 *		Philippe Saint-Pierre
 *		Jonas Sundström
 *		Ryan Leavengood
 *		Vlad Slepukhin
 *		Sarzhuk Zharski
 */


#include "ColorMenuItem.h"
#include "Constants.h"
#include "FindWindow.h"
#include "ReplaceWindow.h"
#include "StatusView.h"
#include "StyledEditApp.h"
#include "StyledEditView.h"
#include "StyledEditWindow.h"

#include <Alert.h>
#include <Autolock.h>
#include <Catalog.h>
#include <CharacterSet.h>
#include <CharacterSetRoster.h>
#include <Clipboard.h>
#include <Debug.h>
#include <File.h>
#include <FilePanel.h>
#include <fs_attr.h>
#include <Locale.h>
#include <Menu.h>
#include <MenuBar.h>
#include <MenuItem.h>
#include <NodeMonitor.h>
#include <Path.h>
#include <PrintJob.h>
#include <RecentItems.h>
#include <Rect.h>
#include <Roster.h>
#include <Screen.h>
#include <ScrollView.h>
#include <TextControl.h>
#include <TextView.h>
#include <TranslationUtils.h>
#include <UnicodeChar.h>
#include <UTF8.h>
#include <Volume.h>


using namespace BPrivate;


const float kLineViewWidth = 30.0;
const char* kInfoAttributeName = "StyledEdit-info";


#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "StyledEditWindow"


// This is a temporary solution for building BString with printf like format.
// will be removed in the future.
static void
bs_printf(BString* string, const char* format, ...)
{
	va_list ap;
	va_start(ap, format);
	char* buf;
	vasprintf(&buf, format, ap);
	string->SetTo(buf);
	free(buf);
	va_end(ap);
}


// #pragma mark -


StyledEditWindow::StyledEditWindow(BRect frame, int32 id, uint32 encoding)
	:
	BWindow(frame, "untitled", B_DOCUMENT_WINDOW, B_ASYNCHRONOUS_CONTROLS),
	fFindWindow(NULL),
	fReplaceWindow(NULL)
{
	_InitWindow(encoding);
	BString unTitled(B_TRANSLATE("Untitled "));
	unTitled << id;
	SetTitle(unTitled.String());
	fSaveItem->SetEnabled(true);
		// allow saving empty files
	Show();
}


StyledEditWindow::StyledEditWindow(BRect frame, entry_ref* ref, uint32 encoding)
	:
	BWindow(frame, "untitled", B_DOCUMENT_WINDOW, B_ASYNCHRONOUS_CONTROLS),
	fFindWindow(NULL),
	fReplaceWindow(NULL)
{
	_InitWindow(encoding);
	OpenFile(ref);
	Show();
}


StyledEditWindow::~StyledEditWindow()
{
	delete fSaveMessage;
	delete fPrintSettings;
	delete fSavePanel;
}


void
StyledEditWindow::Quit()
{
	_SwitchNodeMonitor(false);

	_SaveAttrs();
	if (StyledEditApp* app = dynamic_cast<StyledEditApp*>(be_app))
		app->CloseDocument();
	BWindow::Quit();
}


#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "QuitAlert"


bool
StyledEditWindow::QuitRequested()
{
	if (fClean)
		return true;

	if (fTextView->TextLength() == 0 && fSaveMessage == NULL)
		return true;

	BString alertText;
	bs_printf(&alertText,
		B_TRANSLATE("Save changes to the document \"%s\"? "), Title());

	int32 index = _ShowAlert(alertText, B_TRANSLATE("Cancel"),
		B_TRANSLATE("Don't save"), B_TRANSLATE("Save"),	B_WARNING_ALERT);

	if (index == 0)
		return false;	// "cancel": dont save, dont close the window

	if (index == 1)
		return true;	// "don't save": just close the window

	if (!fSaveMessage) {
		SaveAs(new BMessage(SAVE_THEN_QUIT));
		return false;
	}

	return Save() == B_OK;
}


void
StyledEditWindow::MessageReceived(BMessage* message)
{
	if (message->WasDropped()) {
		entry_ref ref;
		if (message->FindRef("refs", 0, &ref)==B_OK) {
			message->what = B_REFS_RECEIVED;
			be_app->PostMessage(message);
		}
	}

	switch (message->what) {
		// File menu
		case MENU_SAVE:
			if (!fSaveMessage)
				SaveAs();
			else
				Save(fSaveMessage);
			break;

		case MENU_SAVEAS:
			SaveAs();
			break;

		case B_SAVE_REQUESTED:
			Save(message);
			break;

		case SAVE_THEN_QUIT:
			if (Save(message) == B_OK)
				Quit();
			break;

		case MENU_RELOAD:
			_ReloadDocument(message);
			break;

		case MENU_CLOSE:
			if (QuitRequested())
				Quit();
			break;

		case MENU_PAGESETUP:
			PageSetup(fTextView->Window()->Title());
			break;
		case MENU_PRINT:
			Print(fTextView->Window()->Title());
			break;
		case MENU_QUIT:
			be_app->PostMessage(B_QUIT_REQUESTED);
			break;

		// Edit menu

		case B_UNDO:
			ASSERT(fCanUndo || fCanRedo);
			ASSERT(!(fCanUndo && fCanRedo));
			if (fCanUndo)
				fUndoFlag = true;
			if (fCanRedo)
				fRedoFlag = true;

			fTextView->Undo(be_clipboard);
			break;
		case B_CUT:
			fTextView->Cut(be_clipboard);
			break;
		case B_COPY:
			fTextView->Copy(be_clipboard);
			break;
		case B_PASTE:
			fTextView->Paste(be_clipboard);
			break;
		case MENU_CLEAR:
			fTextView->Clear();
			break;
		case MENU_FIND:
		{
			if (fFindWindow == NULL) {
				BRect findWindowFrame(Frame());
				findWindowFrame.InsetBy(
					(findWindowFrame.Width() - 400) / 2,
					(findWindowFrame.Height() - 235) / 2);

				fFindWindow = new FindWindow(findWindowFrame, this,
					&fStringToFind, fCaseSensitive, fWrapAround, fBackSearch);
				fFindWindow->Show();

			} else if (fFindWindow->IsHidden())
				fFindWindow->Show();
			else
				fFindWindow->Activate();
			break;
		}
		case MSG_FIND_WINDOW_QUIT:
		{
			fFindWindow = NULL;
			break;
		}
		case MSG_REPLACE_WINDOW_QUIT:
		{
			fReplaceWindow = NULL;
			break;
		}
		case MSG_SEARCH:
			message->FindString("findtext", &fStringToFind);
			fFindAgainItem->SetEnabled(true);
			message->FindBool("casesens", &fCaseSensitive);
			message->FindBool("wrap", &fWrapAround);
			message->FindBool("backsearch", &fBackSearch);

			_Search(fStringToFind, fCaseSensitive, fWrapAround, fBackSearch);
			break;
		case MENU_FIND_AGAIN:
			_Search(fStringToFind, fCaseSensitive, fWrapAround, fBackSearch);
			break;
		case MENU_FIND_SELECTION:
			_FindSelection();
			break;
		case MENU_REPLACE:
		{
			if (fReplaceWindow == NULL) {
				BRect replaceWindowFrame(Frame());
				replaceWindowFrame.InsetBy(
					(replaceWindowFrame.Width() - 400) / 2,
					(replaceWindowFrame.Height() - 284) / 2);

				fReplaceWindow = new ReplaceWindow(replaceWindowFrame, this,
					&fStringToFind, &fReplaceString, fCaseSensitive,
					fWrapAround, fBackSearch);
				fReplaceWindow->Show();

			} else if (fReplaceWindow->IsHidden())
				fReplaceWindow->Show();
			else
				fReplaceWindow->Activate();
			break;
		}
		case MSG_REPLACE:
		{
			message->FindBool("casesens", &fCaseSensitive);
			message->FindBool("wrap", &fWrapAround);
			message->FindBool("backsearch", &fBackSearch);

			message->FindString("FindText", &fStringToFind);
			message->FindString("ReplaceText", &fReplaceString);

			fFindAgainItem->SetEnabled(true);
			fReplaceSameItem->SetEnabled(true);

			_Replace(fStringToFind, fReplaceString, fCaseSensitive, fWrapAround,
				fBackSearch);
			break;
		}
		case MENU_REPLACE_SAME:
			_Replace(fStringToFind, fReplaceString, fCaseSensitive, fWrapAround,
				fBackSearch);
			break;

		case MSG_REPLACE_ALL:
		{
			message->FindBool("casesens", &fCaseSensitive);
			message->FindString("FindText", &fStringToFind);
			message->FindString("ReplaceText", &fReplaceString);

			bool allWindows;
			message->FindBool("allwindows", &allWindows);

			fFindAgainItem->SetEnabled(true);
			fReplaceSameItem->SetEnabled(true);

			if (allWindows)
				SearchAllWindows(fStringToFind, fReplaceString, fCaseSensitive);
			else
				_ReplaceAll(fStringToFind, fReplaceString, fCaseSensitive);
			break;
		}

		case B_NODE_MONITOR:
			_HandleNodeMonitorEvent(message);
			break;

		// Font menu

		case FONT_SIZE:
		{
			float fontSize;
			if (message->FindFloat("size", &fontSize) == B_OK)
				_SetFontSize(fontSize);
			break;
		}
		case FONT_FAMILY:
		{
			const char* fontFamily = NULL;
			const char* fontStyle = NULL;
			void* ptr;
			if (message->FindPointer("source", &ptr) == B_OK) {
				BMenuItem* item = static_cast<BMenuItem*>(ptr);
				fontFamily = item->Label();
			}

			BFont font;
			font.SetFamilyAndStyle(fontFamily, fontStyle);
			fItalicItem->SetMarked((font.Face() & B_ITALIC_FACE) != 0);
			fBoldItem->SetMarked((font.Face() & B_BOLD_FACE) != 0);

			_SetFontStyle(fontFamily, fontStyle);
			break;
		}
		case FONT_STYLE:
		{
			const char* fontFamily = NULL;
			const char* fontStyle = NULL;
			void* ptr;
			if (message->FindPointer("source", &ptr) == B_OK) {
				BMenuItem* item = static_cast<BMenuItem*>(ptr);
				fontStyle = item->Label();
				BMenu* menu = item->Menu();
				if (menu != NULL) {
					BMenuItem* super_item = menu->Superitem();
					if (super_item != NULL)
						fontFamily = super_item->Label();
				}
			}

			BFont font;
			font.SetFamilyAndStyle(fontFamily, fontStyle);
			fItalicItem->SetMarked((font.Face() & B_ITALIC_FACE) != 0);
			fBoldItem->SetMarked((font.Face() & B_BOLD_FACE) != 0);

			_SetFontStyle(fontFamily, fontStyle);
			break;
		}
		case kMsgSetItalic:
		{
			uint32 sameProperties;
			BFont font;
			fTextView->GetFontAndColor(&font, &sameProperties);

			if (fItalicItem->IsMarked())
				font.SetFace(B_REGULAR_FACE);
			fItalicItem->SetMarked(!fItalicItem->IsMarked());

			font_family family;
			font_style style;
			font.GetFamilyAndStyle(&family, &style);

			_SetFontStyle(family, style);
			break;
		}
		case kMsgSetBold:
		{
			uint32 sameProperties;
			BFont font;
			fTextView->GetFontAndColor(&font, &sameProperties);

			if (fBoldItem->IsMarked())
				font.SetFace(B_REGULAR_FACE);
			fBoldItem->SetMarked(!fBoldItem->IsMarked());

			font_family family;
			font_style style;
			font.GetFamilyAndStyle(&family, &style);

			_SetFontStyle(family, style);
			break;
		}
		case FONT_COLOR:
		{
			ssize_t colorLength;
			rgb_color* color;
			if (message->FindData("color", B_RGB_COLOR_TYPE,
					(const void**)&color, &colorLength) == B_OK
				&& colorLength == sizeof(rgb_color)) {
				/*
				 * TODO: Ideally, when selecting the default color,
				 * you wouldn't naively apply it; it shouldn't lose its nature.
				 * When reloaded with a different default color, it should
				 * reflect that different choice.
				 */
				_SetFontColor(color);
			}
			break;
		}

		// Document menu

		case ALIGN_LEFT:
			fTextView->SetAlignment(B_ALIGN_LEFT);
			_UpdateCleanUndoRedoSaveRevert();
			break;
		case ALIGN_CENTER:
			fTextView->SetAlignment(B_ALIGN_CENTER);
			_UpdateCleanUndoRedoSaveRevert();
			break;
		case ALIGN_RIGHT:
			fTextView->SetAlignment(B_ALIGN_RIGHT);
			_UpdateCleanUndoRedoSaveRevert();
			break;
		case WRAP_LINES:
		{
			BRect textRect(fTextView->Bounds());
			textRect.OffsetTo(B_ORIGIN);
			textRect.InsetBy(TEXT_INSET, TEXT_INSET);
			if (fTextView->DoesWordWrap()) {
				fTextView->SetWordWrap(false);
				fWrapItem->SetMarked(false);
				// the width comes from stylededit R5. TODO: find a better way
				textRect.SetRightBottom(BPoint(1500.0, textRect.RightBottom().y));
			} else {
				fTextView->SetWordWrap(true);
				fWrapItem->SetMarked(true);
			}
			fTextView->SetTextRect(textRect);

			_UpdateCleanUndoRedoSaveRevert();
			break;
		}
		case SHOW_STATISTICS:
			_ShowStatistics();
			break;
		case ENABLE_ITEMS:
			fCutItem->SetEnabled(true);
			fCopyItem->SetEnabled(true);
			break;
		case DISABLE_ITEMS:
			fCutItem->SetEnabled(false);
			fCopyItem->SetEnabled(false);
			break;
		case TEXT_CHANGED:
			if (fUndoFlag) {
				if (fUndoCleans) {
					// we cleaned!
					fClean = true;
					fUndoCleans = false;
				} else if (fClean) {
					// if we were clean
					// then a redo will make us clean again
					fRedoCleans = true;
					fClean = false;
				}
				// set mode
				fCanUndo = false;
				fCanRedo = true;
				fUndoItem->SetLabel(B_TRANSLATE("Redo typing"));
				fUndoItem->SetEnabled(true);
				fUndoFlag = false;
			} else {
				if (fRedoFlag && fRedoCleans) {
					// we cleaned!
					fClean = true;
					fRedoCleans = false;
				} else if (fClean) {
					// if we were clean
					// then an undo will make us clean again
					fUndoCleans = true;
					fClean = false;
				} else {
					// no more cleaning from undo now...
					fUndoCleans = false;
				}
				// set mode
				fCanUndo = true;
				fCanRedo = false;
				fUndoItem->SetLabel(B_TRANSLATE("Undo typing"));
				fUndoItem->SetEnabled(true);
				fRedoFlag = false;
			}
			if (fClean) {
				fSaveItem->SetEnabled(fSaveMessage == NULL);
			} else {
				fSaveItem->SetEnabled(true);
			}
			fReloadItem->SetEnabled(fSaveMessage != NULL);
			fEncodingItem->SetEnabled(fSaveMessage != NULL);
			break;

		case SAVE_AS_ENCODING:
			void* ptr;
			if (message->FindPointer("source", &ptr) == B_OK
				&& fSavePanelEncodingMenu != NULL) {
				fTextView->SetEncoding(
					(uint32)fSavePanelEncodingMenu->IndexOf((BMenuItem*)ptr));
			}
			break;

		case UPDATE_STATUS:
		{
			message->AddBool("modified", !fClean);
			bool readOnly = !fTextView->IsEditable();
			message->AddBool("readOnly", readOnly);
			if (readOnly) {
				BVolume volume(fNodeRef.device);
				message->AddBool("canUnlock", !volume.IsReadOnly());
			}
			fStatusView->SetStatus(message);
			break;
		}

		case UPDATE_STATUS_REF:
		{
			entry_ref ref;
			const char* name;

			if (fSaveMessage != NULL
				&& fSaveMessage->FindRef("directory", &ref) == B_OK
				&& fSaveMessage->FindString("name", &name) == B_OK) {

				BDirectory dir(&ref);
				status_t status = dir.InitCheck();
				BEntry entry;
				if (status == B_OK)
					status = entry.SetTo(&dir, name);
				if (status == B_OK)
					status = entry.GetRef(&ref);
			}
			fStatusView->SetRef(ref);
			break;
		}

		case UNLOCK_FILE:
		{
			status_t status = _UnlockFile();
			if (status != B_OK) {
				BString text;
				bs_printf(&text,
					B_TRANSLATE("Unable to unlock file\n\t%s"),
					strerror(status));
				_ShowAlert(text, B_TRANSLATE("OK"), "", "", B_STOP_ALERT);
			}
			PostMessage(UPDATE_STATUS);
			break;
		}

		case UPDATE_LINE_SELECTION:
		{
			int32 line;
			if (message->FindInt32("be:line", &line) == B_OK) {
				fTextView->GoToLine(line);
				fTextView->ScrollToSelection();
			}

			int32 start, length;
			if (message->FindInt32("be:selection_offset", &start) == B_OK) {
				if (message->FindInt32("be:selection_length", &length) != B_OK)
					length = 0;

				fTextView->Select(start, start + length);
				fTextView->ScrollToOffset(start);
			}
			break;
		}
		default:
			BWindow::MessageReceived(message);
			break;
	}
}


void
StyledEditWindow::MenusBeginning()
{
	// update the font menu
	// unselect the old values
	if (fCurrentFontItem != NULL) {
		fCurrentFontItem->SetMarked(false);
		BMenu* menu = fCurrentFontItem->Submenu();
		if (menu != NULL) {
			BMenuItem* item = menu->FindMarked();
			if (item != NULL)
				item->SetMarked(false);
		}
	}

	if (fCurrentStyleItem != NULL) {
		fCurrentStyleItem->SetMarked(false);
	}

	BMenuItem* oldColorItem = fFontColorMenu->FindMarked();
	if (oldColorItem != NULL)
		oldColorItem->SetMarked(false);

	BMenuItem* oldSizeItem = fFontSizeMenu->FindMarked();
	if (oldSizeItem != NULL)
		oldSizeItem->SetMarked(false);

	// find the current font, color, size
	BFont font;
	uint32 sameProperties;
	rgb_color color = ui_color(B_DOCUMENT_TEXT_COLOR);
	bool sameColor;
	fTextView->GetFontAndColor(&font, &sameProperties, &color, &sameColor);
	color.alpha = 255;

	if (sameColor) {
		if (fDefaultFontColorItem->Color() == color)
			fDefaultFontColorItem->SetMarked(true);
		else {
			for (int i = 0; i < fFontColorMenu->CountItems(); i++) {
				ColorMenuItem* item = dynamic_cast<ColorMenuItem*>
					(fFontColorMenu->ItemAt(i));
				if (item != NULL && item->Color() == color) {
					item->SetMarked(true);
					break;
				}
			}
		}
	}

	if (sameProperties & B_FONT_SIZE) {
		if ((int)font.Size() == font.Size()) {
			// select the current font size
			char fontSizeStr[16];
			snprintf(fontSizeStr, 15, "%i", (int)font.Size());
			BMenuItem* item = fFontSizeMenu->FindItem(fontSizeStr);
			if (item != NULL)
				item->SetMarked(true);
		}
	}

	font_family family;
	font_style style;
	font.GetFamilyAndStyle(&family, &style);

	fCurrentFontItem = fFontMenu->FindItem(family);

	if (fCurrentFontItem != NULL) {
		fCurrentFontItem->SetMarked(true);
		BMenu* menu = fCurrentFontItem->Submenu();
		if (menu != NULL) {
			BMenuItem* item = menu->FindItem(style);
			fCurrentStyleItem = item;
			if (fCurrentStyleItem != NULL)
				item->SetMarked(true);
		}
	}

	fBoldItem->SetMarked((font.Face() & B_BOLD_FACE) != 0);
	fItalicItem->SetMarked((font.Face() & B_ITALIC_FACE) != 0);

	switch (fTextView->Alignment()) {
		case B_ALIGN_LEFT:
		default:
			fAlignLeft->SetMarked(true);
			break;
		case B_ALIGN_CENTER:
			fAlignCenter->SetMarked(true);
			break;
		case B_ALIGN_RIGHT:
			fAlignRight->SetMarked(true);
			break;
	}

	// text encoding
	const BCharacterSet* charset
		= BCharacterSetRoster::GetCharacterSetByFontID(fTextView->GetEncoding());
	BMenu* encodingMenu = fEncodingItem->Submenu();
	if (charset != NULL && encodingMenu != NULL) {
		const char* mime = charset->GetMIMEName();
		BString name(charset->GetPrintName());
		if (mime)
			name << " (" << mime << ")";

		BMenuItem* item = encodingMenu->FindItem(name);
		if (item != NULL)
			item->SetMarked(true);
	}
}


#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "SaveAlert"


status_t
StyledEditWindow::Save(BMessage* message)
{
	_NodeMonitorSuspender nodeMonitorSuspender(this);

	if (!message)
		message = fSaveMessage;

	if (!message)
		return B_ERROR;

	entry_ref dirRef;
	const char* name;
	if (message->FindRef("directory", &dirRef) != B_OK
		|| message->FindString("name", &name) != B_OK)
		return B_BAD_VALUE;

	BDirectory dir(&dirRef);
	BEntry entry(&dir, name);

	status_t status = B_ERROR;
	if (dir.InitCheck() == B_OK && entry.InitCheck() == B_OK) {
		struct stat st;
		BFile file(&entry, B_READ_WRITE | B_CREATE_FILE);
		if (file.InitCheck() == B_OK
			&& (status = file.GetStat(&st)) == B_OK) {
			// check the file permissions
			if (!((getuid() == st.st_uid && (S_IWUSR & st.st_mode))
				|| (getgid() == st.st_gid && (S_IWGRP & st.st_mode))
				|| (S_IWOTH & st.st_mode))) {
				BString alertText;
				bs_printf(&alertText, B_TRANSLATE("This file is marked "
					"read-only. Save changes to the document \"%s\"? "), name);
				switch (_ShowAlert(alertText, B_TRANSLATE("Cancel"),
						B_TRANSLATE("Don't save"),
						B_TRANSLATE("Save"), B_WARNING_ALERT)) {
					case 0:
						return B_CANCELED;
					case 1:
						return B_OK;
					default:
						break;
				}
			}

			status = fTextView->WriteStyledEditFile(&file);
		}
	}

	if (status != B_OK) {
		BString alertText;
		bs_printf(&alertText, B_TRANSLATE("Error saving \"%s\":\n%s"), name,
			strerror(status));

		_ShowAlert(alertText, B_TRANSLATE("OK"), "", "", B_STOP_ALERT);
		return status;
	}

	SetTitle(name);

	if (fSaveMessage != message) {
		delete fSaveMessage;
		fSaveMessage = new BMessage(*message);
	}

	// clear clean modes
	fSaveItem->SetEnabled(false);
	fUndoCleans = false;
	fRedoCleans = false;
	fClean = true;
	fNagOnNodeChange = true;

	PostMessage(UPDATE_STATUS);
	PostMessage(UPDATE_STATUS_REF);

	return status;
}


#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "Open_and_SaveAsPanel"


status_t
StyledEditWindow::SaveAs(BMessage* message)
{
	if (fSavePanel == NULL) {
		entry_ref* directory = NULL;
		entry_ref dirRef;
		if (fSaveMessage != NULL) {
			if (fSaveMessage->FindRef("directory", &dirRef) == B_OK)
				directory = &dirRef;
		}

		BMessenger target(this);
		fSavePanel = new BFilePanel(B_SAVE_PANEL, &target,
			directory, B_FILE_NODE, false);

		BMenuBar* menuBar = dynamic_cast<BMenuBar*>(
			fSavePanel->Window()->FindView("MenuBar"));
		if (menuBar != NULL) {
			fSavePanelEncodingMenu = new BMenu(B_TRANSLATE("Encoding"));
			fSavePanelEncodingMenu->SetRadioMode(true);
			menuBar->AddItem(fSavePanelEncodingMenu);

			BCharacterSetRoster roster;
			BCharacterSet charset;
			while (roster.GetNextCharacterSet(&charset) == B_NO_ERROR) {
				BString name(charset.GetPrintName());
				const char* mime = charset.GetMIMEName();
				if (mime) {
					name.Append(" (");
					name.Append(mime);
					name.Append(")");
				}
				BMenuItem * item = new BMenuItem(name.String(),
					new BMessage(SAVE_AS_ENCODING));
				item->SetTarget(this);
				fSavePanelEncodingMenu->AddItem(item);
				if (charset.GetFontID() == fTextView->GetEncoding())
					item->SetMarked(true);
			}
		}
	}

	fSavePanel->SetSaveText(Title());
	if (message != NULL)
		fSavePanel->SetMessage(message);

	fSavePanel->Show();
	return B_OK;
}


void
StyledEditWindow::OpenFile(entry_ref* ref)
{
	if (_LoadFile(ref) != B_OK) {
		fSaveItem->SetEnabled(true);
			// allow saving new files
		return;
	}

	fSaveMessage = new BMessage(B_SAVE_REQUESTED);
	if (fSaveMessage) {
		BEntry entry(ref, true);
		BEntry parent;
		entry_ref parentRef;
		char name[B_FILE_NAME_LENGTH];

		entry.GetParent(&parent);
		entry.GetName(name);
		parent.GetRef(&parentRef);
		fSaveMessage->AddRef("directory", &parentRef);
		fSaveMessage->AddString("name", name);
		SetTitle(name);

		_LoadAttrs();
	}

	_SwitchNodeMonitor(true, ref);

	PostMessage(UPDATE_STATUS_REF);

	fReloadItem->SetEnabled(fSaveMessage != NULL);
	fEncodingItem->SetEnabled(fSaveMessage != NULL);
}


status_t
StyledEditWindow::PageSetup(const char* documentName)
{
	BPrintJob printJob(documentName);

	if (fPrintSettings != NULL)
		printJob.SetSettings(new BMessage(*fPrintSettings));

	status_t result = printJob.ConfigPage();
	if (result == B_OK) {
		delete fPrintSettings;
		fPrintSettings = printJob.Settings();
	}

	return result;
}


void
StyledEditWindow::Print(const char* documentName)
{
	BPrintJob printJob(documentName);
	if (fPrintSettings)
		printJob.SetSettings(new BMessage(*fPrintSettings));

	if (printJob.ConfigJob() != B_OK)
		return;

	delete fPrintSettings;
	fPrintSettings = printJob.Settings();

	// information from printJob
	BRect printableRect = printJob.PrintableRect();
	int32 firstPage = printJob.FirstPage();
	int32 lastPage = printJob.LastPage();

	// lines eventually to be used to compute pages to print
	int32 firstLine = 0;
	int32 lastLine = fTextView->CountLines();

	// values to be computed
	int32 pagesInDocument = 1;
	int32 linesInDocument = fTextView->CountLines();

	int32 currentLine = 0;
	while (currentLine < linesInDocument) {
		float currentHeight = 0;
		while (currentHeight < printableRect.Height() && currentLine
				< linesInDocument) {
			currentHeight += fTextView->LineHeight(currentLine);
			if (currentHeight < printableRect.Height())
				currentLine++;
		}
		if (pagesInDocument == lastPage)
			lastLine = currentLine - 1;

		if (currentHeight >= printableRect.Height()) {
			pagesInDocument++;
			if (pagesInDocument == firstPage)
				firstLine = currentLine;
		}
	}

	if (lastPage > pagesInDocument - 1) {
		lastPage = pagesInDocument - 1;
		lastLine = currentLine - 1;
	}


	printJob.BeginJob();
	if (fTextView->CountLines() > 0 && fTextView->TextLength() > 0) {
		int32 printLine = firstLine;
		while (printLine <= lastLine) {
			float currentHeight = 0;
			int32 firstLineOnPage = printLine;
			while (currentHeight < printableRect.Height()
				&& printLine <= lastLine)
			{
				currentHeight += fTextView->LineHeight(printLine);
				if (currentHeight < printableRect.Height())
					printLine++;
			}

			float top = 0;
			if (firstLineOnPage != 0)
				top = fTextView->TextHeight(0, firstLineOnPage - 1);

			float bottom = fTextView->TextHeight(0, printLine - 1);
			BRect textRect(0.0, top + TEXT_INSET,
				printableRect.Width(), bottom + TEXT_INSET);
			printJob.DrawView(fTextView, textRect, B_ORIGIN);
			printJob.SpoolPage();
		}
	}


	printJob.CommitJob();
}


void
StyledEditWindow::SearchAllWindows(BString find, BString replace,
	bool caseSensitive)
{
	int32 numWindows;
	numWindows = be_app->CountWindows();

	BMessage* message;
	message= new BMessage(MSG_REPLACE_ALL);
	message->AddString("FindText", find);
	message->AddString("ReplaceText", replace);
	message->AddBool("casesens", caseSensitive);

	while (numWindows >= 0) {
		StyledEditWindow* window = dynamic_cast<StyledEditWindow *>(
			be_app->WindowAt(numWindows));

		BMessenger messenger(window);
		messenger.SendMessage(message);

		numWindows--;
	}
}


bool
StyledEditWindow::IsDocumentEntryRef(const entry_ref* ref)
{
	if (ref == NULL)
		return false;

	if (fSaveMessage == NULL)
		return false;

	entry_ref dir;
	const char* name;
	if (fSaveMessage->FindRef("directory", &dir) != B_OK
		|| fSaveMessage->FindString("name", &name) != B_OK)
		return false;

	entry_ref documentRef;
	BPath documentPath(&dir);
	documentPath.Append(name);
	get_ref_for_path(documentPath.Path(), &documentRef);

	return *ref == documentRef;
}


// #pragma mark - private methods


#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "Menus"


void
StyledEditWindow::_InitWindow(uint32 encoding)
{
	fPrintSettings = NULL;
	fSaveMessage = NULL;

	// undo modes
	fUndoFlag = false;
	fCanUndo = false;
	fRedoFlag = false;
	fCanRedo = false;

	// clean modes
	fUndoCleans = false;
	fRedoCleans = false;
	fClean = true;

	// search- state
	fReplaceString = "";
	fStringToFind = "";
	fCaseSensitive = false;
	fWrapAround = false;
	fBackSearch = false;

	fNagOnNodeChange = true;

	// add menubar
	fMenuBar = new BMenuBar(BRect(0, 0, 0, 0), "menubar");
	AddChild(fMenuBar);

	// add textview and scrollview

	BRect viewFrame = Bounds();
	viewFrame.top = fMenuBar->Bounds().Height() + 1;
	viewFrame.right -=  B_V_SCROLL_BAR_WIDTH;
	viewFrame.left = 0;
	viewFrame.bottom -= B_H_SCROLL_BAR_HEIGHT;

	BRect textBounds = viewFrame;
	textBounds.OffsetTo(B_ORIGIN);
	textBounds.InsetBy(TEXT_INSET, TEXT_INSET);

	fTextView = new StyledEditView(viewFrame, textBounds, this);
	fTextView->SetDoesUndo(true);
	fTextView->SetStylable(true);
	fTextView->SetEncoding(encoding);

	fScrollView = new BScrollView("scrollview", fTextView, B_FOLLOW_ALL, 0,
		true, true, B_PLAIN_BORDER);
	AddChild(fScrollView);
	fTextView->MakeFocus(true);

	fStatusView = new StatusView(fScrollView);
	fScrollView->AddChild(fStatusView);

	// Add "File"-menu:
	BMenu* menu = new BMenu(B_TRANSLATE("File"));
	fMenuBar->AddItem(menu);

	BMenuItem* menuItem;
	menu->AddItem(menuItem = new BMenuItem(B_TRANSLATE("New"),
		new BMessage(MENU_NEW), 'N'));
	menuItem->SetTarget(be_app);

	menu->AddItem(menuItem = new BMenuItem(BRecentFilesList::NewFileListMenu(
		B_TRANSLATE("Open" B_UTF8_ELLIPSIS), NULL, NULL, be_app, 9, true,
		NULL, APP_SIGNATURE), new BMessage(MENU_OPEN)));
	menuItem->SetShortcut('O', 0);
	menuItem->SetTarget(be_app);
	menu->AddSeparatorItem();

	menu->AddItem(fSaveItem = new BMenuItem(B_TRANSLATE("Save"),
		new BMessage(MENU_SAVE), 'S'));
	fSaveItem->SetEnabled(false);
	menu->AddItem(menuItem = new BMenuItem(
		B_TRANSLATE("Save as" B_UTF8_ELLIPSIS), new BMessage(MENU_SAVEAS)));
	menuItem->SetShortcut('S', B_SHIFT_KEY);
	menuItem->SetEnabled(true);

	menu->AddItem(fReloadItem
		= new BMenuItem(B_TRANSLATE("Reload" B_UTF8_ELLIPSIS),
		new BMessage(MENU_RELOAD), 'L'));
	fReloadItem->SetEnabled(false);

	menu->AddItem(new BMenuItem(B_TRANSLATE("Close"),
		new BMessage(MENU_CLOSE), 'W'));

	menu->AddSeparatorItem();
	menu->AddItem(new BMenuItem(B_TRANSLATE("Page setup" B_UTF8_ELLIPSIS),
		new BMessage(MENU_PAGESETUP)));
	menu->AddItem(new BMenuItem(B_TRANSLATE("Print" B_UTF8_ELLIPSIS),
		new BMessage(MENU_PRINT), 'P'));

	menu->AddSeparatorItem();
	menu->AddItem(new BMenuItem(B_TRANSLATE("Quit"),
		new BMessage(MENU_QUIT), 'Q'));

	// Add the "Edit"-menu:
	menu = new BMenu(B_TRANSLATE("Edit"));
	fMenuBar->AddItem(menu);

	menu->AddItem(fUndoItem = new BMenuItem(B_TRANSLATE("Can't undo"),
		new BMessage(B_UNDO), 'Z'));
	fUndoItem->SetEnabled(false);

	menu->AddSeparatorItem();
	menu->AddItem(fCutItem = new BMenuItem(B_TRANSLATE("Cut"),
		new BMessage(B_CUT), 'X'));
	fCutItem->SetEnabled(false);
	fCutItem->SetTarget(fTextView);

	menu->AddItem(fCopyItem = new BMenuItem(B_TRANSLATE("Copy"),
		new BMessage(B_COPY), 'C'));
	fCopyItem->SetEnabled(false);
	fCopyItem->SetTarget(fTextView);

	menu->AddItem(menuItem = new BMenuItem(B_TRANSLATE("Paste"),
		new BMessage(B_PASTE), 'V'));
	menuItem->SetTarget(fTextView);

	menu->AddSeparatorItem();
	menu->AddItem(menuItem = new BMenuItem(B_TRANSLATE("Select all"),
		new BMessage(B_SELECT_ALL), 'A'));
	menuItem->SetTarget(fTextView);

	menu->AddSeparatorItem();
	menu->AddItem(new BMenuItem(B_TRANSLATE("Find" B_UTF8_ELLIPSIS),
		new BMessage(MENU_FIND), 'F'));
	menu->AddItem(fFindAgainItem= new BMenuItem(B_TRANSLATE("Find again"),
		new BMessage(MENU_FIND_AGAIN), 'G'));
	fFindAgainItem->SetEnabled(false);

	menu->AddItem(new BMenuItem(B_TRANSLATE("Find selection"),
		new BMessage(MENU_FIND_SELECTION), 'H'));
	menu->AddItem(fReplaceItem = new BMenuItem(B_TRANSLATE("Replace" B_UTF8_ELLIPSIS),
		new BMessage(MENU_REPLACE), 'R'));
	menu->AddItem(fReplaceSameItem = new BMenuItem(B_TRANSLATE("Replace next"),
		new BMessage(MENU_REPLACE_SAME), 'T'));
	fReplaceSameItem->SetEnabled(false);

	// Add the "Font"-menu:
	fFontMenu = new BMenu(B_TRANSLATE("Font"));
	fMenuBar->AddItem(fFontMenu);

	// "Size"-subMenu
	fFontSizeMenu = new BMenu(B_TRANSLATE("Size"));
	fFontSizeMenu->SetRadioMode(true);
	fFontMenu->AddItem(fFontSizeMenu);

	const int32 fontSizes[] = {9, 10, 11, 12, 14, 18, 24, 36, 48, 72};
	for (uint32 i = 0; i < sizeof(fontSizes) / sizeof(fontSizes[0]); i++) {
		BMessage* fontMessage = new BMessage(FONT_SIZE);
		fontMessage->AddFloat("size", fontSizes[i]);

		char label[64];
		snprintf(label, sizeof(label), "%" B_PRId32, fontSizes[i]);
		fFontSizeMenu->AddItem(menuItem = new BMenuItem(label, fontMessage));

		if (fontSizes[i] == (int32)be_plain_font->Size())
			menuItem->SetMarked(true);
	}

	// "Color"-subMenu
	fFontColorMenu = new BMenu(B_TRANSLATE("Color"), 0, 0);
	fFontColorMenu->SetRadioMode(true);
	fFontMenu->AddItem(fFontColorMenu);

	_BuildFontColorMenu(fFontColorMenu);

	fFontMenu->AddSeparatorItem();

	// "Bold" & "Italic" menu items
	fFontMenu->AddItem(fBoldItem = new BMenuItem(B_TRANSLATE("Bold"),
		new BMessage(kMsgSetBold)));
	fFontMenu->AddItem(fItalicItem = new BMenuItem(B_TRANSLATE("Italic"),
		new BMessage(kMsgSetItalic)));
	fBoldItem->SetShortcut('B', 0);
	fItalicItem->SetShortcut('I', 0);
	fFontMenu->AddSeparatorItem();

	// Available fonts

	fCurrentFontItem = 0;
	fCurrentStyleItem = 0;

	BMenu* subMenu;
	int32 numFamilies = count_font_families();
	for (int32 i = 0; i < numFamilies; i++) {
		font_family family;
		if (get_font_family(i, &family) == B_OK) {
			subMenu = new BMenu(family);
			subMenu->SetRadioMode(true);
			fFontMenu->AddItem(new BMenuItem(subMenu,
				new BMessage(FONT_FAMILY)));

			int32 numStyles = count_font_styles(family);
			for (int32 j = 0; j < numStyles; j++) {
				font_style style;
				uint32 flags;
				if (get_font_style(family, j, &style, &flags) == B_OK) {
					subMenu->AddItem(new BMenuItem(style,
						new BMessage(FONT_STYLE)));
				}
			}
		}
	}

	// Add the "Document"-menu:
	menu = new BMenu(B_TRANSLATE("Document"));
	fMenuBar->AddItem(menu);

	// "Align"-subMenu:
	subMenu = new BMenu(B_TRANSLATE("Align"));
	subMenu->SetRadioMode(true);

	subMenu->AddItem(fAlignLeft = new BMenuItem(B_TRANSLATE("Left"),
		new BMessage(ALIGN_LEFT)));
	fAlignLeft->SetMarked(true);
	fAlignLeft->SetShortcut('L', B_OPTION_KEY);

	subMenu->AddItem(fAlignCenter = new BMenuItem(B_TRANSLATE("Center"),
		new BMessage(ALIGN_CENTER)));
	fAlignCenter->SetShortcut('C', B_OPTION_KEY);

	subMenu->AddItem(fAlignRight = new BMenuItem(B_TRANSLATE("Right"),
		new BMessage(ALIGN_RIGHT)));
	fAlignRight->SetShortcut('R', B_OPTION_KEY);

	menu->AddItem(subMenu);
	menu->AddItem(fWrapItem = new BMenuItem(B_TRANSLATE("Wrap lines"),
		new BMessage(WRAP_LINES)));
	fWrapItem->SetMarked(true);
	fWrapItem->SetShortcut('W', B_OPTION_KEY);

	BMessage *message = new BMessage(MENU_RELOAD);
	message->AddString("encoding", "auto");
	menu->AddItem(fEncodingItem = new BMenuItem(_PopulateEncodingMenu(
		new BMenu(B_TRANSLATE("Text encoding")), "UTF-8"),
		message));
	fEncodingItem->SetEnabled(false);

	menu->AddSeparatorItem();
	menu->AddItem(new BMenuItem(B_TRANSLATE("Statistics" B_UTF8_ELLIPSIS),
		new BMessage(SHOW_STATISTICS)));

	fSavePanel = NULL;
	fSavePanelEncodingMenu = NULL;
		// build lazily
}


void
StyledEditWindow::_BuildFontColorMenu(BMenu* menu)
{
	if (menu == NULL)
		return;

	BFont font;
	menu->GetFont(&font);
	font_height fh;
	font.GetHeight(&fh);

	const float itemHeight = ceilf(fh.ascent + fh.descent + 2 * fh.leading);
	const float margin = 8.0;
	const int nbColumns = 5;

	BMessage msgTemplate(FONT_COLOR);
	BRect matrixArea(0, 0, 0, 0);

	// we place the color palette, reserving room at the top
	for (uint i = 0; i < sizeof(palette) / sizeof(rgb_color); i++) {
		BPoint topLeft((i % nbColumns) * (itemHeight + margin),
			(i / nbColumns) * (itemHeight + margin));
		BRect buttonArea(topLeft.x, topLeft.y, topLeft.x + itemHeight,
			topLeft.y + itemHeight);
		buttonArea.OffsetBy(margin, itemHeight + margin + margin);
		menu->AddItem(
			new ColorMenuItem("", palette[i], new BMessage(msgTemplate)),
			buttonArea);
		buttonArea.OffsetBy(margin, margin);
		matrixArea = matrixArea | buttonArea;
	}

	// separator at the bottom to add spacing in the matrix menu
	matrixArea.top = matrixArea.bottom;
	menu->AddItem(new BSeparatorItem(), matrixArea);

	matrixArea.top = 0;
	matrixArea.bottom = itemHeight + 4;

	BMessage* msg = new BMessage(msgTemplate);
	msg->AddBool("default", true);
	fDefaultFontColorItem = new ColorMenuItem(B_TRANSLATE("Default"),
		ui_color(B_DOCUMENT_TEXT_COLOR), msg);
	menu->AddItem(fDefaultFontColorItem, matrixArea);

	matrixArea.top = matrixArea.bottom;
	matrixArea.bottom = matrixArea.top + margin;
	menu->AddItem(new BSeparatorItem(), matrixArea);
}


void
StyledEditWindow::_LoadAttrs()
{
	entry_ref dir;
	const char* name;
	if (fSaveMessage->FindRef("directory", &dir) != B_OK
		|| fSaveMessage->FindString("name", &name) != B_OK)
		return;

	BPath documentPath(&dir);
	documentPath.Append(name);

	BNode documentNode(documentPath.Path());
	if (documentNode.InitCheck() != B_OK)
		return;

	// info about position of caret may live in the file attributes
	int32 position = 0;
	if (documentNode.ReadAttr("be:caret_position", B_INT32_TYPE, 0,
			&position, sizeof(position)) != sizeof(position))
		position = 0;

	fTextView->Select(position, position);
	fTextView->ScrollToOffset(position);

	BRect newFrame;
	ssize_t bytesRead = documentNode.ReadAttr(kInfoAttributeName, B_RECT_TYPE,
		0, &newFrame, sizeof(BRect));
	if (bytesRead != sizeof(BRect))
		return;

	swap_data(B_RECT_TYPE, &newFrame, sizeof(BRect), B_SWAP_BENDIAN_TO_HOST);

	// Check if the frame in on screen, otherwise, ignore it
	BScreen screen(this);
	if (newFrame.Width() > 32 && newFrame.Height() > 32
		&& screen.Frame().Contains(newFrame)) {
		MoveTo(newFrame.left, newFrame.top);
		ResizeTo(newFrame.Width(), newFrame.Height());
	}
}


void
StyledEditWindow::_SaveAttrs()
{
	if (!fSaveMessage)
		return;

	entry_ref dir;
	const char* name;
	if (fSaveMessage->FindRef("directory", &dir) != B_OK
		|| fSaveMessage->FindString("name", &name) != B_OK)
		return;

	BPath documentPath(&dir);
	documentPath.Append(name);

	BNode documentNode(documentPath.Path());
	if (documentNode.InitCheck() != B_OK)
		return;

	BRect frame(Frame());
	swap_data(B_RECT_TYPE, &frame, sizeof(BRect), B_SWAP_HOST_TO_BENDIAN);

	documentNode.WriteAttr(kInfoAttributeName, B_RECT_TYPE, 0, &frame,
		sizeof(BRect));

	// preserve caret line and position
	int32 start, end;
	fTextView->GetSelection(&start, &end);
	documentNode.WriteAttr("be:caret_position",
			B_INT32_TYPE, 0, &start, sizeof(start));
}


#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "LoadAlert"


status_t
StyledEditWindow::_LoadFile(entry_ref* ref, const char* forceEncoding)
{
	BEntry entry(ref, true);
		// traverse an eventual link

	status_t status = entry.InitCheck();
	if (status == B_OK && entry.IsDirectory())
		status = B_IS_A_DIRECTORY;

	BFile file;
	if (status == B_OK)
		status = file.SetTo(&entry, B_READ_ONLY);
	if (status == B_OK)
		status = fTextView->GetStyledText(&file, forceEncoding);

	if (status == B_ENTRY_NOT_FOUND) {
		// Treat non-existing files consideratley; we just want to get an
		// empty window for them - to create this new document
		status = B_OK;
	}

	if (status != B_OK) {
		// If an error occured, bail out and tell the user what happened
		BEntry entry(ref, true);
		char name[B_FILE_NAME_LENGTH];
		if (entry.GetName(name) != B_OK)
			strlcpy(name, B_TRANSLATE("???"), sizeof(name));

		BString text;
		if (status == B_BAD_TYPE)
			bs_printf(&text,
				B_TRANSLATE("Error loading \"%s\":\n\tUnsupported format"), name);
		else
			bs_printf(&text, B_TRANSLATE("Error loading \"%s\":\n\t%s"),
				name, strerror(status));

		_ShowAlert(text, B_TRANSLATE("OK"), "", "", B_STOP_ALERT);
		return status;
	}

	struct stat st;
	if (file.InitCheck() == B_OK && file.GetStat(&st) == B_OK) {
		bool editable = (getuid() == st.st_uid && S_IWUSR & st.st_mode)
					|| (getgid() == st.st_gid && S_IWGRP & st.st_mode)
					|| (S_IWOTH & st.st_mode);
		BVolume volume(ref->device);
		editable = editable && !volume.IsReadOnly();
		_SetReadOnly(!editable);
	}

	// update alignment
	switch (fTextView->Alignment()) {
		case B_ALIGN_LEFT:
		default:
			fAlignLeft->SetMarked(true);
			break;
		case B_ALIGN_CENTER:
			fAlignCenter->SetMarked(true);
			break;
		case B_ALIGN_RIGHT:
			fAlignRight->SetMarked(true);
			break;
	}

	// update word wrapping
	fWrapItem->SetMarked(fTextView->DoesWordWrap());
	return B_OK;
}


#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "RevertToSavedAlert"


void
StyledEditWindow::_ReloadDocument(BMessage* message)
{
	entry_ref ref;
	const char* name;

	if (fSaveMessage == NULL || message == NULL
		|| fSaveMessage->FindRef("directory", &ref) != B_OK
		|| fSaveMessage->FindString("name", &name) != B_OK)
		return;

	BDirectory dir(&ref);
	status_t status = dir.InitCheck();
	BEntry entry;
	if (status == B_OK)
		status = entry.SetTo(&dir, name);

	if (status == B_OK)
		status = entry.GetRef(&ref);

	if (status != B_OK || !entry.Exists()) {
		BString alertText;
		bs_printf(&alertText,
			B_TRANSLATE("Cannot revert, file not found: \"%s\"."), name);
		_ShowAlert(alertText, B_TRANSLATE("OK"), "", "", B_STOP_ALERT);
		return;
	}

	if (!fClean) {
		BString alertText;
		bs_printf(&alertText,
			B_TRANSLATE("\"%s\" has unsaved changes.\n"
				"Revert it to the last saved version? "), Title());
		if (_ShowAlert(alertText, B_TRANSLATE("Cancel"), B_TRANSLATE("Revert"),
			"", B_WARNING_ALERT) != 1)
			return;
	}

	const BCharacterSet* charset
		= BCharacterSetRoster::GetCharacterSetByFontID(
			fTextView->GetEncoding());
	const char* forceEncoding = NULL;
	if (message->FindString("encoding", &forceEncoding) != B_OK) {
		if (charset != NULL)
			forceEncoding = charset->GetName();
	} else {
		if (charset != NULL) {
			// UTF8 id assumed equal to -1
			const uint32 idUTF8 = (uint32)-1;
			uint32 id = charset->GetConversionID();
			if (strcmp(forceEncoding, "next") == 0)
				id = id == B_MS_WINDOWS_1250_CONVERSION	? idUTF8 : id + 1;
			else if (strcmp(forceEncoding, "previous") == 0)
				id = id == idUTF8 ? B_MS_WINDOWS_1250_CONVERSION : id - 1;
			const BCharacterSet* newCharset
				= BCharacterSetRoster::GetCharacterSetByConversionID(id);
			if (newCharset != NULL)
				forceEncoding = newCharset->GetName();
		}
	}

	BScrollBar* vertBar = fScrollView->ScrollBar(B_VERTICAL);
	float vertPos = vertBar != NULL ? vertBar->Value() : 0.f;

	DisableUpdates();

	fTextView->Reset();

	status = _LoadFile(&ref, forceEncoding);

	if (vertBar != NULL)
		vertBar->SetValue(vertPos);

	EnableUpdates();

	if (status != B_OK)
		return;

#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "Menus"

	// clear undo modes
	fUndoItem->SetLabel(B_TRANSLATE("Can't undo"));
	fUndoItem->SetEnabled(false);
	fUndoFlag = false;
	fCanUndo = false;
	fRedoFlag = false;
	fCanRedo = false;

	// clear clean modes
	fSaveItem->SetEnabled(false);

	fUndoCleans = false;
	fRedoCleans = false;
	fClean = true;

	fNagOnNodeChange = true;
}


status_t
StyledEditWindow::_UnlockFile()
{
	_NodeMonitorSuspender nodeMonitorSuspender(this);

	if (!fSaveMessage)
		return B_ERROR;

	entry_ref dirRef;
	const char* name;
	if (fSaveMessage->FindRef("directory", &dirRef) != B_OK
		|| fSaveMessage->FindString("name", &name) != B_OK)
		return B_BAD_VALUE;

	BDirectory dir(&dirRef);
	BEntry entry(&dir, name);

	status_t status = dir.InitCheck();
	if (status != B_OK)
		return status;

	status = entry.InitCheck();
	if (status != B_OK)
		return status;

	struct stat st;
	BFile file(&entry, B_READ_WRITE);
	status = file.InitCheck();
	if (status != B_OK)
		return status;

	status = file.GetStat(&st);
	if (status != B_OK)
		return status;

	st.st_mode |= S_IWUSR;
	status = file.SetPermissions(st.st_mode);
	if (status == B_OK)
		_SetReadOnly(false);

	return status;
}


bool
StyledEditWindow::_Search(BString string, bool caseSensitive, bool wrap,
	bool backSearch, bool scrollToOccurence)
{
	int32 start;
	int32 finish;

	start = B_ERROR;

	int32 length = string.Length();
	if (length == 0)
		return false;

	BString viewText(fTextView->Text());
	int32 textStart, textFinish;
	fTextView->GetSelection(&textStart, &textFinish);
	if (backSearch) {
		if (caseSensitive)
			start = viewText.FindLast(string, textStart);
		else
			start = viewText.IFindLast(string, textStart);
	} else {
		if (caseSensitive)
			start = viewText.FindFirst(string, textFinish);
		else
			start = viewText.IFindFirst(string, textFinish);
	}
	if (start == B_ERROR && wrap) {
		if (backSearch) {
			if (caseSensitive)
				start = viewText.FindLast(string, viewText.Length());
			else
				start = viewText.IFindLast(string, viewText.Length());
		} else {
			if (caseSensitive)
				start = viewText.FindFirst(string, 0);
			else
				start = viewText.IFindFirst(string, 0);
		}
	}

	if (start != B_ERROR) {
		finish = start + length;
		fTextView->Select(start, finish);

		if (scrollToOccurence)
			fTextView->ScrollToSelection();
		return true;
	}

	return false;
}


void
StyledEditWindow::_FindSelection()
{
	int32 selectionStart, selectionFinish;
	fTextView->GetSelection(&selectionStart, &selectionFinish);

	int32 selectionLength = selectionFinish- selectionStart;

	BString viewText = fTextView->Text();
	viewText.CopyInto(fStringToFind, selectionStart, selectionLength);
	fFindAgainItem->SetEnabled(true);
	_Search(fStringToFind, fCaseSensitive, fWrapAround, fBackSearch);
}


bool
StyledEditWindow::_Replace(BString findThis, BString replaceWith,
	bool caseSensitive, bool wrap, bool backSearch)
{
	if (_Search(findThis, caseSensitive, wrap, backSearch)) {
		int32 start;
		int32 finish;
		fTextView->GetSelection(&start, &finish);

		_UpdateCleanUndoRedoSaveRevert();
		fTextView->SetSuppressChanges(true);
		fTextView->Delete(start, start + findThis.Length());
		fTextView->Insert(start, replaceWith.String(), replaceWith.Length());
		fTextView->SetSuppressChanges(false);
		fTextView->Select(start, start + replaceWith.Length());
		fTextView->ScrollToSelection();
		return true;
	}

	return false;
}


void
StyledEditWindow::_ReplaceAll(BString findThis, BString replaceWith,
	bool caseSensitive)
{
	bool first = true;
	fTextView->SetSuppressChanges(true);

	// start from the beginning of text
	fTextView->Select(0, 0);

	// iterate occurences of findThis without wrapping around
	while (_Search(findThis, caseSensitive, false, false, false)) {
		if (first) {
			_UpdateCleanUndoRedoSaveRevert();
			first = false;
		}
		int32 start;
		int32 finish;

		fTextView->GetSelection(&start, &finish);
		fTextView->Delete(start, start + findThis.Length());
		fTextView->Insert(start, replaceWith.String(), replaceWith.Length());

		// advance the caret behind the inserted text
		start += replaceWith.Length();
		fTextView->Select(start, start);
	}
	fTextView->ScrollToSelection();
	fTextView->SetSuppressChanges(false);
}


void
StyledEditWindow::_SetFontSize(float fontSize)
{
	uint32 sameProperties;
	BFont font;

	fTextView->GetFontAndColor(&font, &sameProperties);
	font.SetSize(fontSize);
	fTextView->SetFontAndColor(&font, B_FONT_SIZE);

	_UpdateCleanUndoRedoSaveRevert();
}


void
StyledEditWindow::_SetFontColor(const rgb_color* color)
{
	uint32 sameProperties;
	BFont font;

	fTextView->GetFontAndColor(&font, &sameProperties, NULL, NULL);
	fTextView->SetFontAndColor(&font, 0, color);

	_UpdateCleanUndoRedoSaveRevert();
}


void
StyledEditWindow::_SetFontStyle(const char* fontFamily, const char* fontStyle)
{
	BFont font;
	uint32 sameProperties;

	// find out what the old font was
	font_family oldFamily;
	font_style oldStyle;
	fTextView->GetFontAndColor(&font, &sameProperties);
	font.GetFamilyAndStyle(&oldFamily, &oldStyle);

	// clear that family's bit on the menu, if necessary
	if (strcmp(oldFamily, fontFamily)) {
		BMenuItem* oldItem = fFontMenu->FindItem(oldFamily);
		if (oldItem != NULL) {
			oldItem->SetMarked(false);
			BMenu* menu = oldItem->Submenu();
			if (menu != NULL) {
				oldItem = menu->FindItem(oldStyle);
				if (oldItem != NULL)
					oldItem->SetMarked(false);
			}
		}
	}

	font.SetFamilyAndStyle(fontFamily, fontStyle);

	uint16 face = 0;

	if (!(font.Face() & B_REGULAR_FACE))
		face = font.Face();

	if (fBoldItem->IsMarked())
		face |= B_BOLD_FACE;

	if (fItalicItem->IsMarked())
		face |= B_ITALIC_FACE;

	font.SetFace(face);

	fTextView->SetFontAndColor(&font, B_FONT_FAMILY_AND_STYLE);

	BMenuItem* superItem;
	superItem = fFontMenu->FindItem(fontFamily);
	if (superItem != NULL) {
		superItem->SetMarked(true);
		fCurrentFontItem = superItem;
	}

	_UpdateCleanUndoRedoSaveRevert();
}


#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "Statistics"


int32
StyledEditWindow::_ShowStatistics()
{
	size_t words = 0;
	bool inWord = false;
	size_t length = fTextView->TextLength();

	for (size_t i = 0; i < length; i++)	{
		if (BUnicodeChar::IsWhitespace(fTextView->Text()[i])) {
			inWord = false;
		} else if (!inWord)	{
			words++;
			inWord = true;
		}
	}

	BString result;
	result << B_TRANSLATE("Document statistics") << '\n' << '\n'
		<< B_TRANSLATE("Lines:") << ' ' << fTextView->CountLines() << '\n'
		<< B_TRANSLATE("Characters:") << ' ' << length << '\n'
		<< B_TRANSLATE("Words:") << ' ' << words;

	BAlert* alert = new BAlert("Statistics", result, B_TRANSLATE("OK"), NULL,
		NULL, B_WIDTH_AS_USUAL, B_EVEN_SPACING, B_INFO_ALERT);
	alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);

	return alert->Go();
}


void
StyledEditWindow::_SetReadOnly(bool readOnly)
{
	fReplaceItem->SetEnabled(!readOnly);
	fReplaceSameItem->SetEnabled(!readOnly);
	fFontMenu->SetEnabled(!readOnly);
	fAlignLeft->Menu()->SetEnabled(!readOnly);
	fWrapItem->SetEnabled(!readOnly);
	fTextView->MakeEditable(!readOnly);
}


#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "Menus"


void
StyledEditWindow::_UpdateCleanUndoRedoSaveRevert()
{
	fClean = false;
	fUndoCleans = false;
	fRedoCleans = false;
	fReloadItem->SetEnabled(fSaveMessage != NULL);
	fEncodingItem->SetEnabled(fSaveMessage != NULL);
	fSaveItem->SetEnabled(true);
	fUndoItem->SetLabel(B_TRANSLATE("Can't undo"));
	fUndoItem->SetEnabled(false);
	fCanUndo = false;
	fCanRedo = false;
}


int32
StyledEditWindow::_ShowAlert(const BString& text, const BString& label,
	const BString& label2, const BString& label3, alert_type type) const
{
	const char* button2 = NULL;
	if (label2.Length() > 0)
		button2 = label2.String();

	const char* button3 = NULL;
	button_spacing spacing = B_EVEN_SPACING;
	if (label3.Length() > 0) {
		button3 = label3.String();
		spacing = B_OFFSET_SPACING;
	}

	BAlert* alert = new BAlert("Alert", text.String(), label.String(), button2,
		button3, B_WIDTH_AS_USUAL, spacing, type);
	alert->SetShortcut(0, B_ESCAPE);

	return alert->Go();
}


BMenu*
StyledEditWindow::_PopulateEncodingMenu(BMenu* menu, const char* currentEncoding)
{
	menu->SetRadioMode(true);
	BString encoding(currentEncoding);
	if (encoding.Length() == 0)
		encoding.SetTo("UTF-8");

	BCharacterSetRoster roster;
	BCharacterSet charset;
	while (roster.GetNextCharacterSet(&charset) == B_OK) {
		const char* mime = charset.GetMIMEName();
		BString name(charset.GetPrintName());

		if (mime)
			name << " (" << mime << ")";

		BMessage *message = new BMessage(MENU_RELOAD);
		if (message != NULL) {
			message->AddString("encoding", charset.GetName());
			BMenuItem* item = new BMenuItem(name, message);
			if (encoding.Compare(charset.GetName()) == 0)
				item->SetMarked(true);
			menu->AddItem(item);
		}
	}

	menu->AddSeparatorItem();
	BMessage *message = new BMessage(MENU_RELOAD);
	message->AddString("encoding", "auto");
	menu->AddItem(new BMenuItem(B_TRANSLATE("Autodetect"), message));

	message = new BMessage(MENU_RELOAD);
	message->AddString("encoding", "next");
	AddShortcut(B_PAGE_DOWN, B_OPTION_KEY, message);
	message = new BMessage(MENU_RELOAD);
	message->AddString("encoding", "previous");
	AddShortcut(B_PAGE_UP, B_OPTION_KEY, message);

	return menu;
}


#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "NodeMonitorAlerts"


void
StyledEditWindow::_ShowNodeChangeAlert(const char* name, bool removed)
{
	if (!fNagOnNodeChange)
		return;

	BString alertText(removed ? B_TRANSLATE("File \"%file%\" was removed by "
		"another application, recover it?")
		: B_TRANSLATE("File \"%file%\" was modified by "
		"another application, reload it?"));
	alertText.ReplaceAll("%file%", name);

	if (_ShowAlert(alertText, removed ? B_TRANSLATE("Recover")
			: B_TRANSLATE("Reload"), B_TRANSLATE("Ignore"), "",
			B_WARNING_ALERT) == 0)
	{
		if (!removed) {
			// supress the warning - user has already agreed
			fClean = true;
			BMessage msg(MENU_RELOAD);
			_ReloadDocument(&msg);
		} else
			Save();
	} else
		fNagOnNodeChange = false;

	fSaveItem->SetEnabled(!fClean);
}


void
StyledEditWindow::_HandleNodeMonitorEvent(BMessage *message)
{
	int32 opcode = 0;
	if (message->FindInt32("opcode", &opcode) != B_OK)
		return;

	if (opcode != B_ENTRY_CREATED
		&& message->FindInt64("node") != fNodeRef.node)
		// bypass foreign nodes' event
		return;

	switch (opcode) {
		case B_STAT_CHANGED:
			{
				int32 fields = 0;
				if (message->FindInt32("fields", &fields) == B_OK
					&& (fields & (B_STAT_SIZE | B_STAT_MODIFICATION_TIME
							| B_STAT_MODE)) == 0)
					break;

				const char* name = NULL;
				if (fSaveMessage->FindString("name", &name) != B_OK)
					break;

				_ShowNodeChangeAlert(name, false);
			}
			break;

		case B_ENTRY_MOVED:
			{
				int32 device = 0;
				int64 srcFolder = 0;
				int64 dstFolder = 0;
				const char* name = NULL;
				if (message->FindInt32("device", &device) != B_OK
					|| message->FindInt64("to directory", &dstFolder) != B_OK
					|| message->FindInt64("from directory", &srcFolder) != B_OK
					|| message->FindString("name", &name) != B_OK)
						break;

				entry_ref newRef(device, dstFolder, name);
				BEntry entry(&newRef);

				BEntry dirEntry;
				entry.GetParent(&dirEntry);

				entry_ref ref;
				dirEntry.GetRef(&ref);
				fSaveMessage->ReplaceRef("directory", &ref);
				fSaveMessage->ReplaceString("name", name);

				// store previous name - it may be useful in case
				// we have just moved to temporary copy of file (vim case)
				const char* sourceName = NULL;
				if (message->FindString("from name", &sourceName) == B_OK) {
					fSaveMessage->RemoveName("org.name");
					fSaveMessage->AddString("org.name", sourceName);
					fSaveMessage->RemoveName("move time");
					fSaveMessage->AddInt64("move time", system_time());
				}

				SetTitle(name);

				if (srcFolder != dstFolder) {
					_SwitchNodeMonitor(false);
					_SwitchNodeMonitor(true);
				}
				PostMessage(UPDATE_STATUS_REF);
			}
			break;

		case B_ENTRY_REMOVED:
			{
				_SwitchNodeMonitor(false);

				fClean = false;

				// some editors like vim save files in following way:
				// 1) move t.txt -> t.txt~
				// 2) re-create t.txt and write data to it
				// 3) remove t.txt~
				// go to catch this case
				int32 device = 0;
				int64 directory = 0;
				BString orgName;
				if (fSaveMessage->FindString("org.name", &orgName) == B_OK
					&& message->FindInt32("device", &device) == B_OK
					&& message->FindInt64("directory", &directory) == B_OK)
				{
					// reuse the source name if it is not too old
					bigtime_t time = fSaveMessage->FindInt64("move time");
					if ((system_time() - time) < 1000000) {
						entry_ref ref(device, directory, orgName);
						BEntry entry(&ref);
						if (entry.InitCheck() == B_OK) {
							_SwitchNodeMonitor(true, &ref);
						}

						fSaveMessage->ReplaceString("name", orgName);
						fSaveMessage->RemoveName("org.name");
						fSaveMessage->RemoveName("move time");

						SetTitle(orgName);
						_ShowNodeChangeAlert(orgName, false);
						break;
					}
				}

				const char* name = NULL;
				if (message->FindString("name", &name) != B_OK
					&& fSaveMessage->FindString("name", &name) != B_OK)
					name = "Unknown";

				_ShowNodeChangeAlert(name, true);
				PostMessage(UPDATE_STATUS_REF);
			}
			break;

		default:
			break;
	}
}


void
StyledEditWindow::_SwitchNodeMonitor(bool on, entry_ref* ref)
{
	if (!on) {
		watch_node(&fNodeRef, B_STOP_WATCHING, this);
		watch_node(&fFolderNodeRef, B_STOP_WATCHING, this);
		fNodeRef = node_ref();
		fFolderNodeRef = node_ref();
		return;
	}

	BEntry entry, folderEntry;

	if (ref != NULL) {
		entry.SetTo(ref, true);
		entry.GetParent(&folderEntry);

	} else if (fSaveMessage != NULL) {
		entry_ref ref;
		const char* name = NULL;
		if (fSaveMessage->FindRef("directory", &ref) != B_OK
			|| fSaveMessage->FindString("name", &name) != B_OK)
			return;

		BDirectory dir(&ref);
		entry.SetTo(&dir, name);
		folderEntry.SetTo(&ref);

	} else
		return;

	if (entry.InitCheck() != B_OK || folderEntry.InitCheck() != B_OK)
		return;

	entry.GetNodeRef(&fNodeRef);
	folderEntry.GetNodeRef(&fFolderNodeRef);

	watch_node(&fNodeRef, B_WATCH_STAT, this);
	watch_node(&fFolderNodeRef, B_WATCH_DIRECTORY, this);
}