⛏️ index : haiku.git

/*
 * Copyright 2012-2019, Adrien Destugues, pulkomandy@pulkomandy.tk
 * Distributed under the terms of the MIT licence.
 */


#include "SerialWindow.h"

#include <stdio.h>

#include <Catalog.h>
#include <ControlLook.h>
#include <FilePanel.h>
#include <GroupLayout.h>
#include <Menu.h>
#include <MenuBar.h>
#include <MenuItem.h>
#include <ScrollView.h>
#include <SerialPort.h>
#include <StatusBar.h>

#include "SerialApp.h"
#include "TermView.h"


#define B_TRANSLATION_CONTEXT "SerialWindow"


const int SerialWindow::kBaudrates[] = { 50, 75, 110, 134, 150, 200, 300, 600,
	1200, 1800, 2400, 4800, 9600, 19200, 31250, 38400, 57600, 115200, 230400
};


// The values for these constants are not in the expected order, so we have to
// rely on this lookup table if we want to keep the menu items sorted.
const int SerialWindow::kBaudrateConstants[] = { B_50_BPS, B_75_BPS, B_110_BPS,
	B_134_BPS, B_150_BPS, B_200_BPS, B_300_BPS, B_600_BPS, B_1200_BPS,
	B_1800_BPS, B_2400_BPS, B_4800_BPS, B_9600_BPS, B_19200_BPS, B_31250_BPS,
	B_38400_BPS, B_57600_BPS, B_115200_BPS, B_230400_BPS
};


const char* SerialWindow::kWindowTitle =
	B_TRANSLATE_MARK_SYSTEM_NAME("SerialConnect");


