root/src/preferences/virtualmemory/SettingsWindow.cpp
/*
 * Copyright 2005-2009, Axel Dörfler, axeld@pinc-software.de
 * All rights reserved. Distributed under the terms of the MIT License.
 *
 * Copyright 2010-2012 Haiku, Inc. All rights reserved.
 * Distributed under the terms of the MIT License.
 *
 * Authors:
 *              Hamish Morrison, hamish@lavabit.com
 *              Alexander von Gluck, kallisti5@unixzen.com
 */


#include "SettingsWindow.h"

#include <Application.h>
#include <Alert.h>
#include <Box.h>
#include <Button.h>
#include <Catalog.h>
#include <CheckBox.h>
#include <Directory.h>
#include <FindDirectory.h>
#include <LayoutBuilder.h>
#include <MenuItem.h>
#include <MenuField.h>
#include <NodeMonitor.h>
#include <Path.h>
#include <PopUpMenu.h>
#include <Screen.h>
#include <StringForSize.h>
#include <StringView.h>
#include <String.h>
#include <Slider.h>
#include <system_info.h>
#include <Volume.h>
#include <VolumeRoster.h>

#include "Settings.h"


#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "SettingsWindow"


static const uint32 kMsgDefaults = 'dflt';
static const uint32 kMsgRevert = 'rvrt';
static const uint32 kMsgSliderUpdate = 'slup';
static const uint32 kMsgSwapEnabledUpdate = 'swen';
static const uint32 kMsgSwapAutomaticUpdate = 'swat';
static const uint32 kMsgVolumeSelected = 'vlsl';
static const off_t kMegaByte = 1024 * 1024;
static dev_t gBootDev = -1;


SizeSlider::SizeSlider(const char* name, const char* label,
        BMessage* message, int32 min, int32 max, uint32 flags)
        :
        BSlider(name, label, message, min, max, B_HORIZONTAL,
                B_BLOCK_THUMB, flags)
{
        rgb_color color = ui_color(B_CONTROL_HIGHLIGHT_COLOR);
        UseFillColor(true, &color);
}


const char*
SizeSlider::UpdateText() const
{
        return string_for_size(Value() * kMegaByte, fText, sizeof(fText));
}


VolumeMenuItem::VolumeMenuItem(BVolume volume, BMessage* message)
        :
        BMenuItem("", message),
        fVolume(volume)
{
        GenerateLabel();
}


void
VolumeMenuItem::MessageReceived(BMessage* message)
{
        if (message->what == B_NODE_MONITOR) {
                int32 code;
                if (message->FindInt32("opcode", &code) == B_OK)
                        if (code == B_ENTRY_MOVED)
                                GenerateLabel();
        }
}


void
VolumeMenuItem::GenerateLabel()
{
        char name[B_FILE_NAME_LENGTH + 1];
        fVolume.GetName(name);

        BDirectory dir;
        if (fVolume.GetRootDirectory(&dir) == B_OK) {
                BEntry entry;
                if (dir.GetEntry(&entry) == B_OK) {
                        BPath path;
                        if (entry.GetPath(&path) == B_OK) {
                                BString label;
                                label << name << " (" << path.Path() << ")";
                                SetLabel(label);
                                return;
                        }
                }
        }

        SetLabel(name);
}


