root/src/servers/package/ProblemWindow.cpp
/*
 * Copyright 2013, Ingo Weinhold, ingo_weinhold@gmx.de.
 * Distributed under the terms of the MIT License.
 */


#include "ProblemWindow.h"

#include <Button.h>
#include <Catalog.h>
#include <GroupView.h>
#include <LayoutBuilder.h>
#include <RadioButton.h>
#include <ScrollView.h>
#include <StringView.h>
#include <package/solver/Solver.h>
#include <package/solver/SolverPackage.h>
#include <package/solver/SolverProblem.h>
#include <package/solver/SolverProblemSolution.h>

#include <AutoLocker.h>
#include <package/manager/Exceptions.h>
#include <ViewPort.h>


#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "PackageProblem"

using namespace BPackageKit;

using BPackageKit::BManager::BPrivate::BFatalErrorException;


static const uint32 kRetryMessage = 'rtry';
static const uint32 kUpdateRetryButtonMessage = 'uprt';


struct ProblemWindow::Solution {
        BSolverProblem*                                 fProblem;
        const BSolverProblemSolution*   fSolution;

        Solution()
                :
                fProblem(NULL),
                fSolution(NULL)
        {
        }

        Solution(BSolverProblem* problem, const BSolverProblemSolution* solution)
                :
                fProblem(problem),
                fSolution(solution)
        {
        }
};


ProblemWindow::ProblemWindow()
        :
        BWindow(BRect(0, 0, 400, 300), B_TRANSLATE_COMMENT("Package problems",
                        "Window title"), B_TITLED_WINDOW_LOOK,
                B_MODAL_APP_WINDOW_FEEL,
                B_ASYNCHRONOUS_CONTROLS | B_NOT_MINIMIZABLE | B_AUTO_UPDATE_SIZE_LIMITS,
                B_ALL_WORKSPACES),
        fDoneSemaphore(-1),
        fClientWaiting(false),
        fAccepted(false),
        fContainerView(NULL),
        fCancelButton(NULL),
        fRetryButton(NULL),
        fSolutions(),
        fPackagesAddedByUser(NULL),
        fPackagesRemovedByUser(NULL)

{
        fDoneSemaphore = create_sem(0, "package problems");
        if (fDoneSemaphore < 0)
                throw std::bad_alloc();

        BStringView* topTextView = NULL;
        BViewPort* viewPort = NULL;

        BLayoutBuilder::Group<>(this, B_VERTICAL, B_USE_DEFAULT_SPACING)
                .SetInsets(B_USE_SMALL_INSETS)
                .Add(topTextView = new BStringView(NULL, B_TRANSLATE(
                                "The following problems have been encountered. Please select "
                                "a solution for each:")))
                .Add(new BScrollView(NULL, viewPort = new BViewPort(), 0, false, true))
                .AddGroup(B_HORIZONTAL)
                        .AddGlue()
                        .Add(fCancelButton = new BButton(B_TRANSLATE("Cancel"),
                                new BMessage(B_CANCEL)))
                        .Add(fRetryButton = new BButton(B_TRANSLATE("Retry"),
                                new BMessage(kRetryMessage)))
                .End();

        topTextView->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET));

        viewPort->SetChildView(fContainerView = new BGroupView(B_VERTICAL, 0));

        // set small scroll step (large step will be set by the view port)
        font_height fontHeight;
        topTextView->GetFontHeight(&fontHeight);
        float smallStep = ceilf(fontHeight.ascent + fontHeight.descent);
        viewPort->ScrollBar(B_VERTICAL)->SetSteps(smallStep, smallStep);
}


ProblemWindow::~ProblemWindow()
{
        if (fDoneSemaphore >= 0)
                delete_sem(fDoneSemaphore);
}


bool
ProblemWindow::Go(BSolver* solver, const SolverPackageSet& packagesAddedByUser,
        const SolverPackageSet& packagesRemovedByUser)
{
        AutoLocker<ProblemWindow> locker(this);

        fPackagesAddedByUser = &packagesAddedByUser;
        fPackagesRemovedByUser = &packagesRemovedByUser;

        _ClearProblemsGui();
        _AddProblemsGui(solver);

        fCancelButton->SetEnabled(true);
        fRetryButton->SetEnabled(false);

        if (IsHidden()) {
                CenterOnScreen();
                Show();
        }

        fAccepted = false;
        fClientWaiting = true;

        locker.Unlock();

        while (acquire_sem(fDoneSemaphore) == B_INTERRUPTED) {
        }

        locker.Lock();

        Hide();

        if (!locker.IsLocked() || !fAccepted || !_AnySolutionSelected())
                return false;

        // set the solutions
        for (SolutionMap::const_iterator it = fSolutions.begin();
                it != fSolutions.end(); ++it) {
                BRadioButton* button = it->first;
                if (button->Value() == B_CONTROL_ON) {
                        const Solution& solution = it->second;
                        status_t error = solver->SelectProblemSolution(solution.fProblem,
                                solution.fSolution);
                        if (error != B_OK)
                                throw BFatalErrorException(error, "failed to set solution");
                }
        }

        return true;
}


