root/src/preferences/filetypes/AttributeWindow.cpp
/*
 * Copyright 2006-2010, Axel Dörfler, axeld@pinc-software.de.
 * Distributed under the terms of the MIT License.
 */


#include "AttributeWindow.h"
#include "FileTypes.h"
#include "FileTypesWindow.h"

#include <Box.h>
#include <Button.h>
#include <Catalog.h>
#include <CheckBox.h>
#include <ControlLook.h>
#include <LayoutBuilder.h>
#include <Locale.h>
#include <MenuField.h>
#include <MenuItem.h>
#include <Mime.h>
#include <PopUpMenu.h>
#include <SpaceLayoutItem.h>
#include <String.h>
#include <TextControl.h>

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>


#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "Attribute Window"


const uint32 kMsgAttributeUpdated = 'atup';
const uint32 kMsgTypeChosen = 'typc';
const uint32 kMsgDisplayAsChosen = 'dach';
const uint32 kMsgVisibilityChanged = 'vsch';
const uint32 kMsgAlignmentChosen = 'alnc';
const uint32 kMsgAccept = 'acpt';


static bool
compare_display_as(const char* a, const char* b)
{
        bool emptyA = a == NULL || !a[0];
        bool emptyB = b == NULL || !b[0];

        if (emptyA && emptyB)
                return true;
        if (emptyA || emptyB)
                return false;

        const char* end = strchr(a, ':');
        int32 lengthA = end ? end - a : strlen(a);
        end = strchr(b, ':');
        int32 lengthB = end ? end - b : strlen(b);

        if (lengthA != lengthB)
                return false;

        return !strncasecmp(a, b, lengthA);
}


static const char*
display_as_parameter(const char* special)
{
        const char* parameter = strchr(special, ':');
        if (parameter != NULL)
                return parameter + 1;

        return NULL;
}


//      #pragma mark -