SettingsWindow::SettingsWindow()
        :
        BWindow(BRect(0, 0, 269, 172), B_TRANSLATE_SYSTEM_NAME("VirtualMemory"),
                B_TITLED_WINDOW, B_NOT_RESIZABLE | B_ASYNCHRONOUS_CONTROLS
                | B_NOT_ZOOMABLE | B_AUTO_UPDATE_SIZE_LIMITS),
        fSwapEnabledCheckBox(NULL),
        fSwapAutomaticCheckBox(NULL),
        fSizeSlider(NULL),
        fDefaultsButton(NULL),
        fRevertButton(NULL),
        fWarningStringView(NULL),
        fVolumeMenuField(NULL),
        fSwapUsageBar(NULL),
        fSetupComplete(false)
{
        gBootDev = dev_for_path("/boot");
        BAlignment align(B_ALIGN_LEFT, B_ALIGN_MIDDLE);

        if (fSettings.ReadWindowSettings() != B_OK)
                CenterOnScreen();
        else
                MoveTo(fSettings.WindowPosition());

        status_t result = fSettings.ReadSwapSettings();
        if (result == kErrorSettingsNotFound)
                fSettings.DefaultSwapSettings(false);
        else if (result == kErrorSettingsInvalid) {
                int32 choice = (new BAlert(B_TRANSLATE_SYSTEM_NAME("VirtualMemory"),
                        B_TRANSLATE("The settings specified in the settings file "
                        "are invalid. You can load the defaults or quit."),
                        B_TRANSLATE("Load defaults"), B_TRANSLATE("Quit")))->Go();
                if (choice == 1) {
                        be_app->PostMessage(B_QUIT_REQUESTED);
                        return;
                }
                fSettings.DefaultSwapSettings(false);
        } else if (result == kErrorVolumeNotFound) {
                int32 choice = (new BAlert(B_TRANSLATE_SYSTEM_NAME("VirtualMemory"),
                        B_TRANSLATE("The volume specified in the settings file "
                        "could not be found. You can use the boot volume or quit."),
                        B_TRANSLATE("Use boot volume"), B_TRANSLATE("Quit")))->Go();
                if (choice == 1) {
                        be_app->PostMessage(B_QUIT_REQUESTED);
                        return;
                }
                fSettings.SetSwapVolume(gBootDev, false);
        }

        fSwapEnabledCheckBox = new BCheckBox("enable swap",
                B_TRANSLATE("Enable virtual memory"),
                new BMessage(kMsgSwapEnabledUpdate));
        fSwapEnabledCheckBox->SetExplicitAlignment(align);

        fSwapAutomaticCheckBox = new BCheckBox("auto swap",
                B_TRANSLATE("Automatic swap management"),
                new BMessage(kMsgSwapAutomaticUpdate));
        fSwapEnabledCheckBox->SetExplicitAlignment(align);

        fSwapUsageBar = new BStatusBar("swap usage");

        BPopUpMenu* menu = new BPopUpMenu("volume menu");
        fVolumeMenuField = new BMenuField("volume menu field",
                B_TRANSLATE("Use volume:"), menu);
        fVolumeMenuField->SetExplicitAlignment(align);

        BVolumeRoster roster;
        BVolume vol;
        while (roster.GetNextVolume(&vol) == B_OK) {
                if (!vol.IsPersistent() || vol.IsReadOnly() || vol.IsRemovable()
                        || vol.IsShared())
                        continue;
                _AddVolumeMenuItem(vol.Device());
        }

        watch_node(NULL, B_WATCH_MOUNT, this, this);

        fSizeSlider = new SizeSlider("size slider",
                B_TRANSLATE("Requested swap file size:"),
                new BMessage(kMsgSliderUpdate), 0, 0, B_WILL_DRAW | B_FRAME_EVENTS);
        fSizeSlider->SetViewColor(255, 0, 255);
        fSizeSlider->SetExplicitAlignment(align);

        fWarningStringView = new BStringView("warning",
                B_TRANSLATE("Changes will take effect upon reboot."));

        BBox* box = new BBox("box");
        box->SetLabel(fSwapEnabledCheckBox);

        box->AddChild(BLayoutBuilder::Group<>(B_VERTICAL)
                .SetInsets(B_USE_DEFAULT_SPACING)
                .Add(fSwapUsageBar)
                .Add(fSwapAutomaticCheckBox)
                .Add(fVolumeMenuField)
                .Add(fSizeSlider)
                .Add(fWarningStringView)
                .View());

        fDefaultsButton = new BButton("defaults", B_TRANSLATE("Defaults"),
                new BMessage(kMsgDefaults));

        fRevertButton = new BButton("revert", B_TRANSLATE("Revert"),
                new BMessage(kMsgRevert));
        fRevertButton->SetEnabled(false);

        BLayoutBuilder::Group<>(this, B_VERTICAL)
                .SetInsets(B_USE_WINDOW_SPACING)
                .Add(box)
                .AddGroup(B_HORIZONTAL)
                        .Add(fDefaultsButton)
                        .Add(fRevertButton)
                        .AddGlue()
                .End();

        BScreen screen;
        BRect screenFrame = screen.Frame();
        if (!screenFrame.Contains(fSettings.WindowPosition()))
                CenterOnScreen();

#ifdef SWAP_VOLUME_IMPLEMENTED
        // Validate the volume specified in settings file
        status_t result = fSettings.SwapVolume().InitCheck();

        if (result != B_OK) {
                BAlert* alert = new BAlert(B_TRANSLATE_SYSTEM_NAME("VirtualMemory"),
                        B_TRANSLATE("The swap volume specified in the settings file is ",
                        "invalid.\n You can keep the current setting or switch to the "
                        "default swap volume."),
                        B_TRANSLATE("Keep"), B_TRANSLATE("Switch"), NULL,
                        B_WIDTH_AS_USUAL, B_WARNING_ALERT);
                alert->SetShortcut(0, B_ESCAPE);
                int32 choice = alert->Go();
                if (choice == 1) {
                        BVolumeRoster volumeRoster;
                        BVolume bootVolume;
                        volumeRoster.GetBootVolume(&bootVolume);
                        fSettings.SetSwapVolume(bootVolume);
                }
        }
#endif

        _Update();

        // TODO: We may want to run this at an interval
        _UpdateSwapInfo();
        fSetupComplete = true;
}