bool
ProblemWindow::QuitRequested()
{
        if (fClientWaiting) {
                fClientWaiting = false;
                release_sem(fDoneSemaphore);
        }
        return true;
}


void
ProblemWindow::MessageReceived(BMessage* message)
{
        switch (message->what) {
                case B_CANCEL:
                        Hide();
                        fClientWaiting = false;
                        release_sem(fDoneSemaphore);
                        break;
                case kRetryMessage:
                        fCancelButton->SetEnabled(false);
                        fRetryButton->SetEnabled(false);
                        fAccepted = true;
                        fClientWaiting = false;
                        release_sem(fDoneSemaphore);
                        break;
                case kUpdateRetryButtonMessage:
                        fRetryButton->SetEnabled(_AnySolutionSelected());
                        break;
                default:
                        BWindow::MessageReceived(message);
                        break;
        }
}


void
ProblemWindow::_ClearProblemsGui()
{
        fSolutions.clear();

        int32 count = fContainerView->CountChildren();
        for (int32 i = count - 1; i >= 0; i--) {
                BView* child = fContainerView->ChildAt(i);
                fContainerView->RemoveChild(child);
                delete child;
        }
}


void
ProblemWindow::_AddProblemsGui(BSolver* solver)
{
        int32 problemCount = solver->CountProblems();
        for (int32 i = 0; i < problemCount; i++) {
                _AddProblem(solver->ProblemAt(i),
                        (i & 1) == 0 ? B_NO_TINT : 1.04);
        }
}


void
ProblemWindow::_AddProblem(BSolverProblem* problem, const float backgroundTint)
{
        BGroupView* problemGroup = new BGroupView(B_VERTICAL);
        fContainerView->AddChild(problemGroup);
        problemGroup->GroupLayout()->SetInsets(B_USE_SMALL_INSETS);

        problemGroup->SetViewUIColor(B_DOCUMENT_BACKGROUND_COLOR, backgroundTint);
        problemGroup->SetHighUIColor(B_DOCUMENT_TEXT_COLOR);

        BStringView* problemView = new BStringView(NULL, problem->ToString());
        problemGroup->AddChild(problemView);
        problemView->AdoptParentColors();

        BFont problemFont;
        problemView->GetFont(&problemFont);
        problemFont.SetFace(B_BOLD_FACE);
        problemView->SetFont(&problemFont);

        int32 solutionCount = problem->CountSolutions();
        for (int k = 0; k < solutionCount; k++) {
                const BSolverProblemSolution* solution = problem->SolutionAt(k);
                BRadioButton* solutionButton = new BRadioButton(
                        BString().SetToFormat(B_TRANSLATE_COMMENT("solution %d:",
                                "Don't change the %d variable"), k + 1),
                        new BMessage(kUpdateRetryButtonMessage));
                problemGroup->AddChild(solutionButton);

                BGroupLayout* elementsGroup = new BGroupLayout(B_VERTICAL);
                problemGroup->AddChild(elementsGroup);
                elementsGroup->SetInsets(20, 0, 0, 0);

                int32 elementCount = solution->CountElements();
                for (int32 l = 0; l < elementCount; l++) {
                        const BSolverProblemSolutionElement* element = solution->ElementAt(l);
                        BStringView* elementView = new BStringView(NULL,
                                BString().SetToFormat("- %s", _SolutionElementText(element).String()));
                        elementsGroup->AddView(elementView);
                        elementView->AdoptParentColors();
                }

                fSolutions[solutionButton] = Solution(problem, solution);
        }

        BRadioButton* ignoreButton = new BRadioButton(B_TRANSLATE(
                "ignore problem for now"), new BMessage(kUpdateRetryButtonMessage));
        problemGroup->AddChild(ignoreButton);
        ignoreButton->SetValue(B_CONTROL_ON);
}


BString
ProblemWindow::_SolutionElementText(
        const BSolverProblemSolutionElement* element) const
{
        // Reword text for B_ALLOW_DEINSTALLATION, if the package has been added
        // by the user.
        BSolverPackage* package = element->SourcePackage();
        if (element->Type() == BSolverProblemSolutionElement::B_ALLOW_DEINSTALLATION
                && package != NULL
                && fPackagesAddedByUser->find(package) != fPackagesAddedByUser->end()) {
                return BString(B_TRANSLATE_COMMENT("don't activate package %source%",
                                "don't change '%source%")).ReplaceAll(
                                "%source%", package->VersionedName());
        }

        return element->ToString();
}


bool
ProblemWindow::_AnySolutionSelected() const
{
        for (SolutionMap::const_iterator it = fSolutions.begin();
                it != fSolutions.end(); ++it) {
                BRadioButton* button = it->first;
                if (button->Value() == B_CONTROL_ON)
                        return true;
        }

        return false;
}