AttributeWindow::AttributeWindow(FileTypesWindow* target, BMimeType& mimeType,
                AttributeItem* attributeItem)
        :
        BWindow(BRect(100, 100, 350, 200), B_TRANSLATE("Attribute"),
                B_MODAL_WINDOW_LOOK, B_MODAL_SUBSET_WINDOW_FEEL,
                B_NOT_ZOOMABLE | B_AUTO_UPDATE_SIZE_LIMITS | B_ASYNCHRONOUS_CONTROLS),
        fTarget(target),
        fMimeType(mimeType.Type())
{
        float padding = be_control_look->DefaultItemSpacing();

        if (attributeItem != NULL)
                fAttribute = *attributeItem;

        fPublicNameControl = new BTextControl(B_TRANSLATE("Attribute name:"),
                fAttribute.PublicName(), NULL);
        fPublicNameControl->SetModificationMessage(
                new BMessage(kMsgAttributeUpdated));
        fPublicNameControl->SetAlignment(B_ALIGN_RIGHT, B_ALIGN_LEFT);

        fAttributeControl = new BTextControl(B_TRANSLATE("Internal name:"),
                fAttribute.Name(), NULL);
        fAttributeControl->SetModificationMessage(
                new BMessage(kMsgAttributeUpdated));
        fAttributeControl->SetAlignment(B_ALIGN_RIGHT, B_ALIGN_LEFT);

        // filter out invalid characters that can't be part of an attribute
        BTextView* textView = fAttributeControl->TextView();
        const char* disallowedCharacters = "/";
        for (int32 i = 0; disallowedCharacters[i]; i++) {
                textView->DisallowChar(disallowedCharacters[i]);
        }

        fTypeMenu = new BPopUpMenu("type");
        BMenuItem* item = NULL;
        for (int32 i = 0; kTypeMap[i].name != NULL; i++) {
                BMessage* message = new BMessage(kMsgTypeChosen);
                message->AddInt32("type", kTypeMap[i].type);

                item = new BMenuItem(kTypeMap[i].name, message);
                fTypeMenu->AddItem(item);

                if (kTypeMap[i].type == fAttribute.Type())
                        item->SetMarked(true);
        }

        BMenuField* typeMenuField = new BMenuField("types" , B_TRANSLATE("Type:"),
                fTypeMenu);
        typeMenuField->SetAlignment(B_ALIGN_RIGHT);
        // we must set the color manually when adding a menuField directly
        // into a window.
        typeMenuField->SetViewUIColor(B_PANEL_BACKGROUND_COLOR);
        typeMenuField->SetLowColor(ui_color(B_PANEL_BACKGROUND_COLOR));

        fVisibleCheckBox = new BCheckBox("visible", B_TRANSLATE("Visible"),
                new BMessage(kMsgVisibilityChanged));
        fVisibleCheckBox->SetValue(fAttribute.Visible());

        BMenu* menu = new BPopUpMenu("display as");
        for (int32 i = 0; kDisplayAsMap[i].name != NULL; i++) {
                BMessage* message = new BMessage(kMsgDisplayAsChosen);
                if (kDisplayAsMap[i].identifier != NULL) {
                        message->AddString("identifier", kDisplayAsMap[i].identifier);
                        for (int32 j = 0; kDisplayAsMap[i].supported[j]; j++) {
                                message->AddInt32("supports", kDisplayAsMap[i].supported[j]);
                        }
                }

                item = new BMenuItem(kDisplayAsMap[i].name, message);
                menu->AddItem(item);

                if (compare_display_as(kDisplayAsMap[i].identifier,
                                fAttribute.DisplayAs()))
                        item->SetMarked(true);
        }

        fDisplayAsMenuField = new BMenuField("display as",
                B_TRANSLATE_COMMENT("Display as:",
                        "Tracker offers different display modes for attributes."), menu);
        fDisplayAsMenuField->SetAlignment(B_ALIGN_RIGHT);

        fEditableCheckBox = new BCheckBox("editable",
                B_TRANSLATE_COMMENT("Editable",
                        "If Tracker allows to edit this attribute."),
                new BMessage(kMsgAttributeUpdated));
        fEditableCheckBox->SetValue(fAttribute.Editable());

        fSpecialControl = new BTextControl(B_TRANSLATE("Special:"),
                display_as_parameter(fAttribute.DisplayAs()), NULL);
        fSpecialControl->SetModificationMessage(
                new BMessage(kMsgAttributeUpdated));
        fSpecialControl->SetAlignment(B_ALIGN_RIGHT, B_ALIGN_LEFT);
        fSpecialControl->SetEnabled(false);

        char text[64];
        snprintf(text, sizeof(text), "%" B_PRId32, fAttribute.Width());
        fWidthControl = new BTextControl(B_TRANSLATE_COMMENT("Width:",
                "Default column width in Tracker for this attribute."),
                text, NULL);
        fWidthControl->SetModificationMessage(
                new BMessage(kMsgAttributeUpdated));
        fWidthControl->SetAlignment(B_ALIGN_RIGHT, B_ALIGN_LEFT);

        // filter out invalid characters that can't be part of a width
        textView = fWidthControl->TextView();
        for (int32 i = 0; i < 256; i++) {
                if (!isdigit(i))
                        textView->DisallowChar(i);
        }
        textView->SetMaxBytes(4);

        const struct alignment_map {
                int32           alignment;
                const char*     name;
        } kAlignmentMap[] = {
                {B_ALIGN_LEFT, B_TRANSLATE_COMMENT("Left",
                        "Attribute column alignment in Tracker")},
                {B_ALIGN_RIGHT, B_TRANSLATE_COMMENT("Right",
                        "Attribute column alignment in Tracker")},
                {B_ALIGN_CENTER, B_TRANSLATE_COMMENT("Center",
                        "Attribute column alignment in Tracker")},
                {0, NULL}
        };

        menu = new BPopUpMenu("alignment");
        for (int32 i = 0; kAlignmentMap[i].name != NULL; i++) {
                BMessage* message = new BMessage(kMsgAlignmentChosen);
                message->AddInt32("alignment", kAlignmentMap[i].alignment);

                item = new BMenuItem(kAlignmentMap[i].name, message);
                menu->AddItem(item);

                if (kAlignmentMap[i].alignment == fAttribute.Alignment())
                        item->SetMarked(true);
        }

        fAlignmentMenuField = new BMenuField("alignment",
                B_TRANSLATE("Alignment:"), menu);
        fAlignmentMenuField->SetAlignment(B_ALIGN_RIGHT);

        fAcceptButton = new BButton("add",
                item ? B_TRANSLATE("Done") : B_TRANSLATE("Add"),
                new BMessage(kMsgAccept));
        fAcceptButton->SetEnabled(false);

        BButton* cancelButton = new BButton("cancel", B_TRANSLATE("Cancel"),
                new BMessage(B_QUIT_REQUESTED));

        BBox* visibleBox;
        BLayoutBuilder::Group<>(this, B_VERTICAL, padding)
                .SetInsets(padding, padding, padding, padding)
                .AddGrid(padding, padding / 2)
                        .Add(fPublicNameControl->CreateLabelLayoutItem(), 0, 0)
                        .Add(fPublicNameControl->CreateTextViewLayoutItem(), 1, 0)
                        .Add(fAttributeControl->CreateLabelLayoutItem(), 0, 1)
                        .Add(fAttributeControl->CreateTextViewLayoutItem(), 1, 1)
                        .Add(typeMenuField->CreateLabelLayoutItem(), 0, 2)
                        .Add(typeMenuField->CreateMenuBarLayoutItem(), 1, 2)
                        .End()
                .Add(visibleBox = new BBox(B_FANCY_BORDER,
                        BLayoutBuilder::Grid<>(padding, padding / 2)
                                .Add(fDisplayAsMenuField->CreateLabelLayoutItem(), 0, 0)
                                .Add(fDisplayAsMenuField->CreateMenuBarLayoutItem(), 1, 0)
                                .Add(fEditableCheckBox, 2, 0)
                                .Add(fSpecialControl->CreateLabelLayoutItem(), 0, 1)
                                .Add(fSpecialControl->CreateTextViewLayoutItem(), 1, 1, 2)
                                .Add(fWidthControl->CreateLabelLayoutItem(), 0, 2)
                                .Add(fWidthControl->CreateTextViewLayoutItem(), 1, 2, 2)
                                .Add(fAlignmentMenuField->CreateLabelLayoutItem(), 0, 3)
                                .Add(fAlignmentMenuField->CreateMenuBarLayoutItem(), 1, 3, 2)
                                .SetInsets(padding, padding, padding, padding)
                                .View())
                        )
                .AddGroup(B_HORIZONTAL, padding)
                        .AddGlue()
                        .Add(cancelButton)
                        .Add(fAcceptButton);

        visibleBox->SetLabel(fVisibleCheckBox);

        fAcceptButton->MakeDefault(true);
        fPublicNameControl->MakeFocus(true);

        target->PlaceSubWindow(this);
        AddToSubset(target);

        _CheckDisplayAs();
        _CheckAcceptable();
}