void
SettingsWindow::MessageReceived(BMessage* message)
{
        switch (message->what) {
                case B_NODE_MONITOR:
                {
                        int32 opcode;
                        if (message->FindInt32("opcode", &opcode) != B_OK)
                                break;
                        dev_t device;
                        if (opcode == B_DEVICE_MOUNTED
                                && message->FindInt32("new device", &device) == B_OK) {
                                BVolume vol(device);
                                if (!vol.IsPersistent() || vol.IsReadOnly()
                                        || vol.IsRemovable() || vol.IsShared()) {
                                        break;
                                }
                                _AddVolumeMenuItem(device);
                        } else if (opcode == B_DEVICE_UNMOUNTED
                                && message->FindInt32("device", &device) == B_OK) {
                                _RemoveVolumeMenuItem(device);
                        }
                        _Update();
                        break;
                }
                case kMsgRevert:
                        fSettings.RevertSwapSettings();
                        _Update();
                        break;
                case kMsgDefaults:
                        fSettings.DefaultSwapSettings();
                        _Update();
                        break;
                case kMsgSliderUpdate:
                        _RecordChoices();
                        _Update();
                        break;
                case kMsgVolumeSelected:
                        _RecordChoices();
                        _Update();
                        break;
                case kMsgSwapEnabledUpdate:
                {
                        if (fSwapEnabledCheckBox->Value() == 0) {
                                // print out warning, give the user the
                                // time to think about it :)
                                // ToDo: maybe we want to remove this possibility in the GUI
                                // as Be did, but I thought a proper warning could be helpful
                                // (for those that want to change that anyway)
                                BAlert* alert = new BAlert(
                                        B_TRANSLATE_SYSTEM_NAME("VirtualMemory"), B_TRANSLATE(
                                        "Disabling virtual memory will have unwanted effects on "
                                        "system stability once the memory is used up.\n"
                                        "Virtual memory does not affect system performance "
                                        "until this point is reached.\n\n"
                                        "Are you really sure you want to turn it off?"),
                                        B_TRANSLATE("Turn off"), B_TRANSLATE("Keep enabled"), NULL,
                                        B_WIDTH_AS_USUAL, B_WARNING_ALERT);
                                alert->SetShortcut(1, B_ESCAPE);
                                int32 choice = alert->Go();
                                if (choice == 1) {
                                        fSwapEnabledCheckBox->SetValue(1);
                                        break;
                                }
                        }

                        _RecordChoices();
                        _Update();
                        break;
                }
                case kMsgSwapAutomaticUpdate:
                {
                        _RecordChoices();
                        _Update();
                        break;
                }

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


bool
SettingsWindow::QuitRequested()
{
        if (!fSetupComplete)
                return true;

        fSettings.SetWindowPosition(Frame().LeftTop());
        _RecordChoices();
        fSettings.WriteWindowSettings();
        fSettings.WriteSwapSettings();
        be_app->PostMessage(B_QUIT_REQUESTED);
        return true;
}


status_t
SettingsWindow::_AddVolumeMenuItem(dev_t device)
{
        if (_FindVolumeMenuItem(device) != NULL)
                return B_ERROR;

        VolumeMenuItem* item = new VolumeMenuItem(device,
                new BMessage(kMsgVolumeSelected));

        fs_info info;
        if (fs_stat_dev(device, &info) == 0) {
                node_ref node;
                node.device = info.dev;
                node.node = info.root;
                AddHandler(item);
                watch_node(&node, B_WATCH_NAME, item);
        }

        fVolumeMenuField->Menu()->AddItem(item);
        return B_OK;
}


status_t
SettingsWindow::_RemoveVolumeMenuItem(dev_t device)
{
        VolumeMenuItem* item = _FindVolumeMenuItem(device);
        if (item != NULL) {
                fVolumeMenuField->Menu()->RemoveItem(item);
                delete item;
                return B_OK;
        }
        return B_ERROR;
}


VolumeMenuItem*
SettingsWindow::_FindVolumeMenuItem(dev_t device)
{
        VolumeMenuItem* item = NULL;
        int32 count = fVolumeMenuField->Menu()->CountItems();
        for (int i = 0; i < count; i++) {
                item = (VolumeMenuItem*)fVolumeMenuField->Menu()->ItemAt(i);
                if (item->Volume().Device() == device)
                        return item;
        }

        return NULL;
}


void
SettingsWindow::_RecordChoices()
{
        fSettings.SetSwapAutomatic(fSwapAutomaticCheckBox->Value());
        fSettings.SetSwapEnabled(fSwapEnabledCheckBox->Value());
        fSettings.SetSwapSize((off_t)fSizeSlider->Value() * kMegaByte);
        fSettings.SetSwapVolume(((VolumeMenuItem*)fVolumeMenuField
                ->Menu()->FindMarked())->Volume().Device());
}


void
SettingsWindow::_Update()
{
        fSwapEnabledCheckBox->SetValue(fSettings.SwapEnabled());
        fSwapAutomaticCheckBox->SetValue(fSettings.SwapAutomatic());

        VolumeMenuItem* item = _FindVolumeMenuItem(fSettings.SwapVolume());
        if (item != NULL) {
                fSizeSlider->SetEnabled(true);
                item->SetMarked(true);
                BEntry swapFile;
                if (gBootDev == item->Volume().Device())
                        swapFile.SetTo("/var/swap");
                else {
                        BDirectory root;
                        item->Volume().GetRootDirectory(&root);
                        swapFile.SetTo(&root, "swap");
                }

                off_t swapFileSize = 0;
                swapFile.GetSize(&swapFileSize);

                char sizeStr[16];

                off_t freeSpace = item->Volume().FreeBytes() + swapFileSize;
                off_t safeSpace = freeSpace - (off_t)(0.15 * freeSpace);
                (safeSpace >>= 20) <<= 20;
                off_t minSize = B_PAGE_SIZE + kMegaByte;
                (minSize >>= 20) <<= 20;
                BString minLabel, maxLabel;
                minLabel << string_for_size(minSize, sizeStr, sizeof(sizeStr));
                maxLabel << string_for_size(safeSpace, sizeStr, sizeof(sizeStr));

                fSizeSlider->SetLimitLabels(minLabel.String(), maxLabel.String());
                fSizeSlider->SetLimits(minSize / kMegaByte, safeSpace / kMegaByte);
                fSizeSlider->SetValue(fSettings.SwapSize() / kMegaByte);
        } else
                fSizeSlider->SetEnabled(false);

        bool revertable = fSettings.IsRevertable();
        if (revertable)
                fWarningStringView->Show();
        else
                fWarningStringView->Hide();

        fRevertButton->SetEnabled(revertable);
        fDefaultsButton->SetEnabled(fSettings.IsDefaultable());

        // Automatic Swap depends on swap being enabled
        fSwapAutomaticCheckBox->SetEnabled(fSettings.SwapEnabled());

        // Manual swap settings depend on enabled swap
        // and automatic swap being disabled
        fSizeSlider->SetEnabled(fSettings.SwapEnabled()
                && !fSwapAutomaticCheckBox->Value());
        fVolumeMenuField->SetEnabled(fSettings.SwapEnabled()
                && !fSwapAutomaticCheckBox->Value());
}


void
SettingsWindow::_UpdateSwapInfo()
{
        system_info info;
        get_system_info(&info);

        off_t currentSwapSize = info.max_swap_pages * B_PAGE_SIZE;
        off_t currentSwapUsed
                = (info.max_swap_pages - info.free_swap_pages) * B_PAGE_SIZE;

        char sizeStr[16];
        BString swapSizeStr = string_for_size(currentSwapSize, sizeStr,
                sizeof(sizeStr));
        BString swapUsedStr = string_for_size(currentSwapUsed, sizeStr,
                sizeof(sizeStr));

        BString string = swapUsedStr << " / " << swapSizeStr;

        fSwapUsageBar->SetMaxValue(currentSwapSize / kMegaByte);
        fSwapUsageBar->Update(currentSwapUsed / kMegaByte,
                B_TRANSLATE("Current Swap:"), string.String());
}