⛏️ index : haiku.git

/*
 * 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-2024, 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 <map>
#include <vector>

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

#include "AppUtils.h"
#include "AutoDeleter.h"
#include "AutoLocker.h"
#include "DecisionProvider.h"
#include "FeaturedPackagesView.h"
#include "FilterView.h"
#include "LocaleUtils.h"
#include "Logger.h"
#include "PackageInfoView.h"
#include "PackageListView.h"
#include "PackageManager.h"
#include "ProcessCoordinator.h"
#include "ProcessCoordinatorFactory.h"
#include "RatePackageWindow.h"
#include "ScreenshotWindow.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_CATEGORIES_LIST_CHANGED				= 'clic',
	MSG_PACKAGE_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_CHANGE_PACKAGE_LIST_VIEW_MODE		= 'cplm',
	MSG_SHOW_AVAILABLE_PACKAGES				= 'savl',
	MSG_SHOW_INSTALLED_PACKAGES				= 'sins',
	MSG_SHOW_SOURCE_PACKAGES				= 'ssrc',
	MSG_SHOW_DEVELOP_PACKAGES				= 'sdvl'
};

#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 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);
		}
	}

private:
	BMessenger	fMessenger;
};


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

	~MainWindowPackageInfoListener()
	{
	}

private:
	// PackageInfoListener
	virtual	void PackageChanged(const PackageInfoEvent& event)
	{
		fMainWindow->PackageChanged(event);
	}

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),
	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, this);

	fSplitView = new BSplitView(B_VERTICAL, 5.0f);

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

	fListTabs = new TabView(BMessenger(this),
		BMessage(MSG_CHANGE_PACKAGE_LIST_VIEW_MODE), "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);

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

	_RestoreModelSettings(settings);
	_MaybePromptCanShareAnonymousUserData(settings);

	if (fModel.PackageListViewMode() == PROMINENT)
		fListTabs->Select(TAB_PROMINENT_PACKAGES);
	else
		fListTabs->Select(TAB_ALL_PACKAGES);

	_RestoreNickname(settings);
	_UpdateAuthorization();
	_RestoreWindowFrame(settings);

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

	_AdoptModel();
	_StartBulkLoad();
}


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

MainWindow::MainWindow(const BMessage& settings, 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),
	fModelListener(new MainWindowModelListener(BMessenger(this)), true),
	fCoordinator(NULL),
	fShouldCloseWhenNoProcessesToCoordinate(false),
	fSinglePackageMode(true),
	fIncrementViewCounterDelayedRunner(NULL)
{
	BString title = B_TRANSLATE("HaikuDepot - %PackageName% %PackageVersion%");
	title.ReplaceAll("%PackageName%", package->Name());
	title.ReplaceAll("%PackageVersion%", package->Version().ToString());
	SetTitle(title);

	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, this);
	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);

	// add the single package into the model so that any internal
	// business logic is able to find the package.
	DepotInfoRef depot(new DepotInfo("single-pkg-depot"), true);
	depot->AddPackage(package);
	fModel.MergeOrAddDepot(depot);

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

	fPackageInfoView->SetPackage(package);

	// 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(KEY_WINDOW_SETTINGS, &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((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:
			_OpenLoginWindow(BMessage());
			break;

		case MSG_SETTINGS:
			_OpenSettingsWindow();
			break;

		case MSG_LOG_OUT:
			fModel.SetNickname("");
			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:
			_StartUserVerify();
			_UpdateAuthorization();
			break;

		case MSG_SCREENSHOT_CACHED:
			_HandleScreenshotCached(message);
			break;

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

		case MSG_CHANGE_PACKAGE_LIST_VIEW_MODE:
			_HandleChangePackageListViewMode();
			break;

		case MSG_SHOW_AVAILABLE_PACKAGES:
			{
				BAutolock locker(fModel.Lock());
				PackageFilterModel* filterModel = fModel.PackageFilter();
				filterModel->SetShowAvailablePackages(!filterModel->ShowAvailablePackages());
			}
			_AdoptModel();
			break;

		case MSG_SHOW_INSTALLED_PACKAGES:
			{
				BAutolock locker(fModel.Lock());
				PackageFilterModel* filterModel = fModel.PackageFilter();
				filterModel->SetShowInstalledPackages(!filterModel->ShowInstalledPackages());
			}
			_AdoptModel();
			break;

		case MSG_SHOW_SOURCE_PACKAGES:
			{
				BAutolock locker(fModel.Lock());
				PackageFilterModel* filterModel = fModel.PackageFilter();
				filterModel->SetShowSourcePackages(!filterModel->ShowSourcePackages());
			}
			_AdoptModel();
			break;

		case MSG_SHOW_DEVELOP_PACKAGES:
			{
				BAutolock locker(fModel.Lock());
				PackageFilterModel* filterModel = fModel.PackageFilter();
				filterModel->SetShowDevelopPackages(!filterModel->ShowDevelopPackages());
			}
			_AdoptModel();
			break;

			// this may be triggered by, for example, a user rating being added
			// or having been altered.
		case MSG_SERVER_DATA_CHANGED:
		{
			BString name;
			if (message->FindString("name", &name) == B_OK) {
				BAutolock locker(fModel.Lock());
				if (fPackageInfoView->Package()->Name() == name) {
					_PopulatePackageAsync(true);
				} else {
					HDDEBUG("pkg [%s] is updated on the server, but is "
						"not selected so will not be updated.",
						name.String());
				}
			}
			break;
		}

		case MSG_INCREMENT_VIEW_COUNTER:
			_HandleIncrementViewCounter(message);
			break;

		case MSG_PACKAGE_SELECTED:
		{
			BString name;
			if (message->FindString("name", &name) == B_OK) {
				PackageInfoRef package;
				{
					BAutolock locker(fModel.Lock());
					package = fModel.PackageForName(name);
				}
				if (!package.IsSet() || name != package->Name())
					debugger("unable to find the named package");
				else {
					_AdoptPackage(package);
					_SetupDelayedIncrementViewCounter(package);
				}
			} else {
				_ClearPackage();
			}
			break;
		}

		case MSG_CATEGORY_SELECTED:
		{
			BString code;
			if (message->FindString("code", &code) != B_OK)
				code = "";
			{
				BAutolock locker(fModel.Lock());
				fModel.PackageFilter()->SetCategory(code);
			}
			_AdoptModel();
			break;
		}

		case MSG_DEPOT_SELECTED:
		{
			BString name;
			if (message->FindString("name", &name) != B_OK)
				name = "";
			{
				BAutolock locker(fModel.Lock());
				fModel.PackageFilter()->SetDepotName(name);
			}
			_AdoptModel();
			_UpdateAvailableRepositories();
			break;
		}

		case MSG_SEARCH_TERMS_MODIFIED:
		{
			// TODO: Do this with a delay!
			BString searchTerms;
			if (message->FindString("search terms", &searchTerms) != B_OK)
				searchTerms = "";
			{
				BAutolock locker(fModel.Lock());
				fModel.PackageFilter()->SetSearchTerms(searchTerms);
			}
			_AdoptModel();
			break;
		}

		case MSG_PACKAGE_CHANGED:
		{
			PackageInfo* info;
			if (message->FindPointer("package", (void**)&info) == B_OK) {
				PackageInfoRef ref(info, true);
				fFeaturedPackagesView->BeginAddRemove();
				_AddRemovePackageFromLists(ref);
				fFeaturedPackagesView->EndAddRemove();
			}
			break;
		}

		case MSG_PROCESS_COORDINATOR_CHANGED:
		{
			ProcessCoordinatorState state(message);
			_HandleProcessCoordinatorChanged(state);
			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;
		}

		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("window frame", Frame());

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

		settings.AddMessage("column settings", &columnSettings);

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

		settings.AddBool(SETTING_SHOW_AVAILABLE_PACKAGES,
			fModel.PackageFilter()->ShowAvailablePackages());
		settings.AddBool(SETTING_SHOW_INSTALLED_PACKAGES,
			fModel.PackageFilter()->ShowInstalledPackages());
		settings.AddBool(SETTING_SHOW_DEVELOP_PACKAGES,
			fModel.PackageFilter()->ShowDevelopPackages());
		settings.AddBool(SETTING_SHOW_SOURCE_PACKAGES,
			fModel.PackageFilter()->ShowSourcePackages());

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

	settings.AddString("username", fModel.Nickname());
}


void
MainWindow::Consume(ProcessCoordinator *item)
{
	_AddProcessCoordinator(item);
}


void
MainWindow::PackageChanged(const PackageInfoEvent& event)
{
	uint32 watchedChanges = PKG_CHANGED_STATE | PKG_CHANGED_PROMINENCE;
	if ((event.Changes() & watchedChanges) != 0) {
		PackageInfoRef ref(event.Package());
		BMessage message(MSG_PACKAGE_CHANGED);
		message.AddPointer("package", ref.Get());
		ref.Detach();
			// reference needs to be released by MessageReceived();
		PostMessage(&message);
	}
}


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"));

	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);

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

	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("username", &nickname) == B_OK
		&& nickname.Length() > 0) {
		fModel.SetNickname(nickname);
	}
}


const char*
MainWindow::_WindowFrameName() const
{
	if (fSinglePackageMode)
		return "small window frame";

	return "window frame";
}


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;

	if (settings.FindBool(SETTING_SHOW_AVAILABLE_PACKAGES, &showOption) == B_OK)
		fModel.PackageFilter()->SetShowAvailablePackages(showOption);
	if (settings.FindBool(SETTING_SHOW_INSTALLED_PACKAGES, &showOption) == B_OK)
		fModel.PackageFilter()->SetShowInstalledPackages(showOption);
	if (settings.FindBool(SETTING_SHOW_DEVELOP_PACKAGES, &showOption) == B_OK)
		fModel.PackageFilter()->SetShowDevelopPackages(showOption);
	if (settings.FindBool(SETTING_SHOW_SOURCE_PACKAGES, &showOption) == B_OK)
		fModel.PackageFilter()->SetShowSourcePackages(showOption);

	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("No"),
		B_TRANSLATE("Yes"));

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


void
MainWindow::_InitPreferredLanguage()
{
	LanguageRepository* repository = fModel.Languages();
	LanguageRef defaultLanguage = LocaleUtils::DeriveDefaultLanguage(repository);
	repository->AddLanguage(defaultLanguage);
	fModel.SetPreferredLanguage(defaultLanguage);
}


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

	BAutolock locker(fModel.Lock());
	fShowAvailablePackagesItem->SetMarked(fModel.PackageFilter()->ShowAvailablePackages());
	fShowInstalledPackagesItem->SetMarked(fModel.PackageFilter()->ShowInstalledPackages());
	fShowSourcePackagesItem->SetMarked(fModel.PackageFilter()->ShowSourcePackages());
	fShowDevelopPackagesItem->SetMarked(fModel.PackageFilter()->ShowDevelopPackages());

	if (fModel.PackageListViewMode() == PROMINENT)
		fListTabs->Select(TAB_PROMINENT_PACKAGES);
	else
		fListTabs->Select(TAB_ALL_PACKAGES);

	fFilterView->AdoptModel(fModel);
}


void
MainWindow::_AdoptModel()
{
	HDTRACE("adopting model to main window ui");

	if (fSinglePackageMode)
		return;

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

	fFeaturedPackagesView->BeginAddRemove();

	for (it = depots.begin(); it != depots.end(); it++) {
		DepotInfoRef depotInfoRef = *it;
		for (int i = 0; i < depotInfoRef->CountPackages(); i++) {
			PackageInfoRef package = depotInfoRef->PackageAtIndex(i);
			_AddRemovePackageFromLists(package);
		}
	}

	fFeaturedPackagesView->EndAddRemove();

	_AdoptModelControls();
}


void
MainWindow::_AddRemovePackageFromLists(const PackageInfoRef& package)
{
	bool matches;

	{
		AutoLocker<BLocker> modelLocker(fModel.Lock());
		matches = fModel.PackageFilter()->Filter()->AcceptsPackage(package);
	}

	if (matches) {
		if (package->IsProminent())
			fFeaturedPackagesView->AddPackage(package);
		fPackageListView->AddPackage(package);
	} else {
		fFeaturedPackagesView->RemovePackage(package);
		fPackageListView->RemovePackage(package);
	}
}


void
MainWindow::_SetupDelayedIncrementViewCounter(const PackageInfoRef package) {
	if (fIncrementViewCounterDelayedRunner != NULL) {
		fIncrementViewCounterDelayedRunner->SetCount(0);
		delete fIncrementViewCounterDelayedRunner;
	}
	BMessage message(MSG_INCREMENT_VIEW_COUNTER);
	message.SetString("name", 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("name", &name) == B_OK) {
		const PackageInfoRef& viewedPackage =
			fPackageInfoView->Package();
		if (viewedPackage.IsSet()) {
			if (viewedPackage->Name() == name)
				_IncrementViewCounter(viewedPackage);
			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 PackageInfoRef package)
{
	bool shouldIncrementViewCounter = false;

	{
		AutoLocker<BLocker> modelLocker(fModel.Lock());
		bool canShareAnonymousUsageData = fModel.CanShareAnonymousUsageData();
		if (canShareAnonymousUsageData && !package->Viewed()) {
			package->SetViewed();
			shouldIncrementViewCounter = true;
		}
	}

	if (shouldIncrementViewCounter) {
		ProcessCoordinator* incrementViewCoordinator =
			ProcessCoordinatorFactory::CreateIncrementViewCounter(
				&fModel, package);
		_AddProcessCoordinator(incrementViewCoordinator);
	}
}


void
MainWindow::_AdoptPackage(const PackageInfoRef& package)
{
	{
		BAutolock locker(fModel.Lock());
		fPackageInfoView->SetPackage(package);

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

	_PopulatePackageAsync(false);
}


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


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

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


void
MainWindow::_BulkLoadCompleteReceived(status_t errorStatus)
{
	if (errorStatus != B_OK) {
		AppUtils::NotifySimpleError(
			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));
	}

	fRefreshRepositoriesItem->SetEnabled(true);
	_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!

	bool hasProminentPackages = fModel.HasAnyProminentPackages();
	fListTabs->TabAt(TAB_PROMINENT_PACKAGES)->SetEnabled(hasProminentPackages);
	if (!hasProminentPackages
			&& fListTabs->Selection() == TAB_PROMINENT_PACKAGES) {
		fModel.SetPackageListViewMode(ALL);
		fListTabs->Select(TAB_ALL_PACKAGES);
	}
}


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(KEY_WORK_STATUS_TEXT, text);
	message.AddFloat(KEY_WORK_STATUS_PROGRESS, 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();
		AutoLocker<BLocker> locker(fModel.Lock());
		fModel.SetStateForPackagesByName(addedPackageNames, ACTIVATED);
	}
	else
		HDINFO("no [added package names] key in inbound message");

	if (message->FindStrings("removed package names",
			&removedPackageNames) == B_OK) {
		removedPackageNames.Sort();
		AutoLocker<BLocker> locker(fModel.Lock());
		fModel.SetStateForPackagesByName(addedPackageNames, UNINSTALLED);
	} else
		HDINFO("no [removed package names] key in inbound message");
}


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

	BString text;
	float progress;

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

	if (message->FindFloat(KEY_WORK_STATUS_PROGRESS, &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);
	}
}


/*! Initially only superficial data is loaded from the server into the data
	model of the packages.  When the package is viewed, additional data needs
	to be populated including ratings.

	This method will cause the package to have its data refreshed from
	the server application.  The refresh happens in the background; this method
	is asynchronous.
*/

