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


#include "AttributeListView.h"
#include "AttributeWindow.h"
#include "DropTargetListView.h"
#include "ExtensionWindow.h"
#include "FileTypes.h"
#include "FileTypesWindow.h"
#include "IconView.h"
#include "MimeTypeListView.h"
#include "NewFileTypeWindow.h"
#include "PreferredAppMenu.h"
#include "StringView.h"

#include <Alignment.h>
#include <AppFileInfo.h>
#include <Application.h>
#include <Bitmap.h>
#include <Box.h>
#include <Button.h>
#include <Catalog.h>
#include <ControlLook.h>
#include <LayoutBuilder.h>
#include <ListView.h>
#include <Locale.h>
#include <MenuBar.h>
#include <MenuField.h>
#include <MenuItem.h>
#include <Mime.h>
#include <NodeInfo.h>
#include <OutlineListView.h>
#include <PopUpMenu.h>
#include <ScrollView.h>
#include <SpaceLayoutItem.h>
#include <SplitView.h>
#include <TextControl.h>

#include <OverrideAlert.h>
#include <be_apps/Tracker/RecentItems.h>

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


#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "FileTypes Window"


const uint32 kMsgTypeSelected = 'typs';
const uint32 kMsgAddType = 'atyp';
const uint32 kMsgRemoveType = 'rtyp';

const uint32 kMsgExtensionSelected = 'exts';
const uint32 kMsgExtensionInvoked = 'exti';
const uint32 kMsgAddExtension = 'aext';
const uint32 kMsgRemoveExtension = 'rext';
const uint32 kMsgRuleEntered = 'rule';

const uint32 kMsgAttributeSelected = 'atrs';
const uint32 kMsgAttributeInvoked = 'atri';
const uint32 kMsgAddAttribute = 'aatr';
const uint32 kMsgRemoveAttribute = 'ratr';
const uint32 kMsgMoveUpAttribute = 'muat';
const uint32 kMsgMoveDownAttribute = 'mdat';

const uint32 kMsgPreferredAppChosen = 'papc';
const uint32 kMsgSelectPreferredApp = 'slpa';
const uint32 kMsgSamePreferredAppAs = 'spaa';

const uint32 kMsgPreferredAppOpened = 'paOp';
const uint32 kMsgSamePreferredAppAsOpened = 'spaO';

const uint32 kMsgTypeEntered = 'type';
const uint32 kMsgDescriptionEntered = 'dsce';

const uint32 kMsgToggleIcons = 'tgic';
const uint32 kMsgToggleRule = 'tgrl';


static const char* kAttributeNames[] = {
        "attr:public_name",
        "attr:name",
        "attr:type",
        "attr:editable",
        "attr:viewable",
        "attr:extra",
        "attr:alignment",
        "attr:width",
        "attr:display_as"
};


class TypeIconView : public IconView {
        typedef IconView _inherited;

        public:
                TypeIconView(const char* name);
                virtual ~TypeIconView();

                virtual void Draw(BRect updateRect);
                virtual void GetPreferredSize(float* _width, float* _height);

        protected:
                virtual BRect BitmapRect() const;
};


class ExtensionListView : public DropTargetListView {
        public:
                ExtensionListView(const char* name,
                        list_view_type type = B_SINGLE_SELECTION_LIST,
                        uint32 flags = B_WILL_DRAW | B_FRAME_EVENTS | B_NAVIGABLE);
                virtual ~ExtensionListView();

                virtual BSize MinSize();

                virtual void MessageReceived(BMessage* message);
                virtual bool AcceptsDrag(const BMessage* message);

                void SetType(BMimeType* type);

        private:
                BMimeType       fType;
                BSize           fMinSize;
};


//      #pragma mark -


TypeIconView::TypeIconView(const char* name)
        : IconView(name)
{
        ShowEmptyFrame(false);
        SetIconSize((icon_size)48);
}


TypeIconView::~TypeIconView()
{
}


void
TypeIconView::Draw(BRect updateRect)
{
        if (!IsEnabled())
                return;

        IconView::Draw(updateRect);

        const char* text = NULL;

        switch (IconSource()) {
                case kNoIcon:
                        text = B_TRANSLATE("no icon");
                        break;
                case kApplicationIcon:
                        text = B_TRANSLATE("(from application)");
                        break;
                case kSupertypeIcon:
                        text = B_TRANSLATE("(from super type)");
                        break;

                default:
                        return;
        }

        SetHighColor(tint_color(ui_color(B_PANEL_BACKGROUND_COLOR),
                B_DISABLED_LABEL_TINT));
        SetLowColor(ViewColor());

        font_height fontHeight;
        GetFontHeight(&fontHeight);

        const BRect bitmapRect = _inherited::BitmapRect();
        float y = fontHeight.ascent;
        if (IconSource() == kNoIcon) {
                // center text in the middle of the icon
                y += (bitmapRect.Height() - fontHeight.ascent - fontHeight.descent) / 2.0f;
        } else
                y += bitmapRect.Height() + 3.0f;

        DrawString(text, BPoint(ceilf((Bounds().Width() - StringWidth(text)) / 2.0f),
                ceilf(y)));
}


