// ***************************************************************************** // * This file is part of the FreeFileSync project. It is distributed under * // * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * // * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * // ***************************************************************************** #include "small_dlgs.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "gui_generated.h" #include "folder_selector.h" #include "abstract_folder_picker.h" #include "../afs/concrete.h" #include "../afs/gdrive.h" #include "../afs/ftp.h" #include "../afs/sftp.h" #include "../base/synchronization.h" #include "../base/icon_loader.h" #include "../status_handler.h" //uiUpdateDue() #include "../version/version.h" #include "../ffs_paths.h" #include "../icon_buffer.h" using namespace zen; using namespace fff; namespace { class AboutDlg : public AboutDlgGenerated { public: AboutDlg(wxWindow* parent); private: void onOkay (wxCommandEvent& event) override { EndModal(static_cast(ConfirmationButton::accept)); } void onClose(wxCloseEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } void onOpenForum(wxCommandEvent& event) override { wxLaunchDefaultBrowser(L"https://freefilesync.org/forum"); } void onDonate (wxCommandEvent& event) override { wxLaunchDefaultBrowser(L"https://freefilesync.org/donate"); } void onSendEmail(wxCommandEvent& event) override { wxLaunchDefaultBrowser(wxString() + L"mailto:zenju" + L'@' + /*don't leave full email in either source or binary*/ L"freefilesync.org"); } void onLocalKeyEvent(wxKeyEvent& event); }; AboutDlg::AboutDlg(wxWindow* parent) : AboutDlgGenerated(parent) { setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonClose)); assert(m_buttonClose->GetId() == wxID_OK); //we cannot use wxID_CLOSE else ESC key won't work: yet another wxWidgets bug?? const bool darkAppearance = wxSystemSettings::GetAppearance().IsDark(); //not "dark mode" necessarily! setImage(*m_bitmapLogo, loadImage(darkAppearance ? "ffs-header-dark" : "ffs-header-light")); setImage(*m_bitmapLogoLeft, loadImage(darkAppearance ? "ffs-logo-dark" : "ffs-logo-light")); setBitmapTextLabel(*m_bpButtonForum, loadImage("ffs_forum"), L"FreeFileSync Forum"); setBitmapTextLabel(*m_bpButtonEmail, loadImage("ffs_email"), wxString() + L"zenju" + L'@' + /*don't leave full email in either source or binary*/ L"freefilesync.org"); m_bpButtonEmail->SetToolTip( wxString() + L"mailto:zenju" + L'@' + /*don't leave full email in either source or binary*/ L"freefilesync.org"); wxString build = utfTo(ffsVersion); const wchar_t* const SPACED_BULLET = L" \u2022 "; build += SPACED_BULLET; build += LTR_MARK; //fix Arabic build += utfTo(cpuArchName); build += SPACED_BULLET; build += utfTo(formatTime(formatDateTag, getCompileTime())); m_staticFfsTextVersion->SetLabelText(replaceCpy(_("Version: %x"), L"%x", build)); wxString variantName; m_staticTextFfsVariant->SetLabelText(variantName); #ifndef wxUSE_UNICODE #error what is going on? #endif { m_bitmapAnimalBig->Hide(); setRelativeFontSize(*m_staticTextDonate, 1.20); m_staticTextDonate->Hide(); //temporarily! => avoid impact to dialog width setRelativeFontSize(*m_buttonDonate1, 1.25); setBitmapTextLabel(*m_buttonDonate1, loadImage("ffs_heart", dipToScreen(28)), m_buttonDonate1->GetLabelText()); m_buttonShowSupporterDetails->Hide(); m_buttonDonate2->Hide(); } //-------------------------------------------------------------------------- m_staticTextThanksForLoc->SetMinSize({dipToWxsize(200), -1}); m_staticTextThanksForLoc->Wrap(dipToWxsize(200)); const int scrollDelta = GetCharHeight(); m_scrolledWindowTranslators->SetScrollRate(scrollDelta, scrollDelta); for (const TranslationInfo& ti : getAvailableTranslations()) { //country flag wxStaticBitmap* staticBitmapFlag = new wxStaticBitmap(m_scrolledWindowTranslators, wxID_ANY, toScaledBitmap(loadImage(ti.languageFlag))); fgSizerTranslators->Add(staticBitmapFlag, 0, wxALIGN_CENTER); //translator name wxStaticText* staticTextTranslator = new wxStaticText(m_scrolledWindowTranslators, wxID_ANY, ti.translatorName, wxDefaultPosition, wxDefaultSize, 0); fgSizerTranslators->Add(staticTextTranslator, 0, wxALIGN_CENTER_VERTICAL); staticBitmapFlag ->SetToolTip(ti.languageName); staticTextTranslator->SetToolTip(ti.languageName); } fgSizerTranslators->Fit(m_scrolledWindowTranslators); //-------------------------------------------------------------------------- wxImage::AddHandler(new wxJPEGHandler /*ownership passed*/); //activate support for .jpg files wxImage animalImg(utfTo(appendPath(getResourceDirPath(), Zstr("Animal.dat"))), wxBITMAP_TYPE_JPEG); convertToVanillaImage(animalImg); assert(animalImg.IsOk()); //-------------------------------------------------------------------------- //have animal + text match *final* dialog width GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() #ifdef __WXGTK3__ Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 //Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404 #endif { const int imageWidth = (m_panelDonate->GetSize().GetWidth() - 5 - 5 - 5 /* grey border*/) / 2; const int textWidth = m_panelDonate->GetSize().GetWidth() - 5 - 5 - 5 - imageWidth; setImage(*m_bitmapAnimalSmall, shrinkImage(animalImg, wxsizeToScreen(imageWidth), -1 /*maxHeight*/)); m_staticTextDonate->Show(); m_staticTextDonate->Wrap(textWidth - 10 /*left gap*/); //wrap *after* changing font size } //-------------------------------------------------------------------------- Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onLocalKeyEvent(event); }); //enable dialog-specific key events GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() #ifdef __WXGTK3__ Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 //Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404 #endif Center(); //apply *after* dialog size change! m_buttonClose->SetFocus(); //on GTK ESC is only associated with wxID_OK correctly if we set at least *any* focus at all!!! } void AboutDlg::onLocalKeyEvent(wxKeyEvent& event) //process key events without explicit menu entry :) { switch (event.GetKeyCode()) { case WXK_RETURN: case WXK_NUMPAD_ENTER: if (event.ControlDown()) //Ctrl+Enter or on macOS: Command+Enter { wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED); m_buttonClose->Command(dummy); //simulate click return; } break; } event.Skip(); } } void fff::showAboutDialog(wxWindow* parent) { AboutDlg dlg(parent); dlg.ShowModal(); } //######################################################################################## namespace { class CloudSetupDlg : public CloudSetupDlgGenerated { public: CloudSetupDlg(wxWindow* parent, Zstring& folderPathPhrase, Zstring& sftpKeyFileLastSelected, size_t& parallelOps, bool canChangeParallelOp); private: void onOkay (wxCommandEvent& event) override; void onCancel(wxCommandEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } void onClose (wxCloseEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } void onGdriveUserAdd (wxCommandEvent& event) override; void onGdriveUserRemove(wxCommandEvent& event) override; void onGdriveUserSelect(wxCommandEvent& event) override; void gdriveUpdateDrivesAndSelect(const std::string& accountEmail, const Zstring& locationToSelect); void onDetectServerChannelLimit(wxCommandEvent& event) override; void onTypingPassword(wxCommandEvent& event) override; void onToggleShowPassword(wxCommandEvent& event) override; void onTogglePasswordPrompt(wxCommandEvent& event) override { updateGui(); } void onBrowseCloudFolder (wxCommandEvent& event) override; void onConnectionGdrive(wxCommandEvent& event) override { type_ = CloudType::gdrive; updateGui(); } void onConnectionSftp (wxCommandEvent& event) override { type_ = CloudType::sftp; updateGui(); } void onConnectionFtp (wxCommandEvent& event) override { type_ = CloudType::ftp; updateGui(); } void onAuthPassword(wxCommandEvent& event) override { sftpAuthType_ = SftpAuthType::password; updateGui(); } void onAuthKeyfile (wxCommandEvent& event) override { sftpAuthType_ = SftpAuthType::keyFile; updateGui(); } void onAuthAgent (wxCommandEvent& event) override { sftpAuthType_ = SftpAuthType::agent; updateGui(); } void onSelectKeyfile(wxCommandEvent& event) override; void updateGui(); //work around defunct keyboard focus on macOS (or is it wxMac?) => not needed for this dialog! //void onLocalKeyEvent(wxKeyEvent& event); static bool acceptFileDrop(const std::vector& shellItemPaths); void onKeyFileDropped(FileDropEvent& event); bool validateParameters(); AbstractPath getFolderPath() const; enum class CloudType { gdrive, sftp, ftp, }; CloudType type_ = CloudType::gdrive; const wxString txtLoading_ = L'(' + _("Loading...") + L')'; const wxString txtMyDrive_ = _("My Drive"); const SftpLogin sftpDefault_; SftpAuthType sftpAuthType_ = sftpDefault_.authType; AsyncGuiQueue guiQueue_; Zstring& sftpKeyFileLastSelected_; //output-only parameters: Zstring& folderPathPhraseOut_; size_t& parallelOpsOut_; }; CloudSetupDlg::CloudSetupDlg(wxWindow* parent, Zstring& folderPathPhrase, Zstring& sftpKeyFileLastSelected, size_t& parallelOps, bool canChangeParallelOp) : CloudSetupDlgGenerated(parent), sftpKeyFileLastSelected_(sftpKeyFileLastSelected), folderPathPhraseOut_(folderPathPhrase), parallelOpsOut_(parallelOps) { setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonOK).setCancel(m_buttonCancel)); setImage(*m_toggleBtnGdrive, loadImage("google_drive")); setRelativeFontSize(*m_toggleBtnGdrive, 1.25); setRelativeFontSize(*m_toggleBtnSftp, 1.25); setRelativeFontSize(*m_toggleBtnFtp, 1.25); setBitmapTextLabel(*m_buttonGdriveAddUser, loadImage("user_add", dipToScreen(20)), m_buttonGdriveAddUser ->GetLabelText()); setBitmapTextLabel(*m_buttonGdriveRemoveUser, loadImage("user_remove", dipToScreen(20)), m_buttonGdriveRemoveUser->GetLabelText()); setImage(*m_bitmapGdriveUser, loadImage("user", dipToScreen(20))); setImage(*m_bitmapGdriveDrive, loadImage("drive", dipToScreen(20))); setImage(*m_bitmapServer, loadImage("server", dipToScreen(20))); setImage(*m_bitmapCloud, loadImage("cloud")); setImage(*m_bitmapPerf, loadImage("speed")); setImage(*m_bitmapServerDir, IconBuffer::genericDirIcon(IconBuffer::IconSize::small)); m_checkBoxShowPassword ->SetValue(false); m_checkBoxPasswordPrompt->SetValue(false); m_textCtrlServer->SetHint(_("Example:") + L" website.com 66.198.240.22"); m_textCtrlServer->SetMinSize({dipToWxsize(260), -1}); m_textCtrlPort->SetMinSize({dipToWxsize(60), -1}); setDefaultWidth(*m_spinCtrlConnectionCount); setDefaultWidth(*m_spinCtrlChannelCountSftp); setDefaultWidth(*m_spinCtrlTimeout); setupFileDrop(*m_panelAuth); m_panelAuth->Bind(EVENT_DROP_FILE, [this](FileDropEvent& event) { onKeyFileDropped(event); }); m_staticTextConnectionsLabelSub->SetLabelText(L'(' + _("Connections") + L')'); //use spacer to keep dialog height stable, no matter if key file options are visible bSizerAuthInner->Add(0, m_panelAuth->GetSize().y); //--------------------------------------------------------- std::vector gdriveAccounts; try { for (const std::string& loginEmail : gdriveListAccounts()) //throw FileError gdriveAccounts.push_back(utfTo(loginEmail)); } catch (const FileError& e) { showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); } m_listBoxGdriveUsers->Append(gdriveAccounts); //set default values for Google Drive: use first item of m_listBoxGdriveUsers if (!gdriveAccounts.empty() && !acceptsItemPathPhraseGdrive(folderPathPhrase)) { m_listBoxGdriveUsers->SetSelection(0); gdriveUpdateDrivesAndSelect(utfTo(gdriveAccounts[0]), Zstring() /*My Drive*/); } m_spinCtrlTimeout->SetValue(sftpDefault_.timeoutSec); assert(sftpDefault_.timeoutSec == FtpLogin().timeoutSec); //make sure the default values are in sync //--------------------------------------------------------- if (acceptsItemPathPhraseGdrive(folderPathPhrase)) { type_ = CloudType::gdrive; const AbstractPath folderPath = createItemPathGdrive(folderPathPhrase); const GdriveLogin login = extractGdriveLogin(folderPath.afsDevice); //noexcept if (const int selPos = m_listBoxGdriveUsers->FindString(utfTo(login.email), false /*caseSensitive*/); selPos != wxNOT_FOUND) { m_listBoxGdriveUsers->EnsureVisible(selPos); m_listBoxGdriveUsers->SetSelection(selPos); gdriveUpdateDrivesAndSelect(login.email, login.locationName); } else { m_listBoxGdriveUsers->DeselectAll(); m_listBoxGdriveDrives->Clear(); } m_textCtrlServerPath->ChangeValue(utfTo(FILE_NAME_SEPARATOR + folderPath.afsPath.value)); m_spinCtrlTimeout->SetValue(login.timeoutSec); } else if (acceptsItemPathPhraseSftp(folderPathPhrase)) { type_ = CloudType::sftp; const AbstractPath folderPath = createItemPathSftp(folderPathPhrase); const SftpLogin login = extractSftpLogin(folderPath.afsDevice); //noexcept if (login.portCfg > 0) m_textCtrlPort->ChangeValue(numberTo(login.portCfg)); m_textCtrlServer ->ChangeValue(utfTo(login.server)); m_textCtrlUserName ->ChangeValue(utfTo(login.username)); sftpAuthType_ = login.authType; if (login.password) m_textCtrlPasswordHidden->ChangeValue(utfTo(*login.password)); else m_checkBoxPasswordPrompt->SetValue(true); m_textCtrlKeyfilePath ->ChangeValue(utfTo(login.privateKeyFilePath)); m_textCtrlServerPath ->ChangeValue(utfTo(FILE_NAME_SEPARATOR + folderPath.afsPath.value)); m_checkBoxAllowZlib ->SetValue(login.allowZlib); m_spinCtrlTimeout ->SetValue(login.timeoutSec); m_spinCtrlChannelCountSftp->SetValue(login.traverserChannelsPerConnection); } else if (acceptsItemPathPhraseFtp(folderPathPhrase)) { type_ = CloudType::ftp; const AbstractPath folderPath = createItemPathFtp(folderPathPhrase); const FtpLogin login = extractFtpLogin(folderPath.afsDevice); //noexcept if (login.portCfg > 0) m_textCtrlPort->ChangeValue(numberTo(login.portCfg)); m_textCtrlServer ->ChangeValue(utfTo(login.server)); m_textCtrlUserName->ChangeValue(utfTo(login.username)); if (login.password) m_textCtrlPasswordHidden ->ChangeValue(utfTo(*login.password)); else m_checkBoxPasswordPrompt->SetValue(true); m_textCtrlServerPath->ChangeValue(utfTo(FILE_NAME_SEPARATOR + folderPath.afsPath.value)); (login.useTls ? m_radioBtnEncryptSsl : m_radioBtnEncryptNone)->SetValue(true); m_spinCtrlTimeout->SetValue(login.timeoutSec); } m_spinCtrlConnectionCount->SetValue(parallelOps); m_spinCtrlConnectionCount->Disable(); m_staticTextConnectionCountDescr->Hide(); m_spinCtrlChannelCountSftp->Disable(); m_buttonChannelCountSftp ->Disable(); //--------------------------------------------------------- //set up default view for dialog size calculation bSizerGdrive->Show(false); bSizerFtpEncrypt->Show(false); m_textCtrlPasswordVisible->Hide(); m_checkBoxPasswordPrompt->Hide(); GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() //=> works like a charm for GTK with window resizing problems and title bar corruption; e.g. Debian!!! #ifdef __WXGTK3__ Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 //Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404 #endif Center(); //apply *after* dialog size change! updateGui(); //*after* SetSizeHints when standard dialog height has been calculated m_buttonOK->SetFocus(); } void CloudSetupDlg::onGdriveUserAdd(wxCommandEvent& event) { guiQueue_.processAsync([timeoutSec = extractGdriveLogin(getFolderPath().afsDevice).timeoutSec]() -> std::variant { try { return gdriveAddUser(nullptr /*updateGui*/, timeoutSec); //throw FileError } catch (const FileError& e) { return e; } }, [this](const std::variant& result) { if (const FileError* e = std::get_if(&result)) showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e->toString())); else { const std::string& loginEmail = std::get(result); int selPos = m_listBoxGdriveUsers->FindString(utfTo(loginEmail), false /*caseSensitive*/); if (selPos == wxNOT_FOUND) selPos = m_listBoxGdriveUsers->Append(utfTo(loginEmail)); m_listBoxGdriveUsers->EnsureVisible(selPos); m_listBoxGdriveUsers->SetSelection(selPos); updateGui(); //enable remove user button gdriveUpdateDrivesAndSelect(loginEmail, Zstring() /*My Drive*/); } }); } void CloudSetupDlg::onGdriveUserRemove(wxCommandEvent& event) { const int selPos = m_listBoxGdriveUsers->GetSelection(); assert(selPos != wxNOT_FOUND); if (selPos != wxNOT_FOUND) try { const std::string& loginEmail = utfTo(m_listBoxGdriveUsers->GetString(selPos)); if (showConfirmationDialog(this, DialogInfoType::warning, PopupDialogCfg(). setTitle(_("Confirm")). setMainInstructions(replaceCpy(_("Do you really want to disconnect from user account %x?"), L"%x", utfTo(loginEmail))), _("&Disconnect")) != ConfirmationButton::accept) return; gdriveRemoveUser(loginEmail, extractGdriveLogin(getFolderPath().afsDevice).timeoutSec); //throw FileError m_listBoxGdriveUsers->Delete(selPos); updateGui(); //disable remove user button m_listBoxGdriveDrives->Clear(); } catch (const FileError& e) { showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); } } void CloudSetupDlg::onGdriveUserSelect(wxCommandEvent& event) { const int selPos = m_listBoxGdriveUsers->GetSelection(); assert(selPos != wxNOT_FOUND); if (selPos != wxNOT_FOUND) { const std::string& loginEmail = utfTo(m_listBoxGdriveUsers->GetString(selPos)); updateGui(); //enable remove user button gdriveUpdateDrivesAndSelect(loginEmail, Zstring() /*My Drive*/); } } void CloudSetupDlg::gdriveUpdateDrivesAndSelect(const std::string& accountEmail, const Zstring& locationToSelect) { m_listBoxGdriveDrives->Clear(); m_listBoxGdriveDrives->Append(txtLoading_); guiQueue_.processAsync([accountEmail, timeoutSec = extractGdriveLogin(getFolderPath().afsDevice).timeoutSec]() -> std::variant, FileError> { try { return gdriveListLocations(accountEmail, timeoutSec); //throw FileError } catch (const FileError& e) { return e; } }, [this, accountEmail, locationToSelect](std::variant, FileError>&& result) { if (const int selPos = m_listBoxGdriveUsers->GetSelection(); selPos == wxNOT_FOUND || utfTo(m_listBoxGdriveUsers->GetString(selPos)) != accountEmail) return; //different accountEmail selected in the meantime! m_listBoxGdriveDrives->Clear(); if (const FileError* e = std::get_if(&result)) showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e->toString())); else { auto& locationNames = std::get>(result); std::sort(locationNames.begin(), locationNames.end(), LessNaturalSort()); m_listBoxGdriveDrives->Append(txtMyDrive_); //sort locations, but keep "My Drive" at top for (const Zstring& itemLabel : locationNames) m_listBoxGdriveDrives->Append(utfTo(itemLabel)); const wxString labelToSelect = locationToSelect.empty() ? txtMyDrive_ : utfTo(locationToSelect); if (const int selPos = m_listBoxGdriveDrives->FindString(labelToSelect, true /*caseSensitive*/); selPos != wxNOT_FOUND) { m_listBoxGdriveDrives->EnsureVisible(selPos); m_listBoxGdriveDrives->SetSelection (selPos); } } }); } void CloudSetupDlg::onDetectServerChannelLimit(wxCommandEvent& event) { assert (type_ == CloudType::sftp); try { m_spinCtrlChannelCountSftp->SetSelection(0, 0); //some visual feedback: clear selection m_spinCtrlChannelCountSftp->Refresh(); //both needed for wxGTK: meh! m_spinCtrlChannelCountSftp->Update(); // AbstractPath folderPath = getFolderPath(); //noexcept //------------------------------------------------------------------- auto requestPassword = [&, password = Zstring()](const std::wstring& msg, const std::wstring& lastErrorMsg) mutable { assert(runningOnMainThread()); if (showPasswordPrompt(this, msg, lastErrorMsg, password) != ConfirmationButton::accept) throw CancelProcess(); return password; }; AFS::authenticateAccess(folderPath.afsDevice, requestPassword); //throw FileError, CancelProcess //------------------------------------------------------------------- const int channelCountMax = getServerMaxChannelsPerConnection(extractSftpLogin(folderPath.afsDevice)); //throw FileError m_spinCtrlChannelCountSftp->SetValue(channelCountMax); m_spinCtrlChannelCountSftp->SetFocus(); //[!] otherwise selection is lost m_spinCtrlChannelCountSftp->SetSelection(-1, -1); //some visual feedback: select all } catch (CancelProcess&) { return; } catch (const FileError& e) { showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); } } void CloudSetupDlg::onToggleShowPassword(wxCommandEvent& event) { assert(type_ != CloudType::gdrive); if (m_checkBoxShowPassword->GetValue()) m_textCtrlPasswordVisible->ChangeValue(m_textCtrlPasswordHidden->GetValue()); else m_textCtrlPasswordHidden->ChangeValue(m_textCtrlPasswordVisible->GetValue()); updateGui(); wxTextCtrl& textCtrl = *(m_checkBoxShowPassword->GetValue() ? m_textCtrlPasswordVisible : m_textCtrlPasswordHidden); textCtrl.SetFocus(); //macOS: selects text as unwanted side effect => *before* SetInsertionPointEnd() textCtrl.SetInsertionPointEnd(); } void CloudSetupDlg::onTypingPassword(wxCommandEvent& event) { assert(m_staticTextPassword->IsShown()); const wxString password = (m_checkBoxShowPassword->GetValue() ? m_textCtrlPasswordVisible : m_textCtrlPasswordHidden)->GetValue(); if (m_checkBoxShowPassword ->IsShown() != !password.empty() || //let's avoid some minor flicker m_checkBoxPasswordPrompt->IsShown() != password.empty()) //in updateGui() Dimensions() updateGui(); } bool CloudSetupDlg::acceptFileDrop(const std::vector& shellItemPaths) { if (shellItemPaths.empty()) return false; const Zstring ext = getFileExtension(shellItemPaths[0]); return ext.empty() || equalAsciiNoCase(ext, "pem") || equalAsciiNoCase(ext, "ppk"); } void CloudSetupDlg::onKeyFileDropped(FileDropEvent& event) { //assert (type_ == CloudType::SFTP); -> no big deal if false if (!event.itemPaths_.empty()) { m_textCtrlKeyfilePath->ChangeValue(utfTo(event.itemPaths_[0])); sftpAuthType_ = SftpAuthType::keyFile; updateGui(); } } void CloudSetupDlg::onSelectKeyfile(wxCommandEvent& event) { assert (type_ == CloudType::sftp && sftpAuthType_ == SftpAuthType::keyFile); std::optional defaultFolderPath = getParentFolderPath(sftpKeyFileLastSelected_); wxFileDialog fileSelector(this, wxString() /*message*/, utfTo(defaultFolderPath ? *defaultFolderPath : Zstr("")), wxString() /*default file name*/, _("All files") + L" (*.*)|*" + L"|" + L"OpenSSL PEM (*.pem)|*.pem" + L"|" + L"PuTTY Private Key (*.ppk)|*.ppk", wxFD_OPEN); if (fileSelector.ShowModal() != wxID_OK) return; m_textCtrlKeyfilePath->ChangeValue(fileSelector.GetPath()); sftpKeyFileLastSelected_ = utfTo(fileSelector.GetPath()); } void CloudSetupDlg::updateGui() { m_toggleBtnGdrive->SetValue(type_ == CloudType::gdrive); m_toggleBtnSftp ->SetValue(type_ == CloudType::sftp); m_toggleBtnFtp ->SetValue(type_ == CloudType::ftp); bSizerGdrive->Show(type_ == CloudType::gdrive); bSizerServer->Show(type_ == CloudType::ftp || type_ == CloudType::sftp); bSizerAuth ->Show(type_ == CloudType::ftp || type_ == CloudType::sftp); bSizerFtpEncrypt->Show(type_ == CloudType:: ftp); bSizerSftpAuth ->Show(type_ == CloudType::sftp); m_staticTextKeyfile->Show(type_ == CloudType::sftp && sftpAuthType_ == SftpAuthType::keyFile); bSizerKeyFile ->Show(type_ == CloudType::sftp && sftpAuthType_ == SftpAuthType::keyFile); m_staticTextPassword->Show(type_ == CloudType::ftp || (type_ == CloudType::sftp && sftpAuthType_ != SftpAuthType::agent)); bSizerPassword ->Show(type_ == CloudType::ftp || (type_ == CloudType::sftp && sftpAuthType_ != SftpAuthType::agent)); if (m_staticTextPassword->IsShown()) { m_textCtrlPasswordVisible->Show( m_checkBoxShowPassword->GetValue()); m_textCtrlPasswordHidden ->Show(!m_checkBoxShowPassword->GetValue()); m_textCtrlPasswordVisible->Enable(!m_checkBoxPasswordPrompt->GetValue()); m_textCtrlPasswordHidden ->Enable(!m_checkBoxPasswordPrompt->GetValue()); const wxString password = (m_checkBoxShowPassword->GetValue() ? m_textCtrlPasswordVisible : m_textCtrlPasswordHidden)->GetValue(); m_checkBoxShowPassword ->Show(!password.empty()); m_checkBoxPasswordPrompt->Show( password.empty()); } switch (type_) { case CloudType::gdrive: m_buttonGdriveRemoveUser->Enable(m_listBoxGdriveUsers->GetSelection() != wxNOT_FOUND); break; case CloudType::sftp: m_radioBtnPassword->SetValue(false); m_radioBtnKeyfile ->SetValue(false); m_radioBtnAgent ->SetValue(false); m_textCtrlPort->SetHint(numberTo(DEFAULT_PORT_SFTP)); switch (sftpAuthType_) //*not* owned by GUI controls { case SftpAuthType::password: m_radioBtnPassword->SetValue(true); m_staticTextPassword->SetLabelText(_("Password:")); break; case SftpAuthType::keyFile: m_radioBtnKeyfile->SetValue(true); m_staticTextPassword->SetLabelText(_("Key passphrase:")); break; case SftpAuthType::agent: m_radioBtnAgent->SetValue(true); break; } break; case CloudType::ftp: m_textCtrlPort->SetHint(numberTo(DEFAULT_PORT_FTP)); m_staticTextPassword->SetLabelText(_("Password:")); break; } m_staticTextChannelCountSftp->Show(type_ == CloudType::sftp); m_spinCtrlChannelCountSftp ->Show(type_ == CloudType::sftp); m_buttonChannelCountSftp ->Show(type_ == CloudType::sftp); m_checkBoxAllowZlib ->Show(type_ == CloudType::sftp); m_staticTextZlibDescr ->Show(type_ == CloudType::sftp); Layout(); //needed! hidden items are not considered during resize Refresh(); } bool CloudSetupDlg::validateParameters() { if (type_ == CloudType::sftp || type_ == CloudType::ftp) { if (trimCpy(m_textCtrlServer->GetValue()).empty()) { showNotificationDialog(this, DialogInfoType::info, PopupDialogCfg().setMainInstructions(_("Server name must not be empty."))); m_textCtrlServer->SetFocus(); return false; } } switch (type_) { case CloudType::gdrive: if (m_listBoxGdriveUsers->GetSelection() == wxNOT_FOUND) { showNotificationDialog(this, DialogInfoType::info, PopupDialogCfg().setMainInstructions(_("Please select a user account first."))); return false; } break; case CloudType::sftp: //username *required* for SFTP, but optional for FTP: libcurl will use "anonymous" if (trimCpy(m_textCtrlUserName->GetValue()).empty()) { showNotificationDialog(this, DialogInfoType::info, PopupDialogCfg().setMainInstructions(_("Please enter a username."))); m_textCtrlUserName->SetFocus(); return false; } if (sftpAuthType_ == SftpAuthType::keyFile) if (trimCpy(m_textCtrlKeyfilePath->GetValue()).empty()) { showNotificationDialog(this, DialogInfoType::info, PopupDialogCfg().setMainInstructions(_("Please enter a file path."))); //don't show error icon to follow "Windows' encouraging tone" m_textCtrlKeyfilePath->SetFocus(); return false; } break; case CloudType::ftp: break; } return true; } AbstractPath CloudSetupDlg::getFolderPath() const { //clean up (messy) user input, but no trim: support folders with trailing blanks! const AfsPath serverRelPath = sanitizeDeviceRelativePath(utfTo(m_textCtrlServerPath->GetValue())); switch (type_) { case CloudType::gdrive: { GdriveLogin login; if (const int selPos = m_listBoxGdriveUsers->GetSelection(); selPos != wxNOT_FOUND) { login.email = utfTo(m_listBoxGdriveUsers->GetString(selPos)); if (const int selPos2 = m_listBoxGdriveDrives->GetSelection(); selPos2 != wxNOT_FOUND) { if (const wxString& locationName = m_listBoxGdriveDrives->GetString(selPos2); locationName != txtMyDrive_ && locationName != txtLoading_) login.locationName = utfTo(locationName); } } login.timeoutSec = m_spinCtrlTimeout->GetValue(); return AbstractPath(condenseToGdriveDevice(login), serverRelPath); //noexcept } case CloudType::sftp: { SftpLogin login; login.server = utfTo(m_textCtrlServer ->GetValue()); login.portCfg = stringTo (m_textCtrlPort ->GetValue()); //0 if empty login.username = utfTo(m_textCtrlUserName->GetValue()); login.authType = sftpAuthType_; login.privateKeyFilePath = utfTo(m_textCtrlKeyfilePath->GetValue()); if (m_checkBoxPasswordPrompt->GetValue()) login.password = std::nullopt; else login.password = utfTo((m_checkBoxShowPassword->GetValue() ? m_textCtrlPasswordVisible : m_textCtrlPasswordHidden)->GetValue()); login.allowZlib = m_checkBoxAllowZlib->GetValue(); login.timeoutSec = m_spinCtrlTimeout->GetValue(); login.traverserChannelsPerConnection = m_spinCtrlChannelCountSftp->GetValue(); return AbstractPath(condenseToSftpDevice(login), serverRelPath); //noexcept } case CloudType::ftp: { FtpLogin login; login.server = utfTo(m_textCtrlServer ->GetValue()); login.portCfg = stringTo (m_textCtrlPort ->GetValue()); //0 if empty login.username = utfTo(m_textCtrlUserName->GetValue()); if (m_checkBoxPasswordPrompt->GetValue()) login.password = std::nullopt; else login.password = utfTo((m_checkBoxShowPassword->GetValue() ? m_textCtrlPasswordVisible : m_textCtrlPasswordHidden)->GetValue()); login.useTls = m_radioBtnEncryptSsl->GetValue(); login.timeoutSec = m_spinCtrlTimeout->GetValue(); return AbstractPath(condenseToFtpDevice(login), serverRelPath); //noexcept } } assert(false); return createAbstractPath(Zstr("")); } void CloudSetupDlg::onBrowseCloudFolder(wxCommandEvent& event) { if (!validateParameters()) return; AbstractPath folderPath = getFolderPath(); //noexcept try { //------------------------------------------------------------------- auto requestPassword = [&, password = Zstring()](const std::wstring& msg, const std::wstring& lastErrorMsg) mutable { assert(runningOnMainThread()); if (showPasswordPrompt(this, msg, lastErrorMsg, password) != ConfirmationButton::accept) throw CancelProcess(); return password; }; AFS::authenticateAccess(folderPath.afsDevice, requestPassword); //throw FileError, CancelProcess //caveat: this could block *indefinitely* for Google Drive, but luckily already authenticated in this context //------------------------------------------------------------------- // //for (S)FTP it makes more sense to start with the home directory rather than root (which often denies access!) if (!AFS::getParentPath(folderPath)) { if (type_ == CloudType::sftp) folderPath.afsPath = getSftpHomePath(extractSftpLogin(folderPath.afsDevice)); //throw FileError if (type_ == CloudType::ftp) folderPath.afsPath = getFtpHomePath(extractFtpLogin(folderPath.afsDevice)); //throw FileError } } catch (CancelProcess&) { return; } catch (const FileError& e) { showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); return; } if (showAbstractFolderPicker(this, folderPath) == ConfirmationButton::accept) m_textCtrlServerPath->ChangeValue(utfTo(FILE_NAME_SEPARATOR + folderPath.afsPath.value)); } void CloudSetupDlg::onOkay(wxCommandEvent& event) { //------- parameter validation (BEFORE writing output!) ------- if (!validateParameters()) return; //------------------------------------------------------------- folderPathPhraseOut_ = AFS::getInitPathPhrase(getFolderPath()); parallelOpsOut_ = m_spinCtrlConnectionCount->GetValue(); EndModal(static_cast(ConfirmationButton::accept)); } } ConfirmationButton fff::showCloudSetupDialog(wxWindow* parent, Zstring& folderPathPhrase, Zstring& sftpKeyFileLastSelected, size_t& parallelOps, bool canChangeParallelOp) { CloudSetupDlg dlg(parent, folderPathPhrase, sftpKeyFileLastSelected, parallelOps, canChangeParallelOp); return static_cast(dlg.ShowModal()); } //######################################################################################## namespace { class CopyToDialog : public CopyToDlgGenerated { public: CopyToDialog(wxWindow* parent, const std::wstring& itemList, int itemCount, Zstring& targetFolderPath, Zstring& targetFolderLastSelected, std::vector& folderHistory, size_t folderHistoryMax, Zstring& sftpKeyFileLastSelected, bool& keepRelPaths, bool& overwriteIfExists); private: void onOkay (wxCommandEvent& event) override; void onCancel(wxCommandEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } void onClose (wxCloseEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } void onLocalKeyEvent(wxKeyEvent& event); std::optional targetFolder; //always bound //output-only parameters: Zstring& targetFolderPathOut_; bool& keepRelPathsOut_; bool& overwriteIfExistsOut_; std::vector& folderHistoryOut_; }; CopyToDialog::CopyToDialog(wxWindow* parent, const std::wstring& itemList, int itemCount, Zstring& targetFolderPath, Zstring& targetFolderLastSelected, std::vector& folderHistory, size_t folderHistoryMax, Zstring& sftpKeyFileLastSelected, bool& keepRelPaths, bool& overwriteIfExists) : CopyToDlgGenerated(parent), targetFolderPathOut_(targetFolderPath), keepRelPathsOut_(keepRelPaths), overwriteIfExistsOut_(overwriteIfExists), folderHistoryOut_(folderHistory) { setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonOK).setCancel(m_buttonCancel)); setMainInstructionFont(*m_staticTextHeader); setImage(*m_bitmapCopyTo, loadImage("copy_to")); targetFolder.emplace(this, *this, *m_buttonSelectTargetFolder, *m_bpButtonSelectAltTargetFolder, *m_targetFolderPath, targetFolderLastSelected, sftpKeyFileLastSelected, nullptr /*staticText*/, nullptr /*wxWindow*/, nullptr /*droppedPathsFilter*/, [](const Zstring& folderPathPhrase) { return 1; } /*getDeviceParallelOps*/, nullptr /*setDeviceParallelOps*/); m_targetFolderPath->setHistory(std::make_shared(folderHistory, folderHistoryMax)); m_textCtrlFileList->SetMinSize({dipToWxsize(500), dipToWxsize(200)}); /* There is a nasty bug on wxGTK under Ubuntu: If a multi-line wxTextCtrl contains so many lines that scrollbars are shown, it re-enables all windows that are supposed to be disabled during the current modal loop! This only affects Ubuntu/wxGTK! No such issue on Debian/wxGTK or Suse/wxGTK => another Unity problem like the following? https://github.com/wxWidgets/wxWidgets/issues/14823 "Menu not disabled when showing modal dialogs in wxGTK under Unity" */ m_staticTextHeader->SetLabelText(_P("Copy the following item to another folder?", "Copy the following %x items to another folder?", itemCount)); m_staticTextHeader->Wrap(dipToWxsize(460)); //needs to be reapplied after SetLabel() m_textCtrlFileList->ChangeValue(itemList); //----------------- set config --------------------------------- targetFolder ->setPath(targetFolderPath); m_checkBoxKeepRelPath ->SetValue(keepRelPaths); m_checkBoxOverwriteIfExists->SetValue(overwriteIfExists); //----------------- /set config -------------------------------- Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onLocalKeyEvent(event); }); //enable dialog-specific key events GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() #ifdef __WXGTK3__ Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 //Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404 #endif Center(); //apply *after* dialog size change! m_buttonOK->SetFocus(); } void CopyToDialog::onLocalKeyEvent(wxKeyEvent& event) //process key events without explicit menu entry :) { switch (event.GetKeyCode()) { case WXK_RETURN: case WXK_NUMPAD_ENTER: if (event.ControlDown()) //Ctrl+Enter or on macOS: Command+Enter { wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED); m_buttonOK->Command(dummy); //simulate click return; } break; } event.Skip(); } void CopyToDialog::onOkay(wxCommandEvent& event) { //------- parameter validation (BEFORE writing output!) ------- if (trimCpy(targetFolder->getPath()).empty()) { showNotificationDialog(this, DialogInfoType::info, PopupDialogCfg().setMainInstructions(_("Please enter a target folder."))); //don't show error icon to follow "Windows' encouraging tone" m_targetFolderPath->SetFocus(); return; } m_targetFolderPath->getHistory()->addItem(targetFolder->getPath()); //------------------------------------------------------------- targetFolderPathOut_ = targetFolder->getPath(); keepRelPathsOut_ = m_checkBoxKeepRelPath->GetValue(); overwriteIfExistsOut_ = m_checkBoxOverwriteIfExists->GetValue(); folderHistoryOut_ = m_targetFolderPath->getHistory()->getList(); EndModal(static_cast(ConfirmationButton::accept)); } } ConfirmationButton fff::showCopyToDialog(wxWindow* parent, const std::wstring& itemList, int itemCount, Zstring& targetFolderPath, Zstring& targetFolderLastSelected, std::vector& folderHistory, size_t folderHistoryMax, Zstring& sftpKeyFileLastSelected, bool& keepRelPaths, bool& overwriteIfExists) { CopyToDialog dlg(parent, itemList, itemCount, targetFolderPath, targetFolderLastSelected, folderHistory, folderHistoryMax, sftpKeyFileLastSelected, keepRelPaths, overwriteIfExists); return static_cast(dlg.ShowModal()); } //######################################################################################## namespace { class DeleteDialog : public DeleteDlgGenerated { public: DeleteDialog(wxWindow* parent, const std::wstring& itemList, int itemCount, bool& useRecycleBin); private: void onUseRecycler(wxCommandEvent& event) override { updateGui(); } void onOkay (wxCommandEvent& event) override; void onCancel (wxCommandEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } void onClose (wxCloseEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } void onLocalKeyEvent(wxKeyEvent& event); void updateGui(); const int itemCount_ = 0; const std::chrono::steady_clock::time_point dlgStartTime_ = std::chrono::steady_clock::now(); const wxImage imgTrash_ = [] { wxImage imgDefault = loadImage("delete_recycler"); //use system icon if available (can fail on Linux??) try { return extractWxImage(fff::getTrashIcon(imgDefault.GetHeight())); /*throw SysError*/ } catch (SysError&) { assert(false); return imgDefault; } }(); //output-only parameters: bool& useRecycleBinOut_; }; DeleteDialog::DeleteDialog(wxWindow* parent, const std::wstring& itemList, int itemCount, bool& useRecycleBin) : DeleteDlgGenerated(parent), itemCount_(itemCount), useRecycleBinOut_(useRecycleBin) { setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonOK).setCancel(m_buttonCancel)); setMainInstructionFont(*m_staticTextHeader); m_textCtrlFileList->SetMinSize({dipToWxsize(500), dipToWxsize(200)}); wxString itemList2(itemList); trim(itemList2); //remove trailing newline m_textCtrlFileList->ChangeValue(itemList2); /* There is a nasty bug on wxGTK under Ubuntu: If a multi-line wxTextCtrl contains so many lines that scrollbars are shown, it re-enables all windows that are supposed to be disabled during the current modal loop! This only affects Ubuntu/wxGTK! No such issue on Debian/wxGTK or Suse/wxGTK => another Unity problem like the following? https://github.com/wxWidgets/wxWidgets/issues/14823 "Menu not disabled when showing modal dialogs in wxGTK under Unity" */ m_checkBoxUseRecycler->SetValue(useRecycleBin); updateGui(); Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onLocalKeyEvent(event); }); //enable dialog-specific key events GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() #ifdef __WXGTK3__ Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 //Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404 #endif Center(); //apply *after* dialog size change! m_buttonOK->SetFocus(); } void DeleteDialog::updateGui() { if (m_checkBoxUseRecycler->GetValue()) { setImage(*m_bitmapDeleteType, imgTrash_); m_staticTextHeader->SetLabelText(_P("Do you really want to move the following item to the recycle bin?", "Do you really want to move the following %x items to the recycle bin?", itemCount_)); m_buttonOK->SetLabelText(_("Move")); //no access key needed: use ENTER! } else { setImage(*m_bitmapDeleteType, loadImage("delete_permanently")); m_staticTextHeader->SetLabelText(_P("Do you really want to delete the following item?", "Do you really want to delete the following %x items?", itemCount_)); m_buttonOK->SetLabelText(wxControl::RemoveMnemonics(_("&Delete"))); //no access key needed: use ENTER! } m_staticTextHeader->Wrap(dipToWxsize(460)); //needs to be reapplied after SetLabel() Layout(); Refresh(); //needed after m_buttonOK label change } void DeleteDialog::onLocalKeyEvent(wxKeyEvent& event) { switch (event.GetKeyCode()) { case WXK_RETURN: case WXK_NUMPAD_ENTER: if (event.ControlDown()) //Ctrl+Enter or on macOS: Command+Enter { wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED); m_buttonOK->Command(dummy); //simulate click return; } break; } event.Skip(); } void DeleteDialog::onOkay(wxCommandEvent& event) { //additional safety net, similar to File Explorer: time delta between DEL and ENTER must be at least 50ms to avoid accidental deletion! if (std::chrono::steady_clock::now() < dlgStartTime_ + std::chrono::milliseconds(50)) //considers chrono-wrap-around! return; useRecycleBinOut_ = m_checkBoxUseRecycler->GetValue(); EndModal(static_cast(ConfirmationButton::accept)); } } ConfirmationButton fff::showDeleteDialog(wxWindow* parent, const std::wstring& itemList, int itemCount, bool& useRecycleBin) { DeleteDialog dlg(parent, itemList, itemCount, useRecycleBin); return static_cast(dlg.ShowModal()); } //######################################################################################## namespace { class SyncConfirmationDlg : public SyncConfirmationDlgGenerated { public: SyncConfirmationDlg(wxWindow* parent, bool syncSelection, std::optional syncVar, const SyncStatistics& st, bool& dontShowAgain); private: void onStartSync(wxCommandEvent& event) override; void onCancel (wxCommandEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } void onClose (wxCloseEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } void onLocalKeyEvent(wxKeyEvent& event); //output-only parameters: bool& dontShowAgainOut_; }; SyncConfirmationDlg::SyncConfirmationDlg(wxWindow* parent, bool syncSelection, std::optional syncVar, const SyncStatistics& st, bool& dontShowAgain) : SyncConfirmationDlgGenerated(parent), dontShowAgainOut_(dontShowAgain) { setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonOK).setCancel(m_buttonCancel)); setMainInstructionFont(*m_staticTextCaption); setImage(*m_bitmapSync, loadImage(syncSelection ? "start_sync_selection" : "start_sync")); m_staticTextCaption->SetLabelText(syncSelection ?_("Start to synchronize the selection?") : _("Start synchronization now?")); m_staticTextSyncVar->SetLabelText(getVariantName(syncVar)); const char* varImgName = nullptr; if (syncVar) switch (*syncVar) { case SyncVariant::twoWay: varImgName = "sync_twoway"; break; case SyncVariant::mirror: varImgName = "sync_mirror"; break; case SyncVariant::update: varImgName = "sync_update"; break; case SyncVariant::custom: varImgName = "sync_custom"; break; } if (varImgName) setImage(*m_bitmapSyncVar, loadImage(varImgName, -1 /*maxWidth*/, dipToScreen(getMenuIconDipSize()))); m_checkBoxDontShowAgain->SetValue(dontShowAgain); Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onLocalKeyEvent(event); }); //update preview of item count and bytes to be transferred: auto setValue = [](wxStaticText& txtControl, bool isZeroValue, const wxString& valueAsString, wxStaticBitmap& bmpControl, const char* imageName) { wxFont fnt = txtControl.GetFont(); fnt.SetWeight(isZeroValue ? wxFONTWEIGHT_NORMAL : wxFONTWEIGHT_BOLD); txtControl.SetFont(fnt); setText(txtControl, valueAsString); setImage(bmpControl, greyScaleIfDisabled(mirrorIfRtl(loadImage(imageName)), !isZeroValue)); }; auto setIntValue = [&setValue](wxStaticText& txtControl, int value, wxStaticBitmap& bmpControl, const char* imageName) { setValue(txtControl, value == 0, formatNumber(value), bmpControl, imageName); }; setValue(*m_staticTextData, st.getBytesToProcess() == 0, formatFilesizeShort(st.getBytesToProcess()), *m_bitmapData, "data"); setIntValue(*m_staticTextCreateLeft, st.createCount(), *m_bitmapCreateLeft, "so_create_left_sicon"); setIntValue(*m_staticTextUpdateLeft, st.updateCount(), *m_bitmapUpdateLeft, "so_update_left_sicon"); setIntValue(*m_staticTextDeleteLeft, st.deleteCount(), *m_bitmapDeleteLeft, "so_delete_left_sicon"); setIntValue(*m_staticTextCreateRight, st.createCount(), *m_bitmapCreateRight, "so_create_right_sicon"); setIntValue(*m_staticTextUpdateRight, st.updateCount(), *m_bitmapUpdateRight, "so_update_right_sicon"); setIntValue(*m_staticTextDeleteRight, st.deleteCount(), *m_bitmapDeleteRight, "so_delete_right_sicon"); GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() #ifdef __WXGTK3__ Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 //Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404 #endif Center(); //apply *after* dialog size change! m_buttonOK->SetFocus(); } void SyncConfirmationDlg::onLocalKeyEvent(wxKeyEvent& event) { switch (event.GetKeyCode()) { case WXK_RETURN: case WXK_NUMPAD_ENTER: if (event.ControlDown()) //Ctrl+Enter or on macOS: Command+Enter { wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED); m_buttonOK->Command(dummy); //simulate click return; } break; } event.Skip(); } void SyncConfirmationDlg::onStartSync(wxCommandEvent& event) { dontShowAgainOut_ = m_checkBoxDontShowAgain->GetValue(); EndModal(static_cast(ConfirmationButton::accept)); } } ConfirmationButton fff::showSyncConfirmationDlg(wxWindow* parent, bool syncSelection, std::optional syncVar, const SyncStatistics& statistics, bool& dontShowAgain) { SyncConfirmationDlg dlg(parent, syncSelection, syncVar, statistics, dontShowAgain); return static_cast(dlg.ShowModal()); } //######################################################################################## namespace { class OptionsDlg : public OptionsDlgGenerated { public: OptionsDlg(wxWindow* parent, GlobalConfig& globalCfg); private: void onOkay (wxCommandEvent& event) override; void onShowHiddenDialogs (wxCommandEvent& event) override { expandConfigArea(ConfigArea::hidden); }; void onShowContextCustomize(wxCommandEvent& event) override { expandConfigArea(ConfigArea::context); }; void onDefault (wxCommandEvent& event) override; void onCancel (wxCommandEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } void onClose (wxCloseEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } void onAddRow (wxCommandEvent& event) override; void onRemoveRow (wxCommandEvent& event) override; void onShowLogFolder (wxCommandEvent& event) override; void onToggleLogfilesLimit(wxCommandEvent& event) override { updateGui(); } void onToggleHiddenDialog (wxCommandEvent& event) override { updateGui(); } void onSelectSoundCompareDone (wxCommandEvent& event) override { selectSound(*m_textCtrlSoundPathCompareDone); } void onSelectSoundSyncDone (wxCommandEvent& event) override { selectSound(*m_textCtrlSoundPathSyncDone); } void onSelectSoundAlertPending(wxCommandEvent& event) override { selectSound(*m_textCtrlSoundPathAlertPending); } void selectSound(wxTextCtrl& txtCtrl); void onChangeSoundFilePath(wxCommandEvent& event) override { updateGui(); } void onChangeColorTheme (wxCommandEvent& event) override { updateGui(); } void onPlayCompareDone (wxCommandEvent& event) override { playSoundWithDiagnostics(trimCpy(m_textCtrlSoundPathCompareDone ->GetValue())); } void onPlaySyncDone (wxCommandEvent& event) override { playSoundWithDiagnostics(trimCpy(m_textCtrlSoundPathSyncDone ->GetValue())); } void onPlayAlertPending(wxCommandEvent& event) override { playSoundWithDiagnostics(trimCpy(m_textCtrlSoundPathAlertPending->GetValue())); } void playSoundWithDiagnostics(const wxString& filePath); void onGridResize(wxEvent& event); void onGridContext(wxGridEvent& event); void copySelectionToClipboard() const; void updateGui(); void onLocalKeyEvent(wxKeyEvent& event); enum class ConfigArea { hidden, context }; void expandConfigArea(ConfigArea area); //work around defunct keyboard focus on macOS (or is it wxMac?) => not needed for this dialog! //void onLocalKeyEvent(wxKeyEvent& event); void setExtApp(const std::vector& extApp); std::vector getExtApp() const; std::unordered_map descriptionTransToEng_; //mapping for external application config const GlobalConfig defaultCfg_; EnumDescrList enumColorTheme_ { *m_choiceColorTheme, { {ColorTheme::System, wxControl::RemoveMnemonics(_("&Default")), {}/*tooltip*/}, {ColorTheme::Light, _("Light"), {}/*tooltip*/}, {ColorTheme::Dark, _("Dark"), {}/*tooltip*/}, } }; std::optional colorThemeIcon_; std::vector /*get dialog shown status*/, std::function /*set dialog shown status*/, wxString /*dialog message*/>> hiddenDialogCfgMapping_ { //*INDENT-OFF* {[](const GlobalConfig& cfg){ return cfg.confirmDlgs.confirmSyncStart; }, []( GlobalConfig& cfg, bool show){ cfg.confirmDlgs.confirmSyncStart = show; }, _("Start synchronization now?")}, {[](const GlobalConfig& cfg){ return cfg.confirmDlgs.confirmSaveConfig; }, []( GlobalConfig& cfg, bool show){ cfg.confirmDlgs.confirmSaveConfig = show; }, _("Do you want to save changes to %x?")}, {[](const GlobalConfig& cfg){ return !cfg.progressDlgAutoClose; }, []( GlobalConfig& cfg, bool show){ cfg.progressDlgAutoClose = !show; }, _("Leave progress dialog open after synchronization. (don't auto-close)")}, {[](const GlobalConfig& cfg){ return cfg.confirmDlgs.confirmSwapSides; }, []( GlobalConfig& cfg, bool show){ cfg.confirmDlgs.confirmSwapSides = show; }, _("Please confirm you want to swap sides.")}, {[](const GlobalConfig& cfg){ return cfg.confirmDlgs.confirmCommandMassInvoke; }, []( GlobalConfig& cfg, bool show){ cfg.confirmDlgs.confirmCommandMassInvoke = show; }, _P("Do you really want to execute the command %y for one item?", "Do you really want to execute the command %y for %x items?", 42)}, {[](const GlobalConfig& cfg){ return cfg.warnDlgs.warnFolderNotExisting; }, []( GlobalConfig& cfg, bool show){ cfg.warnDlgs.warnFolderNotExisting = show; }, _("The following folders do not yet exist:") + L" [...] " + _("The folders are created automatically when needed.")}, {[](const GlobalConfig& cfg){ return cfg.warnDlgs.warnFoldersDifferInCase; }, []( GlobalConfig& cfg, bool show){ cfg.warnDlgs.warnFoldersDifferInCase = show; }, _("The following folder paths differ in case. Please use a single form in order to avoid duplicate accesses.")}, {[](const GlobalConfig& cfg){ return cfg.warnDlgs.warnDependentFolderPair; }, []( GlobalConfig& cfg, bool show){ cfg.warnDlgs.warnDependentFolderPair = show; }, _("One folder of the folder pair is a subfolder of the other.") + L' ' + _("The folder should be excluded via filter.")}, {[](const GlobalConfig& cfg){ return cfg.warnDlgs.warnDependentBaseFolders; }, []( GlobalConfig& cfg, bool show){ cfg.warnDlgs.warnDependentBaseFolders = show; }, _("Some files will be synchronized as part of multiple folder pairs.") + L' ' + _("To avoid conflicts, set up exclude filters so that each updated file is included by only one folder pair.")}, {[](const GlobalConfig& cfg){ return cfg.warnDlgs.warnSignificantDifference; }, []( GlobalConfig& cfg, bool show){ cfg.warnDlgs.warnSignificantDifference = show; }, _("The following folders are significantly different. Please check that the correct folders are selected for synchronization.")}, {[](const GlobalConfig& cfg){ return cfg.warnDlgs.warnNotEnoughDiskSpace; }, []( GlobalConfig& cfg, bool show){ cfg.warnDlgs.warnNotEnoughDiskSpace = show; }, _("Not enough free disk space available in:")}, {[](const GlobalConfig& cfg){ return cfg.warnDlgs.warnUnresolvedConflicts; }, []( GlobalConfig& cfg, bool show){ cfg.warnDlgs.warnUnresolvedConflicts = show; }, _("The following items have unresolved conflicts and will not be synchronized:")}, {[](const GlobalConfig& cfg){ return cfg.warnDlgs.warnRecyclerMissing; }, []( GlobalConfig& cfg, bool show){ cfg.warnDlgs.warnRecyclerMissing = show; }, _("The recycle bin is not available for %x.") + L' ' + _("Ignore and delete permanently each time recycle bin is unavailable?")}, {[](const GlobalConfig& cfg){ return cfg.warnDlgs.warnDirectoryLockFailed; }, []( GlobalConfig& cfg, bool show){ cfg.warnDlgs.warnDirectoryLockFailed = show; }, _("Cannot set directory locks for the following folders:")}, {[](const GlobalConfig& cfg){ return cfg.warnDlgs.warnVersioningFolderPartOfSync; }, []( GlobalConfig& cfg, bool show){ cfg.warnDlgs.warnVersioningFolderPartOfSync = show; }, _("The versioning folder must not be part of the synchronization.") + L' ' + _("The folder should be excluded via filter.")}, //*INDENT-ON* }; FolderSelector logFolderSelector_; //output-only parameters: GlobalConfig& globalCfgOut_; }; OptionsDlg::OptionsDlg(wxWindow* parent, GlobalConfig& globalCfg) : OptionsDlgGenerated(parent), logFolderSelector_(this, *m_panelLogfile, *m_buttonSelectLogFolder, *m_bpButtonSelectAltLogFolder, *m_logFolderPath, globalCfg.logFolderLastSelected, globalCfg.sftpKeyFileLastSelected, nullptr /*staticText*/, nullptr /*dropWindow2*/, nullptr /*droppedPathsFilter*/, [](const Zstring& folderPathPhrase) { return 1; } /*getDeviceParallelOps_*/, nullptr /*setDeviceParallelOps_*/), globalCfgOut_(globalCfg) { setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonOK).setCancel(m_buttonCancel)); //setMainInstructionFont(*m_staticTextHeader); const wxImage imgFileManagerSmall_([] { try { return extractWxImage(fff::getFileManagerIcon(dipToScreen(20))); /*throw SysError*/ } catch (SysError&) { assert(false); return loadImage("file_manager", dipToScreen(20)); } }()); setImage(*m_bpButtonShowLogFolder, imgFileManagerSmall_); m_bpButtonShowLogFolder->SetToolTip(translate(extCommandFileManager.description));//translate default external apps on the fly: "Show in Explorer" m_logFolderPath->SetHint(utfTo(defaultCfg_.logFolderPhrase)); //1. no text shown when control is disabled! 2. apparently there's a refresh problem on GTK m_logFolderPath->setHistory(std::make_shared(globalCfg.logFolderHistory, globalCfg.folderHistoryMax)); logFolderSelector_.setPath(globalCfg.logFolderPhrase); setDefaultWidth(*m_spinCtrlLogFilesMaxAge); setImage(*m_bitmapSettings, loadImage("settings")); setImage(*m_bitmapWarnings, loadImage("msg_warning", dipToScreen(20))); setImage(*m_bitmapLogFile, loadImage("log_file", dipToScreen(20))); setImage(*m_bitmapNotificationSounds, loadImage("notification_sounds")); setImage(*m_bitmapConsole, loadImage("command_line", dipToScreen(20))); setImage(*m_bitmapCompareDone, loadImage("compare", dipToScreen(20))); setImage(*m_bitmapSyncDone, loadImage("start_sync", dipToScreen(20))); setImage(*m_bitmapAlertPending, loadImage("msg_error", dipToScreen(20))); setImage(*m_bpButtonPlayCompareDone, loadImage("play_sound")); setImage(*m_bpButtonPlaySyncDone, loadImage("play_sound")); setImage(*m_bpButtonPlayAlertPending, loadImage("play_sound")); setImage(*m_bpButtonAddRow, loadImage("item_add")); setImage(*m_bpButtonRemoveRow, loadImage("item_remove")); //-------------------------------------------------------------------------------- m_checkListHiddenDialogs->Hide(); m_buttonShowCtxCustomize->Hide(); //fix wxCheckListBox's stupid "per-item toggle" when multiple items are selected m_checkListHiddenDialogs->Bind(wxEVT_KEY_DOWN, [&checklist = *m_checkListHiddenDialogs](wxKeyEvent& event) { switch (event.GetKeyCode()) { case WXK_SPACE: case WXK_NUMPAD_SPACE: assert(checklist.HasMultipleSelection()); if (wxArrayInt selection; checklist.GetSelections(selection), !selection.empty()) { const bool checkedNew = !checklist.IsChecked(selection[0]); for (const int itemPos : selection) checklist.Check(itemPos, checkedNew); wxCommandEvent chkEvent(wxEVT_CHECKLISTBOX); chkEvent.SetInt(selection[0]); checklist.GetEventHandler()->ProcessEvent(chkEvent); } return; } event.Skip(); }); std::stable_partition(hiddenDialogCfgMapping_.begin(), hiddenDialogCfgMapping_.end(), [&](const auto& item) { const auto& [dlgShown, dlgSetShown, msg] = item; return !dlgShown(globalCfg); //move hidden dialogs to the top }); std::vector dialogMessages; for (const auto& [dlgShown, dlgSetShown, msg] : hiddenDialogCfgMapping_) dialogMessages.push_back(msg); m_checkListHiddenDialogs->Append(dialogMessages); unsigned int itemPos = 0; for (const auto& [dlgShown, dlgSetShown, msg] : hiddenDialogCfgMapping_) { if (dlgShown(globalCfg)) m_checkListHiddenDialogs->Check(itemPos); ++itemPos; } //-------------------------------------------------------------------------------- m_checkBoxFailSafe ->SetValue(globalCfg.failSafeFileCopy); m_checkBoxCopyLocked ->SetValue(globalCfg.copyLockedFiles); m_checkBoxCopyPermissions->SetValue(globalCfg.copyFilePermissions); bSizerColorTheme->Show(darkModeAvailable()); enumColorTheme_.set(globalCfg.appColorTheme); m_checkBoxLogFilesMaxAge->SetValue(globalCfg.logfilesMaxAgeDays > 0); m_spinCtrlLogFilesMaxAge->SetValue(globalCfg.logfilesMaxAgeDays > 0 ? globalCfg.logfilesMaxAgeDays : GlobalConfig().logfilesMaxAgeDays); switch (globalCfg.logFormat) { case LogFileFormat::html: m_radioBtnLogHtml->SetValue(true); break; case LogFileFormat::text: m_radioBtnLogText->SetValue(true); break; } m_textCtrlSoundPathCompareDone ->ChangeValue(utfTo(globalCfg.soundFileCompareFinished)); m_textCtrlSoundPathSyncDone ->ChangeValue(utfTo(globalCfg.soundFileSyncFinished)); m_textCtrlSoundPathAlertPending->ChangeValue(utfTo(globalCfg.soundFileAlertPending)); //-------------------------------------------------------------------------------- bSizerLockedFiles->Show(false); m_gridCustomCommand->SetMargins(0, 0); //for onGridResize(): ensure GetClientSize() calculations are correct m_gridCustomCommand->SetTabBehaviour(wxGrid::Tab_Leave); m_gridCustomCommand->SetSelectionMode(wxGrid::wxGridSelectRows); //getting rid of column header highlight the stupid way but the alternative "wxGrid::DisableOverlaySelection()" looks like ass class wxGridCellAttrProviderNoColHighlight : public wxGridCellAttrProvider { const wxGridColumnHeaderRenderer& GetColumnHeaderRenderer(int col) override { return colRenderNoHighlight_; } class : public wxGridColumnHeaderRendererDefault { void DrawHighlighted(const wxGrid& grid, wxDC& dc, wxRect& rect, int col, int flags) const override { DrawBorder(grid, dc, rect); } } colRenderNoHighlight_; }; m_gridCustomCommand->GetTable()->SetAttrProvider(new wxGridCellAttrProviderNoColHighlight); m_gridCustomCommand->GetGridWindow()->Bind(wxEVT_SIZE, [this](wxSizeEvent& event) { onGridResize(event); }); m_gridCustomCommand->Bind(wxEVT_GRID_CELL_RIGHT_CLICK, [this](wxGridEvent& event) { onGridContext(event); }); m_gridCustomCommand->Bind(wxEVT_GRID_LABEL_RIGHT_CLICK, [this](wxGridEvent& event) { onGridContext(event); }); m_gridCustomCommand->Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { switch (event.GetKeyCode()) { //fix \src\generic\grid.cpp calling wxGrid::MoveCursorDown() after pressing ENTER: 1. instead of showing edit control 2. after ending edit mode case WXK_RETURN: case WXK_NUMPAD_ENTER: m_gridCustomCommand->EnableCellEditControl(!m_gridCustomCommand->IsCellEditControlEnabled()); return; case WXK_HOME: //make wxGrid behave like a list instead of a spreadsheet: case WXK_END: //=> add fake CTRL to move up/down instead of left/right { assert(!m_gridCustomCommand->IsCellEditControlEnabled()); //cell edit already handles this event event.m_controlDown = true; break; } case 'A': //CTRL + A - select all if (event.ControlDown()) { assert(!m_gridCustomCommand->IsCellEditControlEnabled()); //cell edit already handles this event m_gridCustomCommand->SelectAll(); return; } break; case 'C': case WXK_INSERT: //CTRL + C || CTRL + INS case WXK_NUMPAD_INSERT: if (event.ControlDown()) { copySelectionToClipboard(); return; } break; } event.Skip(); }); m_gridCustomCommand->Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { switch (event.GetKeyCode()) { case WXK_ESCAPE: //exit cell edit mode + undo (instead of cancelling the whole dialog!!!) if (m_gridCustomCommand->IsCellEditControlEnabled()) { const wxGridCellCoords coords = m_gridCustomCommand->GetGridCursorCoords(); assert(coords != wxGridNoCellCoords); //otherwise what exactly are we editting??? const wxString oldVal = m_gridCustomCommand->GetCellValue(coords); m_gridCustomCommand->DisableCellEditControl(); //saves editted value, unless wxEVT_GRID_CELL_CHANGED is vetoed m_gridCustomCommand->SetCellValue(coords, oldVal); //=> instead of veto, restore old value manually return; } break; } event.Skip(); }); //temporarily set dummy value for window height calculations: setExtApp(std::vector(globalCfg.externalApps.size() + 1)); updateGui(); Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onLocalKeyEvent(event); }); //enable dialog-specific key events GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() #ifdef __WXGTK3__ Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 //Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404 #endif Center(); //apply *after* dialog size change! //restore actual value: setExtApp(globalCfg.externalApps); updateGui(); m_buttonOK->SetFocus(); } void OptionsDlg::onLocalKeyEvent(wxKeyEvent& event) //process key events without explicit menu entry :) { switch (event.GetKeyCode()) { case WXK_RETURN: case WXK_NUMPAD_ENTER: if (event.ControlDown()) //Ctrl+Enter or on macOS: Command+Enter { m_gridCustomCommand->DisableCellEditControl(); wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED); m_buttonOK->Command(dummy); //simulate click return; } break; } event.Skip(); } //automatically fit column width to match total grid width void OptionsDlg::onGridResize(wxEvent& event) { const int widthTotal = m_gridCustomCommand->GetGridWindow()->GetClientSize().GetWidth(); assert(m_gridCustomCommand->GetNumberCols() == 2); const int w0 = widthTotal * 2 / 5; //ratio 2 : 3 const int w1 = widthTotal - w0; m_gridCustomCommand->SetColSize(0, w0); m_gridCustomCommand->SetColSize(1, w1); m_gridCustomCommand->Refresh(); //required on Ubuntu event.Skip(); } void OptionsDlg::onGridContext(wxGridEvent& event) { m_gridCustomCommand->SetFocus(); //ensure cell cursor is highlighted ContextMenu menu; const bool canCopy = m_gridCustomCommand->IsSelection() || m_gridCustomCommand->GetGridCursorCoords() != wxGridNoCellCoords; menu.addItem(_("&Copy") + L"\tCtrl+C", [this] { copySelectionToClipboard(); }, loadImage("item_copy_sicon"), canCopy); menu.addSeparator(); const int rowCount = m_gridCustomCommand->GetNumberRows(); menu.addItem(_("Select all") + L"\tCtrl+A", [this] { m_gridCustomCommand->SelectAll(); }, wxNullImage, rowCount > 0); menu.popup(*m_gridCustomCommand, event.GetPosition()); } //why the fuck does wxGrid even allow multi-block selection and then fail in wxGrid::CopySelection()????????????? => fix [t... s...] void OptionsDlg::copySelectionToClipboard() const { const wxGridBlocks& selBlocks = m_gridCustomCommand->GetSelectedBlocks(); std::vector blocks(selBlocks.begin(), selBlocks.end()); if (blocks.empty()) //=> select cursor position instead if (const wxGridCellCoords curPos = m_gridCustomCommand->GetGridCursorCoords(); curPos != wxGridNoCellCoords) blocks.emplace_back(curPos.GetRow(), curPos.GetCol(), curPos.GetRow(), curPos.GetCol()); wxString clipBuf; //perf: old wxString didn't model exponential growth, but now it's std::string-based: static_assert(std::is_same_v); for (const wxGridBlockCoords& block : blocks) for (int row = block.GetTopRow(); row <= block.GetBottomRow(); ++row) for (int col = block.GetLeftCol(); col <= block.GetRightCol(); ++col) { clipBuf += m_gridCustomCommand->GetCellValue(row, col); clipBuf += col == block.GetRightCol() ? L'\n' : L'\t'; } setClipboardText(clipBuf); } void OptionsDlg::updateGui() { if (!colorThemeIcon_ || *colorThemeIcon_ != enumColorTheme_.get()) //perf? don't update icon unless needed switch (enumColorTheme_.get()) { case ColorTheme::System: setImage(*m_bitmapColorTheme, loadImage("theme-default")); break; case ColorTheme::Light: setImage(*m_bitmapColorTheme, loadImage("theme-light")); break; case ColorTheme::Dark: setImage(*m_bitmapColorTheme, loadImage("theme-dark")); break; } colorThemeIcon_ = enumColorTheme_.get(); m_spinCtrlLogFilesMaxAge->Enable(m_checkBoxLogFilesMaxAge->GetValue()); m_bpButtonPlayCompareDone ->Enable(!trimCpy(m_textCtrlSoundPathCompareDone ->GetValue()).empty()); m_bpButtonPlaySyncDone ->Enable(!trimCpy(m_textCtrlSoundPathSyncDone ->GetValue()).empty()); m_bpButtonPlayAlertPending->Enable(!trimCpy(m_textCtrlSoundPathAlertPending->GetValue()).empty()); int hiddenDialogs = 0; for (unsigned int itemPos = 0; itemPos < hiddenDialogCfgMapping_.size(); ++itemPos) if (!m_checkListHiddenDialogs->IsChecked(itemPos)) ++hiddenDialogs; assert(hiddenDialogCfgMapping_.size() == m_checkListHiddenDialogs->GetCount()); setText(*m_staticTextHiddenDialogsCount, L'(' + (hiddenDialogs == 0 ? _("No dialogs hidden") : _P("1 dialog hidden", "%x dialogs hidden", hiddenDialogs)) + L')'); Layout(); } void OptionsDlg::expandConfigArea(ConfigArea area) { //only show one expanded area at a time (wxGTK even crashes when showing both: not worth debugging) m_buttonShowHiddenDialogs->Show(area != ConfigArea::hidden); m_buttonShowCtxCustomize ->Show(area != ConfigArea::context); m_checkListHiddenDialogs->Show(area == ConfigArea::hidden); bSizerContextCustomize ->Show(area == ConfigArea::context); Layout(); Refresh(); //required on Windows } void OptionsDlg::selectSound(wxTextCtrl& txtCtrl) { std::optional defaultFolderPath = getParentFolderPath(utfTo(txtCtrl.GetValue())); if (!defaultFolderPath) defaultFolderPath = getResourceDirPath(); wxFileDialog fileSelector(this, wxString() /*message*/, utfTo(*defaultFolderPath), wxString() /*default file name*/, wxString(L"WAVE (*.wav)|*.wav") + L"|" + _("All files") + L" (*.*)|*", wxFD_OPEN); if (fileSelector.ShowModal() != wxID_OK) return; txtCtrl.ChangeValue(fileSelector.GetPath()); updateGui(); } void OptionsDlg::playSoundWithDiagnostics(const wxString& filePath) { try { //::PlaySound() on Windows does not set last error! //wxSound::Play(..., wxSOUND_SYNC) can return "false", but also without details! //=> check file access manually: [[maybe_unused]] const std::string& stream = getFileContent(utfTo(filePath), nullptr /*notifyUnbufferedIO*/); //throw FileError if (!wxSound::Play(filePath, wxSOUND_ASYNC)) throw FileError(L"Sound playback failed. No further diagnostics available."); } catch (const FileError& e) { showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); } } void OptionsDlg::onDefault(wxCommandEvent& event) { m_checkBoxFailSafe ->SetValue(defaultCfg_.failSafeFileCopy); m_checkBoxCopyLocked ->SetValue(defaultCfg_.copyLockedFiles); m_checkBoxCopyPermissions->SetValue(defaultCfg_.copyFilePermissions); enumColorTheme_.set(defaultCfg_.appColorTheme); unsigned int itemPos = 0; for (const auto& [dlgShown, dlgSetShown, msg] : hiddenDialogCfgMapping_) m_checkListHiddenDialogs->Check(itemPos++, dlgShown(defaultCfg_)); logFolderSelector_.setPath(defaultCfg_.logFolderPhrase); m_checkBoxLogFilesMaxAge->SetValue(defaultCfg_.logfilesMaxAgeDays > 0); m_spinCtrlLogFilesMaxAge->SetValue(defaultCfg_.logfilesMaxAgeDays > 0 ? defaultCfg_.logfilesMaxAgeDays : 14); switch (defaultCfg_.logFormat) { case LogFileFormat::html: m_radioBtnLogHtml->SetValue(true); break; case LogFileFormat::text: m_radioBtnLogText->SetValue(true); break; } m_textCtrlSoundPathCompareDone ->ChangeValue(utfTo(defaultCfg_.soundFileCompareFinished)); m_textCtrlSoundPathSyncDone ->ChangeValue(utfTo(defaultCfg_.soundFileSyncFinished)); m_textCtrlSoundPathAlertPending->ChangeValue(utfTo(defaultCfg_.soundFileAlertPending)); setExtApp(defaultCfg_.externalApps); updateGui(); } void OptionsDlg::onOkay(wxCommandEvent& event) { //------- parameter validation (BEFORE writing output!) ------- Zstring logFolderPhrase = logFolderSelector_.getPath(); if (AFS::isNullPath(createAbstractPath(logFolderPhrase))) //no need to show an error: just set default! logFolderPhrase = defaultCfg_.logFolderPhrase; //------------------------------------------------------------- //write settings only when okay-button is pressed (except hidden dialog reset)! globalCfgOut_.failSafeFileCopy = m_checkBoxFailSafe->GetValue(); globalCfgOut_.copyLockedFiles = m_checkBoxCopyLocked->GetValue(); globalCfgOut_.copyFilePermissions = m_checkBoxCopyPermissions->GetValue(); globalCfgOut_.appColorTheme = enumColorTheme_.get(); globalCfgOut_.logFolderPhrase = logFolderPhrase; m_logFolderPath->getHistory()->addItem(logFolderPhrase); globalCfgOut_.logFolderHistory = m_logFolderPath->getHistory()->getList(); globalCfgOut_.logfilesMaxAgeDays = m_checkBoxLogFilesMaxAge->GetValue() ? m_spinCtrlLogFilesMaxAge->GetValue() : -1; globalCfgOut_.logFormat = m_radioBtnLogHtml->GetValue() ? LogFileFormat::html : LogFileFormat::text; globalCfgOut_.soundFileCompareFinished = utfTo(trimCpy(m_textCtrlSoundPathCompareDone ->GetValue())); globalCfgOut_.soundFileSyncFinished = utfTo(trimCpy(m_textCtrlSoundPathSyncDone ->GetValue())); globalCfgOut_.soundFileAlertPending = utfTo(trimCpy(m_textCtrlSoundPathAlertPending->GetValue())); globalCfgOut_.externalApps = getExtApp(); unsigned int itemPos = 0; for (const auto& [dlgShown, dlgSetShown, msg] : hiddenDialogCfgMapping_) dlgSetShown(globalCfgOut_, m_checkListHiddenDialogs->IsChecked(itemPos++)); EndModal(static_cast(ConfirmationButton::accept)); } void OptionsDlg::setExtApp(const std::vector& extApps) { int rowDiff = static_cast(extApps.size()) - m_gridCustomCommand->GetNumberRows(); ++rowDiff; //append empty row to facilitate insertions by user if (rowDiff >= 0) m_gridCustomCommand->AppendRows(rowDiff); else m_gridCustomCommand->DeleteRows(0, -rowDiff); int row = 0; for (const auto& [descriptionEng, cmdLine] : extApps) { const std::wstring description = translate(descriptionEng); //remember english description to save in GlobalSettings.xml later rather than hard-code translation descriptionTransToEng_[description] = descriptionEng; m_gridCustomCommand->SetCellValue(row, 0, description); m_gridCustomCommand->SetCellValue(row, 1, utfTo(cmdLine)); ++row; } } std::vector OptionsDlg::getExtApp() const { std::vector output; for (int i = 0; i < m_gridCustomCommand->GetNumberRows(); ++i) { auto description = copyStringTo(m_gridCustomCommand->GetCellValue(i, 0)); auto commandline = utfTo(m_gridCustomCommand->GetCellValue(i, 1)); //try to undo translation of description for GlobalSettings.xml auto it = descriptionTransToEng_.find(description); if (it != descriptionTransToEng_.end()) description = it->second; if (!description.empty() || !commandline.empty()) output.push_back({description, commandline}); } return output; } void OptionsDlg::onAddRow(wxCommandEvent& event) { const int selectedRow = m_gridCustomCommand->GetGridCursorRow(); if (0 <= selectedRow && selectedRow < m_gridCustomCommand->GetNumberRows()) m_gridCustomCommand->InsertRows(selectedRow); else m_gridCustomCommand->AppendRows(); m_gridCustomCommand->SetFocus(); //make grid cursor visible } void OptionsDlg::onRemoveRow(wxCommandEvent& event) { if (m_gridCustomCommand->GetNumberRows() > 0) { const int selectedRow = m_gridCustomCommand->GetGridCursorRow(); if (0 <= selectedRow && selectedRow < m_gridCustomCommand->GetNumberRows()) m_gridCustomCommand->DeleteRows(selectedRow); else m_gridCustomCommand->DeleteRows(m_gridCustomCommand->GetNumberRows() - 1); m_gridCustomCommand->SetFocus(); //make grid cursor visible } } void OptionsDlg::onShowLogFolder(wxCommandEvent& event) { try { AbstractPath logFolderPath = createAbstractPath(logFolderSelector_.getPath()); if (AFS::isNullPath(logFolderPath)) logFolderPath = createAbstractPath(defaultCfg_.logFolderPhrase); openFolderInFileBrowser(logFolderPath); //throw FileError } catch (const FileError& e) { showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); } } } ConfirmationButton fff::showOptionsDlg(wxWindow* parent, GlobalConfig& globalCfg) { OptionsDlg dlg(parent, globalCfg); return static_cast(dlg.ShowModal()); } //######################################################################################## namespace { class SelectTimespanDlg : public SelectTimespanDlgGenerated { public: SelectTimespanDlg(wxWindow* parent, time_t& timeFrom, time_t& timeTo); private: void onOkay (wxCommandEvent& event) override; void onCancel(wxCommandEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } void onClose (wxCloseEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } void onChangeSelectionFrom(wxCalendarEvent& event) override { if (m_calendarFrom->GetDate() > m_calendarTo->GetDate()) m_calendarTo->SetDate(m_calendarFrom->GetDate()); } void onChangeSelectionTo(wxCalendarEvent& event) override { if (m_calendarFrom->GetDate() > m_calendarTo->GetDate()) m_calendarFrom->SetDate(m_calendarTo->GetDate()); } void onLocalKeyEvent(wxKeyEvent& event); //output-only parameters: time_t& timeFromOut_; time_t& timeToOut_; }; SelectTimespanDlg::SelectTimespanDlg(wxWindow* parent, time_t& timeFrom, time_t& timeTo) : SelectTimespanDlgGenerated(parent), timeFromOut_(timeFrom), timeToOut_(timeTo) { setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonOK).setCancel(m_buttonCancel)); assert(m_calendarFrom->GetWindowStyle() == m_calendarTo->GetWindowStyle()); assert(m_calendarFrom->HasFlag(wxCAL_SHOW_HOLIDAYS)); //caveat: for some stupid reason this is not honored when set by SetWindowStyle() assert(m_calendarFrom->HasFlag(wxCAL_SHOW_SURROUNDING_WEEKS)); assert(!m_calendarFrom->HasFlag(wxCAL_MONDAY_FIRST) && !m_calendarFrom->HasFlag(wxCAL_SUNDAY_FIRST)); //...because we set it in the following: long style = m_calendarFrom->GetWindowStyle(); style |= getFirstDayOfWeek() == WeekDay::sunday ? wxCAL_SUNDAY_FIRST : wxCAL_MONDAY_FIRST; //seems to be ignored on CentOS m_calendarFrom->SetWindowStyle(style); m_calendarTo ->SetWindowStyle(style); //set default values time_t timeFromTmp = timeFrom; time_t timeToTmp = timeTo; if (timeToTmp == 0) timeToTmp = std::time(nullptr); // if (timeFromTmp == 0) timeFromTmp = timeToTmp - 7 * 24 * 3600; //default time span: one week from "now" //wxDateTime models local(!) time (in contrast to what documentation says), but it has a constructor taking time_t UTC m_calendarFrom->SetDate(timeFromTmp); m_calendarTo ->SetDate(timeToTmp ); Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onLocalKeyEvent(event); }); //enable dialog-specific key events GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() #ifdef __WXGTK3__ Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 //Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404 #endif Center(); //apply *after* dialog size change! m_buttonOK->SetFocus(); } void SelectTimespanDlg::onLocalKeyEvent(wxKeyEvent& event) //process key events without explicit menu entry :) { switch (event.GetKeyCode()) { case WXK_RETURN: case WXK_NUMPAD_ENTER: if (event.ControlDown()) //Ctrl+Enter or on macOS: Command+Enter { wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED); m_buttonOK->Command(dummy); //simulate click return; } break; } event.Skip(); } void SelectTimespanDlg::onOkay(wxCommandEvent& event) { wxDateTime from = m_calendarFrom->GetDate(); wxDateTime to = m_calendarTo ->GetDate(); //align to full days from.ResetTime(); to .ResetTime(); //reset local(!) time to += wxTimeSpan::Day(); to -= wxTimeSpan::Second(); //go back to end of previous day timeFromOut_ = from.GetTicks(); timeToOut_ = to .GetTicks(); EndModal(static_cast(ConfirmationButton::accept)); } } ConfirmationButton fff::showSelectTimespanDlg(wxWindow* parent, time_t& timeFrom, time_t& timeTo) { SelectTimespanDlg dlg(parent, timeFrom, timeTo); return static_cast(dlg.ShowModal()); } //######################################################################################## namespace { class PasswordPromptDlg : public PasswordPromptDlgGenerated { public: PasswordPromptDlg(wxWindow* parent, const std::wstring& msg, const std::wstring& lastErrorMsg /*optional*/, Zstring& password); private: void onOkay (wxCommandEvent& event) override; void onCancel(wxCommandEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } void onClose (wxCloseEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } void onToggleShowPassword(wxCommandEvent& event) override; void updateGui(); //work around defunct keyboard focus on macOS (or is it wxMac?) => not needed for this dialog! //void onLocalKeyEvent(wxKeyEvent& event); //output-only parameters: Zstring& passwordOut_; }; PasswordPromptDlg::PasswordPromptDlg(wxWindow* parent, const std::wstring& msg, const std::wstring& lastErrorMsg /*optional*/, Zstring& password) : PasswordPromptDlgGenerated(parent), passwordOut_(password) { setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonOK).setCancel(m_buttonCancel)); wxString titleTmp; if (!parent || !parent->IsShownOnScreen()) titleTmp = wxTheApp->GetAppDisplayName(); SetTitle(titleTmp); const int maxWidthDip = 600; m_staticTextMain->SetLabelText(msg); m_staticTextMain->Wrap(dipToWxsize(maxWidthDip)); m_checkBoxShowPassword->SetValue(false); m_textCtrlPasswordHidden->ChangeValue(utfTo(password)); bSizerError->Show(!lastErrorMsg.empty()); if (!lastErrorMsg.empty()) { setImage(*m_bitmapError, loadImage("msg_error", dipToWxsize(32))); m_staticTextError->SetLabelText(lastErrorMsg); m_staticTextError->Wrap(dipToWxsize(maxWidthDip) - m_bitmapError->GetSize().x - 10 /*border in non-DIP pixel*/); } //set up default view for dialog size calculation m_textCtrlPasswordVisible->Hide(); GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() #ifdef __WXGTK3__ Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 //Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404 #endif Center(); //apply *after* dialog size change! updateGui(); //*after* SetSizeHints when standard dialog height has been calculated //m_textCtrlPasswordHidden->SelectAll(); -> apparantly implicitly caused by SetFocus!? m_textCtrlPasswordHidden->SetFocus(); } void PasswordPromptDlg::onToggleShowPassword(wxCommandEvent& event) { if (m_checkBoxShowPassword->GetValue()) m_textCtrlPasswordVisible->ChangeValue(m_textCtrlPasswordHidden->GetValue()); else m_textCtrlPasswordHidden->ChangeValue(m_textCtrlPasswordVisible->GetValue()); updateGui(); wxTextCtrl& textCtrl = *(m_checkBoxShowPassword->GetValue() ? m_textCtrlPasswordVisible : m_textCtrlPasswordHidden); textCtrl.SetFocus(); //macOS: selects text as unwanted side effect => *before* SetInsertionPointEnd() textCtrl.SetInsertionPointEnd(); } void PasswordPromptDlg::updateGui() { m_textCtrlPasswordVisible->Show( m_checkBoxShowPassword->GetValue()); m_textCtrlPasswordHidden ->Show(!m_checkBoxShowPassword->GetValue()); Layout(); Refresh(); } void PasswordPromptDlg::onOkay(wxCommandEvent& event) { passwordOut_ = utfTo((m_checkBoxShowPassword->GetValue() ? m_textCtrlPasswordVisible : m_textCtrlPasswordHidden)->GetValue()); EndModal(static_cast(ConfirmationButton::accept)); } } ConfirmationButton fff::showPasswordPrompt(wxWindow* parent, const std::wstring& msg, const std::wstring& lastErrorMsg /*optional*/, Zstring& password) { PasswordPromptDlg dlg(parent, msg, lastErrorMsg, password); return static_cast(dlg.ShowModal()); } //######################################################################################## namespace { class CfgHighlightDlg : public CfgHighlightDlgGenerated { public: CfgHighlightDlg(wxWindow* parent, int& cfgHistSyncOverdueDays); private: void onOkay (wxCommandEvent& event) override; void onCancel(wxCommandEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } void onClose (wxCloseEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } //work around defunct keyboard focus on macOS (or is it wxMac?) => not needed for this dialog! //void onLocalKeyEvent(wxKeyEvent& event); //output-only parameters: int& cfgHistSyncOverdueDaysOut_; }; CfgHighlightDlg::CfgHighlightDlg(wxWindow* parent, int& cfgHistSyncOverdueDays) : CfgHighlightDlgGenerated(parent), cfgHistSyncOverdueDaysOut_(cfgHistSyncOverdueDays) { setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonOK).setCancel(m_buttonCancel)); m_staticTextHighlight->Wrap(dipToWxsize(300)); setDefaultWidth(*m_spinCtrlOverdueDays); m_spinCtrlOverdueDays->SetValue(cfgHistSyncOverdueDays); GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() #ifdef __WXGTK3__ Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 //Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404 #endif Center(); //apply *after* dialog size change! m_spinCtrlOverdueDays->SetFocus(); } void CfgHighlightDlg::onOkay(wxCommandEvent& event) { cfgHistSyncOverdueDaysOut_ = m_spinCtrlOverdueDays->GetValue(); EndModal(static_cast(ConfirmationButton::accept)); } } ConfirmationButton fff::showCfgHighlightDlg(wxWindow* parent, int& cfgHistSyncOverdueDays) { CfgHighlightDlg dlg(parent, cfgHistSyncOverdueDays); return static_cast(dlg.ShowModal()); } //######################################################################################## namespace { class ActivationDlg : public ActivationDlgGenerated { public: ActivationDlg(wxWindow* parent, const std::wstring& lastErrorMsg, const std::wstring& manualActivationUrl, std::wstring& manualActivationKey); private: void onActivateOnline (wxCommandEvent& event) override; void onActivateOffline(wxCommandEvent& event) override; void onOfflineActivationEnter(wxCommandEvent& event) override { onActivateOffline(event); } void onCopyUrl (wxCommandEvent& event) override; void onCancel(wxCommandEvent& event) override { EndModal(static_cast(ActivationDlgButton::cancel)); } void onClose (wxCloseEvent& event) override { EndModal(static_cast(ActivationDlgButton::cancel)); } std::wstring& manualActivationKeyOut_; //in/out parameter }; ActivationDlg::ActivationDlg(wxWindow* parent, const std::wstring& lastErrorMsg, const std::wstring& manualActivationUrl, std::wstring& manualActivationKey) : ActivationDlgGenerated(parent), manualActivationKeyOut_(manualActivationKey) { setStandardButtonLayout(*bSizerStdButtons, StdButtons().setCancel(m_buttonCancel)); std::wstring title = L"FreeFileSync " + utfTo(ffsVersion); SetTitle(title); //setMainInstructionFont(*m_staticTextMain); m_richTextLastError ->SetMinSize({-1, m_richTextLastError ->GetCharHeight() * 8}); m_richTextManualActivationUrl ->SetMinSize({-1, m_richTextManualActivationUrl->GetCharHeight() * 4}); m_textCtrlOfflineActivationKey->SetMinSize({dipToWxsize(260), -1}); setImage(*m_bitmapActivation, loadImage("internet")); m_textCtrlOfflineActivationKey->ForceUpper(); setTextWithUrls(*m_richTextLastError, lastErrorMsg); setTextWithUrls(*m_richTextManualActivationUrl, manualActivationUrl); m_textCtrlOfflineActivationKey->ChangeValue(manualActivationKey); GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() #ifdef __WXGTK3__ Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 //Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404 #endif Center(); //apply *after* dialog size change! m_buttonActivateOnline->SetFocus(); } void ActivationDlg::onCopyUrl(wxCommandEvent& event) { setClipboardText(m_richTextManualActivationUrl->GetValue()); m_richTextManualActivationUrl->SetFocus(); //[!] otherwise selection is lost m_richTextManualActivationUrl->SelectAll(); //some visual feedback } void ActivationDlg::onActivateOnline(wxCommandEvent& event) { manualActivationKeyOut_ = utfTo(m_textCtrlOfflineActivationKey->GetValue()); EndModal(static_cast(ActivationDlgButton::activateOnline)); } void ActivationDlg::onActivateOffline(wxCommandEvent& event) { manualActivationKeyOut_ = utfTo(m_textCtrlOfflineActivationKey->GetValue()); if (trimCpy(manualActivationKeyOut_).empty()) //alternative: disable button? => user thinks option is not available! { showNotificationDialog(this, DialogInfoType::info, PopupDialogCfg().setMainInstructions(_("Please enter a key for offline activation."))); m_textCtrlOfflineActivationKey->SetFocus(); return; } EndModal(static_cast(ActivationDlgButton::activateOffline)); } } ActivationDlgButton fff::showActivationDialog(wxWindow* parent, const std::wstring& lastErrorMsg, const std::wstring& manualActivationUrl, std::wstring& manualActivationKey) { ActivationDlg dlg(parent, lastErrorMsg, manualActivationUrl, manualActivationKey); return static_cast(dlg.ShowModal()); } //######################################################################################## class DownloadProgressWindow::Impl : public DownloadProgressDlgGenerated { public: Impl(wxWindow* parent, int64_t fileSizeTotal); void notifyNewFile (const Zstring& filePath) { filePath_ = filePath; } void notifyProgress(int64_t delta) { bytesCurrent_ += delta; } void requestUiUpdate() //throw CancelPressed { if (cancelled_) throw CancelPressed(); if (uiUpdateDue()) { updateGui(); //wxTheApp->Yield(); ::wxSafeYield(this); //disables user input except for "this" (using wxWindowDisabler instead would move the FFS main dialog into the background: why?) } } private: void onCancel(wxCommandEvent& event) override { cancelled_ = true; } void updateGui() { const double fraction = bytesTotal_ == 0 ? 0 : 1.0 * bytesCurrent_ / bytesTotal_; m_staticTextHeader->SetLabelText(_("Downloading update...") + L' ' + formatProgressPercent(fraction) + L" (" + formatFilesizeShort(bytesCurrent_) + L')'); m_gaugeProgress->SetValue(std::floor(fraction * GAUGE_FULL_RANGE)); m_staticTextDetails->SetLabelText(utfTo(filePath_)); } bool cancelled_ = false; int64_t bytesCurrent_ = 0; const int64_t bytesTotal_; Zstring filePath_; const int GAUGE_FULL_RANGE = 1000'000; }; DownloadProgressWindow::Impl::Impl(wxWindow* parent, int64_t fileSizeTotal) : DownloadProgressDlgGenerated(parent), bytesTotal_(fileSizeTotal) { setStandardButtonLayout(*bSizerStdButtons, StdButtons().setCancel(m_buttonCancel)); setMainInstructionFont(*m_staticTextHeader); m_staticTextHeader->Wrap(dipToWxsize(460)); //*after* font change! m_staticTextDetails->SetMinSize({dipToWxsize(550), -1}); setImage(*m_bitmapDownloading, loadImage("internet")); m_gaugeProgress->SetRange(GAUGE_FULL_RANGE); updateGui(); GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() #ifdef __WXGTK3__ Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 //Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404 #endif Center(); //apply *after* dialog size change! Show(); //clear gui flicker: window must be visible to make this work! ::wxSafeYield(); //at least on OS X a real Yield() is required to flush pending GUI updates; Update() is not enough m_buttonCancel->SetFocus(); } DownloadProgressWindow::DownloadProgressWindow(wxWindow* parent, int64_t fileSizeTotal) : pimpl_(new DownloadProgressWindow::Impl(parent, fileSizeTotal)) {} DownloadProgressWindow::~DownloadProgressWindow() { pimpl_->Destroy(); } void DownloadProgressWindow::notifyNewFile(const Zstring& filePath) { pimpl_->notifyNewFile(filePath); } void DownloadProgressWindow::notifyProgress(int64_t delta) { pimpl_->notifyProgress(delta); } void DownloadProgressWindow::requestUiUpdate() { pimpl_->requestUiUpdate(); } //throw CancelPressed //########################################################################################