AttributeWindow::~AttributeWindow()
{
}


type_code
AttributeWindow::_CurrentType() const
{
        type_code type = B_STRING_TYPE;
        BMenuItem* item = fTypeMenu->FindMarked();
        if (item != NULL && item->Message() != NULL) {
                int32 value;
                if (item->Message()->FindInt32("type", &value) == B_OK)
                        type = value;
        }

        return type;
}


BMenuItem*
AttributeWindow::_DefaultDisplayAs() const
{
        return fDisplayAsMenuField->Menu()->ItemAt(0);
}


void
AttributeWindow::_CheckDisplayAs()
{
        // check display as suported types

        type_code currentType = _CurrentType();

        BMenu* menu = fDisplayAsMenuField->Menu();
        for (int32 i = menu->CountItems(); i-- > 0;) {
                BMenuItem* item = menu->ItemAt(i);
                bool supported = item == _DefaultDisplayAs();
                        // the default type is always supported
                type_code type;
                for (int32 j = 0; item->Message()->FindInt32("supports",
                                j, (int32*)&type) == B_OK; j++) {
                        if (type == currentType) {
                                supported = true;
                                break;
                        }
                }

                item->SetEnabled(supported);
                if (item->IsMarked() && !supported)
                        menu->ItemAt(0)->SetMarked(true);
        }

        fSpecialControl->SetEnabled(!_DefaultDisplayAs()->IsMarked());
}


void
AttributeWindow::_CheckAcceptable()
{
        bool enabled = fAttributeControl->Text() != NULL
                && fAttributeControl->Text()[0] != '\0'
                && fPublicNameControl->Text() != NULL
                && fPublicNameControl->Text()[0] != '\0';

        if (enabled) {
                // check for equality
                AttributeItem* item = _NewItemFromCurrent();
                enabled = fAttribute != *item;
                delete item;
        }

        // Update button

        if (fAcceptButton->IsEnabled() != enabled)
                fAcceptButton->SetEnabled(enabled);
}