void
TypeIconView::GetPreferredSize(float* _width, float* _height)
{
        const BRect bitmapRect = _inherited::BitmapRect();

        if (_width) {
                float a = StringWidth(B_TRANSLATE("(from application)"));
                float b = StringWidth(B_TRANSLATE("(from super type)"));
                float width = max_c(a, b);
                if (width < bitmapRect.Width())
                        width = bitmapRect.Width();

                *_width = ceilf(width);
        }

        if (_height) {
                font_height fontHeight;
                GetFontHeight(&fontHeight);

                *_height = bitmapRect.Height() + 3.0f + ceilf(fontHeight.ascent
                        + fontHeight.descent);
        }
}


BRect
TypeIconView::BitmapRect() const
{
        const BRect bitmapRect = _inherited::BitmapRect();

        if (IconSource() == kNoIcon) {
                // this also defines the drop target area
                font_height fontHeight;
                GetFontHeight(&fontHeight);

                float width = StringWidth(B_TRANSLATE("no icon")) + 8.0f;
                float height = ceilf(fontHeight.ascent + fontHeight.descent) + 6.0f;
                float x = (Bounds().Width() - width) / 2.0f;
                float y = ceilf((bitmapRect.Height() - fontHeight.ascent - fontHeight.descent)
                        / 2.0f) - 3.0f;

                return BRect(x, y, x + width, y + height);
        }

        float x = (Bounds().Width() - bitmapRect.Width()) / 2.0f;
        return BRect(x, 0.0f, x + bitmapRect.Width(), bitmapRect.Height());
}


//      #pragma mark -


ExtensionListView::ExtensionListView(const char* name,
                list_view_type type, uint32 flags)
        :
        DropTargetListView(name, type, flags)
{
}


ExtensionListView::~ExtensionListView()
{
}


BSize
ExtensionListView::MinSize()
{
        if (!fMinSize.IsWidthSet()) {
                BFont font;
                GetFont(&font);
                fMinSize.width = font.StringWidth(".mmmmm");

                font_height height;
                font.GetHeight(&height);
                fMinSize.height = (height.ascent + height.descent + height.leading) * 3;
        }

        return fMinSize;
}


void
ExtensionListView::MessageReceived(BMessage* message)
{
        if (message->WasDropped() && AcceptsDrag(message)) {
                // create extension list
                BList list;
                entry_ref ref;
                for (int32 index = 0; message->FindRef("refs", index, &ref) == B_OK;
                                index++) {
                        const char* point = strchr(ref.name, '.');
                        if (point != NULL && point[1])
                                list.AddItem(strdup(++point));
                }

                merge_extensions(fType, list);

                // delete extension list
                for (int32 index = list.CountItems(); index-- > 0;) {
                        free(list.ItemAt(index));
                }
        } else
                DropTargetListView::MessageReceived(message);
}


bool
ExtensionListView::AcceptsDrag(const BMessage* message)
{
        if (fType.Type() == NULL)
                return false;

        int32 count = 0;
        entry_ref ref;

        for (int32 index = 0; message->FindRef("refs", index, &ref) == B_OK;
                        index++) {
                const char* point = strchr(ref.name, '.');
                if (point != NULL && point[1])
                        count++;
        }

        return count > 0;
}


void
ExtensionListView::SetType(BMimeType* type)
{
        if (type != NULL)
                fType.SetTo(type->Type());
        else
                fType.Unset();
}


//      #pragma mark -


