/* * Copyright 2015, Axel Dörfler, . * Copyright 2013-2014, Stephan Aßmus . * Copyright 2013, Rene Gollent, rene@gollent.com. * Copyright 2013, Ingo Weinhold, ingo_weinhold@gmx.de. * Copyright 2016-2024, Andrew Lindesay . * Copyright 2017, Julian Harnath . * All rights reserved. Distributed under the terms of the MIT License. */ #include "MainWindow.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 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"); 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"); 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 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 depots = _CreateSnapshotOfDepots(); std::vector::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 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 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 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 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 depots = _CreateSnapshotOfDepots(); std::vector::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 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 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 lock(&fCoordinatorLock); if (fCoordinator == NULL) return; } } } void MainWindow::_StopProcessCoordinators() { HDINFO("will stop all queued process coordinators"); AutoLocker 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 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 MainWindow::_CreateSnapshotOfDepots() { std::vector 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); }