root/src/apps/haikudepot/ui/MainWindow.cpp
/*
 * Copyright 2015, Axel Dörfler, <axeld@pinc-software.de>.
 * Copyright 2013-2014, Stephan Aßmus <superstippi@gmx.de>.
 * Copyright 2013, Rene Gollent, rene@gollent.com.
 * Copyright 2013, Ingo Weinhold, ingo_weinhold@gmx.de.
 * Copyright 2016-2026, Andrew Lindesay <apl@lindesay.co.nz>.
 * Copyright 2017, Julian Harnath <julian.harnath@rwth-aachen.de>.
 * All rights reserved. Distributed under the terms of the MIT License.
 */


#include "MainWindow.h"

#include <algorithm>
#include <map>
#include <vector>

#include <stdio.h>

#include <Alert.h>
#include <Application.h>
#include <Autolock.h>
#include <Button.h>
#include <CardLayout.h>
#include <Catalog.h>
#include <LayoutBuilder.h>
#include <MenuBar.h>
#include <MenuItem.h>
#include <MessageRunner.h>
#include <Messenger.h>
#include <Roster.h>
#include <Screen.h>
#include <StopWatch.h>
#include <StringList.h>
#include <StringView.h>
#include <TabView.h>

#include "AppUtils.h"
#include "AutoLocker.h"
#include "DecisionProvider.h"
#include "FeaturedPackagesView.h"
#include "FilterView.h"
#include "IdentityAndAccessUtils.h"
#include "LocaleUtils.h"
#include "Logger.h"
#include "PackageInfoView.h"
#include "PackageListView.h"
#include "PackageManager.h"
#include "PackageUtils.h"
#include "ProcessCoordinator.h"
#include "ProcessCoordinatorFactory.h"
#include "RatePackageWindow.h"
#include "RatingUtils.h"
#include "ScreenshotWindow.h"
#include "ServerHelper.h"
#include "SettingsWindow.h"
#include "ShuttingDownWindow.h"
#include "ToLatestUserUsageConditionsWindow.h"
#include "UserLoginWindow.h"
#include "UserUsageConditionsWindow.h"
#include "WorkStatusView.h"
#include "support.h"


#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "MainWindow"


enum {
        MSG_REFRESH_REPOS                                                               = 'mrrp',
        MSG_MANAGE_REPOS                                                                = 'mmrp',
        MSG_SOFTWARE_UPDATER                                                    = 'mswu',
        MSG_SETTINGS                                                                    = 'stgs',
        MSG_LOG_IN                                                                              = 'lgin',
        MSG_AUTHORIZATION_CHANGED                                               = 'athc',
        MSG_PACKAGE_FILTER_CHANGED                                              = 'fpch',
        MSG_ICONS_CHANGED                                                               = 'icoc',
        MSG_CATEGORIES_LIST_CHANGED                                             = 'clic',
        MSG_PACKAGES_CHANGED                                                    = 'pchd',
        MSG_PROCESS_COORDINATOR_CHANGED                                 = 'pccd',
        MSG_WORK_STATUS_CHANGE                                                  = 'wsch',
        MSG_WORK_STATUS_CLEAR                                                   = 'wscl',
        MSG_INCREMENT_VIEW_COUNTER                                              = 'icrv',
        MSG_SCREENSHOT_CACHED                                                   = 'ssca',
        MSG_SELECTED_PACKAGE_CHANGED                                    = 'spch',
        MSG_PACKAGE_LIST_MODE_CHANGED                                   = 'pkmc',
        MSG_PACKAGE_LIST_MODE_TAB_CHANGED                               = 'cplm',
        MSG_SHOW_DESKTOP_PACKAGES                                               = 'sodk',
        MSG_SHOW_NATIVE_DESKTOP_PACKAGES                                = 'sond',
        MSG_SHOW_DESKTOP_AND_NON_DESKTOP_PACKAGES               = 'sdan',
        MSG_SHOW_AVAILABLE_PACKAGES                                             = 'savl',
        MSG_SHOW_INSTALLED_PACKAGES                                             = 'sins',
        MSG_SHOW_SOURCE_PACKAGES                                                = 'ssrc',
        MSG_SHOW_DEVELOP_PACKAGES                                               = 'sdvl',
        MSG_CAN_NICKNAME_PASSWORD_AUTHENTICATE_CHANGED  = 'cnpc'
};


const char* const main_window_keys::kKeyWindowSettings = "window_settings";

static const char* const kKeyWorkStatusText = "work_status_text";
static const char* const kKeyWorkStatusProgress = "work_status_progress";
static const char* const kKeyWindowFrame = "window frame";
        // use of spaces is historical
static const char* const kKeySinglePackageWindowFrame = "small window frame";
        // use of spaces is historical
static const char* const kKeyColumnSettings = "column settings";
        // use of spaces is historical


#define KEY_ERROR_STATUS                                "errorStatus"

const bigtime_t kIncrementViewCounterDelayMicros = 3 * 1000 * 1000;

#define TAB_PROMINENT_PACKAGES  0
#define TAB_ALL_PACKAGES                1

using namespace BPackageKit;
using namespace BPackageKit::BManager::BPrivate;


typedef std::map<BString, PackageInfoRef> PackageInfoMap;


struct RefreshWorkerParameters {
        MainWindow* window;
        bool forceRefresh;

        RefreshWorkerParameters(MainWindow* window, bool forceRefresh)
                :
                window(window),
                forceRefresh(forceRefresh)
        {
        }
};


class MainWindowModelListener : public ModelListener {
public:
        MainWindowModelListener(const BMessenger& messenger)
                :
                fMessenger(messenger)
        {
        }

        virtual void PackageFilterChanged()
        {
                if (fMessenger.IsValid())
                        fMessenger.SendMessage(MSG_PACKAGE_FILTER_CHANGED);
        }

        virtual void AuthorizationChanged()
        {
                if (fMessenger.IsValid())
                        fMessenger.SendMessage(MSG_AUTHORIZATION_CHANGED);
        }

        virtual void CategoryListChanged()
        {
                if (fMessenger.IsValid())
                        fMessenger.SendMessage(MSG_CATEGORIES_LIST_CHANGED);
        }

        virtual void ScreenshotCached(const ScreenshotCoordinate& coordinate)
        {
                if (fMessenger.IsValid()) {
                        BMessage message(MSG_SCREENSHOT_CACHED);
                        if (coordinate.Archive(&message) != B_OK)
                                debugger("unable to serialize a screenshot coordinate");
                        fMessenger.SendMessage(&message);
                }
        }

        virtual void IconsChanged()
        {
                if (fMessenger.IsValid())
                        fMessenger.SendMessage(MSG_ICONS_CHANGED);
        }

        virtual void SelectedPackageChanged()
        {
                if (fMessenger.IsValid())
                        fMessenger.SendMessage(MSG_SELECTED_PACKAGE_CHANGED);
        }

        virtual void PackageListViewModeChanged()
        {
                if (fMessenger.IsValid())
                        fMessenger.SendMessage(MSG_PACKAGE_LIST_MODE_CHANGED);
        }

        virtual void CanNicknamePasswordAuthenticateChanged()
        {
                if (fMessenger.IsValid())
                        fMessenger.SendMessage(MSG_CAN_NICKNAME_PASSWORD_AUTHENTICATE_CHANGED);
        }

private:
        BMessenger fMessenger;
};


class MainWindowPackageInfoListener : public PackageInfoListener {
public:
        MainWindowPackageInfoListener(MainWindow* mainWindow)
                :
                fMainWindow(mainWindow)
        {
        }

        ~MainWindowPackageInfoListener() {}

private:
                // PackageInfoListener
        virtual void PackagesChanged(const PackageChangeEvents& events)
        {
                fMainWindow->PackagesChanged(events);
        }

private:
        MainWindow* fMainWindow;
};