FileTypesWindow::FileTypesWindow(const BMessage& settings)
        :
        BWindow(_Frame(settings), B_TRANSLATE_SYSTEM_NAME("FileTypes"),
                B_TITLED_WINDOW, B_NOT_ZOOMABLE | B_ASYNCHRONOUS_CONTROLS
                | B_AUTO_UPDATE_SIZE_LIMITS),
        fNewTypeWindow(NULL)
{
        bool showIcons;
        bool showRule;
        if (settings.FindBool("show_icons", &showIcons) != B_OK)
                showIcons = true;
        if (settings.FindBool("show_rule", &showRule) != B_OK)
                showRule = false;

        float padding = be_control_look->DefaultItemSpacing();
        BAlignment labelAlignment = be_control_look->DefaultLabelAlignment();
        BAlignment fullAlignment(B_ALIGN_USE_FULL_WIDTH, B_ALIGN_USE_FULL_HEIGHT);

        // add the menu
        BMenuBar* menuBar = new BMenuBar("");

        BMenu* menu = new BMenu(B_TRANSLATE("File"));
        BMenuItem* item = new BMenuItem(
                B_TRANSLATE("New resource file" B_UTF8_ELLIPSIS), NULL, 'N',
                B_COMMAND_KEY);
        item->SetEnabled(false);
        menu->AddItem(item);

        BMenu* recentsMenu = BRecentFilesList::NewFileListMenu(
                B_TRANSLATE("Open" B_UTF8_ELLIPSIS), NULL, NULL,
                be_app, 10, false, NULL, kSignature);
        item = new BMenuItem(recentsMenu, new BMessage(kMsgOpenFilePanel));
        item->SetShortcut('O', B_COMMAND_KEY);
        menu->AddItem(item);

        menu->AddItem(new BMenuItem(
                B_TRANSLATE("Application types" B_UTF8_ELLIPSIS),
                new BMessage(kMsgOpenApplicationTypesWindow)));
        menu->AddSeparatorItem();

        menu->AddItem(new BMenuItem(B_TRANSLATE("Quit"),
                new BMessage(B_QUIT_REQUESTED), 'Q', B_COMMAND_KEY));
        menu->SetTargetForItems(be_app);
        menuBar->AddItem(menu);

        menu = new BMenu(B_TRANSLATE("Settings"));
        item = new BMenuItem(B_TRANSLATE("Show icons in list"),
                new BMessage(kMsgToggleIcons));
        item->SetMarked(showIcons);
        item->SetTarget(this);
        menu->AddItem(item);

        item = new BMenuItem(B_TRANSLATE("Show recognition rule"),
                new BMessage(kMsgToggleRule));
        item->SetMarked(showRule);
        item->SetTarget(this);
        menu->AddItem(item);
        menuBar->AddItem(menu);
        menuBar->SetExplicitAlignment(BAlignment(B_ALIGN_LEFT, B_ALIGN_TOP));

        // MIME Types list
        BButton* addTypeButton = new BButton("add",
                B_TRANSLATE("Add" B_UTF8_ELLIPSIS), new BMessage(kMsgAddType));

        fRemoveTypeButton = new BButton("remove", B_TRANSLATE("Remove"),
                new BMessage(kMsgRemoveType) );

        fTypeListView = new MimeTypeListView("typeview", NULL, showIcons, false);
        fTypeListView->SetSelectionMessage(new BMessage(kMsgTypeSelected));
        fTypeListView->SetExplicitMinSize(BSize(200, B_SIZE_UNSET));

        BScrollView* typeListScrollView = new BScrollView("scrollview",
                fTypeListView, B_FRAME_EVENTS | B_WILL_DRAW, false, true);

        // "Icon" group

        fIconView = new TypeIconView("icon");
        fIconBox = new BBox("Icon BBox");
        fIconBox->SetLabel(B_TRANSLATE("Icon"));
        BLayoutBuilder::Group<>(fIconBox, B_VERTICAL, padding)
                .SetInsets(padding)
                .AddGlue(1)
                .Add(fIconView, 3)
                .AddGlue(1);

        // "File Recognition" group

        fRecognitionBox = new BBox("Recognition Box");
        fRecognitionBox->SetLabel(B_TRANSLATE("File recognition"));
        fRecognitionBox->SetExplicitAlignment(fullAlignment);

        fExtensionLabel = new StringView(B_TRANSLATE("Extensions:"), NULL);
        fExtensionLabel->LabelView()->SetExplicitAlignment(labelAlignment);

        fAddExtensionButton = new BButton("add ext",
                B_TRANSLATE("Add" B_UTF8_ELLIPSIS), new BMessage(kMsgAddExtension));
        fAddExtensionButton->SetExplicitMaxSize(
                BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET));

        fRemoveExtensionButton = new BButton("remove ext", B_TRANSLATE("Remove"),
                new BMessage(kMsgRemoveExtension));

        fExtensionListView = new ExtensionListView("listview ext",
                B_SINGLE_SELECTION_LIST);
        fExtensionListView->SetSelectionMessage(
                new BMessage(kMsgExtensionSelected));
        fExtensionListView->SetInvocationMessage(
                new BMessage(kMsgExtensionInvoked));

        BScrollView* scrollView = new BScrollView("scrollview ext",
                fExtensionListView, B_FRAME_EVENTS | B_WILL_DRAW, false, true);

        fRuleControl = new BTextControl("rule", B_TRANSLATE("Rule:"), "",
                new BMessage(kMsgRuleEntered));
        fRuleControl->SetAlignment(B_ALIGN_RIGHT, B_ALIGN_LEFT);
        fRuleControl->Hide();

        BLayoutBuilder::Grid<>(fRecognitionBox, padding, padding / 2)
                .SetInsets(padding, padding * 2, padding, padding)
                .Add(fExtensionLabel->LabelView(), 0, 0)
                .Add(scrollView, 0, 1, 2, 2)
                .Add(fAddExtensionButton, 2, 1)
                .Add(fRemoveExtensionButton, 2, 2)
                .Add(fRuleControl, 0, 3, 3, 1);

        // "Description" group

        fDescriptionBox = new BBox("description BBox");
        fDescriptionBox->SetLabel(B_TRANSLATE("Description"));
        fDescriptionBox->SetExplicitAlignment(fullAlignment);

        fInternalNameView = new StringView(B_TRANSLATE("Internal name:"), NULL);
        fInternalNameView->SetEnabled(false);
        fTypeNameControl = new BTextControl("type", B_TRANSLATE("Type name:"), "",
                new BMessage(kMsgTypeEntered));
        fDescriptionControl = new BTextControl("description",
                B_TRANSLATE("Description:"), "", new BMessage(kMsgDescriptionEntered));

        BLayoutBuilder::Grid<>(fDescriptionBox, padding / 2, padding / 2)
                .SetInsets(padding, padding * 2, padding, padding)
                .Add(fInternalNameView->LabelView(), 0, 0)
                .Add(fInternalNameView->TextView(), 1, 0)
                .Add(fTypeNameControl->CreateLabelLayoutItem(), 0, 1)
                .Add(fTypeNameControl->CreateTextViewLayoutItem(), 1, 1, 2)
                .Add(fDescriptionControl->CreateLabelLayoutItem(), 0, 2)
                .Add(fDescriptionControl->CreateTextViewLayoutItem(), 1, 2, 2);

        // "Preferred Application" group

        fPreferredBox = new BBox("preferred BBox");
        fPreferredBox->SetLabel(B_TRANSLATE("Preferred application"));

        menu = new BPopUpMenu("preferred");
        menu->AddItem(item = new BMenuItem(B_TRANSLATE("None"),
                new BMessage(kMsgPreferredAppChosen)));
        item->SetMarked(true);
        fPreferredField = new BMenuField("preferred", (char*)NULL, menu);

        fSelectButton = new BButton("select",
                B_TRANSLATE("Select" B_UTF8_ELLIPSIS),
                new BMessage(kMsgSelectPreferredApp));

        fSameAsButton = new BButton("same as",
                B_TRANSLATE("Same as" B_UTF8_ELLIPSIS),
                new BMessage(kMsgSamePreferredAppAs));

        BLayoutBuilder::Group<>(fPreferredBox, B_HORIZONTAL, padding)
                .SetInsets(padding, padding * 2, padding, padding)
                .Add(fPreferredField)
                .Add(fSelectButton)
                .Add(fSameAsButton);

        // "Extra Attributes" group

        fAttributeBox = new BBox("Attribute Box");
        fAttributeBox->SetLabel(B_TRANSLATE("Extra attributes"));

        fAddAttributeButton = new BButton("add attr",
                B_TRANSLATE("Add" B_UTF8_ELLIPSIS), new BMessage(kMsgAddAttribute));
        fAddAttributeButton->SetExplicitMaxSize(
                BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET));

        fRemoveAttributeButton = new BButton("remove attr", B_TRANSLATE("Remove"),
                new BMessage(kMsgRemoveAttribute));
        fRemoveAttributeButton->SetExplicitMaxSize(
                BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET));

        fMoveUpAttributeButton = new BButton("move up attr", B_TRANSLATE("Move up"),
                new BMessage(kMsgMoveUpAttribute));
        fMoveUpAttributeButton->SetExplicitMaxSize(
                BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET));
        fMoveDownAttributeButton = new BButton("move down attr",
                B_TRANSLATE("Move down"), new BMessage(kMsgMoveDownAttribute));
        fMoveDownAttributeButton->SetExplicitMaxSize(
                BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET));

        fAttributeListView = new AttributeListView("listview attr");
        fAttributeListView->SetSelectionMessage(
                new BMessage(kMsgAttributeSelected));
        fAttributeListView->SetInvocationMessage(
                new BMessage(kMsgAttributeInvoked));

        BScrollView* attributesScroller = new BScrollView("scrollview attr",
                fAttributeListView, B_FRAME_EVENTS | B_WILL_DRAW, false, true);

        BLayoutBuilder::Group<>(fAttributeBox, B_HORIZONTAL, padding)
                .SetInsets(padding, padding * 2, padding, padding)
                .Add(attributesScroller, 1.0f)
                .AddGroup(B_VERTICAL, padding / 2, 0.0f)
                        .SetInsets(0)
                        .Add(fAddAttributeButton)
                        .Add(fRemoveAttributeButton)
                        .AddStrut(padding)
                        .Add(fMoveUpAttributeButton)
                        .Add(fMoveDownAttributeButton)
                        .AddGlue();

        fMainSplitView = new BSplitView(B_HORIZONTAL, floorf(padding / 2));

        BLayoutBuilder::Group<>(this, B_VERTICAL, 0)
                .SetInsets(0)
                .Add(menuBar)
                .AddGroup(B_HORIZONTAL, 0)
                        .SetInsets(B_USE_WINDOW_SPACING)
                        .AddSplit(fMainSplitView)
                                .AddGroup(B_VERTICAL, padding)
                                        .Add(typeListScrollView)
                                        .AddGroup(B_HORIZONTAL, padding)
                                                .Add(addTypeButton)
                                                .Add(fRemoveTypeButton)
                                                .AddGlue()
                                                .End()
                                        .End()
                                // Right side
                                .AddGroup(B_VERTICAL, padding)
                                        .AddGroup(B_HORIZONTAL, padding)
                                                .Add(fIconBox, 1)
                                                .Add(fRecognitionBox, 3)
                                                .End()
                                        .Add(fDescriptionBox)
                                        .Add(fPreferredBox)
                                        .Add(fAttributeBox, 5);

        _SetType(NULL);
        _ShowSnifferRule(showRule);

        float leftWeight;
        float rightWeight;
        if (settings.FindFloat("left_split_weight", &leftWeight) != B_OK
                || settings.FindFloat("right_split_weight", &rightWeight) != B_OK) {
                leftWeight = 0.2;
                rightWeight = 1.0 - leftWeight;
        }
        fMainSplitView->SetItemWeight(0, leftWeight, false);
        fMainSplitView->SetItemWeight(1, rightWeight, true);

        BMimeType::StartWatching(this);
}


