// ***************************************************************************** // * This file is part of the FreeFileSync project. It is distributed under * // * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * // * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * // ***************************************************************************** #include "main_dlg.h" #include #include #include #include #include #include #include #include #include #include #include "config.h" #include "tray_menu.h" #include "app_icon.h" #include "../icon_buffer.h" #include "../ffs_paths.h" #include "../version/version.h" #include using namespace zen; using namespace rts; namespace { static const size_t MAX_ADD_FOLDERS = 6; std::wstring extractJobName(const Zstring& cfgFilePath) { const Zstring fileName = getItemName(cfgFilePath); const Zstring jobName = beforeLast(fileName, Zstr('.'), IfNotFoundReturn::all); return utfTo(jobName); } bool acceptDialogFileDrop(const std::vector& shellItemPaths) { if (shellItemPaths.empty()) return false; const Zstring ext = getFileExtension(shellItemPaths[0]); return equalAsciiNoCase(ext, "ffs_real") || equalAsciiNoCase(ext, "ffs_batch"); } } std::function& shellItemPaths)> getDroppedPathsFilter(MainDialog& mainDlg) { return [&mainDlg](const std::vector& shellItemPaths) { if (acceptDialogFileDrop(shellItemPaths)) { assert(!shellItemPaths.empty()); mainDlg.loadConfig(shellItemPaths[0]); return false; //don't set dropped paths } return true; //do set dropped paths }; } class rts::DirectoryPanel : public FolderGenerated { public: DirectoryPanel(wxWindow* parent, MainDialog& mainDlg, Zstring& folderLastSelected) : FolderGenerated(parent), folderSelector_(parent, *this, *m_buttonSelectFolder, *m_txtCtrlDirectory, folderLastSelected, nullptr /*staticText*/, getDroppedPathsFilter(mainDlg)) { setImage(*m_bpButtonRemoveFolder, loadImage("item_remove")); } void setPath(const Zstring& dirpath) { folderSelector_.setPath(dirpath); } Zstring getPath() const { return folderSelector_.getPath(); } private: FolderSelector2 folderSelector_; }; void MainDialog::create(const Zstring& cfgFilePath) { /*MainDialog* frame = */ new MainDialog(cfgFilePath); } MainDialog::MainDialog(const Zstring& cfgFilePath) : MainDlgGenerated(nullptr), lastRunConfigPath_(appendPath(fff::getConfigDirPath(), Zstr("LastRun.ffs_real"))) { SetIcon(getRtsIcon()); //set application icon setRelativeFontSize(*m_buttonStart, 1.5); const int scrollDelta = m_buttonSelectFolderMain->GetSize().y; //more approriate than GetCharHeight() here m_scrolledWinFolders->SetScrollRate(scrollDelta, scrollDelta); m_txtCtrlDirectoryMain->SetMinSize({dipToWxsize(300), -1}); setDefaultWidth(*m_spinCtrlDelay); m_bpButtonRemoveTopFolder->Hide(); m_panelMainFolder->Layout(); setImage(*m_bitmapBatch, loadImage("cfg_batch", dipToScreen(20))); setImage(*m_bitmapFolders, fff::IconBuffer::genericDirIcon(fff::IconBuffer::IconSize::small)); setImage(*m_bitmapConsole, loadImage("command_line", dipToScreen(20))); setImage(*m_bpButtonAddFolder, loadImage("item_add")); setImage(*m_bpButtonRemoveTopFolder, loadImage("item_remove")); setBitmapTextLabel(*m_buttonStart, loadImage("start_rts"), m_buttonStart->GetLabelText(), dipToWxsize(5), dipToWxsize(8)); Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onLocalKeyEvent(event); }); //notify about (logical) application main window => program won't quit, but stay on this dialog wxTheApp->SetTopWindow(this); wxTheApp->SetExitOnFrameDelete(true); //prepare drag & drop firstFolderPanel_ = std::make_unique(this, *m_panelMainFolder, *m_buttonSelectFolderMain, *m_txtCtrlDirectoryMain, folderLastSelected_, nullptr /*staticText*/, getDroppedPathsFilter(*this)); //--------------------------- load config values ------------------------------------ FfsRealConfig newConfig; Zstring currentConfigFile = cfgFilePath; if (currentConfigFile.empty()) try { if (itemExists(lastRunConfigPath_)) //throw FileError currentConfigFile = lastRunConfigPath_; } catch (FileError&) { currentConfigFile = lastRunConfigPath_; } //access error? => user should be informed bool loadCfgSuccess = false; if (!currentConfigFile.empty()) try { std::wstring warningMsg; std::tie(newConfig, warningMsg) = readRealOrBatchConfig(currentConfigFile); //throw FileError if (!warningMsg.empty()) showNotificationDialog(this, DialogInfoType::warning, PopupDialogCfg().setDetailInstructions(warningMsg)); loadCfgSuccess = warningMsg.empty(); } catch (const FileError& e) { showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); } const bool startWatchingImmediately = loadCfgSuccess && !cfgFilePath.empty(); setConfiguration(newConfig); setLastUsedConfig(currentConfigFile); //----------------------------------------------------------------------------------------- onSystemShutdownRegister(onBeforeSystemShutdownCookie_); if (startWatchingImmediately) //start watch mode directly { wxCommandEvent dummy2(wxEVT_COMMAND_BUTTON_CLICKED); this->onStart(dummy2); //don't Show()! } else { //GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() => already called by setConfiguration() -> insertAddFolder() #ifdef __WXGTK3__ Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 //Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404 #endif Center(); //apply *after* dialog size change! Show(); m_buttonStart->SetFocus(); //don't "steal" focus if program is running from sys-tray" } //drag and drop .ffs_real and .ffs_batch on main dialog setupFileDrop(*this); Bind(EVENT_DROP_FILE, [this](FileDropEvent& event) { onFilesDropped(event); }); } MainDialog::~MainDialog() { const FfsRealConfig currentCfg = getConfiguration(); try { writeConfig(currentCfg, lastRunConfigPath_); //throw FileError } catch (const FileError& e) { showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); } } void MainDialog::onBeforeSystemShutdown() { try { writeConfig(getConfiguration(), lastRunConfigPath_); } catch (const FileError& e) { logExtraError(e.toString()); } } void MainDialog::onMenuAbout(wxCommandEvent& event) { wxString build = utfTo(fff::ffsVersion); #ifndef wxUSE_UNICODE #error what is going on? #endif const wchar_t* const SPACED_BULLET = L" \u2022 "; build += SPACED_BULLET; build += LTR_MARK; //fix Arabic build += utfTo(cpuArchName); build += SPACED_BULLET; build += utfTo(formatTime(formatDateTag, getCompileTime())); showNotificationDialog(this, DialogInfoType::info, PopupDialogCfg(). setTitle(_("About")). setMainInstructions(L"RealTimeSync" L"\n\n" + replaceCpy(_("Version: %x"), L"%x", build))); } void MainDialog::onLocalKeyEvent(wxKeyEvent& event) { switch (event.GetKeyCode()) { case WXK_ESCAPE: Close(); return; } event.Skip(); } void MainDialog::onStart(wxCommandEvent& event) { Hide(); FfsRealConfig currentCfg = getConfiguration(); const Zstring activeCfgFilePath = !equalNativePath(activeConfigFile_, lastRunConfigPath_) ? activeConfigFile_ : Zstring(); switch (runFolderMonitor(currentCfg, ::extractJobName(activeCfgFilePath))) { case CancelReason::requestExit: Close(); return; case CancelReason::requestGui: break; } //need to center in case of "startWatchingImmediately" #ifdef __WXGTK3__ Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 //Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404 #endif Center(); //apply *after* dialog size change! Show(); //don't show for CancelReason::requestExit Raise(); m_buttonStart->SetFocus(); } void MainDialog::onConfigSave(wxCommandEvent& event) { const Zstring activeCfgFilePath = !equalNativePath(activeConfigFile_, lastRunConfigPath_) ? activeConfigFile_ : Zstring(); std::optional defaultFolderPath = getParentFolderPath(activeCfgFilePath); Zstring defaultFileName = !activeCfgFilePath.empty() ? getItemName(activeCfgFilePath) : 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(defaultFolderPath ? *defaultFolderPath : Zstr("")), utfTo(defaultFileName), wxString(L"RealTimeSync (*.ffs_real)|*.ffs_real") + L"|" +_("All files") + L" (*.*)|*", wxFD_SAVE | wxFD_OVERWRITE_PROMPT); if (fileSelector.ShowModal() != wxID_OK) return; Zstring targetFilePath = utfTo(fileSelector.GetPath()); if (!endsWithAsciiNoCase(targetFilePath, Zstr(".ffs_real"))) //no weird shit! targetFilePath += Zstr(".ffs_real"); //https://freefilesync.org/forum/viewtopic.php?t=9451#p34724 const FfsRealConfig currentCfg = getConfiguration(); try { writeConfig(currentCfg, targetFilePath); //throw FileError setLastUsedConfig(targetFilePath); } catch (const FileError& e) { showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); } } void MainDialog::loadConfig(const Zstring& filepath) { FfsRealConfig newConfig; if (!filepath.empty()) try { std::wstring warningMsg; std::tie(newConfig, warningMsg) = readRealOrBatchConfig(filepath); //throw FileError if (!warningMsg.empty()) showNotificationDialog(this, DialogInfoType::warning, PopupDialogCfg().setDetailInstructions(warningMsg)); } catch (const FileError& e) { showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); return; } setConfiguration(newConfig); setLastUsedConfig(filepath); } void MainDialog::setLastUsedConfig(const Zstring& filepath) { activeConfigFile_ = filepath; const Zstring activeCfgFilePath = !equalNativePath(activeConfigFile_, lastRunConfigPath_) ? activeConfigFile_ : Zstring(); if (!activeCfgFilePath.empty()) SetTitle(utfTo(activeCfgFilePath)); else SetTitle(L"RealTimeSync " + utfTo(fff::ffsVersion) + SPACED_DASH + _("Automated Synchronization")); } void MainDialog::onConfigLoad(wxCommandEvent& event) { const Zstring activeCfgFilePath = !equalNativePath(activeConfigFile_, lastRunConfigPath_) ? activeConfigFile_ : Zstring(); //better: use last user-selected config path instead! std::optional defaultFolderPath = getParentFolderPath(activeCfgFilePath); wxFileDialog fileSelector(this, wxString() /*message*/, utfTo(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(fileSelector.GetPath())); } void MainDialog::onFilesDropped(FileDropEvent& event) { if (!event.itemPaths_.empty()) loadConfig(event.itemPaths_[0]); } void MainDialog::setConfiguration(const FfsRealConfig& cfg) { const Zstring& firstFolderPath = cfg.directories.empty() ? Zstring() : cfg.directories[0]; const std::vector addFolderPaths = cfg.directories.empty() ? std::vector() : std::vector(cfg.directories.begin() + 1, cfg.directories.end()); firstFolderPanel_->setPath(firstFolderPath); bSizerFolders->Clear(true); additionalFolderPanels_.clear(); insertAddFolder(addFolderPaths, 0); m_textCtrlCommand->SetValue(utfTo(cfg.commandline)); m_spinCtrlDelay ->SetValue(static_cast(cfg.delay)); } FfsRealConfig MainDialog::getConfiguration() { FfsRealConfig output; output.directories.push_back(firstFolderPanel_->getPath()); for (const DirectoryPanel* dp : additionalFolderPanels_) output.directories.push_back(dp->getPath()); output.commandline = utfTo(m_textCtrlCommand->GetValue()); output.delay = m_spinCtrlDelay->GetValue(); return output; } void MainDialog::onAddFolder(wxCommandEvent& event) { const Zstring topFolder = firstFolderPanel_->getPath(); //clear existing top folder first firstFolderPanel_->setPath(Zstring()); insertAddFolder({topFolder}, 0); } 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) if (eventObj == static_cast((*it)->m_bpButtonRemoveFolder)) { removeAddFolder(it - additionalFolderPanels_.begin()); return; } } void MainDialog::onRemoveTopFolder(wxCommandEvent& event) { if (!additionalFolderPanels_.empty()) { firstFolderPanel_->setPath(additionalFolderPanels_[0]->getPath()); removeAddFolder(0); //remove first of additional folders } } void MainDialog::insertAddFolder(const std::vector& newFolders, size_t pos) { assert(pos <= additionalFolderPanels_.size() && additionalFolderPanels_.size() == bSizerFolders->GetItemCount()); pos = std::min(pos, additionalFolderPanels_.size()); for (size_t i = 0; i < newFolders.size(); ++i) { //add new folder pair DirectoryPanel* newFolder = new DirectoryPanel(m_scrolledWinFolders, *this, folderLastSelected_); bSizerFolders->Insert(pos + i, newFolder, 0, wxEXPAND); additionalFolderPanels_.insert(additionalFolderPanels_.begin() + pos + i, newFolder); //register events newFolder->m_bpButtonRemoveFolder->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) { onRemoveFolder(event); }); //make sure panel has proper default height newFolder->GetSizer()->SetSizeHints(newFolder); //~=Fit() + SetMinSize() newFolder->setPath(newFolders[i]); } //set size of scrolled window const int folderHeight = additionalFolderPanels_.empty() ? 0 : additionalFolderPanels_[0]->GetSize().GetHeight(); const size_t visibleRows = std::min(additionalFolderPanels_.size(), MAX_ADD_FOLDERS); //up to MAX_ADD_FOLDERS additional folders shall be shown m_scrolledWinFolders->SetMinSize({-1, folderHeight * static_cast(visibleRows)}); m_panelMain->Layout(); //[!] get scrollbars to update correctly //adapt delete top folder pair button m_bpButtonRemoveTopFolder->Show(!additionalFolderPanels_.empty()); GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() Refresh(); //remove a little flicker near the start button } void MainDialog::removeAddFolder(size_t pos) { if (pos < additionalFolderPanels_.size()) { //remove folder pairs from window DirectoryPanel* pairToDelete = additionalFolderPanels_[pos]; bSizerFolders->Detach(pairToDelete); //Remove() does not work on Window*, so do it manually additionalFolderPanels_.erase(additionalFolderPanels_.begin() + pos); //remove last element in vector //more (non-portable) wxWidgets bullshit: on OS X wxWindow::Destroy() screws up and calls "operator delete" directly rather than //the deferred deletion it is expected to do (and which is implemented correctly on Windows and Linux) //http://bb10.com/python-wxpython-devel/2012-09/msg00004.html //=> since we're in a mouse button callback of a sub-component of "pairToDelete" we need to delay deletion ourselves: guiQueue_.processAsync([] {}, [pairToDelete] { pairToDelete->Destroy(); }); //set size of scrolled window const int folderHeight = additionalFolderPanels_.empty() ? 0 : additionalFolderPanels_[0]->GetSize().GetHeight(); const size_t visibleRows = std::min(additionalFolderPanels_.size(), MAX_ADD_FOLDERS); //up to MAX_ADD_FOLDERS additional folders shall be shown m_scrolledWinFolders->SetMinSize({-1, folderHeight * static_cast(visibleRows)}); m_panelMain->Layout(); //[!] get scrollbars to update correctly //adapt delete top folder pair button m_bpButtonRemoveTopFolder->Show(!additionalFolderPanels_.empty()); GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() Refresh(); //remove a little flicker near the start button } }