AttributeItem*
AttributeWindow::_NewItemFromCurrent()
{
        const char* newAttribute = fAttributeControl->Text();

        type_code type = _CurrentType();

        int32 alignment = B_ALIGN_LEFT;
        BMenuItem* item = fAlignmentMenuField->Menu()->FindMarked();
        if (item != NULL && item->Message() != NULL) {
                int32 value;
                if (item->Message()->FindInt32("alignment", &value) == B_OK)
                        alignment = value;
        }

        int32 width = atoi(fWidthControl->Text());
        if (width < 0)
                width = 0;

        BString displayAs;
        item = fDisplayAsMenuField->Menu()->FindMarked();
        if (item != NULL) {
                const char* identifier;
                if (item->Message()->FindString("identifier", &identifier) == B_OK) {
                        displayAs = identifier;

                        if (fSpecialControl->Text() && fSpecialControl->Text()[0]) {
                                displayAs += ":";
                                displayAs += fSpecialControl->Text();
                        }
                }
        }

        return new AttributeItem(newAttribute,
                fPublicNameControl->Text(), type, displayAs.String(), alignment,
                width, fVisibleCheckBox->Value() == B_CONTROL_ON,
                fEditableCheckBox->Value() == B_CONTROL_ON);
}


void
AttributeWindow::MessageReceived(BMessage* message)
{
        switch (message->what) {
                case kMsgAttributeUpdated:
                case kMsgAlignmentChosen:
                case kMsgTypeChosen:
                        _CheckDisplayAs();
                        _CheckAcceptable();
                        break;

                case kMsgDisplayAsChosen:
                        fSpecialControl->SetEnabled(!_DefaultDisplayAs()->IsMarked());
                        _CheckAcceptable();
                        break;

                case kMsgVisibilityChanged:
                {
                        bool enabled = fVisibleCheckBox->Value() != B_CONTROL_OFF;

                        fDisplayAsMenuField->SetEnabled(enabled);
                        fWidthControl->SetEnabled(enabled);
                        fAlignmentMenuField->SetEnabled(enabled);
                        fEditableCheckBox->SetEnabled(enabled);

                        _CheckDisplayAs();
                        _CheckAcceptable();
                        break;
                }

                case kMsgAccept:
                {
                        BMessage attributes;
                        status_t status = fMimeType.GetAttrInfo(&attributes);
                        if (status == B_OK) {
                                // replace the entry, and remove any equivalent entries
                                BList list;

                                const char* newAttribute = fAttributeControl->Text();

                                const char* attribute;
                                for (int32 i = 0; attributes.FindString("attr:name", i,
                                                &attribute) == B_OK; i++) {
                                        if (!strcmp(fAttribute.Name(), attribute)
                                                || !strcmp(newAttribute, attribute)) {
                                                // remove this item
                                                continue;
                                        }

                                        AttributeItem* item = create_attribute_item(attributes, i);
                                        if (item != NULL)
                                                list.AddItem(item);
                                }

                                list.AddItem(_NewItemFromCurrent());

                                // Copy them to a new message (their memory is still part of the
                                // original BMessage)
                                BMessage newAttributes;
                                for (int32 i = 0; i < list.CountItems(); i++) {
                                        AttributeItem* item = (AttributeItem*)list.ItemAt(i);

                                        newAttributes.AddString("attr:name", item->Name());
                                        newAttributes.AddString("attr:public_name", item->PublicName());
                                        newAttributes.AddInt32("attr:type", (int32)item->Type());
                                        newAttributes.AddString("attr:display_as", item->DisplayAs());
                                        newAttributes.AddInt32("attr:alignment", item->Alignment());
                                        newAttributes.AddInt32("attr:width", item->Width());
                                        newAttributes.AddBool("attr:viewable", item->Visible());
                                        newAttributes.AddBool("attr:editable", item->Editable());

                                        delete item;
                                }

                                status = fMimeType.SetAttrInfo(&newAttributes);
                        }

                        if (status != B_OK) {
                                error_alert(B_TRANSLATE("Could not change attributes"),
                                        status);
                        }

                        PostMessage(B_QUIT_REQUESTED);
                        break;
                }

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


bool
AttributeWindow::QuitRequested()
{
        return true;
}