SerialWindow::SerialWindow()
	: BWindow(BRect(100, 100, 400, 400),
		B_TRANSLATE_NOCOLLECT_SYSTEM_NAME(SerialWindow::kWindowTitle),
		B_DOCUMENT_WINDOW, B_QUIT_ON_WINDOW_CLOSE | B_AUTO_UPDATE_SIZE_LIMITS)
	, fLogFilePanel(NULL)
	, fSendFilePanel(NULL)
{
	BMenuBar* menuBar = new BMenuBar(Bounds(), "menuBar");
	menuBar->ResizeToPreferred();

	BRect r = Bounds();
	r.top = menuBar->Bounds().bottom + 1;
	r.right -= be_control_look->GetScrollBarWidth(B_VERTICAL);
	fTermView = new TermView(r);
	fTermView->ResizeToPreferred();

	r = fTermView->Frame();
	r.left = r.right + 1;
	r.right = r.left + be_control_look->GetScrollBarWidth(B_VERTICAL);
	r.top -= 1;
	r.bottom -= be_control_look->GetScrollBarWidth(B_HORIZONTAL) - 1;

	BScrollBar* scrollBar = new BScrollBar(r, "scrollbar", NULL, 0, 0,
		B_VERTICAL);

	scrollBar->SetTarget(fTermView);

	ResizeTo(r.right - 1, r.bottom + be_control_look->GetScrollBarWidth(B_HORIZONTAL) - 1);

	r = fTermView->Frame();
	r.top = r.bottom - 37;

	fStatusBar = new BStatusBar(r, B_TRANSLATE("file transfer progress"),
		NULL, NULL);
	fStatusBar->SetResizingMode(B_FOLLOW_BOTTOM | B_FOLLOW_LEFT_RIGHT);
	fStatusBar->SetViewUIColor(B_PANEL_BACKGROUND_COLOR);
	fStatusBar->Hide();

	AddChild(menuBar);
	AddChild(fTermView);
	AddChild(scrollBar);
	AddChild(fStatusBar);

	fConnectionMenu = new BMenu(B_TRANSLATE("Connection"));
	fFileMenu = new BMenu(B_TRANSLATE("File"));
	BMenu* settingsMenu = new BMenu(B_TRANSLATE("Settings"));
	BMenu* editMenu = new BMenu(B_TRANSLATE("Edit"));

	fConnectionMenu->SetRadioMode(true);

	menuBar->AddItem(fConnectionMenu);
	menuBar->AddItem(editMenu);
	menuBar->AddItem(fFileMenu);
	menuBar->AddItem(settingsMenu);

	BMenuItem* logFile = new BMenuItem(
		B_TRANSLATE("Log to file" B_UTF8_ELLIPSIS), new BMessage(kMsgLogfile));
	fFileMenu->AddItem(logFile);

	// The "send" items are disabled initially. They are enabled only once we
	// are connected to a serial port.
	BMessage* sendMsg = new BMessage(kMsgSendFile);
	sendMsg->AddString("protocol", "xmodem");
	BMenuItem* xmodemSend = new BMenuItem(
		B_TRANSLATE("XModem send" B_UTF8_ELLIPSIS),
		sendMsg);
	fFileMenu->AddItem(xmodemSend);
	xmodemSend->SetEnabled(false);

	BMenuItem* rawSend = new BMenuItem(B_TRANSLATE("Raw send" B_UTF8_ELLIPSIS),
		new BMessage(kMsgSendFile));
	fFileMenu->AddItem(rawSend);
	rawSend->SetEnabled(false);

#if 0
	// TODO implement this
	BMenuItem* xmodemReceive = new BMenuItem(
		"X/Y/Zmodem receive" B_UTF8_ELLIPSIS, NULL);
	fFileMenu->AddItem(xmodemReceive);
	xmodemReceive->SetEnabled(false);
#endif

	// Items for the edit menu
	BMenuItem* clearScreen = new BMenuItem(B_TRANSLATE("Clear history"),
		new BMessage(kMsgClear), 'L');
	editMenu->AddItem(clearScreen);

	BMenuItem* paste = new BMenuItem(B_TRANSLATE("Paste"), new BMessage(B_PASTE), 'V');
	editMenu->AddItem(paste);

	// TODO copy (when we have selection), paste

	// Configuring all this by menus may be a bit unhandy. Make a setting
	// window instead ?
	fBaudrateMenu = new BMenu(B_TRANSLATE("Baud rate"));
	fBaudrateMenu->SetRadioMode(true);
	settingsMenu->AddItem(fBaudrateMenu);

	fParityMenu = new BMenu(B_TRANSLATE("Parity"));
	fParityMenu->SetRadioMode(true);
	settingsMenu->AddItem(fParityMenu);

	fStopbitsMenu = new BMenu(B_TRANSLATE("Stop bits"));
	fStopbitsMenu->SetRadioMode(true);
	settingsMenu->AddItem(fStopbitsMenu);

	fFlowcontrolMenu = new BMenu(B_TRANSLATE("Flow control"));
	fFlowcontrolMenu->SetRadioMode(true);
	settingsMenu->AddItem(fFlowcontrolMenu);

	fDatabitsMenu = new BMenu(B_TRANSLATE("Data bits"));
	fDatabitsMenu->SetRadioMode(true);
	settingsMenu->AddItem(fDatabitsMenu);

	fLineTerminatorMenu = new BMenu(B_TRANSLATE("Line terminator"));
	fLineTerminatorMenu->SetRadioMode(true);
	settingsMenu->AddItem(fLineTerminatorMenu);

	BMessage* message = new BMessage(kMsgSettings);
	message->AddInt32("parity", B_NO_PARITY);
	BMenuItem* parityNone =
		new BMenuItem(B_TRANSLATE_COMMENT("None", "Parity"), message);

	message = new BMessage(kMsgSettings);
	message->AddInt32("parity", B_ODD_PARITY);
	BMenuItem* parityOdd = new BMenuItem(B_TRANSLATE_COMMENT("Odd", "Parity"),
		message);

	message = new BMessage(kMsgSettings);
	message->AddInt32("parity", B_EVEN_PARITY);
	BMenuItem* parityEven =
		new BMenuItem(B_TRANSLATE_COMMENT("Even", "Parity"), message);

	fParityMenu->AddItem(parityNone);
	fParityMenu->AddItem(parityOdd);
	fParityMenu->AddItem(parityEven);
	fParityMenu->SetTargetForItems(be_app);

	message = new BMessage(kMsgSettings);
	message->AddInt32("databits", B_DATA_BITS_7);
	BMenuItem* data7 = new BMenuItem("7", message);

	message = new BMessage(kMsgSettings);
	message->AddInt32("databits", B_DATA_BITS_8);
	BMenuItem* data8 = new BMenuItem("8", message);

	fDatabitsMenu->AddItem(data7);
	fDatabitsMenu->AddItem(data8);
	fDatabitsMenu->SetTargetForItems(be_app);

	message = new BMessage(kMsgSettings);
	message->AddInt32("stopbits", B_STOP_BITS_1);
	BMenuItem* stop1 = new BMenuItem("1", message);

	message = new BMessage(kMsgSettings);
	message->AddInt32("stopbits", B_STOP_BITS_2);
	BMenuItem* stop2 = new BMenuItem("2", message);

	fStopbitsMenu->AddItem(stop1);
	fStopbitsMenu->AddItem(stop2);
	fStopbitsMenu->SetTargetForItems(be_app);

	// Loop backwards to add fastest rates at top of menu
	for (int i = sizeof(kBaudrates) / sizeof(kBaudrates[0]); --i >= 0;)
	{
		message = new BMessage(kMsgSettings);
		message->AddInt32("baudrate", kBaudrateConstants[i]);

		char buffer[7];
		sprintf(buffer, "%d", kBaudrates[i]);
		BMenuItem* item = new BMenuItem(buffer, message);

		fBaudrateMenu->AddItem(item);
	}

	message = new BMessage(kMsgCustomBaudrate);
	BMenuItem* custom =
		new BMenuItem(B_TRANSLATE_COMMENT("custom" B_UTF8_ELLIPSIS,
		"Baudrate"), message);
	fBaudrateMenu->AddItem(custom);

	fBaudrateMenu->SetTargetForItems(be_app);

	message = new BMessage(kMsgSettings);
	message->AddInt32("flowcontrol", B_HARDWARE_CONTROL);
	BMenuItem* hardware =
		new BMenuItem(B_TRANSLATE_COMMENT("Hardware", "Flowcontrol"), message);

	message = new BMessage(kMsgSettings);
	message->AddInt32("flowcontrol", B_SOFTWARE_CONTROL);
	BMenuItem* software =
		new BMenuItem(B_TRANSLATE_COMMENT("Software", "Flowcontrol"), message);

	message = new BMessage(kMsgSettings);
	message->AddInt32("flowcontrol", B_HARDWARE_CONTROL | B_SOFTWARE_CONTROL);
	BMenuItem* both =
		new BMenuItem(B_TRANSLATE_COMMENT("Both", "Flowcontrol"), message);

	message = new BMessage(kMsgSettings);
	message->AddInt32("flowcontrol", 0);
	BMenuItem* noFlow =
		new BMenuItem(B_TRANSLATE_COMMENT("None", "Flowcontrol"), message);

	fFlowcontrolMenu->AddItem(hardware);
	fFlowcontrolMenu->AddItem(software);
	fFlowcontrolMenu->AddItem(both);
	fFlowcontrolMenu->AddItem(noFlow);
	fFlowcontrolMenu->SetTargetForItems(be_app);

	message = new BMessage(kMsgSettings);
	message->AddString("terminator", "\n");
	BMenuItem* lf = new BMenuItem("LF (\\n)", message);

	message = new BMessage(kMsgSettings);
	message->AddString("terminator", "\r");
	BMenuItem* cr = new BMenuItem("CR (\\r)", message);

	message = new BMessage(kMsgSettings);
	message->AddString("terminator", "\r\n");
	BMenuItem* crlf = new BMenuItem("CR/LF (\\r\\n)", message);

	fLineTerminatorMenu->AddItem(lf);
	fLineTerminatorMenu->AddItem(cr);
	fLineTerminatorMenu->AddItem(crlf);

	CenterOnScreen();
}