MainWindow::MainWindow(const BMessage& settings)
        :
        BWindow(BRect(50, 50, 650, 550), B_TRANSLATE_SYSTEM_NAME("HaikuDepot"), B_DOCUMENT_WINDOW_LOOK,
                B_NORMAL_WINDOW_FEEL, B_ASYNCHRONOUS_CONTROLS | B_AUTO_UPDATE_SIZE_LIMITS),
        fScreenshotWindow(NULL),
        fShuttingDownWindow(NULL),
        fUserMenu(NULL),
        fLogInItem(NULL),
        fLogOutItem(NULL),
        fUsersUserUsageConditionsMenuItem(NULL),
        fLatestUserUsageConditionsMenuItem(NULL),
        fModelListener(new MainWindowModelListener(BMessenger(this)), true),
        fCoordinator(NULL),
        fShouldCloseWhenNoProcessesToCoordinate(false),
        fSinglePackageMode(false),
        fIncrementViewCounterDelayedRunner(NULL)
{
        if ((fCoordinatorRunningSem = create_sem(1, "ProcessCoordinatorSem")) < B_OK)
                debugger("unable to create the process coordinator semaphore");

        _InitPreferredLanguage();

        fPackageInfoListener = PackageInfoListenerRef(new MainWindowPackageInfoListener(this), true);

        BMenuBar* menuBar = new BMenuBar("Main Menu");
        _BuildMenu(menuBar);

        BMenuBar* userMenuBar = new BMenuBar("User Menu");
        _BuildUserMenu(userMenuBar);
        set_small_font(userMenuBar);
        userMenuBar->SetExplicitMaxSize(BSize(B_SIZE_UNSET, menuBar->MaxSize().height));

        fFilterView = new FilterView();
        fFeaturedPackagesView = new FeaturedPackagesView(fModel);
        fPackageListView = new PackageListView(&fModel);
        fPackageInfoView = new PackageInfoView(&fModel);

        fSplitView = new BSplitView(B_VERTICAL, 5.0f);

        fWorkStatusView = new WorkStatusView("work status");
        fPackageListView->AttachWorkStatusView(fWorkStatusView);

        fListTabs
                = new TabView(BMessenger(this), BMessage(MSG_PACKAGE_LIST_MODE_TAB_CHANGED), "list tabs");
        fListTabs->AddTab(fFeaturedPackagesView);
        fListTabs->AddTab(fPackageListView);

        BLayoutBuilder::Group<>(this, B_VERTICAL, 0.0f)
                .AddGroup(B_HORIZONTAL, 0.0f)
                        .Add(menuBar, 1.0f)
                        .Add(userMenuBar, 0.0f)
                .End()
                .Add(fFilterView)
                .AddSplit(fSplitView)
                        .AddGroup(B_VERTICAL)
                                .Add(fListTabs)
                                .SetInsets(
                                        B_USE_DEFAULT_SPACING, 0.0f,
                                        B_USE_DEFAULT_SPACING, 0.0f)
                        .End()
                        .Add(fPackageInfoView)
                .End()
                .Add(fWorkStatusView)
        ;

        fSplitView->SetCollapsible(0, false);
        fSplitView->SetCollapsible(1, false);

        fModel.AddListener(fModelListener);
        fModel.AddPackageListener(fPackageInfoListener);

        BMessage columnSettings;
        if (settings.FindMessage(kKeyColumnSettings, &columnSettings) == B_OK)
                fPackageListView->LoadState(&columnSettings);

        _RestoreModelSettings(settings);
        _MaybePromptCanShareAnonymousUserData(settings);
        _RestoreNickname(settings);
        _UpdateAuthorization();
        _RestoreWindowFrame(settings);

        // start worker threads
        BPackageRoster().StartWatching(this, B_WATCH_PACKAGE_INSTALLATION_LOCATIONS);

        _AdoptModel();
        _StartServerRuntimeInformationVerify();
        _StartBulkLoad();
}


/*!     This constructor is used when the application is loaded for the purpose of
        viewing an HPKG file.
*/

MainWindow::MainWindow(const BMessage& settings, const PackageInfoRef package)
        :
        BWindow(BRect(50, 50, 650, 350), B_TRANSLATE_SYSTEM_NAME("HaikuDepot"), B_DOCUMENT_WINDOW_LOOK,
                B_NORMAL_WINDOW_FEEL, B_ASYNCHRONOUS_CONTROLS | B_AUTO_UPDATE_SIZE_LIMITS),
        fFeaturedPackagesView(NULL),
        fPackageListView(NULL),
        fWorkStatusView(NULL),
        fScreenshotWindow(NULL),
        fShuttingDownWindow(NULL),
        fUserMenu(NULL),
        fLogInItem(NULL),
        fLogOutItem(NULL),
        fUsersUserUsageConditionsMenuItem(NULL),
        fLatestUserUsageConditionsMenuItem(NULL),
        fModelListener(new MainWindowModelListener(BMessenger(this)), true),
        fCoordinator(NULL),
        fShouldCloseWhenNoProcessesToCoordinate(false),
        fSinglePackageMode(true),
        fIncrementViewCounterDelayedRunner(NULL)
{
        SetTitle(_WindowTitleForPackage(package));

        if ((fCoordinatorRunningSem = create_sem(1, "ProcessCoordinatorSem")) < B_OK)
                debugger("unable to create the process coordinator semaphore");

        _InitPreferredLanguage();

        fPackageInfoListener = PackageInfoListenerRef(new MainWindowPackageInfoListener(this), true);

        fFilterView = new FilterView();
        fPackageInfoView = new PackageInfoView(&fModel);
        fWorkStatusView = new WorkStatusView("work status");

        BLayoutBuilder::Group<>(this, B_VERTICAL)
                .Add(fPackageInfoView)
                .Add(fWorkStatusView)
                .SetInsets(0, B_USE_WINDOW_INSETS, 0, 0);

        fModel.AddListener(fModelListener);
        fModel.AddPackageListener(fPackageInfoListener);

        // add the single package into the model so that any internal
        // business logic is able to find the package.
        DepotInfoRef depot = DepotInfoBuilder()
                                                         .WithName(SINGLE_PACKAGE_DEPOT_NAME)
                                                         .WithIdentifier(SINGLE_PACKAGE_DEPOT_IDENTIFIER)
                                                         .BuildRef();

        PackageCoreInfoRef coreInfo = PackageCoreInfoBuilder(package->CoreInfo())
                                                                          .WithDepotName(SINGLE_PACKAGE_DEPOT_IDENTIFIER)
                                                                          .BuildRef();

        PackageInfoRef packageWithDepotIdentifier
                = PackageInfoBuilder(package).WithCoreInfo(coreInfo).BuildRef();

        fModel.SetDepots(depot);
        fModel.AddPackage(packageWithDepotIdentifier);

        // Restore settings
        _RestoreNickname(settings);
        _UpdateAuthorization();
        _RestoreWindowFrame(settings);

        fPackageInfoView->SetPackage(packageWithDepotIdentifier);

        // start worker threads
        BPackageRoster().StartWatching(this, B_WATCH_PACKAGE_INSTALLATION_LOCATIONS);
}


MainWindow::~MainWindow()
{
        _SpinUntilProcessCoordinatorComplete();
        delete_sem(fCoordinatorRunningSem);
        fCoordinatorRunningSem = 0;

        BPackageRoster().StopWatching(this);

        if (fScreenshotWindow != NULL) {
                if (fScreenshotWindow->Lock())
                        fScreenshotWindow->Quit();
        }

        if (fShuttingDownWindow != NULL) {
                if (fShuttingDownWindow->Lock())
                        fShuttingDownWindow->Quit();
        }

        // We must clear the model early to release references.
        fModel.Clear();
}


bool
MainWindow::QuitRequested()
{

        _StopProcessCoordinators();

        // If there are any processes in flight we need to be careful to make
        // sure that they are cleanly completed before actually quitting.  By
        // turning on the `fShouldCloseWhenNoProcessesToCoordinate` flag, when
        // the process coordination has completed then it will detect this and
        // quit again.

        {
                AutoLocker<BLocker> lock(&fCoordinatorLock);
                fShouldCloseWhenNoProcessesToCoordinate = true;

                if (fCoordinator != NULL) {
                        HDINFO("a coordinator is running --> will wait before quitting...");

                        if (fShuttingDownWindow == NULL)
                                fShuttingDownWindow = new ShuttingDownWindow(this);
                        fShuttingDownWindow->Show();

                        return false;
                }
        }

        BMessage settings;
        StoreSettings(settings);
        BMessage message(MSG_MAIN_WINDOW_CLOSED);
        message.AddMessage(main_window_keys::kKeyWindowSettings, &settings);
        be_app->PostMessage(&message);

        if (fShuttingDownWindow != NULL) {
                if (fShuttingDownWindow->Lock())
                        fShuttingDownWindow->Quit();
                fShuttingDownWindow = NULL;
        }

        return true;
}


