diff options
87 files changed, 1219 insertions, 920 deletions
@@ -5,7 +5,7 @@ the ones mentioned below. The remaining issues that are yet to be fixed are list ---------------- -| libcurl 7.87| +| libcurl 7.88| ---------------- __________________________________________________________________________________________________________ /lib/ftp.c @@ -71,7 +71,7 @@ ________________________________________________________________________________ ------------------- -| wxWidgets 3.2.1 | +| wxWidgets 3.2.2 | ------------------- __________________________________________________________________________________________________________ /include/wx/features.h diff --git a/Changelog.txt b/Changelog.txt index 601c33dd..9235d34a 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -1,4 +1,20 @@ -FreeFileSync 12.0 [2022-01-21] +FreeFileSync 12.1 [2023-02-20] +------------------------------ +First official build based on GTK3 (Linux) +Allow cancel during folder path normalization (e.g. delay during HDD spin up) +Fixed slow FTP comparison performance due to libcurl regression +Open terminal with log messages on startup error (Linux) +Preserve changed config during auto-update +Save config during unexpected reboot (Linux) +Preserve config upon SIGTERM (Linux, macOS) +Fixed progress dialog z-order after switching windows (macOS) +Removed packet size limit for SFTP directory reading +Mouse hover effects for config and overview grid +Always update existing shortcuts during installation (Windows, Linux) +Fixed another "Some files will be synchronized as part of multiple base folders" false-negative + + +FreeFileSync 12.0 [2023-01-21] ------------------------------ Don't save password and show prompt instead for (S)FTP Fast path check failure on access errors diff --git a/FreeFileSync/Build/Resources/Gtk3Styles.css b/FreeFileSync/Build/Resources/Gtk3Styles.css index 027e7bc3..ca82fe1e 100644 --- a/FreeFileSync/Build/Resources/Gtk3Styles.css +++ b/FreeFileSync/Build/Resources/Gtk3Styles.css @@ -1,9 +1,8 @@ /* CSS format as required by CentOS (GTK 3.22.30) - - https://developer.gnome.org/gtk3/stable/chap-css-properties.html - https://developer.gnome.org/gtk3/stable/gtk-migrating-GtkStyleContext-css.html - pkg-config --modversion gtk+-3.0 */ - + pkg-config --modversion gtk+-3.0 + + https://docs.gtk.org/gtk3/css-overview.html + https://docs.gtk.org/gtk3/css-properties.html */ * { /* see wx+/grid.cpp: spacing wouldn't hurt, but let's be consistent */ @@ -12,8 +11,24 @@ button { - padding: 2px; /*remove excessive inner border from bitmap buttons*/ + padding: 4px 5px; /*remove excessive inner border from bitmap buttons*/ min-width: 0; min-height: 0; /*border-radius: 5px;*/ } + +entry +{ + padding: 2px 5px; /*default is too small for text input*/ +} + +combobox entry +{ + padding: 0 5px; +} + +spinbutton entry +{ + padding: 0 5px; + /*margin-right: -50px; possible hack! but not needed right now */ +} diff --git a/FreeFileSync/Build/Resources/Gtk3Styles.old.css b/FreeFileSync/Build/Resources/Gtk3Styles.old.css index a2c43ce7..ad11061f 100644 --- a/FreeFileSync/Build/Resources/Gtk3Styles.old.css +++ b/FreeFileSync/Build/Resources/Gtk3Styles.old.css @@ -1,9 +1,8 @@ /* CSS format as required by Debian (GTK 3.14.5) + pkg-config --modversion gtk+-3.0 - https://developer.gnome.org/gtk3/stable/chap-css-properties.html - https://developer.gnome.org/gtk3/stable/gtk-migrating-GtkStyleContext-css.html - pkg-config --modversion gtk+-3.0 */ - + https://docs.gtk.org/gtk3/css-overview.html + https://docs.gtk.org/gtk3/css-properties.html */ * { /* see wx+/grid.cpp: spacing wouldn't hurt, but let's be consistent */ @@ -12,7 +11,33 @@ GtkButton { - padding: 2px; /*remove excessive inner border from bitmap buttons*/ + padding: 4px 5px; /*remove excessive inner border*/ /* min-width: 0; => Debian: Error code 3: Gtk3Styles.css:13:10'min-width' is not a valid property name [gtk_css_provider_load_from_path] min-height: 0; */ } + +GtkPaned +{ + border: 10px solid #d0d0d0; /*hack wxAUI panel splitter: not sure why "color" and "background-color" are not working*/ +} + +GtkEntry +{ + padding: 2px 5px; /*fix excessive padding for text input fields*/ +} + +GtkComboBox GtkEntry +{ + padding: 4px 5px; +} + +GtkSpinButton /*GtkEntry*/ +{ + padding: 4px 5px; +} + +.tooltip /* why not GtkTooltip!? */ +{ + color: white; + background-color: #343434; /*fix "Adwaita" theme glitch (Debian): background is *light grey*, while text color is white!*/ +} diff --git a/FreeFileSync/Build/Resources/Languages.zip b/FreeFileSync/Build/Resources/Languages.zip Binary files differindex c2293e8f..a6e9f1e8 100644 --- a/FreeFileSync/Build/Resources/Languages.zip +++ b/FreeFileSync/Build/Resources/Languages.zip diff --git a/FreeFileSync/Source/RealTimeSync/application.cpp b/FreeFileSync/Source/RealTimeSync/application.cpp index a7289497..c3f8a59a 100644 --- a/FreeFileSync/Source/RealTimeSync/application.cpp +++ b/FreeFileSync/Source/RealTimeSync/application.cpp @@ -28,14 +28,15 @@ using namespace zen; using namespace rts; +#ifdef __WXGTK3__ //deprioritize Wayland: see FFS' application.cpp + GLOBAL_RUN_ONCE(::gdk_set_allowed_backends("x11,*")); //call *before* gtk_init() +#endif + IMPLEMENT_APP(Application) namespace { -wxDEFINE_EVENT(EVENT_ENTER_EVENT_LOOP, wxCommandEvent); - - using fff::FfsExitCode; void notifyAppError(const std::wstring& msg, FfsExitCode rc) @@ -144,7 +145,7 @@ bool Application::OnInit() catch (const FileError& e) { notifyAppError(e.toString(), FfsExitCode::warning); } - auto onSystemShutdown = [] + auto onSystemShutdown = [](int /*unused*/ = 0) { onSystemShutdownRunTasks(); @@ -153,20 +154,24 @@ bool Application::OnInit() }; Bind(wxEVT_QUERY_END_SESSION, [onSystemShutdown](wxCloseEvent& event) { onSystemShutdown(); }); //can veto Bind(wxEVT_END_SESSION, [onSystemShutdown](wxCloseEvent& event) { onSystemShutdown(); }); //can *not* veto + try + { + if (auto /*sighandler_t n.a. on macOS*/ oldHandler = ::signal(SIGTERM, onSystemShutdown);//"graceful" exit requested, unlike SIGKILL + oldHandler == SIG_ERR) + THROW_LAST_SYS_ERROR("signal(SIGTERM)"); + else assert(!oldHandler); + } + catch (const SysError& e) { notifyAppError(e.toString(), FfsExitCode::warning); } //Note: app start is deferred: -> see FreeFileSync - Bind(EVENT_ENTER_EVENT_LOOP, &Application::onEnterEventLoop, this); - wxCommandEvent scrollEvent(EVENT_ENTER_EVENT_LOOP); - AddPendingEvent(scrollEvent); + CallAfter([&] { onEnterEventLoop(); }); + return true; //true: continue processing; false: exit immediately. } -void Application::onEnterEventLoop(wxEvent& event) +void Application::onEnterEventLoop() { - [[maybe_unused]] bool ubOk = Unbind(EVENT_ENTER_EVENT_LOOP, &Application::onEnterEventLoop, this); - assert(ubOk); - //wxWidgets app exit handling is weird... we want to exit only if the logical main window is closed, not just *any* window! wxTheApp->SetExitOnFrameDelete(false); //prevent popup-windows from becoming temporary top windows leading to program exit after closure ZEN_ON_SCOPE_EXIT(if (!globalWindowWasSet()) wxTheApp->ExitMainLoop()); //quit application, if no main window was set (batch silent mode) diff --git a/FreeFileSync/Source/RealTimeSync/application.h b/FreeFileSync/Source/RealTimeSync/application.h index b2b52028..2a22b47e 100644 --- a/FreeFileSync/Source/RealTimeSync/application.h +++ b/FreeFileSync/Source/RealTimeSync/application.h @@ -22,7 +22,7 @@ private: void OnUnhandledException () override; wxLayoutDirection GetLayoutDirection() const override; - void onEnterEventLoop(wxEvent& event); + void onEnterEventLoop(); }; } diff --git a/FreeFileSync/Source/RealTimeSync/gui_generated.cpp b/FreeFileSync/Source/RealTimeSync/gui_generated.cpp index 0f91a13d..7ab659cd 100644 --- a/FreeFileSync/Source/RealTimeSync/gui_generated.cpp +++ b/FreeFileSync/Source/RealTimeSync/gui_generated.cpp @@ -56,49 +56,6 @@ MainDlgGenerated::MainDlgGenerated( wxWindow* parent, wxWindowID id, const wxStr wxBoxSizer* bSizer161; bSizer161 = new wxBoxSizer( wxVERTICAL ); - wxBoxSizer* bSizer16; - bSizer16 = new wxBoxSizer( wxHORIZONTAL ); - - m_staticText9 = new wxStaticText( this, wxID_ANY, _("Usage:"), wxDefaultPosition, wxDefaultSize, 0 ); - m_staticText9->Wrap( -1 ); - m_staticText9->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL, false, wxEmptyString ) ); - - bSizer16->Add( m_staticText9, 0, wxALL, 5 ); - - ffgSizer111 = new wxFlexGridSizer( 0, 2, 5, 5 ); - ffgSizer111->SetFlexibleDirection( wxBOTH ); - ffgSizer111->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); - - m_staticText16 = new wxStaticText( this, wxID_ANY, _("1."), wxDefaultPosition, wxDefaultSize, 0 ); - m_staticText16->Wrap( -1 ); - ffgSizer111->Add( m_staticText16, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 ); - - m_staticText3 = new wxStaticText( this, wxID_ANY, _("Select folders to watch."), wxDefaultPosition, wxDefaultSize, 0 ); - m_staticText3->Wrap( -1 ); - ffgSizer111->Add( m_staticText3, 0, wxALIGN_CENTER_VERTICAL, 5 ); - - m_staticText17 = new wxStaticText( this, wxID_ANY, _("2."), wxDefaultPosition, wxDefaultSize, 0 ); - m_staticText17->Wrap( -1 ); - ffgSizer111->Add( m_staticText17, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 ); - - m_staticText4 = new wxStaticText( this, wxID_ANY, _("Enter a command line."), wxDefaultPosition, wxDefaultSize, 0 ); - m_staticText4->Wrap( -1 ); - ffgSizer111->Add( m_staticText4, 0, wxALIGN_CENTER_VERTICAL, 5 ); - - m_staticText18 = new wxStaticText( this, wxID_ANY, _("3."), wxDefaultPosition, wxDefaultSize, 0 ); - m_staticText18->Wrap( -1 ); - ffgSizer111->Add( m_staticText18, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 ); - - m_staticText5 = new wxStaticText( this, wxID_ANY, _("Press 'Start'."), wxDefaultPosition, wxDefaultSize, 0 ); - m_staticText5->Wrap( -1 ); - ffgSizer111->Add( m_staticText5, 0, wxALIGN_CENTER_VERTICAL, 5 ); - - - bSizer16->Add( ffgSizer111, 0, wxALL, 5 ); - - - bSizer161->Add( bSizer16, 0, 0, 5 ); - wxBoxSizer* bSizer152; bSizer152 = new wxBoxSizer( wxHORIZONTAL ); @@ -152,7 +109,7 @@ MainDlgGenerated::MainDlgGenerated( wxWindow* parent, wxWindowID id, const wxStr m_bitmapFolders = new wxStaticBitmap( m_panelMain, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); bSizer142->Add( m_bitmapFolders, 0, wxTOP|wxBOTTOM|wxLEFT, 5 ); - m_staticText7 = new wxStaticText( m_panelMain, wxID_ANY, _("Folders to watch:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText7 = new wxStaticText( m_panelMain, wxID_ANY, _("Folders to watch for changes:"), wxDefaultPosition, wxDefaultSize, 0 ); m_staticText7->Wrap( -1 ); bSizer142->Add( m_staticText7, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); @@ -162,53 +119,31 @@ MainDlgGenerated::MainDlgGenerated( wxWindow* parent, wxWindowID id, const wxStr m_panelMainFolder = new wxPanel( m_panelMain, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); m_panelMainFolder->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); - wxFlexGridSizer* fgSizer1; - fgSizer1 = new wxFlexGridSizer( 0, 2, 0, 0 ); - fgSizer1->AddGrowableCol( 1 ); - fgSizer1->SetFlexibleDirection( wxBOTH ); - fgSizer1->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_ALL ); - - - fgSizer1->Add( 0, 0, 1, wxEXPAND, 5 ); - - m_staticTextFinalPath = new wxStaticText( m_panelMainFolder, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); - m_staticTextFinalPath->Wrap( -1 ); - fgSizer1->Add( m_staticTextFinalPath, 0, wxALIGN_CENTER_VERTICAL|wxALL, 2 ); - - wxBoxSizer* bSizer20; - bSizer20 = new wxBoxSizer( wxHORIZONTAL ); + wxBoxSizer* bSizer143; + bSizer143 = new wxBoxSizer( wxHORIZONTAL ); m_bpButtonAddFolder = new wxBitmapButton( m_panelMainFolder, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); m_bpButtonAddFolder->SetToolTip( _("Add folder") ); - bSizer20->Add( m_bpButtonAddFolder, 0, wxEXPAND, 5 ); + bSizer143->Add( m_bpButtonAddFolder, 0, wxEXPAND, 5 ); m_bpButtonRemoveTopFolder = new wxBitmapButton( m_panelMainFolder, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 ); m_bpButtonRemoveTopFolder->SetToolTip( _("Remove folder") ); - bSizer20->Add( m_bpButtonRemoveTopFolder, 0, wxEXPAND, 5 ); - - - fgSizer1->Add( bSizer20, 0, wxEXPAND, 5 ); - - wxBoxSizer* bSizer19; - bSizer19 = new wxBoxSizer( wxHORIZONTAL ); + bSizer143->Add( m_bpButtonRemoveTopFolder, 0, wxEXPAND, 5 ); m_txtCtrlDirectoryMain = new wxTextCtrl( m_panelMainFolder, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize( -1, -1 ), 0 ); - bSizer19->Add( m_txtCtrlDirectoryMain, 1, wxALIGN_CENTER_VERTICAL, 5 ); + bSizer143->Add( m_txtCtrlDirectoryMain, 1, wxALIGN_CENTER_VERTICAL, 5 ); m_buttonSelectFolderMain = new wxButton( m_panelMainFolder, wxID_ANY, _("Browse"), wxDefaultPosition, wxDefaultSize, 0 ); m_buttonSelectFolderMain->SetToolTip( _("Select a folder") ); - bSizer19->Add( m_buttonSelectFolderMain, 0, wxEXPAND, 5 ); + bSizer143->Add( m_buttonSelectFolderMain, 0, wxEXPAND, 5 ); - fgSizer1->Add( bSizer19, 0, wxEXPAND, 5 ); - - - m_panelMainFolder->SetSizer( fgSizer1 ); + m_panelMainFolder->SetSizer( bSizer143 ); m_panelMainFolder->Layout(); - fgSizer1->Fit( m_panelMainFolder ); + bSizer143->Fit( m_panelMainFolder ); bSizer151->Add( m_panelMainFolder, 0, wxRIGHT|wxLEFT|wxEXPAND, 5 ); m_scrolledWinFolders = new wxScrolledWindow( m_panelMain, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL|wxVSCROLL ); @@ -224,29 +159,11 @@ MainDlgGenerated::MainDlgGenerated( wxWindow* parent, wxWindowID id, const wxStr bSizer151->Add( m_scrolledWinFolders, 1, wxEXPAND|wxBOTTOM|wxRIGHT|wxLEFT, 5 ); - bSizer1->Add( bSizer151, 1, wxALL|wxEXPAND, 5 ); + bSizer1->Add( bSizer151, 1, wxALL|wxEXPAND, 10 ); m_staticline212 = new wxStaticLine( m_panelMain, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); bSizer1->Add( m_staticline212, 0, wxEXPAND, 5 ); - wxBoxSizer* bSizer14; - bSizer14 = new wxBoxSizer( wxHORIZONTAL ); - - m_staticText8 = new wxStaticText( m_panelMain, wxID_ANY, _("Minimum idle time (in seconds):"), wxDefaultPosition, wxDefaultSize, 0 ); - m_staticText8->Wrap( -1 ); - bSizer14->Add( m_staticText8, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 5 ); - - m_spinCtrlDelay = new wxSpinCtrl( m_panelMain, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 0, 2000000000, 0 ); - m_spinCtrlDelay->SetToolTip( _("Idle time between last detected change and execution of command") ); - - bSizer14->Add( m_spinCtrlDelay, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL|wxALL, 5 ); - - - bSizer1->Add( bSizer14, 0, wxALL|wxEXPAND, 5 ); - - m_staticline211 = new wxStaticLine( m_panelMain, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); - bSizer1->Add( m_staticline211, 0, wxEXPAND, 5 ); - wxBoxSizer* bSizer141; bSizer141 = new wxBoxSizer( wxVERTICAL ); @@ -256,7 +173,7 @@ MainDlgGenerated::MainDlgGenerated( wxWindow* parent, wxWindowID id, const wxStr m_bitmapConsole = new wxStaticBitmap( m_panelMain, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); bSizer13->Add( m_bitmapConsole, 0, wxTOP|wxBOTTOM|wxLEFT|wxALIGN_CENTER_VERTICAL, 5 ); - m_staticText6 = new wxStaticText( m_panelMain, wxID_ANY, _("Command line:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText6 = new wxStaticText( m_panelMain, wxID_ANY, _("Command line to run when changes are detected:"), wxDefaultPosition, wxDefaultSize, 0 ); m_staticText6->Wrap( -1 ); bSizer13->Add( m_staticText6, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); @@ -269,7 +186,25 @@ MainDlgGenerated::MainDlgGenerated( wxWindow* parent, wxWindowID id, const wxStr bSizer141->Add( m_textCtrlCommand, 0, wxEXPAND|wxBOTTOM|wxRIGHT|wxLEFT, 5 ); - bSizer1->Add( bSizer141, 0, wxALL|wxEXPAND, 5 ); + bSizer1->Add( bSizer141, 0, wxALL|wxEXPAND, 10 ); + + m_staticline211 = new wxStaticLine( m_panelMain, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer1->Add( m_staticline211, 0, wxEXPAND, 5 ); + + wxBoxSizer* bSizer14; + bSizer14 = new wxBoxSizer( wxHORIZONTAL ); + + m_staticText8 = new wxStaticText( m_panelMain, wxID_ANY, _("Minimum idle time (in seconds) before running command:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText8->Wrap( -1 ); + bSizer14->Add( m_staticText8, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 5 ); + + m_spinCtrlDelay = new wxSpinCtrl( m_panelMain, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 0, 2000000000, 0 ); + m_spinCtrlDelay->SetToolTip( _("Idle time between last detected change and execution of command") ); + + bSizer14->Add( m_spinCtrlDelay, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL|wxALL, 5 ); + + + bSizer1->Add( bSizer14, 0, wxALL|wxEXPAND, 10 ); m_panelMain->SetSizer( bSizer1 ); diff --git a/FreeFileSync/Source/RealTimeSync/gui_generated.h b/FreeFileSync/Source/RealTimeSync/gui_generated.h index 47514409..b3367031 100644 --- a/FreeFileSync/Source/RealTimeSync/gui_generated.h +++ b/FreeFileSync/Source/RealTimeSync/gui_generated.h @@ -22,9 +22,9 @@ namespace zen { class BitmapTextButton; } #include <wx/colour.h> #include <wx/settings.h> #include <wx/stattext.h> -#include <wx/sizer.h> #include <wx/statbmp.h> #include <wx/hyperlink.h> +#include <wx/sizer.h> #include <wx/statline.h> #include <wx/bmpbuttn.h> #include <wx/button.h> @@ -52,14 +52,6 @@ protected: wxMenu* m_menuHelp; wxMenuItem* m_menuItemAbout; wxBoxSizer* bSizerMain; - wxStaticText* m_staticText9; - wxFlexGridSizer* ffgSizer111; - wxStaticText* m_staticText16; - wxStaticText* m_staticText3; - wxStaticText* m_staticText17; - wxStaticText* m_staticText4; - wxStaticText* m_staticText18; - wxStaticText* m_staticText5; wxStaticText* m_staticText811; wxStaticText* m_staticText10; wxStaticBitmap* m_bitmapBatch; @@ -70,7 +62,6 @@ protected: wxStaticBitmap* m_bitmapFolders; wxStaticText* m_staticText7; wxPanel* m_panelMainFolder; - wxStaticText* m_staticTextFinalPath; wxBitmapButton* m_bpButtonAddFolder; wxBitmapButton* m_bpButtonRemoveTopFolder; wxTextCtrl* m_txtCtrlDirectoryMain; @@ -78,12 +69,12 @@ protected: wxScrolledWindow* m_scrolledWinFolders; wxBoxSizer* bSizerFolders; wxStaticLine* m_staticline212; - wxStaticText* m_staticText8; - wxSpinCtrl* m_spinCtrlDelay; - wxStaticLine* m_staticline211; wxStaticBitmap* m_bitmapConsole; wxStaticText* m_staticText6; wxTextCtrl* m_textCtrlCommand; + wxStaticLine* m_staticline211; + wxStaticText* m_staticText8; + wxSpinCtrl* m_spinCtrlDelay; wxStaticLine* m_staticline5; zen::BitmapTextButton* m_buttonStart; diff --git a/FreeFileSync/Source/RealTimeSync/main_dlg.cpp b/FreeFileSync/Source/RealTimeSync/main_dlg.cpp index 707442fc..dbed8788 100644 --- a/FreeFileSync/Source/RealTimeSync/main_dlg.cpp +++ b/FreeFileSync/Source/RealTimeSync/main_dlg.cpp @@ -9,7 +9,7 @@ #include <wx/filedlg.h> #include <wx+/app_main.h> #include <wx+/bitmap_button.h> -#include <wx+/font_size.h> +#include <wx+/window_layout.h> #include <wx+/popup_dlg.h> #include <wx+/image_resources.h> #include <zen/file_access.h> @@ -81,7 +81,7 @@ MainDialog::MainDialog(const Zstring& cfgFilePath) : m_scrolledWinFolders->SetScrollRate(scrollDelta, scrollDelta); m_txtCtrlDirectoryMain->SetMinSize({fastFromDIP(300), -1}); - m_spinCtrlDelay ->SetMinSize({fastFromDIP( 70), -1}); //Hack: set size (why does wxWindow::Size() not work?) + setDefaultWidth(*m_spinCtrlDelay); m_bpButtonRemoveTopFolder->Hide(); m_panelMainFolder->Layout(); @@ -101,7 +101,7 @@ MainDialog::MainDialog(const Zstring& cfgFilePath) : setGlobalWindow(this); //prepare drag & drop - firstFolderPanel_ = std::make_unique<FolderSelector2>(this, *m_panelMainFolder, *m_buttonSelectFolderMain, *m_txtCtrlDirectoryMain, folderLastSelected_, m_staticTextFinalPath); + firstFolderPanel_ = std::make_unique<FolderSelector2>(this, *m_panelMainFolder, *m_buttonSelectFolderMain, *m_txtCtrlDirectoryMain, folderLastSelected_, nullptr /*staticText*/); //--------------------------- load config values ------------------------------------ XmlRealConfig newConfig; diff --git a/FreeFileSync/Source/RealTimeSync/tray_menu.cpp b/FreeFileSync/Source/RealTimeSync/tray_menu.cpp index a1d723e3..0caffe70 100644 --- a/FreeFileSync/Source/RealTimeSync/tray_menu.cpp +++ b/FreeFileSync/Source/RealTimeSync/tray_menu.cpp @@ -251,6 +251,7 @@ rts::AbortReason rts::runFolderMonitor(const XmlRealConfig& config, const wxStri auto executeExternalCommand = [&](const Zstring& changedItemPath, const std::wstring& actionName) //throw FileError { + warn_static("maybe not a good idea!? job for execve? https://rachelbythebay.com/w/2017/01/30/env/") ::wxSetEnv(L"change_path", utfTo<wxString>(changedItemPath)); //crude way to report changed file ::wxSetEnv(L"change_action", actionName); // auto cmdLineExp = expandMacros(cmdLine); diff --git a/FreeFileSync/Source/afs/abstract.h b/FreeFileSync/Source/afs/abstract.h index 2d43736a..c5e8305f 100644 --- a/FreeFileSync/Source/afs/abstract.h +++ b/FreeFileSync/Source/afs/abstract.h @@ -263,7 +263,7 @@ struct AbstractFileSystem //THREAD-SAFETY: "const" member functions must model t //Note: it MAY happen that copyFileTransactional() leaves temp files behind, e.g. temporary network drop. // => clean them up at an appropriate time (automatically set sync directions to delete them). They have the following ending: - static inline const Zchar* const TEMP_FILE_ENDING = Zstr(".ffs_tmp"); //don't use Zstring as global constant: avoid static initialization order problem in global namespace! + static inline const ZstringView TEMP_FILE_ENDING = Zstr(".ffs_tmp"); //don't use Zstring as global constant: avoid static initialization order problem in global namespace! // caveat: ending is hard-coded by RealTimeSync struct FileCopyResult diff --git a/FreeFileSync/Source/afs/ftp.cpp b/FreeFileSync/Source/afs/ftp.cpp index ca3e147e..0bb9ee99 100644 --- a/FreeFileSync/Source/afs/ftp.cpp +++ b/FreeFileSync/Source/afs/ftp.cpp @@ -35,7 +35,7 @@ const size_t FTP_BLOCK_SIZE_UPLOAD = 64 * 1024; //libcurl requests blocks of 6 const size_t FTP_STREAM_BUFFER_SIZE = 1024 * 1024; //unit: [byte] //stream buffer should be big enough to facilitate prefetching during alternating read/write operations => e.g. see serialize.h::unbufferedStreamCopy() -const Zchar ftpPrefix[] = Zstr("ftp:"); +const ZstringView ftpPrefix = Zstr("ftp:"); enum class ServerEncoding @@ -2038,7 +2038,7 @@ struct OutputStreamFtp : public AFS::OutputStreamImpl AFS::FinalizeResult finalize(const IoCallback& notifyUnbufferedIO /*throw X*/) override //throw FileError, X { if (!asyncStreamOut_) - throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); asyncStreamOut_->closeStream(); @@ -2050,7 +2050,7 @@ struct OutputStreamFtp : public AFS::OutputStreamImpl futUploadDone_.get(); //throw FileError //asyncStreamOut_->checkReadErrors(); //throw FileError -> not needed after *successful* upload - asyncStreamOut_.reset(); //do NOT reset on failure, so that ~OutputStreamFtp() will request worker thread to stop + asyncStreamOut_.reset(); //do NOT reset on error, so that ~OutputStreamFtp() will request worker thread to stop //-------------------------------------------------------------------- AFS::FinalizeResult result; @@ -2080,7 +2080,7 @@ private: if (modTime_) try { - const std::string isoTime = utfTo<std::string>(formatTime(Zstr("%Y%m%d%H%M%S"), getUtcTime(*modTime_))); //returns empty string on failure + const std::string isoTime = utfTo<std::string>(formatTime(Zstr("%Y%m%d%H%M%S"), getUtcTime(*modTime_))); //returns empty string on error if (isoTime.empty()) throw SysError(L"Invalid modification time (time_t: " + numberTo<std::wstring>(*modTime_) + L')'); @@ -2449,7 +2449,7 @@ private: //caveat: connection phase only, so disable CURLOPT_SERVER_RESPONSE_TIMEOUT, or next access may fail with CURLE_OPERATION_TIMEDOUT! }); //throw SysError, SysErrorPassword, SysErrorFtpProtocol }; - + try { const std::shared_ptr<FtpSessionManager> mgr = globalFtpSessionManager.get(); @@ -2465,16 +2465,17 @@ private: connectServer(); //throw SysError, SysErrorPassword return; //got new FtpSession (connected in constructor) or already connected session from cache } - catch (SysErrorPassword&) {} - - if (!requestPassword) - throw SysError(_("Password prompt not permitted by current settings.")); + catch (const SysErrorPassword& e) + { + if (!requestPassword) + throw SysError(e.toString() + L'\n' + _("Password prompt not permitted by current settings.")); + } std::wstring lastErrorMsg; for (;;) { //2. request (new) password - std::wstring msg = replaceCpy(_("Please enter your password to connect to %x"), L"%x", fmtPath(getDisplayPath(AfsPath()))); + std::wstring msg = replaceCpy(_("Please enter your password to connect to %x."), L"%x", fmtPath(getDisplayPath(AfsPath()))); if (lastErrorMsg.empty()) msg += L"\n" + _("The password will only be remembered until FreeFileSync is closed."); @@ -2650,16 +2651,17 @@ AbstractPath fff::createItemPathFtp(const Zstring& itemPathPhrase) //noexcept const ZstringView port = afterLast(serverPort, Zstr(':'), IfNotFoundReturn::none); login.portCfg = stringTo<int>(port); //0 if empty - split(options, Zstr('|'), [&](const ZstringView optPhrase) + split(options, Zstr('|'), [&](ZstringView optPhrase) { + optPhrase = trimCpy(optPhrase); if (!optPhrase.empty()) { if (startsWith(optPhrase, Zstr("timeout="))) - login.timeoutSec = stringTo<int>(afterFirst(optPhrase, Zstr("="), IfNotFoundReturn::none)); + login.timeoutSec = stringTo<int>(afterFirst(optPhrase, Zstr('='), IfNotFoundReturn::none)); else if (optPhrase == Zstr("ssl")) login.useTls = true; else if (startsWith(optPhrase, Zstr("pass64="))) - login.password = decodePasswordBase64(afterFirst(optPhrase, Zstr("="), IfNotFoundReturn::none)); + login.password = decodePasswordBase64(afterFirst(optPhrase, Zstr('='), IfNotFoundReturn::none)); else if (optPhrase == Zstr("pwprompt")) login.password = std::nullopt; else diff --git a/FreeFileSync/Source/afs/gdrive.cpp b/FreeFileSync/Source/afs/gdrive.cpp index 33849de9..e82273cc 100644 --- a/FreeFileSync/Source/afs/gdrive.cpp +++ b/FreeFileSync/Source/afs/gdrive.cpp @@ -103,7 +103,7 @@ struct HttpSessionId Zstring server; }; -inline +inline bool operator==(const HttpSessionId& lhs, const HttpSessionId& rhs) { return equalAsciiNoCase(lhs.server, rhs.server); } } @@ -1392,7 +1392,7 @@ std::string /*fileId*/ gdriveCopyFile(const std::string& fileId, const std::stri //more Google Drive peculiarities: changing the file name changes modifiedTime!!! => workaround: //RFC 3339 date-time: e.g. "2018-09-29T08:39:12.053Z" - const std::string modTimeRfc = utfTo<std::string>(formatTime(Zstr("%Y-%m-%dT%H:%M:%S.000Z"), getUtcTime(newModTime))); //returns empty string on failure + const std::string modTimeRfc = utfTo<std::string>(formatTime(Zstr("%Y-%m-%dT%H:%M:%S.000Z"), getUtcTime(newModTime))); //returns empty string on error if (modTimeRfc.empty()) throw SysError(L"Invalid modification time (time_t: " + numberTo<std::wstring>(newModTime) + L')'); @@ -1442,7 +1442,7 @@ void gdriveMoveAndRenameItem(const std::string& itemId, const std::string& paren //more Google Drive peculiarities: changing the file name changes modifiedTime!!! => workaround: //RFC 3339 date-time: e.g. "2018-09-29T08:39:12.053Z" - const std::string modTimeRfc = utfTo<std::string>(formatTime(Zstr("%Y-%m-%dT%H:%M:%S.000Z"), getUtcTime(newModTime))); //returns empty string on failure + const std::string modTimeRfc = utfTo<std::string>(formatTime(Zstr("%Y-%m-%dT%H:%M:%S.000Z"), getUtcTime(newModTime))); //returns empty string on error if (modTimeRfc.empty()) throw SysError(L"Invalid modification time (time_t: " + numberTo<std::wstring>(newModTime) + L')'); @@ -1478,7 +1478,7 @@ void setModTime(const std::string& itemId, time_t modTime, const GdriveAccess& a { //https://developers.google.com/drive/api/v3/reference/files/update //RFC 3339 date-time: e.g. "2018-09-29T08:39:12.053Z" - const std::string& modTimeRfc = formatTime<std::string>("%Y-%m-%dT%H:%M:%S.000Z", getUtcTime2(modTime)); //returns empty string on failure + const std::string& modTimeRfc = formatTime<std::string>("%Y-%m-%dT%H:%M:%S.000Z", getUtcTime2(modTime)); //returns empty string on error if (modTimeRfc.empty()) throw SysError(L"Invalid modification time (time_t: " + numberTo<std::wstring>(modTime) + L')'); @@ -1600,7 +1600,7 @@ std::string /*itemId*/ gdriveUploadSmallFile(const Zstring& fileName, const std: postParams.objectVal.emplace("parents", std::vector<JsonValue> {JsonValue(parentId)}); if (modTime) //convert to RFC 3339 date-time: e.g. "2018-09-29T08:39:12.053Z" { - const std::string& modTimeRfc = utfTo<std::string>(formatTime(Zstr("%Y-%m-%dT%H:%M:%S.000Z"), getUtcTime2(*modTime))); //returns empty string on failure + const std::string& modTimeRfc = utfTo<std::string>(formatTime(Zstr("%Y-%m-%dT%H:%M:%S.000Z"), getUtcTime2(*modTime))); //returns empty string on error if (modTimeRfc.empty()) throw SysError(L"Invalid modification time (time_t: " + numberTo<std::wstring>(*modTime) + L')'); @@ -1711,7 +1711,7 @@ std::string /*itemId*/ gdriveUploadFile(const Zstring& fileName, const std::stri postParams.objectVal.emplace("parents", std::vector<JsonValue> {JsonValue(parentId)}); if (modTime) //convert to RFC 3339 date-time: e.g. "2018-09-29T08:39:12.053Z" { - const std::string& modTimeRfc = utfTo<std::string>(formatTime(Zstr("%Y-%m-%dT%H:%M:%S.000Z"), getUtcTime(*modTime))); //returns empty string on failure + const std::string& modTimeRfc = utfTo<std::string>(formatTime(Zstr("%Y-%m-%dT%H:%M:%S.000Z"), getUtcTime(*modTime))); //returns empty string on error if (modTimeRfc.empty()) throw SysError(L"Invalid modification time (time_t: " + numberTo<std::wstring>(*modTime) + L')'); @@ -1825,7 +1825,7 @@ public: void update(const GdriveAccessInfo& accessInfo) { if (!equalAsciiNoCase(accessInfo.userInfo.email, accessInfo_.userInfo.email)) - throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); accessInfo_ = accessInfo; } @@ -1908,7 +1908,7 @@ public: for (const auto& [folderId, content] : folderContents_) if (folderId.empty()) - throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); else if (content.isKnownFolder) writeContainer(stream, folderId); writeContainer(stream, std::string()); //sentinel @@ -1936,13 +1936,13 @@ public: { const auto& [itemId, details] = *itItem; if (itemId.empty()) - throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); serializeItem(itemId, details); if (details.type == GdriveItemType::shortcut) { if (details.targetId.empty()) - throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); if (auto it = itemDetails_.find(details.targetId); it != itemDetails_.end()) @@ -2002,7 +2002,7 @@ public: return *it; //itemId was already found! => (must either be a location root) or buffered in itemDetails_ - throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); } std::optional<GdriveItemDetails> tryGetBufferedItemDetails(const std::string& itemId) const @@ -2190,7 +2190,7 @@ private: itKnown = folderContents_.find(folderId); assert(itKnown != folderContents_.end()); if (!itKnown->second.isKnownFolder) - throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); } auto itFound = itemDetails_.cend(); @@ -2255,7 +2255,7 @@ private: } break; } - throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); } } @@ -2284,7 +2284,7 @@ private: if (it != itemDetails_.end()) //update { if (it->second.type != details->type) - throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); //WTF!? + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); //WTF!? std::vector<std::string> parentIdsNew = details->parentIds; std::vector<std::string> parentIdsRemoved = it->second.parentIds; @@ -3274,7 +3274,7 @@ struct OutputStreamGdrive : public AFS::OutputStreamImpl AFS::FinalizeResult finalize(const IoCallback& notifyUnbufferedIO /*throw X*/) override //throw FileError, X { if (!asyncStreamOut_) - throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); asyncStreamOut_->closeStream(); @@ -3287,7 +3287,7 @@ struct OutputStreamGdrive : public AFS::OutputStreamImpl result.filePrint = futFilePrint_.get(); //throw FileError //asyncStreamOut_->checkReadErrors(); //throw FileError -> not needed after *successful* upload - asyncStreamOut_.reset(); //do NOT reset on failure, so that ~OutputStreamGdrive() will request worker thread to stop + asyncStreamOut_.reset(); //do NOT reset on error, so that ~OutputStreamGdrive() will request worker thread to stop //-------------------------------------------------------------------- //result.errorModTime -> already (successfully) set during file creation @@ -4081,12 +4081,13 @@ AbstractPath fff::createItemPathGdrive(const Zstring& itemPathPhrase) //noexcept .locationName = Zstring(afterFirst (emailAndDrive, Zstr(':'), IfNotFoundReturn::none)), }; - split(options, Zstr('|'), [&](const ZstringView optPhrase) + split(options, Zstr('|'), [&](ZstringView optPhrase) { + optPhrase = trimCpy(optPhrase); if (!optPhrase.empty()) { if (startsWith(optPhrase, Zstr("timeout="))) - login.timeoutSec = stringTo<int>(afterFirst(optPhrase, Zstr("="), IfNotFoundReturn::none)); + login.timeoutSec = stringTo<int>(afterFirst(optPhrase, Zstr('='), IfNotFoundReturn::none)); else assert(false); } diff --git a/FreeFileSync/Source/afs/native.cpp b/FreeFileSync/Source/afs/native.cpp index bbcd82e9..48fdcac2 100644 --- a/FreeFileSync/Source/afs/native.cpp +++ b/FreeFileSync/Source/afs/native.cpp @@ -711,7 +711,7 @@ void RecycleSessionNative::moveToRecycleBin(const AbstractPath& itemPath, const { const Zstring& itemPathNative = getNativeItemPath(itemPath); if (itemPathNative.empty()) - throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); zen::moveToRecycleBin(itemPathNative); //throw FileError, RecycleBinUnavailable } diff --git a/FreeFileSync/Source/afs/sftp.cpp b/FreeFileSync/Source/afs/sftp.cpp index 86cc8d32..b284345d 100644 --- a/FreeFileSync/Source/afs/sftp.cpp +++ b/FreeFileSync/Source/afs/sftp.cpp @@ -47,7 +47,7 @@ OpenSSL supports the same ciphers like WinCNG plus the following: cast128-cbc blowfish-cbc */ -const Zchar sftpPrefix[] = Zstr("sftp:"); +const ZstringView sftpPrefix = Zstr("sftp:"); constexpr std::chrono::seconds SFTP_SESSION_MAX_IDLE_TIME (20); constexpr std::chrono::seconds SFTP_SESSION_CLEANUP_INTERVAL (4); //facilitate default of 5-seconds delay for error retry @@ -1050,8 +1050,8 @@ private: struct SshSessionCache { //invariant: all cached sessions correspond to activeCfg at any time! - std::vector<std::unique_ptr<SshSession>> idleSshSessions; //extract *temporarily* from this list during use - std::map<std::thread::id, std::weak_ptr<SshSessionShared>> sshSessionsWithThreadAffinity; //Win32 thread IDs may be REUSED! still, shouldn't be a problem... + std::vector<std::unique_ptr<SshSession>> idleSshSessions; //extract *temporarily* from this list during use + std::unordered_map<std::thread::id, std::weak_ptr<SshSessionShared>> sshSessionsWithThreadAffinity; //Win32 thread IDs may be REUSED! still, shouldn't be a problem... std::optional<SshSessionCfg> activeCfg; @@ -1351,7 +1351,7 @@ struct InputStreamSftp : public AFS::InputStream { //libssh2_sftp_read has same semantics as Posix read: if (bytesToRead == 0) //"read() with a count of 0 returns zero" => indistinguishable from end of file! => check! - throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); assert(bytesToRead % getBlockSize() == 0); ssize_t bytesRead = 0; @@ -1433,7 +1433,7 @@ struct OutputStreamSftp : public AFS::OutputStreamImpl size_t tryWrite(const void* buffer, size_t bytesToWrite, const IoCallback& notifyUnbufferedIO /*throw X*/) override //throw FileError, X; may return short! CONTRACT: bytesToWrite > 0 { if (bytesToWrite == 0) - throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); assert(bytesToWrite % getBlockSize() == 0 || bytesToWrite < getBlockSize()); ssize_t bytesWritten = 0; @@ -1484,10 +1484,10 @@ private: void close() //throw FileError { if (!fileHandle_) - throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); try { - ZEN_ON_SCOPE_EXIT(fileHandle_ = nullptr); //reset on failure, too! there's no point in, calling libssh2_sftp_close() a second time in ~OutputStreamSftp() + ZEN_ON_SCOPE_EXIT(fileHandle_ = nullptr); //reset on error, too! there's no point in, calling libssh2_sftp_close() a second time in ~OutputStreamSftp() session_->executeBlocking("libssh2_sftp_close", //throw SysError, SysErrorSftpProtocol [&](const SshSession::Details& sd) { return ::libssh2_sftp_close(fileHandle_); }); //noexcept! @@ -1889,17 +1889,18 @@ private: { /*auto session =*/ mgr->getSharedSession(login_); //throw SysError, SysErrorPassword return; //got new SshSession (connected in constructor) or already connected session from cache - } - catch (SysErrorPassword&) {} - - if (!requestPassword) - throw SysError(_("Password prompt not permitted by current settings.")); + } + catch (const SysErrorPassword& e) + { + if (!requestPassword) + throw SysError(e.toString() + L'\n' + _("Password prompt not permitted by current settings.")); + } std::wstring lastErrorMsg; for (;;) { //2. request (new) password - std::wstring msg = replaceCpy(_("Please enter your password to connect to %x"), L"%x", fmtPath(getDisplayPath(AfsPath()))); + std::wstring msg = replaceCpy(_("Please enter your password to connect to %x."), L"%x", fmtPath(getDisplayPath(AfsPath()))); if (lastErrorMsg.empty()) msg += L"\n" + _("The password will only be remembered until FreeFileSync is closed."); @@ -2146,23 +2147,24 @@ AbstractPath fff::createItemPathSftp(const Zstring& itemPathPhrase) //noexcept assert(login.allowZlib == false); - split(options, Zstr('|'), [&](const ZstringView optPhrase) + split(options, Zstr('|'), [&](ZstringView optPhrase) { + optPhrase = trimCpy(optPhrase); if (!optPhrase.empty()) { if (startsWith(optPhrase, Zstr("timeout="))) - login.timeoutSec = stringTo<int>(afterFirst(optPhrase, Zstr("="), IfNotFoundReturn::none)); + login.timeoutSec = stringTo<int>(afterFirst(optPhrase, Zstr('='), IfNotFoundReturn::none)); else if (startsWith(optPhrase, Zstr("chan="))) - login.traverserChannelsPerConnection = stringTo<int>(afterFirst(optPhrase, Zstr("="), IfNotFoundReturn::none)); + login.traverserChannelsPerConnection = stringTo<int>(afterFirst(optPhrase, Zstr('='), IfNotFoundReturn::none)); else if (startsWith(optPhrase, Zstr("keyfile="))) { login.authType = SftpAuthType::keyFile; - login.privateKeyFilePath = getResolvedFilePath(Zstring(afterFirst(optPhrase, Zstr("="), IfNotFoundReturn::none))); + login.privateKeyFilePath = getResolvedFilePath(Zstring(afterFirst(optPhrase, Zstr('='), IfNotFoundReturn::none))); } else if (optPhrase == Zstr("agent")) login.authType = SftpAuthType::agent; else if (startsWith(optPhrase, Zstr("pass64="))) - login.password = decodePasswordBase64(afterFirst(optPhrase, Zstr("="), IfNotFoundReturn::none)); + login.password = decodePasswordBase64(afterFirst(optPhrase, Zstr('='), IfNotFoundReturn::none)); else if (optPhrase == Zstr("pwprompt")) login.password = std::nullopt; else if (optPhrase == Zstr("zlib")) diff --git a/FreeFileSync/Source/application.cpp b/FreeFileSync/Source/application.cpp index 69d1c3fa..7a2d23fb 100644 --- a/FreeFileSync/Source/application.cpp +++ b/FreeFileSync/Source/application.cpp @@ -35,6 +35,16 @@ using namespace zen; using namespace fff; +#ifdef __WXGTK3__ + /* Wayland backend used by GTK3 does not allow to move windows! (no such issue on GTK2) + + "I'd really like to know if there is some deep technical reason for it or + if this is really as bloody stupid as it seems?" - vadz https://github.com/wxWidgets/wxWidgets/issues/18733#issuecomment-1011235902 + + => workaround: https://docs.gtk.org/gdk3/func.set_allowed_backends.html */ + GLOBAL_RUN_ONCE(::gdk_set_allowed_backends("x11,*")); //call *before* gtk_init() +#endif + IMPLEMENT_APP(Application) @@ -52,8 +62,6 @@ std::vector<Zstring> getCommandlineArgs(const wxApp& app) return args; } -wxDEFINE_EVENT(EVENT_ENTER_EVENT_LOOP, wxCommandEvent); - void showSyntaxHelp() { @@ -197,7 +205,7 @@ bool Application::OnInit() - auto onSystemShutdown = [] + auto onSystemShutdown = [](int /*unused*/ = 0) { onSystemShutdownRunTasks(); @@ -208,20 +216,24 @@ bool Application::OnInit() }; Bind(wxEVT_QUERY_END_SESSION, [onSystemShutdown](wxCloseEvent& event) { onSystemShutdown(); }); //can veto Bind(wxEVT_END_SESSION, [onSystemShutdown](wxCloseEvent& event) { onSystemShutdown(); }); //can *not* veto + //- log off: Windows/macOS generates wxEVT_QUERY_END_SESSION/wxEVT_END_SESSION + // Linux/macOS generates SIGTERM, which we handle below + //- Windows sends WM_QUERYENDSESSION, WM_ENDSESSION during log off, *not* WM_CLOSE https://devblogs.microsoft.com/oldnewthing/20080421-00/?p=22663 + // => taskkill sending WM_CLOSE (without /f) is a misguided app simulating a button-click on X + // -> should send WM_QUERYENDSESSION instead! + try + { + if (auto /*sighandler_t n.a. on macOS*/ oldHandler = ::signal(SIGTERM, onSystemShutdown);//"graceful" exit requested, unlike SIGKILL + oldHandler == SIG_ERR) + THROW_LAST_SYS_ERROR("signal(SIGTERM)"); + else assert(!oldHandler); + } + catch (const SysError& e) { notifyAppError(e.toString(), FfsExitCode::warning); } //Note: app start is deferred: batch mode requires the wxApp eventhandler to be established for UI update events. This is not the case at the time of OnInit()! - Bind(EVENT_ENTER_EVENT_LOOP, &Application::onEnterEventLoop, this); - AddPendingEvent(wxCommandEvent(EVENT_ENTER_EVENT_LOOP)); - return true; //true: continue processing; false: exit immediately. -} - + CallAfter([&] { onEnterEventLoop(); }); -void Application::onEnterEventLoop(wxEvent& event) -{ - [[maybe_unused]] const bool ubOk = Unbind(EVENT_ENTER_EVENT_LOOP, &Application::onEnterEventLoop, this); - assert(ubOk); - - launch(getCommandlineArgs(*this)); //determine FFS mode of operation + return true; //true: continue processing; false: exit immediately. } @@ -267,8 +279,10 @@ void Application::OnUnhandledException() //handles both wxApp::OnInit() + wxApp: -void Application::launch(const std::vector<Zstring>& commandArgs) +void Application::onEnterEventLoop() { + const std::vector<Zstring>& commandArgs = getCommandlineArgs(*this); + //wxWidgets app exit handling is weird... we want to exit only if the logical main window is closed, not just *any* window! wxTheApp->SetExitOnFrameDelete(false); //prevent popup-windows from becoming temporary top windows leading to program exit after closure ZEN_ON_SCOPE_EXIT(if (!globalWindowWasSet()) wxTheApp->ExitMainLoop()); //quit application, if no main window was set (batch silent mode) @@ -548,7 +562,7 @@ void Application::runBatchMode(const Zstring& globalConfigFilePath, const XmlBat batchCfg.mainCfg.autoRetryDelay, globalCfg.soundFileSyncFinished, globalCfg.soundFileAlertPending, - globalCfg.dpiLayouts[getDpiScalePercent()].progressDlg.dlgSize, + globalCfg.dpiLayouts[getDpiScalePercent()].progressDlg.size, globalCfg.dpiLayouts[getDpiScalePercent()].progressDlg.isMaximized, batchCfg.batchExCfg.autoCloseSummary, batchCfg.batchExCfg.postSyncAction, @@ -621,7 +635,7 @@ void Application::runBatchMode(const Zstring& globalConfigFilePath, const XmlBat //*INDENT-ON* } - globalCfg.dpiLayouts[getDpiScalePercent()].progressDlg.dlgSize = r.dlgSize; + globalCfg.dpiLayouts[getDpiScalePercent()].progressDlg.size = r.dlgSize; globalCfg.dpiLayouts[getDpiScalePercent()].progressDlg.isMaximized = r.dlgIsMaximized; //email sending, or saving log file failed? at the very least this should affect the exit code: diff --git a/FreeFileSync/Source/application.h b/FreeFileSync/Source/application.h index 3d8b75e6..1eaf8fe1 100644 --- a/FreeFileSync/Source/application.h +++ b/FreeFileSync/Source/application.h @@ -26,8 +26,7 @@ private: void OnUnhandledException () override; void notifyAppError(const std::wstring& msg, FfsExitCode rc); wxLayoutDirection GetLayoutDirection() const override; - void onEnterEventLoop(wxEvent& event); - void launch(const std::vector<Zstring>& commandArgs); + void onEnterEventLoop(); void runGuiMode (const Zstring& globalConfigFile); void runGuiMode (const Zstring& globalConfigFile, const XmlGuiConfig& guiCfg, const std::vector<Zstring>& cfgFilePaths, bool startComparison); diff --git a/FreeFileSync/Source/base/algorithm.cpp b/FreeFileSync/Source/base/algorithm.cpp index 49cc4883..3c371d34 100644 --- a/FreeFileSync/Source/base/algorithm.cpp +++ b/FreeFileSync/Source/base/algorithm.cpp @@ -698,7 +698,7 @@ std::vector<std::pair<BaseFolderPair*, SyncDirectionConfig>> fff::extractDirecti mainCfg.additionalPairs.end()); if (folderCmp.size() != allPairs.size()) - throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); std::vector<std::pair<BaseFolderPair*, SyncDirectionConfig>> output; @@ -998,7 +998,7 @@ void fff::applyFiltering(FolderComparison& folderCmp, const MainConfiguration& m if (folderCmp.empty()) return; else if (folderCmp.size() != mainCfg.additionalPairs.size() + 1) - throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); //merge first and additional pairs std::vector<LocalPairConfig> allPairs; diff --git a/FreeFileSync/Source/base/comparison.cpp b/FreeFileSync/Source/base/comparison.cpp index 74035b80..240f31e4 100644 --- a/FreeFileSync/Source/base/comparison.cpp +++ b/FreeFileSync/Source/base/comparison.cpp @@ -8,7 +8,6 @@ #include <zen/process_priority.h> #include <zen/perf.h> #include <zen/time.h> -#include <wx/datetime.h> #include "algorithm.h" #include "parallel_scan.h" #include "dir_exist_async.h" @@ -76,25 +75,55 @@ ResolvedBaseFolders initializeBaseFolders(const std::vector<FolderPairCfg>& fpCf WarningDialogs& warnings, PhaseCallback& callback /*throw X*/) //throw X { + std::vector<Zstring> pathPhrases; + for (const FolderPairCfg& fpCfg : fpCfgList) + { + pathPhrases.push_back(fpCfg.folderPathPhraseLeft_); + pathPhrases.push_back(fpCfg.folderPathPhraseRight_); + } + ResolvedBaseFolders output; std::set<AbstractPath> allFolders; tryReportingError([&] { - //support "retry" for environment variable and variable driver letter resolution! - allFolders.clear(); - output.resolvedPairs.clear(); - for (const FolderPairCfg& fpCfg : fpCfgList) + //createAbstractPath() -> tryExpandVolumeName() hangs for idle HDD! => run async to make it cancellable + auto protCurrentPhrase = makeSharedRef<Protected<Zstring>>(); + + std::future<std::vector<AbstractPath>> futFolderPaths = runAsync([pathPhrases, currentPhraseWeak = std::weak_ptr(protCurrentPhrase.ptr())] { - output.resolvedPairs.push_back( + setCurrentThreadName(Zstr("Normalizing folder paths")); + + std::vector<AbstractPath> folderPaths; + for (const Zstring& pathPhrase : pathPhrases) { - createAbstractPath(fpCfg.folderPathPhraseLeft_), - createAbstractPath(fpCfg.folderPathPhraseRight_)}); + if (auto protCurrentPhrase2 = currentPhraseWeak.lock()) //[!] not owned by worker thread! + protCurrentPhrase2->access([&](Zstring& currentPathPhrase) { currentPathPhrase = pathPhrase; }); + else + throw std::runtime_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Caller context gone!"); - allFolders.insert(output.resolvedPairs.back().folderPathLeft); - allFolders.insert(output.resolvedPairs.back().folderPathRight); + folderPaths.push_back(createAbstractPath(pathPhrase)); + } + return folderPaths; + }); + + while (futFolderPaths.wait_for(UI_UPDATE_INTERVAL / 2) == std::future_status::timeout) + { + const Zstring pathPhrase = protCurrentPhrase.ref().access([](const Zstring& currentPathPhrase) { return currentPathPhrase; }); + callback.updateStatus(_("Normalizing folder paths...") + L' ' + utfTo<std::wstring>(pathPhrase)); //throw X } + + const std::vector<AbstractPath>& folderPaths = futFolderPaths.get(); //throw (std::runtime_error) + + //support "retry" for environment variable and variable driver letter resolution! + allFolders.clear(); + allFolders.insert(folderPaths.begin(), folderPaths.end()); + + output.resolvedPairs.clear(); + for (size_t i = 0; i < folderPaths.size(); i += 2) + output.resolvedPairs.push_back({folderPaths[i], folderPaths[i + 1]}); //--------------------------------------------------------------------------- + output.baseFolderStatus = getFolderStatusParallel(allFolders, true /*authenticateAccess*/, requestPassword, callback); //throw X if (!output.baseFolderStatus.failedChecks.empty()) @@ -214,10 +243,9 @@ ComparisonBuffer::ComparisonBuffer(const std::set<DirectoryKey>& folderKeys, UI_UPDATE_INTERVAL / 2); //every ~50 ms const int64_t totalTimeSec = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::steady_clock::now() - compareStartTime).count(); - callback.logMessage(_("Comparison finished:") + L' ' + _P("1 item found", "%x items found", itemsReported) + SPACED_DASH + - _("Time elapsed:") + L' ' + copyStringTo<std::wstring>(wxTimeSpan::Seconds(totalTimeSec).Format()), + _("Time elapsed:") + L' ' + utfTo<std::wstring>(formatTimeSpan(totalTimeSec)), PhaseCallback::MsgType::info); //throw X //------------------------------------------------------------------ @@ -739,19 +767,19 @@ void MergeSides::fillOneSide(const FolderContainer& folderCont, const Zstringc* { forEachSorted(folderCont.files, [&](const Zstring& fileName, const FileAttributes& attrib) { - FilePair& newItem = output.addSubFile<side>(fileName, attrib); + FilePair& newItem = output.addFile<side>(fileName, attrib); checkFailedRead(newItem, errorMsg); }); forEachSorted(folderCont.symlinks, [&](const Zstring& linkName, const LinkAttributes& attrib) { - SymlinkPair& newItem = output.addSubLink<side>(linkName, attrib); + SymlinkPair& newItem = output.addLink<side>(linkName, attrib); checkFailedRead(newItem, errorMsg); }); forEachSorted(folderCont.folders, [&](const Zstring& folderName, const std::pair<FolderAttributes, FolderContainer>& attrib) { - FolderPair& newFolder = output.addSubFolder<side>(folderName, attrib.first); + FolderPair& newFolder = output.addFolder<side>(folderName, attrib.first); const Zstringc* errorMsgNew = checkFailedRead(newFolder, errorMsg); fillOneSide<side>(attrib.second, errorMsgNew, newFolder); //recurse @@ -838,24 +866,24 @@ void MergeSides::mergeTwoSides(const FolderContainer& lhs, const FolderContainer matchFolders(lhs.files, rhs.files, [&](const FileData& fileLeft, const Zstringc* conflictMsg) { - FilePair& newItem = output.addSubFile<SelectSide::left >(fileLeft .first, fileLeft .second); + FilePair& newItem = output.addFile<SelectSide::left >(fileLeft .first, fileLeft .second); checkFailedRead(newItem, conflictMsg ? conflictMsg : errorMsg); }, [&](const FileData& fileRight, const Zstringc* conflictMsg) { - FilePair& newItem = output.addSubFile<SelectSide::right>(fileRight.first, fileRight.second); + FilePair& newItem = output.addFile<SelectSide::right>(fileRight.first, fileRight.second); checkFailedRead(newItem, conflictMsg ? conflictMsg : errorMsg); }, [&](const FileData& fileLeft, const FileData& fileRight) { - FilePair& newItem = output.addSubFile(fileLeft.first, - fileLeft.second, - FILE_CONFLICT, //dummy-value until categorization is finished later - fileRight.first, - fileRight.second); + FilePair& newItem = output.addFile(fileLeft.first, + fileLeft.second, + FILE_CONFLICT, //dummy-value until categorization is finished later + fileRight.first, + fileRight.second); if (!checkFailedRead(newItem, errorMsg)) undefinedFiles_.push_back(&newItem); - static_assert(std::is_same_v<ContainerObject::FileList, std::list<FilePair>>); //ContainerObject::addSubFile() must NOT invalidate references used in "undefinedFiles"! + static_assert(std::is_same_v<ContainerObject::FileList, std::list<FilePair>>); //ContainerObject::addFile() must NOT invalidate references used in "undefinedFiles"! }); //----------------------------------------------------------------------------------------------- @@ -863,21 +891,21 @@ void MergeSides::mergeTwoSides(const FolderContainer& lhs, const FolderContainer matchFolders(lhs.symlinks, rhs.symlinks, [&](const SymlinkData& symlinkLeft, const Zstringc* conflictMsg) { - SymlinkPair& newItem = output.addSubLink<SelectSide::left >(symlinkLeft .first, symlinkLeft .second); + SymlinkPair& newItem = output.addLink<SelectSide::left >(symlinkLeft .first, symlinkLeft .second); checkFailedRead(newItem, conflictMsg ? conflictMsg : errorMsg); }, [&](const SymlinkData& symlinkRight, const Zstringc* conflictMsg) { - SymlinkPair& newItem = output.addSubLink<SelectSide::right>(symlinkRight.first, symlinkRight.second); + SymlinkPair& newItem = output.addLink<SelectSide::right>(symlinkRight.first, symlinkRight.second); checkFailedRead(newItem, conflictMsg ? conflictMsg : errorMsg); }, [&](const SymlinkData& symlinkLeft, const SymlinkData& symlinkRight) //both sides { - SymlinkPair& newItem = output.addSubLink(symlinkLeft.first, - symlinkLeft.second, - SYMLINK_CONFLICT, //dummy-value until categorization is finished later - symlinkRight.first, - symlinkRight.second); + SymlinkPair& newItem = output.addLink(symlinkLeft.first, + symlinkLeft.second, + SYMLINK_CONFLICT, //dummy-value until categorization is finished later + symlinkRight.first, + symlinkRight.second); if (!checkFailedRead(newItem, errorMsg)) undefinedSymlinks_.push_back(&newItem); }); @@ -887,19 +915,19 @@ void MergeSides::mergeTwoSides(const FolderContainer& lhs, const FolderContainer matchFolders(lhs.folders, rhs.folders, [&](const FolderData& dirLeft, const Zstringc* conflictMsg) { - FolderPair& newFolder = output.addSubFolder<SelectSide::left>(dirLeft.first, dirLeft.second.first); + FolderPair& newFolder = output.addFolder<SelectSide::left>(dirLeft.first, dirLeft.second.first); const Zstringc* errorMsgNew = checkFailedRead(newFolder, conflictMsg ? conflictMsg : errorMsg); this->fillOneSide<SelectSide::left>(dirLeft.second.second, errorMsgNew, newFolder); //recurse }, [&](const FolderData& dirRight, const Zstringc* conflictMsg) { - FolderPair& newFolder = output.addSubFolder<SelectSide::right>(dirRight.first, dirRight.second.first); + FolderPair& newFolder = output.addFolder<SelectSide::right>(dirRight.first, dirRight.second.first); const Zstringc* errorMsgNew = checkFailedRead(newFolder, conflictMsg ? conflictMsg : errorMsg); this->fillOneSide<SelectSide::right>(dirRight.second.second, errorMsgNew, newFolder); //recurse }, [&](const FolderData& dirLeft, const FolderData& dirRight) { - FolderPair& newFolder = output.addSubFolder(dirLeft.first, dirLeft.second.first, DIR_EQUAL, dirRight.first, dirRight.second.first); + FolderPair& newFolder = output.addFolder(dirLeft.first, dirLeft.second.first, DIR_EQUAL, dirRight.first, dirRight.second.first); const Zstringc* errorMsgNew = checkFailedRead(newFolder, errorMsg); if (!errorMsgNew) @@ -1067,7 +1095,7 @@ FolderComparison fff::compare(WarningDialogs& warnings, requestPassword, warnings, callback); //throw X //directory existence only checked *once* to avoid race conditions! if (resInfo.resolvedPairs.size() != fpCfgList.size()) - throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); std::vector<std::pair<ResolvedFolderPair, FolderPairCfg>> workLoad; for (size_t i = 0; i < fpCfgList.size(); ++i) diff --git a/FreeFileSync/Source/base/db_file.h b/FreeFileSync/Source/base/db_file.h index 552cdfef..98d681b8 100644 --- a/FreeFileSync/Source/base/db_file.h +++ b/FreeFileSync/Source/base/db_file.h @@ -15,7 +15,7 @@ namespace fff { -const Zchar SYNC_DB_FILE_ENDING[] = Zstr(".ffs_db"); //don't use Zstring as global constant: avoid static initialization order problem in global namespace! +const ZstringView SYNC_DB_FILE_ENDING = Zstr(".ffs_db"); //don't use Zstring as global constant: avoid static initialization order problem in global namespace! struct InSyncDescrFile //subset of FileAttributes { diff --git a/FreeFileSync/Source/base/dir_exist_async.h b/FreeFileSync/Source/base/dir_exist_async.h index a344cc06..f6af51fa 100644 --- a/FreeFileSync/Source/base/dir_exist_async.h +++ b/FreeFileSync/Source/base/dir_exist_async.h @@ -128,7 +128,7 @@ FolderStatus getFolderStatusParallel(const std::set<AbstractPath>& folderPaths, if (protPromptsPending) protPromptsPending->access([&](RingBuffer<AsyncPrompt>& promptsPending) { - //do work while holding Protected<> lock!? device authentication threads blocking doesn't matter because prompts are serialized to GUI anyway + //call back while holding Protected<> lock!? device authentication threads blocking doesn't matter because prompts are serialized to GUI anyway if (!promptsPending.empty()) { assert(requestPassword); //... in this context diff --git a/FreeFileSync/Source/base/dir_lock.cpp b/FreeFileSync/Source/base/dir_lock.cpp index 0ab22486..543313bb 100644 --- a/FreeFileSync/Source/base/dir_lock.cpp +++ b/FreeFileSync/Source/base/dir_lock.cpp @@ -308,6 +308,8 @@ ProcessStatus getProcessStatus(const LockInformation& lockInfo) //throw FileErro DEFINE_NEW_FILE_ERROR(ErrorFileNotExisting) uint64_t getLockFileSize(const Zstring& filePath) //throw FileError, ErrorFileNotExisting { + //yes, checking error ERROR_FILE_NOT_FOUND, ENOENT is not 100% reliable (e.g. might indicate missing parent folder) + //but good enough for our lock file! struct stat fileInfo = {}; if (::stat(filePath.c_str(), &fileInfo) == 0) return fileInfo.st_size; diff --git a/FreeFileSync/Source/base/file_hierarchy.h b/FreeFileSync/Source/base/file_hierarchy.h index 5709f9db..28c3f7ef 100644 --- a/FreeFileSync/Source/base/file_hierarchy.h +++ b/FreeFileSync/Source/base/file_hierarchy.h @@ -107,21 +107,21 @@ struct FolderContainer SymlinkList symlinks; //non-followed symlinks FolderList folders; - void addSubFile(const Zstring& itemName, const FileAttributes& attr) + void addFile(const Zstring& itemName, const FileAttributes& attr) { const auto [it, inserted] = files.emplace(itemName, attr); if (!inserted) //update entry if already existing (e.g. during folder traverser "retry") it->second = attr; } - void addSubLink(const Zstring& itemName, const LinkAttributes& attr) + void addLink(const Zstring& itemName, const LinkAttributes& attr) { const auto [it, inserted] = symlinks.emplace(itemName, attr); if (!inserted) it->second = attr; } - FolderContainer& addSubFolder(const Zstring& itemName, const FolderAttributes& attr) + FolderContainer& addFolder(const Zstring& itemName, const FolderAttributes& attr) { auto& p = folders[itemName]; //value default-construction is okay here p.first = attr; @@ -189,35 +189,35 @@ public: using SymlinkList = std::list<SymlinkPair>; // using FolderList = std::list<FolderPair>; - FolderPair& addSubFolder(const Zstring& itemNameL, - const FolderAttributes& left, //file exists on both sides - CompareDirResult defaultCmpResult, - const Zstring& itemNameR, - const FolderAttributes& right); + FolderPair& addFolder(const Zstring& itemNameL, + const FolderAttributes& left, //file exists on both sides + CompareDirResult defaultCmpResult, + const Zstring& itemNameR, + const FolderAttributes& right); template <SelectSide side> - FolderPair& addSubFolder(const Zstring& itemName, //dir exists on one side only - const FolderAttributes& attr); + FolderPair& addFolder(const Zstring& itemName, //dir exists on one side only + const FolderAttributes& attr); - FilePair& addSubFile(const Zstring& itemNameL, - const FileAttributes& left, //file exists on both sides - CompareFileResult defaultCmpResult, - const Zstring& itemNameR, - const FileAttributes& right); + FilePair& addFile(const Zstring& itemNameL, + const FileAttributes& left, //file exists on both sides + CompareFileResult defaultCmpResult, + const Zstring& itemNameR, + const FileAttributes& right); template <SelectSide side> - FilePair& addSubFile(const Zstring& itemName, //file exists on one side only - const FileAttributes& attr); + FilePair& addFile(const Zstring& itemName, //file exists on one side only + const FileAttributes& attr); - SymlinkPair& addSubLink(const Zstring& itemNameL, - const LinkAttributes& left, //link exists on both sides - CompareSymlinkResult defaultCmpResult, - const Zstring& itemNameR, - const LinkAttributes& right); + SymlinkPair& addLink(const Zstring& itemNameL, + const LinkAttributes& left, //link exists on both sides + CompareSymlinkResult defaultCmpResult, + const Zstring& itemNameR, + const LinkAttributes& right); template <SelectSide side> - SymlinkPair& addSubLink(const Zstring& itemName, //link exists on one side only - const LinkAttributes& attr); + SymlinkPair& addLink(const Zstring& itemName, //link exists on one side only + const LinkAttributes& attr); const FileList& refSubFiles() const { return subFiles_; } /**/ FileList& refSubFiles() { return subFiles_; } @@ -1009,11 +1009,11 @@ void ContainerObject::flip() inline -FolderPair& ContainerObject::addSubFolder(const Zstring& itemNameL, - const FolderAttributes& left, - CompareDirResult defaultCmpResult, - const Zstring& itemNameR, - const FolderAttributes& right) +FolderPair& ContainerObject::addFolder(const Zstring& itemNameL, + const FolderAttributes& left, + CompareDirResult defaultCmpResult, + const Zstring& itemNameR, + const FolderAttributes& right) { subFolders_.emplace_back(itemNameL, left, defaultCmpResult, itemNameR, right, *this); return subFolders_.back(); @@ -1021,7 +1021,7 @@ FolderPair& ContainerObject::addSubFolder(const Zstring& itemNameL, template <> inline -FolderPair& ContainerObject::addSubFolder<SelectSide::left>(const Zstring& itemName, const FolderAttributes& attr) +FolderPair& ContainerObject::addFolder<SelectSide::left>(const Zstring& itemName, const FolderAttributes& attr) { subFolders_.emplace_back(itemName, attr, DIR_LEFT_SIDE_ONLY, Zstring(), FolderAttributes(), *this); return subFolders_.back(); @@ -1029,7 +1029,7 @@ FolderPair& ContainerObject::addSubFolder<SelectSide::left>(const Zstring& itemN template <> inline -FolderPair& ContainerObject::addSubFolder<SelectSide::right>(const Zstring& itemName, const FolderAttributes& attr) +FolderPair& ContainerObject::addFolder<SelectSide::right>(const Zstring& itemName, const FolderAttributes& attr) { subFolders_.emplace_back(Zstring(), FolderAttributes(), DIR_RIGHT_SIDE_ONLY, itemName, attr, *this); return subFolders_.back(); @@ -1037,11 +1037,11 @@ FolderPair& ContainerObject::addSubFolder<SelectSide::right>(const Zstring& item inline -FilePair& ContainerObject::addSubFile(const Zstring& itemNameL, - const FileAttributes& left, //file exists on both sides - CompareFileResult defaultCmpResult, - const Zstring& itemNameR, - const FileAttributes& right) +FilePair& ContainerObject::addFile(const Zstring& itemNameL, + const FileAttributes& left, //file exists on both sides + CompareFileResult defaultCmpResult, + const Zstring& itemNameR, + const FileAttributes& right) { subFiles_.emplace_back(itemNameL, left, defaultCmpResult, itemNameR, right, *this); return subFiles_.back(); @@ -1049,7 +1049,7 @@ FilePair& ContainerObject::addSubFile(const Zstring& itemNameL, template <> inline -FilePair& ContainerObject::addSubFile<SelectSide::left>(const Zstring& itemName, const FileAttributes& attr) +FilePair& ContainerObject::addFile<SelectSide::left>(const Zstring& itemName, const FileAttributes& attr) { subFiles_.emplace_back(itemName, attr, FILE_LEFT_SIDE_ONLY, Zstring(), FileAttributes(), *this); return subFiles_.back(); @@ -1057,7 +1057,7 @@ FilePair& ContainerObject::addSubFile<SelectSide::left>(const Zstring& itemName, template <> inline -FilePair& ContainerObject::addSubFile<SelectSide::right>(const Zstring& itemName, const FileAttributes& attr) +FilePair& ContainerObject::addFile<SelectSide::right>(const Zstring& itemName, const FileAttributes& attr) { subFiles_.emplace_back(Zstring(), FileAttributes(), FILE_RIGHT_SIDE_ONLY, itemName, attr, *this); return subFiles_.back(); @@ -1065,11 +1065,11 @@ FilePair& ContainerObject::addSubFile<SelectSide::right>(const Zstring& itemName inline -SymlinkPair& ContainerObject::addSubLink(const Zstring& itemNameL, - const LinkAttributes& left, //link exists on both sides - CompareSymlinkResult defaultCmpResult, - const Zstring& itemNameR, - const LinkAttributes& right) +SymlinkPair& ContainerObject::addLink(const Zstring& itemNameL, + const LinkAttributes& left, //link exists on both sides + CompareSymlinkResult defaultCmpResult, + const Zstring& itemNameR, + const LinkAttributes& right) { subLinks_.emplace_back(itemNameL, left, defaultCmpResult, itemNameR, right, *this); return subLinks_.back(); @@ -1077,7 +1077,7 @@ SymlinkPair& ContainerObject::addSubLink(const Zstring& itemNameL, template <> inline -SymlinkPair& ContainerObject::addSubLink<SelectSide::left>(const Zstring& itemName, const LinkAttributes& attr) +SymlinkPair& ContainerObject::addLink<SelectSide::left>(const Zstring& itemName, const LinkAttributes& attr) { subLinks_.emplace_back(itemName, attr, SYMLINK_LEFT_SIDE_ONLY, Zstring(), LinkAttributes(), *this); return subLinks_.back(); @@ -1085,7 +1085,7 @@ SymlinkPair& ContainerObject::addSubLink<SelectSide::left>(const Zstring& itemNa template <> inline -SymlinkPair& ContainerObject::addSubLink<SelectSide::right>(const Zstring& itemName, const LinkAttributes& attr) +SymlinkPair& ContainerObject::addLink<SelectSide::right>(const Zstring& itemName, const LinkAttributes& attr) { subLinks_.emplace_back(Zstring(), LinkAttributes(), SYMLINK_RIGHT_SIDE_ONLY, itemName, attr, *this); return subLinks_.back(); diff --git a/FreeFileSync/Source/base/lock_holder.h b/FreeFileSync/Source/base/lock_holder.h index 0b02b1c3..e7d28c6a 100644 --- a/FreeFileSync/Source/base/lock_holder.h +++ b/FreeFileSync/Source/base/lock_holder.h @@ -10,7 +10,7 @@ namespace fff { //intermediate locks created by DirLock use this extension, too: -const Zchar LOCK_FILE_ENDING[] = Zstr(".ffs_lock"); //don't use Zstring as global constant: avoid static initialization order problem in global namespace! +const ZstringView LOCK_FILE_ENDING = Zstr(".ffs_lock"); //don't use Zstring as global constant: avoid static initialization order problem in global namespace! //Attention: 1. call after having checked directory existence! // 2. perf: remove folder aliases (e.g. case differences) *before* calling this function!!! diff --git a/FreeFileSync/Source/base/parallel_scan.cpp b/FreeFileSync/Source/base/parallel_scan.cpp index 389c67bb..626a9371 100644 --- a/FreeFileSync/Source/base/parallel_scan.cpp +++ b/FreeFileSync/Source/base/parallel_scan.cpp @@ -195,7 +195,7 @@ private: std::wstring currentFile_; std::map<int /*threadIdx*/, size_t /*parallelOps*/> activeThreadIdxs_; - std::atomic<int> notifyingThreadIdx_ {0}; //CAVEAT: do NOT use boost::thread::id: https://svn.boost.org/trac/boost/ticket/5754 + std::atomic<int> notifyingThreadIdx_{0}; //CAVEAT: do NOT use boost::thread::id: https://svn.boost.org/trac/boost/ticket/5754 const std::chrono::milliseconds cbInterval_; //---- status updates II (lock-free) ---- @@ -263,7 +263,7 @@ public: output.failedItemReads, acb, threadIdx, - lastReportTime + lastReportTime, } { if (acb.mayReportCurrentFile(threadIdx, lastReportTime)) @@ -291,7 +291,7 @@ void DirCallback::onFile(const AFS::FileInfo& fi) //throw ThreadStopRequest return; //note: sync.ffs_db database and lock files are excluded via path filter! - output_.addSubFile(fi.itemName, FileAttributes(fi.modTime, fi.fileSize, fi.filePrint, fi.isFollowedSymlink)); + output_.addFile(fi.itemName, FileAttributes(fi.modTime, fi.fileSize, fi.filePrint, fi.isFollowedSymlink)); cfg_.acb.incItemsScanned(); //add 1 element to the progress indicator } @@ -315,13 +315,13 @@ std::shared_ptr<AFS::TraverserCallback> DirCallback::onFolder(const AFS::FolderI return nullptr; //do NOT traverse subdirs //else: ensure directory filtering is applied later to exclude actually filtered directories!!! - FolderContainer& subFolder = output_.addSubFolder(fi.itemName, FolderAttributes(fi.isFollowedSymlink)); + FolderContainer& subFolder = output_.addFolder(fi.itemName, FolderAttributes(fi.isFollowedSymlink)); if (passFilter) cfg_.acb.incItemsScanned(); //add 1 element to the progress indicator //------------------------------------------------------------------------------------ if (level_ > FOLDER_TRAVERSAL_LEVEL_MAX) //Win32 traverser: stack overflow approximately at level 1000 - //check after FolderContainer::addSubFolder() + //check after FolderContainer::addFolder() for (size_t retryNumber = 0;; ++retryNumber) switch (reportItemError({replaceCpy(_("Cannot read directory %x."), L"%x", AFS::getDisplayPath(AFS::appendRelPath(cfg_.baseFolderPath, relPath))) + L"\n\n" L"Endless recursion.", std::chrono::steady_clock::now(), retryNumber}, fi.itemName)) //throw ThreadStopRequest @@ -354,7 +354,7 @@ DirCallback::HandleLink DirCallback::onSymlink(const AFS::SymlinkInfo& si) //thr case SymLinkHandling::asLink: if (cfg_.filter.ref().passFileFilter(relPath)) //always use file filter: Link type may not be "stable" on Linux! { - output_.addSubLink(si.itemName, LinkAttributes(si.modTime)); + output_.addLink(si.itemName, LinkAttributes(si.modTime)); cfg_.acb.incItemsScanned(); //add 1 element to the progress indicator } return HandleLink::skip; diff --git a/FreeFileSync/Source/base/structures.cpp b/FreeFileSync/Source/base/structures.cpp index fa6bb31e..b2e70d5e 100644 --- a/FreeFileSync/Source/base/structures.cpp +++ b/FreeFileSync/Source/base/structures.cpp @@ -75,7 +75,7 @@ DirectionSet fff::extractDirections(const SyncDirectionConfig& cfg) switch (cfg.var) { case SyncVariant::twoWay: - throw std::logic_error("there are no predefined directions for automatic mode! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] there are no predefined directions for automatic mode!"); case SyncVariant::mirror: output.exLeftSideOnly = SyncDirection::right; @@ -206,7 +206,7 @@ std::wstring fff::getSymbol(SyncOperation op) case SO_COPY_METADATA_TO_RIGHT: return L"update ->"; case SO_DO_NOTHING: return L" -"; case SO_EQUAL: return L"'=="; //added quotation mark to avoid error in Excel cell when exporting to *.cvs - case SO_UNRESOLVED_CONFLICT: return L"conflict"; + case SO_UNRESOLVED_CONFLICT: return L"conflict"; //portable Unicode symbol: âš¡ //*INDENT-ON* }; assert(false); diff --git a/FreeFileSync/Source/base/synchronization.cpp b/FreeFileSync/Source/base/synchronization.cpp index 982fcf15..105ac8bc 100644 --- a/FreeFileSync/Source/base/synchronization.cpp +++ b/FreeFileSync/Source/base/synchronization.cpp @@ -12,7 +12,6 @@ #include <zen/crc.h> #include "algorithm.h" #include "db_file.h" -//#include "dir_exist_async.h" #include "status_handler_impl.h" #include "versioning.h" #include "binary.h" @@ -447,12 +446,6 @@ bool plannedWriteAccess(const FileSystemObject& fsObj) } -bool plannedReadOrWrite(const FileSystemObject& fsObj) -{ - return !!getTargetDirection(fsObj.getSyncOperation()); -} - - inline AbstractPath getAbstractPath(const FileSystemObject& fsObj, SelectSide side) { @@ -485,17 +478,17 @@ std::weak_ordering comparePathNoCase(const PathRaceItem& lhs, const PathRaceItem std::wstring formatRaceItem(const PathRaceItem& item) { - const wchar_t arrowLeft [] = L" <- "; - const wchar_t arrowRight[] = L" -> "; - - const SelectSide dir = *getTargetDirection(item.fsObj->getSyncOperation()); //non-null! see plannedReadOrWrite() - - return AFS::getDisplayPath(item.fsObj->base().getAbstractPath<SelectSide::left>()) + - (dir == SelectSide::left ? arrowLeft : arrowRight) + - AFS::getDisplayPath(item.fsObj->base().getAbstractPath<SelectSide::right>()) + - (dir == item.side ? L" 💾 " : L" 👓 ") + - utfTo<std::wstring>(item.side == SelectSide::left ? item.fsObj->getRelativePath<SelectSide::left>() : item.fsObj->getRelativePath<SelectSide::right>()); - //e.g. C:\Source -> D:\Target 💾 subfolder\file.txt + const std::optional<SelectSide> syncDir = getTargetDirection(item.fsObj->getSyncOperation()); + + return AFS::getDisplayPath(item.side == SelectSide::left ? + item.fsObj->base().getAbstractPath<SelectSide::left>() : + item.fsObj->base().getAbstractPath<SelectSide::right>()) + + (syncDir && *syncDir == item.side ? L" 💾 " : L" 👓 ") + + utfTo<std::wstring>(item.side == SelectSide::left ? + item.fsObj->getRelativePath<SelectSide::left>() : + item.fsObj->getRelativePath<SelectSide::right>()); + //e.g. D:\folder 💾 subfolder\file.txt + // D:\folder\subfolder 👓 file.txt } @@ -530,20 +523,17 @@ private: void recurse(const ContainerObject& hierObj, uint64_t parentPathHash) { for (const FilePair& file : hierObj.refSubFiles()) - if (plannedReadOrWrite(file)) - childPathRefs_.push_back({&file, getPathHash(file, parentPathHash)}); - //S1 -> T (update) is not a conflict (anymore) if S1, S2 contain different files https://freefilesync.org/forum/viewtopic.php?t=9365#p36466 - //S2 -> T (update) => ignore all items that are not physically read/written (e.g. categories "equal", "do nothing") + childPathRefs_.push_back({&file, getPathHash(file, parentPathHash)}); + //S1 -> T (update) is not a conflict (anymore) if S1, S2 contain different files + //S2 -> T (update) https://freefilesync.org/forum/viewtopic.php?t=9365#p36466 for (const SymlinkPair& symlink : hierObj.refSubLinks()) - if (plannedReadOrWrite(symlink)) - childPathRefs_.push_back({&symlink, getPathHash(symlink, parentPathHash)}); + childPathRefs_.push_back({&symlink, getPathHash(symlink, parentPathHash)}); for (const FolderPair& subFolder : hierObj.refSubFolders()) { const uint64_t folderPathHash = getPathHash(subFolder, parentPathHash); - if (plannedReadOrWrite(subFolder)) - childPathRefs_.push_back({&subFolder, folderPathHash}); + childPathRefs_.push_back({&subFolder, folderPathHash}); recurse(subFolder, folderPathHash); } @@ -2471,7 +2461,7 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime //PERF_START; if (syncConfig.size() != folderCmp.size()) - throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); //aggregate basic information std::vector<SyncStatistics> folderPairStats; diff --git a/FreeFileSync/Source/base/versioning.cpp b/FreeFileSync/Source/base/versioning.cpp index 63af2cbb..3efe0788 100644 --- a/FreeFileSync/Source/base/versioning.cpp +++ b/FreeFileSync/Source/base/versioning.cpp @@ -458,9 +458,8 @@ void fff::applyVersioningLimit(const std::set<VersioningLimitFolder>& folderLimi }; const std::map<DirectoryKey, DirectoryValue> folderBuf = parallelDeviceTraversal(foldersToRead, - [&](const PhaseCallback::ErrorInfo& errorInfo) { return callback.reportError(errorInfo); }, //throw X - onStatusUpdate, //throw X - UI_UPDATE_INTERVAL / 2); //every ~50 ms + [&](const PhaseCallback::ErrorInfo& errorInfo) { return callback.reportError(errorInfo); } /*throw X*/, + onStatusUpdate /*throw X*/, UI_UPDATE_INTERVAL / 2); //every ~50 ms //--------- group versions per (original) relative path --------- std::map<AbstractPath, VersionInfoMap> versionDetails; //versioningFolderPath => <version details> diff --git a/FreeFileSync/Source/base/versioning.h b/FreeFileSync/Source/base/versioning.h index 10dec67d..49c864e9 100644 --- a/FreeFileSync/Source/base/versioning.h +++ b/FreeFileSync/Source/base/versioning.h @@ -42,7 +42,7 @@ public: using namespace zen; if (AbstractFileSystem::isNullPath(versioningFolderPath_)) - throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); if (timeStamp_.size() != 17) //formatTime() returns empty string on error; unexpected length: e.g. problem in year 10,000! throw FileError(_("Unable to create time stamp for versioning:") + L" \"" + utfTo<std::wstring>(timeStamp_) + L'"'); diff --git a/FreeFileSync/Source/config.cpp b/FreeFileSync/Source/config.cpp index 02b21343..98ebfe4a 100644 --- a/FreeFileSync/Source/config.cpp +++ b/FreeFileSync/Source/config.cpp @@ -23,7 +23,7 @@ using namespace fff; //required for correct overload resolution! namespace { //------------------------------------------------------------------------------------------------------------------------------- -const int XML_FORMAT_GLOBAL_CFG = 25; //2022-08-26 +const int XML_FORMAT_GLOBAL_CFG = 26; //2023-02-18 const int XML_FORMAT_SYNC_CFG = 17; //2020-10-14 //------------------------------------------------------------------------------------------------------------------------------- } @@ -1474,8 +1474,9 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& cfg, int formatVer) //TODO: remove old parameter after migration! 2021-03-06 if (formatVer < 21) { - in2["ProgressDialog"].attribute("Width", cfg.dpiLayouts[getDpiScalePercent()].progressDlg.dlgSize.x); - in2["ProgressDialog"].attribute("Height", cfg.dpiLayouts[getDpiScalePercent()].progressDlg.dlgSize.y); + cfg.dpiLayouts[getDpiScalePercent()].progressDlg.size = wxSize(); + in2["ProgressDialog"].attribute("Width", cfg.dpiLayouts[getDpiScalePercent()].progressDlg.size->x); + in2["ProgressDialog"].attribute("Height", cfg.dpiLayouts[getDpiScalePercent()].progressDlg.size->y); in2["ProgressDialog"].attribute("Maximized", cfg.dpiLayouts[getDpiScalePercent()].progressDlg.isMaximized); } @@ -1565,10 +1566,12 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& cfg, int formatVer) //TODO: remove old parameter after migration! 2021-03-06 if (formatVer < 21) { - inMainWin.attribute("Width", cfg.dpiLayouts[getDpiScalePercent()].mainDlg.dlgSize.x); - inMainWin.attribute("Height", cfg.dpiLayouts[getDpiScalePercent()].mainDlg.dlgSize.y); - inMainWin.attribute("PosX", cfg.dpiLayouts[getDpiScalePercent()].mainDlg.dlgPos.x); - inMainWin.attribute("PosY", cfg.dpiLayouts[getDpiScalePercent()].mainDlg.dlgPos.y); + cfg.dpiLayouts[getDpiScalePercent()].mainDlg.size = wxSize(); + inMainWin.attribute("Width", cfg.dpiLayouts[getDpiScalePercent()].mainDlg.size->x); + inMainWin.attribute("Height", cfg.dpiLayouts[getDpiScalePercent()].mainDlg.size->y); + cfg.dpiLayouts[getDpiScalePercent()].mainDlg.pos = wxPoint(); + inMainWin.attribute("PosX", cfg.dpiLayouts[getDpiScalePercent()].mainDlg.pos->x); + inMainWin.attribute("PosY", cfg.dpiLayouts[getDpiScalePercent()].mainDlg.pos->y); inMainWin.attribute("Maximized", cfg.dpiLayouts[getDpiScalePercent()].mainDlg.isMaximized); } @@ -1777,10 +1780,10 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& cfg, int formatVer) //TODO: remove old parameter after migration! 2018-01-16 if (formatVer < 7) - inMainWin["Perspective5"](cfg.dpiLayouts[getDpiScalePercent()].mainDlg.panelLayout); + inMainWin["Perspective5"](cfg.dpiLayouts[getDpiScalePercent()].panelLayout); //TODO: remove old parameter after migration! 2021-03-06 else if (formatVer < 21) - inMainWin["Perspective"](cfg.dpiLayouts[getDpiScalePercent()].mainDlg.panelLayout); + inMainWin["Perspective"](cfg.dpiLayouts[getDpiScalePercent()].panelLayout); //TODO: remove after migration! 2019-11-30 auto splitEditMerge = [](wxString& perspective, wchar_t delim, const std::function<void(wxString& item)>& editItem) @@ -1801,7 +1804,7 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& cfg, int formatVer) //TODO: remove after migration! 2018-07-27 if (formatVer < 10) - splitEditMerge(cfg.dpiLayouts[getDpiScalePercent()].mainDlg.panelLayout, L'|', [&](wxString& paneCfg) + splitEditMerge(cfg.dpiLayouts[getDpiScalePercent()].panelLayout, L'|', [&](wxString& paneCfg) { if (contains(paneCfg, L"name=TopPanel")) replace(paneCfg, L";row=2;", L";row=3;"); @@ -1814,7 +1817,7 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& cfg, int formatVer) std::optional<int> tpDir; std::optional<int> tpLayer; std::optional<int> tpRow; - splitEditMerge(cfg.dpiLayouts[getDpiScalePercent()].mainDlg.panelLayout, L'|', [&](wxString& paneCfg) + splitEditMerge(cfg.dpiLayouts[getDpiScalePercent()].panelLayout, L'|', [&](wxString& paneCfg) { if (contains(paneCfg, L"name=TopPanel")) splitEditMerge(paneCfg, L';', [&](wxString& paneAttr) @@ -1835,7 +1838,7 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& cfg, int formatVer) numberTo<wxString>(*tpLayer) + L"," + numberTo<wxString>(*tpRow ) + L")="; - splitEditMerge(cfg.dpiLayouts[getDpiScalePercent()].mainDlg.panelLayout, L'|', [&](wxString& paneCfg) + splitEditMerge(cfg.dpiLayouts[getDpiScalePercent()].panelLayout, L'|', [&](wxString& paneCfg) { if (startsWith(paneCfg, tpSize)) paneCfg = tpSize + L"0"; @@ -1970,7 +1973,7 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& cfg, int formatVer) in["LastOnlineVersion"](cfg.lastOnlineVersion); } - in["WelcomeShownVersion"](cfg.welcomeShownVersion); + in["WelcomeDialogVersion"](cfg.welcomeDialogLastVersion); //cfg.dpiLayouts.clear(); -> NO: honor migration code above! @@ -1981,23 +1984,68 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& cfg, int formatVer) const int scalePercent = stringTo<int>(beforeLast(scaleTxt, '%', IfNotFoundReturn::none)); DpiLayout layout; - XmlIn inLayoutMain = inLayout["MainDialog"]; - inLayoutMain.attribute("Width", layout.mainDlg.dlgSize.x); - inLayoutMain.attribute("Height", layout.mainDlg.dlgSize.y); - inLayoutMain.attribute("PosX", layout.mainDlg.dlgPos.x); - inLayoutMain.attribute("PosY", layout.mainDlg.dlgPos.y); - inLayoutMain.attribute("Maximized", layout.mainDlg.isMaximized); + //TODO: remove parameter migration after some time! 2023-02-18 + if (formatVer < 26) + { + XmlIn inLayoutMain = inLayout["MainDialog"]; + layout.mainDlg.size = wxSize(); + inLayoutMain.attribute("Width", layout.mainDlg.size->x); + inLayoutMain.attribute("Height", layout.mainDlg.size->y); + + layout.mainDlg.pos = wxPoint(); + inLayoutMain.attribute("PosX", layout.mainDlg.pos->x); + inLayoutMain.attribute("PosY", layout.mainDlg.pos->y); + + inLayoutMain.attribute("Maximized", layout.mainDlg.isMaximized); + + inLayoutMain["PanelLayout" ](layout.panelLayout); + inLayoutMain["ConfigPanel" ](layout.configColumnAttribs); + inLayoutMain["OverviewPanel" ](layout.overviewColumnAttribs); + inLayoutMain["FilePanelLeft" ](layout.fileColumnAttribsLeft); + inLayoutMain["FilePanelRight"](layout.fileColumnAttribsRight); - inLayoutMain["PanelLayout" ](layout.mainDlg.panelLayout); - inLayoutMain["ConfigPanel" ](layout.configColumnAttribs); - inLayoutMain["OverviewPanel" ](layout.overviewColumnAttribs); - inLayoutMain["FilePanelLeft" ](layout.fileColumnAttribsLeft); - inLayoutMain["FilePanelRight"](layout.fileColumnAttribsRight); + XmlIn inLayoutProgress = inLayout["ProgressDialog"]; + layout.progressDlg.size = wxSize(); + inLayoutProgress.attribute("Width", layout.progressDlg.size->x); + inLayoutProgress.attribute("Height", layout.progressDlg.size->y); - XmlIn inLayoutProgress = inLayout["ProgressDialog"]; - inLayoutProgress.attribute("Width", layout.progressDlg.dlgSize.x); - inLayoutProgress.attribute("Height", layout.progressDlg.dlgSize.y); - inLayoutProgress.attribute("Maximized", layout.progressDlg.isMaximized); + inLayoutProgress.attribute("Maximized", layout.progressDlg.isMaximized); + } + else + { + XmlIn inLayoutMain = inLayout["MainWindow"]; + if (inLayoutMain.hasAttribute("Width") && + inLayoutMain.hasAttribute("Height")) + { + layout.mainDlg.size = wxSize(); + inLayoutMain.attribute("Width", layout.mainDlg.size->x); + inLayoutMain.attribute("Height", layout.mainDlg.size->y); + } + if (inLayoutMain.hasAttribute("PosX") && + inLayoutMain.hasAttribute("PosY")) + { + layout.mainDlg.pos = wxPoint(); + inLayoutMain.attribute("PosX", layout.mainDlg.pos->x); + inLayoutMain.attribute("PosY", layout.mainDlg.pos->y); + } + inLayoutMain.attribute("Maximized", layout.mainDlg.isMaximized); + + XmlIn inLayoutProgress = inLayout["ProgressDialog"]; + if (inLayoutProgress.hasAttribute("Width") && + inLayoutProgress.hasAttribute("Height")) + { + layout.progressDlg.size = wxSize(); + inLayoutProgress.attribute("Width", layout.progressDlg.size->x); + inLayoutProgress.attribute("Height", layout.progressDlg.size->y); + } + inLayoutProgress.attribute("Maximized", layout.progressDlg.isMaximized); + + inLayout["Panels" ](layout.panelLayout); + inLayout["ConfigPanel" ](layout.configColumnAttribs); + inLayout["OverviewPanel" ](layout.overviewColumnAttribs); + inLayout["FilePanelLeft" ](layout.fileColumnAttribsLeft); + inLayout["FilePanelRight"](layout.fileColumnAttribsRight); + } cfg.dpiLayouts.emplace(scalePercent, std::move(layout)); } @@ -2435,7 +2483,7 @@ void writeConfig(const XmlGlobalSettings& cfg, XmlOut& out) out["LastOnlineCheck" ](cfg.lastUpdateCheck); out["LastOnlineVersion"](cfg.lastOnlineVersion); - out["WelcomeShownVersion"](cfg.welcomeShownVersion); + out["WelcomeDialogVersion"](cfg.welcomeDialogLastVersion); for (const auto& [scalePercent, layout] : cfg.dpiLayouts) @@ -2443,23 +2491,32 @@ void writeConfig(const XmlGlobalSettings& cfg, XmlOut& out) XmlOut outLayout = out["DpiLayouts"].addChild("Layout"); outLayout.attribute("Scale", numberTo<std::string>(scalePercent) + '%'); - XmlOut outLayoutMain = outLayout["MainDialog"]; - outLayoutMain.attribute("Width", layout.mainDlg.dlgSize.x); - outLayoutMain.attribute("Height", layout.mainDlg.dlgSize.y); - outLayoutMain.attribute("PosX", layout.mainDlg.dlgPos.x); - outLayoutMain.attribute("PosY", layout.mainDlg.dlgPos.y); + XmlOut outLayoutMain = outLayout["MainWindow"]; + if (layout.mainDlg.size) + { + outLayoutMain.attribute("Width", layout.mainDlg.size->x); + outLayoutMain.attribute("Height", layout.mainDlg.size->y); + } + if (layout.mainDlg.pos) + { + outLayoutMain.attribute("PosX", layout.mainDlg.pos->x); + outLayoutMain.attribute("PosY", layout.mainDlg.pos->y); + } outLayoutMain.attribute("Maximized", layout.mainDlg.isMaximized); - outLayoutMain["PanelLayout" ](layout.mainDlg.panelLayout); - outLayoutMain["ConfigPanel" ](layout.configColumnAttribs); - outLayoutMain["OverviewPanel" ](layout.overviewColumnAttribs); - outLayoutMain["FilePanelLeft" ](layout.fileColumnAttribsLeft); - outLayoutMain["FilePanelRight"](layout.fileColumnAttribsRight); - XmlOut outLayoutProgress = outLayout["ProgressDialog"]; - outLayoutProgress.attribute("Width", layout.progressDlg.dlgSize.x); - outLayoutProgress.attribute("Height", layout.progressDlg.dlgSize.y); + if (layout.progressDlg.size) + { + outLayoutProgress.attribute("Width", layout.progressDlg.size->x); + outLayoutProgress.attribute("Height", layout.progressDlg.size->y); + } outLayoutProgress.attribute("Maximized", layout.progressDlg.isMaximized); + + outLayout["Panels" ](layout.panelLayout); + outLayout["ConfigPanel" ](layout.configColumnAttribs); + outLayout["OverviewPanel" ](layout.overviewColumnAttribs); + outLayout["FilePanelLeft" ](layout.fileColumnAttribsLeft); + outLayout["FilePanelRight"](layout.fileColumnAttribsRight); } } diff --git a/FreeFileSync/Source/config.h b/FreeFileSync/Source/config.h index faa14fbb..e19b4fa1 100644 --- a/FreeFileSync/Source/config.h +++ b/FreeFileSync/Source/config.h @@ -118,22 +118,23 @@ struct DpiLayout { struct { - wxPoint dlgPos; - wxSize dlgSize; + std::optional<wxSize> size; + std::optional<wxPoint> pos; bool isMaximized = false; - wxString panelLayout; //for wxAuiManager::LoadPerspective - } mainDlg; - - std::vector<ColAttributesCfg> configColumnAttribs = getCfgGridDefaultColAttribs(); - std::vector<ColumnAttribOverview> overviewColumnAttribs = getOverviewDefaultColAttribs(); - std::vector<ColAttributesRim> fileColumnAttribsLeft = getFileGridDefaultColAttribsLeft(); - std::vector<ColAttributesRim> fileColumnAttribsRight = getFileGridDefaultColAttribsRight(); + } mainDlg; //WindowLayout::getBeforeClose() struct { - wxSize dlgSize; + std::optional<wxSize> size; bool isMaximized = false; } progressDlg; + + wxString panelLayout; //for wxAuiManager::LoadPerspective + + std::vector<ColAttributesCfg> configColumnAttribs = getCfgGridDefaultColAttribs(); + std::vector<ColumnAttribOverview> overviewColumnAttribs = getOverviewDefaultColAttribs(); + std::vector<ColAttributesRim> fileColumnAttribsLeft = getFileGridDefaultColAttribsLeft(); + std::vector<ColAttributesRim> fileColumnAttribsRight = getFileGridDefaultColAttribsRight(); }; @@ -246,7 +247,7 @@ struct XmlGlobalSettings time_t lastUpdateCheck = 0; //number of seconds since Jan 1, 1970 GMT std::string lastOnlineVersion; - std::string welcomeShownVersion; //last FFS version for which the welcome dialog was shown + std::string welcomeDialogLastVersion; std::unordered_map<int /*scale percent*/, DpiLayout> dpiLayouts; }; diff --git a/FreeFileSync/Source/localization.cpp b/FreeFileSync/Source/localization.cpp index 7c8bdd6b..d1de6201 100644 --- a/FreeFileSync/Source/localization.cpp +++ b/FreeFileSync/Source/localization.cpp @@ -248,11 +248,11 @@ public: //https://www.gnu.org/software/gettext/manual/html_node/MO-Files.html transMapping[""] = L"Content-Type: text/plain; charset=UTF-8\n"; - const int headerSize = 28; + const int headerSize = 7 * sizeof(uint32_t); writeNumber<uint32_t>(moBuf_, 0x950412de); //magic number writeNumber<uint32_t>(moBuf_, 0); //format version writeNumber<uint32_t>(moBuf_, transMapping.size()); //string count - writeNumber<uint32_t>(moBuf_, headerSize); //string references offset: original + writeNumber<uint32_t>(moBuf_, headerSize); //string references offset: original writeNumber<uint32_t>(moBuf_, headerSize + (2 * sizeof(uint32_t)) * transMapping.size()); //string references offset: translation writeNumber<uint32_t>(moBuf_, 0); //size of hashing table writeNumber<uint32_t>(moBuf_, 0); //offset of hashing table diff --git a/FreeFileSync/Source/log_file.cpp b/FreeFileSync/Source/log_file.cpp index 5f268de0..dfa28ace 100644 --- a/FreeFileSync/Source/log_file.cpp +++ b/FreeFileSync/Source/log_file.cpp @@ -8,7 +8,7 @@ #include <zen/file_io.h> #include <zen/http.h> #include <zen/sys_info.h> -#include <wx/datetime.h> +//#include <wx/datetime.h> #include "afs/concrete.h" using namespace zen; @@ -66,7 +66,7 @@ std::string generateLogHeaderTxt(const ProcessSummary& s, const ErrorLog& log, i L" (" + formatFilesizeShort(s.statsTotal.bytes - s.statsProcessed.bytes) + L')')); const int64_t totalTimeSec = std::chrono::duration_cast<std::chrono::seconds>(s.totalTime).count(); - summary.push_back(tabSpace + utfTo<std::string>(_("Total time:")) + ' ' + utfTo<std::string>(wxTimeSpan::Seconds(totalTimeSec).Format())); + summary.push_back(tabSpace + utfTo<std::string>(_("Total time:")) + ' ' + utfTo<std::string>(formatTimeSpan(totalTimeSec))); size_t sepLineLen = 0; //calculate max width (considering Unicode!) for (const std::string& str : summary) sepLineLen = std::max(sepLineLen, unicodeLength(str)); @@ -293,7 +293,7 @@ std::string generateLogHeaderHtml(const ProcessSummary& s, const ErrorLog& log, <tr> <td>)" + htmlTxt(_("Total time:")) + R"(</td> <td><img src="https://freefilesync.org/images/log/clock.png" width="24" height="24" alt=""></td> - <td><span style="font-weight: 600;">)" + htmlTxt(wxTimeSpan::Seconds(totalTimeSec).Format()) + R"(</span></td> + <td><span style="font-weight: 600;">)" + htmlTxt(formatTimeSpan(totalTimeSec)) + R"(</span></td> </tr> </table> </div> @@ -350,7 +350,7 @@ std::string generateLogFooterHtml(const std::wstring& logFilePath /*optional*/, output += R"( <div style="border-bottom:1px solid #AAA; margin:5px 0;"></div> - <div style="font-size:small;"> + <div style="font-size:smaller;"> <img src="https://freefilesync.org/images/log/)" + osImage + R"(" width="24" height="24" alt="" style="vertical-align:middle;"> <span style="vertical-align:middle;">)" + htmlTxt(getOsDescription()) + /*throw FileError*/ + " – " + htmlTxt(getUserDescription()) /*throw FileError*/ + @@ -360,7 +360,7 @@ std::string generateLogFooterHtml(const std::wstring& logFilePath /*optional*/, if (!logFilePath.empty()) output += R"( - <div style="font-size:small;"> + <div style="font-size:smaller;"> <img src="https://freefilesync.org/images/log/log.png" width="24" height="24" alt=")" + htmlTxt(_("Log file:")) + R"(" style="vertical-align:middle;"> <span style="font-family: Consolas,'Courier New',Courier,monospace; vertical-align:middle;">)" + htmlTxt(logFilePath) + R"(</span> </div>)"; diff --git a/FreeFileSync/Source/parse_lng.h b/FreeFileSync/Source/parse_lng.h index 7af4766d..2c1a7adb 100644 --- a/FreeFileSync/Source/parse_lng.h +++ b/FreeFileSync/Source/parse_lng.h @@ -21,7 +21,7 @@ using TranslationMap = std::unordered_map<std::string, std::string>; //orig |-> //plural forms using SingularPluralPair = std::pair<std::string, std::string>; //1 house | %x houses -using PluralForms = std::vector<std::string>; //1 dom | 2 domy | %x domów +using PluralForms = std::vector<std::string>; //1 dom | 2 domy | %x domów using TranslationPluralMap = std::unordered_map<SingularPluralPair, PluralForms>; //(sing/plu) |-> pluralforms struct TransHeader @@ -216,7 +216,7 @@ public: private: const TokenMap tokens_ = { - //header information + //header details {TokenType::headerBegin, "<header>"}, {TokenType::headerEnd, "</header>"}, {TokenType::langNameBegin, "<language>"}, @@ -493,15 +493,15 @@ private: if (endsWithSingleAmp(original) || endsWithSingleAmp(translation)) throw ParsingError({L"The & character to mark a menu item access key must not occur at the end of a string", scn_.posRow(), scn_.posCol()}); - //if source ends with colon, so must translation (note: character seems to be universally used, even for asian and arabic languages) - if (endsWith(original, ':') && !endsWithColon(translation)) + //if source ends with colon, so must translation + if (endsWithColon(original) && !endsWithColon(translation)) throw ParsingError({L"Source text ends with a colon character \":\", but translation does not", scn_.posRow(), scn_.posCol()}); - //if source ends with a period, so must translation (note: character seems to be universally used, even for asian and arabic languages) + //if source ends with period, so must translation if (endsWithSingleDot(original) && !endsWithSingleDot(translation)) throw ParsingError({L"Source text ends with a punctuation mark character \".\", but translation does not", scn_.posRow(), scn_.posCol()}); - //if source ends with an ellipsis, so must translation (note: character seems to be universally used, even for asian and arabic languages) + //if source ends with ellipsis, so must translation if (endsWithEllipsis(original) && !endsWithEllipsis(translation)) throw ParsingError({L"Source text ends with an ellipsis \"...\", but translation does not", scn_.posRow(), scn_.posCol()}); @@ -511,7 +511,7 @@ private: throw ParsingError({replaceCpy<std::wstring>(L"Misspelled \"%x\" in translation", L"%x", utfTo<std::wstring>(fixedStr)), scn_.posRow(), scn_.posCol()}); //some languages (French!) put a space before punctuation mark => must be a no-brake space! - for (const char punctChar : std::string(".!?:;$#")) + for (const char punctChar : std::string_view(".!?:;$#")) if (contains(original, std::string(" ") + punctChar) || contains(translation, std::string(" ") + punctChar)) throw ParsingError({replaceCpy<std::wstring>(L"Text contains a space before the \"%x\" character. Are line-breaks really allowed here?" @@ -644,7 +644,6 @@ private: } } - //helper static size_t ampersandTokenCount(const std::string& str) { using namespace zen; @@ -749,34 +748,14 @@ std::string generateLng(const TranslationUnorderedList& in, const TransHeader& h //header out += tokens.text(TokenType::headerBegin) + '\n'; - out += '\t' + tokens.text(TokenType::langNameBegin); - out += header.languageName; - out += tokens.text(TokenType::langNameEnd) + '\n'; - - out += '\t' + tokens.text(TokenType::transNameBegin); - out += header.translatorName; - out += tokens.text(TokenType::transNameEnd) + '\n'; - - out += '\t' + tokens.text(TokenType::localeBegin); - out += header.locale; - out += tokens.text(TokenType::localeEnd) + '\n'; - - out += '\t' + tokens.text(TokenType::flagFileBegin); - out += header.flagFile; - out += tokens.text(TokenType::flagFileEnd) + '\n'; - - out += '\t' + tokens.text(TokenType::pluralCountBegin); - out += zen::numberTo<std::string>(header.pluralCount); - out += tokens.text(TokenType::pluralCountEnd) + '\n'; - - out += '\t' + tokens.text(TokenType::pluralDefBegin); - out += header.pluralDefinition; - out += tokens.text(TokenType::pluralDefEnd) + '\n'; - - out += tokens.text(TokenType::headerEnd) + '\n'; - - out += '\n'; + out += '\t' + tokens.text(TokenType::langNameBegin) + header.languageName + tokens.text(TokenType::langNameEnd) + '\n'; + out += '\t' + tokens.text(TokenType::transNameBegin) + header.translatorName + tokens.text(TokenType::transNameEnd) + '\n'; + out += '\t' + tokens.text(TokenType::localeBegin) + header.locale + tokens.text(TokenType::localeEnd) + '\n'; + out += '\t' + tokens.text(TokenType::flagFileBegin) + header.flagFile + tokens.text(TokenType::flagFileEnd) + '\n'; + out += '\t' + tokens.text(TokenType::pluralCountBegin) + zen::numberTo<std::string>(header.pluralCount) + tokens.text(TokenType::pluralCountEnd) + '\n'; + out += '\t' + tokens.text(TokenType::pluralDefBegin) + header.pluralDefinition + tokens.text(TokenType::pluralDefEnd) + '\n'; + out += tokens.text(TokenType::headerEnd) + "\n\n"; in.visitItems([&](const TranslationMap::value_type& trans) { @@ -786,13 +765,8 @@ std::string generateLng(const TranslationUnorderedList& in, const TransHeader& h formatMultiLineText(original); formatMultiLineText(translation); - out += tokens.text(TokenType::srcBegin); - out += original; - out += tokens.text(TokenType::srcEnd) + '\n'; - - out += tokens.text(TokenType::trgBegin); - out += translation; - out += tokens.text(TokenType::trgEnd) + '\n' + '\n'; + out += tokens.text(TokenType::srcBegin) + original + tokens.text(TokenType::srcEnd) + '\n'; + out += tokens.text(TokenType::trgBegin) + translation + tokens.text(TokenType::trgEnd) + "\n\n"; }, [&](const TranslationPluralMap::value_type& transPlural) { @@ -804,12 +778,8 @@ std::string generateLng(const TranslationUnorderedList& in, const TransHeader& h formatMultiLineText(engPlural); out += tokens.text(TokenType::srcBegin) + '\n'; - out += tokens.text(TokenType::pluralBegin); - out += engSingular; - out += tokens.text(TokenType::pluralEnd) + '\n'; - out += tokens.text(TokenType::pluralBegin); - out += engPlural; - out += tokens.text(TokenType::pluralEnd) + '\n'; + out += '\t' + tokens.text(TokenType::pluralBegin) + engSingular + tokens.text(TokenType::pluralEnd) + '\n'; + out += '\t' + tokens.text(TokenType::pluralBegin) + engPlural + tokens.text(TokenType::pluralEnd) + '\n'; out += tokens.text(TokenType::srcEnd) + '\n'; out += tokens.text(TokenType::trgBegin); @@ -818,12 +788,9 @@ std::string generateLng(const TranslationUnorderedList& in, const TransHeader& h for (std::string plForm : forms) { formatMultiLineText(plForm); - - out += tokens.text(TokenType::pluralBegin); - out += plForm; - out += tokens.text(TokenType::pluralEnd) + '\n'; + out += '\t' + tokens.text(TokenType::pluralBegin) + plForm + tokens.text(TokenType::pluralEnd) + '\n'; } - out += tokens.text(TokenType::trgEnd) + '\n' + '\n'; + out += tokens.text(TokenType::trgEnd) + "\n\n"; }); assert(!zen::contains(out, "\r\n") && !zen::contains(out, "\r")); diff --git a/FreeFileSync/Source/parse_plural.h b/FreeFileSync/Source/parse_plural.h index 5f19c6b2..f5883135 100644 --- a/FreeFileSync/Source/parse_plural.h +++ b/FreeFileSync/Source/parse_plural.h @@ -27,7 +27,7 @@ class ParsingError {}; class PluralForm { public: - PluralForm(const std::string& stream); //throw ParsingError + explicit PluralForm(const std::string& stream); //throw ParsingError size_t getForm(int64_t n) const { n_ = std::abs(n) ; return static_cast<size_t>(expr_->eval()); } private: diff --git a/FreeFileSync/Source/status_handler.cpp b/FreeFileSync/Source/status_handler.cpp index 4c1cacb3..6530ed48 100644 --- a/FreeFileSync/Source/status_handler.cpp +++ b/FreeFileSync/Source/status_handler.cpp @@ -56,14 +56,15 @@ void fff::runCommandAndLogErrors(const Zstring& cmdLine, ErrorLog& errorLog) void fff::delayAndCountDown(std::chrono::steady_clock::time_point delayUntil, const std::function<void(const std::wstring& timeRemMsg)>& notifyStatus) { - for (auto now = std::chrono::steady_clock::now(); now < delayUntil; now = std::chrono::steady_clock::now()) - { - if (notifyStatus) + assert(notifyStatus); + if (notifyStatus) + for (auto now = std::chrono::steady_clock::now(); now < delayUntil; now = std::chrono::steady_clock::now()) { const auto timeRemMs = std::chrono::duration_cast<std::chrono::milliseconds>(delayUntil - now).count(); notifyStatus(_P("1 sec", "%x sec", numeric::intDivCeil(timeRemMs, 1000))); - } - std::this_thread::sleep_for(UI_UPDATE_INTERVAL / 2); - } + std::this_thread::sleep_for(UI_UPDATE_INTERVAL / 2); + } + else + std::this_thread::sleep_until(delayUntil); } diff --git a/FreeFileSync/Source/ui/abstract_folder_picker.cpp b/FreeFileSync/Source/ui/abstract_folder_picker.cpp index 20647e72..6ebb5024 100644 --- a/FreeFileSync/Source/ui/abstract_folder_picker.cpp +++ b/FreeFileSync/Source/ui/abstract_folder_picker.cpp @@ -127,7 +127,10 @@ AbstractFolderPickerDlg::AbstractFolderPickerDlg(wxWindow* parent, AbstractPath& //---------------------------------------------------------------------- GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() - //=> works like a charm for GTK2 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(); //avoid old position flash when Center() moves window (asynchronously?) +#endif Center(); //needs to be re-applied after a dialog size change! Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onLocalKeyEvent(event); }); //dialog-specific local key events diff --git a/FreeFileSync/Source/ui/batch_config.cpp b/FreeFileSync/Source/ui/batch_config.cpp index 68d4dd88..f2cd35f5 100644 --- a/FreeFileSync/Source/ui/batch_config.cpp +++ b/FreeFileSync/Source/ui/batch_config.cpp @@ -7,7 +7,7 @@ #include "batch_config.h" #include <wx/wupdlock.h> //#include <wx+/std_button_layout.h> -#include <wx+/font_size.h> +#include <wx+/window_layout.h> #include <wx+/image_resources.h> #include <wx+/image_tools.h> #include <wx+/choice_enum.h> @@ -82,7 +82,10 @@ BatchDialog::BatchDialog(wxWindow* parent, BatchDialogConfig& dlgCfg) : Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onLocalKeyEvent(event); }); //enable dialog-specific key events GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() - //=> works like a charm for GTK2 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(); //avoid old position flash when Center() moves window (asynchronously?) +#endif Center(); //needs to be re-applied after a dialog size change! m_buttonSaveAs->SetFocus(); diff --git a/FreeFileSync/Source/ui/batch_status_handler.cpp b/FreeFileSync/Source/ui/batch_status_handler.cpp index 16ca24ea..69e92ee8 100644 --- a/FreeFileSync/Source/ui/batch_status_handler.cpp +++ b/FreeFileSync/Source/ui/batch_status_handler.cpp @@ -26,7 +26,7 @@ BatchStatusHandler::BatchStatusHandler(bool showProgress, std::chrono::seconds autoRetryDelay, const Zstring& soundFileSyncComplete, const Zstring& soundFileAlertPending, - wxSize progressDlgSize, bool dlgMaximize, + const std::optional<wxSize>& progressDlgSize, bool dlgMaximize, bool autoCloseDialog, PostSyncAction postSyncAction, BatchErrorHandling batchErrorHandling) : diff --git a/FreeFileSync/Source/ui/batch_status_handler.h b/FreeFileSync/Source/ui/batch_status_handler.h index 02fa9ef4..e0ae7e22 100644 --- a/FreeFileSync/Source/ui/batch_status_handler.h +++ b/FreeFileSync/Source/ui/batch_status_handler.h @@ -29,7 +29,7 @@ public: std::chrono::seconds autoRetryDelay, const Zstring& soundFileSyncComplete, const Zstring& soundFileAlertPending, - wxSize progressDlgSize, bool dlgMaximize, + const std::optional<wxSize>& progressDlgSize, bool dlgMaximize, bool autoCloseDialog, PostSyncAction postSyncAction, BatchErrorHandling batchErrorHandling); //noexcept!! @@ -56,7 +56,7 @@ public: zen::ErrorLogStats logStats; FinalRequest finalRequest; AbstractPath logFilePath; - wxSize dlgSize; + std::optional<wxSize> dlgSize; bool dlgIsMaximized; }; Result reportResults(const Zstring& postSyncCommand, PostSyncCondition postSyncCondition, diff --git a/FreeFileSync/Source/ui/cfg_grid.cpp b/FreeFileSync/Source/ui/cfg_grid.cpp index b50b114a..f38ec7d4 100644 --- a/FreeFileSync/Source/ui/cfg_grid.cpp +++ b/FreeFileSync/Source/ui/cfg_grid.cpp @@ -349,15 +349,14 @@ private: //else: clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); -> already the default } - enum class HoverAreaLog + enum class HoverAreaConfig { + name, link, }; void renderCell(wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover) override { - wxRect rectTmp = rect; - wxDCTextColourChanger textColor(dc); //accessibility: always set both foreground AND background colors! if (selected) textColor.Set(wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHTTEXT)); @@ -375,29 +374,32 @@ private: if (backColor.IsOk()) { - wxRect rectTmp2 = rectTmp; + wxRect rectTmp = rect; if (!selected || item->cfgItem.backColorPreview.IsOk()) { - rectTmp2.width = rectTmp.width * 2 / 3; - clearArea(dc, rectTmp2, backColor); //accessibility: always set both foreground AND background colors! + rectTmp.width = rect.width * 2 / 3; + clearArea(dc, rectTmp, backColor); //accessibility: always set both foreground AND background colors! textColor.Set(*wxBLACK); // - rectTmp2.x += rectTmp2.width; - rectTmp2.width = rectTmp.width - rectTmp2.width; - dc.GradientFillLinear(rectTmp2, backColor, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW), wxEAST); + rectTmp.x += rectTmp.width; + rectTmp.width = rect.width - rectTmp.width; + dc.GradientFillLinear(rectTmp, backColor, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW), wxEAST); } else //always show a glimpse of the background color { - rectTmp2.width = getColumnGapLeft() + getDefaultMenuIconSize(); - clearArea(dc, rectTmp2, backColor); + rectTmp.width = getColumnGapLeft() + getDefaultMenuIconSize(); + clearArea(dc, rectTmp, backColor); - rectTmp2.x += rectTmp2.width; - rectTmp2.width = getColumnGapLeft(); - dc.GradientFillLinear(rectTmp2, backColor, wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHT), wxEAST); + rectTmp.x += rectTmp.width; + rectTmp.width = getColumnGapLeft(); + dc.GradientFillLinear(rectTmp, backColor, wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHT), wxEAST); } } + if (!selected && static_cast<HoverAreaConfig>(rowHover) == HoverAreaConfig::name) + drawInsetRectangle(dc, rect, fastFromDIP(1), wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHT)); //------------------------------------------------------------------------------------- + wxRect rectTmp = rect; rectTmp.x += getColumnGapLeft(); rectTmp.width -= getColumnGapLeft(); @@ -432,7 +434,7 @@ private: if (getDaysPast(item->cfgItem.lastSyncTime) >= syncOverdueDays_) textColor2.Set(*wxRED); - drawCellText(dc, rectTmp, getValue(row, colType), wxALIGN_CENTER); + drawCellText(dc, rect, getValue(row, colType), wxALIGN_CENTER); } break; @@ -455,10 +457,10 @@ private: assert(false); return wxNullImage; }(); - drawBitmapRtlNoMirror(dc, enabled ? statusIcon : statusIcon.ConvertToDisabled(), rectTmp, wxALIGN_CENTER); + drawBitmapRtlNoMirror(dc, enabled ? statusIcon : statusIcon.ConvertToDisabled(), rect, wxALIGN_CENTER); } - if (static_cast<HoverAreaLog>(rowHover) == HoverAreaLog::link) - drawBitmapRtlNoMirror(dc, loadImage("file_link_16"), rectTmp, wxALIGN_CENTER); + if (static_cast<HoverAreaConfig>(rowHover) == HoverAreaConfig::link) + drawBitmapRtlNoMirror(dc, loadImage("file_link_16"), rect, wxALIGN_CENTER); break; } } @@ -485,6 +487,7 @@ private: HoverArea getMouseHover(wxDC& dc, size_t row, ColumnType colType, int cellRelativePosX, int cellWidth) override { if (const ConfigView::Details* item = cfgView_.getItem(row)) + { switch (static_cast<ColumnTypeCfg>(colType)) { case ColumnTypeCfg::name: @@ -493,9 +496,11 @@ private: case ColumnTypeCfg::lastLog: if (!item->isLastRunCfg && !getNativeItemPath(item->cfgItem.logFilePath).empty()) - return static_cast<HoverArea>(HoverAreaLog::link); + return static_cast<HoverArea>(HoverAreaConfig::link); break; } + return static_cast<HoverArea>(HoverAreaConfig::name); + } return HoverArea::none; } @@ -593,9 +598,11 @@ private: void onMouseLeft(GridClickEvent& event) { if (const ConfigView::Details* item = cfgView_.getItem(event.row_)) - switch (static_cast<HoverAreaLog>(event.hoverArea_)) + switch (static_cast<HoverAreaConfig>(event.hoverArea_)) { - case HoverAreaLog::link: + case HoverAreaConfig::name: + break; + case HoverAreaConfig::link: try { if (const Zstring& nativePath = getNativeItemPath(item->cfgItem.logFilePath); @@ -613,9 +620,11 @@ private: void onMouseLeftDouble(GridClickEvent& event) { - switch (static_cast<HoverAreaLog>(event.hoverArea_)) + switch (static_cast<HoverAreaConfig>(event.hoverArea_)) { - case HoverAreaLog::link: + case HoverAreaConfig::name: + break; + case HoverAreaConfig::link: return; //swallow event here before MainDialog considers it as a request to start comparison } event.Skip(); diff --git a/FreeFileSync/Source/ui/command_box.cpp b/FreeFileSync/Source/ui/command_box.cpp index 018a8311..ca12ebe4 100644 --- a/FreeFileSync/Source/ui/command_box.cpp +++ b/FreeFileSync/Source/ui/command_box.cpp @@ -20,8 +20,6 @@ namespace { inline wxString getSeparationLine() { return std::wstring(50, EM_DASH); } //no space between dashes! - -wxDEFINE_EVENT(EVENT_VALIDATE_USER_SELECTION, wxCommandEvent); } @@ -45,8 +43,6 @@ CommandBox::CommandBox(wxWindow* parent, Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent& event) { onUpdateList(event); }); Bind(wxEVT_COMMAND_COMBOBOX_SELECTED, [this](wxCommandEvent& event) { onSelection (event); }); Bind(wxEVT_MOUSEWHEEL, [] (wxMouseEvent& event) {}); //swallow! this gives confusing UI feedback anyway - - Bind(EVENT_VALIDATE_USER_SELECTION, [this](wxCommandEvent& event) { onValidateSelection(event); }); } @@ -129,13 +125,13 @@ void CommandBox::setValueAndUpdateList(const wxString& value) void CommandBox::onSelection(wxCommandEvent& event) { //we cannot replace built-in commands at this position in call stack, so defer to a later time! - GetEventHandler()->AddPendingEvent(wxCommandEvent(EVENT_VALIDATE_USER_SELECTION)); + CallAfter([&] { onValidateSelection(); }); event.Skip(); } -void CommandBox::onValidateSelection(wxCommandEvent& event) +void CommandBox::onValidateSelection() { const wxString value = GetValue(); diff --git a/FreeFileSync/Source/ui/command_box.h b/FreeFileSync/Source/ui/command_box.h index f780b380..02745be1 100644 --- a/FreeFileSync/Source/ui/command_box.h +++ b/FreeFileSync/Source/ui/command_box.h @@ -42,7 +42,7 @@ public: private: void onKeyEvent(wxKeyEvent& event); void onSelection(wxCommandEvent& event); - void onValidateSelection(wxCommandEvent& event); + void onValidateSelection(); void onUpdateList(wxEvent& event); void setValueAndUpdateList(const wxString& value); diff --git a/FreeFileSync/Source/ui/file_grid.cpp b/FreeFileSync/Source/ui/file_grid.cpp index 21114308..e81ec344 100644 --- a/FreeFileSync/Source/ui/file_grid.cpp +++ b/FreeFileSync/Source/ui/file_grid.cpp @@ -464,21 +464,24 @@ private: std::wstring getValue(size_t row, ColumnType colType) const override { - std::wstring value; if (const FileSystemObject* fsObj = getFsObject(row)) if (!fsObj->isEmpty<side>()) + { + if (static_cast<ColumnTypeRim>(colType) == ColumnTypeRim::path) + switch (itemPathFormat_) + { + case ItemPathFormat::name: + return utfTo<std::wstring>(fsObj->getItemName<side>()); + case ItemPathFormat::relative: + return utfTo<std::wstring>(fsObj->getRelativePath<side>()); + case ItemPathFormat::full: + return AFS::getDisplayPath(fsObj->getAbstractPath<side>()); + } + + std::wstring value; //dynamically allocates 16 byte memory! but why? shouldn't SSO make this superfluous?! or is it only in debug? switch (static_cast<ColumnTypeRim>(colType)) { case ColumnTypeRim::path: - switch (itemPathFormat_) - { - case ItemPathFormat::name: - return utfTo<std::wstring>(fsObj->getItemName<side>()); - case ItemPathFormat::relative: - return utfTo<std::wstring>(fsObj->getRelativePath<side>()); - case ItemPathFormat::full: - return AFS::getDisplayPath(fsObj->getAbstractPath<side>()); - } assert(false); break; @@ -501,7 +504,9 @@ private: [&](const SymlinkPair& symlink) { value = utfTo<std::wstring>(getFileExtension(symlink.getItemName<side>())); }); break; } - return value; + return value; + } + return {}; } void renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected, HoverArea rowHover) override @@ -787,9 +792,12 @@ private: rectCud.x += rectCud.width; rectCud.width = gapSize_ + fastFromDIP(2); +#if 0 //wxDC::GetPixel() is broken in GTK3! https://github.com/wxWidgets/wxWidgets/issues/14067 wxColor backCol = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); dc.GetPixel(rectCud.GetTopRight(), &backCol); - +#else + const wxColor backCol = getDefaultBackgroundColorAlternating(pdi.groupIdx % 2 == 0); +#endif dc.GradientFillLinear(rectCud, getBackGroundColorSyncAction(syncOp), backCol, wxEAST); } }; @@ -807,9 +815,6 @@ private: if (row == pdi.groupLastRow - 1 /*last group item*/) //preserve the group separation line! rectNav.height -= fastFromDIP(1); - //wxColor backCol = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); - //dc.GetPixel(rectNav.GetTopRight(), &backCol); //e.g. selected row! - dc.GradientFillLinear(rectNav, getColorSelectionGradientFrom(), getColorSelectionGradientTo(), wxEAST); navMarkerDrawn = true; } @@ -1702,9 +1707,9 @@ public: gridL_.Bind(EVENT_GRID_COL_RESIZE, [this](GridColumnResizeEvent& event) { onResizeColumn(event, gridL_, gridR_); }); gridR_.Bind(EVENT_GRID_COL_RESIZE, [this](GridColumnResizeEvent& event) { onResizeColumn(event, gridR_, gridL_); }); - gridL_.getMainWin().Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onKeyDown(event, gridL_); }); - gridC_.getMainWin().Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onKeyDown(event, gridC_); }); - gridR_.getMainWin().Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onKeyDown(event, gridR_); }); + gridL_.Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onKeyDown(event, gridL_); }); + gridC_.Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onKeyDown(event, gridC_); }); + gridR_.Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onKeyDown(event, gridR_); }); gridC_.getMainWin().Bind(wxEVT_MOTION, [this](wxMouseEvent& event) { onCenterMouseMovement(event); }); gridC_.getMainWin().Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& event) { onCenterMouseLeave (event); }); @@ -1747,7 +1752,7 @@ public: //=> Next keyboard input on left does *not* emit focus change event, but still "scrollMaster" needs to change //=> hook keyboard input instead of focus event: grid.getMainWin().Bind(wxEVT_CHAR, handler); - grid.getMainWin().Bind(wxEVT_KEY_DOWN, handler); + grid.Bind(wxEVT_KEY_DOWN, handler); //grid.getMainWin().Bind(wxEVT_KEY_UP, handler); -> superfluous? grid.getMainWin().Bind(wxEVT_LEFT_DOWN, handler); diff --git a/FreeFileSync/Source/ui/gui_generated.cpp b/FreeFileSync/Source/ui/gui_generated.cpp index 4b1ae973..d0fd57ee 100644 --- a/FreeFileSync/Source/ui/gui_generated.cpp +++ b/FreeFileSync/Source/ui/gui_generated.cpp @@ -1650,7 +1650,7 @@ ConfigDlgGenerated::ConfigDlgGenerated( wxWindow* parent, wxWindowID id, const w m_panelCompSettingsTab->SetSizer( bSizer275 ); m_panelCompSettingsTab->Layout(); bSizer275->Fit( m_panelCompSettingsTab ); - m_notebook->AddPage( m_panelCompSettingsTab, _("dummy"), true ); + m_notebook->AddPage( m_panelCompSettingsTab, _("dummy"), false ); m_panelFilterSettingsTab = new wxPanel( m_notebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); m_panelFilterSettingsTab->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); @@ -1781,7 +1781,10 @@ ConfigDlgGenerated::ConfigDlgGenerated( wxWindow* parent, wxWindowID id, const w bSizer162->Add( m_choiceUnitMinSize, 0, wxEXPAND, 5 ); - bSizer158->Add( bSizer162, 0, wxBOTTOM|wxEXPAND, 5 ); + bSizer158->Add( bSizer162, 0, wxEXPAND, 5 ); + + + bSizer158->Add( 0, 10, 0, 0, 5 ); wxBoxSizer* bSizer163; bSizer163 = new wxBoxSizer( wxVERTICAL ); @@ -1877,7 +1880,7 @@ ConfigDlgGenerated::ConfigDlgGenerated( wxWindow* parent, wxWindowID id, const w m_panelFilterSettingsTab->SetSizer( bSizer278 ); m_panelFilterSettingsTab->Layout(); bSizer278->Fit( m_panelFilterSettingsTab ); - m_notebook->AddPage( m_panelFilterSettingsTab, _("dummy"), false ); + m_notebook->AddPage( m_panelFilterSettingsTab, _("dummy"), true ); m_panelSyncSettingsTab = new wxPanel( m_notebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); m_panelSyncSettingsTab->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); diff --git a/FreeFileSync/Source/ui/gui_status_handler.cpp b/FreeFileSync/Source/ui/gui_status_handler.cpp index 1cf637de..7b7adc95 100644 --- a/FreeFileSync/Source/ui/gui_status_handler.cpp +++ b/FreeFileSync/Source/ui/gui_status_handler.cpp @@ -361,7 +361,7 @@ StatusHandlerFloatingDialog::StatusHandlerFloatingDialog(wxFrame* parentDlg, std::chrono::seconds autoRetryDelay, const Zstring& soundFileSyncComplete, const Zstring& soundFileAlertPending, - const wxSize& progressDlgSize, bool dlgMaximize, + const std::optional<wxSize>& progressDlgSize, bool dlgMaximize, bool autoCloseDialog, const ErrorLog* errorLogStart) : jobNames_(jobNames), @@ -525,7 +525,7 @@ StatusHandlerFloatingDialog::Result StatusHandlerFloatingDialog::reportResults(c wxSound::Play(utfTo<wxString>(soundFileSyncComplete_), wxSOUND_ASYNC); } //if (::GetForegroundWindow() != GetHWND()) - // RequestUserAttention(); -> probably too much since task bar is already colorized with Taskbar::STATUS_ERROR or STATUS_NORMAL + // RequestUserAttention(); -> probably too much since task bar is already colorized with Taskbar::STATUS_ERROR or STATUS_NORMAL } //--------------------- save log file ---------------------- diff --git a/FreeFileSync/Source/ui/gui_status_handler.h b/FreeFileSync/Source/ui/gui_status_handler.h index beb4d558..c2929d59 100644 --- a/FreeFileSync/Source/ui/gui_status_handler.h +++ b/FreeFileSync/Source/ui/gui_status_handler.h @@ -73,7 +73,7 @@ public: std::chrono::seconds autoRetryDelay, const Zstring& soundFileSyncComplete, const Zstring& soundFileAlertPending, - const wxSize& progressDlgSize, bool dlgMaximize, + const std::optional<wxSize>& progressDlgSize, bool dlgMaximize, bool autoCloseDialog, const zen::ErrorLog* errorLogStart /*optional*/); //noexcept! ~StatusHandlerFloatingDialog(); @@ -99,7 +99,7 @@ public: zen::SharedRef<const zen::ErrorLog> errorLog; FinalRequest finalRequest; AbstractPath logFilePath; - wxSize dlgSize; + std::optional<wxSize> dlgSize; bool dlgIsMaximized; bool autoCloseDialog; }; diff --git a/FreeFileSync/Source/ui/log_panel.cpp b/FreeFileSync/Source/ui/log_panel.cpp index 9e839d4b..ff64044e 100644 --- a/FreeFileSync/Source/ui/log_panel.cpp +++ b/FreeFileSync/Source/ui/log_panel.cpp @@ -326,7 +326,7 @@ LogPanel::LogPanel(wxWindow* parent) : LogPanelGenerated(parent) }); //support for CTRL + C - m_gridMessages->getMainWin().Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onGridButtonEvent(event); }); + m_gridMessages->Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onGridButtonEvent(event); }); m_gridMessages->Bind(EVENT_GRID_CONTEXT_MENU, [this](GridContextMenuEvent& event) { onMsgGridContext(event); }); diff --git a/FreeFileSync/Source/ui/main_dlg.cpp b/FreeFileSync/Source/ui/main_dlg.cpp index 341cfec7..3ebebeba 100644 --- a/FreeFileSync/Source/ui/main_dlg.cpp +++ b/FreeFileSync/Source/ui/main_dlg.cpp @@ -25,7 +25,7 @@ #include <wx+/toggle_button.h> #include <wx+/no_flicker.h> #include <wx+/rtl.h> -#include <wx+/font_size.h> +#include <wx+/window_layout.h> #include <wx+/popup_dlg.h> #include <wx+/window_tools.h> #include <wx+/image_resources.h> @@ -64,14 +64,14 @@ const int TOP_BUTTON_OPTIMAL_WIDTH_DIP = 170; constexpr std::chrono::milliseconds LAST_USED_CFG_EXISTENCE_CHECK_TIME_MAX(500); constexpr std::chrono::milliseconds FILE_GRID_POST_UPDATE_DELAY(400); -const Zchar macroNameItemPath [] = Zstr("%item_path%"); -const Zchar macroNameItemPath2 [] = Zstr("%item_path2%"); -const Zchar macroNameLocalPath [] = Zstr("%local_path%"); -const Zchar macroNameLocalPath2 [] = Zstr("%local_path2%"); -const Zchar macroNameItemName [] = Zstr("%item_name%"); -const Zchar macroNameItemName2 [] = Zstr("%item_name2%"); -const Zchar macroNameParentPath [] = Zstr("%parent_path%"); -const Zchar macroNameParentPath2[] = Zstr("%parent_path2%"); +const ZstringView macroNameItemPath = Zstr("%item_path%"); +const ZstringView macroNameItemPath2 = Zstr("%item_path2%"); +const ZstringView macroNameLocalPath = Zstr("%local_path%"); +const ZstringView macroNameLocalPath2 = Zstr("%local_path2%"); +const ZstringView macroNameItemName = Zstr("%item_name%"); +const ZstringView macroNameItemName2 = Zstr("%item_name2%"); +const ZstringView macroNameParentPath = Zstr("%parent_path%"); +const ZstringView macroNameParentPath2 = Zstr("%parent_path2%"); bool containsFileItemMacro(const Zstring& commandLinePhrase) { @@ -436,10 +436,10 @@ void MainDialog::create(const Zstring& globalConfigFilePath, //construction complete! trigger special events: //------------------------------------------------------------------------------------------ - //show welcome screen after FreeFileSync update => show *before* any other dialogs - if (mainDlg->globalCfg_.welcomeShownVersion != ffsVersion) + //show welcome dialog after FreeFileSync update => show *before* any other dialogs + if (mainDlg->globalCfg_.welcomeDialogLastVersion != ffsVersion) { - mainDlg->globalCfg_.welcomeShownVersion = ffsVersion; + mainDlg->globalCfg_.welcomeDialogLastVersion = ffsVersion; showAboutDialog(mainDlg); } @@ -772,7 +772,7 @@ imgFileManagerSmall_([] m_gridOverview->Bind(EVENT_GRID_SELECT_RANGE, [this](GridSelectEvent& event) { onTreeGridSelection(event); }); //cfg grid: - m_gridCfgHistory->getMainWin().Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onCfgGridKeyEvent(event); }); + m_gridCfgHistory->Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onCfgGridKeyEvent(event); }); m_gridCfgHistory->Bind(EVENT_GRID_SELECT_RANGE, [this](GridSelectEvent& event) { onCfgGridSelection (event); }); m_gridCfgHistory->Bind(EVENT_GRID_MOUSE_LEFT_DOUBLE, [this](GridClickEvent& event) { onCfgGridDoubleClick (event); }); m_gridCfgHistory->Bind(EVENT_GRID_CONTEXT_MENU, [this](GridContextMenuEvent& event) { onCfgGridContext (event); }); @@ -896,11 +896,11 @@ imgFileManagerSmall_([] setConfig(guiCfg, referenceFiles); //expects auiMgr_.Update(): e.g. recalcMaxFolderPairsVisible() //support for CTRL + C and DEL on grids - m_gridMainL->getMainWin().Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onGridKeyEvent(event, *m_gridMainL, true /*leftSide*/); }); - m_gridMainC->getMainWin().Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onGridKeyEvent(event, *m_gridMainC, true /*leftSide*/); }); - m_gridMainR->getMainWin().Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onGridKeyEvent(event, *m_gridMainR, false /*leftSide*/); }); + m_gridMainL->Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onGridKeyEvent(event, *m_gridMainL, true /*leftSide*/); }); + m_gridMainC->Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onGridKeyEvent(event, *m_gridMainC, true /*leftSide*/); }); + m_gridMainR->Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onGridKeyEvent(event, *m_gridMainR, false /*leftSide*/); }); - m_gridOverview->getMainWin().Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onTreeKeyEvent(event); }); + m_gridOverview->Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onTreeKeyEvent(event); }); Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onLocalKeyEvent(event); }); //enable dialog-specific key events @@ -1045,11 +1045,7 @@ void MainDialog::setGlobalCfgOnInit(const XmlGlobalSettings& globalSettings) //caveat: set/get language asymmmetry! setLanguage(globalSettings.programLanguage); //throw FileError //we need to set language before creating this class! - setInitialWindowSize(*this, - layout.mainDlg.dlgSize, - layout.mainDlg.dlgPos, - layout.mainDlg.isMaximized, - wxSize{fastFromDIP(900), fastFromDIP(600)} /*defaultSize*/); + WindowLayout::setInitial(*this, {layout.mainDlg.size, layout.mainDlg.pos, layout.mainDlg.isMaximized}, {fastFromDIP(900), fastFromDIP(600)} /*defaultSize*/); //set column attributes m_gridMainL ->setColumnConfig(convertColAttributes(layout.fileColumnAttribsLeft, getFileGridDefaultColAttribsLeft())); @@ -1110,7 +1106,7 @@ void MainDialog::setGlobalCfgOnInit(const XmlGlobalSettings& globalSettings) preserveConstraint(auiMgr_.GetPane(m_panelViewFilter)); preserveConstraint(auiMgr_.GetPane(m_panelConfig)); - auiMgr_.LoadPerspective(layout.mainDlg.panelLayout, false /*update: don't call wxAuiManager::Update() yet*/); + auiMgr_.LoadPerspective(layout.panelLayout, false /*update: don't call wxAuiManager::Update() yet*/); //restore original captions for (const auto& [paneInfo, caption] : paneCaptions) @@ -1204,14 +1200,10 @@ XmlGlobalSettings MainDialog::getGlobalCfgBeforeExit() //else: logPane.best_size already contains non-maximized value //auiMgr_.Update(); //[!] not needed - globalSettings.dpiLayouts[getDpiScalePercent()].mainDlg.panelLayout = auiMgr_.SavePerspective(); //does not need wxAuiManager::Update()! + globalSettings.dpiLayouts[getDpiScalePercent()].panelLayout = auiMgr_.SavePerspective(); //does not need wxAuiManager::Update()! - const auto& [size, pos, isMaximized] = getWindowSizeBeforeClose(*this); //call *after* wxAuiManager::SavePerspective()! - if (size) - globalSettings.dpiLayouts[getDpiScalePercent()].mainDlg.dlgSize = *size; - if (pos) - globalSettings.dpiLayouts[getDpiScalePercent()].mainDlg.dlgPos = *pos; - globalSettings.dpiLayouts[getDpiScalePercent()].mainDlg.isMaximized = isMaximized; + const auto& [size, pos, isMaximized] = WindowLayout::getBeforeClose(*this); //call *after* wxAuiManager::SavePerspective()! + globalSettings.dpiLayouts[getDpiScalePercent()].mainDlg = {size, pos, isMaximized}; return globalSettings; } @@ -4494,7 +4486,7 @@ void MainDialog::onStartSync(wxCommandEvent& event) guiCfg.mainCfg.autoRetryDelay, globalCfg_.soundFileSyncFinished, globalCfg_.soundFileAlertPending, - globalCfg_.dpiLayouts[getDpiScalePercent()].progressDlg.dlgSize, + globalCfg_.dpiLayouts[getDpiScalePercent()].progressDlg.size, globalCfg_.dpiLayouts[getDpiScalePercent()].progressDlg.isMaximized, globalCfg_.progressDlgAutoClose, errorLogStart.get()); @@ -4555,7 +4547,7 @@ void MainDialog::onStartSync(wxCommandEvent& event) setLastOperationLog(r.summary, r.errorLog.ptr()); globalCfg_.progressDlgAutoClose = r.autoCloseDialog; - globalCfg_.dpiLayouts[getDpiScalePercent()].progressDlg.dlgSize = r.dlgSize; + globalCfg_.dpiLayouts[getDpiScalePercent()].progressDlg.size = r.dlgSize; globalCfg_.dpiLayouts[getDpiScalePercent()].progressDlg.isMaximized = r.dlgIsMaximized; //update last sync stats for the selected cfg files @@ -4814,9 +4806,8 @@ void MainDialog::setLastOperationLog(const ProcessSummary& summary, const std::s } const int64_t totalTimeSec = std::chrono::duration_cast<std::chrono::seconds>(summary.totalTime).count(); - - m_staticTextTimeElapsed->SetLabelText(wxTimeSpan::Seconds(totalTimeSec).Format(L"%H:%M:%S")); - //totalTimeSec < 3600 ? wxTimeSpan::Seconds(totalTimeSec).Format(L"%M:%S") -> let's use full precision for max. clarity: https://freefilesync.org/forum/viewtopic.php?t=6308 + m_staticTextTimeElapsed->SetLabelText(utfTo<wxString>(formatTimeSpan(totalTimeSec))); + //hourOptional? -> let's use full precision for max. clarity: https://freefilesync.org/forum/viewtopic.php?t=6308 logPanel_->setLog(errorLog); diff --git a/FreeFileSync/Source/ui/progress_indicator.cpp b/FreeFileSync/Source/ui/progress_indicator.cpp index d10a21db..21a94913 100644 --- a/FreeFileSync/Source/ui/progress_indicator.cpp +++ b/FreeFileSync/Source/ui/progress_indicator.cpp @@ -15,7 +15,7 @@ #include <wx+/image_tools.h> #include <wx+/graph.h> #include <wx+/no_flicker.h> -#include <wx+/font_size.h> +#include <wx+/window_layout.h> #include <zen/file_access.h> #include <zen/thread.h> #include <zen/perf.h> @@ -218,7 +218,10 @@ CompareProgressPanel::Impl::Impl(wxFrame& parentWindow) : m_panelTimeStats->Layout(); GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() - //=> works like a charm for GTK2 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(); //avoid old position flash when Center() moves window (asynchronously?) +#endif } @@ -362,9 +365,7 @@ void CompareProgressPanel::Impl::updateProgressGui() //current time elapsed const int64_t timeElapSec = std::chrono::duration_cast<std::chrono::seconds>(timeElapsed).count(); - setText(*m_staticTextTimeElapsed, timeElapSec < 3600 ? - wxTimeSpan::Seconds(timeElapSec).Format( L"%M:%S") : - wxTimeSpan::Seconds(timeElapSec).Format(L"%H:%M:%S"), &layoutChanged); + setText(*m_staticTextTimeElapsed, utfTo<wxString>(formatTimeSpan(timeElapSec, true /*hourOptional*/)), &layoutChanged); if (haveTotalStats) //remaining time and speed: only visible during binary comparison if (numeric::dist(timeLastSpeedEstimate_, timeElapsed) >= SPEED_ESTIMATE_UPDATE_INTERVAL) @@ -618,12 +619,10 @@ struct LabelFormatterTimeElapsed : public LabelFormatter wxString formatText(double timeElapsed, double optimalBlockSize) const override { const int64_t timeElapsedSec = std::round(timeElapsed); + if (timeElapsedSec < 60) + return _P("1 sec", "%x sec", timeElapsedSec); - return timeElapsedSec < 60 ? - wxString(_P("1 sec", "%x sec", timeElapsedSec)) : - timeElapsedSec < 3600 ? - wxTimeSpan::Seconds(timeElapsedSec).Format( L"%M:%S") : - wxTimeSpan::Seconds(timeElapsedSec).Format(L"%H:%M:%S"); + return utfTo<wxString>(formatTimeSpan(timeElapsedSec, true /*hourOptional*/)); } }; } @@ -638,7 +637,7 @@ class SyncProgressDialogImpl : public TopLevelDialog, public SyncProgressDialog { public: SyncProgressDialogImpl(long style, //wxFrame/wxDialog style - wxSize dlgSize, bool dlgMaximize, + const std::optional<wxSize>& dlgSize, bool dlgMaximize, const std::function<void()>& userRequestAbort, const Statistics& syncStat, wxFrame* parentFrame, @@ -738,14 +737,12 @@ private: bool ignoreErrors_ = false; EnumDescrList<PostSyncAction2> enumPostSyncAction_; - - wxSize dlgSizeBuf_; }; template <class TopLevelDialog> SyncProgressDialogImpl<TopLevelDialog>::SyncProgressDialogImpl(long style, //wxFrame/wxDialog style - wxSize dlgSize, bool dlgMaximize, + const std::optional<wxSize>& dlgSize, bool dlgMaximize, const std::function<void()>& userRequestAbort, const Statistics& syncStat, wxFrame* parentFrame, @@ -773,8 +770,7 @@ SyncProgressDialogImpl<TopLevelDialog>::SyncProgressDialogImpl(long style, //wxF ()), parentFrame_(parentFrame), userRequestAbort_(userRequestAbort), -syncStat_(&syncStat), -dlgSizeBuf_(dlgSize) +syncStat_(&syncStat) { static_assert(std::is_same_v<TopLevelDialog, wxFrame > || std::is_same_v<TopLevelDialog, wxDialog>); @@ -903,10 +899,14 @@ dlgSizeBuf_(dlgSize) //make sure that standard height matches ProcessPhase::comparingContent statistics layout (== largest) this->GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() +#ifdef __WXGTK3__ + this->Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 + this->Hide(); //avoid old position flash when Center() moves window (asynchronously?) +#endif pnl_.Layout(); this->Center(); //call *after* dialog layout update and *before* wxWindow::Show()! - setInitialWindowSize(*this, dlgSizeBuf_, std::nullopt/*pos*/, dlgMaximize, this->GetSize() /*defaultSize*/); + WindowLayout::setInitial(*this, {dlgSize, std::nullopt/*pos*/, dlgMaximize}, this->GetSize() /*defaultSize*/); if (showProgress) { @@ -1141,9 +1141,7 @@ void SyncProgressDialogImpl<TopLevelDialog>::updateProgressGui(bool allowYield) //current time elapsed const int64_t timeElapSec = std::chrono::duration_cast<std::chrono::seconds>(timeElapsed).count(); - setText(*pnl_.m_staticTextTimeElapsed, timeElapSec < 3600 ? - wxTimeSpan::Seconds(timeElapSec).Format( L"%M:%S") : - wxTimeSpan::Seconds(timeElapSec).Format(L"%H:%M:%S"), &layoutChanged); + setText(*pnl_.m_staticTextTimeElapsed, utfTo<wxString>(formatTimeSpan(timeElapSec, true /*hourOptional*/)), &layoutChanged); //remaining time and speed if (numeric::dist(timeLastSpeedEstimate_, timeElapsed) >= SPEED_ESTIMATE_UPDATE_INTERVAL) @@ -1340,8 +1338,9 @@ void SyncProgressDialogImpl<TopLevelDialog>::showSummary(SyncResult syncResult, pnl_.m_staticTextTimeRemaining->Hide(); const int64_t totalTimeSec = std::chrono::duration_cast<std::chrono::seconds>(stopWatch_.elapsed()).count(); - setText(*pnl_.m_staticTextTimeElapsed, wxTimeSpan::Seconds(totalTimeSec).Format(L"%H:%M:%S")); - //totalTimeSec < 3600 ? wxTimeSpan::Seconds(totalTimeSec).Format(L"%M:%S") -> let's use full precision for max. clarity: https://freefilesync.org/forum/viewtopic.php?t=6308 + pnl_.m_staticTextTimeElapsed->SetLabelText(utfTo<wxString>(formatTimeSpan(totalTimeSec))); + //hourOptional? -> let's use full precision for max. clarity: https://freefilesync.org/forum/viewtopic.php?t=6308 + resumeFromSystray(false /*userRequested*/); //if in tray mode... @@ -1519,13 +1518,11 @@ auto SyncProgressDialogImpl<TopLevelDialog>::destroy(bool autoClose, bool restor //------------------------------------------------------------------------ const bool autoCloseDialog = getOptionAutoCloseDialog(); - const auto& [size, pos, isMaximized] = getWindowSizeBeforeClose(*this); - if (size) - dlgSizeBuf_ = *size; + const auto& [dlgSize, pos, isMaximized] = WindowLayout::getBeforeClose(*this); this->Destroy(); //wxWidgets macOS: simple "delete"!!!!!!! - return {autoCloseDialog, dlgSizeBuf_, isMaximized}; + return {autoCloseDialog, dlgSize, isMaximized}; } @@ -1646,7 +1643,7 @@ void SyncProgressDialogImpl<TopLevelDialog>::resumeFromSystray(bool userRequeste //######################################################################################## -SyncProgressDialog* SyncProgressDialog::create(wxSize dlgSize, bool dlgMaximize, +SyncProgressDialog* SyncProgressDialog::create(const std::optional<wxSize>& dlgSize, bool dlgMaximize, const std::function<void()>& userRequestAbort, const Statistics& syncStat, wxFrame* parentWindow, //may be nullptr @@ -1658,21 +1655,15 @@ SyncProgressDialog* SyncProgressDialog::create(wxSize dlgSize, bool dlgMaximize, size_t autoRetryCount, PostSyncAction2 postSyncAction) { - if (parentWindow) //sync from GUI - { -#if 0 //macOS; update 08-2021: Bug seems to be fixed!? - //due to usual "wxBugs", wxDialog on OS X does not float on its parent; wxFrame OTOH does => hack! https://groups.google.com/forum/#!topic/wx-users/J5SjjLaBOQE - return new SyncProgressDialogImpl<wxFrame>(wxDEFAULT_FRAME_STYLE | wxFRAME_FLOAT_ON_PARENT, - dlgSize, dlgMaximize, userRequestAbort, syncStat, parentWindow, showProgress, autoCloseDialog, jobNames, syncStartTime, ignoreErrors, autoRetryCount, postSyncAction); -#else //GNOME bug: wxDialog seems to ignore wxMAXIMIZE_BOX | wxMINIMIZE_BOX! :( wxFrame OTOH has them, but adds an extra taskbar entry + if (parentWindow) //FFS GUI sync return new SyncProgressDialogImpl<wxDialog>(wxDEFAULT_DIALOG_STYLE | wxMAXIMIZE_BOX | wxMINIMIZE_BOX | wxRESIZE_BORDER, - dlgSize, dlgMaximize, userRequestAbort, syncStat, parentWindow, showProgress, autoCloseDialog, jobNames, syncStartTime, ignoreErrors, autoRetryCount, postSyncAction); -#endif - } + dlgSize, dlgMaximize, userRequestAbort, syncStat, parentWindow, showProgress, + autoCloseDialog, jobNames, syncStartTime, ignoreErrors, autoRetryCount, postSyncAction); else //FFS batch job { auto dlg = new SyncProgressDialogImpl<wxFrame>(wxDEFAULT_FRAME_STYLE, - dlgSize, dlgMaximize, userRequestAbort, syncStat, parentWindow, showProgress, autoCloseDialog, jobNames, syncStartTime, ignoreErrors, autoRetryCount, postSyncAction); + dlgSize, dlgMaximize, userRequestAbort, syncStat, parentWindow, showProgress, + autoCloseDialog, jobNames, syncStartTime, ignoreErrors, autoRetryCount, postSyncAction); dlg->SetIcon(getFfsIcon()); //only top level windows should have an icon return dlg; } diff --git a/FreeFileSync/Source/ui/progress_indicator.h b/FreeFileSync/Source/ui/progress_indicator.h index a8536df7..fe63e9f5 100644 --- a/FreeFileSync/Source/ui/progress_indicator.h +++ b/FreeFileSync/Source/ui/progress_indicator.h @@ -55,7 +55,7 @@ enum class PostSyncAction2 struct SyncProgressDialog { - static SyncProgressDialog* create(wxSize dlgSize, bool dlgMaximize, + static SyncProgressDialog* create(const std::optional<wxSize>& dlgSize, bool dlgMaximize, const std::function<void()>& userRequestAbort, const Statistics& syncStat, wxFrame* parentWindow, //may be nullptr @@ -69,7 +69,7 @@ struct SyncProgressDialog struct Result { bool autoCloseDialog; - wxSize dlgSize; + std::optional<wxSize> dlgSize; bool dlgIsMaximized; }; virtual Result destroy(bool autoClose, bool restoreParentFrame, SyncResult syncResult, const zen::SharedRef<const zen::ErrorLog>& log) = 0; diff --git a/FreeFileSync/Source/ui/small_dlgs.cpp b/FreeFileSync/Source/ui/small_dlgs.cpp index 50713faa..6be25206 100644 --- a/FreeFileSync/Source/ui/small_dlgs.cpp +++ b/FreeFileSync/Source/ui/small_dlgs.cpp @@ -20,7 +20,7 @@ #include <wx+/rtl.h> #include <wx+/no_flicker.h> #include <wx+/image_tools.h> -#include <wx+/font_size.h> +#include <wx+/window_layout.h> #include <wx+/popup_dlg.h> #include <wx+/async_task.h> #include <wx+/image_resources.h> @@ -144,6 +144,10 @@ AboutDlg::AboutDlg(wxWindow* parent) : AboutDlgGenerated(parent) //-------------------------------------------------------------------------- //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(); //avoid old position flash when Center() moves window (asynchronously?) +#endif { const int imageWidth = (m_panelDonate->GetSize().GetWidth() - 5 - 5 - 5 /* grey border*/) / 2; @@ -159,7 +163,10 @@ AboutDlg::AboutDlg(wxWindow* parent) : AboutDlgGenerated(parent) Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onLocalKeyEvent(event); }); //enable dialog-specific key events GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() - //=> works like a charm for GTK2 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(); //avoid old position flash when Center() moves window (asynchronously?) +#endif Center(); //needs to be re-applied after a 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!!! @@ -278,10 +285,10 @@ CloudSetupDlg::CloudSetupDlg(wxWindow* parent, Zstring& folderPathPhrase, Zstrin m_textCtrlServer->SetHint(_("Example:") + L" website.com 66.198.240.22"); m_textCtrlServer->SetMinSize({fastFromDIP(260), -1}); - m_textCtrlPort ->SetMinSize({fastFromDIP(60), -1}); // - m_spinCtrlConnectionCount ->SetMinSize({fastFromDIP(70), -1}); //Hack: set size (why does wxWindow::Size() not work?) - m_spinCtrlChannelCountSftp->SetMinSize({fastFromDIP(70), -1}); // - m_spinCtrlTimeout ->SetMinSize({fastFromDIP(70), -1}); // + m_textCtrlPort->SetMinSize({fastFromDIP(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); }); @@ -393,7 +400,11 @@ CloudSetupDlg::CloudSetupDlg(wxWindow* parent, Zstring& folderPathPhrase, Zstrin m_checkBoxPasswordPrompt->Hide(); GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() - //=> works like a charm for GTK2 with window resizing problems and title bar corruption; e.g. Debian!!! + //=> 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(); //avoid old position flash when Center() moves window (asynchronously?) +#endif Center(); //needs to be re-applied after a dialog size change! updateGui(); //*after* SetSizeHints when standard dialog height has been calculated @@ -527,7 +538,7 @@ void CloudSetupDlg::onDetectServerChannelLimit(wxCommandEvent& event) 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 @@ -546,7 +557,7 @@ void CloudSetupDlg::onDetectServerChannelLimit(wxCommandEvent& event) m_spinCtrlChannelCountSftp->SetFocus(); //[!] otherwise selection is lost m_spinCtrlChannelCountSftp->SetSelection(-1, -1); //some visual feedback: select all } - catch (AbortProcess&) { return; } + catch (AbortProcess&) { return; } catch (const FileError& e) { showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); @@ -823,7 +834,7 @@ void CloudSetupDlg::onBrowseCloudFolder(wxCommandEvent& event) AbstractPath folderPath = getFolderPath(); //noexcept try { - //------------------------------------------------------------------- + //------------------------------------------------------------------- auto requestPassword = [&, password = Zstring()](const std::wstring& msg, const std::wstring& lastErrorMsg) mutable { assert(runningOnMainThread()); @@ -834,7 +845,7 @@ void CloudSetupDlg::onBrowseCloudFolder(wxCommandEvent& event) AFS::authenticateAccess(folderPath.afsDevice, requestPassword); //throw FileError, AbortProcess //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)) { @@ -961,7 +972,10 @@ CopyToDialog::CopyToDialog(wxWindow* parent, Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onLocalKeyEvent(event); }); //enable dialog-specific key events GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() - //=> works like a charm for GTK2 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(); //avoid old position flash when Center() moves window (asynchronously?) +#endif Center(); //needs to be re-applied after a dialog size change! m_buttonOK->SetFocus(); @@ -1076,7 +1090,10 @@ DeleteDialog::DeleteDialog(wxWindow* parent, Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onLocalKeyEvent(event); }); //enable dialog-specific key events GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() - //=> works like a charm for GTK2 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(); //avoid old position flash when Center() moves window (asynchronously?) +#endif Center(); //needs to be re-applied after a dialog size change! m_buttonOK->SetFocus(); @@ -1216,7 +1233,10 @@ SyncConfirmationDlg::SyncConfirmationDlg(wxWindow* parent, setIntValue(*m_staticTextDeleteRight, st.deleteCount<SelectSide::right>(), *m_bitmapDeleteRight, "so_delete_right_sicon"); GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() - //=> works like a charm for GTK2 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(); //avoid old position flash when Center() moves window (asynchronously?) +#endif Center(); //needs to be re-applied after a dialog size change! m_buttonStartSync->SetFocus(); @@ -1380,7 +1400,7 @@ OptionsDlg::OptionsDlg(wxWindow* parent, XmlGlobalSettings& globalCfg) : logFolderSelector_.setPath(globalCfg.logFolderPhrase); - m_spinCtrlLogFilesMaxAge->SetMinSize({fastFromDIP(70), -1}); //Hack: set size (why does wxWindow::Size() not work?) + setDefaultWidth(*m_spinCtrlLogFilesMaxAge); setImage(*m_bitmapSettings, loadImage("settings")); setImage(*m_bitmapWarnings, loadImage("msg_warning", fastFromDIP(20))); @@ -1480,7 +1500,10 @@ OptionsDlg::OptionsDlg(wxWindow* parent, XmlGlobalSettings& globalCfg) : updateGui(); GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() - //=> works like a charm for GTK2 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(); //avoid old position flash when Center() moves window (asynchronously?) +#endif Center(); //needs to be re-applied after a dialog size change! //restore actual value: @@ -1800,7 +1823,10 @@ SelectTimespanDlg::SelectTimespanDlg(wxWindow* parent, time_t& timeFrom, time_t& Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onLocalKeyEvent(event); }); //enable dialog-specific key events GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() - //=> works like a charm for GTK2 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(); //avoid old position flash when Center() moves window (asynchronously?) +#endif Center(); //needs to be re-applied after a dialog size change! m_buttonOkay->SetFocus(); @@ -1896,7 +1922,10 @@ PasswordPromptDlg::PasswordPromptDlg(wxWindow* parent, const std::wstring& msg, m_textCtrlPasswordVisible->Hide(); GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() - //=> works like a charm for GTK2 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(); //avoid old position flash when Center() moves window (asynchronously?) +#endif Center(); //needs to be re-applied after a dialog size change! updateGui(); //*after* SetSizeHints when standard dialog height has been calculated @@ -1974,12 +2003,15 @@ CfgHighlightDlg::CfgHighlightDlg(wxWindow* parent, int& cfgHistSyncOverdueDays) m_staticTextHighlight->Wrap(fastFromDIP(300)); - m_spinCtrlOverdueDays->SetMinSize({fastFromDIP(70), -1}); //Hack: set size (why does wxWindow::Size() not work?) + setDefaultWidth(*m_spinCtrlOverdueDays); m_spinCtrlOverdueDays->SetValue(cfgHistSyncOverdueDays); GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() - //=> works like a charm for GTK2 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(); //avoid old position flash when Center() moves window (asynchronously?) +#endif Center(); //needs to be re-applied after a dialog size change! m_spinCtrlOverdueDays->SetFocus(); @@ -2045,7 +2077,10 @@ ActivationDlg::ActivationDlg(wxWindow* parent, m_textCtrlOfflineActivationKey->ChangeValue(manualActivationKey); GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() - //=> works like a charm for GTK2 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(); //avoid old position flash when Center() moves window (asynchronously?) +#endif Center(); //needs to be re-applied after a dialog size change! m_buttonActivateOnline->SetFocus(); @@ -2150,8 +2185,12 @@ DownloadProgressWindow::Impl::Impl(wxWindow* parent, int64_t fileSizeTotal) : updateGui(); GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() - //=> works like a charm for GTK2 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(); //avoid old position flash when Center() moves window (asynchronously?) +#endif Center(); //needs to be re-applied after a dialog size change! + Show(); //clear gui flicker: window must be visible to make this work! diff --git a/FreeFileSync/Source/ui/sync_cfg.cpp b/FreeFileSync/Source/ui/sync_cfg.cpp index 66acc204..65b1b645 100644 --- a/FreeFileSync/Source/ui/sync_cfg.cpp +++ b/FreeFileSync/Source/ui/sync_cfg.cpp @@ -14,7 +14,7 @@ #include <wx+/context_menu.h> #include <wx+/choice_enum.h> #include <wx+/image_tools.h> -#include <wx+/font_size.h> +#include <wx+/window_layout.h> #include <wx+/popup_dlg.h> #include <wx+/image_resources.h> #include <wx+/window_tools.h> @@ -165,7 +165,7 @@ bool sanitizeFilter(FilterConfig& filterCfg, const std::vector<AbstractPath>& ba replacements.emplace_back(phrase, relPath); return; //... to next block } - throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); } } }); @@ -520,8 +520,8 @@ globalLogFolderPhrase_(globalLogFolderPhrase) const int scrollDelta = GetCharHeight(); m_scrolledWindowPerf->SetScrollRate(scrollDelta, scrollDelta); - m_spinCtrlAutoRetryCount->SetMinSize({fastFromDIP(60), -1}); //Hack: set size (why does wxWindow::Size() not work?) - m_spinCtrlAutoRetryDelay->SetMinSize({fastFromDIP(60), -1}); // + setDefaultWidth(*m_spinCtrlAutoRetryCount); + setDefaultWidth(*m_spinCtrlAutoRetryDelay); //ignore invalid input for time shift control: wxTextValidator inputValidator(wxFILTER_DIGITS | wxFILTER_INCLUDE_CHAR_LIST); @@ -536,6 +536,10 @@ globalLogFolderPhrase_(globalLogFolderPhrase) m_textCtrlInclude->Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onFilterKeyEvent(event); }); m_textCtrlExclude->Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onFilterKeyEvent(event); }); + setDefaultWidth(*m_spinCtrlMinSize); + setDefaultWidth(*m_spinCtrlMaxSize); + setDefaultWidth(*m_spinCtrlTimespan); + m_staticTextFilterDescr->Wrap(fastFromDIP(450)); setImage(*m_bpButtonDefaultContext, mirrorIfRtl(loadImage("button_arrow_right"))); @@ -598,9 +602,9 @@ globalLogFolderPhrase_(globalLogFolderPhrase) add(VersioningStyle::timestampFolder, _("Time stamp") + L" [" + _("Folder") + L']', _("Move files into a time-stamped subfolder")). add(VersioningStyle::timestampFile, _("Time stamp") + L" [" + _("File") + L']', _("Append a time stamp to each file name")); - m_spinCtrlVersionMaxDays ->SetMinSize({fastFromDIP(60), -1}); // - m_spinCtrlVersionCountMin->SetMinSize({fastFromDIP(60), -1}); //Hack: set size (why does wxWindow::Size() not work?) - m_spinCtrlVersionCountMax->SetMinSize({fastFromDIP(60), -1}); // + setDefaultWidth(*m_spinCtrlVersionMaxDays ); + setDefaultWidth(*m_spinCtrlVersionCountMin); + setDefaultWidth(*m_spinCtrlVersionCountMax); m_versioningFolderPath->setHistory(std::make_shared<HistoryList>(versioningFolderHistory, folderHistoryMax)); @@ -674,7 +678,10 @@ globalLogFolderPhrase_(globalLogFolderPhrase) selectFolderPairConfig(-1); GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() - //=> works like a charm for GTK2 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(); //avoid old position flash when Center() moves window (asynchronously?) +#endif Center(); //needs to be re-applied after a dialog size change! //keep stable sizer height: "two way" description is smaller than grid of sync directions @@ -1421,7 +1428,7 @@ void ConfigDialog::setMiscSyncOptions(const MiscSyncConfig& miscCfg) for (int i = 0; i < rowsToCreate; ++i) { wxSpinCtrl* spinCtrlParallelOps = new wxSpinCtrl(m_scrolledWindowPerf, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 1, 2000'000'000, 1); - spinCtrlParallelOps->SetMinSize({fastFromDIP(60), -1}); //Hack: set size (why does wxWindow::Size() not work?) + setDefaultWidth(*spinCtrlParallelOps); spinCtrlParallelOps->Enable(enableExtraFeatures_); fgSizerPerf->Add(spinCtrlParallelOps, 0, wxALIGN_CENTER_VERTICAL); diff --git a/FreeFileSync/Source/ui/tree_grid.cpp b/FreeFileSync/Source/ui/tree_grid.cpp index f53b212f..c870be72 100644 --- a/FreeFileSync/Source/ui/tree_grid.cpp +++ b/FreeFileSync/Source/ui/tree_grid.cpp @@ -136,7 +136,7 @@ void TreeView::extractVisibleSubtree(ContainerObject& hierObj, //in namespace { -//generate nice percentage numbers which precisely add up to 100 +//generate "nice" percentage numbers which precisely add up to 100 void calcPercentage(std::vector<std::pair<uint64_t, int*>>& workList) { uint64_t bytesTotal = 0; @@ -693,7 +693,7 @@ public: rootIcon_(loadImage("root_folder", widthNodeIcon_)), grid_(grid) { - grid.getMainWin().Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onKeyDown(event); }); + grid.Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onKeyDown(event); }); grid.Bind(EVENT_GRID_MOUSE_LEFT_DOWN, [this](GridClickEvent& event) { onMouseLeft (event); }); grid.Bind(EVENT_GRID_MOUSE_LEFT_DOUBLE, [this](GridClickEvent& event) { onMouseLeftDouble(event); }); grid.Bind(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, [this](GridLabelClickEvent& event) { onGridLabelContext (event); }); @@ -798,6 +798,7 @@ private: enum class HoverAreaTree { node, + item, }; void renderCell(wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover) override @@ -819,13 +820,6 @@ private: { if (std::unique_ptr<TreeView::Node> node = getDataView().getLine(row)) { - ////clear first section: - //clearArea(dc, wxRect(rect.GetTopLeft(), wxSize( - // node->level_ * widthLevelStep_ + gapSize_ + //width - // (showPercentBar ? percentageBarWidth_ + 2 * gapSize_ : 0) + // - // widthNodeStatus_ + gapSize_ + widthNodeIcon + gapSize_, // - // rect.height)), wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); - auto drawIcon = [&](wxImage icon, const wxRect& rectIcon, bool drawActive) { if (!drawActive) @@ -900,6 +894,9 @@ private: drawIcon(nodeIcon, rectTmp, isActive); + if (static_cast<HoverAreaTree>(rowHover) == HoverAreaTree::item) + drawInsetRectangle(dc, rectTmp, fastFromDIP(1), *wxBLUE); + rectTmp.x += widthNodeIcon_ + gapSize_; rectTmp.width -= widthNodeIcon_ + gapSize_; @@ -956,26 +953,18 @@ private: HoverArea getMouseHover(wxDC& dc, size_t row, ColumnType colType, int cellRelativePosX, int cellWidth) override { - switch (static_cast<ColumnTypeOverview>(colType)) - { - case ColumnTypeOverview::folder: - if (std::unique_ptr<TreeView::Node> node = getDataView().getLine(row)) - { - const int tolerance = 2; - const int nodeStatusXFirst = -tolerance + static_cast<int>(node->level_) * widthLevelStep_ + gapSize_ + (showPercentBar_ ? percentageBarWidth_ + 2 * gapSize_ : 0); - const int nodeStatusXLast = (nodeStatusXFirst + tolerance) + widthNodeStatus_ + tolerance; - // -> synchronize renderCell() <-> getBestSize() <-> getMouseHover() - - if (nodeStatusXFirst <= cellRelativePosX && cellRelativePosX < nodeStatusXLast) - return static_cast<HoverArea>(HoverAreaTree::node); - } - break; + if (static_cast<ColumnTypeOverview>(colType) == ColumnTypeOverview::folder) + if (std::unique_ptr<TreeView::Node> node = getDataView().getLine(row)) + { + const int nodeStatusXFirst = static_cast<int>(node->level_) * widthLevelStep_ + gapSize_ + (showPercentBar_ ? percentageBarWidth_ + 2 * gapSize_ : 0); + const int nodeStatusXLast = nodeStatusXFirst + widthNodeStatus_; + // -> synchronize renderCell() <-> getBestSize() <-> getMouseHover() - case ColumnTypeOverview::itemCount: - case ColumnTypeOverview::bytes: - break; - } - return HoverArea::none; + const int tolerance = fastFromDIP(5); + if (nodeStatusXFirst - tolerance <= cellRelativePosX && cellRelativePosX < nodeStatusXLast + tolerance) + return static_cast<HoverArea>(HoverAreaTree::node); + } + return static_cast<HoverArea>(HoverAreaTree::item); } std::wstring getColumnLabel(ColumnType colType) const override @@ -1007,6 +996,8 @@ private: break; } break; + case HoverAreaTree::item: + break; } event.Skip(); } diff --git a/FreeFileSync/Source/ui/version_check.cpp b/FreeFileSync/Source/ui/version_check.cpp index 543c9510..29b5ed2f 100644 --- a/FreeFileSync/Source/ui/version_check.cpp +++ b/FreeFileSync/Source/ui/version_check.cpp @@ -142,9 +142,9 @@ std::vector<std::pair<std::string, std::string>> geHttpPostParameters(wxWindow& const char* osArch = cpuArchName; params.emplace_back("os_arch", osArch); -#if GTK_MAJOR_VERSION == 2 +#ifdef __WXGTK2__ //wxWindow::GetContentScaleFactor() requires GTK3 or later -#elif GTK_MAJOR_VERSION == 3 +#elif defined __WXGTK3__ params.emplace_back("dip_scale", numberTo<std::string>(parent.GetContentScaleFactor())); #else #error unknown GTK version! diff --git a/FreeFileSync/Source/version/version.h b/FreeFileSync/Source/version/version.h index 4e3f0c59..87e6db77 100644 --- a/FreeFileSync/Source/version/version.h +++ b/FreeFileSync/Source/version/version.h @@ -3,7 +3,7 @@ namespace fff { -const char ffsVersion[] = "12.0"; //internal linkage! +const char ffsVersion[] = "12.1"; //internal linkage! const char FFS_VERSION_SEPARATOR = '.'; } diff --git a/libcurl/curl_wrap.cpp b/libcurl/curl_wrap.cpp index f9a60233..1d72dcac 100644 --- a/libcurl/curl_wrap.cpp +++ b/libcurl/curl_wrap.cpp @@ -223,11 +223,11 @@ HttpSession::Result HttpSession::perform(const std::string& serverRelPath, //Contradicting options: CURLOPT_READFUNCTION, CURLOPT_POSTFIELDS: if (std::any_of(extraOptions.begin(), extraOptions.end(), [](const CurlOption& o) { return o.option == CURLOPT_POSTFIELDS; })) - /**/ throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); + /**/ throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); } if (std::any_of(extraOptions.begin(), extraOptions.end(), [](const CurlOption& o) { return o.option == CURLOPT_WRITEFUNCTION || o.option == CURLOPT_READFUNCTION; })) - /**/ throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); //Option already used here! + /**/ throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); //Option already used here! //--------------------------------------------------- curl_slist* headers = nullptr; //"libcurl will not copy the entire list so you must keep it!" @@ -13,7 +13,7 @@ #include <wx/dcbuffer.h> //for macro: wxALWAYS_NATIVE_DOUBLE_BUFFER #include <wx/dcscreen.h> #include <wx/bmpbndl.h> - #include <gtk/gtk.h> +// #include <gtk/gtk.h> namespace zen diff --git a/wx+/grid.cpp b/wx+/grid.cpp index f997d72c..306d4847 100644 --- a/wx+/grid.cpp +++ b/wx+/grid.cpp @@ -152,7 +152,7 @@ void GridData::drawCellText(wxDC& dc, const wxRect& rect, const std::wstring& te - wxDC::DrawText also calls wxDC::GetTextExtent()!! => wxDC::DrawLabel() boils down to 3(!) calls to wxDC::GetTextExtent()!!! - wxDC::DrawLabel results in GetTextExtent() call even for empty strings!!! - => skip the wxDC::DrawLabel() cruft and directly call wxDC::DrawText()! */ + => NEVER EVER call wxDC::DrawLabel() cruft and directly call wxDC::DrawText()! */ assert(!contains(text, L'\n')); if (rect.width <= 0 || rect.height <= 0 || text.empty()) return; @@ -284,7 +284,11 @@ public: Bind(wxEVT_MOUSEWHEEL, [this](wxMouseEvent& event) { onMouseWheel (event); }); Bind(wxEVT_MOUSE_CAPTURE_LOST, [this](wxMouseCaptureLostEvent& event) { onMouseCaptureLost(event); }); - Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onKeyDown(event); }); + Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) + { + if (!sendEventToParent(event)) //let parent collect all key events + event.Skip(); + }); //Bind(wxEVT_KEY_UP, [this](wxKeyEvent& event) { onKeyUp (event); }); -> superfluous? assert(GetClientAreaOrigin() == wxPoint()); //generally assumed when dealing with coordinates below @@ -332,12 +336,6 @@ private: virtual void onLeaveWindow (wxMouseEvent& event) { event.Skip(); } virtual void onMouseCaptureLost(wxMouseCaptureLostEvent& event) { event.Skip(); } - void onKeyDown(wxKeyEvent& event) - { - if (!sendEventToParent(event)) //let parent collect all key events - event.Skip(); - } - void onMouseWheel(wxMouseEvent& event) { /* MSDN, WM_MOUSEWHEEL: "Sent to the focus window when the mouse wheel is rotated. @@ -816,13 +814,6 @@ private: void onMouseRightDown(wxMouseEvent& event) override { evalMouseMovement(event.GetPosition()); //update highlight in obscure cases (e.g. right-click while other context menu is open) - freezeMouseHighlight_ = true; //e.g. while showing context menu - - ZEN_ON_SCOPE_EXIT( - freezeMouseHighlight_ = false; - //update mouse highlight (e.g. mouse position changed after showing context menu) - evalMouseMovement(ScreenToClient(wxGetMousePosition())); - ); const wxPoint mousePos = GetPosition() + event.GetPosition(); @@ -837,6 +828,9 @@ private: if (fillGapAfterColumns) sendEventToParent(GridLabelClickEvent(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, ColumnType::none, mousePos)); + //update mouse highlight (e.g. mouse position changed after showing context menu) => needed on Linux/macOS + evalMouseMovement(ScreenToClient(wxGetMousePosition())); + event.Skip(); } @@ -883,7 +877,7 @@ private: if (action->wantResize) SetCursor(wxCURSOR_SIZEWE); //window-local only! :) else - SetCursor(*wxSTANDARD_CURSOR); + SetCursor(*wxSTANDARD_CURSOR); //NOOP when setting same cursor } else { @@ -926,7 +920,7 @@ private: void setMouseHighlight(const std::optional<size_t>& hl) { - if (highlightCol_ != hl && !freezeMouseHighlight_) + if (highlightCol_ != hl) { highlightCol_ = hl; Refresh(); @@ -935,8 +929,7 @@ private: std::unique_ptr<ColumnResizing> activeResizing_; std::unique_ptr<ColumnMove> activeClickOrMove_; - std::optional<size_t> highlightCol_; //column during mouse-over - bool freezeMouseHighlight_ = false; + std::optional<size_t> highlightCol_; int colLabelHeight_ = 0; const wxFont labelFont_; @@ -966,9 +959,19 @@ public: { wxMouseCaptureLostEvent evt; GetEventHandler()->ProcessEvent(evt); //better integrate into event handling rather than calling onMouseCaptureLost() directly!? + return; } - else - event.Skip(); + + /* using keyboard: => clear distracting mouse highlights + + wxEVT_KEY_DOWN evaluation order: + 1. this callback + 2. Grid::SubWindow ... sendEventToParent() + 3. clients binding to Grid wxEVT_KEY_DOWN + 4. Grid::onKeyDown() */ + setMouseHighlight(std::nullopt); + + event.Skip(); }); } @@ -1050,8 +1053,11 @@ private: if (activeSelection_->getFirstClick().row_ == row) return activeSelection_->getFirstClick().hoverArea_; } - else if (highlight_.row == row) - return highlight_.rowHover; + else if (highlight_) + { + if (makeSigned(highlight_->row) == row) + return highlight_->rowHover; + } return HoverArea::none; } @@ -1110,13 +1116,6 @@ private: if (auto prov = refParent().getDataProvider()) { evalMouseMovement(event.GetPosition()); //update highlight in obscure cases (e.g. right-click while other context menu is open) - freezeMouseHighlight_ = true; //e.g. while showing context menu - - ZEN_ON_SCOPE_EXIT( - freezeMouseHighlight_ = false; - //update mouse highlight (e.g. mouse position changed after showing context menu) - evalMouseMovement(ScreenToClient(wxGetMousePosition())); - ); const wxPoint mousePos = GetPosition() + event.GetPosition(); const ptrdiff_t rowCount = refParent().getRowCount(); @@ -1166,6 +1165,9 @@ private: } } } + + //update mouse highlight (e.g. mouse position changed after showing context menu) => needed on Linux/macOS + evalMouseMovement(ScreenToClient(wxGetMousePosition())); } event.Skip(); //allow changing focus } @@ -1272,7 +1274,7 @@ private: if (activeSelection_) activeSelection_->evalMousePos(); //call on both mouse movement + timer event! else - setMouseHighlight({row, rowHover}); + setMouseHighlight(rowHover != HoverArea::none ? std::make_optional<MouseHighlight>({static_cast<size_t>(row), rowHover}) : std::nullopt); } } @@ -1286,14 +1288,14 @@ private: activeSelection_.reset(); Refresh(); } - setMouseHighlight({-1, HoverArea::none}); + setMouseHighlight(std::nullopt); //event.Skip(); -> we DID handle it! } void onLeaveWindow(wxMouseEvent& event) override { if (!activeSelection_) //wxEVT_LEAVE_WINDOW does not respect mouse capture! - setMouseHighlight({-1, HoverArea::none}); + setMouseHighlight(std::nullopt); //CAVEAT: we can get wxEVT_MOTION *after* wxEVT_LEAVE_WINDOW: see RowLabelWin::redirectMouseEvent() // => therefore we also redirect wxEVT_LEAVE_WINDOW, but user will see a little flicker when moving between RowLabelWin and MainWin @@ -1433,33 +1435,33 @@ private: struct MouseHighlight { - ptrdiff_t row = -1; + size_t row = 0; HoverArea rowHover = HoverArea::none; bool operator==(const MouseHighlight&) const = default; }; - void setMouseHighlight(const MouseHighlight& hl) + void setMouseHighlight(const std::optional<MouseHighlight>& hl) { - if (highlight_ != hl && !freezeMouseHighlight_) + assert(!hl || hl->row < refParent().getRowCount() && hl->rowHover != HoverArea::none); + if (highlight_ != hl) { - const ptrdiff_t rowCount = refParent().getRowCount(); - if (0 <= highlight_.row && highlight_.row < rowCount && highlight_.rowHover != HoverArea::none) //no highlight_? => NOP! - refreshRow(highlight_.row); + if (highlight_) + refreshRow(highlight_->row); highlight_ = hl; - if (0 <= highlight_.row && highlight_.row < rowCount && highlight_.rowHover != HoverArea::none) //no highlight_? => NOP! - refreshRow(highlight_.row); + if (highlight_) + refreshRow(highlight_->row); } } + RowLabelWin& rowLabelWin_; ColLabelWin& colLabelWin_; std::unique_ptr<MouseSelection> activeSelection_; //bound while user is selecting with mouse - MouseHighlight highlight_; //current mouse highlight - bool freezeMouseHighlight_ = false; + std::optional<MouseHighlight> highlight_; size_t cursorRow_ = 0; size_t selectionAnchor_ = 0; @@ -1736,10 +1738,7 @@ void Grid::onKeyDown(wxKeyEvent& event) auto moveCursorTo = [&](ptrdiff_t row) { if (rowCount > 0) - { - row = std::clamp<ptrdiff_t>(row, 0, rowCount - 1); - setGridCursor(row, GridEventPolicy::allow); - } + setGridCursor(std::clamp<ptrdiff_t>(row, 0, rowCount - 1), GridEventPolicy::allow); }; auto selectWithCursorTo = [&](ptrdiff_t row) diff --git a/wx+/no_flicker.h b/wx+/no_flicker.h index 1c91bd48..9f91bbdb 100644 --- a/wx+/no_flicker.h +++ b/wx+/no_flicker.h @@ -55,9 +55,8 @@ void setTextWithUrls(wxRichTextCtrl& richCtrl, const wxString& newText) for (auto it = newText.begin();;) { - const wchar_t urlPrefix[] = L"https://"; - const auto itUrl = std::search(it, newText.end(), - urlPrefix, urlPrefix + strLength(urlPrefix)); + const std::wstring_view urlPrefix = L"https://"; + const auto itUrl = std::search(it, newText.end(), urlPrefix.begin(), urlPrefix.end()); if (it != itUrl) blocks.emplace_back(BlockType::text, wxString(it, itUrl)); diff --git a/wx+/popup_dlg.cpp b/wx+/popup_dlg.cpp index c30426cb..b5a194ad 100644 --- a/wx+/popup_dlg.cpp +++ b/wx+/popup_dlg.cpp @@ -13,10 +13,9 @@ #include "app_main.h" #include "bitmap_button.h" #include "no_flicker.h" -#include "font_size.h" +#include "window_layout.h" #include "image_resources.h" #include "popup_dlg_generated.h" -#include "std_button_layout.h" #include "taskbar.h" #include "window_tools.h" @@ -273,12 +272,17 @@ public: //set std order after button visibility was set setStandardButtonLayout(*bSizerStdButtons, stdBtns); - updateGui(); + GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() +#ifdef __WXGTK3__ + Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 + Hide(); //avoid old position flash when Center() moves window (asynchronously?) +#endif Center(); //needs to be re-applied after a dialog size change! + Raise(); //[!] popup may be triggered by ffs_batch job running in the background! if (m_buttonAccept->IsEnabled()) diff --git a/wx+/tooltip.cpp b/wx+/tooltip.cpp index c56d80a1..5ad5da31 100644 --- a/wx+/tooltip.cpp +++ b/wx+/tooltip.cpp @@ -13,7 +13,6 @@ #include "image_tools.h" #include "bitmap_button.h" #include "dc.h" - #include <gtk/gtk.h> using namespace zen; @@ -77,11 +76,12 @@ void Tooltip::show(const wxString& text, wxPoint mousePos, const wxImage* img) } if (imgChanged || txtChanged) - { //tipWindow_->Layout(); -> apparently not needed!? tipWindow_->GetSizer()->SetSizeHints(tipWindow_); //~=Fit() + SetMinSize() - //Linux: Fit() seems to be broken => call EVERY time inside show, not only if text or bmp change -> still true?!? - } +#ifdef __WXGTK3__ + //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 + //=> call wxWindow::Show() to "execute" +#endif const wxPoint newPos = wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft ? mousePos - wxPoint(fastFromDIP(TIP_WINDOW_OFFSET_DIP) + tipWindow_->GetSize().GetWidth(), 0) : @@ -101,16 +101,13 @@ void Tooltip::hide() { if (tipWindow_) { -#if GTK_MAJOR_VERSION == 2 //the tooltip sometimes turns blank or is not shown again after it was hidden: e.g. drag-selection on middle grid +#ifdef __WXGTK2__ //the tooltip sometimes turns blank or is not shown again after it was hidden: e.g. drag-selection on middle grid + //=> no such issues on GTK3! tipWindow_->Destroy(); //apply brute force: tipWindow_ = nullptr; // lastUsedImg_ = wxNullImage; - -#elif GTK_MAJOR_VERSION == 3 - tipWindow_->Hide(); #else -#error unknown GTK version! + tipWindow_->Hide(); #endif - } } diff --git a/wx+/font_size.h b/wx+/window_layout.h index da74eada..8a86ec86 100644 --- a/wx+/font_size.h +++ b/wx+/window_layout.h @@ -4,12 +4,14 @@ // * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * // ***************************************************************************** -#ifndef FONT_SIZE_H_23849632846734343234532 -#define FONT_SIZE_H_23849632846734343234532 +#ifndef WINDOW_LAYOUT_H_23849632846734343234532 +#define WINDOW_LAYOUT_H_23849632846734343234532 #include <zen/basic_math.h> #include <wx/window.h> +#include <wx/spinctrl.h> #include <zen/scope_guard.h> + #include <gtk/gtk.h> #include "dc.h" @@ -19,7 +21,7 @@ namespace zen void setRelativeFontSize(wxWindow& control, double factor); void setMainInstructionFont(wxWindow& control); //following Windows/Gnome/OS X guidelines - +void setDefaultWidth(wxSpinCtrl& m_spinCtrl); @@ -47,6 +49,37 @@ void setMainInstructionFont(wxWindow& control) control.SetFont(font); } + + +inline +void setDefaultWidth(wxSpinCtrl& m_spinCtrl) +{ +#ifdef __WXGTK3__ + //there's no way to set width using GTK's CSS! => + m_spinCtrl.InvalidateBestSize(); + ::gtk_entry_set_width_chars(GTK_ENTRY(m_spinCtrl.m_widget), 3); + +#if 0 //apparently not needed!? + if (::gtk_check_version(3, 12, 0) == NULL) + ::gtk_entry_set_max_width_chars(GTK_ENTRY(m_spinCtrl.m_widget), 3); +#endif + + //get rid of excessive default width on old GTK3 3.14 (Debian); + //gtk_entry_set_width_chars() not working => mitigate + m_spinCtrl.SetMinSize({fastFromDIP(100), -1}); //must be wider than gtk_entry_set_width_chars(), or it breaks newer GTK e.g. 3.22! + +#if 0 //generic property syntax: + GValue bval = G_VALUE_INIT; + ::g_value_init(&bval, G_TYPE_BOOLEAN); + ::g_value_set_boolean(&bval, false); + ZEN_ON_SCOPE_EXIT(::g_value_unset(&bval)); + ::g_object_set_property(G_OBJECT(m_spinCtrl.m_widget), "visibility", &bval); +#endif +#else + m_spinCtrl.SetMinSize({fastFromDIP(70), -1}); +#endif + +} } -#endif //FONT_SIZE_H_23849632846734343234532 +#endif //WINDOW_LAYOUT_H_23849632846734343234532 diff --git a/wx+/window_tools.h b/wx+/window_tools.h index 73faf272..179508f8 100644 --- a/wx+/window_tools.h +++ b/wx+/window_tools.h @@ -92,96 +92,172 @@ private: namespace { -void setInitialWindowSize(wxTopLevelWindow& topWin, wxSize size, std::optional<wxPoint> pos, bool isMaximized, wxSize defaultSize) +class WindowLayout { - wxSize newSize = defaultSize; - std::optional<wxPoint> newPos; - //set dialog size and position: - // - width/height are invalid if the window is minimized (eg x,y = -32000; width = 160, height = 28) - // - multi-monitor setup: dialog may be placed on second monitor which is currently turned off - if (size.GetWidth () > 0 && - size.GetHeight() > 0) +public: + struct Layout { - if (pos) + std::optional<wxSize> size; + std::optional<wxPoint> pos; + bool isMaximized = false; + }; + static void setInitial(wxTopLevelWindow& topWin, const Layout& layout, wxSize defaultSize) + { + initialLayouts_[&topWin] = layout; + + wxSize newSize = defaultSize; + std::optional<wxPoint> newPos; + //set dialog size and position: + // - width/height are invalid if the window is minimized (eg x,y = -32000; width = 160, height = 28) + // - multi-monitor setup: dialog may be placed on second monitor which is currently turned off + if (layout.size && + layout.size->GetWidth () > 0 && + layout.size->GetHeight() > 0) { - //calculate how much of the dialog will be visible on screen - const int dlgArea = size.GetWidth() * size.GetHeight(); - int dlgAreaMaxVisible = 0; + if (layout.pos) + { + //calculate how much of the dialog will be visible on screen + const int dlgArea = layout.size->GetWidth() * layout.size->GetHeight(); + int dlgAreaMaxVisible = 0; + + const int monitorCount = wxDisplay::GetCount(); + for (int i = 0; i < monitorCount; ++i) + { + wxRect overlap = wxDisplay(i).GetClientArea().Intersect(wxRect(*layout.pos, *layout.size)); + dlgAreaMaxVisible = std::max(dlgAreaMaxVisible, overlap.GetWidth() * overlap.GetHeight()); + } + + if (dlgAreaMaxVisible > 0.1 * dlgArea //at least 10% of the dialog should be visible! + ) + { + newSize = *layout.size; + newPos = layout.pos; + } + } + else + newSize = *layout.size; + } - const int monitorCount = wxDisplay::GetCount(); - for (int i = 0; i < monitorCount; ++i) + //old comment: "wxGTK's wxWindow::SetSize seems unreliable and behaves like a wxWindow::SetClientSize + // => use wxWindow::SetClientSize instead (for the record: no such issue on Windows/macOS) + //2018-10-15: Weird new problem on CentOS/Ubuntu: SetClientSize() + SetPosition() fail to set correct dialog *position*, but SetSize() + SetPosition() do! + // => old issues with SetSize() seem to be gone... => revert to SetSize() + if (newPos) + topWin.SetSize(wxRect(*newPos, newSize)); + else + { + topWin.SetSize(newSize); + topWin.Center(); + } + + if (layout.isMaximized) //no real need to support both maximize and full screen functions + { + topWin.Maximize(true); + } + + +#if 0 //wxWidgets alternative: apparently no benefits (not even on Wayland! but strange decisions: why restore the minimized state!???) + class GeoSerializer : public wxTopLevelWindow::GeometrySerializer + { + public: + GeoSerializer(const std::string& l) { - wxRect overlap = wxDisplay(i).GetClientArea().Intersect(wxRect(*pos, size)); - dlgAreaMaxVisible = std::max(dlgAreaMaxVisible, overlap.GetWidth() * overlap.GetHeight()); + split(l, ' ', [&](const std::string_view phrase) + { + assert(phrase.empty() || contains(phrase, '=')); + if (contains(phrase, '=')) + valuesByName_[utfTo<wxString>(beforeFirst(phrase, '=', IfNotFoundReturn::none))] = + /**/ stringTo<int>(afterFirst(phrase, '=', IfNotFoundReturn::none)); + }); } - if (dlgAreaMaxVisible > 0.1 * dlgArea //at least 10% of the dialog should be visible! - ) + bool SaveField(const wxString& name, int value) const /*NO, this must not be const!*/ override { return false; } + + bool RestoreField(const wxString& name, int* value) /*const: yes, this MAY(!) be const*/ override { - newSize = size; - newPos = pos; + auto it = valuesByName_.find(name); + if (it == valuesByName_.end()) + return false; + * value = it->second; + return true; } - } - else - newSize = size; - } + private: + std::unordered_map<wxString, int> valuesByName_; + } serializer(layout); - //old comment: "wxGTK's wxWindow::SetSize seems unreliable and behaves like a wxWindow::SetClientSize - // => use wxWindow::SetClientSize instead (for the record: no such issue on Windows/macOS) - //2018-10-15: Weird new problem on CentOS/Ubuntu: SetClientSize() + SetPosition() fail to set correct dialog *position*, but SetSize() + SetPosition() do! - // => old issues with SetSize() seem to be gone... => revert to SetSize() - if (newPos) - topWin.SetSize(wxRect(*newPos, newSize)); - else - { - topWin.SetSize(newSize); - topWin.Center(); + if (!topWin.RestoreToGeometry(serializer)) //apparently no-fail as long as GeometrySerializer::RestoreField is! + assert(false); +#endif } - if (isMaximized) //no real need to support both maximize and full screen functions + //destructive! changes window size! + static Layout getBeforeClose(wxTopLevelWindow& topWin) { - topWin.Maximize(true); - } -} - + //we need to portably retrieve non-iconized, non-maximized size and position + // non-portable: Win32 GetWindowPlacement(); wxWidgets take: wxTopLevelWindow::SaveGeometry/RestoreToGeometry() + if (topWin.IsIconized()) + topWin.Iconize(false); -struct WindowLayoutWeak -{ - std::optional<wxSize> size; - std::optional<wxPoint> pos; - bool isMaximized = false; -}; -//destructive! changes window size! -WindowLayoutWeak getWindowSizeBeforeClose(wxTopLevelWindow& topWin) -{ - //we need to portably retrieve non-iconized, non-maximized size and position - // non-portable: Win32 GetWindowPlacement(); wxWidgets take: wxTopLevelWindow::RestoreToGeometry() - if (topWin.IsIconized()) - topWin.Iconize(false); + bool isMaximized = false; + if (topWin.IsMaximized()) //evaluate AFTER uniconizing! + { + topWin.Maximize(false); + isMaximized = true; + } - WindowLayoutWeak layout; - if (topWin.IsMaximized()) //evaluate AFTER uniconizing! - { - topWin.Maximize(false); - layout.isMaximized = true; - } + std::optional<wxSize> size = topWin.GetSize(); + std::optional<wxPoint> pos = topWin.GetPosition(); - layout.size = topWin.GetSize(); - layout.pos = topWin.GetPosition(); + if (isMaximized) + if (!topWin.IsShown() //=> Win: can't trust size GetSize()/GetPosition(): still at full screen size! + //wxGTK: returns full screen size and strange position (65/-4) + //OS X 10.9 (but NO issue on 10.11!) returns full screen size and strange position (0/-22) + || pos->y < 0 + ) + { + size = std::nullopt; + pos = std::nullopt; + } - if (layout.isMaximized) - if (!topWin.IsShown() //=> Win: can't trust size GetSize()/GetPosition(): still at full screen size! - //wxGTK: returns full screen size and strange position (65/-4) - //OS X 10.9 (but NO issue on 10.11!) returns full screen size and strange position (0/-22) - || layout.pos->y < 0 - ) + //reuse previous values if current ones are not available: + if (const auto it = initialLayouts_.find(&topWin); + it != initialLayouts_.end()) { - layout.size = std::nullopt; - layout.pos = std::nullopt; + if (!size) + size = it->second.size; + + if (!pos) + pos = it->second.pos; } - return layout; -} + return {size, pos, isMaximized}; + +#if 0 //wxWidgets alternative: apparently no benefits (not even on Wayland! but strange decisions: why restore the minimized state!???) + struct : wxTopLevelWindow::GeometrySerializer + { + bool SaveField(const wxString& name, int value) const /*NO, this must not be const!*/ override + { + layout_ += utfTo<std::string>(name) + '=' + numberTo<std::string>(value) + ' '; + return true; + } + + bool RestoreField(const wxString& name, int* value) /*const: yes, this MAY(!) be const*/ override { return false; } + + mutable //wxWidgets people: 1. learn when and when not to use const for input/output functions! see SaveField/RestoreField() + // 2. learn flexible software design: why are input/output tied up in a single GeometrySerializer implementation? + std::string layout_; + } serializer; + + if (topWin.SaveGeometry(serializer)) //apparently no-fail as long as GeometrySerializer::SaveField is! + return serializer.layout_; + else + assert(false); +#endif + } + +private: + inline static std::unordered_map<const wxTopLevelWindow* /*don't access! use as key only!*/, Layout> initialLayouts_; +}; } } diff --git a/xBRZ/src/xbrz.cpp b/xBRZ/src/xbrz.cpp index 448a4b74..6d2b7325 100644 --- a/xBRZ/src/xbrz.cpp +++ b/xBRZ/src/xbrz.cpp @@ -406,7 +406,7 @@ void blendPixel(const Kernel_3x3& ker, return true; //make sure there is no second blending in an adjacent rotation for this pixel: handles insular pixels, mario eyes - if (getTopR(blend) != BLEND_NONE && !eq(e, g)) //but support double-blending for 90° corners + if (getTopR(blend) != BLEND_NONE && !eq(e, g)) //but support double-blending for 90° corners return false; if (getBottomL(blend) != BLEND_NONE && !eq(e, c)) return false; diff --git a/zen/argon2.cpp b/zen/argon2.cpp index d78dc26c..f48abe5e 100644 --- a/zen/argon2.cpp +++ b/zen/argon2.cpp @@ -4,9 +4,9 @@ // * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * // ***************************************************************************** -/* The code in this file, except for zen::zargon2() is from: +/* The code in this file, except for zen::zargon2(), is from PuTTY: - PuTTY is copyright 1997-2022 Simon Tatham. + PuTTY is copyright 1997-2022 Simon Tatham. Portions copyright Robert de Bath, Joris van Rantwijk, Delian Delchev, Andreas Schultz, Jeroen Massar, Wez Furlong, Nicolas Barry, @@ -972,7 +972,7 @@ std::string zen::zargon2(zen::Argon2Flavor flavour, uint32_t mem, uint32_t passe std::string output(taglen, '\0'); argon2_internal(parallel, taglen, mem, passes, static_cast<uint32_t>(flavour), {.ptr = password.data(), .len = password.size()}, - {.ptr = salt.data(), .len = salt.size()}, + {.ptr = salt .data(), .len = salt .size()}, {.ptr = "", .len = 0}, {.ptr = "", .len = 0}, reinterpret_cast<uint8_t*>(output.data())); return output; diff --git a/zen/basic_math.h b/zen/basic_math.h index 7258128f..77ce7b7e 100644 --- a/zen/basic_math.h +++ b/zen/basic_math.h @@ -29,8 +29,8 @@ template <class N, class D> auto intDivFloor(N numerator, D denominator); template <size_t N, class T> T power(T value); -double radToDeg(double rad); //convert unit [rad] into [°] -double degToRad(double degree); //convert unit [°] into [rad] +double radToDeg(double rad); //convert unit [rad] into [°] +double degToRad(double degree); //convert unit [°] into [rad] template <class InputIterator> double arithmeticMean(InputIterator first, InputIterator last); @@ -84,13 +84,13 @@ std::pair<InputIterator, InputIterator> minMaxElement(InputIterator first, Input { //by factor 1.5 to 3 faster than boost::minmax_element (=two-step algorithm) for built-in types! - InputIterator lowest = first; - InputIterator largest = first; + InputIterator itMin = first; + InputIterator itMax = first; if (first != last) { - auto minVal = *lowest; //nice speedup on 64 bit! - auto maxVal = *largest; // + auto minVal = *itMin; //nice speedup on 64 bit! + auto maxVal = *itMax; // for (;;) { ++first; @@ -100,17 +100,17 @@ std::pair<InputIterator, InputIterator> minMaxElement(InputIterator first, Input if (compLess(maxVal, val)) { - largest = first; + itMax = first; maxVal = val; } else if (compLess(val, minVal)) { - lowest = first; + itMin = first; minVal = val; } } } - return {lowest, largest}; + return {itMin, itMax}; } @@ -143,7 +143,7 @@ auto roundToGrid(T val, InputIterator first, InputIterator last) template <class T> inline bool isNull(T value) { - return abs(value) <= std::numeric_limits<T>::epsilon(); //epsilon is 0 für integral types => less-equal + return abs(value) <= std::numeric_limits<T>::epsilon(); //epsilon is 0 für integral types => less-equal } diff --git a/zen/file_access.cpp b/zen/file_access.cpp index d06202ba..ef6cdc80 100644 --- a/zen/file_access.cpp +++ b/zen/file_access.cpp @@ -157,11 +157,15 @@ int64_t zen::getFreeDiskSpace(const Zstring& folderPath) //throw FileError uint64_t zen::getFileSize(const Zstring& filePath) //throw FileError { - struct stat fileInfo = {}; - if (::stat(filePath.c_str(), &fileInfo) != 0) - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(filePath)), "stat"); + try + { + struct stat fileInfo = {}; + if (::stat(filePath.c_str(), &fileInfo) != 0) + THROW_LAST_SYS_ERROR("stat"); - return fileInfo.st_size; + return fileInfo.st_size; + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(filePath)), e.toString()); } } @@ -169,8 +173,8 @@ uint64_t zen::getFileSize(const Zstring& filePath) //throw FileError Zstring zen::getTempFolderPath() //throw FileError { - if (const char* tempPath = ::getenv("TMPDIR")) //no extended error reporting - return tempPath; + if (const std::optional<Zstring> tempDirPath = getEnvironmentVar("TMPDIR")) + return *tempDirPath; //TMPDIR not set on CentOS 7, WTF! return P_tmpdir; //usually resolves to "/tmp" } @@ -268,20 +272,7 @@ namespace //wrapper for file system rename function: void moveAndRenameFileSub(const Zstring& pathFrom, const Zstring& pathTo, bool replaceExisting) //throw FileError, ErrorMoveUnsupported, ErrorTargetExisting { - auto throwException = [&](int ec) - { - const std::wstring errorMsg = replaceCpy(replaceCpy(_("Cannot move file %x to %y."), L"%x", L'\n' + fmtPath(pathFrom)), L"%y", L'\n' + fmtPath(pathTo)); - const std::wstring errorDescr = formatSystemError("rename", ec); - - if (ec == EXDEV) - throw ErrorMoveUnsupported(errorMsg, errorDescr); - - assert(!replaceExisting || ec != EEXIST); - if (!replaceExisting && ec == EEXIST) - throw ErrorTargetExisting(errorMsg, errorDescr); - - throw FileError(errorMsg, errorDescr); - }; + auto getErrorMsg = [&] { return replaceCpy(replaceCpy(_("Cannot move file %x to %y."), L"%x", L'\n' + fmtPath(pathFrom)), L"%y", L'\n' + fmtPath(pathTo)); }; //rename() will never fail with EEXIST, but always (atomically) overwrite! //=> equivalent to SetFileInformationByHandle() + FILE_RENAME_INFO::ReplaceIfExists or ::MoveFileEx() + MOVEFILE_REPLACE_EXISTING @@ -291,22 +282,31 @@ void moveAndRenameFileSub(const Zstring& pathFrom, const Zstring& pathTo, bool r { struct stat sourceInfo = {}; if (::lstat(pathFrom.c_str(), &sourceInfo) != 0) - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(pathFrom)), "stat"); + throw FileError(getErrorMsg(), formatSystemError("lstat(source)", errno)); struct stat targetInfo = {}; - if (::lstat(pathTo.c_str(), &targetInfo) == 0) + if (::lstat(pathTo.c_str(), &targetInfo) != 0) + { + if (errno != ENOENT) + throw FileError(getErrorMsg(), formatSystemError("lstat(target)", errno)); + } + else { if (sourceInfo.st_dev != targetInfo.st_dev || sourceInfo.st_ino != targetInfo.st_ino) - throwException(EEXIST); //that's what we're really here for + throw ErrorTargetExisting(getErrorMsg(), replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(getItemName(pathTo)))); //else: continue with a rename in case //caveat: if we have a hardlink referenced by two different paths, the source one will be unlinked => fine, but not exactly a "rename"... } - //else: not existing or access error (hopefully ::rename will also fail!) } if (::rename(pathFrom.c_str(), pathTo.c_str()) != 0) - throwException(errno); + { + if (errno == EXDEV) + throw ErrorMoveUnsupported(getErrorMsg(), formatSystemError("rename", errno)); + + throw FileError(getErrorMsg(), formatSystemError("rename", errno)); + } } @@ -661,7 +661,7 @@ FileCopyResult zen::copyNewFile(const Zstring& sourceFile, const Zstring& target //close output file handle before setting file time; also good place to catch errors when closing stream! fileOut.close(); //throw FileError //========================================================================================================== - //take over fileOut ownership => from this point on, WE are responsible for calling removeFilePlain() on failure!! + //take over fileOut ownership => from this point on, WE are responsible for calling removeFilePlain() on error!! // not needed *currently*! see below: ZEN_ON_SCOPE_FAIL(try { removeFilePlain(targetFile); } catch (FileError&) {}); //=========================================================================================================== std::optional<FileError> errorModTime; diff --git a/zen/file_io.cpp b/zen/file_io.cpp index 910b75e7..2e4ab60a 100644 --- a/zen/file_io.cpp +++ b/zen/file_io.cpp @@ -71,7 +71,7 @@ void FileBase::close() //throw FileError throw SysError(L"Contract error: close() called more than once."); if (::close(hFile_) != 0) THROW_LAST_SYS_ERROR("close"); - hFile_ = invalidFileHandle; //do NOT set on failure! => ~FileOutputPlain() still wants to (try to) delete the file! + hFile_ = invalidFileHandle; //do NOT set on error! => ~FileOutputPlain() still wants to (try to) delete the file! } catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(getFilePath())), e.toString()); } } @@ -153,7 +153,7 @@ FileInputPlain::FileInputPlain(FileHandle handle, const Zstring& filePath) : size_t FileInputPlain::tryRead(void* buffer, size_t bytesToRead) //throw FileError, ErrorFileLocked { if (bytesToRead == 0) //"read() with a count of 0 returns zero" => indistinguishable from end of file! => check! - throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); assert(bytesToRead % getBlockSize() == 0); try { @@ -263,7 +263,7 @@ void FileOutputPlain::reserveSpace(uint64_t expectedSize) //throw FileError size_t FileOutputPlain::tryWrite(const void* buffer, size_t bytesToWrite) //throw FileError { if (bytesToWrite == 0) - throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); assert(bytesToWrite % getBlockSize() == 0 || bytesToWrite < getBlockSize()); try { @@ -306,7 +306,7 @@ std::string zen::getFileContent(const Zstring& filePath, const IoCallback& notif } -void zen::setFileContent(const Zstring& filePath, const std::string& byteStream, const IoCallback& notifyUnbufferedIO /*throw X*/) //throw FileError, X +void zen::setFileContent(const Zstring& filePath, const std::string_view byteStream, const IoCallback& notifyUnbufferedIO /*throw X*/) //throw FileError, X { const Zstring tmpFilePath = getPathWithTempName(filePath); diff --git a/zen/file_io.h b/zen/file_io.h index 46ffa843..838b5021 100644 --- a/zen/file_io.h +++ b/zen/file_io.h @@ -175,7 +175,7 @@ Zstring getPathWithTempName(const Zstring& filePath) //generate (hopefully) uniq [[nodiscard]] std::string getFileContent(const Zstring& filePath, const IoCallback& notifyUnbufferedIO /*throw X*/); //throw FileError, X //overwrites if existing + transactional! :) -void setFileContent(const Zstring& filePath, const std::string& bytes, const IoCallback& notifyUnbufferedIO /*throw X*/); //throw FileError, X +void setFileContent(const Zstring& filePath, const std::string_view bytes, const IoCallback& notifyUnbufferedIO /*throw X*/); //throw FileError, X } #endif //FILE_IO_H_89578342758342572345 diff --git a/zen/file_path.cpp b/zen/file_path.cpp index d06ab6bd..7ef78569 100644 --- a/zen/file_path.cpp +++ b/zen/file_path.cpp @@ -36,13 +36,13 @@ std::optional<PathComponents> zen::parsePathComponents(const Zstring& itemPath) pc = doParse(3 /*sepCountVolumeRoot*/, false /*rootWithSep*/); if (!pc && startsWith(itemPath, "/media/")) //Ubuntu: e.g. /media/zenju/DEVICE_NAME - if (const char* username = ::getenv("USER")) //no ownership transfer + no extended error reporting - if (startsWith(itemPath, std::string("/media/") + username + "/")) + if (const std::optional<Zstring> username = getEnvironmentVar("USER")) + if (startsWith(itemPath, std::string("/media/") + *username + "/")) pc = doParse(4 /*sepCountVolumeRoot*/, false /*rootWithSep*/); if (!pc && startsWith(itemPath, "/run/media/")) //CentOS, Suse: e.g. /run/media/zenju/DEVICE_NAME - if (const char* username = ::getenv("USER")) - if (startsWith(itemPath, std::string("/run/media/") + username + "/")) + if (const std::optional<Zstring> username = getEnvironmentVar("USER")) + if (startsWith(itemPath, std::string("/run/media/") + *username + "/")) pc = doParse(5 /*sepCountVolumeRoot*/, false /*rootWithSep*/); if (!pc && startsWith(itemPath, "/run/user/")) //Ubuntu, e.g.: /run/user/1000/gvfs/smb-share:server=192.168.62.145,share=folder @@ -154,3 +154,73 @@ std::weak_ordering zen::compareNativePath(const Zstring& lhs, const Zstring& rhs } +namespace +{ +std::unordered_map<Zstring, Zstring> getAllEnvVars() +{ + assert(runningOnMainThread()); + + std::unordered_map<Zstring, Zstring> envVars; + if (char** line = environ) + for (; *line; ++line) + { + const std::string_view l(*line); + envVars.emplace(beforeFirst(l, '=', IfNotFoundReturn::all), + afterFirst(l, '=', IfNotFoundReturn::none)); + } + return envVars; +} + +constinit Global<std::unordered_map<Zstring, Zstring>> globalEnvVars; +GLOBAL_RUN_ONCE( + //*INDENT-OFF* + //mitigate static initialization order fiasco: (whatever comes first) + if (!globalEnvVars.get()) + globalEnvVars.set(std::make_unique<std::unordered_map<Zstring, Zstring>>(getAllEnvVars())) + //*INDENT-ON* +); +} + + +std::optional<Zstring> zen::getEnvironmentVar(const ZstringView name) +{ + /* const char* buffer = ::getenv(name); => NO! *not* thread-safe: returns pointer to internal memory! + might change after setenv(), allegedly possible even after another getenv()! + + getenv_s() to the rescue!? not implemented on GCC, apparently *still* not threadsafe!!! + + => *eff* this: make a global copy during start up! */ + std::shared_ptr<std::unordered_map<Zstring, Zstring>> envVars = globalEnvVars.get(); + if (!envVars) //access during static init or shutdown? + { + if (globalEnvVars.wasDestroyed()) + { + assert(false); + return {}; //SOL! + } + //mitigate static initialization order fiasco: (whatever comes first) + globalEnvVars.set(std::make_unique<std::unordered_map<Zstring, Zstring>>(getAllEnvVars())); + envVars = globalEnvVars.get(); + } + + auto it = envVars->find(name); + if (it == envVars->end()) + return {}; + + Zstring value = it->second; + + //some postprocessing (good idea!? Is this even needed!? + warn_static("let's find out!") +#if 0 + trim(value); //remove leading, trailing blanks + + //remove leading, trailing double-quotes + if (startsWith(value, Zstr('"')) && + endsWith (value, Zstr('"')) && + value.length() >= 2) + value = Zstring(value.c_str() + 1, value.length() - 2); +#endif + return value; +} + + diff --git a/zen/file_path.h b/zen/file_path.h index d67a49d0..960ec52f 100644 --- a/zen/file_path.h +++ b/zen/file_path.h @@ -19,7 +19,7 @@ struct PathComponents Zstring rootPath; //itemPath = rootPath + (FILE_NAME_SEPARATOR?) + relPath Zstring relPath; // }; -std::optional<PathComponents> parsePathComponents(const Zstring& itemPath); //no value on failure +std::optional<PathComponents> parsePathComponents(const Zstring& itemPath); //no value on error std::optional<Zstring> getParentFolderPath(const Zstring& itemPath); inline Zstring getItemName(const Zstring& itemPath) { return afterLast(itemPath, FILE_NAME_SEPARATOR, IfNotFoundReturn::all); } @@ -44,6 +44,8 @@ inline bool equalNativePath(const Zstring& lhs, const Zstring& rhs) { return com struct LessNativePath { bool operator()(const Zstring& lhs, const Zstring& rhs) const { return compareNativePath(lhs, rhs) < 0; } }; //------------------------------------------------------------------------------------------ +std::optional<Zstring> getEnvironmentVar(const ZstringView name); + } diff --git a/zen/globals.h b/zen/globals.h index 9e22f56b..5d4a7041 100644 --- a/zen/globals.h +++ b/zen/globals.h @@ -9,6 +9,7 @@ #include <atomic> #include <memory> +#include <utility> #include "scope_guard.h" @@ -16,7 +17,7 @@ namespace zen { /* Solve static destruction order fiasco by providing shared ownership and serialized access to global variables - => there may be accesses to "Global<T>::get()" during process shutdown e.g. _("") used by message in debug_minidump.cpp or by some detached thread assembling an error message! + => e.g. accesses to "Global<T>::get()" during process shutdown: _("") used by message in debug_minidump.cpp or by some detached thread assembling an error message! => use trivially-destructible POD only!!! ATTENTION: function-static globals have the compiler generate "magic statics" == compiler-genenerated locking code which will crash or leak memory when accessed after global is "dead" @@ -54,7 +55,13 @@ public: ~Global() { static_assert(std::is_trivially_destructible_v<Pod>, "this memory needs to live forever"); - set(nullptr); + + pod_.spinLock.lock(); + std::shared_ptr<T>* oldInst = std::exchange(pod_.inst, nullptr); + pod_.destroyed = true; + pod_.spinLock.unlock(); + + delete oldInst; } std::shared_ptr<T> get() //=> return std::shared_ptr to let instance life time be handled by caller (MT usage!) @@ -76,17 +83,29 @@ public: pod_.spinLock.lock(); ZEN_ON_SCOPE_EXIT(pod_.spinLock.unlock()); - std::swap(pod_.inst, tmpInst); + if (!pod_.destroyed) + std::swap(pod_.inst, tmpInst); + else + assert(false); } delete tmpInst; } + bool wasDestroyed() + { + pod_.spinLock.lock(); + ZEN_ON_SCOPE_EXIT(pod_.spinLock.unlock()); + + return pod_.destroyed; + } + private: struct Pod { PodSpinMutex spinLock; //rely entirely on static zero-initialization! => avoid potential contention with worker thread during Global<> construction! //serialize access: can't use std::mutex: has non-trival destructor std::shared_ptr<T>* inst = nullptr; + bool destroyed = false; } pod_; }; diff --git a/zen/http.cpp b/zen/http.cpp index 7eb3fb76..e1a828c1 100644 --- a/zen/http.cpp +++ b/zen/http.cpp @@ -499,7 +499,7 @@ bool zen::isValidEmail(const std::string_view& email) return false; //--------------------------------------------------------------------- - //not going to parse and validate this! + //we're not going to parse and validate this! const bool quoted = (startsWith(local, '"') && endsWith(local, '"')) || contains(local, '\\'); //e.g. "t\@st@email.com" if (!quoted) diff --git a/zen/resolve_path.cpp b/zen/resolve_path.cpp index 8b81e184..daaf91ff 100644 --- a/zen/resolve_path.cpp +++ b/zen/resolve_path.cpp @@ -10,48 +10,15 @@ #include "file_access.h" #include <zen/sys_info.h> - // #include <stdlib.h> //getenv() - #include <unistd.h> //getuid() - #include <pwd.h> //getpwuid_r() + #include <unistd.h> //getcwd() using namespace zen; namespace { -std::optional<Zstring> getEnvironmentVar(const Zchar* name) -{ - assert(runningOnMainThread()); //getenv() is not thread-safe! - - const char* buffer = ::getenv(name); //no ownership transfer + no extended error reporting - if (!buffer) - return {}; - Zstring value(buffer); - - //some postprocessing: - trim(value); //remove leading, trailing blanks - - //remove leading, trailing double-quotes - if (startsWith(value, Zstr('"')) && - endsWith (value, Zstr('"')) && - value.length() >= 2) - value = Zstring(value.c_str() + 1, value.length() - 2); - - return value; -} - - Zstring resolveRelativePath(const Zstring& relativePath) { - assert(runningOnMainThread()); - /* MSDN: "Multithreaded applications and shared library code should not use the GetFullPathName function - and should avoid using relative path names. The current directory state written by the - SetCurrentDirectory function is stored as a global variable in each process, - therefore multithreaded applications cannot reliably use this value without possible data corruption from other threads, [...]" - - => Just plain wrong, there is no data corruption. What MSDN really means: GetFullPathName() is *perfectly* thread-safe, but depends - on the current directory, which is a process-scope global: https://devblogs.microsoft.com/oldnewthing/20210816-00/?p=105562 */ - if (relativePath.empty()) return relativePath; @@ -99,7 +66,7 @@ Zstring resolveRelativePath(const Zstring& relativePath) //returns value if resolved -std::optional<Zstring> tryResolveMacro(const Zstring& macro) //macro without %-characters +std::optional<Zstring> tryResolveMacro(const ZstringView macro) //macro without %-characters { Zstring timeStr; auto resolveTimePhrase = [&](const Zchar* phrase, const Zchar* format) -> bool @@ -142,7 +109,7 @@ std::optional<Zstring> tryResolveMacro(const Zstring& macro) //macro without %-c } //try to resolve as environment variables - if (std::optional<Zstring> value = getEnvironmentVar(macro.c_str())) + if (std::optional<Zstring> value = getEnvironmentVar(macro)) return *value; return {}; @@ -201,14 +168,14 @@ std::vector<Zstring> zen::getPathPhraseAliases(const Zstring& itemPath) { //environment variables: C:\Users\<user> -> %UserProfile% - auto substByMacro = [&](const Zchar* macroName, const Zstring& macroPath) + auto substByMacro = [&](const ZstringView macroName, const Zstring& macroPath) { //should use a replaceCpy() that considers "local path" case-sensitivity (if only we had one...) if (contains(itemPath, macroPath)) pathAliases.push_back(makePathPhrase(replaceCpyAsciiNoCase(itemPath, macroPath, Zstring() + MACRO_SEP + macroName + MACRO_SEP))); }; - for (const Zchar* envName : + for (const ZstringView envName : { "HOME", //Linux: /home/<user> Mac: /Users/<user> //"USER", -> any benefit? diff --git a/zen/serialize.h b/zen/serialize.h index a996b118..8ccecd53 100644 --- a/zen/serialize.h +++ b/zen/serialize.h @@ -256,7 +256,7 @@ BinContainer unbufferedLoad(Function tryRead /*(void* buffer, size_t bytesToRead { static_assert(sizeof(typename BinContainer::value_type) == 1); //expect: bytes if (blockSize == 0) - throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); BinContainer buf; for (;;) @@ -285,7 +285,7 @@ void unbufferedSave(const BinContainer& cont, { static_assert(sizeof(typename BinContainer::value_type) == 1); //expect: bytes if (blockSize == 0) - throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); const size_t bufPosEnd = cont.size(); size_t bufPos = 0; @@ -311,7 +311,7 @@ void unbufferedStreamCopy(Function1 tryRead /*(void* buffer, size_t bytesToRead) blockSizeOut = std::bit_ceil(blockSizeOut); #endif if (blockSizeIn == 0 || blockSizeOut == 0) - throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); const size_t bufCapacity = blockSizeOut - 1 + blockSizeIn; const size_t alignment = ::sysconf(_SC_PAGESIZE); //-1 on error => posix_memalign() will fail diff --git a/zen/socket.h b/zen/socket.h index df8b768b..4ccde190 100644 --- a/zen/socket.h +++ b/zen/socket.h @@ -143,7 +143,7 @@ namespace size_t tryReadSocket(SocketType socket, void* buffer, size_t bytesToRead) //throw SysError; may return short, only 0 means EOF! { if (bytesToRead == 0) //"read() with a count of 0 returns zero" => indistinguishable from end of file! => check! - throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); int bytesReceived = 0; for (;;) @@ -168,7 +168,7 @@ size_t tryReadSocket(SocketType socket, void* buffer, size_t bytesToRead) //thro size_t tryWriteSocket(SocketType socket, const void* buffer, size_t bytesToWrite) //throw SysError; may return short! CONTRACT: bytesToWrite > 0 { if (bytesToWrite == 0) - throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); int bytesWritten = 0; for (;;) diff --git a/zen/stream_buffer.h b/zen/stream_buffer.h index ee9e18fd..64cb76ca 100644 --- a/zen/stream_buffer.h +++ b/zen/stream_buffer.h @@ -149,7 +149,7 @@ private: size_t tryReadImpl(std::unique_lock<std::mutex>& ul, void* buffer, size_t bytesToRead) //throw <write error>; may return short; only 0 means EOF! CONTRACT: bytesToRead > 0! { if (bytesToRead == 0) //"read() with a count of 0 returns zero" => indistinguishable from end of file! => check! - throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); assert(isLocked(lockStream_)); assert(!errorRead_); @@ -170,7 +170,7 @@ private: size_t tryWriteWhileImpl(std::unique_lock<std::mutex>& ul, const void* buffer, size_t bytesToWrite) //throw <read error>; may return short! CONTRACT: bytesToWrite > 0 { if (bytesToWrite == 0) - throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); assert(isLocked(lockStream_)); assert(!eof_ && !errorWrite_); diff --git a/zen/string_base.h b/zen/string_base.h index 98544ab3..b19b4851 100644 --- a/zen/string_base.h +++ b/zen/string_base.h @@ -663,7 +663,20 @@ void Zbase<Char, SP>::pop_back() template <class Char, template <class> class SP> struct std::hash<zen::Zbase<Char, SP>> { - size_t operator()(const zen::Zbase<Char, SP>& str) const { return zen::hashString<size_t>(str); } + using is_transparent = int; //allow heterogenous lookup! + + template <class String> + size_t operator()(const String& str) const { return zen::hashString<size_t>(str); } +}; + + +template <class Char, template <class> class SP> +struct std::equal_to<zen::Zbase<Char, SP>> +{ + using is_transparent = int; //enable heterogenous lookup! + + template <class String1, class String2> + bool operator()(const String1& lhs, const String2& rhs) const { return zen::equalString(lhs, rhs); } }; #endif //STRING_BASE_H_083217454562342526 diff --git a/zen/string_tools.h b/zen/string_tools.h index ca086efd..03563d41 100644 --- a/zen/string_tools.h +++ b/zen/string_tools.h @@ -624,13 +624,13 @@ namespace impl template <class Num> inline int saferPrintf(char* buffer, size_t bufferSize, const char* format, const Num& number) //there is no such thing as a "safe" printf ;) { - return std::snprintf(buffer, bufferSize, format, number); //C99: returns number of chars written if successful, < 0 or >= bufferSize on failure + return std::snprintf(buffer, bufferSize, format, number); //C99: returns number of chars written if successful, < 0 or >= bufferSize on error } template <class Num> inline int saferPrintf(wchar_t* buffer, size_t bufferSize, const wchar_t* format, const Num& number) { - return std::swprintf(buffer, bufferSize, format, number); //C99: returns number of chars written if successful, < 0 on failure (including buffer too small) + return std::swprintf(buffer, bufferSize, format, number); //C99: returns number of chars written if successful, < 0 on error (including buffer too small) } } diff --git a/zen/sys_error.cpp b/zen/sys_error.cpp index fa7352f0..90d9ee2e 100644 --- a/zen/sys_error.cpp +++ b/zen/sys_error.cpp @@ -248,13 +248,13 @@ std::wstring zen::formatGlibError(const std::string& functionName, GError* error std::wstring zen::getSystemErrorDescription(ErrorCode ec) //return empty string on error { - const ErrorCode currentError = getLastError(); //not necessarily == ec - ZEN_ON_SCOPE_EXIT(errno = currentError); + const ErrorCode ecCurrent = getLastError(); //not necessarily == ec + ZEN_ON_SCOPE_EXIT(errno = ecCurrent); - std::wstring errorMsg; - errorMsg = utfTo<std::wstring>(::g_strerror(ec)); //... vs strerror(): "marginally improves thread safety, and marginally improves consistency" + std::wstring errorMsg = utfTo<std::wstring>(::g_strerror(ec)); //... vs strerror(): "marginally improves thread safety, and marginally improves consistency" - return trimCpy(errorMsg); //Windows messages seem to end with a space... + trim(errorMsg); //Windows messages seem to end with a space... + return errorMsg; } diff --git a/zen/sys_info.cpp b/zen/sys_info.cpp index 244343f2..55465711 100644 --- a/zen/sys_info.cpp +++ b/zen/sys_info.cpp @@ -26,12 +26,12 @@ using namespace zen; Zstring zen::getLoginUser() //throw FileError { - auto tryGetNonRootUser = [](const char* varName) -> const char* + auto tryGetNonRootUser = [](const char* varName) -> std::optional<Zstring> { - if (const char* buf = ::getenv(varName)) //no ownership transfer + no extended error reporting - if (strLength(buf) > 0 && !equalString(buf, "root")) - return buf; - return nullptr; + if (const std::optional<Zstring> username = getEnvironmentVar(varName)) + if (!username->empty() && *username != "root") + return *username; + return {}; }; if (const uid_t userIdNo = ::getuid(); //never fails @@ -65,9 +65,9 @@ Zstring zen::getLoginUser() //throw FileError //BUT: getlogin() can fail with ENOENT on Linux Mint: https://freefilesync.org/forum/viewtopic.php?t=8181 //getting a little desperate: variables used by installer.sh - if (const char* username = tryGetNonRootUser("USER")) return username; - if (const char* username = tryGetNonRootUser("SUDO_USER")) return username; - if (const char* username = tryGetNonRootUser("LOGNAME")) return username; + if (const std::optional<Zstring> username = tryGetNonRootUser("USER")) return *username; + if (const std::optional<Zstring> username = tryGetNonRootUser("SUDO_USER")) return *username; + if (const std::optional<Zstring> username = tryGetNonRootUser("LOGNAME")) return *username; //apparently the current user really IS root: https://freefilesync.org/forum/viewtopic.php?t=8405 @@ -221,8 +221,8 @@ Zstring zen::getUserHome() //throw FileError /* https://linux.die.net/man/3/getpwuid: An application that wants to determine its user's home directory should inspect the value of HOME (rather than the value getpwuid(getuid())->pw_dir) since this allows the user to modify their notion of "the home directory" during a login session. */ - if (const char* homePath = ::getenv("HOME")) //no ownership transfer + no extended error reporting - return homePath; + if (const std::optional<Zstring> homeDirPath = getEnvironmentVar("HOME")) + return *homeDirPath; //root(0) => consider as request for elevation, NOT impersonation! //=> "HOME=/root" :( @@ -234,10 +234,10 @@ Zstring zen::getUserHome() //throw FileError passwd buf2 = {}; passwd* pwEntry = nullptr; if (const int rv = ::getpwnam_r(loginUser.c_str(), //const char *name - &buf2, //struct passwd* pwd - buf.data(), //char* buf - buf.size(), //size_t buflen - &pwEntry); //struct passwd** result + &buf2, //struct passwd* pwd + buf.data(), //char* buf + buf.size(), //size_t buflen + &pwEntry); //struct passwd** result rv != 0 || !pwEntry) { //"If an error occurs, errno is set appropriately" => why the fuck, then also return errno as return value!? @@ -252,9 +252,9 @@ Zstring zen::getUserHome() //throw FileError Zstring zen::getUserDataPath() //throw FileError { if (::getuid() != 0) //nofail; non-root - if (const char* xdgCfgPath = ::getenv("XDG_CONFIG_HOME"); //no ownership transfer + no extended error reporting - xdgCfgPath && xdgCfgPath[0] != 0) - return xdgCfgPath; + if (const std::optional<Zstring> xdgCfgPath = getEnvironmentVar("XDG_CONFIG_HOME"); + xdgCfgPath&& !xdgCfgPath->empty()) + return *xdgCfgPath; //root(0) => consider as request for elevation, NOT impersonation return appendPath(getUserHome(), ".config"); //throw FileError diff --git a/zen/thread.h b/zen/thread.h index 25a6463a..2464f8be 100644 --- a/zen/thread.h +++ b/zen/thread.h @@ -153,7 +153,7 @@ class ThreadGroup { public: ThreadGroup(size_t threadCountMax, const Zstring& groupName) : threadCountMax_(threadCountMax), groupName_(groupName) - { if (threadCountMax == 0) throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); } + { if (threadCountMax == 0) throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); } ThreadGroup (ThreadGroup&& tmp) noexcept = default; //noexcept *required* to support move for reallocations in std::vector and std::swap!!! ThreadGroup& operator=(ThreadGroup&& tmp) noexcept = default; //don't use swap() but end worker_ life time immediately @@ -41,7 +41,7 @@ TimeComp getCompileTime(); //returns TimeComp() on error formatTime(Zstr("%Y|%m|%d")); -> "2011|10|29" formatTime(formatDateTag); -> "2011-10-29" formatTime(formatTimeTag); -> "17:55:34" */ -Zstring formatTime(const Zchar* format, const TimeComp& tc = getLocalTime()); //format as specified by "std::strftime", returns empty string on failure +Zstring formatTime(const Zchar* format, const TimeComp& tc = getLocalTime()); //format as specified by "std::strftime", returns empty string on error //the "format" parameter of formatTime() is partially specialized with the following type tags: const Zchar* const formatDateTag = Zstr("%x"); //locale-dependent date representation: e.g. 8/23/2001 @@ -59,7 +59,8 @@ template <class String, class String2> TimeComp parseTime(const String& format, const String2& str); //similar to ::strptime() //---------------------------------------------------------------------------------------------------------------------------------- - +//format: [-][[d.]HH:]MM:SS e.g. -1.23:45:67 +Zstring formatTimeSpan(int64_t timeInSec, bool hourOptional = false); @@ -385,6 +386,36 @@ TimeComp parseTime(const String& format, const String2& str) return output; } + + +inline +Zstring formatTimeSpan(int64_t timeInSec, bool hourOptional) +{ + Zstring timespanStr; + + if (timeInSec < 0) + { + timeInSec = -timeInSec; //need to fix LLONG_MIN? + timespanStr = Zstr('-'); + } + + //check *before* subtracting days! + const Zchar* timeSpanFmt = hourOptional && timeInSec < 3600 ? Zstr("%M:%S") : formatIsoTimeTag; + + const int secsPerDay = 24 * 3600; + const int64_t days = numeric::intDivFloor(timeInSec, secsPerDay); + if (days > 0) + { + timeInSec -= days * secsPerDay; + timespanStr += numberTo<Zstring>(days) + Zstr("."); //don't need zen::formatNumber(), do we? + } + + //format time span as if absolute UTC time + const TimeComp& tc = getUtcTime(timeInSec); //returns TimeComp() on error + timespanStr += formatTime(timeSpanFmt, tc); //returns empty string on error + + return timespanStr; +} } #endif //TIME_H_8457092814324342453627 @@ -16,7 +16,7 @@ namespace zen template <class TargetString, class SourceString> TargetString utfTo(const SourceString& str); -const char BYTE_ORDER_MARK_UTF8[] = "\xEF\xBB\xBF"; +const std::string_view BYTE_ORDER_MARK_UTF8 = "\xEF\xBB\xBF"; template <class UtfString> bool isValidUtf(const UtfString& str); //check for UTF-8 encoding errors diff --git a/zen/zlib_wrap.cpp b/zen/zlib_wrap.cpp index 28b85c5c..7e680131 100644 --- a/zen/zlib_wrap.cpp +++ b/zen/zlib_wrap.cpp @@ -6,7 +6,7 @@ #include "zlib_wrap.h" //Windows: use the SAME zlib version that wxWidgets is linking against! //C:\Data\Projects\wxWidgets\Source\src\zlib\zlib.h -//Linux/macOS: use zlib system header for both wxWidgets and libcurl (zlib is required for HTTP, SFTP) +//Linux/macOS: use zlib system header for wxWidgets, libcurl (HTTP), libssh2 (SFTP) // => don't compile wxWidgets with: --with-zlib=builtin #include <zlib.h> #include "scope_guard.h" @@ -178,7 +178,7 @@ public: size_t read(void* buffer, size_t bytesToRead) //throw SysError, X; return "bytesToRead" bytes unless end of stream! { if (bytesToRead == 0) //"read() with a count of 0 returns zero" => indistinguishable from end of file! => check! - throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); gzipStream_.next_out = static_cast<Bytef*>(buffer); gzipStream_.avail_out = static_cast<uInt>(bytesToRead); |