SerialWindow::~SerialWindow()
{
	delete fLogFilePanel;
	delete fSendFilePanel;
}


void SerialWindow::MenusBeginning()
{
	// remove all items from the menu
	fConnectionMenu->RemoveItems(0, fConnectionMenu->CountItems(), true);

	// fill it with the (updated) serial port list
	BSerialPort serialPort;
	int deviceCount = serialPort.CountDevices();
	bool connected = false;

	for (int i = 0; i < deviceCount; i++)
	{
		char buffer[256];
		serialPort.GetDeviceName(i, buffer, 256);

		BMessage* message = new BMessage(kMsgOpenPort);
		message->AddString("port name", buffer);
		BMenuItem* portItem = new BMenuItem(buffer, message);
		portItem->SetTarget(be_app);

		const BString& connectedPort = ((SerialApp*)be_app)->GetPort();

		if (connectedPort == buffer) {
			connected = true;
			portItem->SetMarked(true);
		}

		fConnectionMenu->AddItem(portItem);
	}

	if (deviceCount > 0) {
		fConnectionMenu->AddSeparatorItem();

		BMenuItem* disconnect = new BMenuItem(B_TRANSLATE("Disconnect"),
			new BMessage(kMsgOpenPort), 'Z', B_OPTION_KEY);
		if (!connected)
			disconnect->SetEnabled(false);
		disconnect->SetTarget(be_app);
		fConnectionMenu->AddItem(disconnect);
	} else {
		BMenuItem* noDevices =
			new BMenuItem(B_TRANSLATE("<no serial port available>"), NULL);
		noDevices->SetEnabled(false);
		fConnectionMenu->AddItem(noDevices);
	}
}


