diff options
author | B Stack <bgstack15@gmail.com> | 2020-10-02 14:42:30 -0400 |
---|---|---|
committer | B Stack <bgstack15@gmail.com> | 2020-10-02 14:42:30 -0400 |
commit | 8aaf029ab6046eb8cbe600a548d176c1418bd99a (patch) | |
tree | d8a89392817379e3036c42eedebf33d4fb372dfd | |
parent | Merge branch '11.1' into 'master' (diff) | |
download | FreeFileSync-8aaf029ab6046eb8cbe600a548d176c1418bd99a.tar.gz FreeFileSync-8aaf029ab6046eb8cbe600a548d176c1418bd99a.tar.bz2 FreeFileSync-8aaf029ab6046eb8cbe600a548d176c1418bd99a.zip |
add upstream 11.2
93 files changed, 2339 insertions, 2188 deletions
diff --git a/Changelog.txt b/Changelog.txt index e7c9cd75..404b7cb7 100755 --- a/Changelog.txt +++ b/Changelog.txt @@ -1,3 +1,18 @@ +FreeFileSync 11.2 +----------------- +Improved grid layout with file icons hidden +Improved rendering of inactive and disabled grid items +Remember last user-selected paths for file and folder pickers +Fixed folder name hidden in "item name" view type +Fixed determination of unsupported trash folder (Linux) +Fixed copying broken symlinks (macOS) +Fixed default action when pressing Enter in popup dialogs +Fixed default popup dialog size (macOS) +Use localized start of week for %WeekDay% (Linux, macOS) +Swap sides using CTRL+W instead of F10 +Show confirmation dialog before swapping sides + + FreeFileSync 11.1 [2020-08-31] ------------------------------ New file group layout on main grid (reloaded) @@ -528,7 +543,7 @@ Support starting FreeFileSync via Windows Send To Minimized memory operations for I/O buffer Allow multiple config selections on Linux New command line option -DirPair -Fixed ENTER key not working for most dialogs (macOS) +Fixed Enter key not working for most dialogs (macOS) Show only one warning about failed directory locks Show correct synchronization time when resuming from system sleep Don't resolve symlinks that are dropped via mouse diff --git a/FreeFileSync/Build/FreeFileSync.desktop b/FreeFileSync/Build/FreeFileSync.Example.desktop index 797a9c25..797a9c25 100755 --- a/FreeFileSync/Build/FreeFileSync.desktop +++ b/FreeFileSync/Build/FreeFileSync.Example.desktop diff --git a/FreeFileSync/Build/RealTimeSync.desktop b/FreeFileSync/Build/RealTimeSync.Example.desktop index 080dd625..080dd625 100755 --- a/FreeFileSync/Build/RealTimeSync.desktop +++ b/FreeFileSync/Build/RealTimeSync.Example.desktop diff --git a/FreeFileSync/Build/Resources/Icons.zip b/FreeFileSync/Build/Resources/Icons.zip Binary files differindex 79ea7657..b64eada9 100644 --- a/FreeFileSync/Build/Resources/Icons.zip +++ b/FreeFileSync/Build/Resources/Icons.zip diff --git a/FreeFileSync/Build/Resources/Languages.zip b/FreeFileSync/Build/Resources/Languages.zip Binary files differindex 895ef3c0..ed116235 100644 --- a/FreeFileSync/Build/Resources/Languages.zip +++ b/FreeFileSync/Build/Resources/Languages.zip diff --git a/FreeFileSync/Source/Makefile b/FreeFileSync/Source/Makefile index caa599c1..b3024912 100755 --- a/FreeFileSync/Source/Makefile +++ b/FreeFileSync/Source/Makefile @@ -1,7 +1,7 @@ exeName = FreeFileSync_$(shell arch) cxxFlags = -std=c++2a -pipe -DWXINTL_NO_GETTEXT_MACRO -I../.. -I../../zenXml -include "zen/i18n.h" -include "zen/warn_static.h" \ - -Wall -Wfatal-errors -Wmissing-include-dirs -Wswitch-enum -Wcast-align -Wshadow -Wnon-virtual-dtor \ + -Wall -Wfatal-errors -Wmissing-include-dirs -Wswitch-enum -Wcast-align -Wnon-virtual-dtor -Wno-unused-function -Wshadow -Wno-maybe-uninitialized \ -O3 -DNDEBUG `wx-config --cxxflags --debug=no` -pthread linkFlags = -s -no-pie `wx-config --libs std, aui --debug=no` -pthread @@ -93,6 +93,7 @@ cppFiles+=../../zen/shell_execute.cpp cppFiles+=../../zen/shutdown.cpp cppFiles+=../../zen/sys_error.cpp cppFiles+=../../zen/sys_info.cpp +cppFiles+=../../zen/sys_version.cpp cppFiles+=../../zen/thread.cpp cppFiles+=../../zen/zlib_wrap.cpp cppFiles+=../../wx+/file_drop.cpp @@ -106,7 +107,7 @@ cppFiles+=../../wx+/popup_dlg.cpp cppFiles+=../../wx+/popup_dlg_generated.cpp cppFiles+=../../xBRZ/src/xbrz.cpp -tmpPath = $(TMPDIR)/$(exeName)_Make +tmpPath = $(shell dirname "$(shell mktemp -u)")/$(exeName)_Make objFiles = $(cppFiles:%=$(tmpPath)/ffs/src/%.o) diff --git a/FreeFileSync/Source/RealTimeSync/Makefile b/FreeFileSync/Source/RealTimeSync/Makefile index 3670af2e..97d600a9 100755 --- a/FreeFileSync/Source/RealTimeSync/Makefile +++ b/FreeFileSync/Source/RealTimeSync/Makefile @@ -1,7 +1,7 @@ exeName = RealTimeSync_$(shell arch) cxxFlags = -std=c++2a -pipe -DWXINTL_NO_GETTEXT_MACRO -I../../.. -I../../../zenXml -include "zen/i18n.h" -include "zen/warn_static.h" \ - -Wall -Wfatal-errors -Wmissing-include-dirs -Wswitch-enum -Wcast-align -Wshadow -Wnon-virtual-dtor \ + -Wall -Wfatal-errors -Wmissing-include-dirs -Wswitch-enum -Wcast-align -Wnon-virtual-dtor -Wno-unused-function -Wshadow -Wno-maybe-uninitialized \ -O3 -DNDEBUG `wx-config --cxxflags --debug=no` -pthread linkFlags = -s -no-pie `wx-config --libs std, aui --debug=no` -pthread @@ -46,7 +46,7 @@ cppFiles+=../../../zen/sys_version.cpp cppFiles+=../../../zen/thread.cpp cppFiles+=../../../zen/zstring.cpp -tmpPath = $(TMPDIR)/$(exeName)_Make +tmpPath = $(shell dirname "$(shell mktemp -u)")/$(exeName)_Make objFiles = $(cppFiles:%=$(tmpPath)/ffs/src/rts/%.o) diff --git a/FreeFileSync/Source/RealTimeSync/application.cpp b/FreeFileSync/Source/RealTimeSync/application.cpp index 7e408979..7a642545 100644 --- a/FreeFileSync/Source/RealTimeSync/application.cpp +++ b/FreeFileSync/Source/RealTimeSync/application.cpp @@ -184,7 +184,7 @@ void Application::OnUnhandledException() //handles both wxApp::OnInit() + wxApp: std::cerr << utfTo<std::string>(titleFmt + SPACED_DASH) << e.what() << '\n'; terminateProcess(fff::FFS_EXIT_EXCEPTION); } - //catch (...) -> let it crash and create mini dump!!! + //catch (...) -> Windows: let it crash and create mini dump!!! Linux/macOS: std::exception::what() logged to console } diff --git a/FreeFileSync/Source/RealTimeSync/folder_selector2.cpp b/FreeFileSync/Source/RealTimeSync/folder_selector2.cpp index 866f273e..89388a29 100644 --- a/FreeFileSync/Source/RealTimeSync/folder_selector2.cpp +++ b/FreeFileSync/Source/RealTimeSync/folder_selector2.cpp @@ -46,11 +46,13 @@ FolderSelector2::FolderSelector2(wxWindow* parent, wxWindow& dropWindow, wxButton& selectButton, wxTextCtrl& folderPathCtrl, + Zstring& folderLastSelected, wxStaticText* staticText) : parent_(parent), dropWindow_(dropWindow), selectButton_(selectButton), folderPathCtrl_(folderPathCtrl), + folderLastSelected_(folderLastSelected), staticText_(staticText) { //file drag and drop directly into the text control unhelpfully inserts in format "file://..<cr><nl>"; see folder_history_box.cpp @@ -61,7 +63,7 @@ FolderSelector2::FolderSelector2(wxWindow* parent, setupFileDrop(dropWindow_); dropWindow_.Bind(EVENT_DROP_FILE, &FolderSelector2::onFilesDropped, this); - //keep dirPicker and dirpath synchronous + //keep folderSelector and dirpath synchronous folderPathCtrl_.Bind(wxEVT_MOUSEWHEEL, &FolderSelector2::onMouseWheel, this); folderPathCtrl_.Bind(wxEVT_COMMAND_TEXT_UPDATED, &FolderSelector2::onEditFolderPath, this); selectButton_ .Bind(wxEVT_COMMAND_BUTTON_CLICKED, &FolderSelector2::onSelectDir, this); @@ -81,19 +83,16 @@ FolderSelector2::~FolderSelector2() void FolderSelector2::onMouseWheel(wxMouseEvent& event) { - //for combobox: although switching through available items is wxWidgets default, this is NOT windows default, e.g. Explorer + //for combobox: although switching through available items is wxWidgets default, this is NOT Windows default, e.g. Explorer //additionally this will delete manual entries, although all the users wanted is scroll the parent window! //redirect to parent scrolled window! - wxWindow* wnd = &folderPathCtrl_; - while ((wnd = wnd->GetParent()) != nullptr) //silence MSVC warning + for (wxWindow* wnd = folderPathCtrl_.GetParent(); wnd; wnd = wnd->GetParent()) if (dynamic_cast<wxScrolledWindow*>(wnd) != nullptr) if (wxEvtHandler* evtHandler = wnd->GetEventHandler()) - { - evtHandler->AddPendingEvent(event); - break; - } - // event.Skip(); + return evtHandler->AddPendingEvent(event); + assert(false); + event.Skip(); } @@ -134,26 +133,40 @@ void FolderSelector2::onSelectDir(wxCommandEvent& event) //IFileDialog requirements for default path: 1. accepts native paths only!!! 2. path must exist! Zstring defaultFolderPath; { - const Zstring folderPath = fff::getResolvedFilePath(getPath()); - if (!folderPath.empty()) + auto folderExistsTimed = [waitEndTime = std::chrono::steady_clock::now() + FOLDER_SELECTED_EXISTENCE_CHECK_TIME_MAX](const Zstring& folderPath) { auto ft = runAsync([folderPath] { return dirAvailable(folderPath); }); - if (ft.wait_for(FOLDER_SELECTED_EXISTENCE_CHECK_TIME_MAX) == std::future_status::ready && ft.get()) //potentially slow network access: wait 200ms at most - defaultFolderPath = folderPath; - } + return ft.wait_until(waitEndTime) == std::future_status::ready && ft.get(); //potentially slow network access: wait 200ms at most + }; + + auto trySetDefaultPath = [&](const Zstring& folderPathPhrase) + { + + if (const Zstring folderPath = fff::getResolvedFilePath(folderPathPhrase); + !folderPath.empty()) + if (folderExistsTimed(folderPath)) + defaultFolderPath = folderPath; + }; + + const Zstring& currentFolderPath = getPath(); + trySetDefaultPath(currentFolderPath); + + if (defaultFolderPath.empty() && //=> fallback: use last user-selected path + trimCpy(folderLastSelected_) != trimCpy(currentFolderPath) /*case-sensitive comp for path phrase!*/) + trySetDefaultPath(folderLastSelected_); } Zstring newFolderPath; - wxDirDialog dirPicker(parent_, _("Select a folder"), utfTo<wxString>(defaultFolderPath), wxDD_DEFAULT_STYLE | wxDD_SHOW_HIDDEN); - if (dirPicker.ShowModal() != wxID_OK) + wxDirDialog folderSelector(parent_, _("Select a folder"), utfTo<wxString>(defaultFolderPath), wxDD_DEFAULT_STYLE | wxDD_SHOW_HIDDEN); + if (folderSelector.ShowModal() != wxID_OK) return; - newFolderPath = utfTo<Zstring>(dirPicker.GetPath()); - + newFolderPath = utfTo<Zstring>(folderSelector.GetPath()); if (endsWith(newFolderPath, Zstr(' '))) //prevent getResolvedFilePath() from trimming legit trailing blank! newFolderPath += FILE_NAME_SEPARATOR; setPath(newFolderPath); + folderLastSelected_ = newFolderPath; } diff --git a/FreeFileSync/Source/RealTimeSync/folder_selector2.h b/FreeFileSync/Source/RealTimeSync/folder_selector2.h index 04b2036e..5f044557 100644 --- a/FreeFileSync/Source/RealTimeSync/folder_selector2.h +++ b/FreeFileSync/Source/RealTimeSync/folder_selector2.h @@ -24,6 +24,7 @@ public: wxWindow& dropWindow, wxButton& selectButton, wxTextCtrl& folderPathCtrl, + Zstring& folderLastSelected, wxStaticText* staticText); //optional ~FolderSelector2(); @@ -41,6 +42,7 @@ private: wxWindow& dropWindow_; wxButton& selectButton_; wxTextCtrl& folderPathCtrl_; + Zstring& folderLastSelected_; wxStaticText* staticText_ = nullptr; //optional }; } diff --git a/FreeFileSync/Source/RealTimeSync/main_dlg.cpp b/FreeFileSync/Source/RealTimeSync/main_dlg.cpp index a65dbb10..56a65649 100644 --- a/FreeFileSync/Source/RealTimeSync/main_dlg.cpp +++ b/FreeFileSync/Source/RealTimeSync/main_dlg.cpp @@ -47,9 +47,9 @@ std::wstring extractJobName(const Zstring& cfgFilePath) class rts::DirectoryPanel : public FolderGenerated { public: - DirectoryPanel(wxWindow* parent) : + DirectoryPanel(wxWindow* parent, Zstring& folderLastSelected) : FolderGenerated(parent), - folderSelector_(parent, *this, *m_buttonSelectFolder, *m_txtCtrlDirectory, nullptr /*staticText*/) + folderSelector_(parent, *this, *m_buttonSelectFolder, *m_txtCtrlDirectory, folderLastSelected, nullptr /*staticText*/) { m_bpButtonRemoveFolder->SetBitmapLabel(loadImage("item_remove")); } @@ -72,7 +72,6 @@ MainDialog::MainDialog(const Zstring& cfgFileName) : MainDlgGenerated(nullptr), lastRunConfigPath_(fff::getConfigDirPathPf() + Zstr("LastRun.ffs_real")) { - SetIcon(getRtsIcon()); //set application icon setRelativeFontSize(*m_buttonStart, 1.5); @@ -95,7 +94,7 @@ MainDialog::MainDialog(const Zstring& cfgFileName) : //prepare drag & drop - firstFolderPanel_ = std::make_unique<FolderSelector2>(this, *m_panelMainFolder, *m_buttonSelectFolderMain, *m_txtCtrlDirectoryMain, m_staticTextFinalPath); + firstFolderPanel_ = std::make_unique<FolderSelector2>(this, *m_panelMainFolder, *m_buttonSelectFolderMain, *m_txtCtrlDirectoryMain, folderLastSelected_, m_staticTextFinalPath); //--------------------------- load config values ------------------------------------ XmlRealConfig newConfig; @@ -247,24 +246,24 @@ void MainDialog::onStart(wxCommandEvent& event) void MainDialog::onConfigSave(wxCommandEvent& event) { - const Zstring defaultFilePath = !activeConfigFile_.empty() && !equalNativePath(activeConfigFile_, lastRunConfigPath_) ? activeConfigFile_ : Zstr("Realtime.ffs_real"); - auto defaultFolder = utfTo<wxString>(beforeLast(defaultFilePath, FILE_NAME_SEPARATOR, IfNotFoundReturn::none)); - auto defaultFileName = utfTo<wxString>(afterLast (defaultFilePath, FILE_NAME_SEPARATOR, IfNotFoundReturn::all)); - - //attention: currentConfigFileName may be an imported *.ffs_batch file! We don't want to overwrite it with a GUI config! - defaultFileName = beforeLast(defaultFileName, L'.', IfNotFoundReturn::all) + L".ffs_real"; - - wxFileDialog filePicker(this, - wxString(), //message - defaultFolder, defaultFileName, //OS X really needs dir/file separated like this - wxString(L"RealTimeSync (*.ffs_real)|*.ffs_real") + L"|" +_("All files") + L" (*.*)|*", - wxFD_SAVE | wxFD_OVERWRITE_PROMPT); - if (filePicker.ShowModal() != wxID_OK) - return; + const Zstring activeCfgFilePath = !equalNativePath(activeConfigFile_, lastRunConfigPath_) ? activeConfigFile_ : Zstring(); + + std::optional<Zstring> defaultFolderPath = getParentFolderPath(activeCfgFilePath); - const Zstring targetFilePath = utfTo<Zstring>(filePicker.GetPath()); + Zstring defaultFileName = afterLast(activeCfgFilePath, FILE_NAME_SEPARATOR, IfNotFoundReturn::all); + if (defaultFileName.empty()) + defaultFileName = Zstr("RealTime.ffs_real"); + + //attention: activeConfigFile_ may be an imported *.ffs_batch file! We don't want to overwrite it with a RTS config! + defaultFileName = beforeLast(defaultFileName, Zstr('.'), IfNotFoundReturn::all) + Zstr(".ffs_real"); + + wxFileDialog fileSelector(this, wxString() /*message*/, utfTo<wxString>(defaultFolderPath ? *defaultFolderPath : Zstr("")), utfTo<wxString>(defaultFileName), + wxString(L"RealTimeSync (*.ffs_real)|*.ffs_real") + L"|" +_("All files") + L" (*.*)|*", + wxFD_SAVE | wxFD_OVERWRITE_PROMPT); + if (fileSelector.ShowModal() != wxID_OK) + return; + const Zstring targetFilePath = utfTo<Zstring>(fileSelector.GetPath()); - //write config to XML const XmlRealConfig currentCfg = getConfiguration(); try { @@ -320,14 +319,15 @@ void MainDialog::onConfigLoad(wxCommandEvent& event) { const Zstring activeCfgFilePath = !equalNativePath(activeConfigFile_, lastRunConfigPath_) ? activeConfigFile_ : Zstring(); - wxFileDialog filePicker(this, - wxString(), //message - utfTo<wxString>(beforeLast(activeCfgFilePath, FILE_NAME_SEPARATOR, IfNotFoundReturn::none)), //default folder - wxString(), //default file name - wxString(L"RealTimeSync (*.ffs_real; *.ffs_batch)|*.ffs_real;*.ffs_batch") + L"|" +_("All files") + L" (*.*)|*", - wxFD_OPEN); - if (filePicker.ShowModal() == wxID_OK) - loadConfig(utfTo<Zstring>(filePicker.GetPath())); + std::optional<Zstring> defaultFolderPath = getParentFolderPath(activeCfgFilePath); + + wxFileDialog fileSelector(this, wxString() /*message*/, utfTo<wxString>(defaultFolderPath ? *defaultFolderPath : Zstr("")), wxString() /*default file name*/, + wxString(L"RealTimeSync (*.ffs_real; *.ffs_batch)|*.ffs_real;*.ffs_batch") + L"|" +_("All files") + L" (*.*)|*", + wxFD_OPEN); + if (fileSelector.ShowModal() != wxID_OK) + return; + + loadConfig(utfTo<Zstring>(fileSelector.GetPath())); } @@ -340,7 +340,6 @@ void MainDialog::onFilesDropped(FileDropEvent& event) void MainDialog::setConfiguration(const XmlRealConfig& cfg) { - const Zstring& firstFolderPath = cfg.directories.empty() ? Zstring() : cfg.directories[0]; const std::vector<Zstring> addFolderPaths = cfg.directories.empty() ? std::vector<Zstring>() : std::vector<Zstring>(cfg.directories.begin() + 1, cfg.directories.end()); @@ -375,7 +374,6 @@ XmlRealConfig MainDialog::getConfiguration() void MainDialog::onAddFolder(wxCommandEvent& event) { - const Zstring topFolder = firstFolderPanel_->getPath(); //clear existing top folder first @@ -387,7 +385,6 @@ void MainDialog::onAddFolder(wxCommandEvent& event) void MainDialog::onRemoveFolder(wxCommandEvent& event) { - //find folder pair originating the event const wxObject* const eventObj = event.GetEventObject(); for (auto it = additionalFolderPanels_.begin(); it != additionalFolderPanels_.end(); ++it) @@ -401,7 +398,6 @@ void MainDialog::onRemoveFolder(wxCommandEvent& event) void MainDialog::onRemoveTopFolder(wxCommandEvent& event) { - if (!additionalFolderPanels_.empty()) { firstFolderPanel_->setPath(additionalFolderPanels_[0]->getPath()); @@ -418,7 +414,7 @@ void MainDialog::insertAddFolder(const std::vector<Zstring>& newFolders, size_t for (size_t i = 0; i < newFolders.size(); ++i) { //add new folder pair - DirectoryPanel* newFolder = new DirectoryPanel(m_scrolledWinFolders); + DirectoryPanel* newFolder = new DirectoryPanel(m_scrolledWinFolders, folderLastSelected_); bSizerFolders->Insert(pos + i, newFolder, 0, wxEXPAND); additionalFolderPanels_.insert(additionalFolderPanels_.begin() + pos + i, newFolder); diff --git a/FreeFileSync/Source/RealTimeSync/main_dlg.h b/FreeFileSync/Source/RealTimeSync/main_dlg.h index bfe500c1..5de4cc26 100644 --- a/FreeFileSync/Source/RealTimeSync/main_dlg.h +++ b/FreeFileSync/Source/RealTimeSync/main_dlg.h @@ -63,7 +63,9 @@ private: const Zstring lastRunConfigPath_; - Zstring activeConfigFile_; + Zstring activeConfigFile_; //optional + + Zstring folderLastSelected_; zen::AsyncGuiQueue guiQueue_; //schedule and run long-running tasks asynchronously, but process results on GUI queue }; diff --git a/FreeFileSync/Source/RealTimeSync/monitor.cpp b/FreeFileSync/Source/RealTimeSync/monitor.cpp index 7bd4b662..86ba45c0 100644 --- a/FreeFileSync/Source/RealTimeSync/monitor.cpp +++ b/FreeFileSync/Source/RealTimeSync/monitor.cpp @@ -39,14 +39,14 @@ std::set<Zstring, LessNativePath> waitForMissingDirs(const std::vector<Zstring>& Zstring folderPathPhrase; std::future<bool> folderAvailable; }; - std::map<Zstring, FolderInfo, LessNativePath> folderInfos; //folderPath => FolderInfo + std::map<Zstring /*folderPath*/, FolderInfo, LessNativePath> folderInfos; for (const Zstring& phrase : folderPathPhrases) { const Zstring& folderPath = fff::getResolvedFilePath(phrase); //start all folder checks asynchronously (non-existent network path may block) - if (!contains(folderInfos, folderPath)) + if (!folderInfos.contains(folderPath)) folderInfos[folderPath] = { phrase, runAsync([folderPath]{ return dirAvailable(folderPath); }) }; } @@ -67,8 +67,8 @@ std::set<Zstring, LessNativePath> waitForMissingDirs(const std::vector<Zstring>& if (missingPathPhrases.empty()) return availablePaths; //only return when all folders were found on *first* try! - auto delayUntil = std::chrono::steady_clock::now() + FOLDER_EXISTENCE_CHECK_INTERVAL; + auto delayUntil = std::chrono::steady_clock::now() + FOLDER_EXISTENCE_CHECK_INTERVAL; for (const Zstring& folderPathPhrase : missingPathPhrases) for (;;) diff --git a/FreeFileSync/Source/RealTimeSync/tray_menu.cpp b/FreeFileSync/Source/RealTimeSync/tray_menu.cpp index e44123ba..81e717b2 100644 --- a/FreeFileSync/Source/RealTimeSync/tray_menu.cpp +++ b/FreeFileSync/Source/RealTimeSync/tray_menu.cpp @@ -112,9 +112,13 @@ private: { wxIcon realtimeIcon; realtimeIcon.CopyFromBitmap(img); - wxString tooltip = L"RealTimeSync\n" + statusTxt; + + wxString tooltip = L"RealTimeSync"; if (!jobName_.empty()) - tooltip += L"\n\"" + jobName_ + L'"'; + tooltip += L" | " + jobName_; + + tooltip += L"\n" + statusTxt; + SetIcon(realtimeIcon, tooltip); } diff --git a/FreeFileSync/Source/afs/abstract.cpp b/FreeFileSync/Source/afs/abstract.cpp index bcad6443..59b3c52e 100644 --- a/FreeFileSync/Source/afs/abstract.cpp +++ b/FreeFileSync/Source/afs/abstract.cpp @@ -17,9 +17,13 @@ using AFS = AbstractFileSystem; bool fff::isValidRelPath(const Zstring& relPath) { - return !contains(relPath, '\\') && - !startsWith(relPath, FILE_NAME_SEPARATOR) && !endsWith(relPath, FILE_NAME_SEPARATOR) && - !contains(relPath, Zstring() + FILE_NAME_SEPARATOR + FILE_NAME_SEPARATOR); + //relPath is expected to use FILE_NAME_SEPARATOR! + if constexpr (FILE_NAME_SEPARATOR != Zstr('/' )) if (contains(relPath, Zstr('/' ))) return false; + if constexpr (FILE_NAME_SEPARATOR != Zstr('\\')) if (contains(relPath, Zstr('\\'))) return false; + + const Zchar doubleSep[] = { FILE_NAME_SEPARATOR, FILE_NAME_SEPARATOR, 0 }; + return !startsWith(relPath, FILE_NAME_SEPARATOR)&& !endsWith(relPath, FILE_NAME_SEPARATOR)&& + !contains(relPath, doubleSep); } @@ -32,13 +36,13 @@ AfsPath fff::sanitizeDeviceRelativePath(Zstring relPath) } -int AFS::compareDevice(const AbstractFileSystem& lhs, const AbstractFileSystem& rhs) +std::weak_ordering AFS::compareDevice(const AbstractFileSystem& lhs, const AbstractFileSystem& rhs) { //note: in worst case, order is guaranteed to be stable only during each program run //caveat: typeid returns static type for pointers, dynamic type for references!!! if (const std::strong_ordering cmp = std::type_index(typeid(lhs)) <=> std::type_index(typeid(rhs)); - cmp != std::strong_ordering::equal) - return cmp < 0 ? -1 : 1; + std::is_neq(cmp)) + return cmp; return lhs.compareDeviceSameAfsType(rhs); } @@ -120,7 +124,6 @@ AFS::FileCopyResult AFS::copyFileAsStream(const AfsPath& afsSource, const Stream else //use possibly stale ones: attrSourceNew = attrSource; //SFTP/FTP //TODO: evaluate: consequences of stale attributes - warn_static("TODO") //already existing: undefined behavior! (e.g. fail/overwrite/auto-rename) auto streamOut = getOutputStream(apTarget, attrSourceNew.fileSize, attrSourceNew.modTime, notifyUnbufferedWrite); //throw FileError diff --git a/FreeFileSync/Source/afs/abstract.h b/FreeFileSync/Source/afs/abstract.h index 6bf78631..79b7077e 100644 --- a/FreeFileSync/Source/afs/abstract.h +++ b/FreeFileSync/Source/afs/abstract.h @@ -60,7 +60,7 @@ struct AbstractFileSystem //THREAD-SAFETY: "const" member functions must model t static std::optional<AfsPath> getParentPath(const AfsPath& afsPath); //============================================= - static int compareDevice(const AbstractFileSystem& lhs, const AbstractFileSystem& rhs); + static std::weak_ordering compareDevice(const AbstractFileSystem& lhs, const AbstractFileSystem& rhs); static bool isNullDevice(const AfsDevice& afsDevice) { return afsDevice.ref().isNullFileSystem(); } @@ -334,7 +334,7 @@ private: virtual bool isNullFileSystem() const = 0; - virtual int compareDeviceSameAfsType(const AbstractFileSystem& afsRhs) const = 0; + virtual std::weak_ordering compareDeviceSameAfsType(const AbstractFileSystem& afsRhs) const = 0; //---------------------------------------------------------------------------------------------------------------- virtual ItemType getItemType(const AfsPath& afsPath) const = 0; //throw FileError @@ -403,21 +403,19 @@ private: }; -inline std::weak_ordering operator<=>(const AfsDevice& lhs, const AfsDevice& rhs) { return AbstractFileSystem::compareDevice(lhs.ref(), rhs.ref()) <=> 0; } - -inline bool operator==(const AfsDevice& lhs, const AfsDevice& rhs) { return lhs <=> rhs == std::strong_ordering::equal; } +inline std::weak_ordering operator<=>(const AfsDevice& lhs, const AfsDevice& rhs) { return AbstractFileSystem::compareDevice(lhs.ref(), rhs.ref()); } +inline bool operator== (const AfsDevice& lhs, const AfsDevice& rhs) { return std::is_eq(lhs <=> rhs); } inline std::weak_ordering operator<=>(const AbstractPath& lhs, const AbstractPath& rhs) { - if (const std::weak_ordering cmp = lhs.afsDevice <=> rhs.afsDevice; - cmp != std::weak_ordering::equivalent) - return cmp; - - return lhs.afsPath <=> rhs.afsPath; + return std::tie(lhs.afsDevice, lhs.afsPath) <=> + std::tie(rhs.afsDevice, rhs.afsPath); } -inline bool operator==(const AbstractPath& lhs, const AbstractPath& rhs) { return lhs.afsPath == rhs.afsPath && lhs.afsDevice == rhs.afsDevice; } +inline +bool operator==(const AbstractPath& lhs, const AbstractPath& rhs) { return lhs.afsPath == rhs.afsPath && lhs.afsDevice == rhs.afsDevice; } + @@ -433,7 +431,7 @@ AbstractPath AbstractFileSystem::appendRelPath(const AbstractPath& ap, const Zst return AbstractPath(ap.afsDevice, AfsPath(nativeAppendPaths(ap.afsPath.value, relPath))); } -//-------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- inline AbstractFileSystem::OutputStream::OutputStream(std::unique_ptr<OutputStreamImpl>&& outStream, const AbstractPath& filePath, std::optional<uint64_t> streamSize) : diff --git a/FreeFileSync/Source/afs/ftp.cpp b/FreeFileSync/Source/afs/ftp.cpp index 73977021..5293aca8 100644 --- a/FreeFileSync/Source/afs/ftp.cpp +++ b/FreeFileSync/Source/afs/ftp.cpp @@ -60,27 +60,19 @@ struct FtpSessionId bool useTls = false; //timeoutSec => irrelevant for session equality }; + std::weak_ordering operator<=>(const FtpSessionId& lhs, const FtpSessionId& rhs) { //exactly the type of case insensitive comparison we need for server names! - if (const int cmp = compareAsciiNoCase(lhs.server, rhs.server); //https://docs.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfow#IDNs - cmp != 0) - return cmp <=> 0; - - if (lhs.port != rhs.port) - return lhs.port <=> rhs.port; - - if (const std::strong_ordering cmp = lhs.username <=> rhs.username; //case sensitive! - cmp != std::strong_ordering::equal) + if (const std::weak_ordering cmp = compareAsciiNoCase(lhs.server, rhs.server); //https://docs.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfow#IDNs + std::is_neq(cmp)) return cmp; - if (const std::strong_ordering cmp = lhs.password <=> rhs.password; //case sensitive! - cmp != std::strong_ordering::equal) - return cmp; - - return lhs.useTls <=> rhs.useTls; + return std::tie(lhs.port, lhs.username, lhs.password, lhs.useTls) <=> //username, password: case sensitive! + std::tie(rhs.port, rhs.username, rhs.password, rhs.useTls); } + Zstring ansiToUtfEncoding(const std::string& str) //throw SysError { gsize bytesWritten = 0; //not including the terminating null @@ -1837,20 +1829,20 @@ private: bool isNullFileSystem() const override { return login_.server.empty(); } - int compareDeviceSameAfsType(const AbstractFileSystem& afsRhs) const override + std::weak_ordering compareDeviceSameAfsType(const AbstractFileSystem& afsRhs) const override { const FtpLogin& lhs = login_; const FtpLogin& rhs = static_cast<const FtpFileSystem&>(afsRhs).login_; //exactly the type of case insensitive comparison we need for server names! - if (const int rv = compareAsciiNoCase(lhs.server, rhs.server); //https://docs.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfow#IDNs - rv != 0) - return rv; + if (const std::weak_ordering cmp = compareAsciiNoCase(lhs.server, rhs.server); //https://docs.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfow#IDNs + std::is_neq(cmp)) + return cmp; //port does NOT create a *different* data source!!! -> same thing for password! //username: usually *does* create different folder view for FTP - return compareString(lhs.username, rhs.username); //case sensitive! + return lhs.username <=> rhs.username; //case sensitive! } //---------------------------------------------------------------------------------------------------------------- @@ -2084,7 +2076,7 @@ private: L"%y", L'\n' + fmtPath(AFS::getDisplayPath(pathTo))); }; - if (compareDeviceSameAfsType(pathTo.afsDevice.ref()) != 0) + if (std::is_neq(compareDeviceSameAfsType(pathTo.afsDevice.ref()))) throw ErrorMoveUnsupported(generateErrorMsg(), _("Operation not supported between different devices.")); try diff --git a/FreeFileSync/Source/afs/gdrive.cpp b/FreeFileSync/Source/afs/gdrive.cpp index 9e518625..c4814169 100644 --- a/FreeFileSync/Source/afs/gdrive.cpp +++ b/FreeFileSync/Source/afs/gdrive.cpp @@ -48,10 +48,10 @@ inline std::weak_ordering operator<=>(const GdriveRawPath& lhs, const GdriveRawPath& rhs) { if (const std::strong_ordering cmp = lhs.parentId <=> rhs.parentId; - cmp != std::strong_ordering::equal) + std::is_neq(cmp)) return cmp; - return compareNativePath(lhs.itemName, rhs.itemName) <=> 0; + return compareNativePath(lhs.itemName, rhs.itemName); } constinit2 Global<PathAccessLocker<GdriveRawPath>> globalGdrivePathAccessLocker; @@ -101,7 +101,7 @@ struct HttpSessionId std::weak_ordering operator<=>(const HttpSessionId& lhs, const HttpSessionId& rhs) { //exactly the type of case insensitive comparison we need for server names! - return compareAsciiNoCase(lhs.server, rhs.server) <=> 0; //https://docs.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfow#IDNs + return compareAsciiNoCase(lhs.server, rhs.server); //https://docs.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfow#IDNs } @@ -360,33 +360,6 @@ GdriveUser getGdriveUser(const std::string& accessToken) //throw SysError } -const char htmlMessageTemplate[] = R"(<!DOCTYPE html> -<html lang="en"> - <head> - <meta charset="utf-8"> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>TITLE_PLACEHOLDER</title> - <style> - * { - font-family: -apple-system, 'Segoe UI', arial, Tahoma, Helvetica, sans-serif; - text-align: center; - background-color: #eee; } - h1 { - font-size: 45px; - font-weight: 300; - margin: 80px 0 20px 0; } - .descr { - font-size: 21px; - font-weight: 200; } - </style> - </head> - <body> - <h1><img src="https://freefilesync.org/images/FreeFileSync.png" style="vertical-align:middle; height:50px;" alt=""> TITLE_PLACEHOLDER</h1> - <div class="descr">MESSAGE_PLACEHOLDER</div> - </body> -</html> -)"; - struct GdriveAuthCode { std::string code; @@ -439,6 +412,8 @@ GdriveAccessInfo gdriveExchangeAuthCode(const GdriveAuthCode& authCode) //throw } +//Astyle fucks up because of the raw string literal! +//*INDENT-OFF* GdriveAccessInfo gdriveAuthorizeAccess(const std::string& gdriveLoginHint, const std::function<void()>& updateGui /*throw X*/) //throw SysError, X { //spin up a web server to wait for the HTTP GET after Google authentication @@ -497,14 +472,14 @@ GdriveAccessInfo gdriveAuthorizeAccess(const std::string& gdriveLoginHint, const if (addr.ss_family != AF_INET && addr.ss_family != AF_INET6) - throw SysError(formatSystemError("getsockname", L"", L"Unknown protocol family: " + numberTo<std::wstring>(addr.ss_family))); + throw SysError(formatSystemError("getsockname", L"", L"Unknown protocol family: " + numberTo<std::wstring>(addr.ss_family))); -const int port = ntohs(reinterpret_cast<const sockaddr_in&>(addr).sin_port); -//the socket is not bound to a specific local IP => inet_ntoa(reinterpret_cast<const sockaddr_in&>(addr).sin_addr) == "0.0.0.0" -const std::string redirectUrl = "http://127.0.0.1:" + numberTo<std::string>(port); + const int port = ntohs(reinterpret_cast<const sockaddr_in&>(addr).sin_port); + //the socket is not bound to a specific local IP => inet_ntoa(reinterpret_cast<const sockaddr_in&>(addr).sin_addr) == "0.0.0.0" + const std::string redirectUrl = "http://127.0.0.1:" + numberTo<std::string>(port); -if (::listen(socket, SOMAXCONN) != 0) - THROW_LAST_SYS_ERROR_WSA("listen"); + if (::listen(socket, SOMAXCONN) != 0) + THROW_LAST_SYS_ERROR_WSA("listen"); //"A code_verifier is a high-entropy cryptographic random string using the unreserved characters:" @@ -517,132 +492,158 @@ if (::listen(socket, SOMAXCONN) != 0) //authenticate Google Drive via browser: https://developers.google.com/identity/protocols/OAuth2InstalledApp#step-2-send-a-request-to-googles-oauth-20-server const std::string oauthUrl = "https://accounts.google.com/o/oauth2/v2/auth?" + xWwwFormUrlEncode( -{ - { "client_id", getGdriveClientId() }, - { "redirect_uri", redirectUrl }, - { "response_type", "code" }, - { "scope", "https://www.googleapis.com/auth/drive" }, - { "code_challenge", codeChallenge }, - { "code_challenge_method", "plain" }, - { "login_hint", gdriveLoginHint }, -}); -try -{ - openWithDefaultApp(utfTo<Zstring>(oauthUrl)); //throw FileError -} -catch (const FileError& e) { throw SysError(replaceCpy(e.toString(), L"\n\n", L'\n')); } //errors should be further enriched by context info => SysError - -//process incoming HTTP requests -for (;;) -{ -for (;;) //::accept() blocks forever if no client connects (e.g. user just closes the browser window!) => wait for incoming traffic with a time-out via ::select() { - if (updateGui) updateGui(); //throw X - - fd_set rfd = {}; - FD_ZERO(&rfd); - FD_SET(socket, &rfd); - fd_set* readfds = &rfd; - - struct ::timeval tv = {}; - tv.tv_usec = static_cast<long>(100 /*ms*/) * 1000; - - //WSAPoll broken, even ::poll() on OS X? https://daniel.haxx.se/blog/2012/10/10/wsapoll-is-broken/ - //perf: no significant difference compared to ::WSAPoll() - const int rc = ::select(socket + 1, readfds, nullptr /*writefds*/, nullptr /*errorfds*/, &tv); - if (rc < 0) - THROW_LAST_SYS_ERROR_WSA("select"); - if (rc != 0) - break; - //else: time-out! + { "client_id", getGdriveClientId() }, + { "redirect_uri", redirectUrl }, + { "response_type", "code" }, + { "scope", "https://www.googleapis.com/auth/drive" }, + { "code_challenge", codeChallenge }, + { "code_challenge_method", "plain" }, + { "login_hint", gdriveLoginHint }, + }); + try + { + openWithDefaultApp(utfTo<Zstring>(oauthUrl)); //throw FileError } - //potential race! if the connection is gone right after ::select() and before ::accept(), latter will hang - const SocketType clientSocket = ::accept(socket, //SOCKET s, - nullptr, //sockaddr *addr, - nullptr); //int *addrlen - if (clientSocket == invalidSocket) - THROW_LAST_SYS_ERROR_WSA("accept"); + catch (const FileError& e) { throw SysError(replaceCpy(e.toString(), L"\n\n", L'\n')); } //errors should be further enriched by context info => SysError - //receive first line of HTTP request - std::string reqLine; + //process incoming HTTP requests for (;;) { - const size_t blockSize = 64 * 1024; - reqLine.resize(reqLine.size() + blockSize); - const size_t bytesReceived = tryReadSocket(clientSocket, &*(reqLine.end() - blockSize), blockSize); //throw SysError - reqLine.resize(reqLine.size() - (blockSize - bytesReceived)); //caveat: unsigned arithmetics - - if (contains(reqLine, "\r\n")) + for (;;) //::accept() blocks forever if no client connects (e.g. user just closes the browser window!) => wait for incoming traffic with a time-out via ::select() { - reqLine = beforeFirst(reqLine, "\r\n", IfNotFoundReturn::none); - break; + if (updateGui) updateGui(); //throw X + + fd_set rfd = {}; + FD_ZERO(&rfd); + FD_SET(socket, &rfd); + fd_set* readfds = &rfd; + + struct ::timeval tv = {}; + tv.tv_usec = static_cast<long>(100 /*ms*/) * 1000; + + //WSAPoll broken, even ::poll() on OS X? https://daniel.haxx.se/blog/2012/10/10/wsapoll-is-broken/ + //perf: no significant difference compared to ::WSAPoll() + if (const int rc = ::select(socket + 1, readfds, nullptr /*writefds*/, nullptr /*errorfds*/, &tv); + rc < 0) + THROW_LAST_SYS_ERROR_WSA("select"); + else if (rc != 0) + break; + //else: time-out! } - if (bytesReceived == 0 || reqLine.size() >= 100'000 /*bogus line length*/) - break; - } + //potential race! if the connection is gone right after ::select() and before ::accept(), latter will hang + const SocketType clientSocket = ::accept(socket, //SOCKET s, + nullptr, //sockaddr *addr, + nullptr); //int *addrlen + if (clientSocket == invalidSocket) + THROW_LAST_SYS_ERROR_WSA("accept"); - //get OAuth2.0 authorization result from Google, either: - std::string code; - std::string error; + //receive first line of HTTP request + std::string reqLine; + for (;;) + { + const size_t blockSize = 64 * 1024; + reqLine.resize(reqLine.size() + blockSize); + const size_t bytesReceived = tryReadSocket(clientSocket, &*(reqLine.end() - blockSize), blockSize); //throw SysError + reqLine.resize(reqLine.size() - (blockSize - bytesReceived)); //caveat: unsigned arithmetics - //parse header; e.g.: GET http://127.0.0.1:62054/?code=4/ZgBRsB9k68sFzc1Pz1q0__Kh17QK1oOmetySrGiSliXt6hZtTLUlYzm70uElNTH9vt1OqUMzJVeFfplMsYsn4uI HTTP/1.1 - const std::vector<std::string> statusItems = split(reqLine, ' ', SplitOnEmpty::allow); //Method SP Request-URI SP HTTP-Version CRLF + if (contains(reqLine, "\r\n")) + { + reqLine = beforeFirst(reqLine, "\r\n", IfNotFoundReturn::none); + break; + } + if (bytesReceived == 0 || reqLine.size() >= 100'000 /*bogus line length*/) + break; + } - if (statusItems.size() == 3 && statusItems[0] == "GET" && startsWith(statusItems[2], "HTTP/")) - { - for (const auto& [name, value] : xWwwFormUrlDecode(afterFirst(statusItems[1], "?", IfNotFoundReturn::none))) - if (name == "code") - code = value; - else if (name == "error") - error = value; //e.g. "access_denied" => no more detailed error info available :( - } //"add explicit braces to avoid dangling else [-Wdangling-else]" + //get OAuth2.0 authorization result from Google, either: + std::string code; + std::string error; - std::optional<std::variant<GdriveAccessInfo, SysError>> authResult; + //parse header; e.g.: GET http://127.0.0.1:62054/?code=4/ZgBRsB9k68sFzc1Pz1q0__Kh17QK1oOmetySrGiSliXt6hZtTLUlYzm70uElNTH9vt1OqUMzJVeFfplMsYsn4uI HTTP/1.1 + const std::vector<std::string> statusItems = split(reqLine, ' ', SplitOnEmpty::allow); //Method SP Request-URI SP HTTP-Version CRLF - //send HTTP response; https://www.w3.org/Protocols/HTTP/1.0/spec.html#Request-Line - std::string httpResponse; - if (code.empty() && error.empty()) //parsing error or unrelated HTTP request - httpResponse = "HTTP/1.0 400 Bad Request" "\r\n" "\r\n" "400 Bad Request\n" + reqLine; - else - { - std::string htmlMsg = htmlMessageTemplate; - try + if (statusItems.size() == 3 && statusItems[0] == "GET" && startsWith(statusItems[2], "HTTP/")) { - if (!error.empty()) - throw SysError(replaceCpy(_("Error code %x"), L"%x", + L"\"" + utfTo<std::wstring>(error) + L"\"")); + for (const auto& [name, value] : xWwwFormUrlDecode(afterFirst(statusItems[1], "?", IfNotFoundReturn::none))) + if (name == "code") + code = value; + else if (name == "error") + error = value; //e.g. "access_denied" => no more detailed error info available :( + } //"add explicit braces to avoid dangling else [-Wdangling-else]" - //do as many login-related tasks as possible while we have the browser as an error output device! - //see AFS::connectNetworkFolder() => errors will be lost after time out in dir_exist_async.h! - authResult = gdriveExchangeAuthCode({ code, redirectUrl, codeChallenge }); //throw SysError - replace(htmlMsg, "TITLE_PLACEHOLDER", utfTo<std::string>(_("Authentication completed."))); - replace(htmlMsg, "MESSAGE_PLACEHOLDER", utfTo<std::string>(_("You may close this page now and continue with FreeFileSync."))); - } - catch (const SysError& e) + std::optional<std::variant<GdriveAccessInfo, SysError>> authResult; + + //send HTTP response; https://www.w3.org/Protocols/HTTP/1.0/spec.html#Request-Line + std::string httpResponse; + if (code.empty() && error.empty()) //parsing error or unrelated HTTP request + httpResponse = "HTTP/1.0 400 Bad Request" "\r\n" "\r\n" "400 Bad Request\n" + reqLine; + else { - authResult = e; - replace(htmlMsg, "TITLE_PLACEHOLDER", utfTo<std::string>(_("Authentication failed."))); - replace(htmlMsg, "MESSAGE_PLACEHOLDER", utfTo<std::string>(replaceCpy(_("Unable to connect to %x."), L"%x", L"Google Drive") + L"\n\n" + e.toString())); + std::string htmlMsg = R"(<!DOCTYPE html> + <html lang="en"> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>TITLE_PLACEHOLDER</title> + <style> + * { + font-family: -apple-system, 'Segoe UI', arial, Tahoma, Helvetica, sans-serif; + text-align: center; + background-color: #eee; } + h1 { + font-size: 45px; + font-weight: 300; + margin: 80px 0 20px 0; } + .descr { + font-size: 21px; + font-weight: 200; } + </style> + </head> + <body> + <h1><img src="https://freefilesync.org/images/FreeFileSync.png" style="vertical-align:middle; height:50px;" alt=""> TITLE_PLACEHOLDER</h1> + <div class="descr">MESSAGE_PLACEHOLDER</div> + </body> + </html> + )"; + try + { + if (!error.empty()) + throw SysError(replaceCpy(_("Error code %x"), L"%x", + L"\"" + utfTo<std::wstring>(error) + L"\"")); + + //do as many login-related tasks as possible while we have the browser as an error output device! + //see AFS::connectNetworkFolder() => errors will be lost after time out in dir_exist_async.h! + authResult = gdriveExchangeAuthCode({ code, redirectUrl, codeChallenge }); //throw SysError + replace(htmlMsg, "TITLE_PLACEHOLDER", utfTo<std::string>(_("Authentication completed."))); + replace(htmlMsg, "MESSAGE_PLACEHOLDER", utfTo<std::string>(_("You may close this page now and continue with FreeFileSync."))); + } + catch (const SysError& e) + { + authResult = e; + replace(htmlMsg, "TITLE_PLACEHOLDER", utfTo<std::string>(_("Authentication failed."))); + replace(htmlMsg, "MESSAGE_PLACEHOLDER", utfTo<std::string>(replaceCpy(_("Unable to connect to %x."), L"%x", L"Google Drive") + L"\n\n" + e.toString())); + } + httpResponse = "HTTP/1.0 200 OK" "\r\n" + "Content-Type: text/html" "\r\n" + "Content-Length: " + numberTo<std::string>(strLength(htmlMsg)) + "\r\n" + "\r\n" + htmlMsg; } - httpResponse = "HTTP/1.0 200 OK" "\r\n" - "Content-Type: text/html" "\r\n" - "Content-Length: " + numberTo<std::string>(strLength(htmlMsg)) + "\r\n" - "\r\n" + htmlMsg; - } - for (size_t bytesToSend = httpResponse.size(); bytesToSend > 0;) - bytesToSend -= tryWriteSocket(clientSocket, &*(httpResponse.end() - bytesToSend), bytesToSend); //throw SysError + for (size_t bytesToSend = httpResponse.size(); bytesToSend > 0;) + bytesToSend -= tryWriteSocket(clientSocket, &*(httpResponse.end() - bytesToSend), bytesToSend); //throw SysError - shutdownSocketSend(clientSocket); //throw SysError - //--------------------------------------------------------------- + shutdownSocketSend(clientSocket); //throw SysError + //--------------------------------------------------------------- - if (authResult) - { - if (const SysError* e = std::get_if<SysError>(&*authResult)) - throw *e; - return std::get<GdriveAccessInfo>(*authResult); + if (authResult) + { + if (const SysError* e = std::get_if<SysError>(&*authResult)) + throw *e; + return std::get<GdriveAccessInfo>(*authResult); + } } } -} +//*INDENT-ON* GdriveAccessToken gdriveRefreshAccess(const std::string& refreshToken) //throw SysError @@ -678,7 +679,7 @@ void gdriveRevokeAccess(const std::string& accessToken) //throw SysError //https://developers.google.com/identity/protocols/OAuth2InstalledApp#tokenrevoke const std::shared_ptr<HttpSessionManager> mgr = globalHttpSessionManager.get(); if (!mgr) - throw SysError(formatSystemError("gdriveRevokeAccess", L"", L"Function call not allowed during init/shutdown.")); + throw SysError(formatSystemError("gdriveRevokeAccess", L"", L"Function call not allowed during init/shutdown.")); HttpSession::Result httpResult; std::string response; @@ -741,66 +742,65 @@ std::vector<DriveDetails> getSharedDrives(const std::string& accessToken) //thro std::string queryParams = xWwwFormUrlEncode( { { "pageSize", "100" }, //"[1, 100] Default: 10" - { "fields", "nextPageToken,drives(id,name)" -} -}); -if (nextPageToken) -queryParams += '&' + xWwwFormUrlEncode({ { "pageToken", *nextPageToken } }); + { "fields", "nextPageToken,drives(id,name)" }, + }); + if (nextPageToken) + queryParams += '&' + xWwwFormUrlEncode({ { "pageToken", *nextPageToken } }); -std::string response; -gdriveHttpsRequest("/drive/v3/drives?" + queryParams, { "Authorization: Bearer " + accessToken }, {} /*extraOptions*/, //throw SysError -[&](const void* buffer, size_t bytesToWrite) { response.append(static_cast<const char*>(buffer), bytesToWrite); }, nullptr /*readRequest*/); + std::string response; + gdriveHttpsRequest("/drive/v3/drives?" + queryParams, { "Authorization: Bearer " + accessToken }, {} /*extraOptions*/, //throw SysError + [&](const void* buffer, size_t bytesToWrite) { response.append(static_cast<const char*>(buffer), bytesToWrite); }, nullptr /*readRequest*/); -JsonValue jresponse; -try { jresponse = parseJson(response); } -catch (JsonParsingError&) {} + JsonValue jresponse; + try { jresponse = parseJson(response); } + catch (JsonParsingError&) {} -/**/ nextPageToken = getPrimitiveFromJsonObject(jresponse, "nextPageToken"); -const JsonValue* drives = getChildFromJsonObject (jresponse, "drives"); -if (!drives || drives->type != JsonValue::Type::array) -throw SysError(formatGdriveErrorRaw(response)); + /**/ nextPageToken = getPrimitiveFromJsonObject(jresponse, "nextPageToken"); + const JsonValue* drives = getChildFromJsonObject (jresponse, "drives"); + if (!drives || drives->type != JsonValue::Type::array) + throw SysError(formatGdriveErrorRaw(response)); -for (const JsonValue& driveVal : drives->arrayVal) -{ - std::optional<std::string> driveId = getPrimitiveFromJsonObject(driveVal, "id"); - std::optional<std::string> driveName = getPrimitiveFromJsonObject(driveVal, "name"); - if (!driveId || !driveName) - throw SysError(formatGdriveErrorRaw(serializeJson(driveVal))); + for (const JsonValue& driveVal : drives->arrayVal) + { + std::optional<std::string> driveId = getPrimitiveFromJsonObject(driveVal, "id"); + std::optional<std::string> driveName = getPrimitiveFromJsonObject(driveVal, "name"); + if (!driveId || !driveName) + throw SysError(formatGdriveErrorRaw(serializeJson(driveVal))); - sharedDrives.push_back({ std::move(*driveId), utfTo<Zstring>(*driveName) }); + sharedDrives.push_back({ std::move(*driveId), utfTo<Zstring>(*driveName) }); + } + } + while (nextPageToken); } -} -while (nextPageToken); -} -return sharedDrives; + return sharedDrives; } enum class GdriveItemType : unsigned char { -file, -folder, -shortcut, + file, + folder, + shortcut, }; enum class FileOwner : unsigned char { -none, //"ownedByMe" not populated for items in Shared Drives. -me, -other, + none, //"ownedByMe" not populated for items in Shared Drives. + me, + other, }; struct GdriveItemDetails { -Zstring itemName; -uint64_t fileSize = 0; -time_t modTime = 0; -//--- minimize padding --- -GdriveItemType type = GdriveItemType::file; -FileOwner owner = FileOwner::none; -//------------------------ -std::string targetId; //for GdriveItemType::shortcut: https://developers.google.com/drive/api/v3/shortcuts -std::vector<std::string> parentIds; - -bool operator==(const GdriveItemDetails&) const = default; + Zstring itemName; + uint64_t fileSize = 0; + time_t modTime = 0; + //--- minimize padding --- + GdriveItemType type = GdriveItemType::file; + FileOwner owner = FileOwner::none; + //------------------------ + std::string targetId; //for GdriveItemType::shortcut: https://developers.google.com/drive/api/v3/shortcuts + std::vector<std::string> parentIds; + + bool operator==(const GdriveItemDetails&) const = default; }; @@ -1717,578 +1717,578 @@ private: class GdrivePersistentSessions; - class GdriveFileState //per-user-session! => serialize access (perf: amortized fully buffered!) +class GdriveFileState //per-user-session! => serialize access (perf: amortized fully buffered!) +{ +public: + GdriveFileState(GdriveAccessBuffer& accessBuf) : //throw SysError + /* issue getChangesCurrentToken() as the very first Google Drive query!*/ + lastSyncToken_(getChangesCurrentToken(accessBuf.getAccessToken())), //throw SysError + myDriveId_ (getMyDriveId (accessBuf.getAccessToken())), // + accessBuf_(accessBuf) + { + for (const DriveDetails& drive : getSharedDrives(accessBuf.getAccessToken())) //throw SysError + sharedDrives_.emplace(drive.driveId, drive.driveName); + } + + GdriveFileState(MemoryStreamIn<std::string>& stream, GdriveAccessBuffer& accessBuf) : //throw SysError + accessBuf_(accessBuf) { - public: - GdriveFileState(GdriveAccessBuffer& accessBuf) : //throw SysError - /* issue getChangesCurrentToken() as the very first Google Drive query!*/ - lastSyncToken_(getChangesCurrentToken(accessBuf.getAccessToken())), //throw SysError - myDriveId_ (getMyDriveId (accessBuf.getAccessToken())), // - accessBuf_(accessBuf) + lastSyncToken_ = readContainer<std::string>(stream); //SysErrorUnexpectedEos + myDriveId_ = readContainer<std::string>(stream); // + + size_t sharedDrivesCount = readNumber<uint32_t>(stream); //SysErrorUnexpectedEos + while (sharedDrivesCount-- != 0) { - for (const DriveDetails& drive : getSharedDrives(accessBuf.getAccessToken())) //throw SysError - sharedDrives_.emplace(drive.driveId, drive.driveName); + std::string driveId = readContainer<std::string>(stream); //SysErrorUnexpectedEos + std::string driveName = readContainer<std::string>(stream); // + sharedDrives_.emplace(driveId, utfTo<Zstring>(driveName)); } - GdriveFileState(MemoryStreamIn<std::string>& stream, GdriveAccessBuffer& accessBuf) : //throw SysError - accessBuf_(accessBuf) + for (;;) { - lastSyncToken_ = readContainer<std::string>(stream); //SysErrorUnexpectedEos - myDriveId_ = readContainer<std::string>(stream); // - - size_t sharedDrivesCount = readNumber<uint32_t>(stream); //SysErrorUnexpectedEos - while (sharedDrivesCount-- != 0) - { - std::string driveId = readContainer<std::string>(stream); //SysErrorUnexpectedEos - std::string driveName = readContainer<std::string>(stream); // - sharedDrives_.emplace(driveId, utfTo<Zstring>(driveName)); - } - - for (;;) - { - const std::string folderId = readContainer<std::string>(stream); //SysErrorUnexpectedEos - if (folderId.empty()) - break; - folderContents_[folderId].isKnownFolder = true; - } + const std::string folderId = readContainer<std::string>(stream); //SysErrorUnexpectedEos + if (folderId.empty()) + break; + folderContents_[folderId].isKnownFolder = true; + } - for (;;) - { - const std::string itemId = readContainer<std::string>(stream); //SysErrorUnexpectedEos - if (itemId.empty()) - break; + for (;;) + { + const std::string itemId = readContainer<std::string>(stream); //SysErrorUnexpectedEos + if (itemId.empty()) + break; - GdriveItemDetails details = {}; - details.itemName = utfTo<Zstring>(readContainer<std::string>(stream)); // - details.type = readNumber<GdriveItemType>(stream); // - details.owner = readNumber <FileOwner>(stream); // - details.fileSize = readNumber <uint64_t>(stream); //SysErrorUnexpectedEos - details.modTime = readNumber <int64_t>(stream); // - details.targetId = readContainer<std::string>(stream); // + GdriveItemDetails details = {}; + details.itemName = utfTo<Zstring>(readContainer<std::string>(stream)); // + details.type = readNumber<GdriveItemType>(stream); // + details.owner = readNumber <FileOwner>(stream); // + details.fileSize = readNumber <uint64_t>(stream); //SysErrorUnexpectedEos + details.modTime = readNumber <int64_t>(stream); // + details.targetId = readContainer<std::string>(stream); // - size_t parentsCount = readNumber<uint32_t>(stream); //SysErrorUnexpectedEos - while (parentsCount-- != 0) - details.parentIds.push_back(readContainer<std::string>(stream)); //SysErrorUnexpectedEos + size_t parentsCount = readNumber<uint32_t>(stream); //SysErrorUnexpectedEos + while (parentsCount-- != 0) + details.parentIds.push_back(readContainer<std::string>(stream)); //SysErrorUnexpectedEos - updateItemState(itemId, &details); - } + updateItemState(itemId, &details); } + } - void serialize(MemoryStreamOut<std::string>& stream) const - { - writeContainer(stream, lastSyncToken_); - writeContainer(stream, myDriveId_); + void serialize(MemoryStreamOut<std::string>& stream) const + { + writeContainer(stream, lastSyncToken_); + writeContainer(stream, myDriveId_); - writeNumber(stream, static_cast<uint32_t>(sharedDrives_.size())); - for (const auto& [driveId, driveName]: sharedDrives_) - { - writeContainer(stream, driveId); - writeContainer(stream, utfTo<std::string>(driveName)); - } + writeNumber(stream, static_cast<uint32_t>(sharedDrives_.size())); + for (const auto& [driveId, driveName]: sharedDrives_) + { + writeContainer(stream, driveId); + writeContainer(stream, utfTo<std::string>(driveName)); + } - for (const auto& [folderId, content] : folderContents_) - if (folderId.empty()) - throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); - else if (content.isKnownFolder) - writeContainer(stream, folderId); - writeContainer(stream, std::string()); //sentinel + for (const auto& [folderId, content] : folderContents_) + if (folderId.empty()) + throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); + else if (content.isKnownFolder) + writeContainer(stream, folderId); + writeContainer(stream, std::string()); //sentinel + + auto serializeItem = [&](const std::string& itemId, const GdriveItemDetails& details) + { + writeContainer (stream, itemId); + writeContainer (stream, utfTo<std::string>(details.itemName)); + writeNumber<GdriveItemType>(stream, details.type); + writeNumber <FileOwner>(stream, details.owner); + writeNumber <uint64_t>(stream, details.fileSize); + writeNumber <int64_t>(stream, details.modTime); + static_assert(sizeof(details.modTime) <= sizeof(int64_t)); //ensure cross-platform compatibility! + writeContainer(stream, details.targetId); + + writeNumber(stream, static_cast<uint32_t>(details.parentIds.size())); + for (const std::string& parentId : details.parentIds) + writeContainer(stream, parentId); + }; - auto serializeItem = [&](const std::string& itemId, const GdriveItemDetails& details) - { - writeContainer (stream, itemId); - writeContainer (stream, utfTo<std::string>(details.itemName)); - writeNumber<GdriveItemType>(stream, details.type); - writeNumber <FileOwner>(stream, details.owner); - writeNumber <uint64_t>(stream, details.fileSize); - writeNumber <int64_t>(stream, details.modTime); - static_assert(sizeof(details.modTime) <= sizeof(int64_t)); //ensure cross-platform compatibility! - writeContainer(stream, details.targetId); - - writeNumber(stream, static_cast<uint32_t>(details.parentIds.size())); - for (const std::string& parentId : details.parentIds) - writeContainer(stream, parentId); - }; + //serialize + clean up: only save items in "known folders" + items referenced by shortcuts + for (const auto& [folderId, content] : folderContents_) + if (content.isKnownFolder) + for (const auto& itItem : content.childItems) + { + const auto& [itemId, details] = *itItem; + if (itemId.empty()) + throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); + serializeItem(itemId, details); - //serialize + clean up: only save items in "known folders" + items referenced by shortcuts - for (const auto& [folderId, content] : folderContents_) - if (content.isKnownFolder) - for (const auto itItem : content.childItems) + if (details.type == GdriveItemType::shortcut) { - const auto& [itemId, details] = *itItem; - if (itemId.empty()) + if (details.targetId.empty()) throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); - 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__)); - if (auto it = itemDetails_.find(details.targetId); - it != itemDetails_.end()) - serializeItem(details.targetId, it->second); - } + if (auto it = itemDetails_.find(details.targetId); + it != itemDetails_.end()) + serializeItem(details.targetId, it->second); } - writeContainer(stream, std::string()); //sentinel - } + } + writeContainer(stream, std::string()); //sentinel + } - std::vector<Zstring /*sharedDriveName*/> listSharedDrives() const - { - std::vector<Zstring> sharedDriveNames; + std::vector<Zstring /*sharedDriveName*/> listSharedDrives() const + { + std::vector<Zstring> sharedDriveNames; - for (const auto& [driveId, driveName]: sharedDrives_) - sharedDriveNames.push_back(driveName); + for (const auto& [driveId, driveName]: sharedDrives_) + sharedDriveNames.push_back(driveName); - return sharedDriveNames; - } + return sharedDriveNames; + } - struct PathStatus - { - std::string existingItemId; - GdriveItemType existingType = GdriveItemType::file; - AfsPath existingPath; //input path =: existingPath + relPath - std::vector<Zstring> relPath; // - }; - PathStatus getPathStatus(const Zstring& sharedDriveName, const AfsPath& afsPath, bool followLeafShortcut) //throw SysError + struct PathStatus + { + std::string existingItemId; + GdriveItemType existingType = GdriveItemType::file; + AfsPath existingPath; //input path =: existingPath + relPath + std::vector<Zstring> relPath; // + }; + PathStatus getPathStatus(const Zstring& sharedDriveName, const AfsPath& afsPath, bool followLeafShortcut) //throw SysError + { + const std::string driveId = [&] { - const std::string driveId = [&] - { - if (sharedDriveName.empty()) - return myDriveId_; + if (sharedDriveName.empty()) + return myDriveId_; - auto itFound = sharedDrives_.cend(); - for (auto it = sharedDrives_.begin(); it != sharedDrives_.end(); ++it) - if (const auto& [sharedDriveId, driveName] = *it; - equalNativePath(driveName, sharedDriveName)) - { - if (itFound != sharedDrives_.end()) - throw SysError(replaceCpy(_("Cannot find %x."), L"%x", - fmtPath(getGdriveDisplayPath({{ accessBuf_.getUserEmail(), sharedDriveName}, AfsPath() }))) + L' ' + - replaceCpy(_("The name %x is used by more than one item in the folder."), L"%x", fmtPath(sharedDriveName))); - itFound = it; - } - if (itFound == sharedDrives_.end()) - throw SysError(replaceCpy(_("Cannot find %x."), L"%x", - fmtPath(getGdriveDisplayPath({{ accessBuf_.getUserEmail(), sharedDriveName}, AfsPath() })))); + auto itFound = sharedDrives_.cend(); + for (auto it = sharedDrives_.begin(); it != sharedDrives_.end(); ++it) + if (const auto& [sharedDriveId, driveName] = *it; + equalNativePath(driveName, sharedDriveName)) + { + if (itFound != sharedDrives_.end()) + throw SysError(replaceCpy(_("Cannot find %x."), L"%x", + fmtPath(getGdriveDisplayPath({{ accessBuf_.getUserEmail(), sharedDriveName}, AfsPath() }))) + L' ' + + replaceCpy(_("The name %x is used by more than one item in the folder."), L"%x", fmtPath(sharedDriveName))); + itFound = it; + } + if (itFound == sharedDrives_.end()) + throw SysError(replaceCpy(_("Cannot find %x."), L"%x", + fmtPath(getGdriveDisplayPath({{ accessBuf_.getUserEmail(), sharedDriveName}, AfsPath() })))); - return itFound->first; - }(); + return itFound->first; + }(); - const std::vector<Zstring> relPath = split(afsPath.value, FILE_NAME_SEPARATOR, SplitOnEmpty::skip); - if (relPath.empty()) - return { driveId, GdriveItemType::folder, AfsPath(), {} }; - else - return getPathStatusSub(driveId, sharedDriveName, AfsPath(), relPath, followLeafShortcut); //throw SysError - } + const std::vector<Zstring> relPath = split(afsPath.value, FILE_NAME_SEPARATOR, SplitOnEmpty::skip); + if (relPath.empty()) + return { driveId, GdriveItemType::folder, AfsPath(), {} }; + else + return getPathStatusSub(driveId, sharedDriveName, AfsPath(), relPath, followLeafShortcut); //throw SysError + } - std::string /*itemId*/ getItemId(const Zstring& sharedDriveName, const AfsPath& afsPath, bool followLeafShortcut) //throw SysError - { - const GdriveFileState::PathStatus& ps = getPathStatus(sharedDriveName, afsPath, followLeafShortcut); //throw SysError - if (ps.relPath.empty()) - return ps.existingItemId; + std::string /*itemId*/ getItemId(const Zstring& sharedDriveName, const AfsPath& afsPath, bool followLeafShortcut) //throw SysError + { + const GdriveFileState::PathStatus& ps = getPathStatus(sharedDriveName, afsPath, followLeafShortcut); //throw SysError + if (ps.relPath.empty()) + return ps.existingItemId; - const AfsPath afsPathMissingChild(nativeAppendPaths(ps.existingPath.value, ps.relPath.front())); - throw SysError(replaceCpy(_("Cannot find %x."), L"%x", fmtPath(getGdriveDisplayPath({ { accessBuf_.getUserEmail(), sharedDriveName }, afsPathMissingChild })))); - } + const AfsPath afsPathMissingChild(nativeAppendPaths(ps.existingPath.value, ps.relPath.front())); + throw SysError(replaceCpy(_("Cannot find %x."), L"%x", fmtPath(getGdriveDisplayPath({ { accessBuf_.getUserEmail(), sharedDriveName }, afsPathMissingChild })))); + } - std::pair<std::string /*itemId*/, GdriveItemDetails> getFileAttributes(const Zstring& sharedDriveName, const AfsPath& afsPath, bool followLeafShortcut) //throw SysError - { - const std::string itemId = getItemId(sharedDriveName, afsPath, followLeafShortcut); //throw SysError + std::pair<std::string /*itemId*/, GdriveItemDetails> getFileAttributes(const Zstring& sharedDriveName, const AfsPath& afsPath, bool followLeafShortcut) //throw SysError + { + const std::string itemId = getItemId(sharedDriveName, afsPath, followLeafShortcut); //throw SysError - if (afsPath.value.empty()) //root drives obviously not covered by itemDetails_ + if (afsPath.value.empty()) //root drives obviously not covered by itemDetails_ + { + GdriveItemDetails rootDetails = {}; + rootDetails.type = GdriveItemType::folder; + if (itemId == myDriveId_) { - GdriveItemDetails rootDetails = {}; - rootDetails.type = GdriveItemType::folder; - if (itemId == myDriveId_) - { - rootDetails.itemName = Zstr("My Drive"); - rootDetails.owner = FileOwner::me; - return { itemId, std::move(rootDetails) }; - } - - if (auto it = sharedDrives_.find(itemId); - it != sharedDrives_.end()) - { - rootDetails.itemName = it->second; - rootDetails.owner = FileOwner::none; - return { itemId, std::move(rootDetails) }; - } + rootDetails.itemName = Zstr("My Drive"); + rootDetails.owner = FileOwner::me; + return { itemId, std::move(rootDetails) }; } - else if (auto it = itemDetails_.find(itemId); - it != itemDetails_.end()) - return *it; - //itemId was found! => must either be a (shared) drive root or buffered in itemDetails_ - throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); + if (auto it = sharedDrives_.find(itemId); + it != sharedDrives_.end()) + { + rootDetails.itemName = it->second; + rootDetails.owner = FileOwner::none; + return { itemId, std::move(rootDetails) }; + } } + else if (auto it = itemDetails_.find(itemId); + it != itemDetails_.end()) + return *it; + + //itemId was found! => must either be a (shared) drive root or buffered in itemDetails_ + throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); + } + + std::optional<GdriveItemDetails> tryGetBufferedItemDetails(const std::string& itemId) const + { + if (auto it = itemDetails_.find(itemId); + it != itemDetails_.end()) + return it->second; + return {}; + } + + std::optional<std::vector<GdriveItem>> tryGetBufferedFolderContent(const std::string& folderId) const + { + auto it = folderContents_.find(folderId); + if (it == folderContents_.end() || !it->second.isKnownFolder) + return std::nullopt; - std::optional<GdriveItemDetails> tryGetBufferedItemDetails(const std::string& itemId) const + std::vector<GdriveItem> childItems; + for (auto itChild : it->second.childItems) { - if (auto it = itemDetails_.find(itemId); - it != itemDetails_.end()) - return it->second; - return {}; + const auto& [childId, childDetails] = *itChild; + childItems.push_back({ childId, childDetails }); } + return std::move(childItems); //[!] need std::move! + } - std::optional<std::vector<GdriveItem>> tryGetBufferedFolderContent(const std::string& folderId) const - { - auto it = folderContents_.find(folderId); - if (it == folderContents_.end() || !it->second.isKnownFolder) - return std::nullopt; + //-------------- notifications -------------- + using ItemIdDelta = std::unordered_set<std::string>; - std::vector<GdriveItem> childItems; - for (auto itChild : it->second.childItems) - { - const auto& [childId, childDetails] = *itChild; - childItems.push_back({ childId, childDetails }); - } - return std::move(childItems); //[!] need std::move! - } + struct FileStateDelta //as long as instance exists, GdriveItem will log all changed items + { + FileStateDelta() {} + private: + FileStateDelta(const std::shared_ptr<const ItemIdDelta>& cids) : changedIds(cids) {} + friend class GdriveFileState; + std::shared_ptr<const ItemIdDelta> changedIds; //lifetime is managed by caller; access *only* by GdriveFileState! + }; - //-------------- notifications -------------- - using ItemIdDelta = std::unordered_set<std::string>; + void notifyFolderContent(const FileStateDelta& stateDelta, const std::string& folderId, const std::vector<GdriveItem>& childItems) + { + folderContents_[folderId].isKnownFolder = true; - struct FileStateDelta //as long as instance exists, GdriveItem will log all changed items - { - FileStateDelta() {} - private: - FileStateDelta(const std::shared_ptr<const ItemIdDelta>& cids) : changedIds(cids) {} - friend class GdriveFileState; - std::shared_ptr<const ItemIdDelta> changedIds; //lifetime is managed by caller; access *only* by GdriveFileState! - }; + for (const GdriveItem& item : childItems) + notifyItemUpdated(stateDelta, item.itemId, &item.details); - void notifyFolderContent(const FileStateDelta& stateDelta, const std::string& folderId, const std::vector<GdriveItem>& childItems) - { - folderContents_[folderId].isKnownFolder = true; + //- should we remove parent links for items that are not children of folderId anymore (as of this update)?? => fringe case during first update! (still: maybe trigger sync?) + //- what if there are multiple folder state updates incoming in wrong order!? => notifyItemUpdated() will sort it out! + } - for (const GdriveItem& item : childItems) - notifyItemUpdated(stateDelta, item.itemId, &item.details); + void notifyItemCreated(const FileStateDelta& stateDelta, const GdriveItem& item) + { + notifyItemUpdated(stateDelta, item.itemId, &item.details); + } - //- should we remove parent links for items that are not children of folderId anymore (as of this update)?? => fringe case during first update! (still: maybe trigger sync?) - //- what if there are multiple folder state updates incoming in wrong order!? => notifyItemUpdated() will sort it out! - } + void notifyItemUpdated(const FileStateDelta& stateDelta, const GdriveItem& item) + { + notifyItemUpdated(stateDelta, item.itemId, &item.details); + } - void notifyItemCreated(const FileStateDelta& stateDelta, const GdriveItem& item) - { - notifyItemUpdated(stateDelta, item.itemId, &item.details); - } + void notifyFolderCreated(const FileStateDelta& stateDelta, const std::string& folderId, const Zstring& folderName, const std::string& parentId) + { + GdriveItemDetails details = {}; + details.itemName = folderName; + details.type = GdriveItemType::folder; + details.owner = FileOwner::me; + details.modTime = std::time(nullptr); + details.parentIds.push_back(parentId); - void notifyItemUpdated(const FileStateDelta& stateDelta, const GdriveItem& item) - { - notifyItemUpdated(stateDelta, item.itemId, &item.details); - } + //avoid needless conflicts due to different Google Drive folder modTime! + if (auto it = itemDetails_.find(folderId); it != itemDetails_.end()) + details.modTime = it->second.modTime; - void notifyFolderCreated(const FileStateDelta& stateDelta, const std::string& folderId, const Zstring& folderName, const std::string& parentId) - { - GdriveItemDetails details = {}; - details.itemName = folderName; - details.type = GdriveItemType::folder; - details.owner = FileOwner::me; - details.modTime = std::time(nullptr); - details.parentIds.push_back(parentId); + notifyItemUpdated(stateDelta, folderId, &details); + } - //avoid needless conflicts due to different Google Drive folder modTime! - if (auto it = itemDetails_.find(folderId); it != itemDetails_.end()) - details.modTime = it->second.modTime; + void notifyShortcutCreated(const FileStateDelta& stateDelta, const std::string& shortcutId, const Zstring& shortcutName, const std::string& parentId, const std::string& targetId) + { + GdriveItemDetails details = {}; + details.itemName = shortcutName; + details.type = GdriveItemType::shortcut; + details.owner = FileOwner::me; + details.modTime = std::time(nullptr); + details.targetId = targetId; + details.parentIds.push_back(parentId); - notifyItemUpdated(stateDelta, folderId, &details); - } + //avoid needless conflicts due to different Google Drive folder modTime! + if (auto it = itemDetails_.find(shortcutId); it != itemDetails_.end()) + details.modTime = it->second.modTime; - void notifyShortcutCreated(const FileStateDelta& stateDelta, const std::string& shortcutId, const Zstring& shortcutName, const std::string& parentId, const std::string& targetId) - { - GdriveItemDetails details = {}; - details.itemName = shortcutName; - details.type = GdriveItemType::shortcut; - details.owner = FileOwner::me; - details.modTime = std::time(nullptr); - details.targetId = targetId; - details.parentIds.push_back(parentId); - - //avoid needless conflicts due to different Google Drive folder modTime! - if (auto it = itemDetails_.find(shortcutId); it != itemDetails_.end()) - details.modTime = it->second.modTime; - - notifyItemUpdated(stateDelta, shortcutId, &details); - } + notifyItemUpdated(stateDelta, shortcutId, &details); + } - void notifyItemDeleted(const FileStateDelta& stateDelta, const std::string& itemId) - { - notifyItemUpdated(stateDelta, itemId, nullptr); - } + void notifyItemDeleted(const FileStateDelta& stateDelta, const std::string& itemId) + { + notifyItemUpdated(stateDelta, itemId, nullptr); + } - void notifyParentRemoved(const FileStateDelta& stateDelta, const std::string& itemId, const std::string& parentIdOld) + void notifyParentRemoved(const FileStateDelta& stateDelta, const std::string& itemId, const std::string& parentIdOld) + { + if (auto it = itemDetails_.find(itemId); it != itemDetails_.end()) { - if (auto it = itemDetails_.find(itemId); it != itemDetails_.end()) - { - GdriveItemDetails detailsNew = it->second; - std::erase_if(detailsNew.parentIds, [&](const std::string& id) { return id == parentIdOld; }); - notifyItemUpdated(stateDelta, itemId, &detailsNew); - } - else //conflict!!! - markSyncDue(); + GdriveItemDetails detailsNew = it->second; + std::erase_if(detailsNew.parentIds, [&](const std::string& id) { return id == parentIdOld; }); + notifyItemUpdated(stateDelta, itemId, &detailsNew); } + else //conflict!!! + markSyncDue(); + } - void notifyMoveAndRename(const FileStateDelta& stateDelta, const std::string& itemId, const std::string& parentIdFrom, const std::string& parentIdTo, const Zstring& newName) + void notifyMoveAndRename(const FileStateDelta& stateDelta, const std::string& itemId, const std::string& parentIdFrom, const std::string& parentIdTo, const Zstring& newName) + { + if (auto it = itemDetails_.find(itemId); it != itemDetails_.end()) { - if (auto it = itemDetails_.find(itemId); it != itemDetails_.end()) - { - GdriveItemDetails detailsNew = it->second; - detailsNew.itemName = newName; + GdriveItemDetails detailsNew = it->second; + detailsNew.itemName = newName; - std::erase_if(detailsNew.parentIds, [&](const std::string& id) { return id == parentIdFrom || id == parentIdTo; }); // - detailsNew.parentIds.push_back(parentIdTo); //not a duplicate + std::erase_if(detailsNew.parentIds, [&](const std::string& id) { return id == parentIdFrom || id == parentIdTo; }); // + detailsNew.parentIds.push_back(parentIdTo); //not a duplicate - notifyItemUpdated(stateDelta, itemId, &detailsNew); - } - else //conflict!!! - markSyncDue(); + notifyItemUpdated(stateDelta, itemId, &detailsNew); } + else //conflict!!! + markSyncDue(); + } - private: - GdriveFileState (const GdriveFileState&) = delete; - GdriveFileState& operator=(const GdriveFileState&) = delete; +private: + GdriveFileState (const GdriveFileState&) = delete; + GdriveFileState& operator=(const GdriveFileState&) = delete; - friend class GdrivePersistentSessions; + friend class GdrivePersistentSessions; - void notifyItemUpdated(const FileStateDelta& stateDelta, const std::string& itemId, const GdriveItemDetails* details) + void notifyItemUpdated(const FileStateDelta& stateDelta, const std::string& itemId, const GdriveItemDetails* details) + { + if (!stateDelta.changedIds->contains(itemId)) //no conflicting changes in the meantime? + updateItemState(itemId, details); //=> accept new state data + else //conflict? { - if (!contains(*stateDelta.changedIds, itemId)) //no conflicting changes in the meantime? - updateItemState(itemId, details); //=> accept new state data - else //conflict? - { - auto it = itemDetails_.find(itemId); - if (!details == (it == itemDetails_.end())) - if (!details || *details == it->second) - return; //notified changes match our current file state - //else: conflict!!! unclear which has the more recent data! - markSyncDue(); - } + auto it = itemDetails_.find(itemId); + if (!details == (it == itemDetails_.end())) + if (!details || *details == it->second) + return; //notified changes match our current file state + //else: conflict!!! unclear which has the more recent data! + markSyncDue(); } + } - FileStateDelta registerFileStateDelta() - { - auto deltaPtr = std::make_shared<ItemIdDelta>(); - changeLog_.push_back(deltaPtr); - return FileStateDelta(deltaPtr); - } + FileStateDelta registerFileStateDelta() + { + auto deltaPtr = std::make_shared<ItemIdDelta>(); + changeLog_.push_back(deltaPtr); + return FileStateDelta(deltaPtr); + } - bool syncIsDue() const { return std::chrono::steady_clock::now() >= lastSyncTime_ + GDRIVE_SYNC_INTERVAL; } + bool syncIsDue() const { return std::chrono::steady_clock::now() >= lastSyncTime_ + GDRIVE_SYNC_INTERVAL; } - void markSyncDue() { lastSyncTime_ = std::chrono::steady_clock::now() - GDRIVE_SYNC_INTERVAL; } + void markSyncDue() { lastSyncTime_ = std::chrono::steady_clock::now() - GDRIVE_SYNC_INTERVAL; } - void syncWithGoogle() //throw SysError - { - const ChangesDelta delta = getChangesDelta(lastSyncToken_, accessBuf_.getAccessToken()); //throw SysError + void syncWithGoogle() //throw SysError + { + const ChangesDelta delta = getChangesDelta(lastSyncToken_, accessBuf_.getAccessToken()); //throw SysError - for (const FileChange& change : delta.fileChanges) - updateItemState(change.itemId, get(change.details)); + for (const FileChange& change : delta.fileChanges) + updateItemState(change.itemId, get(change.details)); - for (const DriveChange& change : delta.driveChanges) - updateSharedDriveState(change.driveId, change.driveName); + for (const DriveChange& change : delta.driveChanges) + updateSharedDriveState(change.driveId, change.driveName); - lastSyncToken_ = delta.newStartPageToken; - lastSyncTime_ = std::chrono::steady_clock::now(); + lastSyncToken_ = delta.newStartPageToken; + lastSyncTime_ = std::chrono::steady_clock::now(); - //good to know: if item is created and deleted between polling for changes it is still reported as deleted by Google! - //Same goes for any other change that is undone in between change notification syncs. - } + //good to know: if item is created and deleted between polling for changes it is still reported as deleted by Google! + //Same goes for any other change that is undone in between change notification syncs. + } - PathStatus getPathStatusSub(const std::string& folderId, const Zstring& sharedDriveName, const AfsPath& folderPath, const std::vector<Zstring>& relPath, bool followLeafShortcut) //throw SysError + PathStatus getPathStatusSub(const std::string& folderId, const Zstring& sharedDriveName, const AfsPath& folderPath, const std::vector<Zstring>& relPath, bool followLeafShortcut) //throw SysError + { + assert(!relPath.empty()); + + auto itKnown = folderContents_.find(folderId); + if (itKnown == folderContents_.end() || !itKnown->second.isKnownFolder) { - assert(!relPath.empty()); + notifyFolderContent(registerFileStateDelta(), folderId, readFolderContent(folderId, accessBuf_.getAccessToken())); //throw SysError + //perf: always buffered, except for direct, first-time folder access! + 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__)); + } - auto itKnown = folderContents_.find(folderId); - if (itKnown == folderContents_.end() || !itKnown->second.isKnownFolder) + auto itFound = itemDetails_.cend(); + for (const DetailsIterator& itChild : itKnown->second.childItems) + //Since Google Drive has no concept of a file path, we have to roll our own "path to id" mapping => let's use the platform-native style + if (equalNativePath(itChild->second.itemName, relPath.front())) { - notifyFolderContent(registerFileStateDelta(), folderId, readFolderContent(folderId, accessBuf_.getAccessToken())); //throw SysError - //perf: always buffered, except for direct, first-time folder access! - 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__)); - } - - auto itFound = itemDetails_.cend(); - for (const DetailsIterator& itChild : itKnown->second.childItems) - //Since Google Drive has no concept of a file path, we have to roll our own "path to id" mapping => let's use the platform-native style - if (equalNativePath(itChild->second.itemName, relPath.front())) - { - if (itFound != itemDetails_.end()) - throw SysError(replaceCpy(_("Cannot find %x."), L"%x", - fmtPath(getGdriveDisplayPath({{ accessBuf_.getUserEmail(), sharedDriveName}, AfsPath(nativeAppendPaths(folderPath.value, relPath.front())) }))) + L' ' + - replaceCpy(_("The name %x is used by more than one item in the folder."), L"%x", fmtPath(relPath.front()))); + if (itFound != itemDetails_.end()) + throw SysError(replaceCpy(_("Cannot find %x."), L"%x", + fmtPath(getGdriveDisplayPath({{ accessBuf_.getUserEmail(), sharedDriveName}, AfsPath(nativeAppendPaths(folderPath.value, relPath.front())) }))) + L' ' + + replaceCpy(_("The name %x is used by more than one item in the folder."), L"%x", fmtPath(relPath.front()))); - itFound = itChild; - } + itFound = itChild; + } - if (itFound == itemDetails_.end()) - return { folderId, GdriveItemType::folder, folderPath, relPath }; //always a folder, see check before recursion above - else + if (itFound == itemDetails_.end()) + return { folderId, GdriveItemType::folder, folderPath, relPath }; //always a folder, see check before recursion above + else + { + auto getItemDetailsBuffered = [&](const std::string& itemId) -> const GdriveItemDetails& { - auto getItemDetailsBuffered = [&](const std::string& itemId) -> const GdriveItemDetails& + auto it = itemDetails_.find(itemId); + if (it == itemDetails_.end()) { - auto it = itemDetails_.find(itemId); - if (it == itemDetails_.end()) - { - notifyItemUpdated(registerFileStateDelta(), { itemId, getItemDetails(itemId, accessBuf_.getAccessToken()) }); //throw SysError - //perf: always buffered, except for direct, first-time folder access! - it = itemDetails_.find(itemId); - assert(it != itemDetails_.end()); - } - return it->second; - }; + notifyItemUpdated(registerFileStateDelta(), { itemId, getItemDetails(itemId, accessBuf_.getAccessToken()) }); //throw SysError + //perf: always buffered, except for direct, first-time folder access! + it = itemDetails_.find(itemId); + assert(it != itemDetails_.end()); + } + return it->second; + }; - const auto& [childId, childDetails] = *itFound; - const AfsPath childItemPath(nativeAppendPaths(folderPath.value, relPath.front())); - const std::vector<Zstring> childRelPath(relPath.begin() + 1, relPath.end()); + const auto& [childId, childDetails] = *itFound; + const AfsPath childItemPath(nativeAppendPaths(folderPath.value, relPath.front())); + const std::vector<Zstring> childRelPath(relPath.begin() + 1, relPath.end()); - if (childRelPath.empty()) - { - if (childDetails.type == GdriveItemType::shortcut && followLeafShortcut) - return { childDetails.targetId, getItemDetailsBuffered(childDetails.targetId).type, childItemPath, childRelPath }; - else - return { childId, childDetails.type, childItemPath, childRelPath }; - } + if (childRelPath.empty()) + { + if (childDetails.type == GdriveItemType::shortcut && followLeafShortcut) + return { childDetails.targetId, getItemDetailsBuffered(childDetails.targetId).type, childItemPath, childRelPath }; + else + return { childId, childDetails.type, childItemPath, childRelPath }; + } - switch (childDetails.type) - { - case GdriveItemType::file: //parent/file/child-rel-path... => obscure, but possible (and not an error) - return { childId, childDetails.type, childItemPath, childRelPath }; + switch (childDetails.type) + { + case GdriveItemType::file: //parent/file/child-rel-path... => obscure, but possible (and not an error) + return { childId, childDetails.type, childItemPath, childRelPath }; - case GdriveItemType::folder: - return getPathStatusSub(childId, sharedDriveName, childItemPath, childRelPath, followLeafShortcut); //throw SysError + case GdriveItemType::folder: + return getPathStatusSub(childId, sharedDriveName, childItemPath, childRelPath, followLeafShortcut); //throw SysError - case GdriveItemType::shortcut: - switch (getItemDetailsBuffered(childDetails.targetId).type) - { - case GdriveItemType::file: //parent/file-symlink/child-rel-path... => obscure, but possible (and not an error) - return { childDetails.targetId, GdriveItemType::file, childItemPath, childRelPath }; //resolve symlinks if in the *middle* of a path! + case GdriveItemType::shortcut: + switch (getItemDetailsBuffered(childDetails.targetId).type) + { + case GdriveItemType::file: //parent/file-symlink/child-rel-path... => obscure, but possible (and not an error) + return { childDetails.targetId, GdriveItemType::file, childItemPath, childRelPath }; //resolve symlinks if in the *middle* of a path! - case GdriveItemType::folder: //parent/folder-symlink/child-rel-path... => always follow - return getPathStatusSub(childDetails.targetId, sharedDriveName, childItemPath, childRelPath, followLeafShortcut); //throw SysError + case GdriveItemType::folder: //parent/folder-symlink/child-rel-path... => always follow + return getPathStatusSub(childDetails.targetId, sharedDriveName, childItemPath, childRelPath, followLeafShortcut); //throw SysError - case GdriveItemType::shortcut: //should never happen: creating shortcuts to shortcuts fails with "Internal Error" - throw SysError(replaceCpy(_("Cannot resolve symbolic link %x."), L"%x", - fmtPath(getGdriveDisplayPath({{ accessBuf_.getUserEmail(), sharedDriveName}, AfsPath(nativeAppendPaths(folderPath.value, relPath.front())) }))) + L' ' + - L"Google Drive Shortcut points to another Shortcut."); - } - break; - } - throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); + case GdriveItemType::shortcut: //should never happen: creating shortcuts to shortcuts fails with "Internal Error" + throw SysError(replaceCpy(_("Cannot resolve symbolic link %x."), L"%x", + fmtPath(getGdriveDisplayPath({{ accessBuf_.getUserEmail(), sharedDriveName}, AfsPath(nativeAppendPaths(folderPath.value, relPath.front())) }))) + L' ' + + L"Google Drive Shortcut points to another Shortcut."); + } + break; } + throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); } + } - void updateItemState(const std::string& itemId, const GdriveItemDetails* details) - { - auto it = itemDetails_.find(itemId); - if (!details == (it == itemDetails_.end())) - if (!details || *details == it->second) //notified changes match our current file state - return; //=> avoid misleading changeLog_ entries after Google Drive sync!!! + void updateItemState(const std::string& itemId, const GdriveItemDetails* details) + { + auto it = itemDetails_.find(itemId); + if (!details == (it == itemDetails_.end())) + if (!details || *details == it->second) //notified changes match our current file state + return; //=> avoid misleading changeLog_ entries after Google Drive sync!!! - //update change logs (and clean up obsolete entries) - std::erase_if(changeLog_, [&](std::weak_ptr<ItemIdDelta>& weakPtr) + //update change logs (and clean up obsolete entries) + std::erase_if(changeLog_, [&](std::weak_ptr<ItemIdDelta>& weakPtr) + { + if (std::shared_ptr<ItemIdDelta> iid = weakPtr.lock()) { - if (std::shared_ptr<ItemIdDelta> iid = weakPtr.lock()) - { - (*iid).insert(itemId); - return false; - } - else - return true; - }); + (*iid).insert(itemId); + return false; + } + else + return true; + }); - //update file state - if (details) + //update file state + if (details) + { + if (it != itemDetails_.end()) //update { - 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!? + if (it->second.type != details->type) + throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); //WTF!? - std::vector<std::string> parentIdsNew = details->parentIds; - std::vector<std::string> parentIdsRemoved = it->second.parentIds; - std::erase_if(parentIdsNew, [&](const std::string& id) { return std::find(it->second.parentIds.begin(), it->second.parentIds.end(), id) != it->second.parentIds.end(); }); - std::erase_if(parentIdsRemoved, [&](const std::string& id) { return std::find(details->parentIds.begin(), details->parentIds.end(), id) != details->parentIds.end(); }); + std::vector<std::string> parentIdsNew = details->parentIds; + std::vector<std::string> parentIdsRemoved = it->second.parentIds; + std::erase_if(parentIdsNew, [&](const std::string& id) { return std::find(it->second.parentIds.begin(), it->second.parentIds.end(), id) != it->second.parentIds.end(); }); + std::erase_if(parentIdsRemoved, [&](const std::string& id) { return std::find(details->parentIds.begin(), details->parentIds.end(), id) != details->parentIds.end(); }); - for (const std::string& parentId : parentIdsNew) - folderContents_[parentId].childItems.push_back(it); //new insert => no need for duplicate check + for (const std::string& parentId : parentIdsNew) + folderContents_[parentId].childItems.push_back(it); //new insert => no need for duplicate check - for (const std::string& parentId : parentIdsRemoved) - if (auto itP = folderContents_.find(parentId); itP != folderContents_.end()) - std::erase_if(itP->second.childItems, [&](auto itChild) { return itChild == it; }); - //if all parents are removed, Google Drive will (recursively) delete the item => don't prematurely do this now: wait for change notifications! - //OR: item without parents located in "Shared with me", but referenced via Shortcut => don't remove!!! + for (const std::string& parentId : parentIdsRemoved) + if (auto itP = folderContents_.find(parentId); itP != folderContents_.end()) + std::erase_if(itP->second.childItems, [&](auto itChild) { return itChild == it; }); + //if all parents are removed, Google Drive will (recursively) delete the item => don't prematurely do this now: wait for change notifications! + //OR: item without parents located in "Shared with me", but referenced via Shortcut => don't remove!!! - it->second = *details; - } - else //create - { - auto itNew = itemDetails_.emplace(itemId, *details).first; + it->second = *details; + } + else //create + { + auto itNew = itemDetails_.emplace(itemId, *details).first; - for (const std::string& parentId : details->parentIds) - folderContents_[parentId].childItems.push_back(itNew); //new insert => no need for duplicate check - } + for (const std::string& parentId : details->parentIds) + folderContents_[parentId].childItems.push_back(itNew); //new insert => no need for duplicate check } - else //delete + } + else //delete + { + if (it != itemDetails_.end()) { - if (it != itemDetails_.end()) - { - for (const std::string& parentId : it->second.parentIds) //1. delete from parent folders - if (auto itP = folderContents_.find(parentId); itP != folderContents_.end()) - std::erase_if(itP->second.childItems, [&](auto itChild) { return itChild == it; }); + for (const std::string& parentId : it->second.parentIds) //1. delete from parent folders + if (auto itP = folderContents_.find(parentId); itP != folderContents_.end()) + std::erase_if(itP->second.childItems, [&](auto itChild) { return itChild == it; }); - itemDetails_.erase(it); - } + itemDetails_.erase(it); + } - if (auto itP = folderContents_.find(itemId); - itP != folderContents_.end()) - { - for (auto itChild : itP->second.childItems) //2. delete as parent from child items (don't wait for change notifications of children) - std::erase_if(itChild->second.parentIds, [&](const std::string& id) { return id == itemId; }); - folderContents_.erase(itP); - } + if (auto itP = folderContents_.find(itemId); + itP != folderContents_.end()) + { + for (auto itChild : itP->second.childItems) //2. delete as parent from child items (don't wait for change notifications of children) + std::erase_if(itChild->second.parentIds, [&](const std::string& id) { return id == itemId; }); + folderContents_.erase(itP); } } + } - void updateSharedDriveState(const std::string& driveId, const Zstring& driveName /*empty if shared drive was deleted*/) + void updateSharedDriveState(const std::string& driveId, const Zstring& driveName /*empty if shared drive was deleted*/) + { + if (!driveName.empty()) + sharedDrives_[driveId] = driveName; + else //delete { - if (!driveName.empty()) - sharedDrives_[driveId] = driveName; - else //delete - { - sharedDrives_.erase(driveId); + sharedDrives_.erase(driveId); - //when a shared drive is deleted, we also receive change notifications for the contained files: nice! - if (auto itP = folderContents_.find(driveId); - itP != folderContents_.end()) - { - for (auto itChild : itP->second.childItems) //delete as parent from child items (don't wait for change notifications of children) - std::erase_if(itChild->second.parentIds, [&](const std::string& id) { return id == driveId; }); - folderContents_.erase(itP); - } + //when a shared drive is deleted, we also receive change notifications for the contained files: nice! + if (auto itP = folderContents_.find(driveId); + itP != folderContents_.end()) + { + for (auto itChild : itP->second.childItems) //delete as parent from child items (don't wait for change notifications of children) + std::erase_if(itChild->second.parentIds, [&](const std::string& id) { return id == driveId; }); + folderContents_.erase(itP); } } + } - using DetailsIterator = std::unordered_map<std::string, GdriveItemDetails>::iterator; + using DetailsIterator = std::unordered_map<std::string, GdriveItemDetails>::iterator; - struct FolderContent - { - bool isKnownFolder = false; //=we've seen its full content at least once; further changes are calculated via change notifications! - std::vector<DetailsIterator> childItems; - }; - std::unordered_map<std::string /*folderId*/, FolderContent> folderContents_; - std::unordered_map<std::string /*itemId*/, GdriveItemDetails> itemDetails_; //contains ALL known, existing items! + struct FolderContent + { + bool isKnownFolder = false; //=we've seen its full content at least once; further changes are calculated via change notifications! + std::vector<DetailsIterator> childItems; + }; + std::unordered_map<std::string /*folderId*/, FolderContent> folderContents_; + std::unordered_map<std::string /*itemId*/, GdriveItemDetails> itemDetails_; //contains ALL known, existing items! - std::string lastSyncToken_; //marker corresponding to last sync with Google's change notifications - std::chrono::steady_clock::time_point lastSyncTime_ = std::chrono::steady_clock::now() - GDRIVE_SYNC_INTERVAL; //... with Google Drive (default: sync is due) + std::string lastSyncToken_; //marker corresponding to last sync with Google's change notifications + std::chrono::steady_clock::time_point lastSyncTime_ = std::chrono::steady_clock::now() - GDRIVE_SYNC_INTERVAL; //... with Google Drive (default: sync is due) - std::vector<std::weak_ptr<ItemIdDelta>> changeLog_; //track changed items since FileStateDelta was created (includes sync with Google + our own intermediate change notifications) + std::vector<std::weak_ptr<ItemIdDelta>> changeLog_; //track changed items since FileStateDelta was created (includes sync with Google + our own intermediate change notifications) - std::string myDriveId_; - std::unordered_map<std::string /*driveId*/, Zstring /*driveName*/> sharedDrives_; - GdriveAccessBuffer& accessBuf_; - }; + std::string myDriveId_; + std::unordered_map<std::string /*driveId*/, Zstring /*driveName*/> sharedDrives_; + GdriveAccessBuffer& accessBuf_; +}; //========================================================================================== //========================================================================================== @@ -3035,14 +3035,14 @@ private: bool isNullFileSystem() const override { return gdriveLogin_.email.empty(); } - int compareDeviceSameAfsType(const AbstractFileSystem& afsRhs) const override + std::weak_ordering compareDeviceSameAfsType(const AbstractFileSystem& afsRhs) const override { const GdriveLogin& lhs = gdriveLogin_; const GdriveLogin& rhs = static_cast<const GdriveFileSystem&>(afsRhs).gdriveLogin_; - if (const int rv = compareAsciiNoCase(lhs.email, rhs.email); - rv != 0) - return rv; + if (const std::weak_ordering cmp = compareAsciiNoCase(lhs.email, rhs.email); + std::is_neq(cmp)) + return cmp; return compareNativePath(lhs.sharedDriveName, rhs.sharedDriveName); } diff --git a/FreeFileSync/Source/afs/native.cpp b/FreeFileSync/Source/afs/native.cpp index 7c2877ae..4bcaf2a7 100644 --- a/FreeFileSync/Source/afs/native.cpp +++ b/FreeFileSync/Source/afs/native.cpp @@ -382,11 +382,9 @@ private: bool isNullFileSystem() const override { return rootPath_.empty(); } - int compareDeviceSameAfsType(const AbstractFileSystem& afsRhs) const override + std::weak_ordering compareDeviceSameAfsType(const AbstractFileSystem& afsRhs) const override { - const Zstring& rootPathRhs = static_cast<const NativeFileSystem&>(afsRhs).rootPath_; - - return compareNativePath(rootPath_, rootPathRhs); + return compareNativePath(rootPath_, static_cast<const NativeFileSystem&>(afsRhs).rootPath_); } //---------------------------------------------------------------------------------------------------------------- @@ -580,7 +578,7 @@ private: { //perf test: detecting different volumes by path is ~30 times faster than having ::MoveFileEx() fail with ERROR_NOT_SAME_DEVICE (6µs vs 190µs) //=> maybe we can even save some actual I/O in some cases? - if (compareDeviceSameAfsType(pathTo.afsDevice.ref()) != 0) + if (std::is_neq(compareDeviceSameAfsType(pathTo.afsDevice.ref()))) throw ErrorMoveUnsupported(replaceCpy(replaceCpy(_("Cannot move file %x to %y."), L"%x", L'\n' + fmtPath(getDisplayPath(pathFrom))), L"%y", L'\n' + fmtPath(AFS::getDisplayPath(pathTo))), diff --git a/FreeFileSync/Source/afs/sftp.cpp b/FreeFileSync/Source/afs/sftp.cpp index 6d23d330..b7760b91 100644 --- a/FreeFileSync/Source/afs/sftp.cpp +++ b/FreeFileSync/Source/afs/sftp.cpp @@ -121,25 +121,18 @@ struct SshSessionId bool allowZlib = false; //timeoutSec, traverserChannelsPerConnection => irrelevant for session equality }; + std::weak_ordering operator<=>(const SshSessionId& lhs, const SshSessionId& rhs) { //exactly the type of case insensitive comparison we need for server names! - if (const int cmp = compareAsciiNoCase(lhs.server, rhs.server); //https://docs.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfow#IDNs - cmp != 0) - return cmp <=> 0; - - if (lhs.port != rhs.port) - return lhs.port <=> rhs.port; - - if (const std::strong_ordering cmp = lhs.username <=> rhs.username; //case sensitive! - cmp != std::strong_ordering::equal) + if (const std::weak_ordering cmp = compareAsciiNoCase(lhs.server, rhs.server); //https://docs.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfow#IDNs + std::is_neq(cmp)) return cmp; - if (lhs.allowZlib != rhs.allowZlib) - return lhs.allowZlib <=> rhs.allowZlib; - - if (lhs.authType != rhs.authType) - return lhs.authType <=> rhs.authType; + if (const std::strong_ordering cmp = std::tie(lhs.port, lhs.username, lhs.authType, lhs.allowZlib) <=> //username: case sensitive! + std::tie(rhs.port, rhs.username, rhs.authType, rhs.allowZlib); + std::is_neq(cmp)) + return cmp; switch (lhs.authType) { @@ -148,18 +141,19 @@ std::weak_ordering operator<=>(const SshSessionId& lhs, const SshSessionId& rhs) case SftpAuthType::keyFile: if (const std::strong_ordering cmp = lhs.password <=> rhs.password; //case sensitive! - cmp != std::strong_ordering::equal) + std::is_neq(cmp)) return cmp; return lhs.privateKeyFilePath <=> rhs.privateKeyFilePath; //case sensitive! case SftpAuthType::agent: - return std::strong_ordering::equal; + return std::weak_ordering::equivalent; } assert(false); - return std::strong_ordering::equal; + return std::weak_ordering::equivalent; } + std::string getLibssh2Path(const AfsPath& afsPath) { return utfTo<std::string>(getServerRelPath(afsPath)); @@ -1533,21 +1527,21 @@ private: bool isNullFileSystem() const override { return login_.server.empty(); } - int compareDeviceSameAfsType(const AbstractFileSystem& afsRhs) const override + std::weak_ordering compareDeviceSameAfsType(const AbstractFileSystem& afsRhs) const override { const SftpLogin& lhs = login_; const SftpLogin& rhs = static_cast<const SftpFileSystem&>(afsRhs).login_; //exactly the type of case insensitive comparison we need for server names! - if (const int rv = compareAsciiNoCase(lhs.server, rhs.server); //https://docs.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfow#IDNs - rv != 0) - return rv; + if (const std::weak_ordering cmp = compareAsciiNoCase(lhs.server, rhs.server); //https://docs.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfow#IDNs + std::is_neq(cmp)) + return cmp; //port does NOT create a *different* data source!!! -> same thing for password! //consider username: different users may have different views and folder access rights! - return compareString(lhs.username, rhs.username); //case sensitive! + return lhs.username <=> rhs.username; //case sensitive! } //---------------------------------------------------------------------------------------------------------------- @@ -1761,7 +1755,7 @@ private: L"%y", L'\n' + fmtPath(AFS::getDisplayPath(pathTo))); }; - if (compareDeviceSameAfsType(pathTo.afsDevice.ref()) != 0) + if (std::is_neq(compareDeviceSameAfsType(pathTo.afsDevice.ref()))) throw ErrorMoveUnsupported(generateErrorMsg(), _("Operation not supported between different devices.")); try diff --git a/FreeFileSync/Source/application.cpp b/FreeFileSync/Source/application.cpp index 89cc655b..2b63e321 100644 --- a/FreeFileSync/Source/application.cpp +++ b/FreeFileSync/Source/application.cpp @@ -185,7 +185,7 @@ void Application::OnUnhandledException() //handles both wxApp::OnInit() + wxApp: std::cerr << utfTo<std::string>(titleFmt + SPACED_DASH) << e.what() << '\n'; terminateProcess(FFS_EXIT_EXCEPTION); } - //catch (...) -> let it crash and create mini dump!!! + //catch (...) -> Windows: let it crash and create mini dump!!! Linux/macOS: std::exception::what() logged to console } diff --git a/FreeFileSync/Source/base/algorithm.cpp b/FreeFileSync/Source/base/algorithm.cpp index 0a70a2d0..3322f120 100644 --- a/FreeFileSync/Source/base/algorithm.cpp +++ b/FreeFileSync/Source/base/algorithm.cpp @@ -765,7 +765,7 @@ void fff::redetermineSyncDirection(const std::vector<std::pair<BaseFolderPair*, ( //*INDENT-OFF* for (const auto& [baseFolder, dirCfg] : directCfgs) - if (!contains(allEqualPairs, baseFolder)) + if (!allEqualPairs.contains(baseFolder)) { auto it = lastSyncStates.find(baseFolder); const InSyncFolder* lastSyncState = it != lastSyncStates.end() ? &it->second.ref() : nullptr; @@ -1690,7 +1690,7 @@ void TempFileBuffer::createTempFiles(const std::set<FileDescriptor>& workLoad, P for (const FileDescriptor& descr : workLoad) { - assert(!contains(tempFilePaths_, descr)); //ensure correct stats, NO overwrite-copy => caller-contract! + assert(!tempFilePaths_.contains(descr)); //ensure correct stats, NO overwrite-copy => caller-contract! MemoryStreamOut<std::string> cookie; //create hash to distinguish different versions and file locations writeNumber (cookie, descr.attr.modTime); diff --git a/FreeFileSync/Source/base/comparison.cpp b/FreeFileSync/Source/base/comparison.cpp index 431f6719..69b85dac 100644 --- a/FreeFileSync/Source/base/comparison.cpp +++ b/FreeFileSync/Source/base/comparison.cpp @@ -608,8 +608,9 @@ std::list<std::shared_ptr<BaseFolderPair>> ComparisonBuffer::compareByContent(co ParallelOps& posL = bwl.parallelOpsL; ParallelOps& posR = bwl.parallelOpsR; const size_t newTaskCount = std::min<size_t>({ 1 - posL.current, 1 - posR.current, bwl.filesToCompareBytewise.size() }); - if (&posL != &posR) posL.current += newTaskCount; // - /**/ posR.current += newTaskCount; //consider aliasing! + if (&posL != &posR) + posL.current += newTaskCount; // + posR.current += newTaskCount; //consider aliasing! for (size_t i = 0; i < newTaskCount; ++i) { @@ -942,7 +943,7 @@ std::shared_ptr<BaseFolderPair> ComparisonBuffer::performComparison(const Resolv } Zstring excludefilterFailedRead; - if (contains(failedReads, Zstring())) //empty path if read-error for whole base directory + if (failedReads.contains(Zstring())) //empty path if read-error for whole base directory excludefilterFailedRead += Zstr("*\n"); else for (const auto& [relPath, errorMsg] : failedReads) @@ -1027,7 +1028,7 @@ FolderComparison fff::compare(WarningDialogs& warnings, if (resInfo.resolvedPairs.size() != fpCfgList.size()) throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); - auto basefolderExisting = [&](const AbstractPath& folderPath) { return contains(resInfo.existingBaseFolders, folderPath); }; + auto basefolderExisting = [&](const AbstractPath& folderPath) { return resInfo.existingBaseFolders.contains(folderPath); }; std::vector<std::pair<ResolvedFolderPair, FolderPairCfg>> workLoad; diff --git a/FreeFileSync/Source/base/db_file.cpp b/FreeFileSync/Source/base/db_file.cpp index de9741b0..ed32e884 100644 --- a/FreeFileSync/Source/base/db_file.cpp +++ b/FreeFileSync/Source/base/db_file.cpp @@ -5,6 +5,7 @@ // ***************************************************************************** #include "db_file.h" +#include <bit> //std::endian #include <zen/guid.h> #include <zen/crc.h> #include <zen/build_info.h> @@ -46,7 +47,7 @@ using DbStreams = std::unordered_map<UniqueId, SessionData>; //list of streams b template <SelectedSide side> inline AbstractPath getDatabaseFilePath(const BaseFolderPair& baseFolder) { - static_assert(usingLittleEndian()); + static_assert(std::endian::native == std::endian::little); /* Windows, Linux, macOS considerations for uniform database format: - different file IDs: no, but the volume IDs are different! - problem with case sensitivity: no @@ -590,7 +591,7 @@ private: //delete removed items (= "in-sync") from database std::erase_if(dbFiles, [&](const InSyncFolder::FileList::value_type& v) { - if (contains(toPreserve, v.first)) + if (toPreserve.contains(v.first)) return false; //all items not existing in "currentFiles" have either been deleted meanwhile or been excluded via filter: const Zstring& itemRelPath = nativeAppendPaths(parentRelPath, v.first); @@ -627,7 +628,7 @@ private: //delete removed items (= "in-sync") from database std::erase_if(dbSymlinks, [&](const InSyncFolder::SymlinkList::value_type& v) { - if (contains(toPreserve, v.first)) + if (toPreserve.contains(v.first)) return false; //all items not existing in "currentSymlinks" have either been deleted meanwhile or been excluded via filter: const Zstring& itemRelPath = nativeAppendPaths(parentRelPath, v.first); diff --git a/FreeFileSync/Source/base/dir_lock.cpp b/FreeFileSync/Source/base/dir_lock.cpp index 9e247b10..b6ca33c7 100644 --- a/FreeFileSync/Source/base/dir_lock.cpp +++ b/FreeFileSync/Source/base/dir_lock.cpp @@ -536,7 +536,7 @@ private: void tidyUp() //remove obsolete entries { std::erase_if(locksByGuid_, [](const auto& v) { return !v.second.lock(); }); - std::erase_if(guidByPath_, [&](const auto& v) { return !contains(locksByGuid_, v.second); }); + std::erase_if(guidByPath_, [&](const auto& v) { return !locksByGuid_.contains(v.second); }); } std::map<Zstring, UniqueId> guidByPath_; //lockFilePath |-> GUID; n:1; locks can be referenced by a lockFilePath or alternatively a GUID diff --git a/FreeFileSync/Source/base/dir_lock.h b/FreeFileSync/Source/base/dir_lock.h index 1895c0e7..269dc7d1 100644 --- a/FreeFileSync/Source/base/dir_lock.h +++ b/FreeFileSync/Source/base/dir_lock.h @@ -15,16 +15,15 @@ namespace fff { -/* -RAII structure to place a directory lock against other FFS processes: - - recursive locking supported, even with alternate lockfile names, e.g. via symlinks, network mounts, case-differences etc. - - ownership shared between all object instances refering to a specific lock location(= GUID) - - can be copied safely and efficiently! (ref-counting) - - detects and resolves abandoned locks (instantly if lock is associated with local pc, else after 30 seconds) - - temporary locks created during abandoned lock resolution keep "lockFilePath"'s extension - - race-free (Windows, almost on Linux(NFS)) - - NOT thread-safe! (1. global LockAdmin 2. locks for directory aliases should be created sequentially to detect duplicate locks!) -*/ +/* RAII structure to place a directory lock against other FFS processes: + - recursive locking supported, even with alternate lockfile names, e.g. via symlinks, network mounts, case-differences etc. + - ownership shared between all object instances refering to a specific lock location(= GUID) + - can be copied safely and efficiently! (ref-counting) + - detects and resolves abandoned locks (instantly if lock is associated with local pc, else after 30 seconds) + - temporary locks created during abandoned lock resolution keep "lockFilePath"'s extension + - race-free (Windows, almost on Linux(NFS)) + - NOT thread-safe! (1. global LockAdmin 2. locks for directory aliases should be created sequentially to detect duplicate locks!) */ + //while waiting for the lock using DirLockCallback = std::function<void(const std::wstring& msg)>; //throw X diff --git a/FreeFileSync/Source/base/file_hierarchy.h b/FreeFileSync/Source/base/file_hierarchy.h index 14e1cbb6..52000203 100644 --- a/FreeFileSync/Source/base/file_hierarchy.h +++ b/FreeFileSync/Source/base/file_hierarchy.h @@ -388,7 +388,7 @@ public: static const T* retrieve(ObjectIdConst id) //returns nullptr if object is not valid anymore { - return static_cast<const T*>(zen::contains(activeObjects_, id) ? id : nullptr); + return static_cast<const T*>(activeObjects_.contains(id) ? id : nullptr); } static T* retrieve(ObjectId id) { return const_cast<T*>(retrieve(static_cast<ObjectIdConst>(id))); } diff --git a/FreeFileSync/Source/base/parallel_scan.h b/FreeFileSync/Source/base/parallel_scan.h index 9f44f89d..69d644b4 100644 --- a/FreeFileSync/Source/base/parallel_scan.h +++ b/FreeFileSync/Source/base/parallel_scan.h @@ -22,22 +22,9 @@ struct DirectoryKey AbstractPath folderPath; FilterRef filter; SymLinkHandling handleSymlinks = SymLinkHandling::exclude; -}; - -inline -std::weak_ordering operator<=>(const DirectoryKey& lhs, const DirectoryKey& rhs) -{ - if (const std::strong_ordering cmp = lhs.handleSymlinks <=> rhs.handleSymlinks; - cmp != std::strong_ordering::equal) - return cmp; - - if (const std::weak_ordering cmp = lhs.folderPath <=> rhs.folderPath; - cmp != std::weak_ordering::equivalent) - return cmp; - - return lhs.filter.ref() <=> rhs.filter.ref(); -} + std::weak_ordering operator<=>(const DirectoryKey&) const = default; +}; struct DirectoryValue diff --git a/FreeFileSync/Source/base/path_filter.cpp b/FreeFileSync/Source/base/path_filter.cpp index f50f5def..36e75bfb 100644 --- a/FreeFileSync/Source/base/path_filter.cpp +++ b/FreeFileSync/Source/base/path_filter.cpp @@ -16,15 +16,15 @@ using namespace zen; using namespace fff; -std::strong_ordering PathFilter::operator<=>(const PathFilter& rhs) const +std::strong_ordering fff::operator<=>(const FilterRef& lhs, const FilterRef& rhs) { //caveat: typeid returns static type for pointers, dynamic type for references!!! - if (const std::strong_ordering cmp = std::type_index(typeid(*this)) <=> std::type_index(typeid(rhs)); - cmp != std::strong_ordering::equal) + if (const std::strong_ordering cmp = std::type_index(typeid(lhs.ref())) <=> + std::type_index(typeid(rhs.ref())); + std::is_neq(cmp)) return cmp; - //lhs, rhs have same type: - return compareSameType(rhs) <=> 0; + return lhs.ref().compareSameType(rhs.ref()); } @@ -342,25 +342,13 @@ bool NameFilter::isNull() const } -int NameFilter::compareSameType(const PathFilter& other) const +std::strong_ordering NameFilter::compareSameType(const PathFilter& other) const { assert(typeid(*this) == typeid(other)); //always given in this context! - const NameFilter& otherName = static_cast<const NameFilter&>(other); - - if (const std::strong_ordering cmp = includeMasksFileFolder <=> otherName.includeMasksFileFolder; - cmp != std::strong_ordering::equal) - return cmp < 0 ? -1 : 1; - - if (const std::strong_ordering cmp = includeMasksFolder <=> otherName.includeMasksFolder; - cmp != std::strong_ordering::equal) - return cmp < 0 ? -1 : 1; - - if (const std::strong_ordering cmp = excludeMasksFileFolder <=> otherName.excludeMasksFileFolder; - cmp != std::strong_ordering::equal) - return cmp < 0 ? -1 : 1; - - const std::strong_ordering cmp = excludeMasksFolder <=> otherName.excludeMasksFolder; - return cmp == std::strong_ordering::equal ? 0 : (cmp < 0 ? -1 : 1); + const NameFilter& lhs = *this; + const NameFilter& rhs = static_cast<const NameFilter&>(other); + return std::tie(lhs.includeMasksFileFolder, lhs.includeMasksFolder, lhs.excludeMasksFileFolder, lhs.excludeMasksFolder) <=> + std::tie(rhs.includeMasksFileFolder, rhs.includeMasksFolder, rhs.excludeMasksFileFolder, rhs.excludeMasksFolder); } diff --git a/FreeFileSync/Source/base/path_filter.h b/FreeFileSync/Source/base/path_filter.h index 73ac97ab..a972bfe7 100644 --- a/FreeFileSync/Source/base/path_filter.h +++ b/FreeFileSync/Source/base/path_filter.h @@ -27,6 +27,8 @@ namespace fff class PathFilter; using FilterRef = zen::SharedRef<const PathFilter>; +std::strong_ordering operator<=>(const FilterRef& lhs, const FilterRef& rhs); //fix GCC warning: "... has not been declared within ?fff + const Zchar FILTER_ITEM_SEPARATOR = Zstr('|'); class PathFilter @@ -43,10 +45,10 @@ public: virtual FilterRef copyFilterAddingExclusion(const Zstring& excludePhrase) const = 0; - std::strong_ordering operator<=>(const PathFilter& other) const; - private: - virtual int compareSameType(const PathFilter& other) const = 0; //assumes typeid(*this) == typeid(other)! + friend std::strong_ordering operator<=>(const FilterRef& lhs, const FilterRef& rhs); + + virtual std::strong_ordering compareSameType(const PathFilter& other) const = 0; //assumes typeid(*this) == typeid(other)! }; @@ -63,7 +65,7 @@ public: FilterRef copyFilterAddingExclusion(const Zstring& excludePhrase) const override; private: - int compareSameType(const PathFilter& other) const override { assert(typeid(*this) == typeid(other)); return 0; } + std::strong_ordering compareSameType(const PathFilter& other) const override { assert(typeid(*this) == typeid(other)); return std::strong_ordering::equal; } }; @@ -83,7 +85,7 @@ public: private: friend class CombinedFilter; - int compareSameType(const PathFilter& other) const override; + std::strong_ordering compareSameType(const PathFilter& other) const override; std::vector<Zstring> includeMasksFileFolder; // std::vector<Zstring> includeMasksFolder; //upper-case + Unicode-normalized by construction @@ -103,7 +105,7 @@ public: FilterRef copyFilterAddingExclusion(const Zstring& excludePhrase) const override; private: - int compareSameType(const PathFilter& other) const override; + std::strong_ordering compareSameType(const PathFilter& other) const override; const NameFilter first_; const NameFilter second_; @@ -182,17 +184,18 @@ FilterRef CombinedFilter::copyFilterAddingExclusion(const Zstring& excludePhrase inline -int CombinedFilter::compareSameType(const PathFilter& other) const +std::strong_ordering CombinedFilter::compareSameType(const PathFilter& other) const { assert(typeid(*this) == typeid(other)); //always given in this context! - const CombinedFilter& otherComb = static_cast<const CombinedFilter&>(other); + const CombinedFilter& lhs = *this; + const CombinedFilter& rhs = static_cast<const CombinedFilter&>(other); - if (const int cmp = first_.compareSameType(otherComb.first_); - cmp != 0) + if (const std::strong_ordering cmp = lhs.first_.compareSameType(rhs.first_); + std::is_neq(cmp)) return cmp; - return second_.compareSameType(otherComb.second_); + return lhs.second_.compareSameType(rhs.second_); } diff --git a/FreeFileSync/Source/base/resolve_path.cpp b/FreeFileSync/Source/base/resolve_path.cpp index b1c56898..4aa4d58d 100644 --- a/FreeFileSync/Source/base/resolve_path.cpp +++ b/FreeFileSync/Source/base/resolve_path.cpp @@ -128,24 +128,24 @@ std::optional<Zstring> tryResolveMacro(const Zstring& macro) //macro without %-c if (equalAsciiNoCase(macro, Zstr("WeekDay"))) { - const int weekDayStartSunday = stringTo<int>(formatTime(Zstr("%w"))); //[0 == Sunday, 6 == Saturday] => not localized! + const int weekDayStartSunday = stringTo<int>(formatTime(Zstr("%w"))); //[0 (Sunday), 6 (Saturday)] => not localized! //alternative 1: use "%u": ISO 8601 weekday as number with Monday as 1 (1-7) => newer standard than %w //alternative 2: ::mktime() + std::tm::tm_wday const int weekDayStartMonday = (weekDayStartSunday + 6) % 7; //+6 == -1 in Z_7 - // [0 == Monday, 6 == Sunday] + // [0-Monday, 6-Sunday] + + const int weekDayStartLocal = ((weekDayStartMonday + 7 - static_cast<int>(getFirstDayOfWeek())) % 7) + 1; + //[1 (local first day of week), 7 (local last day of week)] - int weekDayStartLocal = weekDayStartMonday + 1; //[1 == Monday, 7 == Sunday] return numberTo<Zstring>(weekDayStartLocal); } - //try to resolve as environment variables if (std::optional<Zstring> value = getEnvironmentVar(macro)) return *value; - return {}; } diff --git a/FreeFileSync/Source/base/structures.cpp b/FreeFileSync/Source/base/structures.cpp index 5ce995d9..e1973ab3 100644 --- a/FreeFileSync/Source/base/structures.cpp +++ b/FreeFileSync/Source/base/structures.cpp @@ -62,7 +62,7 @@ std::wstring fff::getVariantNameWithSymbol(SyncVariant var) case SyncVariant::twoWay: return _("Two way") + L" <->"; case SyncVariant::mirror: return _("Mirror") + L" ->"; case SyncVariant::update: return _("Update") + L" >"; - case SyncVariant::custom: return _("Custom") + L" ?>"; + case SyncVariant::custom: return _("Custom") + L" <>"; //*INDENT-ON* } assert(false); @@ -172,21 +172,16 @@ std::wstring fff::getSymbol(CompareFileResult cmpRes) { switch (cmpRes) { - case FILE_LEFT_SIDE_ONLY: - return L"only <-"; - case FILE_RIGHT_SIDE_ONLY: - return L"only ->"; - case FILE_LEFT_NEWER: - return L"newer <-"; - case FILE_RIGHT_NEWER: - return L"newer ->"; - case FILE_DIFFERENT_CONTENT: - return L"!="; + //*INDENT-OFF* + case FILE_LEFT_SIDE_ONLY: return L"only <-"; + case FILE_RIGHT_SIDE_ONLY: return L"only ->"; + case FILE_LEFT_NEWER: return L"newer <-"; + case FILE_RIGHT_NEWER: return L"newer ->"; + case FILE_DIFFERENT_CONTENT: return L"!="; case FILE_EQUAL: - case FILE_DIFFERENT_METADATA: //= sub-category of equal! - return L"'=="; //added quotation mark to avoid error in Excel cell when exporting to *.cvs - case FILE_CONFLICT: - return L"conflict"; + case FILE_DIFFERENT_METADATA: /*= sub-category of equal!*/ return L"'=="; //added quotation mark to avoid error in Excel cell when exporting to *.cvs + case FILE_CONFLICT: return L"conflict"; + //*INDENT-ON* } assert(false); return std::wstring(); @@ -197,34 +192,23 @@ std::wstring fff::getSymbol(SyncOperation op) { switch (op) { - case SO_CREATE_NEW_LEFT: - return L"create <-"; - case SO_CREATE_NEW_RIGHT: - return L"create ->"; - case SO_DELETE_LEFT: - return L"delete <-"; - case SO_DELETE_RIGHT: - return L"delete ->"; - case SO_MOVE_LEFT_FROM: - return L"move from <-"; - case SO_MOVE_LEFT_TO: - return L"move to <-"; - case SO_MOVE_RIGHT_FROM: - return L"move from ->"; - case SO_MOVE_RIGHT_TO: - return L"move to ->"; - case SO_OVERWRITE_LEFT: - case SO_COPY_METADATA_TO_LEFT: - return L"update <-"; + //*INDENT-OFF* + case SO_CREATE_NEW_LEFT: return L"create <-"; + case SO_CREATE_NEW_RIGHT: return L"create ->"; + case SO_DELETE_LEFT: return L"delete <-"; + case SO_DELETE_RIGHT: return L"delete ->"; + case SO_MOVE_LEFT_FROM: return L"move from <-"; + case SO_MOVE_LEFT_TO: return L"move to <-"; + case SO_MOVE_RIGHT_FROM: return L"move from ->"; + case SO_MOVE_RIGHT_TO: return L"move to ->"; + case SO_OVERWRITE_LEFT: + case SO_COPY_METADATA_TO_LEFT: return L"update <-"; case SO_OVERWRITE_RIGHT: - 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_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"; + //*INDENT-ON* }; assert(false); return std::wstring(); diff --git a/FreeFileSync/Source/base/structures.h b/FreeFileSync/Source/base/structures.h index 4d8d8f44..5cbdab06 100644 --- a/FreeFileSync/Source/base/structures.h +++ b/FreeFileSync/Source/base/structures.h @@ -339,7 +339,6 @@ struct LocalPairConfig //enhanced folder pairs with (optional) alternate configu std::optional<SyncConfig> localSyncCfg; FilterConfig localFilter; - bool operator==(const LocalPairConfig& rhs) const = default; }; diff --git a/FreeFileSync/Source/base/synchronization.cpp b/FreeFileSync/Source/base/synchronization.cpp index 66d81210..0020f081 100644 --- a/FreeFileSync/Source/base/synchronization.cpp +++ b/FreeFileSync/Source/base/synchronization.cpp @@ -2054,7 +2054,7 @@ bool baseFolderDrop(BaseFolderPair& baseFolder, PhaseCallback& callback) if (!status.failedChecks.empty()) throw status.failedChecks.begin()->second; - if (!contains(status.existing, folderPath)) + if (!status.existing.contains(folderPath)) throw FileError(replaceCpy(_("Cannot find folder %x."), L"%x", fmtPath(AFS::getDisplayPath(folderPath)))); //should really be logged as a "fatal error" if ignored by the user... }, callback); //throw X @@ -2087,7 +2087,7 @@ bool createBaseFolder(BaseFolderPair& baseFolder, bool copyFilePermissions, Phas if (!status.failedChecks.empty()) throw status.failedChecks.begin()->second; - if (contains(status.notExisting, baseFolderPath)) + if (status.notExisting.contains(baseFolderPath)) { if (baseFolder.isAvailable<sideSrc>()) //copy file permissions { @@ -2103,7 +2103,7 @@ bool createBaseFolder(BaseFolderPair& baseFolder, bool copyFilePermissions, Phas } else { - assert(contains(status.existing, baseFolderPath)); + assert(status.existing.contains(baseFolderPath)); //TEMPORARY network drop! base directory not found during comparison, but reappears during synchronization //=> sync-directions are based on false assumptions! Abort. callback.reportFatalError(replaceCpy(_("Target folder %x is already existing, but was not available during folder comparison."), @@ -2354,7 +2354,7 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime { assert(!AFS::isNullPath(baseFolderPath)); if (!AFS::isNullPath(baseFolderPath)) - if (!contains(recyclerSupported, baseFolderPath)) //perf: avoid duplicate checks! + if (!recyclerSupported.contains(baseFolderPath)) //perf: avoid duplicate checks! { callback.updateStatus(replaceCpy(_("Checking recycle bin availability for folder %x..."), L"%x", //throw X fmtPath(AFS::getDisplayPath(baseFolderPath)))); diff --git a/FreeFileSync/Source/base/versioning.cpp b/FreeFileSync/Source/base/versioning.cpp index 7ea21d9b..56b23703 100644 --- a/FreeFileSync/Source/base/versioning.cpp +++ b/FreeFileSync/Source/base/versioning.cpp @@ -387,13 +387,11 @@ void getFolderItemCount(std::map<AbstractPath, size_t>& folderItemCount, const F std::weak_ordering fff::operator<=>(const VersioningLimitFolder& lhs, const VersioningLimitFolder& rhs) { - if (const std::weak_ordering cmp = lhs.versioningFolderPath <=> rhs.versioningFolderPath; - cmp != std::weak_ordering::equivalent) + if (const std::weak_ordering cmp = std::tie(lhs.versioningFolderPath, lhs.versionMaxAgeDays) <=> + std::tie(rhs.versioningFolderPath, rhs.versionMaxAgeDays); + std::is_neq(cmp)) return cmp; - if (lhs.versionMaxAgeDays != rhs.versionMaxAgeDays) - return lhs.versionMaxAgeDays <=> rhs.versionMaxAgeDays; - if (lhs.versionMaxAgeDays > 0) if (lhs.versionCountMin != rhs.versionCountMin) return lhs.versionCountMin <=> rhs.versionCountMin; @@ -481,7 +479,7 @@ void fff::applyVersioningLimit(const std::set<VersioningLimitFolder>& folderLimi { const AbstractPath versioningFolderPath = folderKey.folderPath; - assert(!contains(versionDetails, versioningFolderPath)); + assert(!versionDetails.contains(versioningFolderPath)); findFileVersions(versionDetails[versioningFolderPath], folderVal.folderCont, diff --git a/FreeFileSync/Source/base/versioning.h b/FreeFileSync/Source/base/versioning.h index 68120c1e..7e707300 100644 --- a/FreeFileSync/Source/base/versioning.h +++ b/FreeFileSync/Source/base/versioning.h @@ -17,9 +17,9 @@ namespace fff { -//e.g. move C:\Source\subdir\Sample.txt -> D:\Revisions\subdir\Sample.txt 2012-05-15 131513.txt -//scheme: <revisions directory>\<relpath>\<filename>.<ext> YYYY-MM-DD HHMMSS.<ext> -/* +/* e.g. move C:\Source\subdir\Sample.txt -> D:\Revisions\subdir\Sample.txt 2012-05-15 131513.txt + scheme: <revisions directory>\<relpath>\<filename>.<ext> YYYY-MM-DD HHMMSS.<ext> + - ignores missing source files/dirs - creates missing intermediate directories - does not create empty directories @@ -27,8 +27,7 @@ namespace fff - multi-threading: internally synchronized - replaces already existing target files/dirs (supports retry) => (unlikely) risk of data loss for naming convention "versioning": - race-condition if multiple folder pairs process the same filepath!! -*/ + race-condition if multiple folder pairs process the same filepath!! */ class FileVersioner { @@ -97,7 +96,7 @@ struct VersioningLimitFolder int versionCountMin = 0; //only used if versionMaxAgeDays > 0 => < versionCountMax (if versionCountMax > 0) int versionCountMax = 0; //<= 0 := no limit }; - std::weak_ordering operator<=>(const VersioningLimitFolder& lhs, const VersioningLimitFolder& rhs); +std::weak_ordering operator<=>(const VersioningLimitFolder& lhs, const VersioningLimitFolder& rhs); void applyVersioningLimit(const std::set<VersioningLimitFolder>& folderLimits, diff --git a/FreeFileSync/Source/config.cpp b/FreeFileSync/Source/config.cpp index b81005b2..663eec57 100644 --- a/FreeFileSync/Source/config.cpp +++ b/FreeFileSync/Source/config.cpp @@ -1566,7 +1566,7 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& cfg, int formatVer) { XmlIn inOpt = inGeneral["OptionalDialogs"]; inOpt["ConfirmStartSync" ].attribute("Enabled", cfg.confirmDlgs.confirmSyncStart); - inOpt["ConfirmSaveConfig" ].attribute("Enabled", cfg.confirmDlgs.popupOnConfigChange); + inOpt["ConfirmSaveConfig" ].attribute("Enabled", cfg.confirmDlgs.confirmSaveConfig); inOpt["ConfirmExternalCommandMassInvoke"].attribute("Enabled", cfg.confirmDlgs.confirmCommandMassInvoke); inOpt["WarnUnresolvedConflicts" ].attribute("Enabled", cfg.warnDlgs.warnUnresolvedConflicts); inOpt["WarnNotEnoughDiskSpace" ].attribute("Enabled", cfg.warnDlgs.warnNotEnoughDiskSpace); @@ -1583,11 +1583,12 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& cfg, int formatVer) { XmlIn inOpt = inGeneral["OptionalDialogs"]; inOpt["ConfirmStartSync" ].attribute("Show", cfg.confirmDlgs.confirmSyncStart); - inOpt["ConfirmSaveConfig" ].attribute("Show", cfg.confirmDlgs.popupOnConfigChange); + inOpt["ConfirmSaveConfig" ].attribute("Show", cfg.confirmDlgs.confirmSaveConfig); if (formatVer < 12) //TODO: remove old parameter after migration! 2019-02-09 inOpt["ConfirmExternalCommandMassInvoke"].attribute("Show", cfg.confirmDlgs.confirmCommandMassInvoke); else inOpt["ConfirmCommandMassInvoke"].attribute("Show", cfg.confirmDlgs.confirmCommandMassInvoke); + inOpt["ConfirmSwapSides" ].attribute("Show", cfg.confirmDlgs.confirmSwapSides); inOpt["WarnFolderNotExisting" ].attribute("Show", cfg.warnDlgs.warnFolderNotExisting); inOpt["WarnFoldersDifferInCase" ].attribute("Show", cfg.warnDlgs.warnFoldersDifferInCase); inOpt["WarnUnresolvedConflicts" ].attribute("Show", cfg.warnDlgs.warnUnresolvedConflicts); @@ -1674,6 +1675,7 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& cfg, int formatVer) else { inConfig["Configurations"].attribute("MaxSize", cfg.gui.mainDlg.cfgHistItemsMax); + inConfig["Configurations"].attribute("LastSelected", cfg.gui.mainDlg.cfgFileLastSelected); inConfig["Configurations"](cfg.gui.mainDlg.cfgFileHistory); } //TODO: remove after migration! 2019-11-30 @@ -1688,7 +1690,7 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& cfg, int formatVer) //TODO: remove parameter migration after some time! 2018-01-08 if (formatVer < 6) { - inGui["LastUsedConfig"](cfg.gui.mainDlg.lastUsedConfigFiles); + inGui["LastUsedConfig"](cfg.gui.mainDlg.cfgFilesLastUsed); } else { @@ -1698,7 +1700,7 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& cfg, int formatVer) for (Zstring& filePath : cfgPaths) filePath = resolveFreeFileSyncDriveMacro(filePath); - cfg.gui.mainDlg.lastUsedConfigFiles = cfgPaths; + cfg.gui.mainDlg.cfgFilesLastUsed = cfgPaths; } } @@ -1740,8 +1742,8 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& cfg, int formatVer) inFileGrid["FolderHistoryLeft" ](cfg.gui.mainDlg.folderHistoryLeft); inFileGrid["FolderHistoryRight"](cfg.gui.mainDlg.folderHistoryRight); - //inFileGrid["FolderHistoryLeft" ].attribute("DefaultPath", cfg.gui.mainDlg.defaultFolderPathLeft); - //inFileGrid["FolderHistoryRight"].attribute("DefaultPath", cfg.gui.mainDlg.defaultFolderPathRight); + inFileGrid["FolderHistoryLeft" ].attribute("LastSelected", cfg.gui.mainDlg.folderLastSelectedLeft); + inFileGrid["FolderHistoryRight"].attribute("LastSelected", cfg.gui.mainDlg.folderLastSelectedRight); //TODO: remove parameter migration after some time! 2018-01-08 if (formatVer < 6) @@ -1757,7 +1759,8 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& cfg, int formatVer) XmlIn inCopyToHistory = inCopyTo["FolderHistory"]; inCopyToHistory(cfg.gui.mainDlg.copyToCfg.folderHistory); - inCopyToHistory.attribute("LastUsedPath", cfg.gui.mainDlg.copyToCfg.lastUsedPath); + inCopyToHistory.attribute("TargetFolder", cfg.gui.mainDlg.copyToCfg.targetFolderPath); + inCopyToHistory.attribute("LastSelected", cfg.gui.mainDlg.copyToCfg.targetFolderLastSelected); //########################################################### inWnd["DefaultViewFilter"](cfg.gui.mainDlg.viewFilterDefault); @@ -1838,18 +1841,24 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& cfg, int formatVer) } } - std::vector<Zstring> tmp = splitFilterByLines(cfg.gui.defaultExclusionFilter); //default value - inGui["DefaultExclusionFilter"](tmp); - cfg.gui.defaultExclusionFilter = mergeFilterLines(tmp); - //TODO: remove if parameter migration after some time! 2020-01-30 if (formatVer < 16) ; else inGui["FolderHistory" ].attribute("MaxSize", cfg.gui.folderHistoryMax); + inGui["CsvExport" ].attribute("LastSelected", cfg.gui.csvFileLastSelected); + inGui["SftpKeyFile"].attribute("LastSelected", cfg.gui.sftpKeyFileLastSelected); + + std::vector<Zstring> tmp = splitFilterByLines(cfg.gui.defaultExclusionFilter); //default value + inGui["DefaultExclusionFilter"](tmp); + cfg.gui.defaultExclusionFilter = mergeFilterLines(tmp); + inGui["VersioningFolderHistory"](cfg.gui.versioningFolderHistory); - inGui["LogFolderHistory" ](cfg.gui.logFolderHistory); + inGui["VersioningFolderHistory"].attribute("LastSelected", cfg.gui.versioningFolderLastSelected); + + inGui["LogFolderHistory"](cfg.gui.logFolderHistory); + inGui["LogFolderHistory"].attribute("LastSelected", cfg.gui.logFolderLastSelected); inGui["EmailHistory"](cfg.gui.emailHistory); inGui["EmailHistory"].attribute("MaxSize", cfg.gui.emailHistoryMax); @@ -2237,8 +2246,9 @@ void writeConfig(const XmlGlobalSettings& cfg, XmlOut& out) XmlOut outOpt = outGeneral["OptionalDialogs"]; outOpt["ConfirmStartSync" ].attribute("Show", cfg.confirmDlgs.confirmSyncStart); - outOpt["ConfirmSaveConfig" ].attribute("Show", cfg.confirmDlgs.popupOnConfigChange); + outOpt["ConfirmSaveConfig" ].attribute("Show", cfg.confirmDlgs.confirmSaveConfig); outOpt["ConfirmCommandMassInvoke" ].attribute("Show", cfg.confirmDlgs.confirmCommandMassInvoke); + outOpt["ConfirmSwapSides" ].attribute("Show", cfg.confirmDlgs.confirmSwapSides); outOpt["WarnFolderNotExisting" ].attribute("Show", cfg.warnDlgs.warnFolderNotExisting); outOpt["WarnFoldersDifferInCase" ].attribute("Show", cfg.warnDlgs.warnFoldersDifferInCase); outOpt["WarnUnresolvedConflicts" ].attribute("Show", cfg.warnDlgs.warnUnresolvedConflicts); @@ -2275,9 +2285,10 @@ void writeConfig(const XmlGlobalSettings& cfg, XmlOut& out) outConfig["Columns"](cfg.gui.mainDlg.cfgGridColumnAttribs); outConfig["Configurations"].attribute("MaxSize", cfg.gui.mainDlg.cfgHistItemsMax); + outConfig["Configurations"].attribute("LastSelected", cfg.gui.mainDlg.cfgFileLastSelected); outConfig["Configurations"](cfg.gui.mainDlg.cfgFileHistory); { - std::vector<Zstring> cfgPaths = cfg.gui.mainDlg.lastUsedConfigFiles; + std::vector<Zstring> cfgPaths = cfg.gui.mainDlg.cfgFilesLastUsed; for (Zstring& filePath : cfgPaths) filePath = substituteFreeFileSyncDriveLetter(filePath); @@ -2310,8 +2321,8 @@ void writeConfig(const XmlGlobalSettings& cfg, XmlOut& out) outFileGrid["FolderHistoryLeft" ](cfg.gui.mainDlg.folderHistoryLeft); outFileGrid["FolderHistoryRight"](cfg.gui.mainDlg.folderHistoryRight); - //outFileGrid["FolderHistoryLeft" ].attribute("DefaultPath", cfg.gui.mainDlg.defaultFolderPathLeft); - //outFileGrid["FolderHistoryRight"].attribute("DefaultPath", cfg.gui.mainDlg.defaultFolderPathRight); + outFileGrid["FolderHistoryLeft" ].attribute("LastSelected", cfg.gui.mainDlg.folderLastSelectedLeft); + outFileGrid["FolderHistoryRight"].attribute("LastSelected", cfg.gui.mainDlg.folderLastSelectedRight); //########################################################### XmlOut outCopyTo = outWnd["ManualCopyTo"]; @@ -2320,18 +2331,25 @@ void writeConfig(const XmlGlobalSettings& cfg, XmlOut& out) XmlOut outCopyToHistory = outCopyTo["FolderHistory"]; outCopyToHistory(cfg.gui.mainDlg.copyToCfg.folderHistory); - outCopyToHistory.attribute("LastUsedPath", cfg.gui.mainDlg.copyToCfg.lastUsedPath); + outCopyToHistory.attribute("TargetFolder", cfg.gui.mainDlg.copyToCfg.targetFolderPath); + outCopyToHistory.attribute("LastSelected", cfg.gui.mainDlg.copyToCfg.targetFolderLastSelected); //########################################################### outWnd["DefaultViewFilter"](cfg.gui.mainDlg.viewFilterDefault); outWnd["Perspective" ](cfg.gui.mainDlg.guiPerspectiveLast); - outGui["DefaultExclusionFilter"](splitFilterByLines(cfg.gui.defaultExclusionFilter)); - outGui["FolderHistory" ].attribute("MaxSize", cfg.gui.folderHistoryMax); + outGui["CsvExport" ].attribute("LastSelected", cfg.gui.csvFileLastSelected); + outGui["SftpKeyFile"].attribute("LastSelected", cfg.gui.sftpKeyFileLastSelected); + + outGui["DefaultExclusionFilter"](splitFilterByLines(cfg.gui.defaultExclusionFilter)); + outGui["VersioningFolderHistory"](cfg.gui.versioningFolderHistory); - outGui["LogFolderHistory" ](cfg.gui.logFolderHistory); + outGui["VersioningFolderHistory"].attribute("LastSelected", cfg.gui.versioningFolderLastSelected); + + outGui["LogFolderHistory"](cfg.gui.logFolderHistory); + outGui["LogFolderHistory"].attribute("LastSelected", cfg.gui.logFolderLastSelected); outGui["EmailHistory"](cfg.gui.emailHistory); outGui["EmailHistory"].attribute("MaxSize", cfg.gui.emailHistoryMax); diff --git a/FreeFileSync/Source/config.h b/FreeFileSync/Source/config.h index b85b7756..f65c286b 100644 --- a/FreeFileSync/Source/config.h +++ b/FreeFileSync/Source/config.h @@ -76,9 +76,10 @@ struct XmlBatchConfig struct ConfirmationDialogs { - bool popupOnConfigChange = true; + bool confirmSaveConfig = true; bool confirmSyncStart = true; bool confirmCommandMassInvoke = true; + bool confirmSwapSides = true; bool operator==(const ConfirmationDialogs&) const = default; }; @@ -162,8 +163,9 @@ struct XmlGlobalSettings bool cfgGridLastSortAscending = getDefaultSortDirection(cfgGridLastSortColumnDefault); std::vector<ColAttributesCfg> cfgGridColumnAttribs = getCfgGridDefaultColAttribs(); size_t cfgHistItemsMax = 100; + Zstring cfgFileLastSelected; std::vector<ConfigFileItem> cfgFileHistory; - std::vector<Zstring> lastUsedConfigFiles; + std::vector<Zstring> cfgFilesLastUsed; bool treeGridShowPercentBar = treeGridShowPercentageDefault; ColumnTypeTree treeGridLastSortColumn = treeGridLastSortColumnDefault; //remember sort on overview panel @@ -174,16 +176,15 @@ struct XmlGlobalSettings { bool keepRelPaths = false; bool overwriteIfExists = false; - Zstring lastUsedPath; + Zstring targetFolderPath; + Zstring targetFolderLastSelected; std::vector<Zstring> folderHistory; } copyToCfg; std::vector<Zstring> folderHistoryLeft; std::vector<Zstring> folderHistoryRight; - - //warn_static("finish") - //Zstring defaultFolderPathLeft; - //Zstring defaultFolderPathRight; + Zstring folderLastSelectedLeft; + Zstring folderLastSelectedRight; bool showIcons = true; FileIconSize iconSize = FileIconSize::small; @@ -196,15 +197,21 @@ struct XmlGlobalSettings std::vector<ColAttributesRim> columnAttribRight = getFileGridDefaultColAttribsRight(); ViewFilterDefault viewFilterDefault; - wxString guiPerspectiveLast; //used by wxAuiManager + wxString guiPerspectiveLast; //for wxAuiManager } mainDlg; Zstring defaultExclusionFilter = "*/.Trash-*/" "\n" "*/.recycle/"; size_t folderHistoryMax = 20; + Zstring csvFileLastSelected; + Zstring sftpKeyFileLastSelected; + std::vector<Zstring> versioningFolderHistory; + Zstring versioningFolderLastSelected; + std::vector<Zstring> logFolderHistory; + Zstring logFolderLastSelected; std::vector<Zstring> emailHistory; size_t emailHistoryMax = 10; @@ -215,8 +222,7 @@ struct XmlGlobalSettings std::vector<ExternalApp> externalApps { /* CONTRACT: first entry: show item in file browser - - default external app descriptions will be translated "on the fly"!!! */ + default external app descriptions will be translated "on the fly"!!! */ //"xdg-open \"%parent_path%\"" -> not good enough: we need %local_path% for proper MTP/Google Drive handling { L"Browse directory", "xdg-open \"$(dirname \"%local_path%\")\"" }, { L"Open with default application", "xdg-open \"%local_path%\"" }, diff --git a/FreeFileSync/Source/icon_buffer.cpp b/FreeFileSync/Source/icon_buffer.cpp index d63eaf29..2a57d369 100644 --- a/FreeFileSync/Source/icon_buffer.cpp +++ b/FreeFileSync/Source/icon_buffer.cpp @@ -121,7 +121,7 @@ public: bool hasIcon(const AbstractPath& filePath) const { std::lock_guard dummy(lockIconList_); - return contains(iconList, filePath); + return iconList.contains(filePath); } //- must be called by main thread only! => wxImage is NOT thread-safe like an int (non-atomic ref-count!!!) diff --git a/FreeFileSync/Source/localization.h b/FreeFileSync/Source/localization.h index d7be8424..664b373f 100644 --- a/FreeFileSync/Source/localization.h +++ b/FreeFileSync/Source/localization.h @@ -33,7 +33,7 @@ wxLayoutDirection getLayoutDirection(); void setLanguage(wxLanguage lng); //throw FileError void releaseWxLocale(); //wxLocale crashes miserably on wxGTK when destructor runs during global cleanup => call in wxApp::OnExit -//"You should delete all wxWidgets object that you created by the time OnExit finishes. In particular, do not destroy them from application class' destructor!" +//"You should delete all wxWidgets object that you created by the time OnExit() finishes. In particular, do not destroy them from application class' destructor!" } #endif //LOCALIZATION_H_8917342083178321534 diff --git a/FreeFileSync/Source/log_file.cpp b/FreeFileSync/Source/log_file.cpp index fb9c5b92..4b06bb45 100644 --- a/FreeFileSync/Source/log_file.cpp +++ b/FreeFileSync/Source/log_file.cpp @@ -18,13 +18,13 @@ using AFS = AbstractFileSystem; namespace { -const int LOG_FAIL_PREVIEW_MAX = 25; +const int LOG_PREVIEW_FAIL_MAX = 25; const int SEPARATION_LINE_LEN = 40; -std::string generateLogHeaderTxt(const ProcessSummary& s, const ErrorLog& log, int logFailsPreviewMax) +std::string generateLogHeaderTxt(const ProcessSummary& s, const ErrorLog& log, int logPreviewFailsMax) { - const std::string tabSpace(4, ' '); //4, the one true space count for tabs + const std::string tabSpace(4, ' '); //4: the only sensible space count for tabs std::string headerLine; for (const std::wstring& jobName : s.jobNames) @@ -80,12 +80,12 @@ std::string generateLogHeaderTxt(const ProcessSummary& s, const ErrorLog& log, i output += std::string(SEPARATION_LINE_LEN, '_') + '\n'; int previewCount = 0; - if (logFailsPreviewMax > 0) + if (logPreviewFailsMax > 0) for (const LogEntry& entry : log) if (entry.type & (MSG_TYPE_WARNING | MSG_TYPE_ERROR)) { output += utfTo<std::string>(formatMessage(entry)); - if (++previewCount >= logFailsPreviewMax) + if (++previewCount >= logPreviewFailsMax) break; } if (logFailTotal > previewCount) @@ -97,14 +97,14 @@ std::string generateLogHeaderTxt(const ProcessSummary& s, const ErrorLog& log, i } -std::string generateLogFooterTxt(const std::wstring& logFilePath, int logItemsTotal, int logItemsPreviewMax) //throw FileError +std::string generateLogFooterTxt(const std::wstring& logFilePath, int logItemsTotal, int logPreviewItemsMax) //throw FileError { const ComputerModel cm = getComputerModel(); //throw FileError std::string output; - if (logItemsTotal > logItemsPreviewMax) + if (logItemsTotal > logPreviewItemsMax) output += " [...] " + utfTo<std::string>(replaceCpy(_P("Showing %y of 1 item", "Showing %y of %x items", logItemsTotal), //%x used as plural form placeholder! - L"%y", formatNumber(logItemsPreviewMax))) + '\n'; + L"%y", formatNumber(logPreviewItemsMax))) + '\n'; return output += '\n' + std::string(SEPARATION_LINE_LEN, '_') + '\n' + @@ -194,7 +194,7 @@ std::wstring generateLogTitle(const ProcessSummary& s) } -std::string generateLogHeaderHtml(const ProcessSummary& s, const ErrorLog& log, int logFailsPreviewMax) +std::string generateLogHeaderHtml(const ProcessSummary& s, const ErrorLog& log, int logPreviewFailsMax) { std::string output = R"(<!DOCTYPE html> <html lang="en"> @@ -300,12 +300,12 @@ std::string generateLogHeaderHtml(const ProcessSummary& s, const ErrorLog& log, <table class="log-items" style="line-height:1em; border-spacing:0;"> )"; int previewCount = 0; - if (logFailsPreviewMax > 0) + if (logPreviewFailsMax > 0) for (const LogEntry& entry : log) if (entry.type & (MSG_TYPE_WARNING | MSG_TYPE_ERROR)) { output += formatMessageHtml(entry); - if (++previewCount >= logFailsPreviewMax) + if (++previewCount >= logPreviewFailsMax) break; } output += R"( </table> @@ -325,7 +325,8 @@ std::string generateLogHeaderHtml(const ProcessSummary& s, const ErrorLog& log, return output; } -std::string generateLogFooterHtml(const std::wstring& logFilePath, int logItemsTotal, int logItemsPreviewMax) //throw FileError + +std::string generateLogFooterHtml(const std::wstring& logFilePath, int logItemsTotal, int logPreviewItemsMax) //throw FileError { const std::string osImage = "os-linux.png"; const ComputerModel cm = getComputerModel(); //throw FileError @@ -333,10 +334,10 @@ std::string generateLogFooterHtml(const std::wstring& logFilePath, int logItemsT std::string output = R"( </table> )"; - if (logItemsTotal > logItemsPreviewMax) + if (logItemsTotal > logPreviewItemsMax) output += R"( <div><span style="font-weight:600; padding:0 10px;">[…]</span>)" + htmlTxt(replaceCpy(_P("Showing %y of 1 item", "Showing %y of %x items", logItemsTotal), //%x used as plural form placeholder! - L"%y", formatNumber(logItemsPreviewMax))) + "</div>\n"; + L"%y", formatNumber(logPreviewItemsMax))) + "</div>\n"; return output += R"( <br> @@ -367,11 +368,11 @@ void streamToLogFile(const ProcessSummary& summary, //throw FileError LogFileFormat logFormat) { const int logItemsTotal = log.end() - log.begin(); - const int logItemsPreviewMax = std::numeric_limits<int>::max(); + const int logPreviewItemsMax = std::numeric_limits<int>::max(); std::string buffer = logFormat == LogFileFormat::html ? - generateLogHeaderHtml(summary, log, LOG_FAIL_PREVIEW_MAX) : - generateLogHeaderTxt (summary, log, LOG_FAIL_PREVIEW_MAX); + generateLogHeaderHtml(summary, log, LOG_PREVIEW_FAIL_MAX) : + generateLogHeaderTxt (summary, log, LOG_PREVIEW_FAIL_MAX); //write log items in blocks instead of creating one big string: memory allocation might fail; think 1 million entries! for (const LogEntry& entry : log) @@ -385,8 +386,8 @@ void streamToLogFile(const ProcessSummary& summary, //throw FileError } buffer += logFormat == LogFileFormat::html ? - generateLogFooterHtml(AFS::getDisplayPath(logFilePath), logItemsTotal, logItemsPreviewMax) : //throw FileError - generateLogFooterTxt (AFS::getDisplayPath(logFilePath), logItemsTotal, logItemsPreviewMax); //throw FileError + generateLogFooterHtml(AFS::getDisplayPath(logFilePath), logItemsTotal, logPreviewItemsMax) : //throw FileError + generateLogFooterTxt (AFS::getDisplayPath(logFilePath), logItemsTotal, logPreviewItemsMax); //throw FileError //don't forget to flush: streamOut.write(&buffer[0], buffer.size()); //throw FileError, X @@ -516,7 +517,7 @@ void limitLogfileCount(const AbstractPath& logFolderPath, //throw FileError, X for (const LogFileInfo& lfi : logFiles) if (lfi.timeStamp < cutOffTime && - !contains(logFilePathsToKeep, lfi.filePath)) //don't trim latest log files corresponding to last used config files! + !logFilePathsToKeep.contains(lfi.filePath)) //don't trim latest log files corresponding to last used config files! //nitpicker's corner: what about path differences due to case? e.g. user-overriden log file path changed in case { if (notifyStatus) notifyStatus(statusPrefix + fmtPath(AFS::getDisplayPath(lfi.filePath))); //throw X @@ -534,7 +535,7 @@ void limitLogfileCount(const AbstractPath& logFolderPath, //throw FileError, X } -Zstring fff::getDefaultLogFolderPath() { return getConfigDirPathPf() + Zstr("Logs") ; } +Zstring fff::getLogFolderDefaultPath() { return getConfigDirPathPf() + Zstr("Logs") ; } //"Backup FreeFileSync 2013-09-15 015052.123.html" @@ -599,7 +600,7 @@ AbstractPath fff::generateLogFilePath(LogFileFormat logFormat, const ProcessSumm AbstractPath logFolderPath = createAbstractPath(altLogFolderPathPhrase); if (AFS::isNullPath(logFolderPath)) - logFolderPath = createAbstractPath(getDefaultLogFolderPath()); + logFolderPath = createAbstractPath(getLogFolderDefaultPath()); return AFS::appendRelPath(logFolderPath, logFileName); } diff --git a/FreeFileSync/Source/log_file.h b/FreeFileSync/Source/log_file.h index 424f019e..031be320 100644 --- a/FreeFileSync/Source/log_file.h +++ b/FreeFileSync/Source/log_file.h @@ -16,7 +16,7 @@ namespace fff { -Zstring getDefaultLogFolderPath(); +Zstring getLogFolderDefaultPath(); enum class LogFileFormat { diff --git a/FreeFileSync/Source/status_handler.h b/FreeFileSync/Source/status_handler.h index 6bd31b60..faac4e99 100644 --- a/FreeFileSync/Source/status_handler.h +++ b/FreeFileSync/Source/status_handler.h @@ -46,7 +46,7 @@ struct ProgressStats int items = 0; int64_t bytes = 0; - std::strong_ordering operator<=>(const ProgressStats&) const = default; + bool operator==(const ProgressStats&) const = default; }; diff --git a/FreeFileSync/Source/ui/abstract_folder_picker.cpp b/FreeFileSync/Source/ui/abstract_folder_picker.cpp index 0fc1a056..b08c746b 100644 --- a/FreeFileSync/Source/ui/abstract_folder_picker.cpp +++ b/FreeFileSync/Source/ui/abstract_folder_picker.cpp @@ -54,8 +54,8 @@ public: private: void onOkay (wxCommandEvent& event) override; - void onCancel(wxCommandEvent& event) override { EndModal(ReturnAfsPicker::BUTTON_CANCEL); } - void onClose (wxCloseEvent& event) override { EndModal(ReturnAfsPicker::BUTTON_CANCEL); } + void onCancel(wxCommandEvent& event) override { EndModal(static_cast<int>(ConfirmationButton::cancel)); } + void onClose (wxCloseEvent& event) override { EndModal(static_cast<int>(ConfirmationButton::cancel)); } void onLocalKeyEvent(wxKeyEvent& event); void onExpandNode(wxTreeEvent& event) override; @@ -140,17 +140,6 @@ AbstractFolderPickerDlg::AbstractFolderPickerDlg(wxWindow* parent, AbstractPath& void AbstractFolderPickerDlg::onLocalKeyEvent(wxKeyEvent& event) { - switch (event.GetKeyCode()) - { - //wxTreeCtrl seems to eat up ENTER without adding any functionality; we can do better: - case WXK_RETURN: - case WXK_NUMPAD_ENTER: - { - wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED); - onOkay(dummy); - return; - } - } event.Skip(); } @@ -376,13 +365,13 @@ void AbstractFolderPickerDlg::onOkay(wxCommandEvent& event) if (itemData) folderPathOut_ = itemData->folderPath; - EndModal(ReturnAfsPicker::BUTTON_OKAY); + EndModal(static_cast<int>(ConfirmationButton::accept)); } } -ReturnAfsPicker::ButtonPressed fff::showAbstractFolderPicker(wxWindow* parent, AbstractPath& folderPath) +ConfirmationButton fff::showAbstractFolderPicker(wxWindow* parent, AbstractPath& folderPath) { AbstractFolderPickerDlg pickerDlg(parent, folderPath); - return static_cast<ReturnAfsPicker::ButtonPressed>(pickerDlg.ShowModal()); + return static_cast<ConfirmationButton>(pickerDlg.ShowModal()); } diff --git a/FreeFileSync/Source/ui/abstract_folder_picker.h b/FreeFileSync/Source/ui/abstract_folder_picker.h index c7da13f6..877a3323 100644 --- a/FreeFileSync/Source/ui/abstract_folder_picker.h +++ b/FreeFileSync/Source/ui/abstract_folder_picker.h @@ -8,21 +8,13 @@ #define ABSTRACT_FOLDER_PICKER_HEADER_324872346895690 #include <wx/window.h> +#include <wx+/popup_dlg.h> #include "../afs/abstract.h" namespace fff { -struct ReturnAfsPicker -{ - enum ButtonPressed - { - BUTTON_CANCEL, - BUTTON_OKAY = 1 - }; -}; - -ReturnAfsPicker::ButtonPressed showAbstractFolderPicker(wxWindow* parent, AbstractPath& folderPath); +zen::ConfirmationButton showAbstractFolderPicker(wxWindow* parent, AbstractPath& folderPath); } #endif //ABSTRACT_FOLDER_PICKER_HEADER_324872346895690 diff --git a/FreeFileSync/Source/ui/batch_config.cpp b/FreeFileSync/Source/ui/batch_config.cpp index a6e0a900..a7256d47 100644 --- a/FreeFileSync/Source/ui/batch_config.cpp +++ b/FreeFileSync/Source/ui/batch_config.cpp @@ -36,8 +36,8 @@ public: BatchDialog(wxWindow* parent, BatchDialogConfig& dlgCfg); private: - void onClose (wxCloseEvent& event) override { EndModal(ReturnBatchConfig::BUTTON_CANCEL); } - void onCancel (wxCommandEvent& event) override { EndModal(ReturnBatchConfig::BUTTON_CANCEL); } + void onClose (wxCloseEvent& event) override { EndModal(static_cast<int>(ConfirmationButton::cancel)); } + void onCancel (wxCommandEvent& event) override { EndModal(static_cast<int>(ConfirmationButton::cancel)); } void onSaveBatchJob(wxCommandEvent& event) override; void onToggleIgnoreErrors(wxCommandEvent& event) override { updateGui(); } @@ -107,7 +107,6 @@ void BatchDialog::updateGui() //re-evaluate gui after config changes void BatchDialog::setConfig(const BatchDialogConfig& dlgCfg) { - m_checkBoxIgnoreErrors->SetValue(dlgCfg.ignoreErrors); //transfer parameter ownership to GUI @@ -162,21 +161,21 @@ void BatchDialog::onSaveBatchJob(wxCommandEvent& event) //------------------------------------------------------------- dlgCfgOut_ = getConfig(); - EndModal(ReturnBatchConfig::BUTTON_SAVE_AS); + EndModal(static_cast<int>(ConfirmationButton::accept)); } } -ReturnBatchConfig::ButtonPressed fff::showBatchConfigDialog(wxWindow* parent, - BatchExclusiveConfig& batchExCfg, - bool& ignoreErrors) +ConfirmationButton fff::showBatchConfigDialog(wxWindow* parent, + BatchExclusiveConfig& batchExCfg, + bool& ignoreErrors) { BatchDialogConfig dlgCfg = { batchExCfg, ignoreErrors }; BatchDialog batchDlg(parent, dlgCfg); - const auto rv = static_cast<ReturnBatchConfig::ButtonPressed>(batchDlg.ShowModal()); - if (rv != ReturnBatchConfig::BUTTON_CANCEL) + const auto rv = static_cast<ConfirmationButton>(batchDlg.ShowModal()); + if (rv == ConfirmationButton::accept) { batchExCfg = dlgCfg.batchExCfg; ignoreErrors = dlgCfg.ignoreErrors; diff --git a/FreeFileSync/Source/ui/batch_config.h b/FreeFileSync/Source/ui/batch_config.h index 35fb73fa..867f5060 100644 --- a/FreeFileSync/Source/ui/batch_config.h +++ b/FreeFileSync/Source/ui/batch_config.h @@ -8,25 +8,16 @@ #define BATCH_CONFIG_H_3921674832168945 #include <wx/window.h> +#include <wx+/popup_dlg.h> #include "../config.h" namespace fff { -struct ReturnBatchConfig -{ - enum ButtonPressed - { - BUTTON_CANCEL, - BUTTON_SAVE_AS - }; -}; - - //show and let user customize batch settings (without saving) -ReturnBatchConfig::ButtonPressed showBatchConfigDialog(wxWindow* parent, - BatchExclusiveConfig& batchExCfg, - bool& ignoreErrors); +zen::ConfirmationButton showBatchConfigDialog(wxWindow* parent, + BatchExclusiveConfig& batchExCfg, + bool& ignoreErrors); } #endif //BATCH_CONFIG_H_3921674832168945 diff --git a/FreeFileSync/Source/ui/batch_status_handler.cpp b/FreeFileSync/Source/ui/batch_status_handler.cpp index e7ea1686..be80afb0 100644 --- a/FreeFileSync/Source/ui/batch_status_handler.cpp +++ b/FreeFileSync/Source/ui/batch_status_handler.cpp @@ -282,7 +282,7 @@ void BatchStatusHandler::reportWarning(const std::wstring& msg, bool& warningAct bool dontWarnAgain = false; switch (showQuestionDialog(progressDlg_->getWindowIfVisible(), DialogInfoType::warning, PopupDialogCfg().setDetailInstructions(msg + L"\n\n" + _("You can switch to FreeFileSync's main window to resolve this issue.")). - setCheckBox(dontWarnAgain, _("&Don't show this warning again"), QuestionButton2::no), + setCheckBox(dontWarnAgain, _("&Don't show this warning again"), static_cast<ConfirmationButton3>(QuestionButton2::no)), _("&Ignore"), _("&Switch"))) { case QuestionButton2::yes: //ignore @@ -339,7 +339,7 @@ ProcessCallback::Response BatchStatusHandler::reportError(const std::wstring& ms case ConfirmationButton3::accept: //ignore return ProcessCallback::ignore; - case ConfirmationButton3::acceptAll: //ignore all + case ConfirmationButton3::accept2: //ignore all progressDlg_->setOptionIgnoreErrors(true); return ProcessCallback::ignore; @@ -389,7 +389,7 @@ void BatchStatusHandler::reportFatalError(const std::wstring& msg) case ConfirmationButton2::accept: //ignore break; - case ConfirmationButton2::acceptAll: //ignore all + case ConfirmationButton2::accept2: //ignore all progressDlg_->setOptionIgnoreErrors(true); break; diff --git a/FreeFileSync/Source/ui/cfg_grid.cpp b/FreeFileSync/Source/ui/cfg_grid.cpp index 30df75e1..3142d08b 100644 --- a/FreeFileSync/Source/ui/cfg_grid.cpp +++ b/FreeFileSync/Source/ui/cfg_grid.cpp @@ -339,7 +339,7 @@ private: if (selected) clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHT)); else - clearArea(dc, rect, wxSystemSettings::GetColour(enabled ? wxSYS_COLOUR_WINDOW : wxSYS_COLOUR_BTNFACE)); + clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); } enum class HoverAreaLog @@ -373,7 +373,7 @@ private: rectTmp2.x += rectTmp2.width; rectTmp2.width = rectTmp.width - rectTmp2.width; - dc.GradientFillLinear(rectTmp2, item->cfgItem.backColor, wxSystemSettings::GetColour(enabled ? wxSYS_COLOUR_WINDOW : wxSYS_COLOUR_BTNFACE), wxEAST); + dc.GradientFillLinear(rectTmp2, item->cfgItem.backColor, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW), wxEAST); } else //always show a glimpse of the background color { @@ -649,7 +649,7 @@ void cfggrid::addAndSelect(Grid& grid, const std::vector<Zstring>& filePaths, bo std::optional<size_t> selectionTopRow; for (size_t i = 0; i < grid.getRowCount(); ++i) - if (contains(pathsSorted, getDataView(grid).getItem(i)->cfgItem.cfgFilePath)) + if (pathsSorted.contains(getDataView(grid).getItem(i)->cfgItem.cfgFilePath)) { if (!selectionTopRow) selectionTopRow = i; diff --git a/FreeFileSync/Source/ui/file_grid.cpp b/FreeFileSync/Source/ui/file_grid.cpp index 4ee72b97..8bfa23d9 100644 --- a/FreeFileSync/Source/ui/file_grid.cpp +++ b/FreeFileSync/Source/ui/file_grid.cpp @@ -182,23 +182,32 @@ class GridDataRight; struct IconManager { - IconManager(GridDataLeft& provLeft, GridDataRight& provRight, IconBuffer::IconSize sz) : - iconBuffer_(sz), - dirIcon_ (IconBuffer::genericDirIcon (sz)), - linkOverlayIcon_(IconBuffer::linkOverlayIcon(sz)), - iconUpdater_(std::make_unique<IconUpdater>(provLeft, provRight, iconBuffer_)) {} + IconManager() {} + IconManager(GridDataLeft& provLeft, GridDataRight& provRight, IconBuffer::IconSize sz, bool showFileIcons) : + dirIcon_ (IconBuffer::genericDirIcon (showFileIcons ? sz : IconBuffer::SIZE_SMALL)), + linkOverlayIcon_(IconBuffer::linkOverlayIcon(showFileIcons ? sz : IconBuffer::SIZE_SMALL)) + { + if (showFileIcons) + { + iconBuffer_ = std::make_unique<IconBuffer>(sz); + iconUpdater_ = std::make_unique<IconUpdater>(provLeft, provRight, *iconBuffer_); + } + } + + int getIconSize() const { return iconBuffer_ ? iconBuffer_->getSize() : IconBuffer::getSize(IconBuffer::SIZE_SMALL); } + + IconBuffer* getIconBuffer() { return iconBuffer_.get(); } void startIconUpdater(); - IconBuffer& refIconBuffer() { return iconBuffer_; } const wxImage& getGenericDirIcon () const { return dirIcon_; } const wxImage& getLinkOverlayIcon() const { return linkOverlayIcon_; } private: - IconBuffer iconBuffer_; - const wxImage dirIcon_; - const wxImage linkOverlayIcon_; + const wxImage dirIcon_ = IconBuffer::genericDirIcon (IconBuffer::SIZE_SMALL); + const wxImage linkOverlayIcon_ = IconBuffer::linkOverlayIcon(IconBuffer::SIZE_SMALL); + std::unique_ptr<IconBuffer> iconBuffer_; std::unique_ptr<IconUpdater> iconUpdater_; //bind ownership to GridDataRim<>! }; @@ -218,18 +227,18 @@ public: bool isMarked(const FileSystemObject& fsObj) const { - if (contains(markedFilesAndLinks_, &fsObj)) //mark files/links directly + if (markedFilesAndLinks_.contains(&fsObj)) //mark files/links directly return true; if (auto folder = dynamic_cast<const FolderPair*>(&fsObj)) - if (contains(markedContainer_, folder)) //mark folders which *are* the given ContainerObject* + if (markedContainer_.contains(folder)) //mark folders which *are* the given ContainerObject* return true; //also mark all items with any matching ancestors for (const FileSystemObject* fsObj2 = &fsObj;;) { const ContainerObject& parent = fsObj2->parent(); - if (contains(markedContainer_, &parent)) + if (markedContainer_.contains(&parent)) return true; fsObj2 = dynamic_cast<const FolderPair*>(&parent); @@ -248,7 +257,7 @@ private: struct SharedComponents //...between left, center, and right grids { SharedRef<FileView> gridDataView = makeSharedRef<FileView>(); - std::unique_ptr<IconManager> iconMgr; + SharedRef<IconManager> iconMgr = makeSharedRef<IconManager>(); NavigationMarker navMarker; std::unique_ptr<GridEventManager> evtMgr; GridViewType gridViewType = GridViewType::action; @@ -275,9 +284,9 @@ public: /**/ FileView& getDataView() { return sharedComp_.ref().gridDataView.ref(); } const FileView& getDataView() const { return sharedComp_.ref().gridDataView.ref(); } - void setIconManager(std::unique_ptr<IconManager> iconMgr) { sharedComp_.ref().iconMgr = std::move(iconMgr); } + void setIconManager(const SharedRef<IconManager>& iconMgr) { sharedComp_.ref().iconMgr = iconMgr; } - IconManager* getIconManager() { return sharedComp_.ref().iconMgr.get(); } + IconManager& getIconManager() { return sharedComp_.ref().iconMgr.ref(); } GridViewType getViewType() const { return sharedComp_.ref().gridViewType; } void setViewType(GridViewType vt) { sharedComp_.ref().gridViewType = vt; } @@ -328,7 +337,7 @@ public: void getUnbufferedIconsForPreload(std::vector<std::pair<ptrdiff_t, AbstractPath>>& newLoad) //return (priority, filepath) list { - if (IconManager* iconMgr = getIconManager()) + if (IconBuffer* iconBuf = getIconManager().getIconBuffer()) { const auto& rowsOnScreen = getVisibleRows(refGrid()); const ptrdiff_t visibleRowCount = rowsOnScreen.second - rowsOnScreen.first; @@ -343,15 +352,16 @@ public: const IconInfo ii = getIconInfo(currentRow); if (ii.type == IconType::standard) - if (!iconMgr->refIconBuffer().readyForRetrieval(ii.fsObj->template getAbstractPath<side>())) + if (!iconBuf->readyForRetrieval(ii.fsObj->template getAbstractPath<side>())) newLoad.emplace_back(i, ii.fsObj->template getAbstractPath<side>()); //insert least-important items on outer rim first } } + else assert(false); } void updateNewAndGetUnbufferedIcons(std::vector<AbstractPath>& newLoad) //loads all not yet drawn icons { - if (IconManager* iconMgr = getIconManager()) + if (IconBuffer* iconBuf = getIconManager().getIconBuffer()) { const auto& rowsOnScreen = getVisibleRows(refGrid()); const ptrdiff_t visibleRowCount = rowsOnScreen.second - rowsOnScreen.first; @@ -367,7 +377,7 @@ public: ii.type == IconType::standard) { //test if they are already loaded in buffer: - if (iconMgr->refIconBuffer().readyForRetrieval(ii.fsObj->template getAbstractPath<side>())) + if (iconBuf->readyForRetrieval(ii.fsObj->template getAbstractPath<side>())) { //do a *full* refresh for *every* failed load to update partial DC updates while scrolling refGrid().refreshCell(currentRow, static_cast<ColumnType>(ColumnTypeRim::path)); @@ -378,6 +388,7 @@ public: } } } + else assert(false); } private: @@ -468,7 +479,7 @@ private: { const FileView::PathDrawInfo pdi = getDataView().getDrawInfo(row); - if (enabled && !selected) + if (!enabled || !selected) { const wxColor backCol = [&] { @@ -513,10 +524,10 @@ private: clearArea(dc, rect, backCol); } else - GridData::renderRowBackgound(dc, rect, row, enabled, selected); + GridData::renderRowBackgound(dc, rect, row, true /*enabled*/, true /*selected*/ ); //---------------------------------------------------------------------------------- - wxDCPenChanger dummy(dc, wxPen(row == pdi.groupEndRow - 1 /*last group item*/ ? + wxDCPenChanger dummy(dc, wxPen(row == pdi.groupLastRow - 1 /*last group item*/ ? getColorGridLine() : getDefaultBackgroundColorAlternating(pdi.groupIdx % 2 != 0), fastFromDIP(1))); dc.DrawLine(rect.GetBottomLeft(), rect.GetBottomRight() + wxPoint(1, 0)); } @@ -541,14 +552,14 @@ private: itemNamesWidth = getTextExtentBuffered(dc, ELLIPSIS).x; std::vector<int> itemWidths; - for (size_t row2 = pdi.groupBeginRow; row2 < pdi.groupEndRow; ++row2) + for (size_t row2 = pdi.groupFirstRow; row2 < pdi.groupLastRow; ++row2) if (const FileSystemObject* fsObj = getDataView().getFsObject(row2)) - if (!fsObj->isEmpty<side>() && !dynamic_cast<const FolderPair*>(fsObj)) + if (!fsObj->isEmpty<side>() && fsObj != pdi.folderGroupObj) itemWidths.push_back(getTextExtentBuffered(dc, utfTo<std::wstring>(fsObj->getItemName<side>())).x); if (!itemWidths.empty()) { - //ignore (small number of) excess item lengths: + //ignore (small number of) excessive file name widths: auto itPercentile = itemWidths.begin() + itemWidths.size() * 8 / 10; //80th percentile std::nth_element(itemWidths.begin(), itPercentile, itemWidths.end()); //complexity: O(n) itemNamesWidth = std::max(itemNamesWidth, *itPercentile); @@ -564,18 +575,17 @@ private: std::wstring itemName; std::wstring groupName; std::wstring groupParentFolder; - int iconSize; - size_t groupBeginRow; + size_t groupFirstRow; bool stackedGroupRender; int widthGroupParent; int widthGroupName; }; GroupRenderLayout getGroupRenderLayout(wxDC& dc, size_t row, const FileView::PathDrawInfo& pdi, int maxWidth) { - assert(pdi.fsObj && pdi.folderGroupObj); + assert(pdi.fsObj); - IconManager* const iconMgr = getIconManager(); - const int iconSize = iconMgr ? iconMgr->refIconBuffer().getSize() : 0; + const bool drawFileIcons = getIconManager().getIconBuffer(); + const int iconSize = getIconManager().getIconSize(); //-------------------------------------------------------------------- const int ellipsisWidth = getTextExtentBuffered(dc, ELLIPSIS).x; @@ -583,12 +593,15 @@ private: //-------------------------------------------------------------------- //exception for readability: top row is always group start! - const size_t groupBeginRow = std::max(pdi.groupBeginRow, refGrid().getTopRow()); + const size_t groupFirstRow = std::max(pdi.groupFirstRow, refGrid().getTopRow()); - const bool multiItemGroup = pdi.groupEndRow - groupBeginRow > 1; + const bool multiItemGroup = pdi.groupLastRow - groupFirstRow > 1; std::wstring itemName; - if (!pdi.fsObj->isEmpty<side>() && !dynamic_cast<const FolderPair*>(pdi.fsObj)) + if (!pdi.fsObj->isEmpty<side>() && + (itemPathFormat_ == ItemPathFormat::name || //hack: show folder name in item colum since groupName/groupParentFolder are unused! + // => inconsistent with groupItemNamesWidth! (but without consequence) + pdi.fsObj != pdi.folderGroupObj)) itemName = utfTo<std::wstring>(pdi.fsObj->getItemName<side>()); std::wstring groupName; @@ -599,33 +612,23 @@ private: break; case ItemPathFormat::relative: - if (auto groupFolder = dynamic_cast<const FolderPair*>(pdi.folderGroupObj)) + if (pdi.folderGroupObj) { - groupName = utfTo<std::wstring>(groupFolder->template getItemName<side>()); - groupParentFolder = utfTo<std::wstring>(groupFolder->parent().template getRelativePath<side>()); + groupName = utfTo<std::wstring>(pdi.folderGroupObj ->template getItemName <side>()); + groupParentFolder = utfTo<std::wstring>(pdi.folderGroupObj->parent().template getRelativePath<side>()); } break; case ItemPathFormat::full: - if (auto groupFolder = dynamic_cast<const FolderPair*>(pdi.folderGroupObj)) + if (pdi.folderGroupObj) { - groupName = utfTo<std::wstring>(groupFolder->template getItemName<side>()); - groupParentFolder = AFS::getDisplayPath(groupFolder->parent().template getAbstractPath<side>()); + groupName = utfTo<std::wstring>(pdi.folderGroupObj ->template getItemName <side>()); + groupParentFolder = AFS::getDisplayPath(pdi.folderGroupObj->parent().template getAbstractPath<side>()); } else //=> BaseFolderPair groupParentFolder = AFS::getDisplayPath(pdi.fsObj->base().getAbstractPath<side>()); break; } - //add slashes for better readability - assert(!contains(groupParentFolder, L'/') || !contains(groupParentFolder, L'\\')); - const wchar_t groupParentSep = contains(groupParentFolder, L'/') ? L'/' : (contains(groupParentFolder, L'\\') ? L'\\' : FILE_NAME_SEPARATOR); - - if (!iconMgr && !groupParentFolder.empty() && - !endsWith(groupParentFolder, L'/' ) && //e.g. ftp://server/ - !endsWith(groupParentFolder, L'\\')) /*e.g C:\ */ - groupParentFolder += groupParentSep; - if (!iconMgr && !groupName.empty()) - groupName += FILE_NAME_SEPARATOR; //path components should follow the app layout direction and are NOT a single piece of text! //caveat: add Bidi support only during rendering and not in getValue() or AFS::getDisplayPath(): e.g. support "open file in Explorer" @@ -635,20 +638,22 @@ private: /* group details: single row - _______ __________________________ _______________________________________ ____________________________ - | gap | | (group parent | (gap)) | | ((icon | gap) | group name | (gap)) | | (icon | gap) | item name | - ------- -------------------------- --------------------------------------- ---------------------------- + _______ ________________________ ______________________________________ ____________________________________________ + | gap | | (group parent | gap) | | (icon | gap | group name | 2x gap) | | (vline | gap) | (icon | gap) | item name | + ------- ------------------------ -------------------------------------- -------------------------------------------- group details: stacked - _______ _________________________________________________________ ____________________________ - | gap | | <right-aligned> ((icon | gap) | group name | (gap)) | | (icon | gap) | item name | <- group name on first row - ------- --------------------------------------------------------- ---------------------------- - | gap | | (group parent/... | gap) | | (icon | gap) | item name | <- group parent on second - ------- --------------------------------------------------------- ---------------------------- */ + _______ ________________________________________________________ ____________________________________________ + | gap | | <right-aligned> (icon | gap | group name | 2x gap) | | | (icon | gap) | item name | <- group name on first row + |-----| |------------------------------------------------------| | (vline | gap) |--------------------------| + | gap | | (group parent/... | gap) | | | (icon | gap) | item name | <- group parent on second + ------- -------------------------------------------------------- -------------------------------------------- */ + const int widthGroupSep = !groupParentFolder.empty() || !groupName.empty() ? fastFromDIP(1) + gridGap_ : 0; + bool stackedGroupRender = false; - int widthGroupParent = groupParentFolder.empty() ? 0 : (getTextExtentBuffered(dc, groupParentFolder).x + (iconMgr ? gridGap_ : 0)); - int widthGroupName = groupName .empty() ? 0 : ((iconMgr ? iconSize + gridGap_ : 0) + getTextExtentBuffered(dc, groupName).x + (iconMgr ? gridGap_ : 0)); - int widthGroupItems = (iconMgr ? iconSize + gridGap_ : 0) + groupItemNamesWidth; + int widthGroupParent = groupParentFolder.empty() ? 0 : (getTextExtentBuffered(dc, groupParentFolder).x + gridGap_); + int widthGroupName = groupName .empty() ? 0 : (iconSize + gridGap_ + getTextExtentBuffered(dc, groupName).x + 2 * gridGap_); + int widthGroupItems = widthGroupSep + (drawFileIcons ? iconSize + gridGap_ : 0) + groupItemNamesWidth; //not enough space? => collapse if (int excessWidth = gridGap_ + widthGroupParent + widthGroupName + widthGroupItems - maxWidth; @@ -659,8 +664,12 @@ private: //1. render group components on two rows stackedGroupRender = true; - if (!endsWith(groupParentFolder, L'/' ) && - !endsWith(groupParentFolder, L'\\')) + //add slashes for better readability + assert(!contains(groupParentFolder, L'/') || !contains(groupParentFolder, L'\\')); + const wchar_t groupParentSep = contains(groupParentFolder, L'/') ? L'/' : (contains(groupParentFolder, L'\\') ? L'\\' : FILE_NAME_SEPARATOR); + + if (!endsWith(groupParentFolder, L'/' ) && //e.g. ftp://server/ + !endsWith(groupParentFolder, L'\\')) /*e.g. C:\ */ groupParentFolder += groupParentSep; groupParentFolder += ELLIPSIS; @@ -680,13 +689,13 @@ private: if (excessWidth > 0) { //3. shrink item rendering - widthGroupItems = std::max(widthGroupItems - excessWidth, (iconMgr ? iconSize + gridGap_ : 0) + ellipsisWidth); + widthGroupItems = std::max(widthGroupItems - excessWidth, widthGroupSep + (drawFileIcons ? iconSize + gridGap_ : 0) + ellipsisWidth); excessWidth = gridGap_ + widthGroupStack + widthGroupItems - maxWidth; if (excessWidth > 0) { //4. shrink group stack - widthGroupStack = std::max(widthGroupStack - excessWidth, (iconMgr ? iconSize + gridGap_ : 0) + ellipsisWidth + (iconMgr ? gridGap_ : 0)); + widthGroupStack = std::max(widthGroupStack - excessWidth, iconSize + gridGap_ + ellipsisWidth + 2 * gridGap_); widthGroupParent = std::min(widthGroupParent, widthGroupStack); widthGroupName = std::min(widthGroupName, widthGroupStack); @@ -699,19 +708,19 @@ private: //1. shrink group parent if (!groupParentFolder.empty()) { - widthGroupParent = std::max(widthGroupParent - excessWidth, ellipsisWidth + (iconMgr ? gridGap_ : 0)); + widthGroupParent = std::max(widthGroupParent - excessWidth, ellipsisWidth + gridGap_); excessWidth = gridGap_ + widthGroupParent + widthGroupName + widthGroupItems - maxWidth; } if (excessWidth > 0) { //2. shrink item rendering - widthGroupItems = std::max(widthGroupItems - excessWidth, (iconMgr ? iconSize + gridGap_ : 0) + ellipsisWidth); + widthGroupItems = std::max(widthGroupItems - excessWidth, widthGroupSep + (drawFileIcons ? iconSize + gridGap_ : 0) + ellipsisWidth); excessWidth = gridGap_ + widthGroupParent + widthGroupName + widthGroupItems - maxWidth; if (excessWidth > 0) //3. shrink group name if (!groupName.empty()) - widthGroupName = std::max(widthGroupName - excessWidth, (iconMgr ? iconSize + gridGap_ : 0) + ellipsisWidth + (iconMgr ? gridGap_ : 0)); + widthGroupName = std::max(widthGroupName - excessWidth, iconSize + gridGap_ + ellipsisWidth + 2 * gridGap_); } } } @@ -721,8 +730,7 @@ private: itemName, groupName, groupParentFolder, - iconSize, - groupBeginRow, + groupFirstRow, stackedGroupRender, widthGroupParent, widthGroupName, @@ -735,23 +743,22 @@ private: //don't forget: harmonize with getBestSize()!!! //----------------------------------------------- - wxDCTextColourChanger textColor(dc); - if (enabled && selected) //accessibility: always set *both* foreground AND background colors! - textColor.Set(*wxBLACK); - if (const FileView::PathDrawInfo pdi = getDataView().getDrawInfo(row); pdi.fsObj) { const DisplayType dispTp = getObjectDisplayType(pdi.fsObj); //accessibility: always set both foreground AND background colors! - if (enabled && !selected) //=> coordinate with renderRowBackgound() + wxDCTextColourChanger textColor(dc); + if (!enabled || !selected) //=> coordinate with renderRowBackgound() { if (dispTp == DisplayType::inactive) textColor.Set(getColorInactiveText()); else if (dispTp != DisplayType::normal) textColor.Set(*wxBLACK); } + else + textColor.Set(*wxBLACK); wxRect rectTmp = rect; @@ -762,20 +769,20 @@ private: const auto& [itemName, groupName, groupParentFolder, - iconSize, - groupBeginRow, + groupFirstRow, stackedGroupRender, widthGroupParent, widthGroupName] = getGroupRenderLayout(dc, row, pdi, rectTmp.width); - IconManager* const iconMgr = getIconManager(); - - auto drawIcon = [&, iconSize /*clang bug*/= iconSize](wxImage icon, wxRect rectIcon) + auto drawIcon = [&](wxImage icon, wxRect rectIcon, bool drawActive) { - if (!pdi.fsObj->isActive()) + if (!drawActive) icon = icon.ConvertToGreyscale(1.0 / 3, 1.0 / 3, 1.0 / 3); //treat all channels equally! - rectIcon.width = iconSize; //center smaller-than-default icons + if (!enabled) + icon = icon.ConvertToDisabled(); + + rectIcon.width = getIconManager().getIconSize(); //center smaller-than-default icons drawBitmapRtlNoMirror(dc, icon, rectIcon, wxALIGN_CENTER); }; //------------------------------------------------------------------------- @@ -804,22 +811,22 @@ private: wxRect rectGroupItems = rectTmp; //------------------------------------------------------------------------- { + const bool groupFolderActive = groupName.empty() || pdi.folderGroupObj->isActive(); + //clear background below parent path => harmonize with renderRowBackgound() wxDCTextColourChanger textColorGroup(dc); - if (enabled && !selected && - //!pdi.fsObj->isEmpty<side>() && - (!groupParentFolder.empty() || !groupName.empty()) && - pdi.fsObj->isActive()) + if ((!enabled || !selected) && + (!groupParentFolder.empty() || !groupName.empty())) { rectGroup.x -= gridGap_; //include lead gap rectGroup.width += gridGap_; // clearArea(dc, rectGroup, getDefaultBackgroundColorAlternating(pdi.groupIdx % 2 == 0)); //clearArea() is surprisingly expensive => call just once! - textColorGroup.Set(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); + textColorGroup.Set(wxSystemSettings::GetColour(groupFolderActive ? wxSYS_COLOUR_WINDOWTEXT : wxSYS_COLOUR_GRAYTEXT)); //accessibility: always set *both* foreground AND background colors! - if (row == pdi.groupEndRow - 1 /*last group item*/) //restore the group separation line we just cleared + if (row == pdi.groupLastRow - 1 /*last group item*/) //restore the group separation line we just cleared { wxDCPenChanger dummy(dc, wxPen(getColorGridLine(), fastFromDIP(1))); dc.DrawLine(rectGroup.GetBottomLeft(), rectGroup.GetBottomRight() + wxPoint(1, 0)); @@ -837,7 +844,7 @@ private: dc.GradientFillLinear(rectNav, getColorSelectionGradientFrom(), backCol, wxEAST); } - if (!groupName.empty() && row == groupBeginRow) + if (!groupName.empty() && row == groupFirstRow) { wxDCTextColourChanger textColorGroupName(dc); if (static_cast<HoverAreaGroup>(rowHover) == HoverAreaGroup::groupName) @@ -846,32 +853,43 @@ private: textColorGroupName.Set(*wxBLACK); } - if (iconMgr) - { - drawIcon(iconMgr->getGenericDirIcon(), rectGroupName); - rectGroupName.x += iconSize + gridGap_; - rectGroupName.width -= iconSize + gridGap_; - } + drawIcon(getIconManager().getGenericDirIcon(), rectGroupName, groupFolderActive); + + if (!pdi.folderGroupObj->isEmpty<side>() && + pdi.folderGroupObj->isFollowedSymlink<side>()) + drawIcon(getIconManager().getLinkOverlayIcon(), rectGroupName, groupFolderActive); + + rectGroupName.x += getIconManager().getIconSize() + gridGap_; + rectGroupName.width -= getIconManager().getIconSize() + gridGap_; + drawCellText(dc, rectGroupName, groupName, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, &getTextExtentBuffered(dc, groupName)); } if (!groupParentFolder.empty() && - ((stackedGroupRender && row == groupBeginRow + 1) || - (!stackedGroupRender && row == groupBeginRow))) + (( stackedGroupRender && row == groupFirstRow + 1) || + (!stackedGroupRender && row == groupFirstRow))) { drawCellText(dc, rectGroupParent, groupParentFolder, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, &getTextExtentBuffered(dc, groupParentFolder)); } } + if (!groupParentFolder.empty() || !groupName.empty()) + { + wxDCPenChanger dummy(dc, wxPen(getColorGridLine(), fastFromDIP(1))); + dc.DrawLine(rectGroupItems.GetTopLeft(), rectGroupItems.GetBottomLeft() + wxPoint(0, 1)); //draws half-open range! + rectGroupItems.x += fastFromDIP(1) + gridGap_; + rectGroupItems.width -= fastFromDIP(1) + gridGap_; + } + if (!itemName.empty()) { - if (iconMgr) //draw file icon + if (IconBuffer* iconBuf = getIconManager().getIconBuffer()) //=> draw file icons { //whenever there's something new to render on screen, start up watching for failed icon drawing: //=> ideally it would suffice to start watching only when scrolling grid or showing new grid content, but this solution is more robust //and the icon updater will stop automatically when finished anyway //Note: it's not sufficient to start up on failed icon loads only, since we support prefetching of not yet visible rows!!! - iconMgr->startIconUpdater(); + getIconManager().startIconUpdater(); wxImage fileIcon; @@ -879,18 +897,18 @@ private: switch (ii.type) { case IconType::folder: - fileIcon = iconMgr->getGenericDirIcon(); + fileIcon = getIconManager().getGenericDirIcon(); break; case IconType::standard: - if (std::optional<wxImage> tmpIco = iconMgr->refIconBuffer().retrieveFileIcon(ii.fsObj->template getAbstractPath<side>())) + if (std::optional<wxImage> tmpIco = iconBuf->retrieveFileIcon(ii.fsObj->template getAbstractPath<side>())) fileIcon = *tmpIco; else { setFailedLoad(row); //save status of failed icon load -> used for async. icon loading //falsify only! we want to avoid writing incorrect success values when only partially updating the DC, e.g. when scrolling, //see repaint behavior of ::ScrollWindow() function! - fileIcon = iconMgr->refIconBuffer().getIconByExtension(ii.fsObj->template getItemName<side>()); //better than nothing + fileIcon = iconBuf->getIconByExtension(ii.fsObj->template getItemName<side>()); //better than nothing } break; @@ -900,13 +918,13 @@ private: if (fileIcon.IsOk()) { - drawIcon(fileIcon, rectGroupItems); + drawIcon(fileIcon, rectGroupItems, pdi.fsObj->isActive()); if (ii.drawAsLink) - drawIcon(iconMgr->getLinkOverlayIcon(), rectGroupItems); + drawIcon(getIconManager().getLinkOverlayIcon(), rectGroupItems, pdi.fsObj->isActive()); } - rectGroupItems.x += iconSize + gridGap_; - rectGroupItems.width -= iconSize + gridGap_; + rectGroupItems.x += getIconManager().getIconSize() + gridGap_; + rectGroupItems.width -= getIconManager().getIconSize() + gridGap_; } drawCellText(dc, rectGroupItems, itemName, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, &getTextExtentBuffered(dc, itemName)); @@ -948,13 +966,12 @@ private: const auto& [itemName, groupName, groupParentFolder, - iconSize, - groupBeginRow, + groupFirstRow, stackedGroupRender, widthGroupParent, widthGroupName] = getGroupRenderLayout(dc, row, pdi, cellWidth); - if (!groupName.empty() && row == groupBeginRow) + if (!groupName.empty() && row == groupFirstRow) { const int groupNameCellBeginX = gridGap_ + (stackedGroupRender ? std::max(widthGroupParent, widthGroupName) - widthGroupName : //right-align @@ -977,25 +994,28 @@ private: if (const FileView::PathDrawInfo pdi = getDataView().getDrawInfo(row); pdi.fsObj) { - /* _______ __________________________ _______________________________________ ____________________________ - | gap | | (group parent | (gap)) | | ((icon | gap) | group name | (gap)) | | (icon | gap) | item name | - ------- -------------------------- --------------------------------------- ---------------------------- */ + /* _______ ________________________ ______________________________________ ____________________________________________ + | gap | | (group parent | gap) | | (icon | gap | group name | 2x gap) | | (vline | gap) | (icon | gap) | item name | + ------- ------------------------ -------------------------------------- -------------------------------------------- */ const int insanelyHugeWidth = 1000'000'000; //(hopefully) still small enough to avoid integer overflows const auto& [itemName, groupName, groupParentFolder, - iconSize, - groupBeginRow, + groupFirstRow, stackedGroupRender, widthGroupParent, widthGroupName] = getGroupRenderLayout(dc, row, pdi, insanelyHugeWidth); assert(!stackedGroupRender); - const int widthGroupItem = itemName.empty() ? 0 : ((iconSize > 0 ? iconSize + gridGap_ : 0) + getTextExtentBuffered(dc, itemName).x); + const int ellipsisWidth = getTextExtentBuffered(dc, ELLIPSIS).x; + const int fileIconSize = getIconManager().getIconBuffer() ? getIconManager().getIconSize() + gridGap_ : 0; + const int widthGroupSep = !groupParentFolder.empty() || !groupName.empty() ? fastFromDIP(1) + gridGap_ : 0; + + const int widthGroupItems = widthGroupSep + fileIconSize + std::max(getTextExtentBuffered(dc, itemName).x, ellipsisWidth); - bestSize += gridGap_ + widthGroupParent + widthGroupName + widthGroupItem + gridGap_ /*[!]*/; + bestSize += gridGap_ + widthGroupParent + widthGroupName + widthGroupItems + gridGap_ /*[!]*/; } return bestSize; } @@ -1260,7 +1280,7 @@ private: { const FileView::PathDrawInfo pdi = getDataView().getDrawInfo(row); - if (enabled && !selected) + if (!enabled || !selected) { if (pdi.fsObj) { @@ -1273,10 +1293,10 @@ private: clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); } else - GridData::renderRowBackgound(dc, rect, row, enabled, selected); + GridData::renderRowBackgound(dc, rect, row, true /*enabled*/, true /*selected*/ ); //---------------------------------------------------------------------------------- - wxDCPenChanger dummy(dc, wxPen(row == pdi.groupEndRow - 1 /*last group item*/ ? + wxDCPenChanger dummy(dc, wxPen(row == pdi.groupLastRow - 1 /*last group item*/ ? getColorGridLine() : getDefaultBackgroundColorAlternating(pdi.groupIdx % 2 != 0), fastFromDIP(1))); dc.DrawLine(rect.GetBottomLeft(), rect.GetBottomRight() + wxPoint(1, 0)); } @@ -1291,20 +1311,16 @@ private: void renderCell(wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover) override { - wxDCTextColourChanger textColor(dc); - if (enabled && selected) //accessibility: always set *both* foreground AND background colors! - textColor.Set(*wxBLACK); - if (const FileView::PathDrawInfo pdi = getDataView().getDrawInfo(row); pdi.fsObj) { auto drawHighlightBackground = [&](const wxColor& col) { - if (enabled && !selected && pdi.fsObj->isActive()) //coordinate with renderRowBackgound()! + if ((!enabled || !selected) && pdi.fsObj->isActive()) //coordinate with renderRowBackgound()! { clearArea(dc, rect, col); - if (row == pdi.groupEndRow - 1 /*last group item*/) //restore the group separation line we just cleared + if (row == pdi.groupLastRow - 1 /*last group item*/) //restore the group separation line we just cleared { wxDCPenChanger dummy(dc, wxPen(getColorGridLine(), fastFromDIP(1))); dc.DrawLine(rect.GetBottomLeft(), rect.GetBottomRight() + wxPoint(1, 0)); @@ -1318,10 +1334,13 @@ private: { const bool drawMouseHover = static_cast<HoverAreaCenter>(rowHover) == HoverAreaCenter::checkbox; - if (pdi.fsObj->isActive()) - drawBitmapRtlNoMirror(dc, loadImage(drawMouseHover ? "checkbox_true_hover" : "checkbox_true"), rect, wxALIGN_CENTER); - else //default - drawBitmapRtlNoMirror(dc, loadImage(drawMouseHover ? "checkbox_false_hover" : "checkbox_false"), rect, wxALIGN_CENTER); + wxImage icon = loadImage(pdi.fsObj->isActive() ? + (drawMouseHover ? "checkbox_true_hover" : "checkbox_true") : + (drawMouseHover ? "checkbox_false_hover" : "checkbox_false")); + if (!enabled) + icon = icon.ConvertToDisabled(); + + drawBitmapRtlNoMirror(dc, icon, rect, wxALIGN_CENTER); } break; @@ -1342,10 +1361,18 @@ private: rectTmp.width -= notch_.GetWidth(); } + auto drawIcon = [&](wxImage icon, int alignment) + { + if (!enabled) + icon = icon.ConvertToDisabled(); + + drawBitmapRtlMirror(dc, icon, rectTmp, alignment, renderBufCmp_); + }; + if (getViewType() == GridViewType::category) - drawBitmapRtlMirror(dc, getCmpResultImage(pdi.fsObj->getCategory()), rectTmp, wxALIGN_CENTER, renderBufCmp_); + drawIcon(getCmpResultImage(pdi.fsObj->getCategory()), wxALIGN_CENTER); else if (pdi.fsObj->getCategory() != FILE_EQUAL) //don't show = in both middle columns - drawBitmapRtlMirror(dc, greyScale(getCmpResultImage(pdi.fsObj->getCategory())), rectTmp, wxALIGN_CENTER, renderBufCmp_); + drawIcon(greyScale(getCmpResultImage(pdi.fsObj->getCategory())), wxALIGN_CENTER); } break; @@ -1354,24 +1381,32 @@ private: if (getViewType() == GridViewType::action) drawHighlightBackground(getBackGroundColorSyncAction(pdi.fsObj->getSyncOperation(), false /*faint*/)); + auto drawIcon = [&](wxImage icon, int alignment) + { + if (!enabled) + icon = icon.ConvertToDisabled(); + + drawBitmapRtlMirror(dc, icon, rect, alignment, renderBufSync_); + }; + //synchronization preview const auto rowHoverCenter = rowHover == HoverArea::none ? HoverAreaCenter::checkbox : static_cast<HoverAreaCenter>(rowHover); switch (rowHoverCenter) { case HoverAreaCenter::dirLeft: - drawBitmapRtlMirror(dc, getSyncOpImage(pdi.fsObj->testSyncOperation(SyncDirection::left)), rect, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, renderBufSync_); + drawIcon(getSyncOpImage(pdi.fsObj->testSyncOperation(SyncDirection::left)), wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); break; case HoverAreaCenter::dirNone: - drawBitmapRtlNoMirror(dc, getSyncOpImage(pdi.fsObj->testSyncOperation(SyncDirection::none)), rect, wxALIGN_CENTER); + drawIcon(getSyncOpImage(pdi.fsObj->testSyncOperation(SyncDirection::none)), wxALIGN_CENTER); break; case HoverAreaCenter::dirRight: - drawBitmapRtlMirror(dc, getSyncOpImage(pdi.fsObj->testSyncOperation(SyncDirection::right)), rect, wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL, renderBufSync_); + drawIcon(getSyncOpImage(pdi.fsObj->testSyncOperation(SyncDirection::right)), wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL); break; case HoverAreaCenter::checkbox: if (getViewType() == GridViewType::action) - drawBitmapRtlMirror(dc, getSyncOpImage(pdi.fsObj->getSyncOperation()), rect, wxALIGN_CENTER, renderBufSync_); + drawIcon(getSyncOpImage(pdi.fsObj->getSyncOperation()), wxALIGN_CENTER); else if (pdi.fsObj->getSyncOperation() != SO_EQUAL) //don't show = in both middle columns - drawBitmapRtlMirror(dc, greyScale(getSyncOpImage(pdi.fsObj->getSyncOperation())), rect, wxALIGN_CENTER, renderBufSync_); + drawIcon(greyScale(getSyncOpImage(pdi.fsObj->getSyncOperation())), wxALIGN_CENTER); break; } } @@ -1464,6 +1499,7 @@ private: } } + //*INDENT-OFF* void showToolTip(size_t row, ColumnTypeCenter colType, wxPoint posScreen) { if (const FileSystemObject* fsObj = getFsObject(row)) @@ -1475,10 +1511,8 @@ private: { const char* imageName = [&] { - const CompareFileResult cmpRes = fsObj->getCategory(); - switch (cmpRes) - { - //*INDENT-OFF* + switch (fsObj->getCategory()) + { case FILE_LEFT_SIDE_ONLY: return "cat_left_only"; case FILE_RIGHT_SIDE_ONLY: return "cat_right_only"; case FILE_LEFT_NEWER: return "cat_left_newer"; @@ -1486,26 +1520,22 @@ private: case FILE_DIFFERENT_CONTENT: return "cat_different"; case FILE_EQUAL: case FILE_DIFFERENT_METADATA: return "cat_equal"; //= sub-category of equal -case FILE_CONFLICT: return "cat_conflict"; -//*INDENT-ON* + case FILE_CONFLICT: return "cat_conflict"; } assert(false); return ""; - } - (); + }(); const auto& img = mirrorIfRtl(loadImage(imageName)); toolTip_.show(getCategoryDescription(*fsObj), posScreen, &img); } break; - case ColumnTypeCenter::action: - { - const char* imageName = [&] + case ColumnTypeCenter::action: { - const SyncOperation syncOp = fsObj->getSyncOperation(); - switch (syncOp) - { - //*INDENT-OFF* + const char* imageName = [&] + { + switch (fsObj->getSyncOperation()) + { case SO_CREATE_NEW_LEFT: return "so_create_left"; case SO_CREATE_NEW_RIGHT: return "so_create_right"; case SO_DELETE_LEFT: return "so_delete_left"; @@ -1520,21 +1550,21 @@ case FILE_CONFLICT: return "cat_conflict"; case SO_COPY_METADATA_TO_RIGHT: return "so_move_right"; case SO_DO_NOTHING: return "so_none"; case SO_EQUAL: return "cat_equal"; -case SO_UNRESOLVED_CONFLICT: return "cat_conflict"; -//*INDENT-ON* - }; - assert(false); - return ""; - }(); - const auto& img = mirrorIfRtl(loadImage(imageName)); - toolTip_.show(getSyncOpDescription(*fsObj), posScreen, &img); + case SO_UNRESOLVED_CONFLICT: return "cat_conflict"; + }; + assert(false); + return ""; + }(); + const auto& img = mirrorIfRtl(loadImage(imageName)); + toolTip_.show(getSyncOpDescription(*fsObj), posScreen, &img); + } + break; } - break; - } -} -else - toolTip_.hide(); //if invalid row... + } + else + toolTip_.hide(); //if invalid row... } + //*INDENT-ON* bool selectionInProgress_ = false; @@ -1553,67 +1583,67 @@ class GridEventManager : private wxEvtHandler { public: GridEventManager(Grid& gridL, - Grid& gridC, - Grid& gridR, - GridDataCenter& provCenter) : -gridL_(gridL), gridC_(gridC), gridR_(gridR), -provCenter_(provCenter) + Grid& gridC, + Grid& gridR, + GridDataCenter& provCenter) : + gridL_(gridL), gridC_(gridC), gridR_(gridR), + provCenter_(provCenter) { -gridL_.Bind(EVENT_GRID_COL_RESIZE, [this](GridColumnResizeEvent& event) { onResizeColumnL(event); }); -gridR_.Bind(EVENT_GRID_COL_RESIZE, [this](GridColumnResizeEvent& event) { onResizeColumnR(event); }); + gridL_.Bind(EVENT_GRID_COL_RESIZE, [this](GridColumnResizeEvent& event) { onResizeColumnL(event); }); + gridR_.Bind(EVENT_GRID_COL_RESIZE, [this](GridColumnResizeEvent& event) { onResizeColumnR(event); }); -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_.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_); }); -gridC_.getMainWin().Bind(wxEVT_MOTION, [this](wxMouseEvent& event) { onCenterMouseMovement(event); }); -gridC_.getMainWin().Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& event) { onCenterMouseLeave (event); }); + gridC_.getMainWin().Bind(wxEVT_MOTION, [this](wxMouseEvent& event) { onCenterMouseMovement(event); }); + gridC_.getMainWin().Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& event) { onCenterMouseLeave (event); }); -gridC_.Bind(EVENT_GRID_MOUSE_LEFT_DOWN, [this](GridClickEvent& event) { onCenterSelectBegin(event); }); -gridC_.Bind(EVENT_GRID_SELECT_RANGE, [this](GridSelectEvent& event) { onCenterSelectEnd (event); }); + gridC_.Bind(EVENT_GRID_MOUSE_LEFT_DOWN, [this](GridClickEvent& event) { onCenterSelectBegin(event); }); + gridC_.Bind(EVENT_GRID_SELECT_RANGE, [this](GridSelectEvent& event) { onCenterSelectEnd (event); }); -//clear selection of other grid when selecting on -gridL_.Bind(EVENT_GRID_SELECT_RANGE, [this](GridSelectEvent& event) { onGridSelectionL(event); }); -gridR_.Bind(EVENT_GRID_SELECT_RANGE, [this](GridSelectEvent& event) { onGridSelectionR(event); }); + //clear selection of other grid when selecting on + gridL_.Bind(EVENT_GRID_SELECT_RANGE, [this](GridSelectEvent& event) { onGridSelectionL(event); }); + gridR_.Bind(EVENT_GRID_SELECT_RANGE, [this](GridSelectEvent& event) { onGridSelectionR(event); }); -//parallel grid scrolling: do NOT use DoPrepareDC() to align grids! GDI resource leak! Use regular paint event instead: -gridL_.getMainWin().Bind(wxEVT_PAINT, [this](wxPaintEvent& event) { onPaintGrid(gridL_); event.Skip(); }); -gridC_.getMainWin().Bind(wxEVT_PAINT, [this](wxPaintEvent& event) { onPaintGrid(gridC_); event.Skip(); }); -gridR_.getMainWin().Bind(wxEVT_PAINT, [this](wxPaintEvent& event) { onPaintGrid(gridR_); event.Skip(); }); + //parallel grid scrolling: do NOT use DoPrepareDC() to align grids! GDI resource leak! Use regular paint event instead: + gridL_.getMainWin().Bind(wxEVT_PAINT, [this](wxPaintEvent& event) { onPaintGrid(gridL_); event.Skip(); }); + gridC_.getMainWin().Bind(wxEVT_PAINT, [this](wxPaintEvent& event) { onPaintGrid(gridC_); event.Skip(); }); + gridR_.getMainWin().Bind(wxEVT_PAINT, [this](wxPaintEvent& event) { onPaintGrid(gridR_); event.Skip(); }); -auto connectGridAccess = [&](Grid& grid, std::function<void(wxEvent& event)> handler) -{ - grid.Bind(wxEVT_SCROLLWIN_TOP, handler); - grid.Bind(wxEVT_SCROLLWIN_BOTTOM, handler); - grid.Bind(wxEVT_SCROLLWIN_LINEUP, handler); - grid.Bind(wxEVT_SCROLLWIN_LINEDOWN, handler); - grid.Bind(wxEVT_SCROLLWIN_PAGEUP, handler); - grid.Bind(wxEVT_SCROLLWIN_PAGEDOWN, handler); - grid.Bind(wxEVT_SCROLLWIN_THUMBTRACK, handler); - //wxEVT_KILL_FOCUS -> there's no need to reset "scrollMaster" - //wxEVT_SET_FOCUS -> not good enough: - //e.g.: left grid has input, right grid is "scrollMaster" due to dragging scroll thumb via mouse. - //=> 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_UP, handler); - grid.getMainWin().Bind(wxEVT_KEY_DOWN, handler); - - grid.getMainWin().Bind(wxEVT_LEFT_DOWN, handler); - grid.getMainWin().Bind(wxEVT_LEFT_DCLICK, handler); - grid.getMainWin().Bind(wxEVT_RIGHT_DOWN, handler); - grid.getMainWin().Bind(wxEVT_MOUSEWHEEL, handler); -}; -connectGridAccess(gridL_, [this](wxEvent& event) { onGridAccessL(event); }); // -connectGridAccess(gridC_, [this](wxEvent& event) { onGridAccessC(event); }); //connect *after* onKeyDown() in order to receive callback *before*!!! -connectGridAccess(gridR_, [this](wxEvent& event) { onGridAccessR(event); }); // + auto connectGridAccess = [&](Grid& grid, std::function<void(wxEvent& event)> handler) + { + grid.Bind(wxEVT_SCROLLWIN_TOP, handler); + grid.Bind(wxEVT_SCROLLWIN_BOTTOM, handler); + grid.Bind(wxEVT_SCROLLWIN_LINEUP, handler); + grid.Bind(wxEVT_SCROLLWIN_LINEDOWN, handler); + grid.Bind(wxEVT_SCROLLWIN_PAGEUP, handler); + grid.Bind(wxEVT_SCROLLWIN_PAGEDOWN, handler); + grid.Bind(wxEVT_SCROLLWIN_THUMBTRACK, handler); + //wxEVT_KILL_FOCUS -> there's no need to reset "scrollMaster" + //wxEVT_SET_FOCUS -> not good enough: + //e.g.: left grid has input, right grid is "scrollMaster" due to dragging scroll thumb via mouse. + //=> 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_UP, handler); + grid.getMainWin().Bind(wxEVT_KEY_DOWN, handler); + + grid.getMainWin().Bind(wxEVT_LEFT_DOWN, handler); + grid.getMainWin().Bind(wxEVT_LEFT_DCLICK, handler); + grid.getMainWin().Bind(wxEVT_RIGHT_DOWN, handler); + grid.getMainWin().Bind(wxEVT_MOUSEWHEEL, handler); + }; + connectGridAccess(gridL_, [this](wxEvent& event) { onGridAccessL(event); }); // + connectGridAccess(gridC_, [this](wxEvent& event) { onGridAccessC(event); }); //connect *after* onKeyDown() in order to receive callback *before*!!! + connectGridAccess(gridR_, [this](wxEvent& event) { onGridAccessR(event); }); // -Bind(EVENT_ALIGN_SCROLLBARS, [this](wxCommandEvent& event) { onAlignScrollBars(event); }); + Bind(EVENT_ALIGN_SCROLLBARS, [this](wxCommandEvent& event) { onAlignScrollBars(event); }); } ~GridEventManager() { -//assert(!scrollbarUpdatePending_); => false-positives: e.g. start ffs, right-click on grid, close dialog by clicking X + //assert(!scrollbarUpdatePending_); => false-positives: e.g. start ffs, right-click on grid, close dialog by clicking X } void setScrollMaster(const Grid& grid) { scrollMaster_ = &grid; } @@ -1621,32 +1651,32 @@ Bind(EVENT_ALIGN_SCROLLBARS, [this](wxCommandEvent& event) { onAlignScrollBars(e private: void onCenterSelectBegin(GridClickEvent& event) { -provCenter_.onSelectBegin(); -event.Skip(); + provCenter_.onSelectBegin(); + event.Skip(); } void onCenterSelectEnd(GridSelectEvent& event) { -if (event.positive_) -{ - if (event.mouseClick_) - provCenter_.onSelectEnd(event.rowFirst_, event.rowLast_, event.mouseClick_->hoverArea_, event.mouseClick_->row_); - else - provCenter_.onSelectEnd(event.rowFirst_, event.rowLast_, HoverArea::none, -1); -} -event.Skip(); + if (event.positive_) + { + if (event.mouseClick_) + provCenter_.onSelectEnd(event.rowFirst_, event.rowLast_, event.mouseClick_->hoverArea_, event.mouseClick_->row_); + else + provCenter_.onSelectEnd(event.rowFirst_, event.rowLast_, HoverArea::none, -1); + } + event.Skip(); } void onCenterMouseMovement(wxMouseEvent& event) { -provCenter_.onMouseMovement(event.GetPosition()); -event.Skip(); + provCenter_.onMouseMovement(event.GetPosition()); + event.Skip(); } void onCenterMouseLeave(wxMouseEvent& event) { -provCenter_.onMouseLeave(); -event.Skip(); + provCenter_.onMouseLeave(); + event.Skip(); } void onGridSelectionL(GridSelectEvent& event) { onGridSelection(gridL_, gridR_); event.Skip(); } @@ -1654,48 +1684,48 @@ event.Skip(); void onGridSelection(const Grid& grid, Grid& other) { -if (!wxGetKeyState(WXK_CONTROL)) //clear other grid unless user is holding CTRL - other.clearSelection(GridEventPolicy::deny); //don't emit event, prevent recursion! + if (!wxGetKeyState(WXK_CONTROL)) //clear other grid unless user is holding CTRL + other.clearSelection(GridEventPolicy::deny); //don't emit event, prevent recursion! } void onKeyDown(wxKeyEvent& event, const Grid& grid) { -int keyCode = event.GetKeyCode(); -if (grid.GetLayoutDirection() == wxLayout_RightToLeft) -{ - if (keyCode == WXK_LEFT || keyCode == WXK_NUMPAD_LEFT) - keyCode = WXK_RIGHT; - else if (keyCode == WXK_RIGHT || keyCode == WXK_NUMPAD_RIGHT) - keyCode = WXK_LEFT; -} + int keyCode = event.GetKeyCode(); + if (grid.GetLayoutDirection() == wxLayout_RightToLeft) + { + if (keyCode == WXK_LEFT || keyCode == WXK_NUMPAD_LEFT) + keyCode = WXK_RIGHT; + else if (keyCode == WXK_RIGHT || keyCode == WXK_NUMPAD_RIGHT) + keyCode = WXK_LEFT; + } -//skip middle component when navigating via keyboard -const size_t row = grid.getGridCursor(); + //skip middle component when navigating via keyboard + const size_t row = grid.getGridCursor(); -if (event.ShiftDown()) - ; -else if (event.ControlDown()) - ; -else - switch (keyCode) - { - case WXK_LEFT: - case WXK_NUMPAD_LEFT: - gridL_.setGridCursor(row, GridEventPolicy::allow); - gridL_.SetFocus(); - //since key event is likely originating from right grid, we need to set scrollMaster manually! - scrollMaster_ = &gridL_; //onKeyDown is called *after* onGridAccessL()! - return; //swallow event - - case WXK_RIGHT: - case WXK_NUMPAD_RIGHT: - gridR_.setGridCursor(row, GridEventPolicy::allow); - gridR_.SetFocus(); - scrollMaster_ = &gridR_; - return; //swallow event - } + if (event.ShiftDown()) + ; + else if (event.ControlDown()) + ; + else + switch (keyCode) + { + case WXK_LEFT: + case WXK_NUMPAD_LEFT: + gridL_.setGridCursor(row, GridEventPolicy::allow); + gridL_.SetFocus(); + //since key event is likely originating from right grid, we need to set scrollMaster manually! + scrollMaster_ = &gridL_; //onKeyDown is called *after* onGridAccessL()! + return; //swallow event + + case WXK_RIGHT: + case WXK_NUMPAD_RIGHT: + gridR_.setGridCursor(row, GridEventPolicy::allow); + gridR_.SetFocus(); + scrollMaster_ = &gridR_; + return; //swallow event + } -event.Skip(); + event.Skip(); } void onResizeColumnL(GridColumnResizeEvent& event) { resizeOtherSide(gridL_, gridR_, event.colType_, event.offset_); } @@ -1703,23 +1733,23 @@ event.Skip(); void resizeOtherSide(const Grid& src, Grid& trg, ColumnType type, int offset) { -//find stretch factor of resized column: type is unique due to makeConsistent()! -std::vector<Grid::ColAttributes> cfgSrc = src.getColumnConfig(); -auto it = std::find_if(cfgSrc.begin(), cfgSrc.end(), [&](Grid::ColAttributes& ca) { return ca.type == type; }); -if (it == cfgSrc.end()) - return; -const int stretchSrc = it->stretch; - -//we do not propagate resizings on stretched columns to the other side: awkward user experience -if (stretchSrc > 0) - return; - -//apply resized offset to other side, but only if stretch factors match! -std::vector<Grid::ColAttributes> cfgTrg = trg.getColumnConfig(); -for (Grid::ColAttributes& ca : cfgTrg) - if (ca.type == type && ca.stretch == stretchSrc) - ca.offset = offset; -trg.setColumnConfig(cfgTrg); + //find stretch factor of resized column: type is unique due to makeConsistent()! + std::vector<Grid::ColAttributes> cfgSrc = src.getColumnConfig(); + auto it = std::find_if(cfgSrc.begin(), cfgSrc.end(), [&](Grid::ColAttributes& ca) { return ca.type == type; }); + if (it == cfgSrc.end()) + return; + const int stretchSrc = it->stretch; + + //we do not propagate resizings on stretched columns to the other side: awkward user experience + if (stretchSrc > 0) + return; + + //apply resized offset to other side, but only if stretch factors match! + std::vector<Grid::ColAttributes> cfgTrg = trg.getColumnConfig(); + for (Grid::ColAttributes& ca : cfgTrg) + if (ca.type == type && ca.stretch == stretchSrc) + ca.offset = offset; + trg.setColumnConfig(cfgTrg); } void onGridAccessL(wxEvent& event) { scrollMaster_ = &gridL_; event.Skip(); } @@ -1728,74 +1758,74 @@ trg.setColumnConfig(cfgTrg); void onPaintGrid(const Grid& grid) { -//align scroll positions of all three grids *synchronously* during paint event! (wxGTK has visible delay when this is done asynchronously, no delay on Windows) - -//determine lead grid -const Grid* lead = nullptr; -Grid* follow1 = nullptr; -Grid* follow2 = nullptr; -auto setGrids = [&](const Grid& l, Grid& f1, Grid& f2) { lead = &l; follow1 = &f1; follow2 = &f2; }; - -if (&gridC_ == scrollMaster_) - setGrids(gridC_, gridL_, gridR_); -else if (&gridR_ == scrollMaster_) - setGrids(gridR_, gridL_, gridC_); -else //default: left panel - setGrids(gridL_, gridC_, gridR_); - -//align other grids only while repainting the lead grid to avoid scrolling and updating a grid at the same time! -if (lead == &grid) -{ - auto scroll = [](Grid& target, int y) //support polling - { - //scroll vertically only - scrolling horizontally becomes annoying if left and right sides have different widths; - //e.g. h-scroll on left would be undone when scrolling vertically on right which doesn't have a h-scrollbar - int yOld = 0; - target.GetViewStart(nullptr, &yOld); - if (yOld != y) - target.Scroll(-1, y); //empirical test Windows/Ubuntu: this call does NOT trigger a wxEVT_SCROLLWIN event, which would incorrectly set "scrollMaster" to "&target"! - //CAVEAT: wxScrolledWindow::Scroll() internally calls wxWindow::Update(), leading to immediate WM_PAINT handling in the target grid! - // an this while we're still in our WM_PAINT handler! => no recursion, fine (hopefully) - }; - int y = 0; - lead->GetViewStart(nullptr, &y); - scroll(*follow1, y); - scroll(*follow2, y); -} + //align scroll positions of all three grids *synchronously* during paint event! (wxGTK has visible delay when this is done asynchronously, no delay on Windows) + + //determine lead grid + const Grid* lead = nullptr; + Grid* follow1 = nullptr; + Grid* follow2 = nullptr; + auto setGrids = [&](const Grid& l, Grid& f1, Grid& f2) { lead = &l; follow1 = &f1; follow2 = &f2; }; + + if (&gridC_ == scrollMaster_) + setGrids(gridC_, gridL_, gridR_); + else if (&gridR_ == scrollMaster_) + setGrids(gridR_, gridL_, gridC_); + else //default: left panel + setGrids(gridL_, gridC_, gridR_); + + //align other grids only while repainting the lead grid to avoid scrolling and updating a grid at the same time! + if (lead == &grid) + { + auto scroll = [](Grid& target, int y) //support polling + { + //scroll vertically only - scrolling horizontally becomes annoying if left and right sides have different widths; + //e.g. h-scroll on left would be undone when scrolling vertically on right which doesn't have a h-scrollbar + int yOld = 0; + target.GetViewStart(nullptr, &yOld); + if (yOld != y) + target.Scroll(-1, y); //empirical test Windows/Ubuntu: this call does NOT trigger a wxEVT_SCROLLWIN event, which would incorrectly set "scrollMaster" to "&target"! + //CAVEAT: wxScrolledWindow::Scroll() internally calls wxWindow::Update(), leading to immediate WM_PAINT handling in the target grid! + // an this while we're still in our WM_PAINT handler! => no recursion, fine (hopefully) + }; + int y = 0; + lead->GetViewStart(nullptr, &y); + scroll(*follow1, y); + scroll(*follow2, y); + } -//harmonize placement of horizontal scrollbar to avoid grids getting out of sync! -//since this affects the grid that is currently repainted as well, we do work asynchronously! -if (!scrollbarUpdatePending_) //send one async event at most, else they may accumulate and create perf issues, see grid.cpp -{ - scrollbarUpdatePending_ = true; - wxCommandEvent alignEvent(EVENT_ALIGN_SCROLLBARS); - AddPendingEvent(alignEvent); //waits until next idle event - may take up to a second if the app is busy on wxGTK! -} + //harmonize placement of horizontal scrollbar to avoid grids getting out of sync! + //since this affects the grid that is currently repainted as well, we do work asynchronously! + if (!scrollbarUpdatePending_) //send one async event at most, else they may accumulate and create perf issues, see grid.cpp + { + scrollbarUpdatePending_ = true; + wxCommandEvent alignEvent(EVENT_ALIGN_SCROLLBARS); + AddPendingEvent(alignEvent); //waits until next idle event - may take up to a second if the app is busy on wxGTK! + } } void onAlignScrollBars(wxEvent& event) { -assert(scrollbarUpdatePending_); -ZEN_ON_SCOPE_EXIT(scrollbarUpdatePending_ = false); + assert(scrollbarUpdatePending_); + ZEN_ON_SCOPE_EXIT(scrollbarUpdatePending_ = false); -auto needsHorizontalScrollbars = [](const Grid& grid) -> bool -{ - const wxWindow& mainWin = grid.getMainWin(); - return mainWin.GetVirtualSize().GetWidth() > mainWin.GetClientSize().GetWidth(); - //assuming Grid::updateWindowSizes() does its job well, this should suffice! - //CAVEAT: if horizontal and vertical scrollbar are circular dependent from each other - //(h-scrollbar is shown due to v-scrollbar consuming horizontal width, etc...) - //while in fact both are NOT needed, this special case results in a bogus need for scrollbars! - //see https://sourceforge.net/tracker/?func=detail&aid=3514183&group_id=234430&atid=1093083 - // => since we're outside the Grid abstraction, we should not duplicate code to handle this special case as it seems to be insignificant -}; + auto needsHorizontalScrollbars = [](const Grid& grid) -> bool + { + const wxWindow& mainWin = grid.getMainWin(); + return mainWin.GetVirtualSize().GetWidth() > mainWin.GetClientSize().GetWidth(); + //assuming Grid::updateWindowSizes() does its job well, this should suffice! + //CAVEAT: if horizontal and vertical scrollbar are circular dependent from each other + //(h-scrollbar is shown due to v-scrollbar consuming horizontal width, etc...) + //while in fact both are NOT needed, this special case results in a bogus need for scrollbars! + //see https://sourceforge.net/tracker/?func=detail&aid=3514183&group_id=234430&atid=1093083 + // => since we're outside the Grid abstraction, we should not duplicate code to handle this special case as it seems to be insignificant + }; -Grid::ScrollBarStatus sbStatusX = needsHorizontalScrollbars(gridL_) || - needsHorizontalScrollbars(gridR_) ? - Grid::SB_SHOW_ALWAYS : Grid::SB_SHOW_NEVER; -gridL_.showScrollBars(sbStatusX, Grid::SB_SHOW_NEVER); -gridC_.showScrollBars(sbStatusX, Grid::SB_SHOW_NEVER); -gridR_.showScrollBars(sbStatusX, Grid::SB_SHOW_AUTOMATIC); + Grid::ScrollBarStatus sbStatusX = needsHorizontalScrollbars(gridL_) || + needsHorizontalScrollbars(gridR_) ? + Grid::SB_SHOW_ALWAYS : Grid::SB_SHOW_NEVER; + gridL_.showScrollBars(sbStatusX, Grid::SB_SHOW_NEVER); + gridC_.showScrollBars(sbStatusX, Grid::SB_SHOW_NEVER); + gridR_.showScrollBars(sbStatusX, Grid::SB_SHOW_AUTOMATIC); } Grid& gridL_; @@ -1842,9 +1872,9 @@ void filegrid::init(Grid& gridLeft, Grid& gridCenter, Grid& gridRight) gridCenter.setColumnConfig( { -{ static_cast<ColumnType>(ColumnTypeCenter::checkbox), widthCheckbox, 0, true }, -{ static_cast<ColumnType>(ColumnTypeCenter::category), widthCategory, 0, true }, -{ static_cast<ColumnType>(ColumnTypeCenter::action), widthAction, 0, true }, + { static_cast<ColumnType>(ColumnTypeCenter::checkbox), widthCheckbox, 0, true }, + { static_cast<ColumnType>(ColumnTypeCenter::category), widthCategory, 0, true }, + { static_cast<ColumnType>(ColumnTypeCenter::action), widthAction, 0, true }, }); } @@ -1852,7 +1882,7 @@ void filegrid::init(Grid& gridLeft, Grid& gridCenter, Grid& gridRight) void filegrid::setData(Grid& grid, FolderComparison& folderCmp) { if (auto* prov = dynamic_cast<GridDataBase*>(grid.getDataProvider())) -return prov->setData(folderCmp); + return prov->setData(folderCmp); throw std::runtime_error("filegrid was not initialized! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); } @@ -1861,7 +1891,7 @@ return prov->setData(folderCmp); FileView& filegrid::getDataView(Grid& grid) { if (auto* prov = dynamic_cast<GridDataBase*>(grid.getDataProvider())) -return prov->getDataView(); + return prov->getDataView(); throw std::runtime_error("filegrid was not initialized! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__)); } @@ -1874,7 +1904,7 @@ class IconUpdater : private wxEvtHandler //update file icons periodically: use S public: IconUpdater(GridDataLeft& provLeft, GridDataRight& provRight, IconBuffer& iconBuffer) : provLeft_(provLeft), provRight_(provRight), iconBuffer_(iconBuffer) { -timer_.Bind(wxEVT_TIMER, [this](wxTimerEvent& event) { loadIconsAsynchronously(event); }); + timer_.Bind(wxEVT_TIMER, [this](wxTimerEvent& event) { loadIconsAsynchronously(event); }); } void start() { if (!timer_.IsRunning()) timer_.Start(100); } //timer interval in [ms] @@ -1885,26 +1915,26 @@ private: void loadIconsAsynchronously(wxEvent& event) //loads all (not yet) drawn icons { -std::vector<std::pair<ptrdiff_t, AbstractPath>> prefetchLoad; -provLeft_ .getUnbufferedIconsForPreload(prefetchLoad); -provRight_.getUnbufferedIconsForPreload(prefetchLoad); + std::vector<std::pair<ptrdiff_t, AbstractPath>> prefetchLoad; + provLeft_ .getUnbufferedIconsForPreload(prefetchLoad); + provRight_.getUnbufferedIconsForPreload(prefetchLoad); -//make sure least-important prefetch rows are inserted first into workload (=> processed last) -//priority index nicely considers both grids at the same time! -std::sort(prefetchLoad.begin(), prefetchLoad.end(), [](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; }); + //make sure least-important prefetch rows are inserted first into workload (=> processed last) + //priority index nicely considers both grids at the same time! + std::sort(prefetchLoad.begin(), prefetchLoad.end(), [](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; }); -//last inserted items are processed first in icon buffer: -std::vector<AbstractPath> newLoad; -for (const auto& [priority, filePath] : prefetchLoad) - newLoad.push_back(filePath); + //last inserted items are processed first in icon buffer: + std::vector<AbstractPath> newLoad; + for (const auto& [priority, filePath] : prefetchLoad) + newLoad.push_back(filePath); -provRight_.updateNewAndGetUnbufferedIcons(newLoad); -provLeft_ .updateNewAndGetUnbufferedIcons(newLoad); + provRight_.updateNewAndGetUnbufferedIcons(newLoad); + provLeft_ .updateNewAndGetUnbufferedIcons(newLoad); -iconBuffer_.setWorkload(newLoad); + iconBuffer_.setWorkload(newLoad); -if (newLoad.empty()) //let's only pay for IconUpdater while needed - stop(); + if (newLoad.empty()) //let's only pay for IconUpdater while needed + stop(); } GridDataLeft& provLeft_; @@ -1916,49 +1946,39 @@ if (newLoad.empty()) //let's only pay for IconUpdater while needed //resolve circular linker dependencies inline -void IconManager::startIconUpdater() { if (iconUpdater_) iconUpdater_->start(); } +void IconManager::startIconUpdater() { assert(iconUpdater_); if (iconUpdater_) iconUpdater_->start(); } } -void filegrid::setupIcons(Grid& gridLeft, Grid& gridCenter, Grid& gridRight, bool show, IconBuffer::IconSize sz) +void filegrid::setupIcons(Grid& gridLeft, Grid& gridCenter, Grid& gridRight, bool showFileIcons, IconBuffer::IconSize sz) { auto* provLeft = dynamic_cast<GridDataLeft*>(gridLeft .getDataProvider()); auto* provRight = dynamic_cast<GridDataRight*>(gridRight.getDataProvider()); if (provLeft && provRight) { -int iconHeight = 0; -if (show) -{ - auto iconMgr = std::make_unique<IconManager>(*provLeft, *provRight, sz); - iconHeight = iconMgr->refIconBuffer().getSize(); - provLeft ->setIconManager(std::move(iconMgr)); -} -else -{ - iconHeight = IconBuffer::getSize(IconBuffer::SIZE_SMALL); - provLeft ->setIconManager(nullptr); -} + auto iconMgr = makeSharedRef<IconManager>(*provLeft, *provRight, sz, showFileIcons); + provLeft ->setIconManager(iconMgr); -const int newRowHeight = std::max(iconHeight, gridLeft.getMainWin().GetCharHeight()) + fastFromDIP(1); //add some space + const int newRowHeight = std::max(iconMgr.ref().getIconSize(), gridLeft.getMainWin().GetCharHeight()) + fastFromDIP(1); //add some space -gridLeft .setRowHeight(newRowHeight); -gridCenter.setRowHeight(newRowHeight); -gridRight .setRowHeight(newRowHeight); + gridLeft .setRowHeight(newRowHeight); + gridCenter.setRowHeight(newRowHeight); + gridRight .setRowHeight(newRowHeight); } else -assert(false); + assert(false); } void filegrid::setItemPathForm(Grid& grid, ItemPathFormat fmt) { if (auto* provLeft = dynamic_cast<GridDataLeft*>(grid.getDataProvider())) -provLeft->setItemPathForm(fmt); + provLeft->setItemPathForm(fmt); else if (auto* provRight = dynamic_cast<GridDataRight*>(grid.getDataProvider())) -provRight->setItemPathForm(fmt); + provRight->setItemPathForm(fmt); else -assert(false); + assert(false); grid.Refresh(); } @@ -1974,24 +1994,24 @@ void filegrid::refresh(Grid& gridLeft, Grid& gridCenter, Grid& gridRight) void filegrid::setScrollMaster(Grid& grid) { if (auto prov = dynamic_cast<GridDataBase*>(grid.getDataProvider())) -if (auto evtMgr = prov->getEventManager()) -{ - evtMgr->setScrollMaster(grid); - return; -} + if (auto evtMgr = prov->getEventManager()) + { + evtMgr->setScrollMaster(grid); + return; + } assert(false); } void filegrid::setNavigationMarker(Grid& gridLeft, - zen::Grid& gridRight, - std::unordered_set<const FileSystemObject*>&& markedFilesAndLinks, - std::unordered_set<const ContainerObject*>&& markedContainer) + zen::Grid& gridRight, + std::unordered_set<const FileSystemObject*>&& markedFilesAndLinks, + std::unordered_set<const ContainerObject*>&& markedContainer) { if (auto grid = dynamic_cast<GridDataBase*>(gridLeft.getDataProvider())) -grid->setNavigationMarker(std::move(markedFilesAndLinks), std::move(markedContainer)); + grid->setNavigationMarker(std::move(markedFilesAndLinks), std::move(markedContainer)); else -assert(false); + assert(false); gridLeft .Refresh(); gridRight.Refresh(); } @@ -2000,9 +2020,9 @@ assert(false); void filegrid::setViewType(Grid& gridCenter, GridViewType vt) { if (auto prov = dynamic_cast<GridDataBase*>(gridCenter.getDataProvider())) -prov->setViewType(vt); + prov->setViewType(vt); else -assert(false); + assert(false); gridCenter.Refresh(); } @@ -2026,8 +2046,8 @@ wxImage fff::getSyncOpImage(SyncOperation syncOp) case SO_COPY_METADATA_TO_RIGHT: return loadImage("so_move_right_sicon"); case SO_DO_NOTHING: return loadImage("so_none_sicon"); case SO_EQUAL: return loadImage("cat_equal_sicon"); -case SO_UNRESOLVED_CONFLICT: return loadImage("cat_conflict_small"); -//*INDENT-ON* + case SO_UNRESOLVED_CONFLICT: return loadImage("cat_conflict_small"); + //*INDENT-ON* } assert(false); return wxNullImage; @@ -2046,8 +2066,8 @@ wxImage fff::getCmpResultImage(CompareFileResult cmpResult) case FILE_DIFFERENT_CONTENT: return loadImage("cat_different_sicon"); case FILE_EQUAL: case FILE_DIFFERENT_METADATA: return loadImage("cat_equal_sicon"); //= sub-category of equal -case FILE_CONFLICT: return loadImage("cat_conflict_small"); -//*INDENT-ON* + case FILE_CONFLICT: return loadImage("cat_conflict_small"); + //*INDENT-ON* } assert(false); return wxNullImage; diff --git a/FreeFileSync/Source/ui/file_grid.h b/FreeFileSync/Source/ui/file_grid.h index a18d8daa..d6406fad 100644 --- a/FreeFileSync/Source/ui/file_grid.h +++ b/FreeFileSync/Source/ui/file_grid.h @@ -25,7 +25,7 @@ void setData(zen::Grid& grid, FolderComparison& folderCmp); //takes (shared) own void setViewType(zen::Grid& gridCenter, GridViewType vt); -void setupIcons(zen::Grid& gridLeft, zen::Grid& gridCenter, zen::Grid& gridRight, bool show, IconBuffer::IconSize sz); +void setupIcons(zen::Grid& gridLeft, zen::Grid& gridCenter, zen::Grid& gridRight, bool showFileIcons, IconBuffer::IconSize sz); void setItemPathForm(zen::Grid& grid, ItemPathFormat fmt); //only for left/right grid diff --git a/FreeFileSync/Source/ui/file_view.cpp b/FreeFileSync/Source/ui/file_view.cpp index ad25d37e..57a5a500 100644 --- a/FreeFileSync/Source/ui/file_view.cpp +++ b/FreeFileSync/Source/ui/file_view.cpp @@ -7,6 +7,7 @@ #include "file_view.h" #include <zen/stl_tools.h> #include <zen/perf.h> +#include <zen/thread.h> #include "../base/synchronization.h" using namespace zen; @@ -90,7 +91,6 @@ void FileView::updateView(Predicate pred) //save row position for direct random access to FilePair or FolderPair rowPositions_.emplace(objId, row); //costs: 0.28 µs per call - MSVC based on std::set - //"this->" required by two-pass lookup as enforced by GCC 4.7 parentsBuf.clear(); for (const FileSystemObject* fsObj2 = fsObj;;) @@ -334,18 +334,18 @@ FileView::PathDrawInfo FileView::getDrawInfo(size_t row) const size_t groupIdx = viewRef_[row].groupIdx; assert(groupIdx < groupDetails_.size()); - const size_t groupBeginRow = groupDetails_[groupIdx].groupBeginRow; + const size_t groupFirstRow = groupDetails_[groupIdx].groupFirstRow; - const size_t groupEndRow = groupIdx + 1 < groupDetails_.size() ? - groupDetails_[groupIdx + 1].groupBeginRow : - viewRef_.size(); + const size_t groupLastRow = groupIdx + 1 < groupDetails_.size() ? + groupDetails_[groupIdx + 1].groupFirstRow : + viewRef_.size(); FileSystemObject* fsObj = FileSystemObject::retrieve(viewRef_[row].objId); - ContainerObject* folderGroupObj = dynamic_cast<FolderPair*>(fsObj); + FolderPair* folderGroupObj = dynamic_cast<FolderPair*>(fsObj); if (fsObj && !folderGroupObj) - folderGroupObj = &fsObj->parent(); + folderGroupObj = dynamic_cast<FolderPair*>(&fsObj->parent()); - return { groupBeginRow, groupEndRow, groupIdx, viewUpdateId_, folderGroupObj, fsObj }; + return { groupFirstRow, groupLastRow, groupIdx, viewUpdateId_, folderGroupObj, fsObj }; } assert(false); //unexpected: check rowsOnView()! return {}; @@ -497,17 +497,22 @@ bool lessFilePath(const FileSystemObject::ObjectId& lhs, const FileSystemObject: } else if (itR == parentsR.rend()) return false; - else //different components... + + //different components... + if (const std::weak_ordering cmp = compareNatural((*itL)->getItemNameAny(), (*itR)->getItemNameAny()); + std::is_neq(cmp)) { - if (const int rv = compareNatural((*itL)->getItemNameAny(), (*itR)->getItemNameAny()); - rv != 0) - return zen::makeSortDirection(std::less<>(), std::bool_constant<ascending>())(rv, 0); - - /*...with equivalent names: - 1. functional correctness => must not compare equal! e.g. a/a/x and a/A/y - 2. ensure stable sort order */ - return *itL < *itR; + if constexpr (ascending) + return std::is_lt(cmp); + else + return std::is_gt(cmp); } + //return zen::makeSortDirection(std::less<>(), std::bool_constant<ascending>())(rv, 0); + + /*...with equivalent names: + 1. functional correctness => must not compare equal! e.g. a/a/x and a/A/y + 2. ensure stable sort order */ + return *itL < *itR; } diff --git a/FreeFileSync/Source/ui/file_view.h b/FreeFileSync/Source/ui/file_view.h index 955b385f..416a4edf 100644 --- a/FreeFileSync/Source/ui/file_view.h +++ b/FreeFileSync/Source/ui/file_view.h @@ -36,12 +36,12 @@ public: struct PathDrawInfo { - size_t groupBeginRow = 0; //half-open range - size_t groupEndRow = 0; // + size_t groupFirstRow = 0; //half-open range + size_t groupLastRow = 0; // const size_t groupIdx = 0; - uint64_t viewUpdateId = 0; //detect invalid buffers after updateView() - ContainerObject* folderGroupObj = nullptr; // - FileSystemObject* fsObj = nullptr; //nullptr if object is not found + uint64_t viewUpdateId = 0; //help detect invalid buffers after updateView() + FolderPair* folderGroupObj = nullptr; //nullptr if group is BaseFolderPair (or fsObj not found) + FileSystemObject* fsObj = nullptr; //nullptr if object is not found }; PathDrawInfo getDrawInfo(size_t row); //complexity: constant! @@ -138,10 +138,12 @@ private: struct GroupDetail { - size_t groupBeginRow = 0; + size_t groupFirstRow = 0; }; std::vector<GroupDetail> groupDetails_; + uint64_t viewUpdateId_ = 0; //help clients detect invalid buffers after updateView() + struct ViewRow { FileSystemObject::ObjectId objId = nullptr; @@ -158,8 +160,6 @@ private: std::vector<std::tuple<const void* /*BaseFolderPair*/, AbstractPath, AbstractPath>> folderPairs_; std::optional<SortInfo> currentSort_; - - uint64_t viewUpdateId_ = 0; //help clients detect invalid buffers after updateView() }; } diff --git a/FreeFileSync/Source/ui/folder_selector.cpp b/FreeFileSync/Source/ui/folder_selector.cpp index c62243d8..44328c95 100644 --- a/FreeFileSync/Source/ui/folder_selector.cpp +++ b/FreeFileSync/Source/ui/folder_selector.cpp @@ -41,12 +41,9 @@ void setFolderPathPhrase(const Zstring& folderPathPhrase, FolderHistoryBox* comb else tooltipWnd.SetToolTip(utfTo<wxString>(folderPathPhraseFmt)); - if (staticText) - { - //change static box label only if there is a real difference to what is shown in wxTextCtrl anyway + if (staticText) //change static box label only if there is a real difference to what is shown in wxTextCtrl anyway staticText->SetLabel(equalNoCase(appendSeparator(trimCpy(folderPathPhrase)), appendSeparator(folderPathPhraseFmt)) ? wxString(_("Drag && drop")) : utfTo<wxString>(folderPathPhraseFmt)); - } } @@ -66,6 +63,7 @@ FolderSelector::FolderSelector(wxWindow* parent, wxButton& selectFolderButton, wxButton& selectAltFolderButton, FolderHistoryBox& folderComboBox, + Zstring& folderLastSelected, Zstring& sftpKeyFileLastSelected, wxStaticText* staticText, wxWindow* dropWindow2, const std::function<bool (const std::vector<Zstring>& shellItemPaths)>& droppedPathsFilter, @@ -80,6 +78,8 @@ FolderSelector::FolderSelector(wxWindow* parent, selectFolderButton_(selectFolderButton), selectAltFolderButton_(selectAltFolderButton), folderComboBox_(folderComboBox), + folderLastSelected_(folderLastSelected), + sftpKeyFileLastSelected_(sftpKeyFileLastSelected), staticText_(staticText) { assert(getDeviceParallelOps_); @@ -96,7 +96,7 @@ FolderSelector::FolderSelector(wxWindow* parent, selectAltFolderButton_.SetBitmapLabel(loadImage("cloud_small")); - //keep dirPicker and dirpath synchronous + //keep folderSelector and dirpath synchronous folderComboBox_ .Bind(wxEVT_MOUSEWHEEL, &FolderSelector::onMouseWheel, this); folderComboBox_ .Bind(wxEVT_COMMAND_TEXT_UPDATED, &FolderSelector::onEditFolderPath, this); folderComboBox_ .Bind(wxEVT_COMMAND_COMBOBOX_SELECTED, &FolderSelector::onHistoryPathSelected, this); @@ -123,19 +123,16 @@ FolderSelector::~FolderSelector() void FolderSelector::onMouseWheel(wxMouseEvent& event) { - //for combobox: although switching through available items is wxWidgets default, this is NOT windows default, e.g. Explorer + //for combobox: although switching through available items is wxWidgets default, this is NOT Windows default, e.g. Explorer //additionally this will delete manual entries, although all the users wanted is scroll the parent window! //redirect to parent scrolled window! - wxWindow* wnd = &folderComboBox_; - while ((wnd = wnd->GetParent()) != nullptr) //silence MSVC warning + for (wxWindow* wnd = folderComboBox_.GetParent(); wnd; wnd = wnd->GetParent()) if (dynamic_cast<wxScrolledWindow*>(wnd) != nullptr) if (wxEvtHandler* evtHandler = wnd->GetEventHandler()) - { - evtHandler->AddPendingEvent(event); - break; - } - // event.Skip(); + return evtHandler->AddPendingEvent(event); + assert(false); + event.Skip(); } @@ -199,10 +196,10 @@ void FolderSelector::onEditFolderPath(wxCommandEvent& event) void FolderSelector::onSelectFolder(wxCommandEvent& event) { - Zstring defaultFolderPath; + Zstring defaultFolderNative; { //make sure default folder exists: don't let folder picker hang on non-existing network share! - auto folderExistsTimed = [](const AbstractPath& folderPath) + auto folderExistsTimed = [waitEndTime = std::chrono::steady_clock::now() + FOLDER_SELECTED_EXISTENCE_CHECK_TIME_MAX](const AbstractPath& folderPath) { auto ft = runAsync([folderPath] { @@ -212,28 +209,35 @@ void FolderSelector::onSelectFolder(wxCommandEvent& event) } catch (FileError&) { return false; } }); - return ft.wait_for(FOLDER_SELECTED_EXISTENCE_CHECK_TIME_MAX) == std::future_status::ready && ft.get(); //potentially slow network access: wait 200ms at most + return ft.wait_until(waitEndTime) == std::future_status::ready && ft.get(); //potentially slow network access: wait 200ms at most }; - const Zstring folderPathPhrase = getPath(); - - if (acceptsItemPathPhraseNative(folderPathPhrase)) //noexcept + auto trySetDefaultPath = [&](const Zstring& folderPathPhrase) { - const AbstractPath folderPath = createItemPathNative(folderPathPhrase); - if (folderExistsTimed(folderPath)) - if (std::optional<Zstring> nativeFolderPath = AFS::getNativeItemPath(folderPath)) - defaultFolderPath = *nativeFolderPath; - } + if (acceptsItemPathPhraseNative(folderPathPhrase)) //noexcept + { + const AbstractPath folderPath = createItemPathNative(folderPathPhrase); + if (folderExistsTimed(folderPath)) + if (std::optional<Zstring> nativeFolderPath = AFS::getNativeItemPath(folderPath)) + defaultFolderNative = *nativeFolderPath; + } + }; + const Zstring& currentFolderPath = getPath(); + trySetDefaultPath(currentFolderPath); + + if (defaultFolderNative.empty() && //=> fallback: use last user-selected path + trimCpy(folderLastSelected_) != trimCpy(currentFolderPath) /*case-sensitive comp for path phrase!*/) + trySetDefaultPath(folderLastSelected_); } Zstring shellItemPath; - wxDirDialog dirPicker(parent_, _("Select a folder"), utfTo<wxString>(defaultFolderPath), wxDD_DEFAULT_STYLE | wxDD_SHOW_HIDDEN); + //default size? Windows: not implemented, Linux(GTK2): not implemented, macOS: not implemented => wxWidgets, what is this shit!? + wxDirDialog folderSelector(parent_, _("Select a folder"), utfTo<wxString>(defaultFolderNative), wxDD_DEFAULT_STYLE | wxDD_SHOW_HIDDEN); //GTK2: "Show hidden" is also available as a context menu option in the folder picker! //It looks like wxDD_SHOW_HIDDEN only sets the default when opening for the first time!? - - if (dirPicker.ShowModal() != wxID_OK) + if (folderSelector.ShowModal() != wxID_OK) return; - shellItemPath = utfTo<Zstring>(dirPicker.GetPath()); + shellItemPath = utfTo<Zstring>(folderSelector.GetPath()); if (endsWith(shellItemPath, Zstr(' '))) //prevent createAbstractPath() from trimming legit trailing blank! shellItemPath += FILE_NAME_SEPARATOR; @@ -241,6 +245,7 @@ void FolderSelector::onSelectFolder(wxCommandEvent& event) const Zstring newFolderPathPhrase = AFS::getInitPathPhrase(createAbstractPath(shellItemPath)); //noexcept setPath(newFolderPathPhrase); + folderLastSelected_ = newFolderPathPhrase; //notify action invoked by user wxCommandEvent dummy(EVENT_ON_FOLDER_SELECTED); @@ -257,7 +262,7 @@ void FolderSelector::onSelectAltFolder(wxCommandEvent& event) parallelOpsDisabledReason = _("Requires FreeFileSync Donation Edition"); - if (showCloudSetupDialog(parent_, folderPathPhrase, parallelOps, get(parallelOpsDisabledReason)) != ReturnSmallDlg::BUTTON_OKAY) + if (showCloudSetupDialog(parent_, folderPathPhrase, sftpKeyFileLastSelected_, parallelOps, get(parallelOpsDisabledReason)) != ConfirmationButton::accept) return; setPath(folderPathPhrase); diff --git a/FreeFileSync/Source/ui/folder_selector.h b/FreeFileSync/Source/ui/folder_selector.h index 0b46acb2..9b1f6a08 100644 --- a/FreeFileSync/Source/ui/folder_selector.h +++ b/FreeFileSync/Source/ui/folder_selector.h @@ -37,6 +37,7 @@ public: wxButton& selectFolderButton, wxButton& selectAltFolderButton, FolderHistoryBox& folderComboBox, + Zstring& folderLastSelected, Zstring& sftpKeyFileLastSelected, wxStaticText* staticText, //optional wxWindow* dropWindow2, // const std::function<bool (const std::vector<Zstring>& shellItemPaths)>& droppedPathsFilter, //optional @@ -47,10 +48,10 @@ public: void setSiblingSelector(FolderSelector* selector) { siblingSelector_ = selector; } - Zstring getPath() const; void setPath(const Zstring& folderPathPhrase); + Zstring getPath() const; - void setBackgroundText(const std::wstring& text) { folderComboBox_.SetHint(text); } + //void setBackgroundText(const std::wstring& text) { folderComboBox_.SetHint(text); } => no text shown when control is disabled! private: void onMouseWheel (wxMouseEvent& event); @@ -60,18 +61,21 @@ private: void onSelectFolder (wxCommandEvent& event); void onSelectAltFolder(wxCommandEvent& event); - const std::function<bool (const std::vector<Zstring>& shellItemPaths)> droppedPathsFilter_; + const std::function<bool(const std::vector<Zstring>& shellItemPaths)> droppedPathsFilter_; + const std::function<size_t(const Zstring& folderPathPhrase)> getDeviceParallelOps_; const std::function<void (const Zstring& folderPathPhrase, size_t parallelOps)> setDeviceParallelOps_; wxWindow* parent_; wxWindow& dropWindow_; - wxWindow* dropWindow2_ = nullptr; + wxWindow* dropWindow2_ = nullptr; // wxButton& selectFolderButton_; wxButton& selectAltFolderButton_; FolderHistoryBox& folderComboBox_; - wxStaticText* staticText_ = nullptr; //optional - FolderSelector* siblingSelector_ = nullptr; + Zstring& folderLastSelected_; + Zstring& sftpKeyFileLastSelected_; + wxStaticText* staticText_ = nullptr; //optional + FolderSelector* siblingSelector_ = nullptr; // }; } diff --git a/FreeFileSync/Source/ui/gui_generated.cpp b/FreeFileSync/Source/ui/gui_generated.cpp index f7607001..c5c1c80a 100644 --- a/FreeFileSync/Source/ui/gui_generated.cpp +++ b/FreeFileSync/Source/ui/gui_generated.cpp @@ -4877,10 +4877,10 @@ SelectTimespanDlgGenerated::SelectTimespanDlgGenerated( wxWindow* parent, wxWind wxBoxSizer* bSizer98; bSizer98 = new wxBoxSizer( wxHORIZONTAL ); - m_calendarFrom = new wxCalendarCtrl( m_panel35, wxID_ANY, wxDefaultDateTime, wxDefaultPosition, wxDefaultSize, wxCAL_SHOW_HOLIDAYS|wxBORDER_NONE ); + m_calendarFrom = new wxCalendarCtrl( m_panel35, wxID_ANY, wxDefaultDateTime, wxDefaultPosition, wxDefaultSize, wxCAL_SHOW_HOLIDAYS|wxCAL_SHOW_SURROUNDING_WEEKS|wxBORDER_NONE ); bSizer98->Add( m_calendarFrom, 0, wxTOP|wxBOTTOM|wxLEFT, 10 ); - m_calendarTo = new wxCalendarCtrl( m_panel35, wxID_ANY, wxDefaultDateTime, wxDefaultPosition, wxDefaultSize, wxCAL_SHOW_HOLIDAYS|wxBORDER_NONE ); + m_calendarTo = new wxCalendarCtrl( m_panel35, wxID_ANY, wxDefaultDateTime, wxDefaultPosition, wxDefaultSize, wxCAL_SHOW_HOLIDAYS|wxCAL_SHOW_SURROUNDING_WEEKS|wxBORDER_NONE ); bSizer98->Add( m_calendarTo, 0, wxALL, 10 ); diff --git a/FreeFileSync/Source/ui/gui_status_handler.cpp b/FreeFileSync/Source/ui/gui_status_handler.cpp index 3cb6aaaa..4928a220 100644 --- a/FreeFileSync/Source/ui/gui_status_handler.cpp +++ b/FreeFileSync/Source/ui/gui_status_handler.cpp @@ -252,7 +252,7 @@ ProcessCallback::Response StatusHandlerTemporaryPanel::reportError(const std::ws case ConfirmationButton3::accept: //ignore return ProcessCallback::ignore; - case ConfirmationButton3::acceptAll: //ignore all + case ConfirmationButton3::accept2: //ignore all mainDlg_.compareStatus_->setOptionIgnoreErrors(true); return ProcessCallback::ignore; @@ -292,7 +292,7 @@ void StatusHandlerTemporaryPanel::reportFatalError(const std::wstring& msg) case ConfirmationButton2::accept: //ignore break; - case ConfirmationButton2::acceptAll: //ignore all + case ConfirmationButton2::accept2: //ignore all mainDlg_.compareStatus_->setOptionIgnoreErrors(true); break; @@ -607,7 +607,7 @@ ProcessCallback::Response StatusHandlerFloatingDialog::reportError(const std::ws case ConfirmationButton3::accept: //ignore return ProcessCallback::ignore; - case ConfirmationButton3::acceptAll: //ignore all + case ConfirmationButton3::accept2: //ignore all progressDlg_->setOptionIgnoreErrors(true); return ProcessCallback::ignore; @@ -647,7 +647,7 @@ void StatusHandlerFloatingDialog::reportFatalError(const std::wstring& msg) case ConfirmationButton2::accept: //ignore break; - case ConfirmationButton2::acceptAll: //ignore all + case ConfirmationButton2::accept2: //ignore all progressDlg_->setOptionIgnoreErrors(true); break; diff --git a/FreeFileSync/Source/ui/log_panel.cpp b/FreeFileSync/Source/ui/log_panel.cpp index 2b9b91da..73412a0c 100644 --- a/FreeFileSync/Source/ui/log_panel.cpp +++ b/FreeFileSync/Source/ui/log_panel.cpp @@ -97,7 +97,7 @@ public: if (c == '\n') { if (!lastCharNewline) //do not reference empty lines! - viewRef_.emplace_back(it, rowNumber); + viewRef_.push_back({it, rowNumber}); ++rowNumber; lastCharNewline = true; } @@ -105,7 +105,7 @@ public: lastCharNewline = false; if (!lastCharNewline) - viewRef_.emplace_back(it, rowNumber); + viewRef_.push_back({it, rowNumber}); } } @@ -132,8 +132,6 @@ private: struct Line { - Line(ErrorLog::const_iterator it, size_t rowNum) : logIt(it), row(rowNum) {} - ErrorLog::const_iterator logIt; //always bound! size_t row; //LogEntry::message may span multiple rows }; @@ -189,7 +187,10 @@ public: void renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected) override { - GridData::renderRowBackgound(dc, rect, row, true /*enabled*/, selected); + if (!enabled || !selected) + clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); + else + GridData::renderRowBackgound(dc, rect, row, true /*enabled*/, true /*selected*/ ); //-------------- draw item separation line ----------------- wxDCPenChanger dummy2(dc, wxPen(getColorGridLine(), fastFromDIP(1))); @@ -381,7 +382,6 @@ MessageView& LogPanel::getDataView() } - void LogPanel::updateGrid() { int includedTypes = 0; diff --git a/FreeFileSync/Source/ui/main_dlg.cpp b/FreeFileSync/Source/ui/main_dlg.cpp index 491b7321..ad0c898d 100644 --- a/FreeFileSync/Source/ui/main_dlg.cpp +++ b/FreeFileSync/Source/ui/main_dlg.cpp @@ -114,22 +114,24 @@ public: FolderPairCallback(GuiPanel& basicPanel, MainDialog& mainDialog, wxPanel& dropWindow1L, - wxButton& selectFolderButtonL, - wxButton& selectSftpButtonL, - FolderHistoryBox& dirpathL, - wxStaticText* staticTextL, - wxWindow* dropWindow2L, - wxPanel& dropWindow1R, + wxButton& selectFolderButtonL, wxButton& selectFolderButtonR, + wxButton& selectSftpButtonL, wxButton& selectSftpButtonR, + FolderHistoryBox& dirpathL, FolderHistoryBox& dirpathR, + Zstring& folderLastSelectedL, + Zstring& folderLastSelectedR, + Zstring& sftpKeyFileLastSelected, + wxStaticText* staticTextL, wxStaticText* staticTextR, + wxWindow* dropWindow2L, wxWindow* dropWindow2R) : FolderPairPanelBasic<GuiPanel>(basicPanel), //pass FolderPairPanelGenerated part... mainDlg_(mainDialog), - folderSelectorLeft_ (&mainDialog, dropWindow1L, selectFolderButtonL, selectSftpButtonL, dirpathL, staticTextL, dropWindow2L, droppedPathsFilter_, getDeviceParallelOps_, setDeviceParallelOps_), - folderSelectorRight_(&mainDialog, dropWindow1R, selectFolderButtonR, selectSftpButtonR, dirpathR, staticTextR, dropWindow2R, droppedPathsFilter_, getDeviceParallelOps_, setDeviceParallelOps_) + folderSelectorLeft_ (&mainDialog, dropWindow1L, selectFolderButtonL, selectSftpButtonL, dirpathL, folderLastSelectedL, sftpKeyFileLastSelected, staticTextL, dropWindow2L, droppedPathsFilter_, getDeviceParallelOps_, setDeviceParallelOps_), + folderSelectorRight_(&mainDialog, dropWindow1R, selectFolderButtonR, selectSftpButtonR, dirpathR, folderLastSelectedR, sftpKeyFileLastSelected, staticTextR, dropWindow2R, droppedPathsFilter_, getDeviceParallelOps_, setDeviceParallelOps_) { folderSelectorLeft_ .setSiblingSelector(&folderSelectorRight_); folderSelectorRight_.setSiblingSelector(&folderSelectorLeft_); @@ -195,43 +197,54 @@ class fff::FolderPairPanel : public FolderPairCallback<FolderPairPanelGenerated> { public: - FolderPairPanel(wxWindow* parent, MainDialog& mainDialog) : + FolderPairPanel(wxWindow* parent, + MainDialog& mainDlg, + Zstring& folderLastSelectedL, + Zstring& folderLastSelectedR, + Zstring& sftpKeyFileLastSelected) : FolderPairPanelGenerated(parent), - FolderPairCallback<FolderPairPanelGenerated>(static_cast<FolderPairPanelGenerated&>(*this), mainDialog, + FolderPairCallback<FolderPairPanelGenerated>(static_cast<FolderPairPanelGenerated&>(*this), mainDlg, *m_panelLeft, - *m_buttonSelectFolderLeft, - *m_bpButtonSelectAltFolderLeft, - *m_folderPathLeft, - nullptr /*staticText*/, nullptr /*dropWindow2*/, - *m_panelRight, + *m_buttonSelectFolderLeft, *m_buttonSelectFolderRight, + *m_bpButtonSelectAltFolderLeft, *m_bpButtonSelectAltFolderRight, + *m_folderPathLeft, *m_folderPathRight, - nullptr /*staticText*/, nullptr /*dropWindow2*/) {} + folderLastSelectedL, + folderLastSelectedR, + sftpKeyFileLastSelected, + nullptr /*staticText*/, nullptr /*staticText*/, + nullptr /*dropWindow2*/, nullptr /*dropWindow2*/) {} }; class fff::FolderPairFirst : public FolderPairCallback<MainDialogGenerated> { public: - FolderPairFirst(MainDialog& mainDialog) : - FolderPairCallback<MainDialogGenerated>(mainDialog, mainDialog, - - *mainDialog.m_panelTopLeft, - *mainDialog.m_buttonSelectFolderLeft, - *mainDialog.m_bpButtonSelectAltFolderLeft, - *mainDialog.m_folderPathLeft, - mainDialog.m_staticTextResolvedPathL, - &mainDialog.m_gridMainL->getMainWin(), - - *mainDialog.m_panelTopRight, - *mainDialog.m_buttonSelectFolderRight, - *mainDialog.m_bpButtonSelectAltFolderRight, - *mainDialog.m_folderPathRight, - mainDialog.m_staticTextResolvedPathR, - &mainDialog.m_gridMainR->getMainWin()) {} + FolderPairFirst(MainDialog& mainDlg, + Zstring& folderLastSelectedL, + Zstring& folderLastSelectedR, + Zstring& sftpKeyFileLastSelected) : + FolderPairCallback<MainDialogGenerated>(mainDlg, mainDlg, + + *mainDlg.m_panelTopLeft, + *mainDlg.m_panelTopRight, + *mainDlg.m_buttonSelectFolderLeft, + *mainDlg.m_buttonSelectFolderRight, + *mainDlg.m_bpButtonSelectAltFolderLeft, + *mainDlg.m_bpButtonSelectAltFolderRight, + *mainDlg.m_folderPathLeft, + *mainDlg.m_folderPathRight, + folderLastSelectedL, + folderLastSelectedR, + sftpKeyFileLastSelected, + mainDlg.m_staticTextResolvedPathL, + mainDlg.m_staticTextResolvedPathR, + &mainDlg.m_gridMainL->getMainWin(), + &mainDlg.m_gridMainR->getMainWin()) {} }; @@ -307,7 +320,7 @@ void MainDialog::create(const Zstring& globalConfigFilePath) { const XmlGlobalSettings globalSettings = tryLoadGlobalConfig(globalConfigFilePath); - std::vector<Zstring> cfgFilePaths = globalSettings.gui.mainDlg.lastUsedConfigFiles; + std::vector<Zstring> cfgFilePaths = globalSettings.gui.mainDlg.cfgFilesLastUsed; //------------------------------------------------------------------------------------------ //check existence of all files in parallel: @@ -405,7 +418,6 @@ MainDialog::MainDialog(const Zstring& globalConfigFilePath, m_splitterMain->SetSizer(nullptr); //alas wxFormbuilder doesn't allow us to have child windows without a sizer, so we have to remove it here m_splitterMain->setupWindows(m_gridMainL, m_gridMainC, m_gridMainR); - setRelativeFontSize(*m_buttonCompare, 1.4); setRelativeFontSize(*m_buttonSync, 1.4); setRelativeFontSize(*m_buttonCancel, 1.4); @@ -646,7 +658,7 @@ MainDialog::MainDialog(const Zstring& globalConfigFilePath, m_bpButtonCmpConfig ->SetToolTip(replaceCpy(_("C&omparison settings"), L"&", L"") + L" (F6)"); // m_bpButtonSyncConfig->SetToolTip(replaceCpy(_("S&ynchronization settings"), L"&", L"") + L" (F8)"); // m_buttonSync ->SetToolTip(replaceCpy(_("Start &synchronization"), L"&", L"") + L" (F9)"); // - m_bpButtonSwapSides ->SetToolTip(_("Swap sides") + L" (F10)"); + m_bpButtonSwapSides ->SetToolTip(_("Swap sides") + L" (Ctrl+W)"); m_bpButtonCmpContext ->SetToolTip(m_bpButtonCmpConfig ->GetToolTipText()); m_bpButtonSyncContext->SetToolTip(m_bpButtonSyncConfig->GetToolTipText()); @@ -714,25 +726,15 @@ MainDialog::MainDialog(const Zstring& globalConfigFilePath, m_menuTools->Bind(wxEVT_MENU_OPEN, [this](wxMenuEvent& event) { onOpenMenuTools(event); }); - //show FreeFileSync update reminder - if (!globalSettings.gui.lastOnlineVersion.empty() && haveNewerVersionOnline(globalSettings.gui.lastOnlineVersion)) - { - auto menu = new wxMenu(); - wxMenuItem* newItem = new wxMenuItem(menu, wxID_ANY, _("&Show details")); - Bind(wxEVT_COMMAND_MENU_SELECTED, [this](wxCommandEvent& event) { onMenuUpdateAvailable(event); }, newItem->GetId()); - - menu->Append(newItem); //pass ownership - - const std::wstring blackStar = utfTo<std::wstring>("★"); - m_menubar->Append(menu, blackStar + L' ' + replaceCpy(_("FreeFileSync %x is available!"), L"%x", utfTo<std::wstring>(globalSettings.gui.lastOnlineVersion)) + L' ' + blackStar); - } - //notify about (logical) application main window => program won't quit, but stay on this dialog setGlobalWindow(this); //init handling of first folder pair - firstFolderPair_ = std::make_unique<FolderPairFirst>(*this); + firstFolderPair_ = std::make_unique<FolderPairFirst>(*this, + globalCfg_.gui.mainDlg.folderLastSelectedLeft, + globalCfg_.gui.mainDlg.folderLastSelectedRight, + globalCfg_.gui.sftpKeyFileLastSelected); //init grid settings filegrid::init(*m_gridMainL, *m_gridMainC, *m_gridMainR); @@ -780,7 +782,7 @@ MainDialog::MainDialog(const Zstring& globalConfigFilePath, updateGui(); //register regular check for update on next idle event - Bind(wxEVT_IDLE, &MainDialog::onRegularUpdateCheck, this); + Bind(wxEVT_IDLE, &MainDialog::onStartupUpdateCheck, this); //asynchronous call to wxWindow::Layout(): fix superfluous frame on right and bottom when FFS is started in fullscreen mode Bind(wxEVT_IDLE, &MainDialog::onLayoutWindowAsync, this); @@ -947,7 +949,7 @@ void MainDialog::setGlobalCfgOnInit(const XmlGlobalSettings& globalSettings) 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 setups: dialog may be placed on second monitor which is currently turned off + // - multi-monitor setup: dialog may be placed on second monitor which is currently turned off if (globalSettings.gui.mainDlg.dlgSize.GetWidth () > 0 && globalSettings.gui.mainDlg.dlgSize.GetHeight() > 0) { @@ -1120,7 +1122,7 @@ XmlGlobalSettings MainDialog::getGlobalCfgBeforeExit() std::tie(globalSettings.gui.mainDlg.cfgGridLastSortColumn, globalSettings.gui.mainDlg.cfgGridLastSortAscending) = cfggrid::getDataView(*m_gridCfgHistory).getSortDirection(); //-------------------------------------------------------------------------------- - globalSettings.gui.mainDlg.lastUsedConfigFiles = activeConfigFiles_; + globalSettings.gui.mainDlg.cfgFilesLastUsed = activeConfigFiles_; //write list of last used folders globalSettings.gui.mainDlg.folderHistoryLeft = folderHistoryLeft_ ->getList(); @@ -1355,10 +1357,12 @@ void MainDialog::copyToAlternateFolder(const std::vector<FileSystemObject*>& sel if (showCopyToDialog(this, selectionLeft, selectionRight, - globalCfg_.gui.mainDlg.copyToCfg.lastUsedPath, + globalCfg_.gui.mainDlg.copyToCfg.targetFolderPath, + globalCfg_.gui.mainDlg.copyToCfg.targetFolderLastSelected, globalCfg_.gui.mainDlg.copyToCfg.folderHistory, globalCfg_.gui.folderHistoryMax, + globalCfg_.gui.sftpKeyFileLastSelected, globalCfg_.gui.mainDlg.copyToCfg.keepRelPaths, - globalCfg_.gui.mainDlg.copyToCfg.overwriteIfExists) != ReturnSmallDlg::BUTTON_OKAY) + globalCfg_.gui.mainDlg.copyToCfg.overwriteIfExists) != ConfirmationButton::accept) return; disableAllElements(true /*enableAbort*/); //StatusHandlerTemporaryPanel will internally process Window messages, so avoid unexpected callbacks! @@ -1374,7 +1378,7 @@ void MainDialog::copyToAlternateFolder(const std::vector<FileSystemObject*>& sel try { fff::copyToAlternateFolder(selectionLeft, selectionRight, - globalCfg_.gui.mainDlg.copyToCfg.lastUsedPath, + globalCfg_.gui.mainDlg.copyToCfg.targetFolderPath, globalCfg_.gui.mainDlg.copyToCfg.keepRelPaths, globalCfg_.gui.mainDlg.copyToCfg.overwriteIfExists, globalCfg_.warnDlgs, @@ -1402,7 +1406,7 @@ void MainDialog::deleteSelectedFiles(const std::vector<FileSystemObject*>& selec FocusPreserver fp; if (showDeleteDialog(this, selectionLeft, selectionRight, - moveToRecycler) != ReturnSmallDlg::BUTTON_OKAY) + moveToRecycler) != ConfirmationButton::accept) return; disableAllElements(true /*enableAbort*/); //StatusHandlerTemporaryPanel will internally process Window messages, so avoid unexpected callbacks! @@ -2044,13 +2048,19 @@ void MainDialog::onLocalKeyEvent(wxKeyEvent& event) //process key events without const int keyCode = event.GetKeyCode(); //CTRL + X - //if (event.ControlDown()) - // switch (keyCode) - // { - // case 'F': //CTRL + F - // showFindPanel(); - // return; //-> swallow event! - // } + if (event.ControlDown()) + switch (keyCode) + { + // case 'F': //CTRL + F + // showFindPanel(); + // return; //-> swallow event! + + case 'W': //CTRL + W + //don't use F10: and avoid accidental clicks: https://freefilesync.org/forum/viewtopic.php?t=1663 + wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED); + m_bpButtonSwapSides->Command(dummy); //simulate click + return; //-> swallow event! + } switch (keyCode) { @@ -2080,16 +2090,6 @@ void MainDialog::onLocalKeyEvent(wxKeyEvent& event) //process key events without //} //return; //-> swallow event! - case WXK_F10: - if (event.ShiftDown()) //shfit + F10 == alias for menu key - break; - else - { - wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED); - m_bpButtonSwapSides->Command(dummy); //simulate click - return; //-> swallow event! - } - case WXK_F11: setGridViewType(m_bpButtonViewTypeSyncAction->isActive() ? GridViewType::category : GridViewType::action); return; //-> swallow event! @@ -2331,18 +2331,15 @@ void MainDialog::onGridGroupContextRim(GridClickEvent& event, bool leftSide) if (const FileView::PathDrawInfo pdi = filegrid::getDataView(*m_gridMainC).getDrawInfo(event.row_); pdi.folderGroupObj) { - assert(dynamic_cast<FolderPair*>(pdi.folderGroupObj)); - FolderPair* groupFolder = static_cast<FolderPair*>(pdi.folderGroupObj); - m_gridMainL->clearSelection(GridEventPolicy::deny); m_gridMainC->clearSelection(GridEventPolicy::deny); m_gridMainR->clearSelection(GridEventPolicy::deny); std::vector<FileSystemObject*> selectionLeft; std::vector<FileSystemObject*> selectionRight; - (leftSide ? selectionLeft : selectionRight).push_back(groupFolder); + (leftSide ? selectionLeft : selectionRight).push_back(pdi.folderGroupObj); - onGridContextRim({groupFolder}, + onGridContextRim({pdi.folderGroupObj}, selectionLeft, selectionRight, event.mousePos_, leftSide); return; //"swallow" event => suppress default context menu handling @@ -2505,9 +2502,6 @@ void MainDialog::onGridSelectRim(GridSelectEvent& event, bool leftSide) if (const FileView::PathDrawInfo pdi = gridDataView.getDrawInfo(event.mouseClick_->row_); pdi.folderGroupObj) { - assert(dynamic_cast<FolderPair*>(pdi.folderGroupObj)); - FolderPair* groupFolder = static_cast<FolderPair*>(pdi.folderGroupObj); - std::vector<size_t> groupRows; const size_t rowsTotal = gridDataView.rowsOnView(); @@ -2516,13 +2510,13 @@ void MainDialog::onGridSelectRim(GridSelectEvent& event, bool leftSide) { const bool insideGroupFolder = [&] { - if (fsObj == groupFolder) + if (fsObj == pdi.folderGroupObj) return true; for (const FileSystemObject* fsObj2 = fsObj;;) { const ContainerObject& parent = fsObj2->parent(); - if (&parent == groupFolder) + if (&parent == pdi.folderGroupObj) return true; fsObj2 = dynamic_cast<const FolderPair*>(&parent); @@ -2756,7 +2750,7 @@ void MainDialog::onGridLabelContextRim(bool leftSide) auto selectTimeSpan = [&] { - if (showSelectTimespanDlg(this, manualTimeSpanFrom_, manualTimeSpanTo_) == ReturnSmallDlg::BUTTON_OKAY) + if (showSelectTimespanDlg(this, manualTimeSpanFrom_, manualTimeSpanTo_) == ConfirmationButton::accept) { applyTimeSpanFilter(folderCmp_, manualTimeSpanFrom_, manualTimeSpanTo_); //overwrite current active/inactive settings //updateGuiDelayedIf(!m_bpButtonShowExcluded->isActive()); //show update GUI before removing rows @@ -2776,11 +2770,13 @@ void MainDialog::onOpenMenuTools(wxMenuEvent& event) //each layout menu item is either shown and owned by m_menuTools OR detached from m_menuTools and owned by detachedMenuItems_: auto filterLayoutItems = [&](wxMenuItem* menuItem, wxWindow* panelWindow) { - if (!contains(detachedMenuItems_, menuItem)) - detachedMenuItems_.insert(m_menuTools->Remove(menuItem)); //pass ownership - wxAuiPaneInfo& paneInfo = this->auiMgr_.GetPane(panelWindow); - if (!paneInfo.IsShown()) + if (paneInfo.IsShown()) + { + if (!detachedMenuItems_.contains(menuItem)) + detachedMenuItems_.insert(m_menuTools->Remove(menuItem)); //pass ownership + } + else if (detachedMenuItems_.contains(menuItem)) { detachedMenuItems_.erase(menuItem); //pass ownership m_menuTools->Append(menuItem); // @@ -2791,14 +2787,12 @@ void MainDialog::onOpenMenuTools(wxMenuEvent& event) filterLayoutItems(m_menuItemShowViewFilter, m_panelViewFilter); filterLayoutItems(m_menuItemShowConfig, m_panelConfig); filterLayoutItems(m_menuItemShowOverview, m_gridOverview); - event.Skip(); } void MainDialog::resetLayout() { - m_splitterMain->setSashOffset(0); auiMgr_.LoadPerspective(defaultPerspective_, false /*don't call wxAuiManager::Update() => already done in updateGuiForFolderPair() */); updateGuiForFolderPair(); @@ -2904,7 +2898,7 @@ void MainDialog::onDialogFilesDropped(FileDropEvent& event) void MainDialog::onDirSelected(wxCommandEvent& event) { - //left and right directory text-control and dirpicker are synchronized by MainFolderDragDrop automatically + //left and right directory text-control and folderSelector are synchronized by MainFolderDragDrop automatically clearGrid(); //disable the sync button event.Skip(); } @@ -3041,21 +3035,25 @@ bool MainDialog::trySaveConfig(const Zstring* guiCfgPath) //return true if saved } else { - const Zstring defaultFilePath = activeConfigFiles_.size() == 1 && !equalNativePath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstr("SyncSettings.ffs_gui"); - auto defaultFolder = utfTo<wxString>(beforeLast(defaultFilePath, FILE_NAME_SEPARATOR, IfNotFoundReturn::none)); - auto defaultFileName = utfTo<wxString>(afterLast (defaultFilePath, FILE_NAME_SEPARATOR, IfNotFoundReturn::all)); + const Zstring activeCfgFilePath = activeConfigFiles_.size() == 1 && !equalNativePath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstring(); + + std::optional<Zstring> defaultFolderPath = getParentFolderPath(activeCfgFilePath); + if (!defaultFolderPath) + defaultFolderPath = getParentFolderPath(globalCfg_.gui.mainDlg.cfgFileLastSelected); + + Zstring defaultFileName = afterLast(activeCfgFilePath, FILE_NAME_SEPARATOR, IfNotFoundReturn::all); + if (defaultFileName.empty()) + defaultFileName = Zstr("SyncSettings.ffs_gui"); - //attention: activeConfigFiles may be an imported *.ffs_batch file! We don't want to overwrite it with a GUI config! - defaultFileName = beforeLast(defaultFileName, L'.', IfNotFoundReturn::all) + L".ffs_gui"; + //attention: activeConfigFiles_ may be an imported ffs_batch file! We don't want to overwrite it with a GUI config! + defaultFileName = beforeLast(defaultFileName, Zstr('.'), IfNotFoundReturn::all) + Zstr(".ffs_gui"); - wxFileDialog filePicker(this, //put modal dialog on stack: creating this on freestore leads to memleak! - wxString(), //message - defaultFolder, defaultFileName, //OS X really needs dir/file separated like this - wxString(L"FreeFileSync (*.ffs_gui)|*.ffs_gui") + L"|" +_("All files") + L" (*.*)|*", - wxFD_SAVE | wxFD_OVERWRITE_PROMPT); - if (filePicker.ShowModal() != wxID_OK) + wxFileDialog fileSelector(this, wxString() /*message*/, utfTo<wxString>(defaultFolderPath ? *defaultFolderPath : Zstr("")), utfTo<wxString>(defaultFileName), + wxString(L"FreeFileSync (*.ffs_gui)|*.ffs_gui") + L"|" +_("All files") + L" (*.*)|*", + wxFD_SAVE | wxFD_OVERWRITE_PROMPT); + if (fileSelector.ShowModal() != wxID_OK) return false; - cfgFilePath = utfTo<Zstring>(filePicker.GetPath()); + cfgFilePath = globalCfg_.gui.mainDlg.cfgFileLastSelected = utfTo<Zstring>(fileSelector.GetPath()); } const XmlGuiConfig guiCfg = getConfig(); @@ -3120,25 +3118,28 @@ bool MainDialog::trySaveBatchConfig(const Zstring* batchCfgPath) //let user update batch config: this should change batch-exclusive settings only, else the "setLastUsedConfig" below would be somewhat of a lie if (showBatchConfigDialog(this, batchExCfg, - currentCfg_.mainCfg.ignoreErrors) != ReturnBatchConfig::BUTTON_SAVE_AS) + currentCfg_.mainCfg.ignoreErrors) != ConfirmationButton::accept) return false; updateUnsavedCfgStatus(); //nothing else to update on GUI! - const Zstring defaultFilePath = !activeCfgFilePath.empty() ? activeCfgFilePath : Zstr("BatchRun.ffs_batch"); - auto defaultFolder = utfTo<wxString>(beforeLast(defaultFilePath, FILE_NAME_SEPARATOR, IfNotFoundReturn::none)); - auto defaultFileName = utfTo<wxString>(afterLast (defaultFilePath, FILE_NAME_SEPARATOR, IfNotFoundReturn::all)); - //attention: activeConfigFiles may be a *.ffs_gui file! We don't want to overwrite it with a BATCH config! - defaultFileName = beforeLast(defaultFileName, L'.', IfNotFoundReturn::all) + L".ffs_batch"; + std::optional<Zstring> defaultFolderPath = getParentFolderPath(activeCfgFilePath); + if (!defaultFolderPath) + defaultFolderPath = getParentFolderPath(globalCfg_.gui.mainDlg.cfgFileLastSelected); - wxFileDialog filePicker(this, //put modal dialog on stack: creating this on freestore leads to memleak! - wxString(), //message - defaultFolder, defaultFileName, //OS X really needs dir/file separated like this - _("FreeFileSync batch") + L" (*.ffs_batch)|*.ffs_batch" + L"|" +_("All files") + L" (*.*)|*", - wxFD_SAVE | wxFD_OVERWRITE_PROMPT); - if (filePicker.ShowModal() != wxID_OK) + Zstring defaultFileName = afterLast(activeCfgFilePath, FILE_NAME_SEPARATOR, IfNotFoundReturn::all); + if (defaultFileName.empty()) + defaultFileName = Zstr("BatchRun.ffs_batch"); + + //attention: activeConfigFiles_ may be an ffs_gui file! We don't want to overwrite it with a BATCH config! + defaultFileName = beforeLast(defaultFileName, Zstr('.'), IfNotFoundReturn::all) + Zstr(".ffs_batch"); + + wxFileDialog fileSelector(this, wxString() /*message*/, utfTo<wxString>(defaultFolderPath ? *defaultFolderPath : Zstr("")), utfTo<wxString>(defaultFileName), + _("FreeFileSync batch") + L" (*.ffs_batch)|*.ffs_batch" + L"|" +_("All files") + L" (*.*)|*", + wxFD_SAVE | wxFD_OVERWRITE_PROMPT); + if (fileSelector.ShowModal() != wxID_OK) return false; - cfgFilePath = utfTo<Zstring>(filePicker.GetPath()); + cfgFilePath = globalCfg_.gui.mainDlg.cfgFileLastSelected = utfTo<Zstring>(fileSelector.GetPath()); } const XmlGuiConfig guiCfg = getConfig(); @@ -3169,7 +3170,7 @@ bool MainDialog::saveOldConfig() //return false on user abort const Zstring activeCfgFilePath = activeConfigFiles_.size() == 1 && !equalNativePath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstring(); //notify user about changed settings - if (globalCfg_.confirmDlgs.popupOnConfigChange) + if (globalCfg_.confirmDlgs.confirmSaveConfig) if (!activeCfgFilePath.empty()) //only if check is active and non-default config file loaded { @@ -3178,7 +3179,7 @@ bool MainDialog::saveOldConfig() //return false on user abort setTitle(utfTo<wxString>(activeCfgFilePath)). setMainInstructions(replaceCpy(_("Do you want to save changes to %x?"), L"%x", fmtPath(afterLast(activeCfgFilePath, FILE_NAME_SEPARATOR, IfNotFoundReturn::all)))). - setCheckBox(neverSaveChanges, _("Never save &changes"), QuestionButton2::yes), + setCheckBox(neverSaveChanges, _("Never save &changes"), static_cast<ConfirmationButton3>(QuestionButton2::yes)), _("&Save"), _("Do&n't save"))) { case QuestionButton2::yes: //save @@ -3205,7 +3206,7 @@ bool MainDialog::saveOldConfig() //return false on user abort break; case QuestionButton2::no: //don't save - globalCfg_.confirmDlgs.popupOnConfigChange = !neverSaveChanges; + globalCfg_.confirmDlgs.confirmSaveConfig = !neverSaveChanges; break; case QuestionButton2::cancel: @@ -3226,24 +3227,28 @@ void MainDialog::onConfigLoad(wxCommandEvent& event) { const Zstring activeCfgFilePath = activeConfigFiles_.size() == 1 && !equalNativePath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstring(); - wxFileDialog filePicker(this, - wxString(), //message - utfTo<wxString>(beforeLast(activeCfgFilePath, FILE_NAME_SEPARATOR, IfNotFoundReturn::none)), //default folder - wxString(), //default file name - wxString(L"FreeFileSync (*.ffs_gui; *.ffs_batch)|*.ffs_gui;*.ffs_batch") + L"|" +_("All files") + L" (*.*)|*", - wxFD_OPEN | wxFD_MULTIPLE); - if (filePicker.ShowModal() == wxID_OK) - { - wxArrayString tmp; - filePicker.GetPaths(tmp); + std::optional<Zstring> defaultFolderPath = getParentFolderPath(activeCfgFilePath); + if (!defaultFolderPath) + defaultFolderPath = getParentFolderPath(globalCfg_.gui.mainDlg.cfgFileLastSelected); - std::vector<Zstring> filePaths; - for (const wxString& path : tmp) - filePaths.push_back(utfTo<Zstring>(path)); + wxFileDialog fileSelector(this, wxString() /*message*/, utfTo<wxString>(defaultFolderPath ? *defaultFolderPath : Zstr("")), wxString() /*default file name*/, + wxString(L"FreeFileSync (*.ffs_gui; *.ffs_batch)|*.ffs_gui;*.ffs_batch") + L"|" +_("All files") + L" (*.*)|*", + wxFD_OPEN | wxFD_MULTIPLE); + if (fileSelector.ShowModal() != wxID_OK) + return; - assert(!filePaths.empty()); - loadConfiguration(filePaths); - } + wxArrayString tmp; + fileSelector.GetPaths(tmp); + + std::vector<Zstring> filePaths; + for (const wxString& path : tmp) + filePaths.push_back(utfTo<Zstring>(path)); + + if (!filePaths.empty()) + globalCfg_.gui.mainDlg.cfgFileLastSelected = filePaths[0]; + + assert(!filePaths.empty()); + loadConfiguration(filePaths); } @@ -3566,7 +3571,7 @@ void MainDialog::onCfgGridLabelContext(GridLabelClickEvent& event) { int cfgGridSyncOverdueDays = cfggrid::getSyncOverdueDays(*m_gridCfgHistory); - if (showCfgHighlightDlg(this, cfgGridSyncOverdueDays) == ReturnSmallDlg::BUTTON_OKAY) + if (showCfgHighlightDlg(this, cfgGridSyncOverdueDays) == ConfirmationButton::accept) cfggrid::setSyncOverdueDays(*m_gridCfgHistory, cfgGridSyncOverdueDays); }; menu.addItem(_("Highlight..."), setCfgHighlight); @@ -3639,7 +3644,6 @@ void MainDialog::setLastUsedConfig(const XmlGuiConfig& guiConfig, const std::vec void MainDialog::setConfig(const XmlGuiConfig& newGuiCfg, const std::vector<Zstring>& referenceFiles) { - currentCfg_ = newGuiCfg; //evaluate new settings... @@ -3746,11 +3750,12 @@ void MainDialog::showConfigDialog(SyncConfigPanel panelToShow, int localPairInde showMultipleCfgs, globalPairCfg, localCfgs, - globalCfg_.gui.versioningFolderHistory, - globalCfg_.gui.logFolderHistory, + globalCfg_.gui.versioningFolderHistory, globalCfg_.gui.versioningFolderLastSelected, + globalCfg_.gui.logFolderHistory, globalCfg_.gui.logFolderLastSelected, globalCfg_.gui.folderHistoryMax, + globalCfg_.gui.sftpKeyFileLastSelected, globalCfg_.gui.emailHistory, globalCfg_.gui.emailHistoryMax, - globalCfg_.gui.commandHistory, globalCfg_.gui.commandHistoryMax) != ReturnSyncConfig::BUTTON_OKAY) + globalCfg_.gui.commandHistory, globalCfg_.gui.commandHistoryMax) != ConfirmationButton::accept) return; assert(localCfgs.size() == localPairCfgOld.size()); @@ -4056,7 +4061,6 @@ void MainDialog::onCompare(wxCommandEvent& event) void MainDialog::updateGui() { - updateGridViewData(); //update gridDataView and write status information updateStatistics(); @@ -4194,7 +4198,7 @@ void MainDialog::onStartSync(wxCommandEvent& event) if (showSyncConfirmationDlg(this, false /*syncSelection*/, getSyncVariant(guiCfg.mainCfg), SyncStatistics(folderCmp_), - dontShowAgain) != ReturnSmallDlg::BUTTON_OKAY) + dontShowAgain) != ConfirmationButton::accept) return; globalCfg_.confirmDlgs.confirmSyncStart = !dontShowAgain; } @@ -4398,7 +4402,7 @@ void MainDialog::startSyncForSelecction(const std::vector<FileSystemObject*>& se std::vector<FolderPairSyncCfg> fpCfgSelect; for (size_t i = 0; i < folderCmp_.size(); ++i) - if (contains(basePairsSelect, folderCmp_[i].get())) + if (basePairsSelect.contains(folderCmp_[i].get())) { folderCmpSelect.push_back(folderCmp_[i]); fpCfgSelect .push_back( fpCfg[i]); @@ -4413,7 +4417,7 @@ void MainDialog::startSyncForSelecction(const std::vector<FileSystemObject*>& se true /*syncSelection*/, getSyncVariant(guiCfg.mainCfg), SyncStatistics(folderCmpSelect), - dontShowAgain) != ReturnSmallDlg::BUTTON_OKAY) + dontShowAgain) != ConfirmationButton::accept) return; globalCfg_.confirmDlgs.confirmSyncStart = !dontShowAgain; } @@ -4682,6 +4686,23 @@ void MainDialog::onGridLabelLeftClickR(GridLabelClickEvent& event) void MainDialog::onSwapSides(wxCommandEvent& event) { + if (globalCfg_.confirmDlgs.confirmSwapSides) + { + bool dontWarnAgain = false; + switch (showConfirmationDialog(this, DialogInfoType::info, + PopupDialogCfg().setMainInstructions(_("Please confirm you want to swap sides.")). + setCheckBox(dontWarnAgain, _("&Don't show this dialog again")), + _("&Swap"))) + { + case ConfirmationButton::accept: //swap + globalCfg_.confirmDlgs.confirmSwapSides = !dontWarnAgain; + break; + case ConfirmationButton::cancel: + return; + } + } + //------------------------------------------------------ + //swap directory names: LocalPairConfig lpc1st = firstFolderPair_->getValues(); std::swap(lpc1st.folderPathPhraseLeft, lpc1st.folderPathPhraseRight); @@ -4716,7 +4737,6 @@ void MainDialog::onSwapSides(wxCommandEvent& event) m_bpButtonShowUpdateLeft->setActive(m_bpButtonShowUpdateRight->isActive()); m_bpButtonShowUpdateRight->setActive(tmp); */ - //---------------------------------------------------------------------- if (!folderCmp_.empty()) @@ -5097,7 +5117,6 @@ void MainDialog::startFindNext(bool searchAscending) //F3 or ENTER in m_textCtrl void MainDialog::onTopFolderPairAdd(wxCommandEvent& event) { - insertAddFolderPair({ LocalPairConfig() }, 0); moveAddFolderPairUp(0); } @@ -5105,7 +5124,6 @@ void MainDialog::onTopFolderPairAdd(wxCommandEvent& event) void MainDialog::onTopFolderPairRemove(wxCommandEvent& event) { - assert(!additionalFolderPairs_.empty()); if (!additionalFolderPairs_.empty()) { @@ -5121,7 +5139,7 @@ void MainDialog::onLocalCompCfg(wxCommandEvent& event) for (auto it = additionalFolderPairs_.begin(); it != additionalFolderPairs_.end(); ++it) if (eventObj == (*it)->m_bpButtonLocalCompCfg) { - showConfigDialog(SyncConfigPanel::COMPARISON, (it - additionalFolderPairs_.begin()) + 1); + showConfigDialog(SyncConfigPanel::compare, (it - additionalFolderPairs_.begin()) + 1); break; } } @@ -5133,7 +5151,7 @@ void MainDialog::onLocalSyncCfg(wxCommandEvent& event) for (auto it = additionalFolderPairs_.begin(); it != additionalFolderPairs_.end(); ++it) if (eventObj == (*it)->m_bpButtonLocalSyncCfg) { - showConfigDialog(SyncConfigPanel::SYNC, (it - additionalFolderPairs_.begin()) + 1); + showConfigDialog(SyncConfigPanel::sync, (it - additionalFolderPairs_.begin()) + 1); break; } } @@ -5145,7 +5163,7 @@ void MainDialog::onLocalFilterCfg(wxCommandEvent& event) for (auto it = additionalFolderPairs_.begin(); it != additionalFolderPairs_.end(); ++it) if (eventObj == (*it)->m_bpButtonLocalFilter) { - showConfigDialog(SyncConfigPanel::FILTER, (it - additionalFolderPairs_.begin()) + 1); + showConfigDialog(SyncConfigPanel::filter, (it - additionalFolderPairs_.begin()) + 1); break; } } @@ -5153,7 +5171,6 @@ void MainDialog::onLocalFilterCfg(wxCommandEvent& event) void MainDialog::onRemoveFolderPair(wxCommandEvent& event) { - const wxObject* const eventObj = event.GetEventObject(); //find folder pair originating the event for (auto it = additionalFolderPairs_.begin(); it != additionalFolderPairs_.end(); ++it) if (eventObj == (*it)->m_bpButtonRemovePair) @@ -5166,7 +5183,6 @@ void MainDialog::onRemoveFolderPair(wxCommandEvent& event) void MainDialog::onShowFolderPairOptions(wxEvent& event) { - const wxObject* const eventObj = event.GetEventObject(); //find folder pair originating the event for (auto it = additionalFolderPairs_.begin(); it != additionalFolderPairs_.end(); ++it) if (eventObj == (*it)->m_bpButtonFolderPairOptions) @@ -5224,27 +5240,27 @@ void MainDialog::onAddFolderPairKeyEvent(wxKeyEvent& event) { case WXK_PAGEUP: //Alt + Page Up case WXK_NUMPAD_PAGEUP: + { + const ptrdiff_t pos = getAddFolderPairPos(); + if (pos >= 0) { - const ptrdiff_t pos = getAddFolderPairPos(); - if (pos >= 0) - { - moveAddFolderPairUp(pos); - (pos == 0 ? m_folderPathLeft : additionalFolderPairs_[pos - 1]->m_folderPathLeft)->SetFocus(); - } + moveAddFolderPairUp(pos); + (pos == 0 ? m_folderPathLeft : additionalFolderPairs_[pos - 1]->m_folderPathLeft)->SetFocus(); } - return; + } + return; case WXK_PAGEDOWN: //Alt + Page Down case WXK_NUMPAD_PAGEDOWN: + { + const ptrdiff_t pos = getAddFolderPairPos(); + if (0 <= pos && pos + 1 < makeSigned(additionalFolderPairs_.size())) { - const ptrdiff_t pos = getAddFolderPairPos(); - if (0 <= pos && pos + 1 < makeSigned(additionalFolderPairs_.size())) - { - moveAddFolderPairUp(pos + 1); - additionalFolderPairs_[pos + 1]->m_folderPathLeft->SetFocus(); - } + moveAddFolderPairUp(pos + 1); + additionalFolderPairs_[pos + 1]->m_folderPathLeft->SetFocus(); } - return; + } + return; } event.Skip(); @@ -5341,7 +5357,10 @@ void MainDialog::insertAddFolderPair(const std::vector<LocalPairConfig>& newPair } else { - newPair = new FolderPairPanel(m_scrolledWindowFolderPairs, *this); + newPair = new FolderPairPanel(m_scrolledWindowFolderPairs, *this, + globalCfg_.gui.mainDlg.folderLastSelectedLeft, + globalCfg_.gui.mainDlg.folderLastSelectedRight, + globalCfg_.gui.sftpKeyFileLastSelected); //setHistory dropdown history newPair->m_folderPathLeft ->setHistory(folderHistoryLeft_ ); @@ -5458,20 +5477,22 @@ void MainDialog::onMenuOptions(wxCommandEvent& event) void MainDialog::onMenuExportFileList(wxCommandEvent& event) { - //get a filepath - wxFileDialog filePicker(this, //creating this on freestore leads to memleak! - wxString(), //message - wxString(), //default folder path - L"FileList.csv", //default file name - _("Comma-separated values") + L" (*.csv)|*.csv" + L"|" +_("All files") + L" (*.*)|*", - wxFD_SAVE | wxFD_OVERWRITE_PROMPT); - if (filePicker.ShowModal() != wxID_OK) + std::optional<Zstring> defaultFolderPath = getParentFolderPath(globalCfg_.gui.csvFileLastSelected); + + Zstring defaultFileName = afterLast(globalCfg_.gui.csvFileLastSelected, FILE_NAME_SEPARATOR, IfNotFoundReturn::all); + if (defaultFileName.empty()) + defaultFileName = Zstr("FileList.csv"); + + wxFileDialog fileSelector(this, wxString() /*message*/, utfTo<wxString>(defaultFolderPath ? *defaultFolderPath : Zstr("")), utfTo<wxString>(defaultFileName), + _("Comma-separated values") + L" (*.csv)|*.csv" + L"|" +_("All files") + L" (*.*)|*", + wxFD_SAVE | wxFD_OVERWRITE_PROMPT); + if (fileSelector.ShowModal() != wxID_OK) return; + const Zstring csvFilePath = globalCfg_.gui.csvFileLastSelected = utfTo<Zstring>(fileSelector.GetPath()); + //--------------------------------------------------------------------- wxBusyCursor dummy; - const Zstring filePath = utfTo<Zstring>(filePicker.GetPath()); - //https://en.wikipedia.org/wiki/Comma-separated_values const lconv* localInfo = ::localeconv(); //always bound according to doc const bool haveCommaAsDecimalSep = std::string(localInfo->decimal_point) == ","; @@ -5539,7 +5560,7 @@ void MainDialog::onMenuExportFileList(wxCommandEvent& event) try { - TempFileOutput fileOut(filePath, nullptr /*notifyUnbufferedIO*/); //throw FileError + TempFileOutput fileOut(csvFilePath, nullptr /*notifyUnbufferedIO*/); //throw FileError fileOut.write(&header[0], header.size()); //throw FileError, (X) //main grid: write rows one after the other instead of creating one big string: memory allocation might fail; think 1 million rows! @@ -5592,12 +5613,6 @@ void MainDialog::onMenuCheckVersion(wxCommandEvent& event) } -void MainDialog::onMenuUpdateAvailable(wxCommandEvent& event) -{ - checkForUpdateNow(*this, globalCfg_.gui.lastOnlineVersion); //show changelog + handle Donation Edition auto-updater (including expiration) -} - - void MainDialog::onMenuCheckVersionAutomatically(wxCommandEvent& event) { if (updateCheckActive(globalCfg_.gui.lastUpdateCheck)) @@ -5617,12 +5632,27 @@ void MainDialog::onMenuCheckVersionAutomatically(wxCommandEvent& event) } -void MainDialog::onRegularUpdateCheck(wxIdleEvent& event) +void MainDialog::onStartupUpdateCheck(wxIdleEvent& event) { //execute just once per startup! - [[maybe_unused]] bool ubOk = Unbind(wxEVT_IDLE, &MainDialog::onRegularUpdateCheck, this); + [[maybe_unused]] bool ubOk = Unbind(wxEVT_IDLE, &MainDialog::onStartupUpdateCheck, this); assert(ubOk); + auto showNewVersionReminder = [this] + { + if (!globalCfg_.gui.lastOnlineVersion.empty() && haveNewerVersionOnline(globalCfg_.gui.lastOnlineVersion)) + { + auto menu = new wxMenu(); + wxMenuItem* newItem = new wxMenuItem(menu, wxID_ANY, _("&Show details")); + Bind(wxEVT_COMMAND_MENU_SELECTED, [this](wxCommandEvent&) { checkForUpdateNow(*this, globalCfg_.gui.lastOnlineVersion); }, newItem->GetId()); + //show changelog + handle Donation Edition auto-updater (including expiration) + menu->Append(newItem); //pass ownership + + const std::wstring& blackStar = utfTo<std::wstring>("★"); + m_menubar->Append(menu, blackStar + L' ' + replaceCpy(_("FreeFileSync %x is available!"), L"%x", utfTo<std::wstring>(globalCfg_.gui.lastOnlineVersion)) + L' ' + blackStar); + } + }; + if (shouldRunAutomaticUpdateCheck(globalCfg_.gui.lastUpdateCheck)) { flashStatusInformation(_("Searching for program updates...")); @@ -5630,12 +5660,15 @@ void MainDialog::onRegularUpdateCheck(wxIdleEvent& event) std::shared_ptr<const UpdateCheckResultPrep> resultPrep = automaticUpdateCheckPrepare(*this); //run on main thread: guiQueue_.processAsync([resultPrep] { return automaticUpdateCheckRunAsync(resultPrep.get()); }, //run on worker thread: (long-running part of the check) - [this] (std::shared_ptr<const UpdateCheckResult>&& resultAsync) + [this, showNewVersionReminder] (std::shared_ptr<const UpdateCheckResult>&& resultAsync) { automaticUpdateCheckEval(this, globalCfg_.gui.lastUpdateCheck, globalCfg_.gui.lastOnlineVersion, resultAsync.get()); //run on main thread: + showNewVersionReminder(); }); } + else + showNewVersionReminder(); } @@ -5645,7 +5678,6 @@ void MainDialog::onLayoutWindowAsync(wxIdleEvent& event) [[maybe_unused]] bool ubOk = Unbind(wxEVT_IDLE, &MainDialog::onLayoutWindowAsync, this); assert(ubOk); - //adjust folder pair distortion on startup for (FolderPairPanel* panel : additionalFolderPairs_) panel->Layout(); diff --git a/FreeFileSync/Source/ui/main_dlg.h b/FreeFileSync/Source/ui/main_dlg.h index ecd65c7a..9e9ead04 100644 --- a/FreeFileSync/Source/ui/main_dlg.h +++ b/FreeFileSync/Source/ui/main_dlg.h @@ -216,7 +216,7 @@ private: void deleteSelectedCfgHistoryItems(); void renameSelectedCfgHistoryItem(); - void onRegularUpdateCheck(wxIdleEvent& event); + void onStartupUpdateCheck(wxIdleEvent& event); void onLayoutWindowAsync (wxIdleEvent& event); void onResizeLeftFolderWidth(wxEvent& event); @@ -231,9 +231,9 @@ private: void startSyncForSelecction(const std::vector<FileSystemObject*>& selection); - void onCmpSettings (wxCommandEvent& event) override { showConfigDialog(SyncConfigPanel::COMPARISON, -1); } - void onConfigureFilter(wxCommandEvent& event) override { showConfigDialog(SyncConfigPanel::FILTER, -1); } - void onSyncSettings (wxCommandEvent& event) override { showConfigDialog(SyncConfigPanel::SYNC, -1); } + void onCmpSettings (wxCommandEvent& event) override { showConfigDialog(SyncConfigPanel::compare, -1); } + void onConfigureFilter(wxCommandEvent& event) override { showConfigDialog(SyncConfigPanel::filter, -1); } + void onSyncSettings (wxCommandEvent& event) override { showConfigDialog(SyncConfigPanel::sync, -1); } void showConfigDialog(SyncConfigPanel panelToShow, int localPairIndexToShow); @@ -252,9 +252,9 @@ private: void onRemoveFolderPair (wxCommandEvent& event); void onShowFolderPairOptions(wxEvent& event); - void onTopLocalCompCfg (wxCommandEvent& event) override { showConfigDialog(SyncConfigPanel::COMPARISON, 0); } - void onTopLocalSyncCfg (wxCommandEvent& event) override { showConfigDialog(SyncConfigPanel::SYNC, 0); } - void onTopLocalFilterCfg(wxCommandEvent& event) override { showConfigDialog(SyncConfigPanel::FILTER, 0); } + void onTopLocalCompCfg (wxCommandEvent& event) override { showConfigDialog(SyncConfigPanel::compare, 0); } + void onTopLocalSyncCfg (wxCommandEvent& event) override { showConfigDialog(SyncConfigPanel::sync, 0); } + void onTopLocalFilterCfg(wxCommandEvent& event) override { showConfigDialog(SyncConfigPanel::filter, 0); } void onLocalCompCfg (wxCommandEvent& event); void onLocalSyncCfg (wxCommandEvent& event); @@ -284,7 +284,6 @@ private: void onMenuFindItem (wxCommandEvent& event) override { showFindPanel(); } //CTRL + F void onMenuCheckVersion (wxCommandEvent& event) override; void onMenuCheckVersionAutomatically(wxCommandEvent& event) override; - void onMenuUpdateAvailable(wxCommandEvent& event); void onMenuAbout (wxCommandEvent& event) override; void onShowHelp (wxCommandEvent& event) override; void onMenuQuit (wxCommandEvent& event) override { Close(); } diff --git a/FreeFileSync/Source/ui/progress_indicator.cpp b/FreeFileSync/Source/ui/progress_indicator.cpp index 4882d010..54ab9c42 100644 --- a/FreeFileSync/Source/ui/progress_indicator.cpp +++ b/FreeFileSync/Source/ui/progress_indicator.cpp @@ -987,15 +987,24 @@ namespace template <class TopLevelDialog> -void SyncProgressDialogImpl<TopLevelDialog>::setExternalStatus(const wxString& status, const wxString& progress) //progress may be empty! +void SyncProgressDialogImpl<TopLevelDialog>::setExternalStatus(const wxString& statusTxt, const wxString& progress) //progress may be empty! { //sys tray: order "top-down": jobname, status, progress - wxString systrayTooltip = jobName_.empty() ? status : jobName_ + L'\n' + status; + wxString tooltip = L"FreeFileSync"; + if (!jobName_.empty()) + tooltip += L" | " + jobName_; + + tooltip += L"\n" + statusTxt; + if (!progress.empty()) - systrayTooltip += L' ' + progress; + tooltip += L' ' + progress; //window caption/taskbar; inverse order: progress, status, jobname - wxString title = progress.empty() ? status : progress + L" | " + status; + wxString title; + if (!progress.empty()) + title += progress + L" | "; + + title += statusTxt; if (!jobName_.empty()) title += L" | " + jobName_; @@ -1006,7 +1015,7 @@ void SyncProgressDialogImpl<TopLevelDialog>::setExternalStatus(const wxString& s //systray tooltip, if window is minimized if (trayIcon_) - trayIcon_->setToolTip(systrayTooltip); + trayIcon_->setToolTip(tooltip); //show text in dialog title (and at the same time in taskbar) if (parentFrame_) @@ -1264,8 +1273,6 @@ void SyncProgressDialogImpl<TopLevelDialog>::showSummary(SyncResult syncResult, //at the LATEST(!) to prevent access to currentStatusHandler //enable okay and close events; may be set in this method ONLY - //In wxWidgets 2.9.3 upwards, the wxWindow::Reparent() below fails on GTK and OS X if window is frozen! https://forums.codeblocks.org/index.php?topic=13388.45 - paused_ = false; //you never know? //update numbers one last time (as if sync were still running) diff --git a/FreeFileSync/Source/ui/small_dlgs.cpp b/FreeFileSync/Source/ui/small_dlgs.cpp index 654c6681..3010c344 100644 --- a/FreeFileSync/Source/ui/small_dlgs.cpp +++ b/FreeFileSync/Source/ui/small_dlgs.cpp @@ -59,8 +59,8 @@ public: AboutDlg(wxWindow* parent); private: - void onOkay (wxCommandEvent& event) override { EndModal(ReturnSmallDlg::BUTTON_OKAY); } - void onClose (wxCloseEvent& event) override { EndModal(ReturnSmallDlg::BUTTON_CANCEL); } + void onOkay (wxCommandEvent& event) override { EndModal(static_cast<int>(ConfirmationButton::accept)); } + void onClose (wxCloseEvent& event) override { EndModal(static_cast<int>(ConfirmationButton::cancel)); } void onDonate(wxCommandEvent& event) override { wxLaunchDefaultBrowser(L"https://freefilesync.org/donate.php"); } void onOpenHomepage(wxCommandEvent& event) override { wxLaunchDefaultBrowser(L"https://freefilesync.org/"); } void onOpenForum (wxCommandEvent& event) override { wxLaunchDefaultBrowser(L"https://freefilesync.org/forum/"); } @@ -182,12 +182,12 @@ namespace class CloudSetupDlg : public CloudSetupDlgGenerated { public: - CloudSetupDlg(wxWindow* parent, Zstring& folderPathPhrase, size_t& parallelOps, const std::wstring* parallelOpsDisabledReason); + CloudSetupDlg(wxWindow* parent, Zstring& folderPathPhrase, Zstring& sftpKeyFileLastSelected, size_t& parallelOps, const std::wstring* parallelOpsDisabledReason); private: void onOkay (wxCommandEvent& event) override; - void onCancel(wxCommandEvent& event) override { EndModal(ReturnSmallDlg::BUTTON_CANCEL); } - void onClose (wxCloseEvent& event) override { EndModal(ReturnSmallDlg::BUTTON_CANCEL); } + void onCancel(wxCommandEvent& event) override { EndModal(static_cast<int>(ConfirmationButton::cancel)); } + void onClose (wxCloseEvent& event) override { EndModal(static_cast<int>(ConfirmationButton::cancel)); } void onGdriveUserAdd (wxCommandEvent& event) override; void onGdriveUserRemove(wxCommandEvent& event) override; @@ -236,21 +236,24 @@ private: AsyncGuiQueue guiQueue_; + Zstring& sftpKeyFileLastSelected_; + //output-only parameters: Zstring& folderPathPhraseOut_; size_t& parallelOpsOut_; }; -CloudSetupDlg::CloudSetupDlg(wxWindow* parent, Zstring& folderPathPhrase, size_t& parallelOps, const std::wstring* parallelOpsDisabledReason) : +CloudSetupDlg::CloudSetupDlg(wxWindow* parent, Zstring& folderPathPhrase, Zstring& sftpKeyFileLastSelected, size_t& parallelOps, const std::wstring* parallelOpsDisabledReason) : CloudSetupDlgGenerated(parent), + sftpKeyFileLastSelected_(sftpKeyFileLastSelected), folderPathPhraseOut_(folderPathPhrase), parallelOpsOut_(parallelOps) { setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonOkay).setCancel(m_buttonCancel)); m_toggleBtnGdrive->SetBitmap(loadImage("google_drive")); - m_toggleBtnSftp ->SetBitmap(getTransparentPixel()); //set dummy image (can't be empty!): text-only buttons are rendered smaller on OS X! + m_toggleBtnSftp ->SetBitmap(getTransparentPixel()); //set dummy image (can't be empty!): text-only buttons are rendered smaller on macOS! m_toggleBtnFtp ->SetBitmap(getTransparentPixel()); // setRelativeFontSize(*m_toggleBtnGdrive, 1.25); @@ -561,22 +564,25 @@ void CloudSetupDlg::onKeyFileDropped(FileDropEvent& event) void CloudSetupDlg::onSelectKeyfile(wxCommandEvent& event) { assert (type_ == CloudType::sftp && sftpAuthType_ == SftpAuthType::keyFile); - wxFileDialog filePicker(this, - wxString(), //message - beforeLast(m_textCtrlKeyfilePath->GetValue(), utfTo<wxString>(FILE_NAME_SEPARATOR), IfNotFoundReturn::none), //default folder - wxString(), //default file name - _("All files") + L" (*.*)|*" + - L"|" + L"OpenSSL PEM (*.pem)|*.pem" + - L"|" + L"PuTTY Private Key (*.ppk)|*.ppk", - wxFD_OPEN); - if (filePicker.ShowModal() == wxID_OK) - m_textCtrlKeyfilePath->ChangeValue(filePicker.GetPath()); + + std::optional<Zstring> defaultFolderPath = getParentFolderPath(utfTo<Zstring>(m_textCtrlKeyfilePath->GetValue())); + if (!defaultFolderPath) + defaultFolderPath = getParentFolderPath(sftpKeyFileLastSelected_); + + wxFileDialog fileSelector(this, wxString() /*message*/, utfTo<wxString>(defaultFolderPath ? *defaultFolderPath : Zstr("")), wxString() /*default file name*/, + _("All files") + L" (*.*)|*" + + L"|" + L"OpenSSL PEM (*.pem)|*.pem" + + L"|" + L"PuTTY Private Key (*.ppk)|*.ppk", + wxFD_OPEN); + if (fileSelector.ShowModal() != wxID_OK) + return; + m_textCtrlKeyfilePath->ChangeValue(fileSelector.GetPath()); + sftpKeyFileLastSelected_ = utfTo<Zstring>(fileSelector.GetPath()); } void CloudSetupDlg::updateGui() { - m_toggleBtnGdrive->SetValue(type_ == CloudType::gdrive); m_toggleBtnSftp ->SetValue(type_ == CloudType::sftp); m_toggleBtnFtp ->SetValue(type_ == CloudType::ftp); @@ -722,7 +728,7 @@ void CloudSetupDlg::onBrowseCloudFolder(wxCommandEvent& event) return; } - if (showAbstractFolderPicker(this, folderPath) == ReturnAfsPicker::BUTTON_OKAY) + if (showAbstractFolderPicker(this, folderPath) == ConfirmationButton::accept) m_textCtrlServerPath->ChangeValue(utfTo<wxString>(FILE_NAME_SEPARATOR + folderPath.afsPath.value)); } @@ -743,14 +749,14 @@ void CloudSetupDlg::onOkay(wxCommandEvent& event) folderPathPhraseOut_ = AFS::getInitPathPhrase(getFolderPath()); parallelOpsOut_ = m_spinCtrlConnectionCount->GetValue(); - EndModal(ReturnSmallDlg::BUTTON_OKAY); + EndModal(static_cast<int>(ConfirmationButton::accept)); } } -ReturnSmallDlg::ButtonPressed fff::showCloudSetupDialog(wxWindow* parent, Zstring& folderPathPhrase, size_t& parallelOps, const std::wstring* parallelOpsDisabledReason) +ConfirmationButton fff::showCloudSetupDialog(wxWindow* parent, Zstring& folderPathPhrase, Zstring& sftpKeyFileLastSelected, size_t& parallelOps, const std::wstring* parallelOpsDisabledReason) { - CloudSetupDlg dlg(parent, folderPathPhrase, parallelOps, parallelOpsDisabledReason); - return static_cast<ReturnSmallDlg::ButtonPressed>(dlg.ShowModal()); + CloudSetupDlg dlg(parent, folderPathPhrase, sftpKeyFileLastSelected, parallelOps, parallelOpsDisabledReason); + return static_cast<ConfirmationButton>(dlg.ShowModal()); } //######################################################################################## @@ -763,22 +769,23 @@ public: CopyToDialog(wxWindow* parent, std::span<const FileSystemObject* const> rowsOnLeft, std::span<const FileSystemObject* const> rowsOnRight, - Zstring& lastUsedPath, + Zstring& targetFolderPath, Zstring& targetFolderLastSelected, std::vector<Zstring>& folderHistory, size_t folderHistoryMax, + Zstring& sftpKeyFileLastSelected, bool& keepRelPaths, bool& overwriteIfExists); private: void onOkay (wxCommandEvent& event) override; - void onCancel(wxCommandEvent& event) override { EndModal(ReturnSmallDlg::BUTTON_CANCEL); } - void onClose (wxCloseEvent& event) override { EndModal(ReturnSmallDlg::BUTTON_CANCEL); } + void onCancel(wxCommandEvent& event) override { EndModal(static_cast<int>(ConfirmationButton::cancel)); } + void onClose (wxCloseEvent& event) override { EndModal(static_cast<int>(ConfirmationButton::cancel)); } void onLocalKeyEvent(wxKeyEvent& event); std::unique_ptr<FolderSelector> targetFolder; //always bound //output-only parameters: - Zstring& lastUsedPathOut_; + Zstring& targetFolderPathOut_; bool& keepRelPathsOut_; bool& overwriteIfExistsOut_; std::vector<Zstring>& folderHistoryOut_; @@ -788,27 +795,26 @@ private: CopyToDialog::CopyToDialog(wxWindow* parent, std::span<const FileSystemObject* const> rowsOnLeft, std::span<const FileSystemObject* const> rowsOnRight, - Zstring& lastUsedPath, + Zstring& targetFolderPath, Zstring& targetFolderLastSelected, std::vector<Zstring>& folderHistory, size_t folderHistoryMax, + Zstring& sftpKeyFileLastSelected, bool& keepRelPaths, bool& overwriteIfExists) : CopyToDlgGenerated(parent), - lastUsedPathOut_(lastUsedPath), + targetFolderPathOut_(targetFolderPath), keepRelPathsOut_(keepRelPaths), overwriteIfExistsOut_(overwriteIfExists), folderHistoryOut_(folderHistory) { - setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonOK).setCancel(m_buttonCancel)); setMainInstructionFont(*m_staticTextHeader); m_bitmapCopyTo->SetBitmap(loadImage("copy_to")); - targetFolder = std::make_unique<FolderSelector>(this, *this, *m_buttonSelectTargetFolder, *m_bpButtonSelectAltTargetFolder, *m_targetFolderPath, nullptr /*staticText*/, nullptr /*wxWindow*/, - nullptr /*droppedPathsFilter*/, - [](const Zstring& folderPathPhrase) { return 1; } /*getDeviceParallelOps*/, - nullptr /*setDeviceParallelOps*/); + targetFolder = std::make_unique<FolderSelector>(this, *this, *m_buttonSelectTargetFolder, *m_bpButtonSelectAltTargetFolder, *m_targetFolderPath, + targetFolderLastSelected, sftpKeyFileLastSelected, nullptr /*staticText*/, nullptr /*wxWindow*/, nullptr /*droppedPathsFilter*/, + [](const Zstring& folderPathPhrase) { return 1; } /*getDeviceParallelOps*/, nullptr /*setDeviceParallelOps*/); m_targetFolderPath->setHistory(std::make_shared<HistoryList>(folderHistory, folderHistoryMax)); @@ -830,7 +836,7 @@ CopyToDialog::CopyToDialog(wxWindow* parent, m_textCtrlFileList->ChangeValue(itemList); //----------------- set config --------------------------------- - targetFolder ->setPath(lastUsedPath); + targetFolder ->setPath(targetFolderPath); m_checkBoxKeepRelPath ->SetValue(keepRelPaths); m_checkBoxOverwriteIfExists->SetValue(overwriteIfExists); //----------------- /set config -------------------------------- @@ -864,25 +870,26 @@ void CopyToDialog::onOkay(wxCommandEvent& event) m_targetFolderPath->getHistory()->addItem(targetFolder->getPath()); //------------------------------------------------------------- - lastUsedPathOut_ = targetFolder->getPath(); + targetFolderPathOut_ = targetFolder->getPath(); keepRelPathsOut_ = m_checkBoxKeepRelPath->GetValue(); overwriteIfExistsOut_ = m_checkBoxOverwriteIfExists->GetValue(); folderHistoryOut_ = m_targetFolderPath->getHistory()->getList(); - EndModal(ReturnSmallDlg::BUTTON_OKAY); + EndModal(static_cast<int>(ConfirmationButton::accept)); } } -ReturnSmallDlg::ButtonPressed fff::showCopyToDialog(wxWindow* parent, - std::span<const FileSystemObject* const> rowsOnLeft, - std::span<const FileSystemObject* const> rowsOnRight, - Zstring& lastUsedPath, - std::vector<Zstring>& folderHistory, size_t folderHistoryMax, - bool& keepRelPaths, - bool& overwriteIfExists) +ConfirmationButton fff::showCopyToDialog(wxWindow* parent, + std::span<const FileSystemObject* const> rowsOnLeft, + std::span<const FileSystemObject* const> rowsOnRight, + Zstring& targetFolderPath, Zstring& targetFolderLastSelected, + std::vector<Zstring>& folderHistory, size_t folderHistoryMax, + Zstring& sftpKeyFileLastSelected, + bool& keepRelPaths, + bool& overwriteIfExists) { - CopyToDialog dlg(parent, rowsOnLeft, rowsOnRight, lastUsedPath, folderHistory, folderHistoryMax, keepRelPaths, overwriteIfExists); - return static_cast<ReturnSmallDlg::ButtonPressed>(dlg.ShowModal()); + CopyToDialog dlg(parent, rowsOnLeft, rowsOnRight, targetFolderPath, targetFolderLastSelected, folderHistory, folderHistoryMax, sftpKeyFileLastSelected, keepRelPaths, overwriteIfExists); + return static_cast<ConfirmationButton>(dlg.ShowModal()); } //######################################################################################## @@ -900,8 +907,8 @@ public: private: void onUseRecycler(wxCommandEvent& event) override { updateGui(); } void onOkay (wxCommandEvent& event) override; - void onCancel (wxCommandEvent& event) override { EndModal(ReturnSmallDlg::BUTTON_CANCEL); } - void onClose (wxCloseEvent& event) override { EndModal(ReturnSmallDlg::BUTTON_CANCEL); } + void onCancel (wxCommandEvent& event) override { EndModal(static_cast<int>(ConfirmationButton::cancel)); } + void onClose (wxCloseEvent& event) override { EndModal(static_cast<int>(ConfirmationButton::cancel)); } void onLocalKeyEvent(wxKeyEvent& event); @@ -947,7 +954,6 @@ DeleteDialog::DeleteDialog(wxWindow* parent, void DeleteDialog::updateGui() { - const auto& [itemList, itemCount] = getSelectedItemsAsString(rowsToDeleteOnLeft_, rowsToDeleteOnRight_); wxString header; if (m_checkBoxUseRecycler->GetValue()) @@ -995,17 +1001,17 @@ void DeleteDialog::onOkay(wxCommandEvent& event) useRecycleBinOut_ = m_checkBoxUseRecycler->GetValue(); - EndModal(ReturnSmallDlg::BUTTON_OKAY); + EndModal(static_cast<int>(ConfirmationButton::accept)); } } -ReturnSmallDlg::ButtonPressed fff::showDeleteDialog(wxWindow* parent, - std::span<const FileSystemObject* const> rowsOnLeft, - std::span<const FileSystemObject* const> rowsOnRight, - bool& useRecycleBin) +ConfirmationButton fff::showDeleteDialog(wxWindow* parent, + std::span<const FileSystemObject* const> rowsOnLeft, + std::span<const FileSystemObject* const> rowsOnRight, + bool& useRecycleBin) { DeleteDialog dlg(parent, rowsOnLeft, rowsOnRight, useRecycleBin); - return static_cast<ReturnSmallDlg::ButtonPressed>(dlg.ShowModal()); + return static_cast<ConfirmationButton>(dlg.ShowModal()); } //######################################################################################## @@ -1022,8 +1028,8 @@ public: bool& dontShowAgain); private: void onStartSync(wxCommandEvent& event) override; - void onCancel (wxCommandEvent& event) override { EndModal(ReturnSmallDlg::BUTTON_CANCEL); } - void onClose (wxCloseEvent& event) override { EndModal(ReturnSmallDlg::BUTTON_CANCEL); } + void onCancel (wxCommandEvent& event) override { EndModal(static_cast<int>(ConfirmationButton::cancel)); } + void onClose (wxCloseEvent& event) override { EndModal(static_cast<int>(ConfirmationButton::cancel)); } void onLocalKeyEvent(wxKeyEvent& event); @@ -1108,22 +1114,22 @@ void SyncConfirmationDlg::onLocalKeyEvent(wxKeyEvent& event) void SyncConfirmationDlg::onStartSync(wxCommandEvent& event) { dontShowAgainOut_ = m_checkBoxDontShowAgain->GetValue(); - EndModal(ReturnSmallDlg::BUTTON_OKAY); + EndModal(static_cast<int>(ConfirmationButton::accept)); } } -ReturnSmallDlg::ButtonPressed fff::showSyncConfirmationDlg(wxWindow* parent, - bool syncSelection, - std::optional<SyncVariant> syncVar, - const SyncStatistics& statistics, - bool& dontShowAgain) +ConfirmationButton fff::showSyncConfirmationDlg(wxWindow* parent, + bool syncSelection, + std::optional<SyncVariant> syncVar, + const SyncStatistics& statistics, + bool& dontShowAgain) { SyncConfirmationDlg dlg(parent, syncSelection, syncVar, statistics, dontShowAgain); - return static_cast<ReturnSmallDlg::ButtonPressed>(dlg.ShowModal()); + return static_cast<ConfirmationButton>(dlg.ShowModal()); } //######################################################################################## @@ -1139,8 +1145,8 @@ private: void onOkay (wxCommandEvent& event) override; void onRestoreDialogs(wxCommandEvent& event) override; void onDefault (wxCommandEvent& event) override; - void onCancel (wxCommandEvent& event) override { EndModal(ReturnSmallDlg::BUTTON_CANCEL); } - void onClose (wxCloseEvent& event) override { EndModal(ReturnSmallDlg::BUTTON_CANCEL); } + void onCancel (wxCommandEvent& event) override { EndModal(static_cast<int>(ConfirmationButton::cancel)); } + void onClose (wxCloseEvent& event) override { EndModal(static_cast<int>(ConfirmationButton::cancel)); } void onAddRow (wxCommandEvent& event) override; void onRemoveRow (wxCommandEvent& event) override; void onHelpExternalApps (wxHyperlinkEvent& event) override { displayHelpEntry(L"external-applications", this); } @@ -1193,7 +1199,7 @@ OptionsDlg::OptionsDlg(wxWindow* parent, XmlGlobalSettings& globalSettings) : m_gridCustomCommand->SetTabBehaviour(wxGrid::Tab_Leave); m_spinCtrlLogFilesMaxAge->SetMinSize({fastFromDIP(70), -1}); //Hack: set size (why does wxWindow::Size() not work?) - m_hyperlinkLogFolder->SetLabel(utfTo<wxString>(getDefaultLogFolderPath())); + m_hyperlinkLogFolder->SetLabel(utfTo<wxString>(getLogFolderDefaultPath())); setRelativeFontSize(*m_hyperlinkLogFolder, 1.2); m_bitmapSettings ->SetBitmap (loadImage("settings")); @@ -1310,20 +1316,17 @@ void OptionsDlg::onRestoreDialogs(wxCommandEvent& event) void OptionsDlg::selectSound(wxTextCtrl& txtCtrl) { - wxString defaultFolderPath = beforeLast(txtCtrl.GetValue(), utfTo<wxString>(FILE_NAME_SEPARATOR), IfNotFoundReturn::none); - if (defaultFolderPath.empty()) - defaultFolderPath = utfTo<wxString>(beforeLast(getResourceDirPf(), FILE_NAME_SEPARATOR, IfNotFoundReturn::all)); - - wxFileDialog filePicker(this, - wxString(), //message - defaultFolderPath, - wxString(), //default file name - wxString(L"WAVE (*.wav)|*.wav") + L"|" + _("All files") + L" (*.*)|*", - wxFD_OPEN); - if (filePicker.ShowModal() != wxID_OK) + std::optional<Zstring> defaultFolderPath = getParentFolderPath(utfTo<Zstring>(txtCtrl.GetValue())); + if (!defaultFolderPath) + defaultFolderPath = beforeLast(getResourceDirPf(), FILE_NAME_SEPARATOR, IfNotFoundReturn::all); + + wxFileDialog fileSelector(this, wxString() /*message*/, utfTo<wxString>(*defaultFolderPath), wxString() /*default file name*/, + wxString(L"WAVE (*.wav)|*.wav") + L"|" + _("All files") + L" (*.*)|*", + wxFD_OPEN); + if (fileSelector.ShowModal() != wxID_OK) return; - txtCtrl.ChangeValue(filePicker.GetPath()); + txtCtrl.ChangeValue(fileSelector.GetPath()); updateGui(); } @@ -1390,7 +1393,7 @@ void OptionsDlg::onOkay(wxCommandEvent& event) globalCfgOut_.warnDlgs = warnDlgs_; globalCfgOut_.autoCloseProgressDialog = autoCloseProgressDialog_; - EndModal(ReturnSmallDlg::BUTTON_OKAY); + EndModal(static_cast<int>(ConfirmationButton::accept)); } @@ -1475,16 +1478,16 @@ void OptionsDlg::onShowLogFolder(wxHyperlinkEvent& event) { try { - openWithDefaultApp(getDefaultLogFolderPath()); //throw FileError + openWithDefaultApp(getLogFolderDefaultPath()); //throw FileError } catch (const FileError& e) { showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); } } } -ReturnSmallDlg::ButtonPressed fff::showOptionsDlg(wxWindow* parent, XmlGlobalSettings& globalCfg) +ConfirmationButton fff::showOptionsDlg(wxWindow* parent, XmlGlobalSettings& globalCfg) { OptionsDlg dlg(parent, globalCfg); - return static_cast<ReturnSmallDlg::ButtonPressed>(dlg.ShowModal()); + return static_cast<ConfirmationButton>(dlg.ShowModal()); } //######################################################################################## @@ -1498,8 +1501,8 @@ public: private: void onOkay (wxCommandEvent& event) override; - void onCancel(wxCommandEvent& event) override { EndModal(ReturnSmallDlg::BUTTON_CANCEL); } - void onClose (wxCloseEvent& event) override { EndModal(ReturnSmallDlg::BUTTON_CANCEL); } + void onCancel(wxCommandEvent& event) override { EndModal(static_cast<int>(ConfirmationButton::cancel)); } + void onClose (wxCloseEvent& event) override { EndModal(static_cast<int>(ConfirmationButton::cancel)); } void onChangeSelectionFrom(wxCalendarEvent& event) override { @@ -1527,9 +1530,14 @@ SelectTimespanDlg::SelectTimespanDlg(wxWindow* parent, time_t& timeFrom, time_t& { setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonOkay).setCancel(m_buttonCancel)); - long style = wxCAL_SHOW_HOLIDAYS | wxCAL_SHOW_SURROUNDING_WEEKS; + assert(m_calendarFrom->GetWindowStyleFlag() == m_calendarTo->GetWindowStyleFlag()); + assert(m_calendarFrom->HasFlag(wxCAL_SHOW_HOLIDAYS)); //caveat: for some stupid reason this is not honored when set by SetWindowStyleFlag() + assert(m_calendarFrom->HasFlag(wxCAL_SHOW_SURROUNDING_WEEKS)); + assert(!m_calendarFrom->HasFlag(wxCAL_MONDAY_FIRST) && + !m_calendarFrom->HasFlag(wxCAL_SUNDAY_FIRST)); //...because we set it in the following: + long style = m_calendarFrom->GetWindowStyleFlag(); - style |= wxCAL_MONDAY_FIRST; + style |= getFirstDayOfWeek() == WeekDay::sunday ? wxCAL_SUNDAY_FIRST : wxCAL_MONDAY_FIRST; //seems to be ignored on CentOS m_calendarFrom->SetWindowStyleFlag(style); m_calendarTo ->SetWindowStyleFlag(style); @@ -1577,14 +1585,14 @@ void SelectTimespanDlg::onOkay(wxCommandEvent& event) timeFromOut_ = from.GetTicks(); timeToOut_ = to .GetTicks(); - EndModal(ReturnSmallDlg::BUTTON_OKAY); + EndModal(static_cast<int>(ConfirmationButton::accept)); } } -ReturnSmallDlg::ButtonPressed fff::showSelectTimespanDlg(wxWindow* parent, time_t& timeFrom, time_t& timeTo) +ConfirmationButton fff::showSelectTimespanDlg(wxWindow* parent, time_t& timeFrom, time_t& timeTo) { SelectTimespanDlg dlg(parent, timeFrom, timeTo); - return static_cast<ReturnSmallDlg::ButtonPressed>(dlg.ShowModal()); + return static_cast<ConfirmationButton>(dlg.ShowModal()); } //######################################################################################## @@ -1598,8 +1606,8 @@ public: private: void onOkay (wxCommandEvent& event) override; - void onCancel(wxCommandEvent& event) override { EndModal(ReturnSmallDlg::BUTTON_CANCEL); } - void onClose (wxCloseEvent& event) override { EndModal(ReturnSmallDlg::BUTTON_CANCEL); } + void onCancel(wxCommandEvent& event) override { EndModal(static_cast<int>(ConfirmationButton::cancel)); } + void onClose (wxCloseEvent& event) override { EndModal(static_cast<int>(ConfirmationButton::cancel)); } //work around defunct keyboard focus on macOS (or is it wxMac?) => not needed for this dialog! //void onLocalKeyEvent(wxKeyEvent& event); @@ -1633,14 +1641,14 @@ CfgHighlightDlg::CfgHighlightDlg(wxWindow* parent, int& cfgHistSyncOverdueDays) void CfgHighlightDlg::onOkay(wxCommandEvent& event) { cfgHistSyncOverdueDaysOut_ = m_spinCtrlOverdueDays->GetValue(); - EndModal(ReturnSmallDlg::BUTTON_OKAY); + EndModal(static_cast<int>(ConfirmationButton::accept)); } } -ReturnSmallDlg::ButtonPressed fff::showCfgHighlightDlg(wxWindow* parent, int& cfgHistSyncOverdueDays) +ConfirmationButton fff::showCfgHighlightDlg(wxWindow* parent, int& cfgHistSyncOverdueDays) { CfgHighlightDlg dlg(parent, cfgHistSyncOverdueDays); - return static_cast<ReturnSmallDlg::ButtonPressed>(dlg.ShowModal()); + return static_cast<ConfirmationButton>(dlg.ShowModal()); } //######################################################################################## @@ -1657,8 +1665,8 @@ private: void onActivateOffline(wxCommandEvent& event) override; void onOfflineActivationEnter(wxCommandEvent& event) override { onActivateOffline(event); } void onCopyUrl (wxCommandEvent& event) override; - void onCancel(wxCommandEvent& event) override { EndModal(static_cast<int>(ReturnActivationDlg::CANCEL)); } - void onClose (wxCloseEvent& event) override { EndModal(static_cast<int>(ReturnActivationDlg::CANCEL)); } + void onCancel(wxCommandEvent& event) override { EndModal(static_cast<int>(ActivationDlgButton::cancel)); } + void onClose (wxCloseEvent& event) override { EndModal(static_cast<int>(ActivationDlgButton::cancel)); } std::wstring& manualActivationKeyOut_; //in/out parameter }; @@ -1708,21 +1716,21 @@ void ActivationDlg::onCopyUrl(wxCommandEvent& event) void ActivationDlg::onActivateOnline(wxCommandEvent& event) { manualActivationKeyOut_ = m_textCtrlOfflineActivationKey->GetValue(); - EndModal(static_cast<int>(ReturnActivationDlg::ACTIVATE_ONLINE)); + EndModal(static_cast<int>(ActivationDlgButton::activateOnline)); } void ActivationDlg::onActivateOffline(wxCommandEvent& event) { manualActivationKeyOut_ = m_textCtrlOfflineActivationKey->GetValue(); - EndModal(static_cast<int>(ReturnActivationDlg::ACTIVATE_OFFLINE)); + EndModal(static_cast<int>(ActivationDlgButton::activateOffline)); } } -ReturnActivationDlg fff::showActivationDialog(wxWindow* parent, const std::wstring& lastErrorMsg, const std::wstring& manualActivationUrl, std::wstring& manualActivationKey) +ActivationDlgButton fff::showActivationDialog(wxWindow* parent, const std::wstring& lastErrorMsg, const std::wstring& manualActivationUrl, std::wstring& manualActivationKey) { ActivationDlg dlg(parent, lastErrorMsg, manualActivationUrl, manualActivationKey); - return static_cast<ReturnActivationDlg>(dlg.ShowModal()); + return static_cast<ActivationDlgButton>(dlg.ShowModal()); } //######################################################################################## diff --git a/FreeFileSync/Source/ui/small_dlgs.h b/FreeFileSync/Source/ui/small_dlgs.h index 9f5aa689..7b051085 100644 --- a/FreeFileSync/Source/ui/small_dlgs.h +++ b/FreeFileSync/Source/ui/small_dlgs.h @@ -9,6 +9,7 @@ #include <span> #include <wx/window.h> +#include <wx+/popup_dlg.h> #include "../base/synchronization.h" #include "../config.h" @@ -17,52 +18,44 @@ namespace fff { //parent window, optional: support correct dialog placement above parent on multiple monitor systems -struct ReturnSmallDlg -{ - enum ButtonPressed - { - BUTTON_CANCEL, - BUTTON_OKAY = 1 - }; -}; - void showAboutDialog(wxWindow* parent); -ReturnSmallDlg::ButtonPressed showCopyToDialog(wxWindow* parent, - std::span<const FileSystemObject* const> rowsOnLeft, - std::span<const FileSystemObject* const> rowsOnRight, - Zstring& lastUsedPath, - std::vector<Zstring>& folderPathHistory, size_t folderPathHistoryMax, - bool& keepRelPaths, - bool& overwriteIfExists); +zen::ConfirmationButton showCopyToDialog(wxWindow* parent, + std::span<const FileSystemObject* const> rowsOnLeft, + std::span<const FileSystemObject* const> rowsOnRight, + Zstring& targetFolderPath, Zstring& targetFolderLastSelected, + std::vector<Zstring>& folderPathHistory, size_t folderPathHistoryMax, + Zstring& sftpKeyFileLastSelected, + bool& keepRelPaths, + bool& overwriteIfExists); -ReturnSmallDlg::ButtonPressed showDeleteDialog(wxWindow* parent, - std::span<const FileSystemObject* const> rowsOnLeft, - std::span<const FileSystemObject* const> rowsOnRight, - bool& useRecycleBin); +zen::ConfirmationButton showDeleteDialog(wxWindow* parent, + std::span<const FileSystemObject* const> rowsOnLeft, + std::span<const FileSystemObject* const> rowsOnRight, + bool& useRecycleBin); -ReturnSmallDlg::ButtonPressed showSyncConfirmationDlg(wxWindow* parent, - bool syncSelection, - std::optional<SyncVariant> syncVar, - const SyncStatistics& statistics, - bool& dontShowAgain); +zen::ConfirmationButton showSyncConfirmationDlg(wxWindow* parent, + bool syncSelection, + std::optional<SyncVariant> syncVar, + const SyncStatistics& statistics, + bool& dontShowAgain); -ReturnSmallDlg::ButtonPressed showOptionsDlg(wxWindow* parent, XmlGlobalSettings& globalCfg); +zen::ConfirmationButton showOptionsDlg(wxWindow* parent, XmlGlobalSettings& globalCfg); -ReturnSmallDlg::ButtonPressed showSelectTimespanDlg(wxWindow* parent, time_t& timeFrom, time_t& timeTo); +zen::ConfirmationButton showSelectTimespanDlg(wxWindow* parent, time_t& timeFrom, time_t& timeTo); -ReturnSmallDlg::ButtonPressed showCfgHighlightDlg(wxWindow* parent, int& cfgHistSyncOverdueDays); +zen::ConfirmationButton showCfgHighlightDlg(wxWindow* parent, int& cfgHistSyncOverdueDays); -ReturnSmallDlg::ButtonPressed showCloudSetupDialog(wxWindow* parent, Zstring& folderPathPhrase, - size_t& parallelOps, const std::wstring* parallelOpsDisabledReason /*optional: disable control + show text*/); +zen::ConfirmationButton showCloudSetupDialog(wxWindow* parent, Zstring& folderPathPhrase, Zstring& sftpKeyFileLastSelected, + size_t& parallelOps, const std::wstring* parallelOpsDisabledReason /*optional: disable control + show text*/); -enum class ReturnActivationDlg +enum class ActivationDlgButton { - CANCEL, - ACTIVATE_ONLINE, - ACTIVATE_OFFLINE, + cancel, + activateOnline, + activateOffline, }; -ReturnActivationDlg showActivationDialog(wxWindow* parent, const std::wstring& lastErrorMsg, const std::wstring& manualActivationUrl, std::wstring& manualActivationKey); +ActivationDlgButton showActivationDialog(wxWindow* parent, const std::wstring& lastErrorMsg, const std::wstring& manualActivationUrl, std::wstring& manualActivationKey); class DownloadProgressWindow //temporary progress info => life-time: stack diff --git a/FreeFileSync/Source/ui/sync_cfg.cpp b/FreeFileSync/Source/ui/sync_cfg.cpp index 72251758..dfad6a6f 100644 --- a/FreeFileSync/Source/ui/sync_cfg.cpp +++ b/FreeFileSync/Source/ui/sync_cfg.cpp @@ -92,16 +92,16 @@ public: int localPairIndexToShow, bool showMultipleCfgs, GlobalPairConfig& globalPairCfg, std::vector<LocalPairConfig>& localPairConfig, - std::vector<Zstring>& versioningFolderHistory, - std::vector<Zstring>& logFolderHistory, - size_t folderHistoryMax, + std::vector<Zstring>& versioningFolderHistory, Zstring& versioningFolderLastSelected, + std::vector<Zstring>& logFolderHistory, Zstring& logFolderLastSelected, + size_t folderHistoryMax, Zstring& sftpKeyFileLastSelected, std::vector<Zstring>& emailHistory, size_t emailHistoryMax, std::vector<Zstring>& commandHistory, size_t commandHistoryMax); private: void onOkay (wxCommandEvent& event) override; - void onCancel(wxCommandEvent& event) override { EndModal(ReturnSyncConfig::BUTTON_CANCEL); } - void onClose (wxCloseEvent& event) override { EndModal(ReturnSyncConfig::BUTTON_CANCEL); } + void onCancel(wxCommandEvent& event) override { EndModal(static_cast<int>(ConfirmationButton::cancel)); } + void onClose (wxCloseEvent& event) override { EndModal(static_cast<int>(ConfirmationButton::cancel)); } void onLocalKeyEvent(wxKeyEvent& event); void onListBoxKeyEvent(wxKeyEvent& event) override; @@ -294,9 +294,9 @@ ConfigDialog::ConfigDialog(wxWindow* parent, int localPairIndexToShow, bool showMultipleCfgs, GlobalPairConfig& globalPairCfg, std::vector<LocalPairConfig>& localPairConfig, - std::vector<Zstring>& versioningFolderHistory, - std::vector<Zstring>& logFolderHistory, - size_t folderHistoryMax, + std::vector<Zstring>& versioningFolderHistory, Zstring& versioningFolderLastSelected, + std::vector<Zstring>& logFolderHistory, Zstring& logFolderLastSelected, + size_t folderHistoryMax, Zstring& sftpKeyFileLastSelected, std::vector<Zstring>& emailHistory, size_t emailHistoryMax, std::vector<Zstring>& commandHistory, size_t commandHistoryMax) : ConfigDlgGenerated(parent), @@ -311,7 +311,7 @@ ConfigDialog::ConfigDialog(wxWindow* parent, setDeviceParallelOps_([this](const Zstring& folderPathPhrase, size_t parallelOps) //setDeviceParallelOps() { - assert(selectedPairIndexToShow_ == -1 || makeUnsigned(selectedPairIndexToShow_) < localPairCfg_.size()); + assert(selectedPairIndexToShow_ == -1 || makeUnsigned(selectedPairIndexToShow_) < localPairCfg_.size()); if (selectedPairIndexToShow_ < 0) { MiscSyncConfig miscCfg = getMiscSyncOptions(); @@ -322,10 +322,10 @@ setDeviceParallelOps_([this](const Zstring& folderPathPhrase, size_t parallelOps setDeviceParallelOps(globalPairCfg_.miscCfg.deviceParallelOps, folderPathPhrase, parallelOps); }), -versioningFolder_(this, *m_panelVersioning, *m_buttonSelectVersioningFolder, *m_bpButtonSelectVersioningAltFolder, *m_versioningFolderPath, +versioningFolder_(this, *m_panelVersioning, *m_buttonSelectVersioningFolder, *m_bpButtonSelectVersioningAltFolder, *m_versioningFolderPath, versioningFolderLastSelected, sftpKeyFileLastSelected, nullptr /*staticText*/, nullptr /*dropWindow2*/, nullptr /*droppedPathsFilter*/, getDeviceParallelOps_, setDeviceParallelOps_), -logfileDir_(this, *m_panelLogfile, *m_buttonSelectLogFolder, *m_bpButtonSelectAltLogFolder, *m_logFolderPath, +logfileDir_(this, *m_panelLogfile, *m_buttonSelectLogFolder, *m_bpButtonSelectAltLogFolder, *m_logFolderPath, logFolderLastSelected, sftpKeyFileLastSelected, nullptr /*staticText*/, nullptr /*dropWindow2*/, nullptr /*droppedPathsFilter*/, getDeviceParallelOps_, setDeviceParallelOps_), globalPairCfgOut_(globalPairCfg), @@ -362,9 +362,9 @@ showMultipleCfgs_(showMultipleCfgs) m_notebook->AssignImageList(imgList.release()); //pass ownership - m_notebook->SetPageText(static_cast<size_t>(SyncConfigPanel::COMPARISON), _("Comparison") + L" (F6)"); - m_notebook->SetPageText(static_cast<size_t>(SyncConfigPanel::FILTER ), _("Filter") + L" (F7)"); - m_notebook->SetPageText(static_cast<size_t>(SyncConfigPanel::SYNC ), _("Synchronization") + L" (F8)"); + m_notebook->SetPageText(static_cast<size_t>(SyncConfigPanel::compare), _("Comparison") + L" (F6)"); + m_notebook->SetPageText(static_cast<size_t>(SyncConfigPanel::filter ), _("Filter") + L" (F7)"); + m_notebook->SetPageText(static_cast<size_t>(SyncConfigPanel::sync ), _("Synchronization") + L" (F8)"); m_notebook->ChangeSelection(static_cast<size_t>(panelToShow)); @@ -558,13 +558,13 @@ void ConfigDialog::onLocalKeyEvent(wxKeyEvent& event) //process key events witho switch (event.GetKeyCode()) { case WXK_F6: - changeSelection(SyncConfigPanel::COMPARISON); + changeSelection(SyncConfigPanel::compare); return; //handled! case WXK_F7: - changeSelection(SyncConfigPanel::FILTER); + changeSelection(SyncConfigPanel::filter); return; case WXK_F8: - changeSelection(SyncConfigPanel::SYNC); + changeSelection(SyncConfigPanel::sync); return; } event.Skip(); @@ -588,13 +588,13 @@ void ConfigDialog::onListBoxKeyEvent(wxKeyEvent& event) case WXK_NUMPAD_LEFT: switch (static_cast<SyncConfigPanel>(m_notebook->GetSelection())) { - case SyncConfigPanel::COMPARISON: + case SyncConfigPanel::compare: break; - case SyncConfigPanel::FILTER: - m_notebook->ChangeSelection(static_cast<size_t>(SyncConfigPanel::COMPARISON)); + case SyncConfigPanel::filter: + m_notebook->ChangeSelection(static_cast<size_t>(SyncConfigPanel::compare)); break; - case SyncConfigPanel::SYNC: - m_notebook->ChangeSelection(static_cast<size_t>(SyncConfigPanel::FILTER)); + case SyncConfigPanel::sync: + m_notebook->ChangeSelection(static_cast<size_t>(SyncConfigPanel::filter)); break; } m_listBoxFolderPair->SetFocus(); //needed! wxNotebook::ChangeSelection() leads to focus change! @@ -604,13 +604,13 @@ void ConfigDialog::onListBoxKeyEvent(wxKeyEvent& event) case WXK_NUMPAD_RIGHT: switch (static_cast<SyncConfigPanel>(m_notebook->GetSelection())) { - case SyncConfigPanel::COMPARISON: - m_notebook->ChangeSelection(static_cast<size_t>(SyncConfigPanel::FILTER)); + case SyncConfigPanel::compare: + m_notebook->ChangeSelection(static_cast<size_t>(SyncConfigPanel::filter)); break; - case SyncConfigPanel::FILTER: - m_notebook->ChangeSelection(static_cast<size_t>(SyncConfigPanel::SYNC)); + case SyncConfigPanel::filter: + m_notebook->ChangeSelection(static_cast<size_t>(SyncConfigPanel::sync)); break; - case SyncConfigPanel::SYNC: + case SyncConfigPanel::sync: break; } m_listBoxFolderPair->SetFocus(); @@ -714,7 +714,7 @@ void ConfigDialog::updateCompGui() m_panelComparisonSettings->Enable(compOptionsEnabled); - m_notebook->SetPageImage(static_cast<size_t>(SyncConfigPanel::COMPARISON), + m_notebook->SetPageImage(static_cast<size_t>(SyncConfigPanel::compare), static_cast<int>(compOptionsEnabled ? ConfigTypeImage::compare : ConfigTypeImage::compareGrey)); //update toggle buttons -> they have no parameter-ownership at all! @@ -799,7 +799,7 @@ void ConfigDialog::updateFilterGui() { const FilterConfig activeCfg = getFilterConfig(); - m_notebook->SetPageImage(static_cast<size_t>(SyncConfigPanel::FILTER), + m_notebook->SetPageImage(static_cast<size_t>(SyncConfigPanel::filter), static_cast<int>(!isNullFilter(activeCfg) ? ConfigTypeImage::filter: ConfigTypeImage::filterGrey)); m_bitmapInclude ->SetBitmap(greyScaleIfDisabled(loadImage("filter_include"), !NameFilter::isNull(activeCfg.includeFilter, FilterConfig().excludeFilter))); @@ -1052,12 +1052,11 @@ void ConfigDialog::setSyncConfig(const SyncConfig* syncCfg) void ConfigDialog::updateSyncGui() { - const bool syncOptionsEnabled = m_checkBoxUseLocalSyncOptions->GetValue(); m_panelSyncSettings->Enable(syncOptionsEnabled); - m_notebook->SetPageImage(static_cast<size_t>(SyncConfigPanel::SYNC), + m_notebook->SetPageImage(static_cast<size_t>(SyncConfigPanel::sync), static_cast<int>(syncOptionsEnabled ? ConfigTypeImage::sync: ConfigTypeImage::syncGrey)); updateSyncDirectionIcons(directionCfg_, @@ -1277,7 +1276,7 @@ void ConfigDialog::setMiscSyncOptions(const MiscSyncConfig& miscCfg) setEnumVal(enumPostSyncCondition_, *m_choicePostSyncCondition, miscCfg.postSyncCondition); //---------------------------------------------------------------------------- m_checkBoxOverrideLogPath->SetValue(!trimCpy(miscCfg.altLogFolderPathPhrase).empty()); - logfileDir_.setPath(m_checkBoxOverrideLogPath->GetValue() ? miscCfg.altLogFolderPathPhrase : getDefaultLogFolderPath()); + logfileDir_.setPath(m_checkBoxOverrideLogPath->GetValue() ? miscCfg.altLogFolderPathPhrase : getLogFolderDefaultPath()); //can't use logfileDir_.setBackgroundText(): no text shown when control is disabled! //---------------------------------------------------------------------------- Zstring defaultEmail; @@ -1457,7 +1456,7 @@ bool ConfigDialog::unselectFolderPairConfig(bool validateParams) { if (AFS::isNullPath(createAbstractPath(syncCfg->versioningFolderPhrase))) { - m_notebook->ChangeSelection(static_cast<size_t>(SyncConfigPanel::SYNC)); + m_notebook->ChangeSelection(static_cast<size_t>(SyncConfigPanel::sync)); showNotificationDialog(this, DialogInfoType::info, PopupDialogCfg().setMainInstructions(_("Please enter a target folder for versioning."))); //don't show error icon to follow "Windows' encouraging tone" m_versioningFolderPath->SetFocus(); @@ -1471,7 +1470,7 @@ bool ConfigDialog::unselectFolderPairConfig(bool validateParams) syncCfg->versionCountMax > 0 && syncCfg->versionCountMin >= syncCfg->versionCountMax) { - m_notebook->ChangeSelection(static_cast<size_t>(SyncConfigPanel::SYNC)); + m_notebook->ChangeSelection(static_cast<size_t>(SyncConfigPanel::sync)); showNotificationDialog(this, DialogInfoType::info, PopupDialogCfg().setMainInstructions(_("Minimum version count must be smaller than maximum count."))); m_spinCtrlVersionCountMin->SetFocus(); return false; @@ -1483,7 +1482,7 @@ bool ConfigDialog::unselectFolderPairConfig(bool validateParams) if (!miscCfg->altLogFolderPathPhrase.empty() && trimCpy(miscCfg->altLogFolderPathPhrase).empty()) { - m_notebook->ChangeSelection(static_cast<size_t>(SyncConfigPanel::SYNC)); + m_notebook->ChangeSelection(static_cast<size_t>(SyncConfigPanel::sync)); showNotificationDialog(this, DialogInfoType::info, PopupDialogCfg().setMainInstructions(_("Please enter a folder path."))); m_logFolderPath->SetFocus(); return false; @@ -1493,7 +1492,7 @@ bool ConfigDialog::unselectFolderPairConfig(bool validateParams) if (!miscCfg->emailNotifyAddress.empty() && !isValidEmail(trimCpy(miscCfg->emailNotifyAddress))) { - m_notebook->ChangeSelection(static_cast<size_t>(SyncConfigPanel::SYNC)); + m_notebook->ChangeSelection(static_cast<size_t>(SyncConfigPanel::sync)); showNotificationDialog(this, DialogInfoType::info, PopupDialogCfg().setMainInstructions(_("Please enter a valid email address."))); m_comboBoxEmail->SetFocus(); return false; @@ -1538,24 +1537,24 @@ void ConfigDialog::onOkay(wxCommandEvent& event) commandHistoryOut_ = m_comboBoxPostSyncCommand->getHistory(); emailHistoryOut_ = m_comboBoxEmail ->getHistory(); - EndModal(ReturnSyncConfig::BUTTON_OKAY); + EndModal(static_cast<int>(ConfirmationButton::accept)); } } //######################################################################################## -ReturnSyncConfig::ButtonPressed fff::showSyncConfigDlg(wxWindow* parent, - SyncConfigPanel panelToShow, - int localPairIndexToShow, bool showMultipleCfgs, +ConfirmationButton fff::showSyncConfigDlg(wxWindow* parent, + SyncConfigPanel panelToShow, + int localPairIndexToShow, bool showMultipleCfgs, - GlobalPairConfig& globalPairCfg, - std::vector<LocalPairConfig>& localPairConfig, + GlobalPairConfig& globalPairCfg, + std::vector<LocalPairConfig>& localPairConfig, - std::vector<Zstring>& versioningFolderHistory, - std::vector<Zstring>& logFolderHistory, - size_t folderHistoryMax, - std::vector<Zstring>& emailHistory, size_t emailHistoryMax, - std::vector<Zstring>& commandHistory, size_t commandHistoryMax) + std::vector<Zstring>& versioningFolderHistory, Zstring& versioningFolderLastSelected, + std::vector<Zstring>& logFolderHistory, Zstring& logFolderLastSelected, + size_t folderHistoryMax, Zstring& sftpKeyFileLastSelected, + std::vector<Zstring>& emailHistory, size_t emailHistoryMax, + std::vector<Zstring>& commandHistory, size_t commandHistoryMax) { ConfigDialog syncDlg(parent, @@ -1563,12 +1562,12 @@ ReturnSyncConfig::ButtonPressed fff::showSyncConfigDlg(wxWindow* parent, localPairIndexToShow, showMultipleCfgs, globalPairCfg, localPairConfig, - versioningFolderHistory, - logFolderHistory, - folderHistoryMax, + versioningFolderHistory, versioningFolderLastSelected, + logFolderHistory, logFolderLastSelected, + folderHistoryMax, sftpKeyFileLastSelected, emailHistory, emailHistoryMax, commandHistory, commandHistoryMax); - return static_cast<ReturnSyncConfig::ButtonPressed>(syncDlg.ShowModal()); + return static_cast<ConfirmationButton>(syncDlg.ShowModal()); } diff --git a/FreeFileSync/Source/ui/sync_cfg.h b/FreeFileSync/Source/ui/sync_cfg.h index 9b1dffb5..b853ad2d 100644 --- a/FreeFileSync/Source/ui/sync_cfg.h +++ b/FreeFileSync/Source/ui/sync_cfg.h @@ -8,25 +8,17 @@ #define SYNC_CFG_H_31289470134253425 #include <wx/window.h> +#include <wx+/popup_dlg.h> #include "../base/structures.h" namespace fff { -struct ReturnSyncConfig -{ - enum ButtonPressed - { - BUTTON_CANCEL, - BUTTON_OKAY - }; -}; - enum class SyncConfigPanel { - COMPARISON = 0, //used as zero-based notebook page index! - FILTER, - SYNC + compare = 0, //used as zero-based notebook page index! + filter, + sync }; struct MiscSyncConfig @@ -54,18 +46,18 @@ struct GlobalPairConfig }; -ReturnSyncConfig::ButtonPressed showSyncConfigDlg(wxWindow* parent, - SyncConfigPanel panelToShow, - int localPairIndexToShow, //< 0 to show global config - bool showMultipleCfgs, +zen::ConfirmationButton showSyncConfigDlg(wxWindow* parent, + SyncConfigPanel panelToShow, + int localPairIndexToShow, //< 0 to show global config + bool showMultipleCfgs, - GlobalPairConfig& globalPairCfg, - std::vector<LocalPairConfig>& localPairConfig, - std::vector<Zstring>& versioningFolderHistory, - std::vector<Zstring>& logFolderHistory, - size_t folderHistoryMax, - std::vector<Zstring>& emailHistory, size_t emailHistoryMax, - std::vector<Zstring>& commandHistory, size_t commandHistoryMax); + GlobalPairConfig& globalPairCfg, + std::vector<LocalPairConfig>& localPairConfig, + std::vector<Zstring>& versioningFolderHistory, Zstring& versioningFolderLastSelected, + std::vector<Zstring>& logFolderHistory, Zstring& logFolderLastSelected, + size_t folderHistoryMax, Zstring& sftpKeyFileLastSelected, + std::vector<Zstring>& emailHistory, size_t emailHistoryMax, + std::vector<Zstring>& commandHistory, size_t commandHistoryMax); } #endif //SYNC_CFG_H_31289470134253425 diff --git a/FreeFileSync/Source/ui/tree_grid.cpp b/FreeFileSync/Source/ui/tree_grid.cpp index cadf2d2a..e101a094 100644 --- a/FreeFileSync/Source/ui/tree_grid.cpp +++ b/FreeFileSync/Source/ui/tree_grid.cpp @@ -363,7 +363,7 @@ void TreeView::applySubView(std::vector<RootNodeImpl>&& newView) const TreeLine& line = flatTree_[row]; if (auto hierObj = getHierAlias(line)) - if (contains(expandedNodes, hierObj)) + if (expandedNodes.contains(hierObj)) { std::vector<TreeLine> newLines; getChildren(*line.node, line.level + 1, newLines); @@ -784,7 +784,15 @@ private: } } - //void renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected) override => GridData default is fine + + void renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected) override + { + if (!enabled || !selected) + clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); + else + GridData::renderRowBackgound(dc, rect, row, true /*enabled*/, true /*selected*/ ); + } + enum class HoverAreaTree { @@ -817,6 +825,17 @@ private: // widthNodeStatus_ + gridGap_ + widthNodeIcon + gridGap_, // // rect.height)), wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); + auto drawIcon = [&](wxImage icon, const wxRect& rectIcon, bool drawActive) + { + if (!drawActive) + icon = icon.ConvertToGreyscale(1.0 / 3, 1.0 / 3, 1.0 / 3); //treat all channels equally! + + if (!enabled) + icon = icon.ConvertToDisabled(); + + drawBitmapRtlNoMirror(dc, icon, rectIcon, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); + }; + //consume space rectTmp.x += static_cast<int>(node->level_) * widthLevelStep_; rectTmp.width -= static_cast<int>(node->level_) * widthLevelStep_; @@ -848,24 +867,14 @@ private: if (rectTmp.width > 0) { //node status - auto drawStatus = [&](const char* imageName) - { - const wxImage& img = loadImage(imageName); - - wxRect rectStat(rectTmp.GetTopLeft(), wxSize(img.GetWidth(), img.GetHeight())); - rectStat.y += (rectTmp.height - rectStat.height) / 2; - - drawBitmapRtlNoMirror(dc, img, rectStat, wxALIGN_CENTER); - }; - const bool drawMouseHover = static_cast<HoverAreaTree>(rowHover) == HoverAreaTree::node; switch (node->status_) { case TreeView::STATUS_EXPANDED: - drawStatus(drawMouseHover ? "node_expanded_hover" : "node_expanded"); + drawIcon(loadImage(drawMouseHover ? "node_expanded_hover" : "node_expanded"), rectTmp, true /*drawActive*/); break; case TreeView::STATUS_REDUCED: - drawStatus(drawMouseHover ? "node_reduced_hover" : "node_reduced"); + drawIcon(loadImage(drawMouseHover ? "node_reduced_hover" : "node_reduced"), rectTmp, true /*drawActive*/); break; case TreeView::STATUS_EMPTY: break; @@ -888,10 +897,7 @@ private: else if (dynamic_cast<const TreeView::FilesNode*>(node.get())) nodeIcon = fileIcon_; - if (!isActive) - nodeIcon = nodeIcon.ConvertToGreyscale(1.0 / 3, 1.0 / 3, 1.0 / 3); //treat all channels equally! - - drawBitmapRtlNoMirror(dc, nodeIcon, rectTmp, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); + drawIcon(nodeIcon, rectTmp, isActive); rectTmp.x += widthNodeIcon_ + gridGap_; rectTmp.width -= widthNodeIcon_ + gridGap_; diff --git a/FreeFileSync/Source/ui/version_check.cpp b/FreeFileSync/Source/ui/version_check.cpp index 91199026..e45ea5bb 100644 --- a/FreeFileSync/Source/ui/version_check.cpp +++ b/FreeFileSync/Source/ui/version_check.cpp @@ -80,7 +80,7 @@ bool fff::shouldRunAutomaticUpdateCheck(time_t lastUpdateCheck) std::wstring getIso639Language() { - assert(runningOnMainThread()); //this function is not thread-safe, consider wxWidgets usage + assert(runningOnMainThread()); //this function is not thread-safe: consider wxWidgets usage std::wstring localeName(wxLocale::GetLanguageCanonicalName(wxLocale::GetSystemLanguage())); localeName = beforeFirst(localeName, L"@", IfNotFoundReturn::all); //the locale may contain an @, e.g. "sr_RS@latin"; see wxLocale::InitLanguagesDB() @@ -150,32 +150,25 @@ std::vector<std::pair<std::string, std::string>> geHttpPostParameters(wxWindow& void showUpdateAvailableDialog(wxWindow* parent, const std::string& onlineVersion) { + wxImage ffsVersionIcon = loadImage("FreeFileSync", fastFromDIP(48)); + std::function<void()> openBrowserForDownload = [] { wxLaunchDefaultBrowser(L"https://freefilesync.org/get_latest.php"); }; + std::wstring updateDetailsMsg; try { - try - { - const std::string buf = sendHttpGet(utfTo<Zstring>("https://api.freefilesync.org/latest_changes?" + xWwwFormUrlEncode({ { "since", ffsVersion } })), - ffsUpdateCheckUserAgent, nullptr /*caCertFilePath*/, nullptr /*notifyUnbufferedIO*/).readAll(); //throw SysError - updateDetailsMsg = utfTo<std::wstring>(buf); - } - catch (const SysError& e) { throw FileError(_("Failed to retrieve update information."), e.toString()); } - - } - catch (const FileError& e) //fall back to regular update info dialog: - { - updateDetailsMsg = e.toString() + L"\n\n\n" + updateDetailsMsg; + updateDetailsMsg = utfTo<std::wstring>(sendHttpGet(utfTo<Zstring>("https://api.freefilesync.org/latest_changes?" + xWwwFormUrlEncode({ { "since", ffsVersion } })), + ffsUpdateCheckUserAgent, nullptr /*caCertFilePath*/, nullptr /*notifyUnbufferedIO*/).readAll()); //throw SysError } + catch (const SysError& e) { updateDetailsMsg = _("Failed to retrieve update information.") + + L"\n\n" + e.toString(); } switch (showConfirmationDialog(parent, DialogInfoType::info, PopupDialogCfg(). - setIcon(loadImage("update_available")). + setIcon(ffsVersionIcon). setTitle(_("Check for Program Updates")). - setMainInstructions(replaceCpy(_("FreeFileSync %x is available!"), L"%x", utfTo<std::wstring>(onlineVersion)) + L' ' + _("Download now?")). - setDetailInstructions(updateDetailsMsg), - _("&Download"))) + setMainInstructions(replaceCpy(_("FreeFileSync %x is available!"), L"%x", utfTo<std::wstring>(onlineVersion)) + L"\n\n" + _("Download now?")). + setDetailInstructions(updateDetailsMsg), _("&Download"))) { - case ConfirmationButton::accept: - wxLaunchDefaultBrowser(L"https://freefilesync.org/get_latest.php"); + case ConfirmationButton::accept: //download + openBrowserForDownload(); break; case ConfirmationButton::cancel: break; @@ -247,18 +240,18 @@ void fff::checkForUpdateNow(wxWindow& parent, std::string& lastOnlineVersion) { lastOnlineVersion = "Unknown"; - switch (showQuestionDialog(&parent, DialogInfoType::error, PopupDialogCfg(). - setTitle(_("Check for Program Updates")). - setMainInstructions(_("Cannot find current FreeFileSync version number online. A newer version is likely available. Check manually now?")). - setDetailInstructions(e.toString()), _("&Check"), _("&Retry"))) + switch (showConfirmationDialog(&parent, DialogInfoType::error, PopupDialogCfg(). + setTitle(_("Check for Program Updates")). + setMainInstructions(_("Cannot find current FreeFileSync version number online. A newer version is likely available. Check manually now?")). + setDetailInstructions(e.toString()), _("&Check"), _("&Retry"))) { - case QuestionButton2::yes: + case ConfirmationButton2::accept: wxLaunchDefaultBrowser(L"https://freefilesync.org/get_latest.php"); break; - case QuestionButton2::no: //retry + case ConfirmationButton2::accept2: //retry checkForUpdateNow(parent, lastOnlineVersion); //note: retry via recursion!!! break; - case QuestionButton2::cancel: + case ConfirmationButton2::cancel: break; } } @@ -337,19 +330,19 @@ void fff::automaticUpdateCheckEval(wxWindow* parent, time_t& lastUpdateCheck, st { lastOnlineVersion = "Unknown"; - switch (showQuestionDialog(parent, DialogInfoType::error, PopupDialogCfg(). - setTitle(_("Check for Program Updates")). - setMainInstructions(_("Cannot find current FreeFileSync version number online. A newer version is likely available. Check manually now?")). - setDetailInstructions(result.error->toString()), - _("&Check"), _("&Retry"))) + switch (showConfirmationDialog(parent, DialogInfoType::error, PopupDialogCfg(). + setTitle(_("Check for Program Updates")). + setMainInstructions(_("Cannot find current FreeFileSync version number online. A newer version is likely available. Check manually now?")). + setDetailInstructions(result.error->toString()), + _("&Check"), _("&Retry"))) { - case QuestionButton2::yes: + case ConfirmationButton2::accept: wxLaunchDefaultBrowser(L"https://freefilesync.org/get_latest.php"); break; - case QuestionButton2::no: //retry + case ConfirmationButton2::accept2: //retry automaticUpdateCheckEval(parent, lastUpdateCheck, lastOnlineVersion, asyncResult); //note: retry via recursion!!! break; - case QuestionButton2::cancel: + case ConfirmationButton2::cancel: break; } } diff --git a/FreeFileSync/Source/version/version.h b/FreeFileSync/Source/version/version.h index cbf84370..990af5ad 100644 --- a/FreeFileSync/Source/version/version.h +++ b/FreeFileSync/Source/version/version.h @@ -3,7 +3,7 @@ namespace fff { -const char ffsVersion[] = "11.1"; //internal linkage! +const char ffsVersion[] = "11.2"; //internal linkage! const char FFS_VERSION_SEPARATOR = '.'; } diff --git a/libcurl/curl_wrap.h b/libcurl/curl_wrap.h index 40a330ef..bc924713 100644 --- a/libcurl/curl_wrap.h +++ b/libcurl/curl_wrap.h @@ -148,12 +148,10 @@ std::wstring formatCurlStatusCode(CURLcode sc) void applyCurlOptions(CURL* easyHandle, const std::vector<CurlOption>& options) //throw SysError { for (const CurlOption& opt : options) - { - const CURLcode rc = ::curl_easy_setopt(easyHandle, opt.option, opt.value); - if (rc != CURLE_OK) + if (const CURLcode rc = ::curl_easy_setopt(easyHandle, opt.option, opt.value); + rc != CURLE_OK) throw SysError(formatSystemError("curl_easy_setopt(" + numberTo<std::string>(static_cast<int>(opt.option)) + ")", formatCurlStatusCode(rc), utfTo<std::wstring>(::curl_easy_strerror(rc)))); - } } } } diff --git a/wx+/focus.h b/wx+/focus.h index 297d0754..2920828f 100644 --- a/wx+/focus.h +++ b/wx+/focus.h @@ -45,15 +45,14 @@ wxTopLevelWindow* getTopLevelWindow(wxWindow* child) } -/* -Preserving input focus has to be more clever than: - wxWindow* oldFocus = wxWindow::FindFocus(); - ZEN_ON_SCOPE_EXIT(if (oldFocus) oldFocus->SetFocus()); +/* Preserving input focus has to be more clever than: + wxWindow* oldFocus = wxWindow::FindFocus(); + ZEN_ON_SCOPE_EXIT(if (oldFocus) oldFocus->SetFocus()); => wxWindow::SetFocus() internally calls Win32 ::SetFocus, which calls ::SetActiveWindow, which - lord knows why - changes the foreground window to the focus window even if the user is currently busy using a different app! More curiosity: this foreground focus stealing happens only during the *first* SetFocus() after app start! - It also can be avoided by changing focus back and forth with some other app after start => wxWidgets bug or Win32 feature??? -*/ + It also can be avoided by changing focus back and forth with some other app after start => wxWidgets bug or Win32 feature??? */ + struct FocusPreserver { FocusPreserver() diff --git a/wx+/popup_dlg.cpp b/wx+/popup_dlg.cpp index ffde9824..ee682e19 100644 --- a/wx+/popup_dlg.cpp +++ b/wx+/popup_dlg.cpp @@ -21,7 +21,10 @@ namespace { void setBestInitialSize(wxTextCtrl& ctrl, const wxString& text, wxSize maxSize) { - const int scrollbarWidth = fastFromDIP(20); + const int scrollbarWidth = fastFromDIP(25); /*not only scrollbar, but also left/right padding (on macOS)! + better use slightly larger than exact value (Windows: 17, Linux(CentOS): 14, macOS: 25) + => worst case: minor increase in rowCount (no big deal) + slightly larger bestSize.x (good!) */ + if (maxSize.x <= scrollbarWidth) //implicitly checks for non-zero, too! return; maxSize.x -= scrollbarWidth; @@ -69,7 +72,7 @@ class zen::StandardPopupDialog : public PopupDialogGenerated public: StandardPopupDialog(wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg, const wxString& labelAccept, // - const wxString& labelAcceptAll, //optional, except: if "decline" or "acceptAll" is passed, so must be "accept" + const wxString& labelAccept2, //optional, except: if "decline" or "accept2" is passed, so must be "accept" const wxString& labelDecline) : // PopupDialogGenerated(parent), checkBoxValue_(cfg.checkBoxValue), @@ -93,10 +96,10 @@ public: break; } } - catch (const TaskbarNotAvailable&) {} + catch (TaskbarNotAvailable&) {} - wxBitmap iconTmp; + wxImage iconTmp; wxString titleTmp; switch (type) { @@ -120,7 +123,8 @@ public: if (!cfg.title.empty()) titleTmp = cfg.title; //----------------------------------------------- - m_bitmapMsgType->SetBitmap(iconTmp); + if (iconTmp.IsOk()) + m_bitmapMsgType->SetBitmap(iconTmp); if (titleTmp.empty()) SetTitle(wxTheApp->GetAppDisplayName()); @@ -158,6 +162,7 @@ public: if (!cfg.textMain.empty()) text += L'\n'; text += trimCpy(cfg.textDetail) + L'\n'; //add empty top/bottom lines *instead* of using border space! + setBestInitialSize(*m_textCtrlTextDetail, text, wxSize(maxWidth, maxHeight)); m_textCtrlTextDetail->ChangeValue(text); } @@ -180,11 +185,11 @@ public: stdBtns.setAffirmative(m_buttonAccept); if (labelAccept.empty()) //notification dialog { - assert(labelAcceptAll.empty() && labelDecline.empty()); + assert(labelAccept2.empty() && labelDecline.empty()); m_buttonAccept->SetLabel(_("Close")); //UX Guide: use "Close" for errors, warnings and windows in which users can't make changes (no ampersand!) - m_buttonAcceptAll->Hide(); + m_buttonAccept2->Hide(); m_buttonDecline->Hide(); - m_buttonCancel->Hide(); + m_buttonCancel ->Hide(); } else { @@ -204,15 +209,22 @@ public: //m_buttonDecline->SetId(wxID_RETRY); -> also wxWidgets docs seem to hide some info: "Normally, the identifier should be provided on creation and should not be modified subsequently." } - if (labelAcceptAll.empty()) - m_buttonAcceptAll->Hide(); + if (labelAccept2.empty()) + m_buttonAccept2->Hide(); else { - assert(contains(labelAcceptAll, L"&")); - m_buttonAcceptAll->SetLabel(labelAcceptAll); - stdBtns.setAffirmativeAll(m_buttonAcceptAll); + assert(contains(labelAccept2, L"&")); + m_buttonAccept2->SetLabel(labelAccept2); + stdBtns.setAffirmativeAll(m_buttonAccept2); } } + + if (cfg.disabledButtons.contains(ConfirmationButton3::accept )) m_buttonAccept ->Disable(); + if (cfg.disabledButtons.contains(ConfirmationButton3::accept2)) m_buttonAccept2->Disable(); + if (cfg.disabledButtons.contains(ConfirmationButton3::decline)) m_buttonDecline->Disable(); + assert(!cfg.disabledButtons.contains(ConfirmationButton3::cancel)); + assert(!cfg.disabledButtons.contains(cfg.buttonToDisableWhenChecked)); + updateGui(); //set std order after button visibility was set @@ -221,7 +233,12 @@ public: GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() Center(); //needs to be re-applied after a dialog size change! - m_buttonAccept->SetFocus(); + if (m_buttonAccept->IsEnabled()) + m_buttonAccept->SetFocus(); + else if (m_buttonAccept2->IsEnabled()) + m_buttonAccept2->SetFocus(); + else + m_buttonCancel->SetFocus(); } private: @@ -235,11 +252,11 @@ private: EndModal(static_cast<int>(ConfirmationButton3::accept)); } - void onButtonAcceptAll(wxCommandEvent& event) override + void onButtonAccept2(wxCommandEvent& event) override { if (checkBoxValue_) *checkBoxValue_ = m_checkBoxCustom->GetValue(); - EndModal(static_cast<int>(ConfirmationButton3::acceptAll)); + EndModal(static_cast<int>(ConfirmationButton3::accept2)); } void onButtonDecline(wxCommandEvent& event) override @@ -253,13 +270,6 @@ private: { switch (event.GetKeyCode()) { - case WXK_RETURN: - case WXK_NUMPAD_ENTER: - { - wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED); - onButtonAccept(dummy); - return; - } case WXK_ESCAPE: //handle case where cancel button is hidden! EndModal(static_cast<int>(ConfirmationButton3::cancel)); @@ -274,20 +284,17 @@ private: { switch (buttonToDisableWhenChecked_) { - case QuestionButton2::yes: - m_buttonAccept ->Enable(!m_checkBoxCustom->GetValue()); - m_buttonAcceptAll->Enable(!m_checkBoxCustom->GetValue()); - break; - case QuestionButton2::no: - m_buttonDecline->Enable(!m_checkBoxCustom->GetValue()); - break; - case QuestionButton2::cancel: - break; + //*INDENT-OFF* + case ConfirmationButton3::accept: m_buttonAccept ->Enable(!m_checkBoxCustom->GetValue()); break; + case ConfirmationButton3::accept2: m_buttonAccept2->Enable(!m_checkBoxCustom->GetValue()); break; + case ConfirmationButton3::decline: m_buttonDecline->Enable(!m_checkBoxCustom->GetValue()); break; + case ConfirmationButton3::cancel: break; + //*INDENT-ON* } } bool* checkBoxValue_; - const QuestionButton2 buttonToDisableWhenChecked_; + const ConfirmationButton3 buttonToDisableWhenChecked_; std::unique_ptr<Taskbar> taskbar_; }; @@ -295,34 +302,34 @@ private: void zen::showNotificationDialog(wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg) { - StandardPopupDialog dlg(parent, type, cfg, wxString() /*labelAccept*/, wxString() /*labelAcceptAll*/, wxString() /*labelDecline*/); + StandardPopupDialog dlg(parent, type, cfg, wxString() /*labelAccept*/, wxString() /*labelAccept2*/, wxString() /*labelDecline*/); dlg.ShowModal(); } ConfirmationButton zen::showConfirmationDialog(wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg, const wxString& labelAccept) { - StandardPopupDialog dlg(parent, type, cfg, labelAccept, wxString() /*labelAcceptAll*/, wxString() /*labelDecline*/); + StandardPopupDialog dlg(parent, type, cfg, labelAccept, wxString() /*labelAccept2*/, wxString() /*labelDecline*/); return static_cast<ConfirmationButton>(dlg.ShowModal()); } -ConfirmationButton2 zen::showConfirmationDialog(wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg, const wxString& labelAccept, const wxString& labelAcceptAll) +ConfirmationButton2 zen::showConfirmationDialog(wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg, const wxString& labelAccept, const wxString& labelAccept2) { - StandardPopupDialog dlg(parent, type, cfg, labelAccept, labelAcceptAll, wxString() /*labelDecline*/); + StandardPopupDialog dlg(parent, type, cfg, labelAccept, labelAccept2, wxString() /*labelDecline*/); return static_cast<ConfirmationButton2>(dlg.ShowModal()); } -ConfirmationButton3 zen::showConfirmationDialog(wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg, const wxString& labelAccept, const wxString& labelAcceptAll, const wxString& labelDecline) +ConfirmationButton3 zen::showConfirmationDialog(wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg, const wxString& labelAccept, const wxString& labelAccept2, const wxString& labelDecline) { - StandardPopupDialog dlg(parent, type, cfg, labelAccept, labelAcceptAll, labelDecline); + StandardPopupDialog dlg(parent, type, cfg, labelAccept, labelAccept2, labelDecline); return static_cast<ConfirmationButton3>(dlg.ShowModal()); } QuestionButton2 zen::showQuestionDialog(wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg, const wxString& labelYes, const wxString& labelNo) { - StandardPopupDialog dlg(parent, type, cfg, labelYes, wxString() /*labelAcceptAll*/, labelNo); + StandardPopupDialog dlg(parent, type, cfg, labelYes, wxString() /*labelAccept2*/, labelNo); return static_cast<QuestionButton2>(dlg.ShowModal()); } diff --git a/wx+/popup_dlg.h b/wx+/popup_dlg.h index 1e0a1656..bb7ba51b 100644 --- a/wx+/popup_dlg.h +++ b/wx+/popup_dlg.h @@ -7,6 +7,7 @@ #ifndef POPUP_DLG_H_820780154723456 #define POPUP_DLG_H_820780154723456 +#include <set> #include <wx/window.h> #include <wx/bitmap.h> #include <wx/string.h> @@ -28,45 +29,46 @@ enum class DialogInfoType enum class ConfirmationButton3 { + cancel, accept, - acceptAll, + accept2, decline, - cancel, }; enum class ConfirmationButton { - accept = static_cast<int>(ConfirmationButton3::accept), //[!] Clang requires a "static_cast" - cancel = static_cast<int>(ConfirmationButton3::cancel), // + cancel = static_cast<int>(ConfirmationButton3::cancel), //[!] Clang requires "static_cast" + accept = static_cast<int>(ConfirmationButton3::accept), // }; enum class ConfirmationButton2 { - accept = static_cast<int>(ConfirmationButton3::accept), - acceptAll = static_cast<int>(ConfirmationButton3::acceptAll), - cancel = static_cast<int>(ConfirmationButton3::cancel), + cancel = static_cast<int>(ConfirmationButton3::cancel), + accept = static_cast<int>(ConfirmationButton3::accept), + accept2 = static_cast<int>(ConfirmationButton3::accept2), }; enum class QuestionButton2 { + cancel = static_cast<int>(ConfirmationButton3::cancel), yes = static_cast<int>(ConfirmationButton3::accept), no = static_cast<int>(ConfirmationButton3::decline), - cancel = static_cast<int>(ConfirmationButton3::cancel), }; void showNotificationDialog(wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg); ConfirmationButton showConfirmationDialog(wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg, const wxString& labelAccept); -ConfirmationButton2 showConfirmationDialog(wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg, const wxString& labelAccept, const wxString& labelAcceptAll); -ConfirmationButton3 showConfirmationDialog(wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg, const wxString& labelAccept, const wxString& labelAcceptAll, const wxString& labelDecline); -QuestionButton2 showQuestionDialog (wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg, const wxString& labelYes, const wxString& labelNo); +ConfirmationButton2 showConfirmationDialog(wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg, const wxString& labelAccept, const wxString& labelAccept2); +ConfirmationButton3 showConfirmationDialog(wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg, const wxString& labelAccept, const wxString& labelAccept2, const wxString& labelDecline); +QuestionButton2 showQuestionDialog (wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg, const wxString& labelYes, const wxString& labelNo); //---------------------------------------------------------------------------------------------------------------- class StandardPopupDialog; struct PopupDialogCfg { - PopupDialogCfg& setIcon (const wxBitmap& bmp ) { icon = bmp; return *this; } + PopupDialogCfg& setIcon (const wxImage& bmp ) { icon = bmp; return *this; } PopupDialogCfg& setTitle (const wxString& label) { title = label; return *this; } PopupDialogCfg& setMainInstructions (const wxString& label) { textMain = label; return *this; } //set at least one of these! PopupDialogCfg& setDetailInstructions(const wxString& label) { textDetail = label; return *this; } // - PopupDialogCfg& setCheckBox(bool& value, const wxString& label, QuestionButton2 disableWhenChecked = QuestionButton2::cancel) + PopupDialogCfg& disableButton(ConfirmationButton3 button) { disabledButtons.insert(button); return *this; } + PopupDialogCfg& setCheckBox(bool& value, const wxString& label, ConfirmationButton3 disableWhenChecked = ConfirmationButton3::cancel) { checkBoxValue = &value; checkBoxLabel = label; @@ -77,13 +79,14 @@ struct PopupDialogCfg private: friend class StandardPopupDialog; - wxBitmap icon; + wxImage icon; wxString title; wxString textMain; wxString textDetail; + std::set<ConfirmationButton3> disabledButtons; bool* checkBoxValue = nullptr; //in/out wxString checkBoxLabel; - QuestionButton2 buttonToDisableWhenChecked = QuestionButton2::cancel; + ConfirmationButton3 buttonToDisableWhenChecked = ConfirmationButton3::cancel; }; } diff --git a/wx+/popup_dlg_generated.cpp b/wx+/popup_dlg_generated.cpp index b9da4c38..8933b135 100644 --- a/wx+/popup_dlg_generated.cpp +++ b/wx+/popup_dlg_generated.cpp @@ -67,8 +67,8 @@ PopupDialogGenerated::PopupDialogGenerated( wxWindow* parent, wxWindowID id, con m_buttonAccept->SetDefault(); bSizerStdButtons->Add( m_buttonAccept, 0, wxALIGN_CENTER_VERTICAL|wxBOTTOM|wxRIGHT|wxLEFT, 5 ); - m_buttonAcceptAll = new wxButton( this, wxID_YESTOALL, _("dummy"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); - bSizerStdButtons->Add( m_buttonAcceptAll, 0, wxALIGN_CENTER_VERTICAL|wxBOTTOM|wxRIGHT, 5 ); + m_buttonAccept2 = new wxButton( this, wxID_YESTOALL, _("dummy"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizerStdButtons->Add( m_buttonAccept2, 0, wxALIGN_CENTER_VERTICAL|wxBOTTOM|wxRIGHT, 5 ); m_buttonDecline = new wxButton( this, wxID_NO, _("dummy"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); bSizerStdButtons->Add( m_buttonDecline, 0, wxALIGN_CENTER_VERTICAL|wxBOTTOM|wxRIGHT, 5 ); @@ -93,7 +93,7 @@ PopupDialogGenerated::PopupDialogGenerated( wxWindow* parent, wxWindowID id, con this->Connect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( PopupDialogGenerated::onClose ) ); m_checkBoxCustom->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( PopupDialogGenerated::onCheckBoxClick ), NULL, this ); m_buttonAccept->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( PopupDialogGenerated::onButtonAccept ), NULL, this ); - m_buttonAcceptAll->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( PopupDialogGenerated::onButtonAcceptAll ), NULL, this ); + m_buttonAccept2->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( PopupDialogGenerated::onButtonAccept2 ), NULL, this ); m_buttonDecline->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( PopupDialogGenerated::onButtonDecline ), NULL, this ); m_buttonCancel->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( PopupDialogGenerated::onCancel ), NULL, this ); } diff --git a/wx+/popup_dlg_generated.h b/wx+/popup_dlg_generated.h index 52bd593c..87474708 100644 --- a/wx+/popup_dlg_generated.h +++ b/wx+/popup_dlg_generated.h @@ -49,7 +49,7 @@ protected: wxCheckBox* m_checkBoxCustom; wxBoxSizer* bSizerStdButtons; wxButton* m_buttonAccept; - wxButton* m_buttonAcceptAll; + wxButton* m_buttonAccept2; wxButton* m_buttonDecline; wxButton* m_buttonCancel; @@ -57,7 +57,7 @@ protected: virtual void onClose( wxCloseEvent& event ) { event.Skip(); } virtual void onCheckBoxClick( wxCommandEvent& event ) { event.Skip(); } virtual void onButtonAccept( wxCommandEvent& event ) { event.Skip(); } - virtual void onButtonAcceptAll( wxCommandEvent& event ) { event.Skip(); } + virtual void onButtonAccept2( wxCommandEvent& event ) { event.Skip(); } virtual void onButtonDecline( wxCommandEvent& event ) { event.Skip(); } virtual void onCancel( wxCommandEvent& event ) { event.Skip(); } diff --git a/wx+/std_button_layout.h b/wx+/std_button_layout.h index d2c5fc32..25745132 100644 --- a/wx+/std_button_layout.h +++ b/wx+/std_button_layout.h @@ -18,12 +18,12 @@ namespace zen struct StdButtons { StdButtons& setAffirmative (wxButton* btn) { btnYes = btn; return *this; } - StdButtons& setAffirmativeAll(wxButton* btn) { btnYesAll = btn; return *this; } + StdButtons& setAffirmativeAll(wxButton* btn) { btnYes2 = btn; return *this; } StdButtons& setNegative (wxButton* btn) { btnNo = btn; return *this; } StdButtons& setCancel (wxButton* btn) { btnCancel = btn; return *this; } wxButton* btnYes = nullptr; - wxButton* btnYesAll = nullptr; + wxButton* btnYes2 = nullptr; wxButton* btnNo = nullptr; wxButton* btnCancel = nullptr; }; @@ -67,7 +67,7 @@ void setStandardButtonLayout(wxBoxSizer& sizer, const StdButtons& buttons) }; detach(buttonsTmp.btnYes); - detach(buttonsTmp.btnYesAll); + detach(buttonsTmp.btnYes2); detach(buttonsTmp.btnNo); detach(buttonsTmp.btnCancel); @@ -110,7 +110,7 @@ void setStandardButtonLayout(wxBoxSizer& sizer, const StdButtons& buttons) sizer.Add(spaceRimH, 0); attach(buttonsTmp.btnNo); attach(buttonsTmp.btnCancel); - attach(buttonsTmp.btnYesAll); + attach(buttonsTmp.btnYes2); attach(buttonsTmp.btnYes); sizer.Add(spaceRimH, 0); diff --git a/zen/basic_math.h b/zen/basic_math.h index 0e30c276..26fb9e58 100644 --- a/zen/basic_math.h +++ b/zen/basic_math.h @@ -183,18 +183,15 @@ auto integerDivideRoundUp(N numerator, D denominator) namespace { template <size_t N, class T> struct PowerImpl; -/* - template <size_t N, class T> -> let's use non-recursive specializations to help the compiler - struct PowerImpl { static T result(const T& value) { return PowerImpl<N - 1, T>::result(value) * value; } }; -*/ +//let's use non-recursive specializations to help the compiler template <class T> struct PowerImpl<2, T> { static T result(T value) { return value * value; } }; template <class T> struct PowerImpl<3, T> { static T result(T value) { return value * value * value; } }; } -template <size_t n, class T> inline +template <size_t N, class T> inline T power(T value) { - return PowerImpl<n, T>::result(value); + return PowerImpl<N, T>::result(value); } diff --git a/zen/build_info.h b/zen/build_info.h index 01f1aeb8..adb19f86 100644 --- a/zen/build_info.h +++ b/zen/build_info.h @@ -7,8 +7,6 @@ #ifndef BUILD_INFO_H_5928539285603428657 #define BUILD_INFO_H_5928539285603428657 - #include <bit> //std::endian - #define ZEN_ARCH_32BIT 32 #define ZEN_ARCH_64BIT 64 @@ -20,11 +18,4 @@ static_assert(ZEN_BUILD_ARCH == sizeof(void*) * 8); -//-------------------------------------------------------------------- - -constexpr bool usingLittleEndian() -{ - return std::endian::native == std::endian::little; -} - #endif //BUILD_INFO_H_5928539285603428657 diff --git a/zen/file_access.cpp b/zen/file_access.cpp index 7d3fbfc5..3269bef4 100644 --- a/zen/file_access.cpp +++ b/zen/file_access.cpp @@ -82,7 +82,7 @@ std::optional<Zstring> zen::getParentFolderPath(const Zstring& itemPath) if (const std::optional<PathComponents> comp = parsePathComponents(itemPath)) { if (comp->relPath.empty()) - return {}; + return std::nullopt; const Zstring parentRelPath = beforeLast(comp->relPath, FILE_NAME_SEPARATOR, IfNotFoundReturn::none); if (parentRelPath.empty()) @@ -90,7 +90,7 @@ std::optional<Zstring> zen::getParentFolderPath(const Zstring& itemPath) return appendSeparator(comp->rootPath) + parentRelPath; } assert(false); - return {}; + return std::nullopt; } diff --git a/zen/file_io.cpp b/zen/file_io.cpp index 4c6602cc..33c41fbc 100644 --- a/zen/file_io.cpp +++ b/zen/file_io.cpp @@ -15,7 +15,7 @@ using namespace zen; FileBase::~FileBase() { - if (hFile_ != invalidFileHandle_) + if (hFile_ != invalidFileHandle) try { close(); //throw FileError @@ -26,9 +26,9 @@ FileBase::~FileBase() void FileBase::close() //throw FileError { - if (hFile_ == invalidFileHandle_) + if (hFile_ == invalidFileHandle) throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(getFilePath())), L"Contract error: close() called more than once."); - ZEN_ON_SCOPE_EXIT(hFile_ = invalidFileHandle_); + ZEN_ON_SCOPE_EXIT(hFile_ = invalidFileHandle); if (::close(hFile_) != 0) THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(getFilePath())), "close"); @@ -198,7 +198,7 @@ FileOutput::FileOutput(const Zstring& filePath, const IOCallback& notifyUnbuffer FileOutput::~FileOutput() { - if (getHandle() != invalidFileHandle_) //not finalized => clean up garbage + if (getHandle() != invalidFileHandle) //not finalized => clean up garbage { //"deleting while handle is open" == FILE_FLAG_DELETE_ON_CLOSE if (::unlink(getFilePath().c_str()) != 0) diff --git a/zen/file_io.h b/zen/file_io.h index 81e1e7cc..4210cc57 100644 --- a/zen/file_io.h +++ b/zen/file_io.h @@ -27,7 +27,7 @@ class FileBase { public: using FileHandle = int; - static const int invalidFileHandle_ = -1; + static const int invalidFileHandle = -1; FileHandle getHandle() { return hFile_; } @@ -48,7 +48,7 @@ private: FileBase (const FileBase&) = delete; FileBase& operator=(const FileBase&) = delete; - FileHandle hFile_ = invalidFileHandle_; + FileHandle hFile_ = invalidFileHandle; const Zstring filePath_; }; diff --git a/zen/format_unit.cpp b/zen/format_unit.cpp index 4984c1d7..0d75a9d4 100644 --- a/zen/format_unit.cpp +++ b/zen/format_unit.cpp @@ -7,12 +7,16 @@ #include "format_unit.h" #include <ctime> #include <cstdio> +#include <stdexcept> #include "basic_math.h" +#include "sys_error.h" #include "i18n.h" #include "time.h" #include "globals.h" #include "utf.h" + #include <iostream> + #include <langinfo.h> #include <clocale> //thousands separator #include "utf.h" // @@ -181,3 +185,38 @@ std::wstring zen::formatUtcToLocalTime(time_t utcTime) } + + +WeekDay impl::getFirstDayOfWeekImpl() //throw SysError +{ + /* testing: change locale via command line + --------------------------------------- + LC_TIME=en_DK.utf8 => Monday + LC_TIME=en_US.utf8 => Sunday */ + + const char* firstDay = ::nl_langinfo(_NL_TIME_FIRST_WEEKDAY); //[1-Sunday, 7-Saturday] + ASSERT_SYSERROR(firstDay && 1 <= *firstDay && *firstDay <= 7); + + const int weekDayStartSunday = *firstDay; + const int weekDayStartMonday = (weekDayStartSunday - 1 + 6) % 7; //+6 == -1 in Z_7 + // [0-Monday, 6-Sunday] + return static_cast<WeekDay>(weekDayStartMonday); +} + + +WeekDay zen::getFirstDayOfWeek() +{ + static const WeekDay weekDay = [] + { + try + { + return impl::getFirstDayOfWeekImpl(); //throw SysError + } + catch (const SysError& e) + { + throw std::runtime_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Failed to get first day of the week." + "\n\n" + + utfTo<std::string>(e.toString())); + } + }(); + return weekDay; +} diff --git a/zen/format_unit.h b/zen/format_unit.h index de5a0811..1c96da51 100644 --- a/zen/format_unit.h +++ b/zen/format_unit.h @@ -24,6 +24,21 @@ std::wstring formatThreeDigitPrecision(double value); //(unless value is too lar std::wstring formatNumber(int64_t n); //format integer number including thousands separator + + +enum class WeekDay +{ + monday, + tuesday, + wednesday, + thursday, + friday, + saturday, + sunday, +}; +WeekDay getFirstDayOfWeek(); + +namespace impl { WeekDay getFirstDayOfWeekImpl(); } //throw SysError } #endif diff --git a/zen/legacy_compiler.cpp b/zen/legacy_compiler.cpp index 66125b0f..81efb4dd 100644 --- a/zen/legacy_compiler.cpp +++ b/zen/legacy_compiler.cpp @@ -9,13 +9,13 @@ #error get rid of workarounds #endif -double zen::from_chars(const char* first, const char* last) +double zen::fromChars(const char* first, const char* last) { return std::strtod(std::string(first, last).c_str(), nullptr); } -const char* zen::to_chars(char* first, char* last, double num) +const char* zen::toChars(char* first, char* last, double num) { const size_t bufSize = last - first; const int charsWritten = std::snprintf(first, bufSize, "%g", num); diff --git a/zen/legacy_compiler.h b/zen/legacy_compiler.h index 82c404d8..ad5442fe 100644 --- a/zen/legacy_compiler.h +++ b/zen/legacy_compiler.h @@ -7,26 +7,37 @@ #ifndef LEGACY_COMPILER_H_839567308565656789 #define LEGACY_COMPILER_H_839567308565656789 +#include <version> +/* C++ standard conformance: + https://en.cppreference.com/w/cpp/feature_test + https://en.cppreference.com/w/User:D41D8CD98F/feature_testing_macros + https://isocpp.org/std/standing-documents/sd-6-sg10-feature-test-recommendations + + MSVC https://docs.microsoft.com/en-us/cpp/overview/visual-cpp-language-conformance + + GCC https://gcc.gnu.org/projects/cxx-status.html + libstdc++ https://gcc.gnu.org/onlinedocs/libstdc++/manual/status.html + + Clang https://clang.llvm.org/cxx_status.html#cxx20 + libc++ https://libcxx.llvm.org/cxx2a_status.html */ -//https://en.cppreference.com/w/cpp/feature_test -//https://en.cppreference.com/w/User:D41D8CD98F/feature_testing_macros -//https://isocpp.org/std/standing-documents/sd-6-sg10-feature-test-recommendations -//https://gcc.gnu.org/onlinedocs/libstdc++/manual/status.html namespace std { + + } //--------------------------------------------------------------------------------- //constinit, consteval - #define constinit2 constinit //GCC has it + #define constinit2 constinit //GCC, clang have it #define consteval2 consteval // namespace zen { -double from_chars(const char* first, const char* last); -const char* to_chars(char* first, char* last, double num); +double fromChars(const char* first, const char* last); +const char* toChars(char* first, char* last, double num); } #endif //LEGACY_COMPILER_H_839567308565656789 diff --git a/zen/open_ssl.cpp b/zen/open_ssl.cpp index 1d0c4bf2..ea77db43 100644 --- a/zen/open_ssl.cpp +++ b/zen/open_ssl.cpp @@ -5,6 +5,7 @@ // ***************************************************************************** #include "open_ssl.h" +#include <bit> //std::endian #include <stdexcept> #include "base64.h" #include "build_info.h" @@ -774,7 +775,7 @@ std::string zen::convertPuttyKeyToPkix(const std::string& keyStream, const std:: auto numToBeString = [](size_t n) -> std::string { - static_assert(usingLittleEndian()&& sizeof(n) >= 4); + static_assert(std::endian::native == std::endian::little&& sizeof(n) >= 4); const char* numStr = reinterpret_cast<const char*>(&n); return { numStr[3], numStr[2], numStr[1], numStr[0] }; //big endian! }; @@ -806,7 +807,7 @@ std::string zen::convertPuttyKeyToPkix(const std::string& keyStream, const std:: if (itEnd - it < makeSigned(sizeof(byteCount))) throw SysError(L"String extraction failed: unexpected end of stream"); - static_assert(usingLittleEndian()); + static_assert(std::endian::native == std::endian::little); char* numStr = reinterpret_cast<char*>(&byteCount); numStr[3] = *it++; // numStr[2] = *it++; //Putty uses big endian! diff --git a/zen/recycler.cpp b/zen/recycler.cpp index b1f2c0fd..4448fd60 100644 --- a/zen/recycler.cpp +++ b/zen/recycler.cpp @@ -16,6 +16,7 @@ using namespace zen; +//*INDENT-OFF* bool zen::recycleOrDeleteIfExists(const Zstring& itemPath) //throw FileError { GFile* file = ::g_file_new_for_path(itemPath.c_str()); //never fails according to docu @@ -32,12 +33,73 @@ bool zen::recycleOrDeleteIfExists(const Zstring& itemPath) //throw FileError /* g_file_trash() can fail with different error codes/messages when trash is unavailable: Debian 8 (GLib 2.42): G_IO_ERROR_NOT_SUPPORTED: Unable to find or create trash directory - CentOS 7 (GLib 2.56): G_IO_ERROR_FAILED: Unable to find or create trash directory for file.txt + CentOS 7 (GLib 2.56): G_IO_ERROR_FAILED: Unable to find or create trash directory for file.txt => localized! >:( master (GLib 2.64): G_IO_ERROR_NOT_SUPPORTED: Trashing on system internal mounts is not supported https://gitlab.gnome.org/GNOME/glib/blob/master/gio/glocalfile.c#L2042 */ - const bool trashUnavailable = error && - ((error->domain == G_IO_ERROR && error->code == G_IO_ERROR_NOT_SUPPORTED) || - startsWith(error->message, "Unable to find or create trash directory")); + const bool trashUnavailable = error && error->domain == G_IO_ERROR && + (error->code == G_IO_ERROR_NOT_SUPPORTED || + + //yes, the following is a cluster fuck, but what can you do? + (error->code == G_IO_ERROR_FAILED && [&] + { + for (const char* msgLoc : //translations from https://gitlab.gnome.org/GNOME/glib/-/tree/master/po + { + "Unable to find or create trash directory for", + "No s'ha pogut trobar o crear el directori de la paperera per", + "Nelze nalézt nebo vytvořit složku koše pro", + "Kan ikke finde eller oprette papirkurvskatalog for", + "Αδύνατη η εύρεση ή δημιουργία του καταλόγου απορριμμάτων", + "Unable to find or create wastebasket directory for", + "Ne eblas trovi aŭ krei rubujan dosierujon", + "No se pudo encontrar o crear la carpeta de la papelera para", + "Prügikasti kataloogi pole võimalik leida või luua", + "zakarrontziaren direktorioa aurkitu edo sortu", + "Roskakori kansiota ei löydy tai sitä ei voi luoda", + "Impossible de trouver ou créer le répertoire de la corbeille pour", + "Non é posíbel atopar ou crear o directorio do lixo para", + "Nisam mogao promijeniti putanju u mapu", + "Nem található vagy nem hozható létre a Kuka könyvtár ehhez:", + "Tidak bisa menemukan atau membuat direktori tong sampah bagi", + "Impossibile trovare o creare la directory cestino per", + "のゴミ箱ディレクトリが存在しないか作成できません", + "휴지통 디렉터리를 찾을 수 없거나 만들 수 없습니다", + "Nepavyko rasti ar sukurti šiukšlių aplanko", + "Nevar atrast vai izveidot miskastes mapi priekš", + "Tidak boleh mencari atau mencipta direktori tong sampah untuk", + "Kan ikke finne eller opprette mappe for papirkurv for", + "फाइल सिर्जना गर्न असफल:", + "Impossible de trobar o crear lo repertòri de l'escobilhièr per", + "ਲਈ ਰੱਦੀ ਡਾਇਰੈਕਟਰੀ ਲੱਭਣ ਜਾਂ ਬਣਾਉਣ ਲਈ ਅਸਮਰੱਥ", + "Nie można odnaleźć lub utworzyć katalogu kosza dla", + "Impossível encontrar ou criar a pasta de lixo para", + "Não é possível localizar ou criar o diretório da lixeira para", + "Nu se poate găsi sau crea directorul coșului de gunoi pentru", + "Не удалось найти или создать каталог корзины для", + "Nepodarilo sa nájsť ani vytvoriť adresár Kôš pre", + "Ni mogoče najti oziroma ustvariti mape smeti za", + "Не могу да нађем или направим директоријум смећа за", + "Ne mogu da nađem ili napravim direktorijum smeća za", + "Kunde inte hitta eller skapa papperskorgskatalog för", + "için çöp dizini bulunamıyor ya da oluşturulamıyor", + "Не вдалося знайти або створити каталог смітника для", + "หาหรือสร้างไดเรกทอรีถังขยะสำหรับ", + }) + if (contains(error->message, msgLoc)) + return true; + + for (const auto& [msgLoc1, msgLoc2] : + { + std::pair{"Papierkorb-Ordner konnte für", "nicht gefunden oder angelegt werden"}, + std::pair{"Kan prullenbakmap voor", "niet vinden of aanmaken"}, + std::pair{"无法为", "找到或创建回收站目录"}, + std::pair{"無法找到或建立", "的垃圾桶目錄"}, + }) + if (contains(error->message, msgLoc1) && contains(error->message, msgLoc2)) + return true; + + return false; + }())); + if (trashUnavailable) //implement same behavior as on Windows: if recycler is not existing, delete permanently { if (*type == ItemType::folder) @@ -52,6 +114,7 @@ bool zen::recycleOrDeleteIfExists(const Zstring& itemPath) //throw FileError } return true; } +//*INDENT-ON* /* We really need access to a similar function to check whether a directory supports trashing and emit a warning if it does not! diff --git a/zen/socket.h b/zen/socket.h index f1d26450..62386801 100644 --- a/zen/socket.h +++ b/zen/socket.h @@ -145,7 +145,6 @@ void shutdownSocketSend(SocketType socket) //throw SysError if (::shutdown(socket, SHUT_WR) != 0) THROW_LAST_SYS_ERROR_WSA("shutdown"); } - } #endif //SOCKET_H_23498325972583947678456437 diff --git a/zen/stl_tools.h b/zen/stl_tools.h index 7d071413..495ff8d1 100644 --- a/zen/stl_tools.h +++ b/zen/stl_tools.h @@ -185,18 +185,8 @@ BidirectionalIterator1 searchLast(const BidirectionalIterator1 first1, Bid } } - -//--------------------------------------------------------------------------------------- -//http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0458r2.html - -template <class Container, class ValueType, typename = std::enable_if_t<!IsStringLikeV<Container>>> inline - bool contains(const Container& c, const ValueType& val, int dummy = 0 /*overload string_tools.h contains()*/) -{ - return c.find(val) != c.end(); -} //--------------------------------------------------------------------------------------- - //read-only variant of std::merge; input: two sorted ranges template <class Iterator, class FunctionLeftOnly, class FunctionBoth, class FunctionRightOnly> inline void mergeTraversal(Iterator first1, Iterator last1, diff --git a/zen/string_base.h b/zen/string_base.h index 1052de56..f0899433 100644 --- a/zen/string_base.h +++ b/zen/string_base.h @@ -10,7 +10,7 @@ #include <algorithm> #include <atomic> #include "string_tools.h" - +#include "legacy_compiler.h" //constinit2 //Zbase - a policy based string class optimizing performance and flexibility @@ -312,7 +312,6 @@ template <class Char, template <class> class SP> std::strong_ordering operator<= template <class Char, template <class> class SP> std::strong_ordering operator<=>(const Zbase<Char, SP>& lhs, const Char* rhs); template <class Char, template <class> class SP> std::strong_ordering operator<=>(const Char* lhs, const Zbase<Char, SP>& rhs); - template <class Char, template <class> class SP> inline Zbase<Char, SP> operator+(const Zbase<Char, SP>& lhs, const Zbase<Char, SP>& rhs) { return Zbase<Char, SP>(lhs) += rhs; } template <class Char, template <class> class SP> inline Zbase<Char, SP> operator+(const Zbase<Char, SP>& lhs, const Char* rhs) { return Zbase<Char, SP>(lhs) += rhs; } template <class Char, template <class> class SP> inline Zbase<Char, SP> operator+(const Zbase<Char, SP>& lhs, Char rhs) { return Zbase<Char, SP>(lhs) += rhs; } @@ -515,7 +514,6 @@ std::strong_ordering operator<=>(const Char* lhs, const Zbase<Char, SP>& rhs) } - template <class Char, template <class> class SP> inline size_t Zbase<Char, SP>::length() const { diff --git a/zen/string_tools.h b/zen/string_tools.h index 2c33a4f8..fc715961 100644 --- a/zen/string_tools.h +++ b/zen/string_tools.h @@ -16,7 +16,7 @@ #include <vector> #include "stl_tools.h" #include "string_traits.h" -#include "legacy_compiler.h" //<charconv> (without compiler crashes) +#include "legacy_compiler.h" //<charconv> but without the compiler crashes :> //enhance *any* string class with useful non-member functions: @@ -44,12 +44,12 @@ template <class S, class T, typename = std::enable_if_t<IsStringLikeV<S>>> bool template <class S, class T> bool equalString (const S& lhs, const T& rhs); template <class S, class T> bool equalAsciiNoCase(const S& lhs, const T& rhs); - template <class S, class T> int compareString (const S& lhs, const T& rhs); - template <class S, class T> int compareAsciiNoCase(const S& lhs, const T& rhs); //basic case-insensitive comparison (considering A-Z only!) + // template <class S, class T> std::strong_ordering compareString (const S& lhs, const T& rhs); + template <class S, class T> std::weak_ordering compareAsciiNoCase(const S& lhs, const T& rhs); //basic case-insensitive comparison (considering A-Z only!) struct LessAsciiNoCase //STL container predicate { - template <class S> bool operator()(const S& lhs, const S& rhs) const { return compareAsciiNoCase(lhs, rhs) < 0; } + template <class S> bool operator()(const S& lhs, const S& rhs) const { return std::is_lt(compareAsciiNoCase(lhs, rhs)); } }; @@ -186,22 +186,22 @@ Char asciiToLower(Char c) namespace impl { -inline int strcmpWithNulls(const char* ptr1, const char* ptr2, size_t num) { return std:: memcmp(ptr1, ptr2, num); } //support embedded 0, unlike strncmp/wcsncmp! -inline int strcmpWithNulls(const wchar_t* ptr1, const wchar_t* ptr2, size_t num) { return std::wmemcmp(ptr1, ptr2, num); } // +//support embedded 0, unlike strncmp/wcsncmp: +inline std::strong_ordering strcmpWithNulls(const char* ptr1, const char* ptr2, size_t num) { return std:: memcmp(ptr1, ptr2, num) <=> 0; } +inline std::strong_ordering strcmpWithNulls(const wchar_t* ptr1, const wchar_t* ptr2, size_t num) { return std::wmemcmp(ptr1, ptr2, num) <=> 0; } template <class Char1, class Char2> inline -int strcmpAsciiNoCase(const Char1* lhs, const Char2* rhs, size_t len) +std::weak_ordering strcmpAsciiNoCase(const Char1* lhs, const Char2* rhs, size_t len) { while (len-- > 0) { const Char1 charL = asciiToLower(*lhs++); //ordering: lower-case chars have higher code points than uppper-case const Char2 charR = asciiToLower(*rhs++); // if (charL != charR) - return static_cast<unsigned int>(charL) - static_cast<unsigned int>(charR); //unsigned char-comparison is the convention! - //unsigned underflow is well-defined! + return makeUnsigned(charL) <=> makeUnsigned(charR); //unsigned char-comparison is the convention! } - return 0; + return std::weak_ordering::equivalent; } } @@ -210,7 +210,7 @@ template <class S, class T> inline bool startsWith(const S& str, const T& prefix) { const size_t pfLen = strLength(prefix); - return strLength(str) >= pfLen && impl::strcmpWithNulls(strBegin(str), strBegin(prefix), pfLen) == 0; + return strLength(str) >= pfLen && std::is_eq(impl::strcmpWithNulls(strBegin(str), strBegin(prefix), pfLen)); } @@ -218,7 +218,7 @@ template <class S, class T> inline bool startsWithAsciiNoCase(const S& str, const T& prefix) { const size_t pfLen = strLength(prefix); - return strLength(str) >= pfLen && impl::strcmpAsciiNoCase(strBegin(str), strBegin(prefix), pfLen) == 0; + return strLength(str) >= pfLen && std::is_eq(impl::strcmpAsciiNoCase(strBegin(str), strBegin(prefix), pfLen)); } @@ -227,7 +227,7 @@ bool endsWith(const S& str, const T& postfix) { const size_t strLen = strLength(str); const size_t pfLen = strLength(postfix); - return strLen >= pfLen && impl::strcmpWithNulls(strBegin(str) + strLen - pfLen, strBegin(postfix), pfLen) == 0; + return strLen >= pfLen && std::is_eq(impl::strcmpWithNulls(strBegin(str) + strLen - pfLen, strBegin(postfix), pfLen)); } @@ -236,7 +236,7 @@ bool endsWithAsciiNoCase(const S& str, const T& postfix) { const size_t strLen = strLength(str); const size_t pfLen = strLength(postfix); - return strLen >= pfLen && impl::strcmpAsciiNoCase(strBegin(str) + strLen - pfLen, strBegin(postfix), pfLen) == 0; + return strLen >= pfLen && std::is_eq(impl::strcmpAsciiNoCase(strBegin(str) + strLen - pfLen, strBegin(postfix), pfLen)); } @@ -244,7 +244,7 @@ template <class S, class T> inline bool equalString(const S& lhs, const T& rhs) { const size_t lhsLen = strLength(lhs); - return lhsLen == strLength(rhs) && impl::strcmpWithNulls(strBegin(lhs), strBegin(rhs), lhsLen) == 0; + return lhsLen == strLength(rhs) && std::is_eq(impl::strcmpWithNulls(strBegin(lhs), strBegin(rhs), lhsLen)); } @@ -252,34 +252,36 @@ template <class S, class T> inline bool equalAsciiNoCase(const S& lhs, const T& rhs) { const size_t lhsLen = strLength(lhs); - return lhsLen == strLength(rhs) && impl::strcmpAsciiNoCase(strBegin(lhs), strBegin(rhs), lhsLen) == 0; + return lhsLen == strLength(rhs) && std::is_eq(impl::strcmpAsciiNoCase(strBegin(lhs), strBegin(rhs), lhsLen)); } +#if 0 template <class S, class T> inline -int compareString(const S& lhs, const T& rhs) +std::strong_ordering compareString(const S& lhs, const T& rhs) { const size_t lhsLen = strLength(lhs); const size_t rhsLen = strLength(rhs); - //length check *after* strcmpWithNulls(): we do care about natural ordering: e.g. for "compareString(getUpperCase(lhs), getUpperCase(rhs))" - if (const int rv = impl::strcmpWithNulls(strBegin(lhs), strBegin(rhs), std::min(lhsLen, rhsLen)); - rv != 0) - return rv; - return static_cast<int>(lhsLen) - static_cast<int>(rhsLen); + //length check *after* strcmpWithNulls(): we DO care about natural ordering: e.g. for "compareString(getUpperCase(lhs), getUpperCase(rhs))" + if (const std::strong_ordering cmp = impl::strcmpWithNulls(strBegin(lhs), strBegin(rhs), std::min(lhsLen, rhsLen)); + std::is_neq(cmp)) + return cmp; + return lhsLen <=> rhsLen; } +#endif template <class S, class T> inline -int compareAsciiNoCase(const S& lhs, const T& rhs) +std::weak_ordering compareAsciiNoCase(const S& lhs, const T& rhs) { const size_t lhsLen = strLength(lhs); const size_t rhsLen = strLength(rhs); - if (const int rv = impl::strcmpAsciiNoCase(strBegin(lhs), strBegin(rhs), std::min(lhsLen, rhsLen)); - rv != 0) - return rv; - return static_cast<int>(lhsLen) - static_cast<int>(rhsLen); + if (const std::weak_ordering cmp = impl::strcmpAsciiNoCase(strBegin(lhs), strBegin(rhs), std::min(lhsLen, rhsLen)); + std::is_neq(cmp)) + return cmp; + return lhsLen <=> rhsLen; } @@ -583,17 +585,17 @@ namespace impl { enum class NumberType { - SIGNED_INT, - UNSIGNED_INT, - FLOATING_POINT, - OTHER, + signedInt, + unsignedInt, + floatingPoint, + other, }; -template <class S, class Num> S numberTo(const Num& number, std::integral_constant<NumberType, NumberType::OTHER>) = delete; +template <class S, class Num> S numberTo(const Num& number, std::integral_constant<NumberType, NumberType::other>) = delete; #if 0 //default number to string conversion using streams: convenient, but SLOW, SLOW, SLOW!!!! (~ factor of 20) template <class S, class Num> inline -S numberTo(const Num& number, std::integral_constant<NumberType, NumberType::OTHER>) +S numberTo(const Num& number, std::integral_constant<NumberType, NumberType::other>) { std::basic_ostringstream<GetCharTypeT<S>> ss; ss << number; @@ -603,13 +605,13 @@ S numberTo(const Num& number, std::integral_constant<NumberType, NumberType::OTH template <class S, class Num> inline -S numberTo(const Num& number, std::integral_constant<NumberType, NumberType::FLOATING_POINT>) +S numberTo(const Num& number, std::integral_constant<NumberType, NumberType::floatingPoint>) { //don't use sprintf("%g"): way SLOWWWWWWER than std::to_chars() char buffer[128]; //zero-initialize? //let's give some leeway, but 24 chars should suffice: https://www.reddit.com/r/cpp/comments/dgj89g/cppcon_2019_stephan_t_lavavej_floatingpoint/f3j7d3q/ - const char* strEnd = to_chars(std::begin(buffer), std::end(buffer), number); + const char* strEnd = toChars(std::begin(buffer), std::end(buffer), number); S output; std::for_each(static_cast<const char*>(buffer), strEnd, @@ -657,7 +659,7 @@ void formatPositiveInteger(Num n, OutputIterator& it) template <class S, class Num> inline -S numberTo(const Num& number, std::integral_constant<NumberType, NumberType::SIGNED_INT>) +S numberTo(const Num& number, std::integral_constant<NumberType, NumberType::signedInt>) { GetCharTypeT<S> buffer[2 + sizeof(Num) * 241 / 100]; //zero-initialize? //it's generally faster to use a buffer than to rely on String::operator+=() (in)efficiency @@ -677,7 +679,7 @@ S numberTo(const Num& number, std::integral_constant<NumberType, NumberType::SIG template <class S, class Num> inline -S numberTo(const Num& number, std::integral_constant<NumberType, NumberType::UNSIGNED_INT>) +S numberTo(const Num& number, std::integral_constant<NumberType, NumberType::unsignedInt>) { GetCharTypeT<S> buffer[1 + sizeof(Num) * 241 / 100]; //zero-initialize? //required chars: ceil(ln_10(256^sizeof(n))) =~ ceil(sizeof(n) * 2.4082) <= 1 + floor(sizeof(n) * 2.41) @@ -691,10 +693,10 @@ S numberTo(const Num& number, std::integral_constant<NumberType, NumberType::UNS //-------------------------------------------------------------------------------- -template <class Num, class S> Num stringTo(const S& str, std::integral_constant<NumberType, NumberType::OTHER>) = delete; +template <class Num, class S> Num stringTo(const S& str, std::integral_constant<NumberType, NumberType::other>) = delete; #if 0 //default string to number conversion using streams: convenient, but SLOW template <class Num, class S> inline -Num stringTo(const S& str, std::integral_constant<NumberType, NumberType::OTHER>) +Num stringTo(const S& str, std::integral_constant<NumberType, NumberType::other>) { using CharType = GetCharTypeT<S>; Num number = 0; @@ -708,7 +710,7 @@ inline double stringToFloat(const char* first, const char* last) { //don't use std::strtod(): 1. requires null-terminated string 2. SLOWER than std::from_chars() - return from_chars(first, last); + return fromChars(first, last); } @@ -718,12 +720,12 @@ double stringToFloat(const wchar_t* first, const wchar_t* last) std::string buf(last - first, '\0'); std::transform(first, last, buf.begin(), [](wchar_t c) { return static_cast<char>(c); }); - return from_chars(buf.c_str(), buf.c_str() + buf.size()); + return fromChars(buf.c_str(), buf.c_str() + buf.size()); } template <class Num, class S> inline -Num stringTo(const S& str, std::integral_constant<NumberType, NumberType::FLOATING_POINT>) +Num stringTo(const S& str, std::integral_constant<NumberType, NumberType::floatingPoint>) { const auto* const first = strBegin(str); const auto* const last = first + strLength(str); @@ -771,7 +773,7 @@ Num extractInteger(const S& str, bool& hasMinusSign) //very fast conversion to i template <class Num, class S> inline -Num stringTo(const S& str, std::integral_constant<NumberType, NumberType::SIGNED_INT>) +Num stringTo(const S& str, std::integral_constant<NumberType, NumberType::signedInt>) { bool hasMinusSign = false; //handle minus sign const Num number = extractInteger<Num>(str, hasMinusSign); @@ -780,7 +782,7 @@ Num stringTo(const S& str, std::integral_constant<NumberType, NumberType::SIGNED template <class Num, class S> inline -Num stringTo(const S& str, std::integral_constant<NumberType, NumberType::UNSIGNED_INT>) //very fast conversion to integers: slightly faster than std::atoi, but more importantly: generic +Num stringTo(const S& str, std::integral_constant<NumberType, NumberType::unsignedInt>) //very fast conversion to integers: slightly faster than std::atoi, but more importantly: generic { bool hasMinusSign = false; //handle minus sign const Num number = extractInteger<Num>(str, hasMinusSign); @@ -798,10 +800,10 @@ template <class S, class Num> inline S numberTo(const Num& number) { using TypeTag = std::integral_constant<impl::NumberType, - IsSignedInt <Num>::value ? impl::NumberType::SIGNED_INT : - IsUnsignedInt<Num>::value ? impl::NumberType::UNSIGNED_INT : - IsFloat <Num>::value ? impl::NumberType::FLOATING_POINT : - impl::NumberType::OTHER>; + IsSignedInt <Num>::value ? impl::NumberType::signedInt : + IsUnsignedInt<Num>::value ? impl::NumberType::unsignedInt : + IsFloat <Num>::value ? impl::NumberType::floatingPoint : + impl::NumberType::other>; return impl::numberTo<S>(number, TypeTag()); } @@ -811,10 +813,10 @@ template <class Num, class S> inline Num stringTo(const S& str) { using TypeTag = std::integral_constant<impl::NumberType, - IsSignedInt <Num>::value ? impl::NumberType::SIGNED_INT : - IsUnsignedInt<Num>::value ? impl::NumberType::UNSIGNED_INT : - IsFloat <Num>::value ? impl::NumberType::FLOATING_POINT : - impl::NumberType::OTHER>; + IsSignedInt <Num>::value ? impl::NumberType::signedInt : + IsUnsignedInt<Num>::value ? impl::NumberType::unsignedInt : + IsFloat <Num>::value ? impl::NumberType::floatingPoint : + impl::NumberType::other>; return impl::stringTo<Num>(str, TypeTag()); } diff --git a/zen/sys_version.cpp b/zen/sys_version.cpp index 46918315..d07bbc33 100644 --- a/zen/sys_version.cpp +++ b/zen/sys_version.cpp @@ -78,14 +78,17 @@ OsVersionDetail zen::getOsVersionDetail() //throw SysError OsVersion zen::getOsVersion() { - try - { - static const OsVersionDetail verDetail = getOsVersionDetail(); //throw SysError - return verDetail.version; - } - catch (const SysError& e) + static const OsVersionDetail verDetail = [] { - std::cerr << utfTo<std::string>(e.toString()) << '\n'; - return {}; //sigh, it's a jungle out there: https://freefilesync.org/forum/viewtopic.php?t=7276 - } + try + { + return getOsVersionDetail(); //throw SysError + } + catch (const SysError& e) + { + std::cerr << utfTo<std::string>(e.toString()) << '\n'; + return OsVersionDetail{}; //sigh, it's a jungle out there: https://freefilesync.org/forum/viewtopic.php?t=7276 + } + }(); + return verDetail.version; } diff --git a/zen/zstring.cpp b/zen/zstring.cpp index 06c839e3..c9861219 100644 --- a/zen/zstring.cpp +++ b/zen/zstring.cpp @@ -106,38 +106,38 @@ Zstring replaceCpyAsciiNoCase(const Zstring& str, const Zstring& oldTerm, const /* https://docs.microsoft.com/de-de/windows/desktop/Intl/handling-sorting-in-your-applications -Perf test: compare strings 10 mio times; 64 bit build ------------------------------------------------------ - string a = "Fjk84$%kgfj$%T\\\\Gffg\\gsdgf\\fgsx----------d-" - string b = "fjK84$%kgfj$%T\\\\gfFg\\gsdgf\\fgSy----------dfdf" - -Windows (UTF16 wchar_t) - 4 ns | wcscmp - 67 ns | CompareStringOrdinalFunc+ + bIgnoreCase -314 ns | LCMapString + wmemcmp - -OS X (UTF8 char) - 6 ns | strcmp - 98 ns | strcasecmp - 120 ns | strncasecmp + std::min(sizeLhs, sizeRhs); - 856 ns | CFStringCreateWithCString + CFStringCompare(kCFCompareCaseInsensitive) -1110 ns | CFStringCreateWithCStringNoCopy + CFStringCompare(kCFCompareCaseInsensitive) -________________________ -time per call | function */ - -int compareNativePath(const Zstring& lhs, const Zstring& rhs) + Perf test: compare strings 10 mio times; 64 bit build + ----------------------------------------------------- + string a = "Fjk84$%kgfj$%T\\\\Gffg\\gsdgf\\fgsx----------d-" + string b = "fjK84$%kgfj$%T\\\\gfFg\\gsdgf\\fgSy----------dfdf" + + Windows (UTF16 wchar_t) + 4 ns | wcscmp + 67 ns | CompareStringOrdinalFunc+ + bIgnoreCase + 314 ns | LCMapString + wmemcmp + + OS X (UTF8 char) + 6 ns | strcmp + 98 ns | strcasecmp + 120 ns | strncasecmp + std::min(sizeLhs, sizeRhs); + 856 ns | CFStringCreateWithCString + CFStringCompare(kCFCompareCaseInsensitive) + 1110 ns | CFStringCreateWithCStringNoCopy + CFStringCompare(kCFCompareCaseInsensitive) + ________________________ + time per call | function */ + +std::weak_ordering compareNativePath(const Zstring& lhs, const Zstring& rhs) { assert(lhs.find(Zchar('\0')) == Zstring::npos); //don't expect embedded nulls! assert(rhs.find(Zchar('\0')) == Zstring::npos); // - return compareString(lhs, rhs); + return lhs <=> rhs; } namespace { -int compareNoCaseUtf8(const char* lhs, size_t lhsLen, const char* rhs, size_t rhsLen) +std::weak_ordering compareNoCaseUtf8(const char* lhs, size_t lhsLen, const char* rhs, size_t rhsLen) { //- strncasecmp implements ASCII CI-comparsion only! => signature is broken for UTF8-input; toupper() similarly doesn't support Unicode //- wcsncasecmp: https://opensource.apple.com/source/Libc/Libc-763.12/string/wcsncasecmp-fbsd.c @@ -150,7 +150,7 @@ int compareNoCaseUtf8(const char* lhs, size_t lhsLen, const char* rhs, size_t rh const std::optional<impl::CodePoint> cpL = decL.getNext(); const std::optional<impl::CodePoint> cpR = decR.getNext(); if (!cpL || !cpR) - return static_cast<int>(!cpR) - static_cast<int>(!cpL); + return !cpR <=> !cpL; static_assert(sizeof(gunichar) == sizeof(impl::CodePoint)); @@ -158,14 +158,13 @@ int compareNoCaseUtf8(const char* lhs, size_t lhsLen, const char* rhs, size_t rh const gunichar charR = ::g_unichar_toupper(*cpR); //e.g. "Σ" (upper case) can be lower-case "ς" in the end of the word or "σ" in the middle. if (charL != charR) //ordering: "to lower" converts to higher code points than "to upper" - return static_cast<unsigned int>(charL) - static_cast<unsigned int>(charR); //unsigned char-comparison is the convention! - //unsigned underflow is well-defined! + return makeUnsigned(charL) <=> makeUnsigned(charR); //unsigned char-comparison is the convention! } } } -int compareNatural(const Zstring& lhs, const Zstring& rhs) +std::weak_ordering compareNatural(const Zstring& lhs, const Zstring& rhs) { //Unicode normal forms: // Windows: CompareString() already ignores NFD/NFC differences: nice... @@ -192,13 +191,13 @@ int compareNatural(const Zstring& lhs, const Zstring& rhs) for (;;) { if (strL == strEndL || strR == strEndR) - return static_cast<int>(strL != strEndL) - static_cast<int>(strR != strEndR); //"nothing" before "something" + return (strL != strEndL) <=> (strR != strEndR); //"nothing" before "something" //note: "something" never would have been condensed to "nothing" further below => can finish evaluation here const bool wsL = isWhiteSpace(*strL); const bool wsR = isWhiteSpace(*strR); if (wsL != wsR) - return static_cast<int>(!wsL) - static_cast<int>(!wsR); //whitespace before non-ws! + return !wsL <=> !wsR; //whitespace before non-ws! if (wsL) { ++strL, ++strR; @@ -210,7 +209,7 @@ int compareNatural(const Zstring& lhs, const Zstring& rhs) const bool digitL = isDigit(*strL); const bool digitR = isDigit(*strR); if (digitL != digitR) - return static_cast<int>(!digitL) - static_cast<int>(!digitR); //number before chars! + return !digitL <=> !digitR; //numbers before chars! if (digitL) { while (strL != strEndL && *strL == '0') ++strL; @@ -222,7 +221,7 @@ int compareNatural(const Zstring& lhs, const Zstring& rhs) const bool endL = strL == strEndL || !isDigit(*strL); const bool endR = strR == strEndR || !isDigit(*strR); if (endL != endR) - return static_cast<int>(!endL) - static_cast<int>(!endR); //more digits means bigger number + return !endL <=> !endR; //more digits means bigger number if (endL) break; //same number of digits @@ -230,7 +229,7 @@ int compareNatural(const Zstring& lhs, const Zstring& rhs) rv = *strL - *strR; //found first digit difference comparing from left } if (rv != 0) - return rv; + return rv <=> 0; continue; } @@ -240,9 +239,9 @@ int compareNatural(const Zstring& lhs, const Zstring& rhs) while (strL != strEndL && !isWhiteSpace(*strL) && !isDigit(*strL)) ++strL; while (strR != strEndR && !isWhiteSpace(*strR) && !isDigit(*strR)) ++strR; - const int rv = compareNoCaseUtf8(textBeginL, strL - textBeginL, textBeginR, strR - textBeginR); - if (rv != 0) - return rv; + if (const std::weak_ordering cmp = compareNoCaseUtf8(textBeginL, strL - textBeginL, textBeginR, strR - textBeginR); + std::is_neq(cmp)) + return cmp; } } diff --git a/zen/zstring.h b/zen/zstring.h index 607d3859..09aa43b9 100644 --- a/zen/zstring.h +++ b/zen/zstring.h @@ -33,9 +33,9 @@ Zstring getUpperCase(const Zstring& str); //Windows, Linux: precomposed //macOS: decomposed Zstring getUnicodeNormalForm(const Zstring& str); -// "In fact, Unicode declares that there is an equivalence relationship between decomposed and composed sequences, -// and conformant software should not treat canonically equivalent sequences, whether composed or decomposed or something in between, as different." -// https://www.win.tue.nl/~aeb/linux/uc/nfc_vs_nfd.html +/* "In fact, Unicode declares that there is an equivalence relationship between decomposed and composed sequences, + and conformant software should not treat canonically equivalent sequences, whether composed or decomposed or something in between, as different." + https://www.win.tue.nl/~aeb/linux/uc/nfc_vs_nfd.html */ struct LessUnicodeNormal { bool operator()(const Zstring& lhs, const Zstring& rhs) const { return getUnicodeNormalForm(lhs) < getUnicodeNormalForm(rhs); } }; @@ -55,20 +55,20 @@ struct ZstringNoCase //use as STL container key: avoid needless upper-case conve //------------------------------------------------------------------------------------------ -//Compare *local* file paths: -// Windows: igore case -// Linux: byte-wise comparison -// macOS: ignore case + Unicode normalization forms -int compareNativePath(const Zstring& lhs, const Zstring& rhs); +/* Compare *local* file paths: + Windows: igore case + Linux: byte-wise comparison + macOS: ignore case + Unicode normalization forms */ +std::weak_ordering compareNativePath(const Zstring& lhs, const Zstring& rhs); -inline bool equalNativePath(const Zstring& lhs, const Zstring& rhs) { return compareNativePath(lhs, rhs) == 0; } +inline bool equalNativePath(const Zstring& lhs, const Zstring& rhs) { return std::is_eq(compareNativePath(lhs, rhs)); } -struct LessNativePath { bool operator()(const Zstring& lhs, const Zstring& rhs) const { return compareNativePath(lhs, rhs) < 0; } }; +struct LessNativePath { bool operator()(const Zstring& lhs, const Zstring& rhs) const { return std::is_lt(compareNativePath(lhs, rhs)); } }; //------------------------------------------------------------------------------------------ -int compareNatural(const Zstring& lhs, const Zstring& rhs); +std::weak_ordering compareNatural(const Zstring& lhs, const Zstring& rhs); -struct LessNaturalSort { bool operator()(const Zstring& lhs, const Zstring& rhs) const { return compareNatural(lhs, rhs) < 0; } }; +struct LessNaturalSort { bool operator()(const Zstring& lhs, const Zstring& rhs) const { return std::is_lt(compareNatural(lhs, rhs)); } }; //------------------------------------------------------------------------------------------ |