void
MainWindow::MessageReceived(BMessage* message)
{
        switch (message->what) {
                case MSG_BULK_LOAD_DONE:
                {
                        int64 errorStatus64;
                        if (message->FindInt64(KEY_ERROR_STATUS, &errorStatus64) == B_OK)
                                _BulkLoadCompleteReceived(static_cast<status_t>(errorStatus64));
                        else
                                HDERROR("expected [%s] value in message", KEY_ERROR_STATUS);
                        break;
                }
                case B_SIMPLE_DATA:
                case B_REFS_RECEIVED:
                        // TODO: ?
                        break;

                case B_PACKAGE_UPDATE:
                        _HandleExternalPackageUpdateMessageReceived(message);
                        break;

                case MSG_REFRESH_REPOS:
                        _StartBulkLoad(true);
                        break;

                case MSG_WORK_STATUS_CLEAR:
                        _HandleWorkStatusClear();
                        break;

                case MSG_WORK_STATUS_CHANGE:
                        _HandleWorkStatusChangeMessageReceived(message);
                        break;

                case MSG_MANAGE_REPOS:
                        be_roster->Launch("application/x-vnd.Haiku-Repositories");
                        break;

                case MSG_SOFTWARE_UPDATER:
                        be_roster->Launch("application/x-vnd.haiku-softwareupdater");
                        break;

                case MSG_LOG_IN:
                        if (!fModel.CanNicknamePasswordAuthenticate())
                                HDFATAL("nickname and password authentication is disabled");
                        _OpenLoginWindow(BMessage());
                        break;

                case MSG_SETTINGS:
                        _OpenSettingsWindow();
                        break;

                case MSG_LOG_OUT:
                        _HandleLogout();
                        break;

                case MSG_VIEW_LATEST_USER_USAGE_CONDITIONS:
                        _ViewUserUsageConditions(LATEST);
                        break;

                case MSG_VIEW_USERS_USER_USAGE_CONDITIONS:
                        _ViewUserUsageConditions(USER);
                        break;

                case MSG_AUTHORIZATION_CHANGED:
                        if (!fSinglePackageMode)
                                _StartUserVerify();
                        _UpdateAuthorization();
                        break;

                case MSG_SELECTED_PACKAGE_CHANGED:
                        _HandleSelectedPackageChanged();
                        break;

                case MSG_ICONS_CHANGED:
                        _HandleIconsChanged();
                        break;

                case MSG_SCREENSHOT_CACHED:
                        _HandleScreenshotCached(message);
                        break;

                case MSG_CATEGORIES_LIST_CHANGED:
                        fFilterView->AdoptModel(fModel);
                        break;

                case MSG_PACKAGE_FILTER_CHANGED:
                        // This message is originating from the model via a listener and
                        // signals that specification of the package filter have changed
                        // so that the application should re-filter the packages.
                        _AdoptModel();
                        break;

                case MSG_PACKAGE_LIST_MODE_CHANGED:
                        // This message will originate from a change in the model.
                        _AdoptModelControls();
                        break;

                case MSG_PACKAGE_LIST_MODE_TAB_CHANGED:
                        // This message fired when the user chooses one of list view mode
                        // tabs. It is happening as a result of a UI interaction rather
                        // than a model change; the model change is handled on a different
                        // event.
                        _PackageListViewRevokeFocus();
                        _HandleChangePackageListViewMode();
                        break;

                case MSG_SHOW_DESKTOP_PACKAGES:
                        _SetPackageDesktopFilterMode(DESKTOP);
                        break;

                case MSG_SHOW_NATIVE_DESKTOP_PACKAGES:
                        _SetPackageDesktopFilterMode(NATIVE_DESKTOP);
                        break;

                case MSG_SHOW_DESKTOP_AND_NON_DESKTOP_PACKAGES:
                        _SetPackageDesktopFilterMode(DESKTOP_AND_NON_DESKTOP);
                        break;

                case MSG_SHOW_AVAILABLE_PACKAGES:
                {
                        fModel.SetFilterSpecification(
                                PackageFilterSpecificationBuilder(fModel.FilterSpecification())
                                        .WithShowAvailablePackages(!fShowAvailablePackagesItem->IsMarked())
                                        // inverse because it should be toggled
                                        .BuildRef());
                        break;
                }

                case MSG_SHOW_INSTALLED_PACKAGES:
                {
                        fModel.SetFilterSpecification(
                                PackageFilterSpecificationBuilder(fModel.FilterSpecification())
                                        .WithShowInstalledPackages(!fShowInstalledPackagesItem->IsMarked())
                                                // inverse because it should be toggled
                                        .BuildRef());
                        break;
                }

                case MSG_SHOW_SOURCE_PACKAGES:
                {
                        fModel.SetFilterSpecification(
                                PackageFilterSpecificationBuilder(fModel.FilterSpecification())
                                        .WithShowSourcePackages(!fShowSourcePackagesItem->IsMarked())
                                        // inverse because it should be toggled
                                        .BuildRef());
                        break;
                }

                case MSG_SHOW_DEVELOP_PACKAGES:
                {
                        fModel.SetFilterSpecification(
                                PackageFilterSpecificationBuilder(fModel.FilterSpecification())
                                        .WithShowDevelopPackages(!fShowDevelopPackagesItem->IsMarked())
                                        // inverse because it should be toggled
                                        .BuildRef());
                        break;
                }

                case MSG_INCREMENT_VIEW_COUNTER:
                        _HandleIncrementViewCounter(message);
                        break;

                case MSG_CATEGORY_SELECTED:
                {
                        BString code;
                        if (message->FindString(shared_message_keys::kKeyCode, &code) != B_OK)
                                code = "";
                        fModel.SetFilterSpecification(
                                PackageFilterSpecificationBuilder(fModel.FilterSpecification())
                                        .WithCategory(code)
                                        .BuildRef());
                        break;
                }

                case MSG_DEPOT_SELECTED:
                {
                        BString name;
                        if (message->FindString(shared_message_keys::kKeyDepotName, &name) != B_OK)
                                name = "";
                        fModel.SetFilterSpecification(
                                PackageFilterSpecificationBuilder(fModel.FilterSpecification())
                                        .WithDepotName(name)
                                        .BuildRef());
                        _UpdateAvailableRepositories();
                        break;
                }

                case MSG_SEARCH_TERMS_MODIFIED:
                {
                        // TODO: Do this with a delay!
                        BString searchTerms;
                        if (message->FindString(filter_view_keys::kKeySearchTerms, &searchTerms) != B_OK)
                                searchTerms = "";
                        fModel.SetFilterSpecification(
                                PackageFilterSpecificationBuilder(fModel.FilterSpecification())
                                        .WithSearchTerms(searchTerms)
                                        .BuildRef());
                        break;
                }

                case MSG_PACKAGES_CHANGED:
                        _HandlePackagesChanged(message);
                        break;

                case MSG_PROCESS_COORDINATOR_CHANGED:
                {
                        ProcessCoordinatorState state(message);
                        _HandleProcessCoordinatorChanged(state);
                        break;
                }

                case MSG_PKG_INSTALL:
                {
                        InstallPackageAction action(message);
                        ProcessCoordinator* coordinator
                                = ProcessCoordinatorFactory::CreateInstallPackageActionCoordinator(&fModel, action);
                        _AddProcessCoordinator(coordinator);
                        break;
                }

                case MSG_PKG_UNINSTALL:
                {
                        UninstallPackageAction action(message);
                        ProcessCoordinator* coordinator
                                = ProcessCoordinatorFactory::CreateUninstallPackageActionCoordinator(&fModel,
                                        action);
                        _AddProcessCoordinator(coordinator);
                        break;
                }

                case MSG_PKG_CACHE_SCREENSHOT:
                {
                        CacheScreenshotPackageAction action(message);
                        ProcessCoordinator* coordinator
                                = ProcessCoordinatorFactory::CreateCacheScreenshotPackageActionCoordinator(&fModel,
                                        action);
                        _AddProcessCoordinator(coordinator);
                        break;
                }

                case MSG_PKG_POPULATE_CHANGELOG:
                {
                        PopulateChangelogPackageAction action(message);
                        ProcessCoordinator* coordinator
                                = ProcessCoordinatorFactory::CreatePopulateChangelogPackageActionCoordinator(
                                        &fModel, action);
                        _AddProcessCoordinator(coordinator);
                        break;
                }

                case MSG_PKG_POPULATE_USER_RATINGS:
                {
                        PopulateUserRatingsPackageAction action(message);
                        ProcessCoordinator* coordinator
                                = ProcessCoordinatorFactory::CreatePopulateUserRatingsPackageActionCoordinator(
                                        &fModel, action);
                        _AddProcessCoordinator(coordinator);
                        break;
                }

                case MSG_PKG_OPEN:
                {
                        OpenPackageAction action(message);
                        ProcessCoordinator* coordinator
                                = ProcessCoordinatorFactory::CreateOpenPackageActionCoordinator(&fModel, action);
                        _AddProcessCoordinator(coordinator);
                        break;
                }

                case MSG_RATE_PACKAGE:
                        _RatePackage();
                        break;

                case MSG_SHOW_SCREENSHOT:
                        _ShowScreenshot();
                        break;

                case MSG_PACKAGE_WORKER_BUSY:
                {
                        BString reason;
                        status_t status = message->FindString("reason", &reason);
                        if (status != B_OK)
                                break;
                        fWorkStatusView->SetBusy(reason);
                        break;
                }

                case MSG_PACKAGE_WORKER_IDLE:
                        fWorkStatusView->SetIdle();
                        break;

                case MSG_USER_USAGE_CONDITIONS_NOT_LATEST:
                {
                        BMessage userDetailMsg;
                        if (message->FindMessage("userDetail", &userDetailMsg) != B_OK)
                                debugger("expected the [userDetail] data to be carried in the message.");
                        UserDetail userDetail(&userDetailMsg);
                        _HandleUserUsageConditionsNotLatest(userDetail);
                        break;
                }

                case MSG_SHOW_ALL_PACKAGES_TAB:
                        // An example of where this might be sent is the hyperlink in the
                        // featured packages view where there is a link to the all packages
                        // view if the list is empty. By setting this on the model, an
                        // event will return back to this class to switch the tabs.
                        fModel.SetPackageListViewMode(ALL);
                        break;

                case MSG_CAN_NICKNAME_PASSWORD_AUTHENTICATE_CHANGED:
                        // A check with the server will have verified if this is possible or
                        // not from the server side.
                        _UpdateAuthorization();
                        break;

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


static const char*
main_window_package_list_view_mode_str(package_list_view_mode mode)
{
        if (mode == PROMINENT)
                return "PROMINENT";
        return "ALL";
}


static package_list_view_mode
main_window_str_to_package_list_view_mode(const BString& str)
{
        if (str == "PROMINENT")
                return PROMINENT;
        return ALL;
}


void
MainWindow::StoreSettings(BMessage& settings)
{
        settings.AddRect(_WindowFrameName(), Frame());
        if (!fSinglePackageMode) {
                settings.AddRect(kKeyWindowFrame, Frame());

                BMessage columnSettings;
                if (fPackageListView != NULL)
                        fPackageListView->SaveState(&columnSettings);

                settings.AddMessage(kKeyColumnSettings, &columnSettings);

                settings.AddString(SETTING_PACKAGE_LIST_VIEW_MODE,
                        main_window_package_list_view_mode_str(fModel.PackageListViewMode()));

                settings.AddBool(SETTING_SHOW_DESKTOP_PACKAGES,
                        fModel.FilterSpecification()->ShowOnlyDesktopPackages());
                settings.AddBool(SETTING_SHOW_NATIVE_DESKTOP_PACKAGES,
                        fModel.FilterSpecification()->ShowOnlyNativeDesktopPackages());
                settings.AddBool(SETTING_SHOW_AVAILABLE_PACKAGES,
                        fModel.FilterSpecification()->ShowAvailablePackages());
                settings.AddBool(SETTING_SHOW_INSTALLED_PACKAGES,
                        fModel.FilterSpecification()->ShowInstalledPackages());
                settings.AddBool(SETTING_SHOW_DEVELOP_PACKAGES,
                        fModel.FilterSpecification()->ShowDevelopPackages());
                settings.AddBool(SETTING_SHOW_SOURCE_PACKAGES,
                        fModel.FilterSpecification()->ShowSourcePackages());

                settings.AddBool(SETTING_CAN_SHARE_ANONYMOUS_USER_DATA,
                        fModel.CanShareAnonymousUsageData());
        }

        settings.AddString(SETTING_NICKNAME, fModel.Nickname());
}


/*!     Informs the window that the selected package has changed. This gives the
        window the opportunity to update the UI accordingly.
*/
void
MainWindow::_HandleSelectedPackageChanged()
{
        PackageInfoRef package = fModel.SelectedPackage();

        if (package.IsSet()) {
                _AdoptPackage(package);
                _SetupDelayedIncrementViewCounter(package);
        } else {
                _ClearPackage();
        }
}


/*!     This method is invoked in the situation that a package changes in the model.

        The events are processed into a `BMessage` which is then posted to the
        window. The message is then trans-coded back into events for processing
        in the GUI.
*/
void
MainWindow::PackagesChanged(const PackageChangeEvents& events)
{
        BMessage message(MSG_PACKAGES_CHANGED);
        status_t result = events.Archive(&message);

        if (result == B_OK)
                PostMessage(&message);
        else
                HDERROR("unable to archive the package into events to a message");
}


/*!     This method is invoked as a result of the packages changing and the
        `MainWindow` finding out about this in the UI thread. From here it
        is able to inform other elements of the GUI about the change.
*/
void
MainWindow::_HandlePackagesChanged(const BMessage* message)
{
        PackageChangeEvents packageEvents(message);

        // if there are no events to process then drop.

        if (packageEvents.IsEmpty()) {
                HDINFO("window encountered an empty packages changed");
                return;
        }

        bool isFeaturedTabEnabled
                = !fSinglePackageMode && fListTabs->TabAt(TAB_PROMINENT_PACKAGES)->IsEnabled();

        HDTRACE("window processing %" B_PRIi32 " package changes", packageEvents.CountEvents());

        // Transfer the package change events into package *info* change events so
        // that the immutable PackageInfo instances can be used in the processing
        // without having to continuously query the model causing lock overhead.

        std::vector<PackageInfoChangeEvent> packageInfoEvents;

        for (int32 i = packageEvents.CountEvents() - 1; i >= 0; i--) {
                const PackageChangeEvent packageEvent = packageEvents.EventAtIndex(i);
                const PackageInfoRef packageInfo = fModel.PackageForName(packageEvent.PackageName());

                if (packageInfo.IsSet()) {
                        if (!fSinglePackageMode && !isFeaturedTabEnabled
                                && !PackageUtils::IsProminent(packageInfo)) {
                                fListTabs->TabAt(TAB_PROMINENT_PACKAGES)->SetEnabled(true);
                                isFeaturedTabEnabled = true;
                        }

                        const PackageInfoChangeEvent packageInfoEvent(packageInfo, packageEvent.Changes());
                        packageInfoEvents.push_back(packageInfoEvent);
                } else {
                        HDERROR("package [%s] from change event not found",
                                packageEvent.PackageName().String());
                }
        }

        // now process the messages by adding and removing them from lists.

        if (!fSinglePackageMode) {
                uint32 watchedChanges = PKG_CHANGED_LOCAL_INFO | PKG_CHANGED_CLASSIFICATION;
                PackageFilterRef filter = fModel.Filter();

                std::vector<PackageInfoRef> addedPackages;
                std::vector<PackageInfoRef> removedPackages;
                std::vector<PackageInfoRef> addedFeaturedPackages;
                std::vector<PackageInfoRef> removedFeaturedPackages;

                std::vector<PackageInfoChangeEvent>::const_iterator it;

                for (it = packageInfoEvents.begin(); it != packageInfoEvents.end(); it++) {
                        const PackageInfoChangeEvent packageInfoEvent = *it;

                        if (packageInfoEvent.Changes() & watchedChanges) {
                                const PackageInfoRef package = packageInfoEvent.Package();

                                if (package.IsSet()) {
                                        const bool isProminent = PackageUtils::IsProminent(package);

                                        if (filter->AcceptsPackage(package)) {
                                                addedPackages.push_back(package);

                                                if (isProminent)
                                                        addedFeaturedPackages.push_back(package);
                                        } else {
                                                removedPackages.push_back(package);

                                                if (isProminent)
                                                        removedFeaturedPackages.push_back(package);
                                        }
                                } else {
                                        HDFATAL("package change event for missing package");
                                                // should have checked earlier so this is an illegal state
                                }
                        }
                }

                fPackageListView->AddRemovePackages(addedPackages, removedPackages);
                fFeaturedPackagesView->AddRemovePackages(addedFeaturedPackages, removedFeaturedPackages);
        }

        // Now relay the changes to the other UI elements. This seems a bit strange
        // because we're controlling which packages are in the lists above, but then
        // updating anyway. This makes sense because the lists themselves should not
        // determine which packages are assigned.

        if (!fSinglePackageMode) {
                fFeaturedPackagesView->HandlePackagesChanged(packageInfoEvents);
                fPackageListView->HandlePackagesChanged(packageInfoEvents);
        }

        fPackageInfoView->HandlePackagesChanged(packageInfoEvents);
}


void
MainWindow::_BuildMenu(BMenuBar* menuBar)
{
        BMenu* menu = new BMenu(B_TRANSLATE_SYSTEM_NAME("HaikuDepot"));
        fRefreshRepositoriesItem
                = new BMenuItem(B_TRANSLATE("Refresh repositories"), new BMessage(MSG_REFRESH_REPOS));
        menu->AddItem(fRefreshRepositoriesItem);
        menu->AddItem(new BMenuItem(B_TRANSLATE("Manage repositories" B_UTF8_ELLIPSIS),
                new BMessage(MSG_MANAGE_REPOS)));
        menu->AddItem(new BMenuItem(B_TRANSLATE("Check for updates" B_UTF8_ELLIPSIS),
                new BMessage(MSG_SOFTWARE_UPDATER)));
        menu->AddSeparatorItem();
        menu->AddItem(
                new BMenuItem(B_TRANSLATE("Settings" B_UTF8_ELLIPSIS), new BMessage(MSG_SETTINGS), ','));
        menu->AddItem(new BMenuItem(B_TRANSLATE("Quit"), new BMessage(B_QUIT_REQUESTED), 'Q'));
        menuBar->AddItem(menu);

        fRepositoryMenu = new BMenu(B_TRANSLATE("Repositories"));
        menuBar->AddItem(fRepositoryMenu);

        menu = new BMenu(B_TRANSLATE("Show"));

        fShowDesktopAndNonDesktopItem = new BMenuItem(B_TRANSLATE("Desktop and non-desktop packages"),
                new BMessage(MSG_SHOW_DESKTOP_AND_NON_DESKTOP_PACKAGES));
        menu->AddItem(fShowDesktopAndNonDesktopItem);

        fShowOnlyDesktopItem
                = new BMenuItem(B_TRANSLATE("Desktop packages only"), new BMessage(MSG_SHOW_DESKTOP_PACKAGES));
        menu->AddItem(fShowOnlyDesktopItem);

        fShowOnlyNativeDesktopItem = new BMenuItem(B_TRANSLATE("Native desktop packages only"),
                new BMessage(MSG_SHOW_NATIVE_DESKTOP_PACKAGES));
        menu->AddItem(fShowOnlyNativeDesktopItem);

        menu->AddSeparatorItem();

        fShowAvailablePackagesItem = new BMenuItem(B_TRANSLATE("Available packages"),
                new BMessage(MSG_SHOW_AVAILABLE_PACKAGES));
        menu->AddItem(fShowAvailablePackagesItem);

        fShowInstalledPackagesItem = new BMenuItem(B_TRANSLATE("Installed packages"),
                new BMessage(MSG_SHOW_INSTALLED_PACKAGES));
        menu->AddItem(fShowInstalledPackagesItem);

        menu->AddSeparatorItem();

        fShowDevelopPackagesItem
                = new BMenuItem(B_TRANSLATE("Develop packages"), new BMessage(MSG_SHOW_DEVELOP_PACKAGES));
        menu->AddItem(fShowDevelopPackagesItem);

        fShowSourcePackagesItem
                = new BMenuItem(B_TRANSLATE("Source packages"), new BMessage(MSG_SHOW_SOURCE_PACKAGES));
        menu->AddItem(fShowSourcePackagesItem);

        menuBar->AddItem(menu);
}


void
MainWindow::_BuildUserMenu(BMenuBar* menuBar)
{
        fUserMenu = new BMenu(B_TRANSLATE("Not logged in"));

        fLogInItem = new BMenuItem(B_TRANSLATE("Log in" B_UTF8_ELLIPSIS), new BMessage(MSG_LOG_IN));
        fUserMenu->AddItem(fLogInItem);

        fLogOutItem = new BMenuItem(B_TRANSLATE("Log out"), new BMessage(MSG_LOG_OUT));
        fUserMenu->AddItem(fLogOutItem);

        fLatestUserUsageConditionsMenuItem
                = new BMenuItem(B_TRANSLATE("View latest usage conditions" B_UTF8_ELLIPSIS),
                        new BMessage(MSG_VIEW_LATEST_USER_USAGE_CONDITIONS));
        fUserMenu->AddItem(fLatestUserUsageConditionsMenuItem);

        fUsersUserUsageConditionsMenuItem
                = new BMenuItem(B_TRANSLATE("View agreed usage conditions" B_UTF8_ELLIPSIS),
                        new BMessage(MSG_VIEW_USERS_USER_USAGE_CONDITIONS));
        fUserMenu->AddItem(fUsersUserUsageConditionsMenuItem);

        menuBar->AddItem(fUserMenu);
}


void
MainWindow::_RestoreNickname(const BMessage& settings)
{
        BString nickname;
        if (settings.FindString(SETTING_NICKNAME, &nickname) == B_OK && nickname.Length() > 0) {
                UserCredentials credentials;
                if (IdentityAndAccessUtils::RetrieveCredentials(nickname, credentials) == B_OK) {
                        fModel.SetCredentials(credentials);
                        HDINFO("credentials restored");
                } else {
                        HDERROR("the credentials are unable to be restored");
                }
        } else {
                HDINFO("no credentials to restore");
        }
}


const char*
MainWindow::_WindowFrameName() const
{
        if (fSinglePackageMode)
                return kKeySinglePackageWindowFrame;

        return kKeyWindowFrame;
}


void
MainWindow::_RestoreWindowFrame(const BMessage& settings)
{
        BRect frame = Frame();

        BRect windowFrame;
        bool fromSettings = false;
        if (settings.FindRect(_WindowFrameName(), &windowFrame) == B_OK) {
                frame = windowFrame;
                fromSettings = true;
        } else if (!fSinglePackageMode) {
                // Resize to occupy a certain screen size
                BRect screenFrame = BScreen(this).Frame();
                float width = frame.Width();
                float height = frame.Height();
                if (width < screenFrame.Width() * .666f && height < screenFrame.Height() * .666f) {
                        frame.bottom = frame.top + screenFrame.Height() * .666f;
                        frame.right = frame.left + std::min(screenFrame.Width() * .666f, height * 7 / 5);
                }
        }

        MoveTo(frame.LeftTop());
        ResizeTo(frame.Width(), frame.Height());

        if (fromSettings)
                MoveOnScreen();
        else
                CenterOnScreen();
}


void
MainWindow::_RestoreModelSettings(const BMessage& settings)
{
        BString packageListViewMode;
        if (settings.FindString(SETTING_PACKAGE_LIST_VIEW_MODE, &packageListViewMode) == B_OK) {
                fModel.SetPackageListViewMode(
                        main_window_str_to_package_list_view_mode(packageListViewMode));
        }

        bool showOption;
        PackageFilterSpecificationBuilder filterSpecificationBuilder;

        if (settings.FindBool(SETTING_SHOW_DESKTOP_PACKAGES, &showOption) == B_OK)
                filterSpecificationBuilder.WithShowOnlyDesktopPackages(showOption);
        if (settings.FindBool(SETTING_SHOW_NATIVE_DESKTOP_PACKAGES, &showOption) == B_OK)
                filterSpecificationBuilder.WithShowOnlyNativeDesktopPackages(showOption);
        if (settings.FindBool(SETTING_SHOW_AVAILABLE_PACKAGES, &showOption) == B_OK)
                filterSpecificationBuilder.WithShowAvailablePackages(showOption);
        if (settings.FindBool(SETTING_SHOW_INSTALLED_PACKAGES, &showOption) == B_OK)
                filterSpecificationBuilder.WithShowInstalledPackages(showOption);
        if (settings.FindBool(SETTING_SHOW_DEVELOP_PACKAGES, &showOption) == B_OK)
                filterSpecificationBuilder.WithShowDevelopPackages(showOption);
        if (settings.FindBool(SETTING_SHOW_SOURCE_PACKAGES, &showOption) == B_OK)
                filterSpecificationBuilder.WithShowSourcePackages(showOption);

        fModel.SetFilterSpecification(filterSpecificationBuilder.BuildRef());

        if (settings.FindBool(SETTING_CAN_SHARE_ANONYMOUS_USER_DATA, &showOption) == B_OK)
                fModel.SetCanShareAnonymousUsageData(showOption);
}


void
MainWindow::_MaybePromptCanShareAnonymousUserData(const BMessage& settings)
{
        bool showOption;
        if (settings.FindBool(SETTING_CAN_SHARE_ANONYMOUS_USER_DATA, &showOption) == B_NAME_NOT_FOUND)
                _PromptCanShareAnonymousUserData();
}


void
MainWindow::_PromptCanShareAnonymousUserData()
{
        BAlert* alert = new(std::nothrow) BAlert(B_TRANSLATE("Sending anonymous usage data"),
                B_TRANSLATE("Would it be acceptable to send anonymous usage data to the"
                                        " HaikuDepotServer system from this computer? You can change your"
                                        " preference in the \"Settings\" window later."),
                B_TRANSLATE("Yes"), B_TRANSLATE("No"));

        int32 result = alert->Go();
        fModel.SetCanShareAnonymousUsageData(0 == result);
}


/*!     This method will be invoked before reference data is loaded in; it is an
        initialization process.
*/
void
MainWindow::_InitPreferredLanguage()
{
        // TODO (apl) it would be nice to pre-detect the presence of a reference data
        //    file and load this early. See the logic in `ServerReferenceDataUpdateProcess`
        //    for details.
        const std::vector<LanguageRef> existingLanguages = fModel.Languages();
        LanguageRef defaultLanguage = LocaleUtils::DeriveDefaultLanguage(existingLanguages);
        std::vector<LanguageRef> languages(existingLanguages.begin(), existingLanguages.end());

        if (std::find(languages.begin(), languages.end(), defaultLanguage) == languages.end())
                languages.push_back(defaultLanguage);

        fModel.SetLanguagesAndPreferred(languages, defaultLanguage);
}


void
MainWindow::_SetPackageDesktopFilterMode(PackageDesktopFilterMode mode)
{
        fModel.SetFilterSpecification(PackageFilterSpecificationBuilder(fModel.FilterSpecification())
                        .WithShowOnlyNativeDesktopPackages(mode == NATIVE_DESKTOP)
                        .WithShowOnlyDesktopPackages(mode == DESKTOP)
                        .BuildRef());
}


void
MainWindow::_AdoptModelControls()
{
        if (fSinglePackageMode)
                return;

        {
                PackagesSummary packagesSummary = fModel.GeneratePackagesSummary();
                fShowOnlyDesktopItem->SetEnabled(packagesSummary.HasAnyDesktop());
                fShowOnlyNativeDesktopItem->SetEnabled(packagesSummary.HasAnyNativeDesktop());
                fListTabs->TabAt(TAB_PROMINENT_PACKAGES)
                        ->SetEnabled(packagesSummary.HasAnyProminentPackages());
        }

        PackageFilterSpecificationRef packageFilterSpecification = fModel.FilterSpecification();

        fShowOnlyDesktopItem->SetMarked(packageFilterSpecification->ShowOnlyDesktopPackages());
        fShowOnlyNativeDesktopItem->SetMarked(
                packageFilterSpecification->ShowOnlyNativeDesktopPackages());
        fShowDesktopAndNonDesktopItem->SetMarked(!packageFilterSpecification->ShowOnlyDesktopPackages()
                && !packageFilterSpecification->ShowOnlyNativeDesktopPackages());

        fShowAvailablePackagesItem->SetMarked(packageFilterSpecification->ShowAvailablePackages());
        fShowInstalledPackagesItem->SetMarked(packageFilterSpecification->ShowInstalledPackages());
        fShowSourcePackagesItem->SetMarked(packageFilterSpecification->ShowSourcePackages());
        fShowDevelopPackagesItem->SetMarked(packageFilterSpecification->ShowDevelopPackages());

        switch (fModel.PackageListViewMode()) {
                case PROMINENT:
                        fListTabs->Select(TAB_PROMINENT_PACKAGES);
                        // if the currently selected package is not in the prominent list then it should
                        // be removed.
                        if (!PackageUtils::IsProminent(fModel.SelectedPackage()))
                                fModel.ClearSelectedPackage();
                        break;
                case ALL:
                        fListTabs->Select(TAB_ALL_PACKAGES);
                        break;
                default:
                        HDFATAL("attempt to select unknown tab");
                        break;
        }

        fFilterView->AdoptModel(fModel);
}


void
MainWindow::_PackageListViewRevokeFocus()
{
        BView* currentFocusView = CurrentFocus();
        if (currentFocusView != NULL && _IsPackageListView(currentFocusView)) {
                currentFocusView->MakeFocus(false);
                HDINFO("revoked focus on package list view");
        }
}


/*!     Returns true if the view supplied is a sub-view of one of the package list
 *      views.
 */
bool
MainWindow::_IsPackageListView(BView* view)
{
        if (view == NULL)
                return false;

        BView* packageListViews[2] = {fFeaturedPackagesView, fPackageListView};

        for (int i = 0; i < 2; i++) {
                BView* v = view;

                while (v != NULL) {
                        if (v == packageListViews[i])
                                return true;
                        v = v->Parent();
                }
        }

        return false;
}


void
MainWindow::_AdoptModel()
{
        BStopWatch stopWatch("adopt_model", true);
        HDTRACE("adopting model to main window ui");

        if (fSinglePackageMode)
                return;

        bool retainSelectedPackage = false;
        PackageInfoRef selectedPackage = fModel.SelectedPackage();

        // This list will contain all the packages that are under consideration.
        // Create a new list which only contains the featured packages.

        const std::vector<PackageInfoRef> packages = _CreateSnapshotOfFilteredPackages();
        std::vector<PackageInfoRef> featuredPackages;
        std::vector<PackageInfoRef>::const_iterator it;

        for (it = packages.begin(); it != packages.end(); it++) {
                PackageInfoRef package = *it;

                if (PackageUtils::IsProminent(package))
                        featuredPackages.push_back(package);

                // The fact that it is prominent or not also determines if it should
                // remain selected, but this is handled in _AdoptModelControls() rather
                // than here.

                if (!retainSelectedPackage && selectedPackage.IsSet()
                        && selectedPackage->Name() == package->Name()) {
                        retainSelectedPackage = true;
                }
        }

        // If the selected package is not in the filtered packages then drop the
        // selection.

        if (!retainSelectedPackage && selectedPackage.IsSet()) {
                HDINFO("selected package not in filtered list; will unset.");
                fModel.ClearSelectedPackage();
        }

        // Now get the UI elements which display those packages to only remain the
        // filtered list.

        fPackageListView->RetainPackages(packages);
        fFeaturedPackagesView->RetainPackages(featuredPackages);

        _AdoptModelControls();

        double durationSeconds = ((double)stopWatch.ElapsedTime() / 1000000.0);
        HDDEBUG("did adopt model in %6.3f seconds", durationSeconds);
}


void
MainWindow::_SetupDelayedIncrementViewCounter(const PackageInfoRef package)
{
        if (fIncrementViewCounterDelayedRunner != NULL) {
                fIncrementViewCounterDelayedRunner->SetCount(0);
                delete fIncrementViewCounterDelayedRunner;
        }
        BMessage message(MSG_INCREMENT_VIEW_COUNTER);
        message.SetString(shared_message_keys::kKeyPackageName, package->Name());
        fIncrementViewCounterDelayedRunner
                = new BMessageRunner(BMessenger(this), &message, kIncrementViewCounterDelayMicros, 1);
        if (fIncrementViewCounterDelayedRunner->InitCheck() != B_OK)
                HDERROR("unable to init the increment view counter");
}


void
MainWindow::_HandleIncrementViewCounter(const BMessage* message)
{
        BString name;
        if (message->FindString(shared_message_keys::kKeyPackageName, &name) == B_OK) {
                const PackageInfoRef& viewedPackage = fPackageInfoView->Package();
                if (viewedPackage.IsSet()) {
                        const BString& viewedPackageName = viewedPackage->Name();

                        if (viewedPackageName == name)
                                _IncrementViewCounter(viewedPackageName);
                        else
                                HDINFO("incr. view counter; name mismatch");
                } else {
                        HDINFO("incr. view counter; no viewed pkg");
                }
        } else {
                HDERROR("incr. view counter; no name");
        }
        fIncrementViewCounterDelayedRunner->SetCount(0);
        delete fIncrementViewCounterDelayedRunner;
        fIncrementViewCounterDelayedRunner = NULL;
}


void
MainWindow::_IncrementViewCounter(const BString& packageName)
{
        const char* packageNameStr = packageName.String();
        const PackageInfoRef package = fModel.PackageForName(packageName);

        if (!package.IsSet()) {
                HDERROR("the package [%s] it not available -- won't increment the view counter",
                        packageNameStr);
                return;
        }

        bool canShareAnonymousUsageData = fModel.CanShareAnonymousUsageData();

        // The logic here checks to see that the user has approved that their viewing of the package
        // can be anonymously recorded as a metric. We also check that within this session, the
        // package has not already been incremented as we count a viewing of a package within a
        // session of HaikuDepot desktop application as unique.

        if (canShareAnonymousUsageData && !PackageUtils::Viewed(package)) {
                if (ServerHelper::IsNetworkAvailable()) {
                        PackageLocalInfoRef localInfoRef
                                = PackageLocalInfoBuilder(package->LocalInfo()).WithViewed().BuildRef();
                        fModel.AddPackage(PackageInfoBuilder(package).WithLocalInfo(localInfoRef).BuildRef());

                        ProcessCoordinator* incrementViewCoordinator
                                = ProcessCoordinatorFactory::CreateIncrementViewCounter(&fModel, package);
                        _AddProcessCoordinator(incrementViewCoordinator);

                        HDINFO("pkg [%s] will increment counter", packageNameStr);
                } else {
                        HDINFO("pkg [%s] won't increment counter; network unavailable", packageNameStr);
                }
        }
}


void
MainWindow::_AdoptPackage(const PackageInfoRef& package)
{
        fPackageInfoView->SetPackage(package);

        if (!fSinglePackageMode) {
                if (fFeaturedPackagesView != NULL)
                        fFeaturedPackagesView->SelectPackage(package);
                if (fPackageListView != NULL)
                        fPackageListView->SelectPackage(package);
        }
}


void
MainWindow::_ClearPackage()
{
        fPackageInfoView->Clear();
}


void
MainWindow::_StartBulkLoad(bool force)
{
        if (!fSinglePackageMode) {
                if (fFeaturedPackagesView != NULL)
                        fFeaturedPackagesView->Clear();
                if (fPackageListView != NULL)
                        fPackageListView->Clear();
        }

        fPackageInfoView->Clear();
        fFeaturedPackagesView->SetLoading(true);

        fRefreshRepositoriesItem->SetEnabled(false);
        ProcessCoordinator* bulkLoadCoordinator
                = ProcessCoordinatorFactory::CreateBulkLoadCoordinator(&fModel, force);
        _AddProcessCoordinator(bulkLoadCoordinator);
}


void
MainWindow::_BulkLoadCompleteReceived(status_t errorStatus)
{
        fFeaturedPackagesView->SetLoading(false);
        fRefreshRepositoriesItem->SetEnabled(true);
        PackagesSummary packagesSummary = fModel.GeneratePackagesSummary();

        if (errorStatus != B_OK) {
                AppUtils::NotifySimpleError(SimpleAlert(B_TRANSLATE("Package update error"),
                        B_TRANSLATE("While updating package data, a problem has arisen "
                                                "that may cause data to be outdated or missing from the "
                                                "application's display. Additional details regarding this "
                                                "problem may be able to be obtained from the application "
                                                "logs." ALERT_MSG_LOGS_USER_GUIDE)));
        }

        // after the bulk load concludes, if there are no desktop applications
        // and some error did occur during load then the choice to filter by
        // desktop applications should be disabled.

        if (errorStatus != B_OK && !packagesSummary.HasAnyDesktop()
                && fModel.FilterSpecification()->ShowOnlyDesktopPackages()) {
                fModel.SetFilterSpecification(
                        PackageFilterSpecificationBuilder(fModel.FilterSpecification())
                                .WithShowOnlyDesktopPackages(false)
                                .BuildRef());
        }

        // after the bulk load concludes, if there are no native desktop
        // applications and some error did occur during load then the choice to
        // filter by native desktop applications should be disabled.

        if (errorStatus != B_OK && !packagesSummary.HasAnyNativeDesktop()
                && fModel.FilterSpecification()->ShowOnlyNativeDesktopPackages()) {
                HDINFO("no native desktop packages; will disable filter");
                fModel.SetFilterSpecification(
                        PackageFilterSpecificationBuilder(fModel.FilterSpecification())
                                .WithShowOnlyNativeDesktopPackages(false)
                                .BuildRef());
        }

        _AdoptModel();
        _UpdateAvailableRepositories();

        // if after loading everything in, it transpires that there are no
        // featured packages then the featured packages should be disabled
        // and the user should be switched to the "all packages" view so
        // that they are not presented with a blank window!

        // if there are no prominent packages then the disablement of the featured
        // tab is made elsewhere.

        if (!packagesSummary.HasAnyProminentPackages()
                && fListTabs->Selection() == TAB_PROMINENT_PACKAGES) {
                HDINFO("no prominent packages; will display all packages instead");
                fModel.SetPackageListViewMode(ALL);
        }
}


void
MainWindow::_NotifyWorkStatusClear()
{
        BMessage message(MSG_WORK_STATUS_CLEAR);
        this->PostMessage(&message, this);
}


void
MainWindow::_HandleWorkStatusClear()
{
        fWorkStatusView->SetText("");
        fWorkStatusView->SetIdle();
}


/*!     Sends off a message to the Window so that it can change the status view
        on the front-end in the UI thread.
*/

void
MainWindow::_NotifyWorkStatusChange(const BString& text, float progress)
{
        BMessage message(MSG_WORK_STATUS_CHANGE);

        if (!text.IsEmpty())
                message.AddString(kKeyWorkStatusText, text);
        message.AddFloat(kKeyWorkStatusProgress, progress);

        this->PostMessage(&message, this);
}


void
MainWindow::_HandleExternalPackageUpdateMessageReceived(const BMessage* message)
{
        BStringList addedPackageNames;
        BStringList removedPackageNames;

        if (message->FindStrings("added package names", &addedPackageNames) == B_OK) {
                addedPackageNames.Sort();
                _SetStateForPackagesByName(addedPackageNames, ACTIVATED);
        } else {
                HDINFO("no [added package names] key in inbound message");
        }

        if (message->FindStrings("removed package names", &removedPackageNames) == B_OK) {
                removedPackageNames.Sort();
                _SetStateForPackagesByName(removedPackageNames, UNINSTALLED);
        } else {
                HDINFO("no [removed package names] key in inbound message");
        }
}


void
MainWindow::_SetStateForPackagesByName(BStringList& packageNames, PackageState state)
{
        std::vector<PackageInfoRef> modifiedPackages;

        for (int32 i = 0; i < packageNames.CountStrings(); i++) {
                BString packageName = packageNames.StringAt(i);
                const PackageInfoRef package = fModel.PackageForName(packageName);

                if (package.IsSet()) {
                        PackageLocalInfoRef localInfo
                                = PackageLocalInfoBuilder(package->LocalInfo()).WithState(state)
                                        .ClearInstallationLocations().BuildRef();

                        modifiedPackages.push_back(
                                PackageInfoBuilder(package).WithLocalInfo(localInfo).BuildRef());

                        HDINFO("will update package [%s] with state [%s]", packageName.String(),
                                PackageUtils::StateToString(state));
                } else {
                        HDINFO("was unable to find package [%s] so was not possible to set the state to [%s]",
                                packageName.String(), PackageUtils::StateToString(state));
                }
        }

        fModel.AddPackagesWithChange(modifiedPackages, PKG_CHANGED_LOCAL_INFO);
}


void
MainWindow::_HandleWorkStatusChangeMessageReceived(const BMessage* message)
{
        if (fWorkStatusView == NULL)
                return;

        BString text;
        float progress;

        if (message->FindString(kKeyWorkStatusText, &text) == B_OK)
                fWorkStatusView->SetText(text);

        if (message->FindFloat(kKeyWorkStatusProgress, &progress) == B_OK) {
                if (progress < 0.0f)
                        fWorkStatusView->SetBusy();
                else
                        fWorkStatusView->SetProgress(progress);
        } else {
                HDERROR("work status change missing progress on update message");
                fWorkStatusView->SetProgress(0.0f);
        }
}


void
MainWindow::_OpenSettingsWindow()
{
        SettingsWindow* window = new SettingsWindow(this, &fModel);
        window->Show();
}


void
MainWindow::_OpenLoginWindow(const BMessage& onSuccessMessage)
{
        UserLoginWindow* window = new UserLoginWindow(this, BRect(0, 0, 500, 400), fModel);

        if (onSuccessMessage.what != 0)
                window->SetOnSuccessMessage(BMessenger(this), onSuccessMessage);

        window->Show();
}


void
MainWindow::_StartUserVerify()
{
        if (!fModel.Nickname().IsEmpty()) {
                _AddProcessCoordinator(ProcessCoordinatorFactory::CreateUserDetailVerifierCoordinator(this,
                        // UserDetailVerifierListener
                        &fModel));
        }
}


void
MainWindow::_StartServerRuntimeInformationVerify()
{
        ProcessCoordinator* coordinator
                = ProcessCoordinatorFactory::CreateServerRuntimeInformationVerifierCoordinator(&fModel);
        _AddProcessCoordinator(coordinator);
}


void
MainWindow::_UpdateAuthorization()
{
        bool networking = ServerHelper::IsNetworkAvailable();
        BString nickname(fModel.Nickname());
        bool hasUser = !nickname.IsEmpty();

        // At some point in the future, the HDS server may not allow older HaikuDepot clients to
        // continue to authenticate with nickname and password. To be able to handle this gracefully,
        // the server is able to inform the client if it supported and if not then this client is able
        // to resort to a logged out only experience but most things will function.

        bool canNicknamePasswordAuthenticate = fModel.CanNicknamePasswordAuthenticate();

        if (fUserMenu != NULL) {
                BString label;
                if (hasUser) {
                        label = B_TRANSLATE("Logged in as %User%");
                        label.ReplaceAll("%User%", nickname);
                } else {
                        if (!canNicknamePasswordAuthenticate)
                                label = B_TRANSLATE("Login disabled");
                        else
                                label = B_TRANSLATE("Not logged in");
                }
                fUserMenu->Superitem()->SetLabel(label);
        }

        if (fLogInItem != NULL) {
                if (hasUser)
                        fLogInItem->SetLabel(B_TRANSLATE("Switch account" B_UTF8_ELLIPSIS));
                else
                        fLogInItem->SetLabel(B_TRANSLATE("Log in" B_UTF8_ELLIPSIS));
                fLogInItem->SetEnabled(canNicknamePasswordAuthenticate && networking);
        }

        if (fLogOutItem != NULL)
                fLogOutItem->SetEnabled(hasUser);

        if (fUsersUserUsageConditionsMenuItem != NULL)
                fUsersUserUsageConditionsMenuItem->SetEnabled(hasUser && networking);

        if (fLatestUserUsageConditionsMenuItem != NULL)
                fLatestUserUsageConditionsMenuItem->SetEnabled(networking);
}


void
MainWindow::_HandleLogout()
{
        if (IdentityAndAccessUtils::ClearCredentials() != B_OK)
                HDERROR("unable to remove stored credentials");
        fModel.SetCredentials(UserCredentials());
}


void
MainWindow::_UpdateAvailableRepositories()
{
        fRepositoryMenu->RemoveItems(0, fRepositoryMenu->CountItems(), true);

        fRepositoryMenu->AddItem(
                new BMenuItem(B_TRANSLATE("All repositories"), new BMessage(MSG_DEPOT_SELECTED)));

        fRepositoryMenu->AddItem(new BSeparatorItem());

        bool foundSelectedDepot = false;
        std::vector<DepotInfoRef> depots = _CreateSnapshotOfDepots();
        std::vector<DepotInfoRef>::iterator it;

        for (it = depots.begin(); it != depots.end(); it++) {
                DepotInfoRef depot = *it;

                if (depot->Name().Length() != 0) {
                        BMessage* message = new BMessage(MSG_DEPOT_SELECTED);
                        message->AddString(shared_message_keys::kKeyDepotName, depot->Name());
                        BMenuItem* item = new(std::nothrow) BMenuItem(depot->Name(), message);

                        if (item == NULL)
                                HDFATAL("memory exhaustion");

                        fRepositoryMenu->AddItem(item);

                        if (depot->Name() == fModel.FilterSpecification()->DepotName()) {
                                item->SetMarked(true);
                                foundSelectedDepot = true;
                        }
                }
        }

        if (!foundSelectedDepot)
                fRepositoryMenu->ItemAt(0)->SetMarked(true);
}


bool
MainWindow::_SelectedPackageHasWebAppRepositoryCode()
{
        const PackageInfoRef& package = fPackageInfoView->Package();
        const BString depotName = PackageUtils::DepotName(package);

        if (depotName.IsEmpty()) {
                HDDEBUG("the package [%s] has no depot name", package->Name().String());
        } else {
                const DepotInfo* depot = fModel.DepotForName(depotName);

                if (depot == NULL) {
                        HDINFO("the depot [%s] was not able to be found", depotName.String());
                } else {
                        BString repositoryCode = depot->WebAppRepositoryCode();

                        if (repositoryCode.IsEmpty())
                                HDINFO("the depot [%s] has no web app repository code", depotName.String());
                        else
                                return true;
                }
        }

        return false;
}


void
MainWindow::_RatePackage()
{
        if (!ServerHelper::IsNetworkAvailable()) {
                HDERROR("no network available -> not possible to rate package");
                return;
        }

        if (!_SelectedPackageHasWebAppRepositoryCode()) {
                BAlert* alert = new(std::nothrow) BAlert(B_TRANSLATE("Rating not possible"),
                        B_TRANSLATE("This package doesn't seem to be on the HaikuDepot Server, so it's not "
                                                "possible to create a new rating or edit an existing rating."),
                        B_TRANSLATE("OK"));
                alert->Go();
                return;
        }

        if (fModel.Nickname().IsEmpty()) {
                if (!fModel.CanNicknamePasswordAuthenticate()) {
                        HDERROR("nickname / password auth not available -> not possible to rate package");
                        return;
                }

                BAlert* alert = new(std::nothrow) BAlert(B_TRANSLATE("Not logged in"),
                        B_TRANSLATE("You need to be logged into an account before you can rate packages."),
                        B_TRANSLATE("Cancel"), B_TRANSLATE("Login or Create account"));

                if (alert == NULL)
                        return;

                int32 choice = alert->Go();
                if (choice == 1)
                        _OpenLoginWindow(BMessage(MSG_RATE_PACKAGE));
                return;
        }

        // TODO: Allow only one RatePackageWindow
        // TODO: Mechanism for remembering the window frame
        RatePackageWindow* window = new RatePackageWindow(this, BRect(0, 0, 500, 400), fModel);
        window->SetPackage(fPackageInfoView->Package());
        window->Show();
}


void
MainWindow::_ShowScreenshot()
{
        // TODO: Mechanism for remembering the window frame
        if (fScreenshotWindow == NULL)
                fScreenshotWindow = new ScreenshotWindow(this, BRect(0, 0, 500, 400), &fModel);

        if (fScreenshotWindow->LockWithTimeout(1000) != B_OK)
                return;

        fScreenshotWindow->SetPackage(fPackageInfoView->Package());

        if (fScreenshotWindow->IsHidden())
                fScreenshotWindow->Show();
        else
                fScreenshotWindow->Activate();

        fScreenshotWindow->Unlock();
}


void
MainWindow::_ViewUserUsageConditions(UserUsageConditionsSelectionMode mode)
{
        UserUsageConditionsWindow* window = new UserUsageConditionsWindow(fModel, mode);
        window->Show();
}


void
MainWindow::UserCredentialsFailed()
{
        BString message = B_TRANSLATE("The password previously "
                                                                  "supplied for the user [%Nickname%] is not currently "
                                                                  "valid. The user will be logged-out of this application "
                                                                  "and you should login again with your updated password.");
        message.ReplaceAll("%Nickname%", fModel.Nickname());

        AppUtils::NotifySimpleError(SimpleAlert(B_TRANSLATE("Login issue"), message));

        if (IdentityAndAccessUtils::ClearCredentials() != B_OK)
                HDERROR("unable to remove stored credentials");
        fModel.SetCredentials(UserCredentials());
}


/*! \brief This method is invoked from the UserDetailVerifierProcess on a
                   background thread.  For this reason it lodges a message into itself
                   which can then be handled on the main thread.
*/

void
MainWindow::UserUsageConditionsNotLatest(const UserDetail& userDetail)
{
        BMessage message(MSG_USER_USAGE_CONDITIONS_NOT_LATEST);
        BMessage detailsMessage;
        if (userDetail.Archive(&detailsMessage, true) != B_OK
                || message.AddMessage("userDetail", &detailsMessage) != B_OK) {
                HDERROR("unable to archive the user detail into a message");
        } else {
                BMessenger(this).SendMessage(&message);
        }
}


void
MainWindow::_HandleUserUsageConditionsNotLatest(const UserDetail& userDetail)
{
        ToLatestUserUsageConditionsWindow* window
                = new ToLatestUserUsageConditionsWindow(this, fModel, userDetail);
        window->Show();
}


void
MainWindow::_AddProcessCoordinator(ProcessCoordinator* item)
{
        AutoLocker<BLocker> lock(&fCoordinatorLock);

        if (fShouldCloseWhenNoProcessesToCoordinate) {
                HDINFO("system shutting down --> new process coordinator [%s] rejected",
                        item->Name().String());
                return;
        }

        item->SetListener(this);

        if (fCoordinator == NULL) {
                if (acquire_sem(fCoordinatorRunningSem) != B_OK)
                        debugger("unable to acquire the process coordinator sem");
                HDINFO("adding and starting a process coordinator [%s]", item->Name().String());
                delete fCoordinator;
                fCoordinator = item;
                fCoordinator->Start();
        } else {
                HDINFO("adding process coordinator [%s] to the queue", item->Name().String());
                fCoordinatorQueue.push(item);
        }
}


void
MainWindow::_SpinUntilProcessCoordinatorComplete()
{
        while (true) {
                if (acquire_sem(fCoordinatorRunningSem) != B_OK)
                        debugger("unable to acquire the process coordinator sem");
                if (release_sem(fCoordinatorRunningSem) != B_OK)
                        debugger("unable to release the process coordinator sem");
                {
                        AutoLocker<BLocker> lock(&fCoordinatorLock);
                        if (fCoordinator == NULL)
                                return;
                }
        }
}


void
MainWindow::_StopProcessCoordinators()
{
        HDINFO("will stop all queued process coordinators");
        AutoLocker<BLocker> lock(&fCoordinatorLock);

        while (!fCoordinatorQueue.empty()) {
                ProcessCoordinator* processCoordinator = fCoordinatorQueue.front();
                HDINFO("will drop queued process coordinator [%s]", processCoordinator->Name().String());
                fCoordinatorQueue.pop();
                delete processCoordinator;
        }

        if (fCoordinator != NULL)
                fCoordinator->RequestStop();
}


/*! This method is called when there is some change in the bulk load process
        or other process coordinator.
        A change may mean that a new process has started / stopped etc... or it
        may mean that the entire coordinator has finished.
*/
void
MainWindow::CoordinatorChanged(ProcessCoordinatorState& coordinatorState)
{
        BMessage message(MSG_PROCESS_COORDINATOR_CHANGED);
        if (coordinatorState.Archive(&message, true) != B_OK)
                HDFATAL("unable to archive message when the process coordinator has changed");
        BMessenger(this).SendMessage(&message);
}


void
MainWindow::_HandleProcessCoordinatorChanged(ProcessCoordinatorState& coordinatorState)
{
        AutoLocker<BLocker> lock(&fCoordinatorLock);

        if (fCoordinator->Identifier() == coordinatorState.ProcessCoordinatorIdentifier()) {
                if (!coordinatorState.IsRunning()) {
                        if (release_sem(fCoordinatorRunningSem) != B_OK)
                                debugger("unable to release the process coordinator sem");
                        HDINFO("process coordinator [%s] did complete", fCoordinator->Name().String());
                        // complete the last one that just finished
                        BMessage* message = fCoordinator->Message();

                        if (message != NULL) {
                                BMessenger messenger(this);
                                message->AddInt64(KEY_ERROR_STATUS,
                                        static_cast<int64>(fCoordinator->ErrorStatus()));
                                messenger.SendMessage(message);
                        }

                        HDDEBUG("process coordinator report;\n---\n%s\n----",
                                fCoordinator->LogReport().String());

                        delete fCoordinator;
                        fCoordinator = NULL;

                        // now schedule the next one.
                        if (!fCoordinatorQueue.empty()) {
                                if (acquire_sem(fCoordinatorRunningSem) != B_OK)
                                        debugger("unable to acquire the process coordinator sem");
                                fCoordinator = fCoordinatorQueue.front();
                                HDINFO("starting next process coordinator [%s]", fCoordinator->Name().String());
                                fCoordinatorQueue.pop();
                                fCoordinator->Start();
                        } else {
                                _NotifyWorkStatusClear();
                                if (fShouldCloseWhenNoProcessesToCoordinate) {
                                        HDINFO("no more processes to coord --> will quit");
                                        BMessage message(B_QUIT_REQUESTED);
                                        PostMessage(&message);
                                }
                        }
                } else {
                        _NotifyWorkStatusChange(coordinatorState.Message(), coordinatorState.Progress());
                                // show the progress to the user.
                }
        } else {
                _NotifyWorkStatusClear();
                HDINFO("! unknown process coordinator changed");
        }
}


/*!     This method will update the selected tab in the model but does not change
 *      the UI display directly. Instead, the model will message back to this
 *      class to do that.
 */
void
MainWindow::_HandleChangePackageListViewMode()
{
        switch (fListTabs->Selection()) {
                case TAB_PROMINENT_PACKAGES:
                        fModel.SetPackageListViewMode(PROMINENT);
                        break;
                case TAB_ALL_PACKAGES:
                        fModel.SetPackageListViewMode(ALL);
                        break;
                default:
                        HDFATAL("unknown package list tab");
                        break;
        }
}


/*!     This method will create a vector of packages that are filtered by the filter
        defined in the model.
*/
const std::vector<PackageInfoRef>
MainWindow::_CreateSnapshotOfFilteredPackages()
{
        return fModel.FilteredPackages();
}


std::vector<DepotInfoRef>
MainWindow::_CreateSnapshotOfDepots()
{
        return fModel.Depots();
}


/*!     This method will get invoked in the case that the icons have been
        reloaded. It applies to all packages because the icons are loaded
        all in one go.
*/

void
MainWindow::_HandleIconsChanged()
{
        if (!fSinglePackageMode) {
                fFeaturedPackagesView->HandleIconsChanged();
                fPackageListView->HandleIconsChanged();
        }

        fPackageInfoView->HandleIconsChanged();
}


/*!     This will get invoked in the case that a screenshot has been cached
        and so could now be loaded by some UI element. This method will then
        signal to other UI elements that they could load a screenshot should
        they have been waiting for it.
*/

void
MainWindow::_HandleScreenshotCached(const BMessage* message)
{
        ScreenshotCoordinate coordinate(message);
        fPackageInfoView->HandleScreenshotCached(coordinate);
}


/*static*/ const BString
MainWindow::_WindowTitleForPackage(const PackageInfoRef& pkg)
{
        PackageVersionRef version = PackageUtils::Version(pkg);
        BString versionString = "???";

        if (version.IsSet())
                versionString = version->ToString();

        BString title("%AppName% - %PackageName% %PackageVersion%");
        title.ReplaceFirst("%AppName%", B_TRANSLATE_SYSTEM_NAME("HaikuDepot"));
        title.ReplaceFirst("%PackageName%", pkg->Name());
        title.ReplaceFirst("%PackageVersion%", versionString);

        return title;
}