FileTypesWindow::~FileTypesWindow()
{
        BMimeType::StopWatching(this);
}


void
FileTypesWindow::MessageReceived(BMessage* message)
{
        switch (message->what) {
                case B_SIMPLE_DATA:
                {
                        type_code type;
                        if (message->GetInfo("refs", &type) == B_OK
                                && type == B_REF_TYPE) {
                                be_app->PostMessage(message);
                        }
                        break;
                }

                case kMsgToggleIcons:
                {
                        BMenuItem* item;
                        if (message->FindPointer("source", (void **)&item) != B_OK)
                                break;

                        item->SetMarked(!fTypeListView->IsShowingIcons());
                        fTypeListView->ShowIcons(item->IsMarked());

                        // update settings
                        BMessage update(kMsgSettingsChanged);
                        update.AddBool("show_icons", item->IsMarked());
                        be_app_messenger.SendMessage(&update);
                        break;
                }

                case kMsgToggleRule:
                {
                        BMenuItem* item;
                        if (message->FindPointer("source", (void **)&item) != B_OK)
                                break;

                        item->SetMarked(fRuleControl->IsHidden());
                        _ShowSnifferRule(item->IsMarked());

                        // update settings
                        BMessage update(kMsgSettingsChanged);
                        update.AddBool("show_rule", item->IsMarked());
                        be_app_messenger.SendMessage(&update);
                        break;
                }

                case kMsgTypeSelected:
                {
                        int32 index;
                        if (message->FindInt32("index", &index) == B_OK) {
                                MimeTypeItem* item
                                        = (MimeTypeItem*)fTypeListView->ItemAt(index);
                                if (item != NULL) {
                                        BMimeType type(item->Type());
                                        _SetType(&type);
                                } else
                                        _SetType(NULL);
                        }
                        break;
                }

                case kMsgAddType:
                        if (fNewTypeWindow == NULL) {
                                fNewTypeWindow
                                        = new NewFileTypeWindow(this, fCurrentType.Type());
                                fNewTypeWindow->Show();
                        } else
                                fNewTypeWindow->Activate();
                        break;

                case kMsgNewTypeWindowClosed:
                        fNewTypeWindow = NULL;
                        break;

                case kMsgRemoveType:
                {
                        if (fCurrentType.Type() == NULL)
                                break;

                        BAlert* alert;
                        if (fCurrentType.IsSupertypeOnly()) {
                                alert = new BPrivate::OverrideAlert(
                                        B_TRANSLATE("FileTypes request"),
                                        B_TRANSLATE("Removing a super type cannot be reverted.\n"
                                        "All file types that belong to this super type "
                                        "will be lost!\n\n"
                                        "Are you sure you want to do this? To remove the whole "
                                        "group, hold down the Shift key and press \"Remove\"."),
                                        B_TRANSLATE("Remove"), B_SHIFT_KEY, B_TRANSLATE("Cancel"),
                                        0, NULL, 0, B_WIDTH_AS_USUAL, B_STOP_ALERT);
                                alert->SetShortcut(1, B_ESCAPE);
                        } else {
                                alert = new BAlert(B_TRANSLATE("FileTypes request"),
                                        B_TRANSLATE("Removing a file type cannot be reverted.\n"
                                        "Are you sure you want to remove it?"),
                                        B_TRANSLATE("Remove"), B_TRANSLATE("Cancel"),
                                        NULL, B_WIDTH_AS_USUAL, B_WARNING_ALERT);
                                alert->SetShortcut(1, B_ESCAPE);
                        }
                        if (alert->Go())
                                break;

                        status_t status = fCurrentType.Delete();
                        if (status != B_OK) {
                                fprintf(stderr, B_TRANSLATE(
                                        "Could not remove file type: %s\n"), strerror(status));
                        }
                        break;
                }

                case kMsgSelectNewType:
                {
                        const char* type;
                        if (message->FindString("type", &type) == B_OK)
                                fTypeListView->SelectNewType(type);
                        break;
                }

                // File Recognition group

                case kMsgExtensionSelected:
                {
                        int32 index;
                        if (message->FindInt32("index", &index) == B_OK) {
                                BStringItem* item
                                        = (BStringItem*)fExtensionListView->ItemAt(index);
                                fRemoveExtensionButton->SetEnabled(item != NULL);
                        }
                        break;
                }

                case kMsgExtensionInvoked:
                {
                        if (fCurrentType.Type() == NULL)
                                break;

                        int32 index;
                        if (message->FindInt32("index", &index) == B_OK) {
                                BStringItem* item
                                        = (BStringItem*)fExtensionListView->ItemAt(index);
                                if (item == NULL)
                                        break;

                                BWindow* window
                                        = new ExtensionWindow(this, fCurrentType, item->Text());
                                window->Show();
                        }
                        break;
                }

                case kMsgAddExtension:
                {
                        if (fCurrentType.Type() == NULL)
                                break;

                        BWindow* window = new ExtensionWindow(this, fCurrentType, NULL);
                        window->Show();
                        break;
                }

                case kMsgRemoveExtension:
                {
                        int32 index = fExtensionListView->CurrentSelection();
                        if (index < 0 || fCurrentType.Type() == NULL)
                                break;

                        BMessage extensions;
                        if (fCurrentType.GetFileExtensions(&extensions) == B_OK) {
                                extensions.RemoveData("extensions", index);
                                fCurrentType.SetFileExtensions(&extensions);
                        }
                        break;
                }

                case kMsgRuleEntered:
                {
                        // check rule
                        BString parseError;
                        if (BMimeType::CheckSnifferRule(fRuleControl->Text(),
                                        &parseError) != B_OK) {
                                parseError.Prepend(
                                        B_TRANSLATE("Recognition rule is not valid:\n\n"));
                                error_alert(parseError.String());
                        } else
                                fCurrentType.SetSnifferRule(fRuleControl->Text());
                        break;
                }

                // Description group

                case kMsgTypeEntered:
                {
                        fCurrentType.SetShortDescription(fTypeNameControl->Text());
                        break;
                }

                case kMsgDescriptionEntered:
                {
                        fCurrentType.SetLongDescription(fDescriptionControl->Text());
                        break;
                }

                // Preferred Application group

                case kMsgPreferredAppChosen:
                {
                        const char* signature;
                        if (message->FindString("signature", &signature) != B_OK)
                                signature = NULL;

                        fCurrentType.SetPreferredApp(signature);
                        break;
                }

                case kMsgSelectPreferredApp:
                {
                        BMessage panel(kMsgOpenFilePanel);
                        panel.AddString("title",
                                B_TRANSLATE("Select preferred application"));
                        panel.AddInt32("message", kMsgPreferredAppOpened);
                        panel.AddMessenger("target", this);

                        be_app_messenger.SendMessage(&panel);
                        break;
                }
                case kMsgPreferredAppOpened:
                        _AdoptPreferredApplication(message, false);
                        break;

                case kMsgSamePreferredAppAs:
                {
                        BMessage panel(kMsgOpenFilePanel);
                        panel.AddString("title",
                                B_TRANSLATE("Select same preferred application as"));
                        panel.AddInt32("message", kMsgSamePreferredAppAsOpened);
                        panel.AddMessenger("target", this);
                        panel.AddBool("allowDirs", true);

                        be_app_messenger.SendMessage(&panel);
                        break;
                }
                case kMsgSamePreferredAppAsOpened:
                        _AdoptPreferredApplication(message, true);
                        break;

                // Extra Attributes group

                case kMsgAttributeSelected:
                {
                        int32 index;
                        if (message->FindInt32("index", &index) == B_OK) {
                                AttributeItem* item
                                        = (AttributeItem*)fAttributeListView->ItemAt(index);
                                fRemoveAttributeButton->SetEnabled(item != NULL);
                                fMoveUpAttributeButton->SetEnabled(index > 0);
                                fMoveDownAttributeButton->SetEnabled(index >= 0
                                        && index < fAttributeListView->CountItems() - 1);
                        }
                        break;
                }

                case kMsgAttributeInvoked:
                {
                        if (fCurrentType.Type() == NULL)
                                break;

                        int32 index;
                        if (message->FindInt32("index", &index) == B_OK) {
                                AttributeItem* item
                                        = (AttributeItem*)fAttributeListView->ItemAt(index);
                                if (item == NULL)
                                        break;

                                BWindow* window = new AttributeWindow(this, fCurrentType,
                                        item);
                                window->Show();
                        }
                        break;
                }

                case kMsgAddAttribute:
                {
                        if (fCurrentType.Type() == NULL)
                                break;

                        BWindow* window = new AttributeWindow(this, fCurrentType, NULL);
                        window->Show();
                        break;
                }

                case kMsgRemoveAttribute:
                {
                        int32 index = fAttributeListView->CurrentSelection();
                        if (index < 0 || fCurrentType.Type() == NULL)
                                break;

                        BMessage attributes;
                        if (fCurrentType.GetAttrInfo(&attributes) == B_OK) {
                                for (uint32 i = 0; i <
                                                sizeof(kAttributeNames) / sizeof(kAttributeNames[0]);
                                                i++) {
                                        attributes.RemoveData(kAttributeNames[i], index);
                                }

                                fCurrentType.SetAttrInfo(&attributes);
                        }
                        break;
                }

                case kMsgMoveUpAttribute:
                {
                        int32 index = fAttributeListView->CurrentSelection();
                        if (index < 1 || fCurrentType.Type() == NULL)
                                break;

                        _MoveUpAttributeIndex(index);
                        break;
                }

                case kMsgMoveDownAttribute:
                {
                        int32 index = fAttributeListView->CurrentSelection();
                        if (index < 0 || index == fAttributeListView->CountItems() - 1
                                || fCurrentType.Type() == NULL) {
                                break;
                        }

                        _MoveUpAttributeIndex(index + 1);
                        break;
                }

                case B_META_MIME_CHANGED:
                {
                        const char* type;
                        int32 which;
                        if (message->FindString("be:type", &type) != B_OK
                                || message->FindInt32("be:which", &which) != B_OK)
                                break;

                        if (fCurrentType.Type() == NULL)
                                break;

                        if (!strcasecmp(fCurrentType.Type(), type)) {
                                if (which != B_MIME_TYPE_DELETED)
                                        _SetType(&fCurrentType, which);
                                else
                                        _SetType(NULL);
                        } else {
                                // this change could still affect our current type

                                if (which == B_MIME_TYPE_DELETED
                                        || which == B_SUPPORTED_TYPES_CHANGED
                                        || which == B_PREFERRED_APP_CHANGED) {
                                        _UpdatePreferredApps(&fCurrentType);
                                }
                        }
                        break;
                }

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


void
FileTypesWindow::SelectType(const char* type)
{
        fTypeListView->SelectType(type);
}


bool
FileTypesWindow::QuitRequested()
{
        BMessage update(kMsgSettingsChanged);
        update.AddRect("file_types_frame", Frame());
        update.AddFloat("left_split_weight", fMainSplitView->ItemWeight((int32)0));
        update.AddFloat("right_split_weight", fMainSplitView->ItemWeight(1));
        be_app_messenger.SendMessage(&update);

        be_app->PostMessage(kMsgTypesWindowClosed);
        return true;
}


void
FileTypesWindow::PlaceSubWindow(BWindow* window)
{
        window->MoveTo(Frame().left + (Frame().Width() - window->Frame().Width())
                / 2.0f, Frame().top + (Frame().Height() - window->Frame().Height())
                / 2.0f);
}


// #pragma mark - private


BRect
FileTypesWindow::_Frame(const BMessage& settings) const
{
        BRect rect;
        if (settings.FindRect("file_types_frame", &rect) == B_OK)
                return rect;

        return BRect(80.0f, 80.0f, 0.0f, 0.0f);
}


void
FileTypesWindow::_ShowSnifferRule(bool show)
{
        if (fRuleControl->IsHidden() == !show)
                return;

        if (!show)
                fRuleControl->Hide();
        else
                fRuleControl->Show();
}


void
FileTypesWindow::_UpdateExtensions(BMimeType* type)
{
        // clear list

        for (int32 i = fExtensionListView->CountItems(); i-- > 0;) {
                delete fExtensionListView->ItemAt(i);
        }
        fExtensionListView->MakeEmpty();

        // fill it again

        if (type == NULL)
                return;

        BMessage extensions;
        if (type->GetFileExtensions(&extensions) != B_OK)
                return;

        const char* extension;
        int32 i = 0;
        while (extensions.FindString("extensions", i++, &extension) == B_OK) {
                char dotExtension[B_FILE_NAME_LENGTH];
                snprintf(dotExtension, B_FILE_NAME_LENGTH, ".%s", extension);

                fExtensionListView->AddItem(new BStringItem(dotExtension));
        }
}


void
FileTypesWindow::_AdoptPreferredApplication(BMessage* message, bool sameAs)
{
        if (fCurrentType.Type() == NULL)
                return;

        BString preferred;
        if (retrieve_preferred_app(message, sameAs, fCurrentType.Type(), preferred)
                != B_OK) {
                return;
        }

        status_t status = fCurrentType.SetPreferredApp(preferred.String());
        if (status != B_OK)
                error_alert(B_TRANSLATE("Could not set preferred application"),
                        status);
}


void
FileTypesWindow::_UpdatePreferredApps(BMimeType* type)
{
        update_preferred_app_menu(fPreferredField->Menu(), type,
                kMsgPreferredAppChosen);
}


void
FileTypesWindow::_UpdateIcon(BMimeType* type)
{
        if (type != NULL)
                fIconView->SetTo(*type);
        else
                fIconView->Unset();
}


void
FileTypesWindow::_SetType(BMimeType* type, int32 forceUpdate)
{
        bool enabled = type != NULL;

        // update controls

        if (type != NULL) {
                if (fCurrentType == *type) {
                        if (!forceUpdate)
                                return;
                } else
                        forceUpdate = B_EVERYTHING_CHANGED;

                if (&fCurrentType != type)
                        fCurrentType.SetTo(type->Type());

                fInternalNameView->SetText(type->Type());

                char description[B_MIME_TYPE_LENGTH];

                if ((forceUpdate & B_SHORT_DESCRIPTION_CHANGED) != 0) {
                        if (type->GetShortDescription(description) != B_OK)
                                description[0] = '\0';
                        fTypeNameControl->SetText(description);
                }

                if ((forceUpdate & B_LONG_DESCRIPTION_CHANGED) != 0) {
                        if (type->GetLongDescription(description) != B_OK)
                                description[0] = '\0';
                        fDescriptionControl->SetText(description);
                }

                if ((forceUpdate & B_SNIFFER_RULE_CHANGED) != 0) {
                        BString rule;
                        if (type->GetSnifferRule(&rule) != B_OK)
                                rule = "";
                        fRuleControl->SetText(rule.String());
                }

                fExtensionListView->SetType(&fCurrentType);
        } else {
                fCurrentType.Unset();
                fInternalNameView->SetText(NULL);
                fTypeNameControl->SetText(NULL);
                fDescriptionControl->SetText(NULL);
                fRuleControl->SetText(NULL);
                fPreferredField->Menu()->ItemAt(0)->SetMarked(true);
                fExtensionListView->SetType(NULL);
                fAttributeListView->SetTo(NULL);
        }

        if ((forceUpdate & B_FILE_EXTENSIONS_CHANGED) != 0)
                _UpdateExtensions(type);

        if ((forceUpdate & B_PREFERRED_APP_CHANGED) != 0)
                _UpdatePreferredApps(type);

        if ((forceUpdate & (B_ICON_CHANGED | B_PREFERRED_APP_CHANGED)) != 0)
                _UpdateIcon(type);

        if ((forceUpdate & B_ATTR_INFO_CHANGED) != 0)
                fAttributeListView->SetTo(type);

        // enable/disable controls

        fIconView->SetEnabled(enabled);

        fInternalNameView->SetEnabled(enabled);
        fTypeNameControl->SetEnabled(enabled);
        fDescriptionControl->SetEnabled(enabled);
        fPreferredField->SetEnabled(enabled);

        fRemoveTypeButton->SetEnabled(enabled);

        fSelectButton->SetEnabled(enabled);
        fSameAsButton->SetEnabled(enabled);

        fExtensionLabel->SetEnabled(enabled);
        fAddExtensionButton->SetEnabled(enabled);
        fRemoveExtensionButton->SetEnabled(false);
        fRuleControl->SetEnabled(enabled);

        fAddAttributeButton->SetEnabled(enabled);
        fRemoveAttributeButton->SetEnabled(false);
        fMoveUpAttributeButton->SetEnabled(false);
        fMoveDownAttributeButton->SetEnabled(false);
}


void
FileTypesWindow::_MoveUpAttributeIndex(int32 index)
{
        BMessage attributes;
        if (fCurrentType.GetAttrInfo(&attributes) != B_OK)
                return;

        // Iterate over all known attribute fields, and for each field,
        // iterate over all fields of the same name and build a copy
        // of the attributes message with the field at the given index swapped
        // with the previous field.
        BMessage resortedAttributes;
        for (uint32 i = 0; i <
                        sizeof(kAttributeNames) / sizeof(kAttributeNames[0]);
                        i++) {

                type_code type;
                int32 count;
                bool isFixedSize;
                if (attributes.GetInfo(kAttributeNames[i], &type, &count,
                                &isFixedSize) != B_OK) {
                        // Apparently the message does not contain this name,
                        // so just ignore this attribute name.
                        // NOTE: This shows that the attribute description is
                        // too fragile. It would have been better to pack each
                        // attribute description into a separate BMessage.
                        continue;
                }

                for (int32 j = 0; j < count; j++) {
                        const void* data;
                        ssize_t size;
                        int32 originalIndex;
                        if (j == index - 1)
                                originalIndex = j + 1;
                        else if (j == index)
                                originalIndex = j - 1;
                        else
                                originalIndex = j;
                        attributes.FindData(kAttributeNames[i], type,
                                originalIndex, &data, &size);
                        if (j == 0) {
                                resortedAttributes.AddData(kAttributeNames[i], type,
                                        data, size, isFixedSize);
                        } else {
                                resortedAttributes.AddData(kAttributeNames[i], type,
                                        data, size);
                        }
                }
        }

        // Setting it directly on the type will trigger an update of the GUI as
        // well. TODO: FileTypes is heavily descructive, it should use an
        // Undo/Redo stack.
        fCurrentType.SetAttrInfo(&resortedAttributes);
}