* Copyright 2022-2025 Haiku, Inc. All rights reserved.
* Distributed under the terms of the MIT License.
*/
#include "ThemeView.h"
#include <stdio.h>
#include <Alert.h>
#include <Catalog.h>
#include <ColorItem.h>
#include <ColorListView.h>
#include <ControlLook.h>
#include <Directory.h>
#include <Entry.h>
#include <File.h>
#include <Font.h>
#include <LayoutBuilder.h>
#include <Locale.h>
#include <Messenger.h>
#include <Path.h>
#include <PopUpMenu.h>
#include <SpaceLayoutItem.h>
#include "ThemeWindow.h"
#include "Colors.h"
#include "TermConst.h"
#include "PrefHandler.h"
#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "Terminal ThemeView"
#define MSG_COLOR_DROPPED 'cldp'
#define DECORATOR_CHANGED 'dcch'
using BPrivate::BColorItem;
using BPrivate::BColorListView;
using BPrivate::BColorPreview;
const char*
ThemeView::kColorTable[] = {
B_TRANSLATE_MARK("Text"),
B_TRANSLATE_MARK("Background"),
B_TRANSLATE_MARK("Cursor"),
B_TRANSLATE_MARK("Text under cursor"),
B_TRANSLATE_MARK("Selected text"),
B_TRANSLATE_MARK("Selected background"),
B_TRANSLATE_MARK("ANSI black color"),
B_TRANSLATE_MARK("ANSI red color"),
B_TRANSLATE_MARK("ANSI green color"),
B_TRANSLATE_MARK("ANSI yellow color"),
B_TRANSLATE_MARK("ANSI blue color"),
B_TRANSLATE_MARK("ANSI magenta color"),
B_TRANSLATE_MARK("ANSI cyan color"),
B_TRANSLATE_MARK("ANSI white color"),
B_TRANSLATE_MARK("ANSI bright black color"),
B_TRANSLATE_MARK("ANSI bright red color"),
B_TRANSLATE_MARK("ANSI bright green color"),
B_TRANSLATE_MARK("ANSI bright yellow color"),
B_TRANSLATE_MARK("ANSI bright blue color"),
B_TRANSLATE_MARK("ANSI bright magenta color"),
B_TRANSLATE_MARK("ANSI bright cyan color"),
B_TRANSLATE_MARK("ANSI bright white color"),
NULL
};
ThemeView::ThemeView(const char* name, const BMessenger& messenger)
:
BGroupView(name, B_VERTICAL, 5),
fTerminalMessenger(messenger)
{
SetViewUIColor(B_PANEL_BACKGROUND_COLOR);
fAttrList = new BColorListView("AttributeList");
fScrollView = new BScrollView("ScrollView", fAttrList, 0, false, true);
fScrollView->SetViewUIColor(B_PANEL_BACKGROUND_COLOR);
PrefHandler* prefHandler = PrefHandler::Default();
for (const char** table = kColorTable; *table != NULL; ++table) {
fAttrList->AddItem(
new BColorItem(B_TRANSLATE_NOCOLLECT(*table), prefHandler->getRGB(*table)));
}
fColorSchemeMenu = new BPopUpMenu("");
fColorSchemeField = new BMenuField(B_TRANSLATE("Color scheme:"), fColorSchemeMenu);
fColorPreview = new BColorPreview("color preview", "", new BMessage(MSG_COLOR_DROPPED));
fPicker = new BColorControl(B_ORIGIN, B_CELLS_32x8, 8.0,
"picker", new BMessage(MSG_UPDATE_COLOR));
fPreview = new BTextView("preview");
BLayoutBuilder::Group<>(this)
.AddGrid()
.Add(fColorSchemeField->CreateLabelLayoutItem(), 0, 5)
.Add(fColorSchemeField->CreateMenuBarLayoutItem(), 1, 5)
.End()
.Add(fScrollView, 10.0)
.Add(fPreview)
.AddGroup(B_HORIZONTAL, B_USE_DEFAULT_SPACING)
.Add(fColorPreview)
.AddGlue()
.Add(fPicker);
fColorPreview->Parent()->SetExplicitMaxSize(BSize(B_SIZE_UNSET, fPicker->Bounds().Height()));
fAttrList->SetSelectionMessage(new BMessage(MSG_COLOR_ATTRIBUTE_CHOSEN));
float minHeight = be_control_look->DefaultItemSpacing() * 20;
fScrollView->SetExplicitMinSize(BSize(B_SIZE_UNSET, minHeight));
fColorSchemeField->SetAlignment(B_ALIGN_RIGHT);
_MakeColorSchemeMenu();
_UpdateStyle();
InvalidateLayout(true);
}
ThemeView::~ThemeView()
{
}
void
ThemeView::_UpdateStyle()
{
PrefHandler* prefHandler = PrefHandler::Default();
const char* colours[] = {
B_TRANSLATE("black"),
B_TRANSLATE("red"),
B_TRANSLATE("green"),
B_TRANSLATE("yellow"),
B_TRANSLATE("blue"),
B_TRANSLATE("magenta"),
B_TRANSLATE("cyan"),
B_TRANSLATE("white"),
};
text_run_array *array = (text_run_array*)malloc(sizeof(text_run_array)
+ sizeof(text_run) * 16);
int offset = 1;
int index = 0;
array->count = 16;
array->runs[0].offset = offset;
array->runs[0].font = *be_fixed_font;
array->runs[0].color = prefHandler->getRGB(PREF_ANSI_BLACK_COLOR);
offset += strlen(colours[index++]) + 1;
array->runs[1].offset = offset;
array->runs[1].font = *be_fixed_font;
array->runs[1].color = prefHandler->getRGB(PREF_ANSI_RED_COLOR);
offset += strlen(colours[index++]) + 1;
array->runs[2].offset = offset;
array->runs[2].font = *be_fixed_font;
array->runs[2].color = prefHandler->getRGB(PREF_ANSI_GREEN_COLOR);
offset += strlen(colours[index++]) + 1;
array->runs[3].offset = offset;
array->runs[3].font = *be_fixed_font;
array->runs[3].color = prefHandler->getRGB(PREF_ANSI_YELLOW_COLOR);
offset += strlen(colours[index++]) + 1;
array->runs[4].offset = offset;
array->runs[4].font = *be_fixed_font;
array->runs[4].color = prefHandler->getRGB(PREF_ANSI_BLUE_COLOR);
offset += strlen(colours[index++]) + 1;
array->runs[5].offset = offset;
array->runs[5].font = *be_fixed_font;
array->runs[5].color = prefHandler->getRGB(PREF_ANSI_MAGENTA_COLOR);
offset += strlen(colours[index++]) + 1;
array->runs[6].offset = offset;
array->runs[6].font = *be_fixed_font;
array->runs[6].color = prefHandler->getRGB(PREF_ANSI_CYAN_COLOR);
offset += strlen(colours[index++]) + 1;
array->runs[7].offset = offset;
array->runs[7].font = *be_fixed_font;
array->runs[7].color = prefHandler->getRGB(PREF_ANSI_WHITE_COLOR);
offset += strlen(colours[index++]) + 1;
index = 0;
offset++;
array->runs[8].offset = offset;
array->runs[8].font = *be_fixed_font;
array->runs[8].color = prefHandler->getRGB(PREF_ANSI_BLACK_HCOLOR);
offset += strlen(colours[index++]) + 1;
array->runs[9].offset = offset;
array->runs[9].font = *be_fixed_font;
array->runs[9].color = prefHandler->getRGB(PREF_ANSI_RED_HCOLOR);
offset += strlen(colours[index++]) + 1;
array->runs[10].offset = offset;
array->runs[10].font = *be_fixed_font;
array->runs[10].color = prefHandler->getRGB(PREF_ANSI_GREEN_HCOLOR);
offset += strlen(colours[index++]) + 1;
array->runs[11].offset = offset;
array->runs[11].font = *be_fixed_font;
array->runs[11].color = prefHandler->getRGB(PREF_ANSI_YELLOW_HCOLOR);
offset += strlen(colours[index++]) + 1;
array->runs[12].offset = offset;
array->runs[12].font = *be_fixed_font;
array->runs[12].color = prefHandler->getRGB(PREF_ANSI_BLUE_HCOLOR);
offset += strlen(colours[index++]) + 1;
array->runs[13].offset = offset;
array->runs[13].font = *be_fixed_font;
array->runs[13].color = prefHandler->getRGB(PREF_ANSI_MAGENTA_HCOLOR);
offset += strlen(colours[index++]) + 1;
array->runs[14].offset = offset;
array->runs[14].font = *be_fixed_font;
array->runs[14].color = prefHandler->getRGB(PREF_ANSI_CYAN_HCOLOR);
offset += strlen(colours[index++]) + 1;
array->runs[15].offset = offset;
array->runs[15].font = *be_fixed_font;
array->runs[15].color = prefHandler->getRGB(PREF_ANSI_WHITE_HCOLOR);
fPreview->SetStylable(true);
fPreview->MakeEditable(false);
fPreview->MakeSelectable(false);
fPreview->SetViewColor(prefHandler->getRGB(PREF_TEXT_BACK_COLOR));
BString previewText;
previewText << '\n';
for (int ix = 0; ix < 8; ++ix) {
previewText << ' ' << colours[ix];
}
previewText << '\n';
for (int ix = 0; ix < 8; ++ix) {
previewText << ' ' << colours[ix];
}
previewText << '\n';
fPreview->SetAlignment(B_ALIGN_CENTER);
fPreview->SetText(previewText.String(), array);
font_height height;
be_fixed_font->GetHeight(&height);
fPreview->SetExplicitMinSize(BSize(B_SIZE_UNSET, (height.ascent + height.descent) * 5));
}
void
ThemeView::AttachedToWindow()
{
fPicker->SetTarget(this);
fAttrList->SetTarget(this);
fColorPreview->SetTarget(this);
fColorSchemeField->Menu()->SetTargetForItems(this);
fAttrList->Select(0);
SetViewUIColor(B_PANEL_BACKGROUND_COLOR);
_SetCurrentColorScheme();
}
void
ThemeView::WindowActivated(bool active)
{
if (!active)
return;
UpdateMenu();
}
void
ThemeView::UpdateMenu()
{
PrefHandler::Default()->LoadThemes();
_MakeColorSchemeMenu();
_SetCurrentColorScheme();
fColorSchemeField->Menu()->SetTargetForItems(this);
}
void
ThemeView::MessageReceived(BMessage* message)
{
bool modified = false;
if (message->WasDropped()) {
char* name;
type_code type;
rgb_color* color;
ssize_t size;
if (message->GetInfo(B_RGB_COLOR_TYPE, 0, &name, &type) == B_OK
&& message->FindData(name, type, (const void**)&color, &size) == B_OK) {
BPoint dropLoc = message->DropPoint();
if (fAttrList->Bounds().Contains(fAttrList->ConvertFromScreen(dropLoc))) {
int32 index = fAttrList->IndexOf(fAttrList->ConvertFromScreen(dropLoc));
bool selected = index == fAttrList->CurrentSelection();
if (index < 0 || index >= fAttrList->CountItems() || selected)
_SetCurrentColor(*color);
else
_SetColor(index, *color);
modified = true;
} else if (fColorPreview->Bounds().Contains(fColorPreview->ConvertFromScreen(dropLoc))
|| fPicker->Bounds().Contains(fPicker->ConvertFromScreen(dropLoc))) {
_SetCurrentColor(*color);
modified = true;
}
}
}
switch (message->what) {
case MSG_COLOR_SCHEME_CHANGED:
{
color_scheme* newScheme = NULL;
if (message->FindPointer("color_scheme", (void**)&newScheme) == B_OK) {
_ChangeColorScheme(newScheme);
modified = true;
}
break;
}
case MSG_UPDATE_COLOR:
{
rgb_color color = fPicker->ValueAsColor();
_SetCurrentColor(color);
modified = true;
break;
}
case MSG_COLOR_ATTRIBUTE_CHOSEN:
{
const int32 index = fAttrList->CurrentSelection();
if (index < 0 || index >= fAttrList->CountItems())
break;
rgb_color color = PrefHandler::Default()->getRGB(kColorTable[index]);
_SetCurrentColor(color);
break;
}
case MSG_THEME_MODIFIED:
_SetCurrentColorScheme();
BView::MessageReceived(message);
return;
default:
BView::MessageReceived(message);
break;
}
if (modified) {
_UpdateStyle();
fTerminalMessenger.SendMessage(message);
BMessenger messenger(this);
messenger.SendMessage(MSG_THEME_MODIFIED);
}
}
void
ThemeView::SetDefaults()
{
PrefHandler* prefHandler = PrefHandler::Default();
int32 count = fAttrList->CountItems();
for (int32 index = 0; index < count; ++index) {
BColorItem* item = static_cast<BColorItem*>(fAttrList->ItemAt(index));
item->SetColor(prefHandler->getRGB(kColorTable[index]));
fAttrList->InvalidateItem(index);
}
int32 index = fAttrList->CurrentSelection();
BColorItem* item = static_cast<BColorItem*>(fAttrList->ItemAt(index));
if (item != NULL) {
rgb_color color = item->Color();
fPicker->SetValue(color);
fColorPreview->SetColor(color);
fColorPreview->Invalidate();
}
_UpdateStyle();
BMessage message(MSG_COLOR_SCHEME_CHANGED);
fTerminalMessenger.SendMessage(&message);
}
void
ThemeView::Revert()
{
_SetCurrentColorScheme();
SetDefaults();
}
void
ThemeView::_ChangeColorScheme(color_scheme* scheme)
{
PrefHandler* pref = PrefHandler::Default();
pref->setRGB(PREF_TEXT_FORE_COLOR, scheme->text_fore_color);
pref->setRGB(PREF_TEXT_BACK_COLOR, scheme->text_back_color);
pref->setRGB(PREF_SELECT_FORE_COLOR, scheme->select_fore_color);
pref->setRGB(PREF_SELECT_BACK_COLOR, scheme->select_back_color);
pref->setRGB(PREF_CURSOR_FORE_COLOR, scheme->cursor_fore_color);
pref->setRGB(PREF_CURSOR_BACK_COLOR, scheme->cursor_back_color);
pref->setRGB(PREF_ANSI_BLACK_COLOR, scheme->ansi_colors.black);
pref->setRGB(PREF_ANSI_RED_COLOR, scheme->ansi_colors.red);
pref->setRGB(PREF_ANSI_GREEN_COLOR, scheme->ansi_colors.green);
pref->setRGB(PREF_ANSI_YELLOW_COLOR, scheme->ansi_colors.yellow);
pref->setRGB(PREF_ANSI_BLUE_COLOR, scheme->ansi_colors.blue);
pref->setRGB(PREF_ANSI_MAGENTA_COLOR, scheme->ansi_colors.magenta);
pref->setRGB(PREF_ANSI_CYAN_COLOR, scheme->ansi_colors.cyan);
pref->setRGB(PREF_ANSI_WHITE_COLOR, scheme->ansi_colors.white);
pref->setRGB(PREF_ANSI_BLACK_HCOLOR, scheme->ansi_colors_h.black);
pref->setRGB(PREF_ANSI_RED_HCOLOR, scheme->ansi_colors_h.red);
pref->setRGB(PREF_ANSI_GREEN_HCOLOR, scheme->ansi_colors_h.green);
pref->setRGB(PREF_ANSI_YELLOW_HCOLOR, scheme->ansi_colors_h.yellow);
pref->setRGB(PREF_ANSI_BLUE_HCOLOR, scheme->ansi_colors_h.blue);
pref->setRGB(PREF_ANSI_MAGENTA_HCOLOR, scheme->ansi_colors_h.magenta);
pref->setRGB(PREF_ANSI_CYAN_HCOLOR, scheme->ansi_colors_h.cyan);
pref->setRGB(PREF_ANSI_WHITE_HCOLOR, scheme->ansi_colors_h.white);
int32 count = fAttrList->CountItems();
for (int32 index = 0; index < count; ++index) {
BColorItem* item = static_cast<BColorItem*>(fAttrList->ItemAt(index));
rgb_color color = pref->getRGB(kColorTable[index]);
item->SetColor(color);
if (item->IsSelected()) {
fPicker->SetValue(color);
fColorPreview->SetColor(color);
fColorPreview->Invalidate();
}
}
fAttrList->Invalidate();
}
void
ThemeView::_SetCurrentColorScheme()
{
PrefHandler* pref = PrefHandler::Default();
pref->LoadColorScheme(&gCustomColorScheme);
const char* currentSchemeName = NULL;
int32 i = 0;
while (i < gColorSchemes->CountItems()) {
const color_scheme *item = gColorSchemes->ItemAt(i);
i++;
if (gCustomColorScheme == *item) {
currentSchemeName = item->name;
break;
}
}
if (currentSchemeName == NULL)
currentSchemeName = "Custom";
for (int32 i = 0; i < fColorSchemeField->Menu()->CountItems(); i++) {
BMenuItem* item = fColorSchemeField->Menu()->ItemAt(i);
if (strcmp(item->Label(), currentSchemeName) == 0) {
item->SetMarked(true);
break;
}
}
}
void
ThemeView::_MakeColorSchemeMenuItem(const color_scheme *item)
{
if (item == NULL)
return;
BMessage* message = new BMessage(MSG_COLOR_SCHEME_CHANGED);
message->AddPointer("color_scheme", (const void*)item);
fColorSchemeMenu->AddItem(new BMenuItem(item->name, message));
}
void
ThemeView::_MakeColorSchemeMenu()
{
while (fColorSchemeMenu->CountItems() > 0)
delete(fColorSchemeMenu->RemoveItem((int32)0));
FindColorSchemeByName comparator("Default");
const color_scheme* defaultItem = gColorSchemes->FindIf(comparator);
_MakeColorSchemeMenuItem(defaultItem);
int32 index = 0;
while (index < gColorSchemes->CountItems()) {
const color_scheme *item = gColorSchemes->ItemAt(index);
index++;
if (strcmp(item->name, "") == 0)
fColorSchemeMenu->AddSeparatorItem();
else if (item == defaultItem)
continue;
else
_MakeColorSchemeMenuItem(item);
}
fColorSchemeMenu->AddSeparatorItem();
_MakeColorSchemeMenuItem(&gCustomColorScheme);
}
void
ThemeView::_SetCurrentColor(rgb_color color)
{
_SetColor(fAttrList->CurrentSelection(), color);
fPicker->SetValue(color);
fColorPreview->SetColor(color);
}
void
ThemeView::_SetColor(int32 index, rgb_color color)
{
BColorItem* item = dynamic_cast<BColorItem*>(fAttrList->ItemAt(index));
if (item != NULL) {
item->SetColor(color);
fAttrList->InvalidateItem(index);
PrefHandler::Default()->setRGB(kColorTable[index], color);
}
}