2447 lines
102 KiB
C++
2447 lines
102 KiB
C++
// *****************************************************************************
|
|
// * 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 <variant>
|
|
#include <zen/time.h>
|
|
#include <zen/format_unit.h>
|
|
#include <zen/build_info.h>
|
|
#include <zen/file_io.h>
|
|
#include <wx/wupdlock.h>
|
|
#include <wx/filedlg.h>
|
|
#include <wx/sound.h>
|
|
#include <wx+/context_menu.h>
|
|
#include <wx+/bitmap_button.h>
|
|
#include <wx+/choice_enum.h>
|
|
#include <wx+/rtl.h>
|
|
#include <wx+/no_flicker.h>
|
|
#include <wx+/image_tools.h>
|
|
#include <wx+/window_layout.h>
|
|
#include <wx+/popup_dlg.h>
|
|
#include <wx+/async_task.h>
|
|
#include <wx+/image_resources.h>
|
|
#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<int>(ConfirmationButton::accept)); }
|
|
void onClose(wxCloseEvent& event) override { EndModal(static_cast<int>(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<wxString>(ffsVersion);
|
|
|
|
const wchar_t* const SPACED_BULLET = L" \u2022 ";
|
|
build += SPACED_BULLET;
|
|
|
|
build += LTR_MARK; //fix Arabic
|
|
build += utfTo<wxString>(cpuArchName);
|
|
|
|
build += SPACED_BULLET;
|
|
build += utfTo<wxString>(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<wxString>(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<int>(ConfirmationButton::cancel)); }
|
|
void onClose (wxCloseEvent& event) override { EndModal(static_cast<int>(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<Zstring>& 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<wxString> gdriveAccounts;
|
|
try
|
|
{
|
|
for (const std::string& loginEmail : gdriveListAccounts()) //throw FileError
|
|
gdriveAccounts.push_back(utfTo<wxString>(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<std::string>(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<wxString>(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<wxString>(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<wxString>(login.portCfg));
|
|
m_textCtrlServer ->ChangeValue(utfTo<wxString>(login.server));
|
|
m_textCtrlUserName ->ChangeValue(utfTo<wxString>(login.username));
|
|
sftpAuthType_ = login.authType;
|
|
if (login.password)
|
|
m_textCtrlPasswordHidden->ChangeValue(utfTo<wxString>(*login.password));
|
|
else
|
|
m_checkBoxPasswordPrompt->SetValue(true);
|
|
m_textCtrlKeyfilePath ->ChangeValue(utfTo<wxString>(login.privateKeyFilePath));
|
|
m_textCtrlServerPath ->ChangeValue(utfTo<wxString>(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<wxString>(login.portCfg));
|
|
m_textCtrlServer ->ChangeValue(utfTo<wxString>(login.server));
|
|
m_textCtrlUserName->ChangeValue(utfTo<wxString>(login.username));
|
|
if (login.password)
|
|
m_textCtrlPasswordHidden ->ChangeValue(utfTo<wxString>(*login.password));
|
|
else
|
|
m_checkBoxPasswordPrompt->SetValue(true);
|
|
m_textCtrlServerPath->ChangeValue(utfTo<wxString>(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<std::string /*email*/, FileError>
|
|
{
|
|
try
|
|
{
|
|
return gdriveAddUser(nullptr /*updateGui*/, timeoutSec); //throw FileError
|
|
}
|
|
catch (const FileError& e) { return e; }
|
|
},
|
|
[this](const std::variant<std::string, FileError>& result)
|
|
{
|
|
if (const FileError* e = std::get_if<FileError>(&result))
|
|
showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e->toString()));
|
|
else
|
|
{
|
|
const std::string& loginEmail = std::get<std::string>(result);
|
|
|
|
int selPos = m_listBoxGdriveUsers->FindString(utfTo<wxString>(loginEmail), false /*caseSensitive*/);
|
|
if (selPos == wxNOT_FOUND)
|
|
selPos = m_listBoxGdriveUsers->Append(utfTo<wxString>(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<std::string>(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<std::wstring>(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<std::string>(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<std::vector<Zstring /*locationName*/>, FileError>
|
|
{
|
|
try
|
|
{
|
|
return gdriveListLocations(accountEmail, timeoutSec); //throw FileError
|
|
}
|
|
catch (const FileError& e) { return e; }
|
|
},
|
|
[this, accountEmail, locationToSelect](std::variant<std::vector<Zstring /*locationName*/>, FileError>&& result)
|
|
{
|
|
if (const int selPos = m_listBoxGdriveUsers->GetSelection();
|
|
selPos == wxNOT_FOUND || utfTo<std::string>(m_listBoxGdriveUsers->GetString(selPos)) != accountEmail)
|
|
return; //different accountEmail selected in the meantime!
|
|
|
|
m_listBoxGdriveDrives->Clear();
|
|
|
|
if (const FileError* e = std::get_if<FileError>(&result))
|
|
showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e->toString()));
|
|
else
|
|
{
|
|
auto& locationNames = std::get<std::vector<Zstring>>(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<wxString>(itemLabel));
|
|
|
|
const wxString labelToSelect = locationToSelect.empty() ? txtMyDrive_ : utfTo<wxString>(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<Zstring>& 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<wxString>(event.itemPaths_[0]));
|
|
|
|
sftpAuthType_ = SftpAuthType::keyFile;
|
|
updateGui();
|
|
}
|
|
}
|
|
|
|
|
|
void CloudSetupDlg::onSelectKeyfile(wxCommandEvent& event)
|
|
{
|
|
assert (type_ == CloudType::sftp && sftpAuthType_ == SftpAuthType::keyFile);
|
|
|
|
std::optional<Zstring> defaultFolderPath = getParentFolderPath(sftpKeyFileLastSelected_);
|
|
|
|
wxFileDialog fileSelector(this, wxString() /*message*/, utfTo<wxString>(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<Zstring>(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<wxString>(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<wxString>(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<Zstring>(m_textCtrlServerPath->GetValue()));
|
|
|
|
switch (type_)
|
|
{
|
|
case CloudType::gdrive:
|
|
{
|
|
GdriveLogin login;
|
|
if (const int selPos = m_listBoxGdriveUsers->GetSelection();
|
|
selPos != wxNOT_FOUND)
|
|
{
|
|
login.email = utfTo<std::string>(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<Zstring>(locationName);
|
|
}
|
|
}
|
|
login.timeoutSec = m_spinCtrlTimeout->GetValue();
|
|
return AbstractPath(condenseToGdriveDevice(login), serverRelPath); //noexcept
|
|
}
|
|
|
|
case CloudType::sftp:
|
|
{
|
|
SftpLogin login;
|
|
login.server = utfTo<Zstring>(m_textCtrlServer ->GetValue());
|
|
login.portCfg = stringTo<int> (m_textCtrlPort ->GetValue()); //0 if empty
|
|
login.username = utfTo<Zstring>(m_textCtrlUserName->GetValue());
|
|
login.authType = sftpAuthType_;
|
|
login.privateKeyFilePath = utfTo<Zstring>(m_textCtrlKeyfilePath->GetValue());
|
|
if (m_checkBoxPasswordPrompt->GetValue())
|
|
login.password = std::nullopt;
|
|
else
|
|
login.password = utfTo<Zstring>((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<Zstring>(m_textCtrlServer ->GetValue());
|
|
login.portCfg = stringTo<int> (m_textCtrlPort ->GetValue()); //0 if empty
|
|
login.username = utfTo<Zstring>(m_textCtrlUserName->GetValue());
|
|
if (m_checkBoxPasswordPrompt->GetValue())
|
|
login.password = std::nullopt;
|
|
else
|
|
login.password = utfTo<Zstring>((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<wxString>(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<int>(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<ConfirmationButton>(dlg.ShowModal());
|
|
}
|
|
|
|
//########################################################################################
|
|
|
|
namespace
|
|
{
|
|
class CopyToDialog : public CopyToDlgGenerated
|
|
{
|
|
public:
|
|
CopyToDialog(wxWindow* parent,
|
|
const std::wstring& itemList, int itemCount,
|
|
Zstring& targetFolderPath, Zstring& targetFolderLastSelected,
|
|
std::vector<Zstring>& folderHistory, size_t folderHistoryMax,
|
|
Zstring& sftpKeyFileLastSelected,
|
|
bool& keepRelPaths,
|
|
bool& overwriteIfExists);
|
|
|
|
private:
|
|
void onOkay (wxCommandEvent& event) override;
|
|
void onCancel(wxCommandEvent& event) override { EndModal(static_cast<int>(ConfirmationButton::cancel)); }
|
|
void onClose (wxCloseEvent& event) override { EndModal(static_cast<int>(ConfirmationButton::cancel)); }
|
|
|
|
void onLocalKeyEvent(wxKeyEvent& event);
|
|
|
|
std::optional<FolderSelector> targetFolder; //always bound
|
|
|
|
//output-only parameters:
|
|
Zstring& targetFolderPathOut_;
|
|
bool& keepRelPathsOut_;
|
|
bool& overwriteIfExistsOut_;
|
|
std::vector<Zstring>& folderHistoryOut_;
|
|
};
|
|
|
|
|
|
CopyToDialog::CopyToDialog(wxWindow* parent,
|
|
const std::wstring& itemList, int itemCount,
|
|
Zstring& targetFolderPath, Zstring& targetFolderLastSelected,
|
|
std::vector<Zstring>& 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<HistoryList>(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<int>(ConfirmationButton::accept));
|
|
}
|
|
}
|
|
|
|
ConfirmationButton fff::showCopyToDialog(wxWindow* parent,
|
|
const std::wstring& itemList, int itemCount,
|
|
Zstring& targetFolderPath, Zstring& targetFolderLastSelected,
|
|
std::vector<Zstring>& 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<ConfirmationButton>(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<int>(ConfirmationButton::cancel)); }
|
|
void onClose (wxCloseEvent& event) override { EndModal(static_cast<int>(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<int>(ConfirmationButton::accept));
|
|
}
|
|
}
|
|
|
|
ConfirmationButton fff::showDeleteDialog(wxWindow* parent,
|
|
const std::wstring& itemList, int itemCount,
|
|
bool& useRecycleBin)
|
|
{
|
|
DeleteDialog dlg(parent, itemList, itemCount, useRecycleBin);
|
|
return static_cast<ConfirmationButton>(dlg.ShowModal());
|
|
}
|
|
|
|
//########################################################################################
|
|
|
|
namespace
|
|
{
|
|
class SyncConfirmationDlg : public SyncConfirmationDlgGenerated
|
|
{
|
|
public:
|
|
SyncConfirmationDlg(wxWindow* parent,
|
|
bool syncSelection,
|
|
std::optional<SyncVariant> syncVar,
|
|
const SyncStatistics& st,
|
|
bool& dontShowAgain);
|
|
private:
|
|
void onStartSync(wxCommandEvent& event) override;
|
|
void onCancel (wxCommandEvent& event) override { EndModal(static_cast<int>(ConfirmationButton::cancel)); }
|
|
void onClose (wxCloseEvent& event) override { EndModal(static_cast<int>(ConfirmationButton::cancel)); }
|
|
|
|
void onLocalKeyEvent(wxKeyEvent& event);
|
|
|
|
//output-only parameters:
|
|
bool& dontShowAgainOut_;
|
|
};
|
|
|
|
|
|
SyncConfirmationDlg::SyncConfirmationDlg(wxWindow* parent,
|
|
bool syncSelection,
|
|
std::optional<SyncVariant> 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<SelectSide::left >(), *m_bitmapCreateLeft, "so_create_left_sicon");
|
|
setIntValue(*m_staticTextUpdateLeft, st.updateCount<SelectSide::left >(), *m_bitmapUpdateLeft, "so_update_left_sicon");
|
|
setIntValue(*m_staticTextDeleteLeft, st.deleteCount<SelectSide::left >(), *m_bitmapDeleteLeft, "so_delete_left_sicon");
|
|
setIntValue(*m_staticTextCreateRight, st.createCount<SelectSide::right>(), *m_bitmapCreateRight, "so_create_right_sicon");
|
|
setIntValue(*m_staticTextUpdateRight, st.updateCount<SelectSide::right>(), *m_bitmapUpdateRight, "so_update_right_sicon");
|
|
setIntValue(*m_staticTextDeleteRight, st.deleteCount<SelectSide::right>(), *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<int>(ConfirmationButton::accept));
|
|
}
|
|
}
|
|
|
|
ConfirmationButton fff::showSyncConfirmationDlg(wxWindow* parent,
|
|
bool syncSelection,
|
|
std::optional<SyncVariant> syncVar,
|
|
const SyncStatistics& statistics,
|
|
bool& dontShowAgain)
|
|
{
|
|
SyncConfirmationDlg dlg(parent,
|
|
syncSelection,
|
|
syncVar,
|
|
statistics,
|
|
dontShowAgain);
|
|
return static_cast<ConfirmationButton>(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<int>(ConfirmationButton::cancel)); }
|
|
void onClose (wxCloseEvent& event) override { EndModal(static_cast<int>(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<ExternalApp>& extApp);
|
|
std::vector<ExternalApp> getExtApp() const;
|
|
|
|
std::unordered_map<std::wstring /*translation*/, std::wstring /*english*/> descriptionTransToEng_; //mapping for external application config
|
|
|
|
const GlobalConfig defaultCfg_;
|
|
|
|
EnumDescrList<ColorTheme> enumColorTheme_
|
|
{
|
|
*m_choiceColorTheme,
|
|
{
|
|
{ColorTheme::System, wxControl::RemoveMnemonics(_("&Default")), {}/*tooltip*/},
|
|
{ColorTheme::Light, _("Light"), {}/*tooltip*/},
|
|
{ColorTheme::Dark, _("Dark"), {}/*tooltip*/},
|
|
}
|
|
};
|
|
|
|
std::optional<ColorTheme> colorThemeIcon_;
|
|
|
|
std::vector<std::tuple<std::function<bool(const GlobalConfig& cfg)> /*get dialog shown status*/,
|
|
std::function<void(GlobalConfig& gs, bool show)> /*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<wxString>(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<HistoryList>(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<wxString> 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<wxString>(globalCfg.soundFileCompareFinished));
|
|
m_textCtrlSoundPathSyncDone ->ChangeValue(utfTo<wxString>(globalCfg.soundFileSyncFinished));
|
|
m_textCtrlSoundPathAlertPending->ChangeValue(utfTo<wxString>(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<ExternalApp>(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<wxGridBlockCoords> 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<wxStringImpl, std::wstring>);
|
|
|
|
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<Zstring> defaultFolderPath = getParentFolderPath(utfTo<Zstring>(txtCtrl.GetValue()));
|
|
if (!defaultFolderPath)
|
|
defaultFolderPath = getResourceDirPath();
|
|
|
|
wxFileDialog fileSelector(this, wxString() /*message*/, utfTo<wxString>(*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<Zstring>(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<wxString>(defaultCfg_.soundFileCompareFinished));
|
|
m_textCtrlSoundPathSyncDone ->ChangeValue(utfTo<wxString>(defaultCfg_.soundFileSyncFinished));
|
|
m_textCtrlSoundPathAlertPending->ChangeValue(utfTo<wxString>(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<Zstring>(trimCpy(m_textCtrlSoundPathCompareDone ->GetValue()));
|
|
globalCfgOut_.soundFileSyncFinished = utfTo<Zstring>(trimCpy(m_textCtrlSoundPathSyncDone ->GetValue()));
|
|
globalCfgOut_.soundFileAlertPending = utfTo<Zstring>(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<int>(ConfirmationButton::accept));
|
|
}
|
|
|
|
|
|
void OptionsDlg::setExtApp(const std::vector<ExternalApp>& extApps)
|
|
{
|
|
int rowDiff = static_cast<int>(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<wxString>(cmdLine));
|
|
++row;
|
|
}
|
|
}
|
|
|
|
|
|
std::vector<ExternalApp> OptionsDlg::getExtApp() const
|
|
{
|
|
std::vector<ExternalApp> output;
|
|
for (int i = 0; i < m_gridCustomCommand->GetNumberRows(); ++i)
|
|
{
|
|
auto description = copyStringTo<std::wstring>(m_gridCustomCommand->GetCellValue(i, 0));
|
|
auto commandline = utfTo<Zstring>(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<ConfirmationButton>(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<int>(ConfirmationButton::cancel)); }
|
|
void onClose (wxCloseEvent& event) override { EndModal(static_cast<int>(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<int>(ConfirmationButton::accept));
|
|
}
|
|
}
|
|
|
|
ConfirmationButton fff::showSelectTimespanDlg(wxWindow* parent, time_t& timeFrom, time_t& timeTo)
|
|
{
|
|
SelectTimespanDlg dlg(parent, timeFrom, timeTo);
|
|
return static_cast<ConfirmationButton>(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<int>(ConfirmationButton::cancel)); }
|
|
void onClose (wxCloseEvent& event) override { EndModal(static_cast<int>(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<wxString>(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<Zstring>((m_checkBoxShowPassword->GetValue() ? m_textCtrlPasswordVisible : m_textCtrlPasswordHidden)->GetValue());
|
|
EndModal(static_cast<int>(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<ConfirmationButton>(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<int>(ConfirmationButton::cancel)); }
|
|
void onClose (wxCloseEvent& event) override { EndModal(static_cast<int>(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<int>(ConfirmationButton::accept));
|
|
}
|
|
}
|
|
|
|
ConfirmationButton fff::showCfgHighlightDlg(wxWindow* parent, int& cfgHistSyncOverdueDays)
|
|
{
|
|
CfgHighlightDlg dlg(parent, cfgHistSyncOverdueDays);
|
|
return static_cast<ConfirmationButton>(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<int>(ActivationDlgButton::cancel)); }
|
|
void onClose (wxCloseEvent& event) override { EndModal(static_cast<int>(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<std::wstring>(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<std::wstring>(m_textCtrlOfflineActivationKey->GetValue());
|
|
EndModal(static_cast<int>(ActivationDlgButton::activateOnline));
|
|
}
|
|
|
|
|
|
void ActivationDlg::onActivateOffline(wxCommandEvent& event)
|
|
{
|
|
manualActivationKeyOut_ = utfTo<std::wstring>(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<int>(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<ActivationDlgButton>(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<std::wstring>(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
|
|
|
|
//########################################################################################
|
|
|