void
MainWindow::_PopulatePackageAsync(bool forcePopulate)
{
	const PackageInfoRef package = fPackageInfoView->Package();

	if (!fModel.CanPopulatePackage(package))
		return;

	const char* packageNameStr = package->Name().String();

	if (package->HasChangelog() && (forcePopulate || package->Changelog().IsEmpty())) {
		_AddProcessCoordinator(
			ProcessCoordinatorFactory::PopulatePkgChangelogCoordinator(&fModel, package));
		HDINFO("pkg [%s] will have changelog updated from server.", packageNameStr);
	} else {
		HDDEBUG("pkg [%s] not have changelog updated from server.", packageNameStr);
	}

	// TODO; (apl 4.Aug.2024) soon HDS will be able to pass through the count of user ratings; when
	//    available this will mean we can avoid the need to fetch the user ratings if we know in
	//    advance that there are none to get. This will reduce network chatter and speed up the
	//    UI display. Also note that the member variable `fDidPopulateUserRatings` can then also be
	//    removed from the `PackageInfo` class too.

	if (forcePopulate || !package->DidPopulateUserRatings()) {
		_AddProcessCoordinator(
			ProcessCoordinatorFactory::PopulatePkgUserRatingsCoordinator(&fModel, package));
		HDINFO("pkg [%s] will have user ratings updated from server.", packageNameStr);
	} else {
		HDDEBUG("pkg [%s] not have user ratings updated from server.", packageNameStr);
	}
}


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::_UpdateAuthorization()
{
	BString nickname(fModel.Nickname());
	bool hasUser = !nickname.IsEmpty();

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

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


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("name", depot->Name());
			BMenuItem* item = new(std::nothrow) BMenuItem(depot->Name(), message);

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

			fRepositoryMenu->AddItem(item);

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

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


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

	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 (!_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()) {
		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(B_TRANSLATE("Login issue"),
		message);

	{
		AutoLocker<BLocker> locker(fModel.Lock());
		fModel.SetNickname("");
	}
}


/*! \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,
					(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");
	}
}


static package_list_view_mode
main_window_tab_to_package_list_view_mode(int32 tab)
{
	if (tab == TAB_PROMINENT_PACKAGES)
		return PROMINENT;
	return ALL;
}


void
MainWindow::_HandleChangePackageListViewMode()
{
	package_list_view_mode tabMode = main_window_tab_to_package_list_view_mode(
		fListTabs->Selection());
	package_list_view_mode modelMode = fModel.PackageListViewMode();

	if (tabMode != modelMode) {
		BAutolock locker(fModel.Lock());
		fModel.SetPackageListViewMode(tabMode);
	}
}


std::vector<DepotInfoRef>
MainWindow::_CreateSnapshotOfDepots()
{
	std::vector<DepotInfoRef> result;
	BAutolock locker(fModel.Lock());
	int32 countDepots = fModel.CountDepots();
	for(int32 i = 0; i < countDepots; i++)
		result.push_back(fModel.DepotAtIndex(i));
	return result;
}


/*! 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);
}