void SerialWindow::MessageReceived(BMessage* message)
{
	switch (message->what)
	{
		case kMsgOpenPort:
		{
			BString path;
			bool open = (message->FindString("port name", &path) == B_OK);
			int i = 1; // Skip "log to file", which woeks even when offline.
			BMenuItem* item;
			while((item = fFileMenu->ItemAt(i++)))
			{
				item->SetEnabled(open);
			}
			return;
		}
		case kMsgDataRead:
		{
			const char* bytes;
			ssize_t length;
			if (message->FindData("data", B_RAW_TYPE, (const void**)&bytes,
					&length) == B_OK)
				fTermView->PushBytes(bytes, length);
			return;
		}
		case kMsgLogfile:
		{
			// Let's lazy init the file panel
			if (fLogFilePanel == NULL) {
				fLogFilePanel = new BFilePanel(B_SAVE_PANEL,
					&be_app_messenger, NULL, B_FILE_NODE, false);
				fLogFilePanel->SetMessage(message);
			}
			fLogFilePanel->Show();
			return;
		}
		case kMsgSendFile:
		{
			// Let's lazy init the file panel
			if (fSendFilePanel == NULL) {
				fSendFilePanel = new BFilePanel(B_OPEN_PANEL,
					&be_app_messenger, NULL, B_FILE_NODE, false);
			}
			fSendFilePanel->SetMessage(message);
			fSendFilePanel->Show();
			return;
		}
		case kMsgSettings:
		{
			int32 baudrate;
			stop_bits stopBits;
			data_bits dataBits;
			parity_mode parity;
			uint32 flowcontrol;
			BString terminator;

			if (message->FindInt32("databits", (int32*)&dataBits) == B_OK) {
				for (int i = 0; i < fDatabitsMenu->CountItems(); i++) {
					BMenuItem* item = fDatabitsMenu->ItemAt(i);
					int32 code;
					item->Message()->FindInt32("databits", &code);

					if (code == dataBits)
						item->SetMarked(true);
				}
			}

			if (message->FindInt32("stopbits", (int32*)&stopBits) == B_OK) {
				for (int i = 0; i < fStopbitsMenu->CountItems(); i++) {
					BMenuItem* item = fStopbitsMenu->ItemAt(i);
					int32 code;
					item->Message()->FindInt32("stopbits", &code);

					if (code == stopBits)
						item->SetMarked(true);
				}
			}

			if (message->FindInt32("parity", (int32*)&parity) == B_OK)
			{
				for (int i = 0; i < fParityMenu->CountItems(); i++) {
					BMenuItem* item = fParityMenu->ItemAt(i);
					int32 code;
					item->Message()->FindInt32("parity", &code);

					if (code == parity)
						item->SetMarked(true);
				}
			}

			if (message->FindInt32("flowcontrol", (int32*)&flowcontrol)
					== B_OK) {
				for (int i = 0; i < fFlowcontrolMenu->CountItems(); i++) {
					BMenuItem* item = fFlowcontrolMenu->ItemAt(i);
					int32 code;
					item->Message()->FindInt32("flowcontrol", &code);

					if (code == (int32)flowcontrol)
						item->SetMarked(true);
				}
			}

			if (message->FindInt32("baudrate", &baudrate) == B_OK) {
				int i;
				BMenuItem* item = NULL;
				for (i = 0; i < fBaudrateMenu->CountItems(); i++) {
					item = fBaudrateMenu->ItemAt(i);
					int32 code = 0;
					item->Message()->FindInt32("baudrate", &code);

					if (baudrate == code) {
						item->SetMarked(true);
						break;
					}
				}

				if (i == fBaudrateMenu->CountItems() && item != NULL) {
					// Rate was not found, mark it as "custom".
					// Since that is the last item in the menu, we still point
					// to it.
					item->SetMarked(true);
					item->Message()->SetInt32("baudrate", baudrate);
				}
			}

			if (message->FindString("terminator", &terminator) == B_OK) {
				fTermView->SetLineTerminator(terminator);
				for (int i = 0; i < fLineTerminatorMenu->CountItems(); i++) {
					BMenuItem* item = fLineTerminatorMenu->ItemAt(i);
					BString code;
					item->Message()->FindString("terminator", &code);

					if (terminator == code)
						item->SetMarked(true);
				}
			}

			return;
		}
		case kMsgClear:
		{
			fTermView->Clear();
			return;
		}
		case B_PASTE:
		{
			fTermView->PasteFromClipboard();
		}
		case kMsgProgress:
		{
			// File transfer progress
			int32 pos = message->FindInt32("pos");
			int32 size = message->FindInt32("size");
			BString label = message->FindString("info");

			if (pos >= size) {
				if (!fStatusBar->IsHidden()) {
					fStatusBar->Hide();
					fTermView->ResizeBy(0, fStatusBar->Bounds().Height() - 1);
				}
			} else {
				BString text;
				text.SetToFormat("%" B_PRId32 "/%" B_PRId32, pos, size);
				fStatusBar->SetMaxValue(size);
				fStatusBar->SetTo(pos, label, text);
				if (fStatusBar->IsHidden()) {
					fStatusBar->Show();
					fTermView->ResizeBy(0, -(fStatusBar->Bounds().Height() - 1));
				}
			}
			return;
		}
		default:
			BWindow::MessageReceived(message);
	}
}