diff options
Diffstat (limited to 'ui/main_dlg.cpp')
-rw-r--r-- | ui/main_dlg.cpp | 4547 |
1 files changed, 0 insertions, 4547 deletions
diff --git a/ui/main_dlg.cpp b/ui/main_dlg.cpp deleted file mode 100644 index 8ca9cc0b..00000000 --- a/ui/main_dlg.cpp +++ /dev/null @@ -1,4547 +0,0 @@ -// ************************************************************************** -// * This file is part of the FreeFileSync project. It is distributed under * -// * GNU General Public License: http://www.gnu.org/licenses/gpl.html * -// * Copyright (C) Zenju (zenju AT gmx DOT de) - All Rights Reserved * -// ************************************************************************** - -#include "main_dlg.h" -#include <zen/format_unit.h> -#include <zen/file_handling.h> -#include <zen/serialize.h> -//#include <zen/perf.h> -#include <zen/thread.h> -#include <zen/shell_execute.h> -#include <wx/clipbrd.h> -#include <wx/wupdlock.h> -#include <wx/sound.h> -#include <wx/filedlg.h> -#include <wx/display.h> -#include <wx+/context_menu.h> -#include <wx+/string_conv.h> -#include <wx+/bitmap_button.h> -#include <wx+/app_main.h> -#include <wx+/toggle_button.h> -#include <wx+/mouse_move_dlg.h> -#include <wx+/no_flicker.h> -#include <wx+/rtl.h> -#include <wx+/font_size.h> -#include <wx+/popup_dlg.h> -#include <wx+/image_resources.h> -#include "check_version.h" -#include "gui_status_handler.h" -#include "sync_cfg.h" -#include "small_dlgs.h" -#include "progress_indicator.h" -#include "folder_pair.h" -#include "search.h" -#include "batch_config.h" -#include "triple_splitter.h" -#include "app_icon.h" -//#include "config_history.h" -#include "../comparison.h" -#include "../synchronization.h" -#include "../algorithm.h" -#include "../lib/resolve_path.h" -#include "../lib/ffs_paths.h" -#include "../lib/help_provider.h" -#include "../lib/lock_holder.h" -#include "../lib/localization.h" - -#ifdef ZEN_MAC -#include <ApplicationServices/ApplicationServices.h> -#endif - -using namespace zen; -using namespace std::rel_ops; - - -namespace -{ -struct wxClientHistoryData : public wxClientData //we need a wxClientData derived class to tell wxWidgets to take object ownership! -{ - wxClientHistoryData(const Zstring& cfgFile, int lastUseIndex) : cfgFile_(cfgFile), lastUseIndex_(lastUseIndex) {} - - Zstring cfgFile_; - int lastUseIndex_; //support sorting history by last usage, the higher the index the more recent the usage -}; - -IconBuffer::IconSize convert(xmlAccess::FileIconSize isize) -{ - using namespace xmlAccess; - switch (isize) - { - case ICON_SIZE_SMALL: - return IconBuffer::SIZE_SMALL; - case ICON_SIZE_MEDIUM: - return IconBuffer::SIZE_MEDIUM; - case ICON_SIZE_LARGE: - return IconBuffer::SIZE_LARGE; - } - return IconBuffer::SIZE_SMALL; -} -} - - -class DirectoryNameMainImpl : public DirectoryName<FolderHistoryBox> -{ -public: - DirectoryNameMainImpl(MainDialog& mainDlg, - wxWindow& dropWindow1, - Grid& dropGrid, - wxButton& dirSelectButton, - FolderHistoryBox& dirName, - wxStaticText& staticText) : - DirectoryName(dropWindow1, dirSelectButton, dirName, &staticText, &dropGrid.getMainWin()), - mainDlg_(mainDlg) {} - - virtual bool acceptDrop(const std::vector<wxString>& droppedFiles, const wxPoint& clientPos, const wxWindow& wnd) - { - if (std::any_of(droppedFiles.begin(), droppedFiles.end(), [](const wxString& filename) - { - using namespace xmlAccess; - switch (getXmlType(utfCvrtTo<Zstring>(filename))) //throw() - { - case XML_TYPE_GUI: - case XML_TYPE_BATCH: - return true; - case XML_TYPE_GLOBAL: - case XML_TYPE_OTHER: - break; - } - return false; - })) - { - mainDlg_.loadConfiguration(toZ(droppedFiles)); - return false; - } - - //=> return true: change directory selection via drag and drop - return true; - } - -private: - DirectoryNameMainImpl(const DirectoryNameMainImpl&); - DirectoryNameMainImpl& operator=(const DirectoryNameMainImpl&); - - MainDialog& mainDlg_; -}; - -//------------------------------------------------------------------ -/* class hierarchy: - - template<> - FolderPairPanelBasic - /|\ - | - template<> - FolderPairCallback FolderPairPanelGenerated - /|\ /|\ - _________|________ ________| - | | | - FolderPairFirst FolderPairPanel -*/ - -template <class GuiPanel> -class FolderPairCallback : public FolderPairPanelBasic<GuiPanel> //implements callback functionality to MainDialog as imposed by FolderPairPanelBasic -{ -public: - FolderPairCallback(GuiPanel& basicPanel, MainDialog& mainDialog) : - FolderPairPanelBasic<GuiPanel>(basicPanel), //pass FolderPairPanelGenerated part... - mainDlg(mainDialog) {} - -private: - virtual MainConfiguration getMainConfig() const { return mainDlg.getConfig().mainCfg; } - virtual wxWindow* getParentWindow() { return &mainDlg; } - virtual std::unique_ptr<FilterConfig>& getFilterCfgOnClipboardRef() { return mainDlg.filterCfgOnClipboard; } - - virtual void onAltCompCfgChange () { mainDlg.applyCompareConfig(); } - virtual void onAltSyncCfgChange () { mainDlg.applySyncConfig(); } - virtual void onLocalFilterCfgChange() { mainDlg.applyFilterConfig(); } //re-apply filter - - MainDialog& mainDlg; -}; - - -class FolderPairPanel : - public FolderPairPanelGenerated, //FolderPairPanel "owns" FolderPairPanelGenerated! - public FolderPairCallback<FolderPairPanelGenerated> -{ -public: - FolderPairPanel(wxWindow* parent, MainDialog& mainDialog) : - FolderPairPanelGenerated(parent), - FolderPairCallback<FolderPairPanelGenerated>(static_cast<FolderPairPanelGenerated&>(*this), mainDialog), //pass FolderPairPanelGenerated part... - dirNameLeft (*m_panelLeft, *m_buttonSelectDirLeft, *m_directoryLeft), - dirNameRight(*m_panelRight, *m_buttonSelectDirRight, *m_directoryRight) - { - dirNameLeft .Connect(EVENT_ON_DIR_SELECTED, wxCommandEventHandler(MainDialog::onDirSelected), nullptr, &mainDialog); - dirNameRight.Connect(EVENT_ON_DIR_SELECTED, wxCommandEventHandler(MainDialog::onDirSelected), nullptr, &mainDialog); - - dirNameLeft .Connect(EVENT_ON_DIR_MANUAL_CORRECTION, wxCommandEventHandler(MainDialog::onDirManualCorrection), nullptr, &mainDialog); - dirNameRight.Connect(EVENT_ON_DIR_MANUAL_CORRECTION, wxCommandEventHandler(MainDialog::onDirManualCorrection), nullptr, &mainDialog); - } - - void setValues(const Zstring& leftDir, - const Zstring& rightDir, - AltCompCfgPtr cmpCfg, - AltSyncCfgPtr syncCfg, - const FilterConfig& filter) - { - setConfig(cmpCfg, syncCfg, filter); - dirNameLeft .setName(toWx(leftDir)); - dirNameRight.setName(toWx(rightDir)); - } - Zstring getLeftDir () const { return toZ(dirNameLeft .getName()); } - Zstring getRightDir() const { return toZ(dirNameRight.getName()); } - -private: - //support for drag and drop - DirectoryName<FolderHistoryBox> dirNameLeft; - DirectoryName<FolderHistoryBox> dirNameRight; -}; - - -class FolderPairFirst : public FolderPairCallback<MainDialogGenerated> -{ -public: - FolderPairFirst(MainDialog& mainDialog) : - FolderPairCallback<MainDialogGenerated>(mainDialog, mainDialog), - - //prepare drag & drop - dirNameLeft(mainDialog, - *mainDialog.m_panelTopLeft, - *mainDialog.m_gridMainL, - *mainDialog.m_buttonSelectDirLeft, - *mainDialog.m_directoryLeft, - *mainDialog.m_staticTextResolvedPathL), - dirNameRight(mainDialog, - *mainDialog.m_panelTopRight, - *mainDialog.m_gridMainR, - *mainDialog.m_buttonSelectDirRight, - *mainDialog.m_directoryRight, - *mainDialog.m_staticTextResolvedPathR) - { - dirNameLeft .Connect(EVENT_ON_DIR_SELECTED, wxCommandEventHandler(MainDialog::onDirSelected), nullptr, &mainDialog); - dirNameRight.Connect(EVENT_ON_DIR_SELECTED, wxCommandEventHandler(MainDialog::onDirSelected), nullptr, &mainDialog); - - dirNameLeft .Connect(EVENT_ON_DIR_MANUAL_CORRECTION, wxCommandEventHandler(MainDialog::onDirManualCorrection), nullptr, &mainDialog); - dirNameRight.Connect(EVENT_ON_DIR_MANUAL_CORRECTION, wxCommandEventHandler(MainDialog::onDirManualCorrection), nullptr, &mainDialog); - } - - void setValues(const Zstring& leftDir, - const Zstring& rightDir, - AltCompCfgPtr cmpCfg, - AltSyncCfgPtr syncCfg, - const FilterConfig& filter) - { - setConfig(cmpCfg, syncCfg, filter); - dirNameLeft .setName(toWx(leftDir)); - dirNameRight.setName(toWx(rightDir)); - } - Zstring getLeftDir () const { return toZ(dirNameLeft .getName()); } - Zstring getRightDir() const { return toZ(dirNameRight.getName()); } - -private: - //support for drag and drop - DirectoryNameMainImpl dirNameLeft; - DirectoryNameMainImpl dirNameRight; -}; - - -#ifdef ZEN_WIN -class PanelMoveWindow : public MouseMoveWindow -{ -public: - PanelMoveWindow(MainDialog& mainDlg) : - MouseMoveWindow(mainDlg, false), //don't include main dialog itself, thereby prevent various mouse capture lost issues - mainDlg_(mainDlg) {} - - virtual bool allowMove(const wxMouseEvent& event) - { - if (wxPanel* panel = dynamic_cast<wxPanel*>(event.GetEventObject())) - { - const wxAuiPaneInfo& paneInfo = mainDlg_.auiMgr.GetPane(panel); - if (paneInfo.IsOk() && - paneInfo.IsFloating()) - return false; //prevent main dialog move - } - - return true; //allow dialog move - } - -private: - MainDialog& mainDlg_; -}; -#endif - - -namespace -{ -//workaround for wxWidgets bug failing to update menu item bitmaps (affects Windows- and Linux-build) -void setMenuItemImage(wxMenuItem*& menuItem, const wxBitmap& bmp) -{ - assert(menuItem->GetKind() == wxITEM_NORMAL); - - //support polling - if (isEqual(bmp, menuItem->GetBitmap())) - return; - - if (wxMenu* menu = menuItem->GetMenu()) - { - int pos = menu->GetMenuItems().IndexOf(menuItem); - if (pos != wxNOT_FOUND) - { - /* - menu->Remove(menuItem); ->this simple sequence crashes on Kubuntu x64, wxWidgets 2.9.2 - menu->Insert(pos, menuItem); - */ - const bool enabled = menuItem->IsEnabled(); - wxMenuItem* newItem = new wxMenuItem(menu, menuItem->GetId(), menuItem->GetItemLabel()); - - newItem->SetBitmap(bmp); -#ifdef __WXMSW__ //not availabe on wxGTK or wxOSX - //for some inconceivable reason wxWidgets is not consistent with itself and renders disabled icons - //just greyscale instead of brightened like bitmap buttons; much better: - newItem->SetDisabledBitmap(bmp.ConvertToDisabled()); -#endif - bool isDestroyed = menu->Destroy(menuItem); //actual workaround - assert(isDestroyed); - (void)isDestroyed; - menuItem = menu->Insert(pos, newItem); //don't forget to update input item pointer! - - if (!enabled) - menuItem->Enable(false); //do not enable BEFORE appending item! wxWidgets screws up for yet another crappy reason - } - } -} - -//################################################################################################################################## - -xmlAccess::XmlGlobalSettings retrieveGlobalCfgFromDisk() //blocks on GUI on errors! -{ - using namespace xmlAccess; - XmlGlobalSettings globalCfg; - try - { - if (fileExists(getGlobalConfigFile())) - readConfig(globalCfg); //throw FfsXmlError - //else: globalCfg already has default values - } - catch (const FfsXmlError& e) - { - assert(false); - if (e.getSeverity() != FfsXmlError::WARNING) //ignore parsing errors: should be migration problems only *cross-fingers* - showNotificationDialog(nullptr, DialogInfoType::ERROR2, PopupDialogCfg().setDetailInstructions(e.toString())); //no parent window: main dialog not yet created! - } - return globalCfg; -} -} - - -void MainDialog::create() -{ - using namespace xmlAccess; - const XmlGlobalSettings globalSettings = retrieveGlobalCfgFromDisk(); - - std::vector<Zstring> filenames = globalSettings.gui.lastUsedConfigFiles; //2. now try last used files - - //------------------------------------------------------------------------------------------ - //check existence of all files in parallel: - RunUntilFirstHit<NullType> findFirstMissing; - - std::for_each(filenames.begin(), filenames.end(), [&](const Zstring& filename) - { - findFirstMissing.addJob([=] { return filename.empty() /*ever empty??*/ || !fileExists(filename) ? zen::make_unique<NullType>() : nullptr; }); - }); - //potentially slow network access: give all checks 500ms to finish - const bool allFilesExist = findFirstMissing.timedWait(boost::posix_time::milliseconds(500)) && //false: time elapsed - !findFirstMissing.get(); //no missing - if (!allFilesExist) - filenames.clear(); //we do NOT want to show an error due to last config file missing on application start! - //------------------------------------------------------------------------------------------ - - if (filenames.empty()) - { - if (zen::fileExists(lastRunConfigName())) //3. try to load auto-save config - filenames.push_back(lastRunConfigName()); - } - - XmlGuiConfig guiCfg; //structure to receive gui settings with default values - - if (filenames.empty()) - { - //add default exclusion filter: this is only ever relevant when creating new configurations! - //a default XmlGuiConfig does not need these user-specific exclusions! - Zstring& excludeFilter = guiCfg.mainCfg.globalFilter.excludeFilter; - if (!excludeFilter.empty() && !endsWith(excludeFilter, Zstr("\n"))) - excludeFilter += Zstr("\n"); - excludeFilter += globalSettings.gui.defaultExclusionFilter; - } - else - try - { - readAnyConfig(filenames, guiCfg); //throw FfsXmlError - } - catch (const FfsXmlError& e) - { - if (e.getSeverity() == FfsXmlError::WARNING) - showNotificationDialog(nullptr, DialogInfoType::WARNING, PopupDialogCfg().setDetailInstructions(e.toString())); - //what about simulating changed config on parsing errors???? - else - showNotificationDialog(nullptr, DialogInfoType::ERROR2, PopupDialogCfg().setDetailInstructions(e.toString())); - } - - //------------------------------------------------------------------------------------------ - - create(guiCfg, filenames, &globalSettings, false); -} - - -void MainDialog::create(const xmlAccess::XmlGuiConfig& guiCfg, - const std::vector<Zstring>& referenceFiles, - const xmlAccess::XmlGlobalSettings* globalSettings, - bool startComparison) -{ - const xmlAccess::XmlGlobalSettings& globSett = globalSettings ? *globalSettings : retrieveGlobalCfgFromDisk(); - try - { - //we need to set language *before* creating MainDialog! - setLanguage(globSett.programLanguage); //throw FileError - } - catch (const FileError& e) - { - showNotificationDialog(nullptr, DialogInfoType::ERROR2, PopupDialogCfg().setDetailInstructions(e.toString())); - //continue! - } - - MainDialog* frame = new MainDialog(guiCfg, referenceFiles, globSett, startComparison); - frame->Show(); -#ifdef ZEN_MAC - ProcessSerialNumber psn = { 0, kCurrentProcess }; - ::SetFrontProcess(&psn); //call before TransformProcessType() so that OSX menu is updated correctly - ::TransformProcessType(&psn, kProcessTransformToForegroundApplication); //show dock icon, even if we're not an application bundle - //if the executable is not yet in a bundle or if it is called through a launcher, we need to set focus manually: -#endif -} - - -MainDialog::MainDialog(const xmlAccess::XmlGuiConfig& guiCfg, - const std::vector<Zstring>& referenceFiles, - const xmlAccess::XmlGlobalSettings& globalSettings, - bool startComparison) : - MainDialogGenerated(nullptr), - folderHistoryLeft (std::make_shared<FolderHistory>()), //make sure it is always bound - folderHistoryRight(std::make_shared<FolderHistory>()), // - focusWindowAfterSearch(nullptr) -{ - m_directoryLeft ->init(folderHistoryLeft); - m_directoryRight->init(folderHistoryRight); - - //setup sash: detach + reparent: - 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); - -#ifdef ZEN_WIN - wxWindowUpdateLocker dummy(this); //leads to GUI corruption problems on Linux/OS X! -#endif - - setRelativeFontSize(*m_buttonCompare, 1.4); - setRelativeFontSize(*m_buttonSync, 1.4); - setRelativeFontSize(*m_buttonCancel, 1.4); - - //---------------- support for dockable gui style -------------------------------- - bSizerPanelHolder->Detach(m_panelTopButtons); - bSizerPanelHolder->Detach(m_panelDirectoryPairs); - bSizerPanelHolder->Detach(m_gridNavi); - bSizerPanelHolder->Detach(m_panelCenter); - bSizerPanelHolder->Detach(m_panelConfig); - bSizerPanelHolder->Detach(m_panelFilter); - bSizerPanelHolder->Detach(m_panelViewFilter); - bSizerPanelHolder->Detach(m_panelStatistics); - - auiMgr.SetManagedWindow(this); - auiMgr.SetFlags(wxAUI_MGR_DEFAULT | wxAUI_MGR_LIVE_RESIZE); - - compareStatus = make_unique<CompareProgressDialog>(*this); //integrate the compare status panel (in hidden state) - - //caption required for all panes that can be manipulated by the users => used by context menu - auiMgr.AddPane(m_panelCenter, - wxAuiPaneInfo().Name(L"Panel3").CenterPane().PaneBorder(false)); - - auiMgr.AddPane(m_panelDirectoryPairs, - wxAuiPaneInfo().Name(L"Panel2").Layer(2).Top().Caption(_("Folder Pairs")).CaptionVisible(false).PaneBorder(false).Gripper()); - - auiMgr.AddPane(m_panelSearch, - wxAuiPaneInfo().Name(L"PanelFind").Layer(2).Bottom().Caption(_("Find")).CaptionVisible(false).PaneBorder(false).Gripper().MinSize(200, m_bpButtonHideSearch->GetSize().GetHeight()).Hide()); - - auiMgr.AddPane(m_gridNavi, - wxAuiPaneInfo().Name(L"Panel10").Layer(3).Left().Position(1).Caption(_("Overview")).MinSize(300, m_gridNavi->GetSize().GetHeight())); //MinSize(): just default size, see comment below - - auiMgr.AddPane(m_panelConfig, - wxAuiPaneInfo().Name(L"Panel4").Layer(3).Left().Position(2).Caption(_("Configuration")).MinSize(m_listBoxHistory->GetSize().GetWidth(), m_panelConfig->GetSize().GetHeight())); - - auiMgr.AddPane(m_panelTopButtons, - wxAuiPaneInfo().Name(L"Panel1").Layer(4).Top().Row(1).Caption(_("Main Bar")).CaptionVisible(false).PaneBorder(false).Gripper().MinSize(-1, m_panelTopButtons->GetSize().GetHeight())); - //note: min height is calculated incorrectly by wxAuiManager if panes with and without caption are in the same row => use smaller min-size - - auiMgr.AddPane(compareStatus->getAsWindow(), - wxAuiPaneInfo().Name(L"Panel9").Layer(4).Top().Row(2).CaptionVisible(false).PaneBorder(false).Hide()); - - auiMgr.AddPane(m_panelFilter, - wxAuiPaneInfo().Name(L"Panel5").Layer(4).Bottom().Position(1).Caption(_("Filter Files")).MinSize(m_bpButtonFilter->GetSize().GetWidth(), m_panelFilter->GetSize().GetHeight())); - - auiMgr.AddPane(m_panelViewFilter, - wxAuiPaneInfo().Name(L"Panel6").Layer(4).Bottom().Position(2).Caption(_("Select View")).MinSize(m_bpButtonShowDoNothing->GetSize().GetWidth(), m_panelViewFilter->GetSize().GetHeight())); - - auiMgr.AddPane(m_panelStatistics, - wxAuiPaneInfo().Name(L"Panel7").Layer(4).Bottom().Position(3).Caption(_("Statistics")).MinSize(m_bitmapData->GetSize().GetWidth() + m_staticTextData->GetSize().GetWidth(), m_panelStatistics->GetSize().GetHeight())); - - auiMgr.Update(); - - //give panel captions bold typeface - if (wxAuiDockArt* artProvider = auiMgr.GetArtProvider()) - { - wxFont font = artProvider->GetFont(wxAUI_DOCKART_CAPTION_FONT); - font.SetWeight(wxFONTWEIGHT_BOLD); - font.SetPointSize(wxNORMAL_FONT->GetPointSize()); //= larger than the wxAuiDockArt default; looks better on OS X - artProvider->SetFont(wxAUI_DOCKART_CAPTION_FONT, font); - - //accessibility: fix wxAUI drawing black text on black background on high-contrast color schemes: - artProvider->SetColor(wxAUI_DOCKART_INACTIVE_CAPTION_TEXT_COLOUR, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); - } - - auiMgr.GetPane(m_gridNavi).MinSize(-1, -1); //we successfully tricked wxAuiManager into setting an initial Window size :> incomplete API anyone?? - auiMgr.Update(); // - - defaultPerspective = auiMgr.SavePerspective(); - //---------------------------------------------------------------------------------- - //register view layout context menu - m_panelTopButtons->Connect(wxEVT_RIGHT_DOWN, wxMouseEventHandler(MainDialog::OnContextSetLayout), nullptr, this); - m_panelConfig ->Connect(wxEVT_RIGHT_DOWN, wxMouseEventHandler(MainDialog::OnContextSetLayout), nullptr, this); - m_panelFilter ->Connect(wxEVT_RIGHT_DOWN, wxMouseEventHandler(MainDialog::OnContextSetLayout), nullptr, this); - m_panelViewFilter->Connect(wxEVT_RIGHT_DOWN, wxMouseEventHandler(MainDialog::OnContextSetLayout), nullptr, this); - m_panelStatistics->Connect(wxEVT_RIGHT_DOWN, wxMouseEventHandler(MainDialog::OnContextSetLayout), nullptr, this); - m_panelStatusBar ->Connect(wxEVT_RIGHT_DOWN, wxMouseEventHandler(MainDialog::OnContextSetLayout), nullptr, this); - //---------------------------------------------------------------------------------- - - //sort grids - m_gridMainL->Connect(EVENT_GRID_COL_LABEL_MOUSE_LEFT, GridClickEventHandler(MainDialog::onGridLabelLeftClickL ), nullptr, this); - m_gridMainC->Connect(EVENT_GRID_COL_LABEL_MOUSE_LEFT, GridClickEventHandler(MainDialog::onGridLabelLeftClickC ), nullptr, this); - m_gridMainR->Connect(EVENT_GRID_COL_LABEL_MOUSE_LEFT, GridClickEventHandler(MainDialog::onGridLabelLeftClickR ), nullptr, this); - - m_gridMainL->Connect(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, GridClickEventHandler(MainDialog::onGridLabelContextL ), nullptr, this); - m_gridMainC->Connect(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, GridClickEventHandler(MainDialog::onGridLabelContextC ), nullptr, this); - m_gridMainR->Connect(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, GridClickEventHandler(MainDialog::onGridLabelContextR ), nullptr, this); - - //grid context menu - m_gridMainL->Connect(EVENT_GRID_MOUSE_RIGHT_UP, GridClickEventHandler(MainDialog::onMainGridContextL), nullptr, this); - m_gridMainC->Connect(EVENT_GRID_MOUSE_RIGHT_UP, GridClickEventHandler(MainDialog::onMainGridContextC), nullptr, this); - m_gridMainR->Connect(EVENT_GRID_MOUSE_RIGHT_UP, GridClickEventHandler(MainDialog::onMainGridContextR), nullptr, this); - m_gridNavi ->Connect(EVENT_GRID_MOUSE_RIGHT_UP, GridClickEventHandler(MainDialog::onNaviGridContext ), nullptr, this); - - m_gridMainL->Connect(EVENT_GRID_MOUSE_LEFT_DOUBLE, GridClickEventHandler(MainDialog::onGridDoubleClickL), nullptr, this ); - m_gridMainR->Connect(EVENT_GRID_MOUSE_LEFT_DOUBLE, GridClickEventHandler(MainDialog::onGridDoubleClickR), nullptr, this ); - - m_gridNavi->Connect(EVENT_GRID_SELECT_RANGE, GridRangeSelectEventHandler(MainDialog::onNaviSelection), nullptr, this); - //---------------------------------------------------------------------------------- - - m_panelSearch->Connect(wxEVT_CHAR_HOOK, wxKeyEventHandler(MainDialog::OnSearchPanelKeyPressed), nullptr, this); - - //set tool tips with (non-translated!) short cut hint - m_bpButtonOpen ->SetToolTip(_("Open...") + L" (Ctrl+O)"); - m_bpButtonSave ->SetToolTip(_("Save") + L" (Ctrl+S)"); - m_buttonCompare ->SetToolTip(_("Compare both sides") + L" (F5)"); - m_bpButtonCmpConfig ->SetToolTip(_("Comparison settings") + L" (F6)"); - m_bpButtonSyncConfig->SetToolTip(_("Synchronization settings") + L" (F7)"); - m_buttonSync ->SetToolTip(_("Start synchronization") + L" (F8)"); - - gridDataView = std::make_shared<GridView>(); - treeDataView = std::make_shared<TreeView>(); - - cleanedUp = false; - processingGlobalKeyEvent = false; - -#ifdef ZEN_WIN - new PanelMoveWindow(*this); //allow moving main dialog by clicking (nearly) anywhere... //ownership passed to "this" -#endif - - //set icons for this dialog - SetIcon(getFfsIcon()); //set application icon - - m_bpButtonSyncConfig->SetBitmapLabel(getResourceImage(L"cfg_sync")); - m_bpButtonCmpConfig ->SetBitmapLabel(getResourceImage(L"cfg_compare")); - m_bpButtonOpen ->SetBitmapLabel(getResourceImage(L"load")); - m_bpButtonBatchJob ->SetBitmapLabel(getResourceImage(L"batch")); - m_bpButtonAddPair ->SetBitmapLabel(getResourceImage(L"item_add")); - m_bpButtonHideSearch->SetBitmapLabel(getResourceImage(L"close_panel")); - - //we can't use a wxButton for cancel: it's rendered smaller on OS X than a wxBitmapButton! - setBitmapTextLabel(*m_buttonCancel, wxImage(), m_buttonCancel->GetLabel()); - - { - IconBuffer tmp(IconBuffer::SIZE_SMALL); - const wxBitmap& bmpFile = tmp.genericFileIcon(); - const wxBitmap& bmpDir = tmp.genericDirIcon(); - - m_bitmapSmallDirectoryLeft ->SetBitmap(bmpDir); - m_bitmapSmallFileLeft ->SetBitmap(bmpFile); - m_bitmapSmallDirectoryRight->SetBitmap(bmpDir); - m_bitmapSmallFileRight ->SetBitmap(bmpFile); - } - - //menu icons: workaround for wxWidgets: small hack to update menu items: actually this is a wxWidgets bug (affects Windows- and Linux-build) - setMenuItemImage(m_menuItem10, getResourceImage(L"compare_small")); - setMenuItemImage(m_menuItem11, getResourceImage(L"sync_small")); - - setMenuItemImage(m_menuItemLoad, getResourceImage(L"load_small")); - setMenuItemImage(m_menuItemSave, getResourceImage(L"save_small")); - - setMenuItemImage(m_menuItemGlobSett, getResourceImage(L"settings_small")); - setMenuItemImage(m_menuItem7, getResourceImage(L"batch_small")); - - setMenuItemImage(m_menuItemManual, getResourceImage(L"help_small")); - setMenuItemImage(m_menuItemAbout, getResourceImage(L"about_small")); - - if (!manualProgramUpdateRequired()) - { - m_menuItemCheckVersionNow ->Enable(false); - m_menuItemCheckVersionAuto->Enable(false); - - //wxFormbuilder doesn't give us a wxMenuItem for m_menuCheckVersion, so we need this abomination: - wxMenuItemList& items = m_menuHelp->GetMenuItems(); - for (auto it = items.begin(); it != items.end(); ++it) - if ((*it)->GetSubMenu() == m_menuCheckVersion) - (*it)->Enable(false); - } - - //create language selection menu - std::for_each(zen::ExistingTranslations::get().begin(), ExistingTranslations::get().end(), - [&](const ExistingTranslations::Entry& entry) - { - wxMenuItem* newItem = new wxMenuItem(m_menuLanguages, wxID_ANY, entry.languageName); - newItem->SetBitmap(getResourceImage(entry.languageFlag)); - - //map menu item IDs with language IDs: evaluated when processing event handler - languageMenuItemMap.insert(std::make_pair(newItem->GetId(), entry.languageID)); - - //connect event - this->Connect(newItem->GetId(), wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler(MainDialog::OnMenuLanguageSwitch)); - m_menuLanguages->Append(newItem); - }); - - //notify about (logical) application main window => program won't quit, but stay on this dialog - zen::setMainWindow(this); - - //init handling of first folder pair - firstFolderPair = make_unique<FolderPairFirst>(*this); - - initViewFilterButtons(); - - //init grid settings - gridview::init(*m_gridMainL, *m_gridMainC, *m_gridMainR, gridDataView); - treeview::init(*m_gridNavi, treeDataView); - //config_history::init(*m_gridConfigHistory, lastRunConfigName()); - - //initialize and load configuration - setGlobalCfgOnInit(globalSettings); - setConfig(guiCfg, referenceFiles); - - //support for CTRL + C and DEL on grids - m_gridMainL->getMainWin().Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(MainDialog::onGridButtonEventL), nullptr, this); - m_gridMainC->getMainWin().Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(MainDialog::onGridButtonEventC), nullptr, this); - m_gridMainR->getMainWin().Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(MainDialog::onGridButtonEventR), nullptr, this); - - m_gridNavi->getMainWin().Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(MainDialog::onTreeButtonEvent), nullptr, this); - - //register global hotkeys (without explicit menu entry) - wxTheApp->Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(MainDialog::OnGlobalKeyEvent), nullptr, this); - wxTheApp->Connect(wxEVT_CHAR_HOOK, wxKeyEventHandler(MainDialog::OnGlobalKeyEvent), nullptr, this); //capture direction keys - - //drag & drop on navi panel - setupFileDrop(*m_gridNavi); - m_gridNavi->Connect(EVENT_DROP_FILE, FileDropEventHandler(MainDialog::onNaviPanelFilesDropped), nullptr, this); - - timerForAsyncTasks.Connect(wxEVT_TIMER, wxEventHandler(MainDialog::onProcessAsyncTasks), nullptr, this); - - //Connect(wxEVT_SIZE, wxSizeEventHandler(MainDialog::OnResize), nullptr, this); - //Connect(wxEVT_MOVE, wxSizeEventHandler(MainDialog::OnResize), nullptr, this); - - //calculate witdh of folder pair manually (if scrollbars are visible) - m_panelTopLeft->Connect(wxEVT_SIZE, wxEventHandler(MainDialog::OnResizeLeftFolderWidth), nullptr, this); - - //dynamically change sizer direction depending on size - m_panelConfig ->Connect(wxEVT_SIZE, wxEventHandler(MainDialog::OnResizeConfigPanel), nullptr, this); - m_panelViewFilter->Connect(wxEVT_SIZE, wxEventHandler(MainDialog::OnResizeViewPanel), nullptr, this); - m_panelStatistics->Connect(wxEVT_SIZE, wxEventHandler(MainDialog::OnResizeStatisticsPanel), nullptr, this); - wxSizeEvent dummy3; - OnResizeConfigPanel (dummy3); //call once on window creation - OnResizeViewPanel (dummy3); // - OnResizeStatisticsPanel(dummy3); // - - //event handler for manual (un-)checking of rows and setting of sync direction - m_gridMainC->Connect(EVENT_GRID_CHECK_ROWS, CheckRowsEventHandler (MainDialog::onCheckRows), nullptr, this); - m_gridMainC->Connect(EVENT_GRID_SYNC_DIRECTION, SyncDirectionEventHandler(MainDialog::onSetSyncDirection), nullptr, this); - - //mainly to update row label sizes... - updateGui(); - - //register regular check for update on next idle event - Connect(wxEVT_IDLE, wxIdleEventHandler(MainDialog::OnRegularUpdateCheck), nullptr, this); - - //asynchronous call to wxWindow::Layout(): fix superfluous frame on right and bottom when FFS is started in fullscreen mode - Connect(wxEVT_IDLE, wxIdleEventHandler(MainDialog::OnLayoutWindowAsync), nullptr, this); - wxCommandEvent evtDummy; //call once before OnLayoutWindowAsync() - OnResizeLeftFolderWidth(evtDummy); // - - //---------------------------------------------------------------------------------------------------------------------------------------------------------------- - //some convenience: if FFS is started with a *.ffs_gui file as commandline parameter AND all directories contained exist, comparison shall be started right away - if (startComparison) - { - const zen::MainConfiguration currMainCfg = getConfig().mainCfg; - - //------------------------------------------------------------------------------------------ - //check existence of all directories in parallel! - RunUntilFirstHit<NullType> findFirstMissing; - - //harmonize checks with comparison.cpp:: checkForIncompleteInput() - //we're really doing two checks: 1. check directory existence 2. check config validity -> don't mix them! - bool havePartialPair = false; - bool haveFullPair = false; - - auto addDirCheck = [&](const FolderPairEnh& fp) - { - const Zstring dirLeft = getFormattedDirectoryName(fp.leftDirectory ); //should not block!? - const Zstring dirRight = getFormattedDirectoryName(fp.rightDirectory); // - - if (dirLeft.empty() != dirRight.empty()) //only skip check if both sides are empty! - havePartialPair = true; - else if (!dirLeft.empty()) - haveFullPair = true; - - if (!dirLeft.empty()) - findFirstMissing.addJob([=] { return !dirExists(dirLeft ) ? zen::make_unique<NullType>() : nullptr; }); - if (!dirRight.empty()) - findFirstMissing.addJob([=] { return !dirExists(dirRight) ? zen::make_unique<NullType>() : nullptr; }); - }; - - addDirCheck(currMainCfg.firstPair); - std::for_each(currMainCfg.additionalPairs.begin(), currMainCfg.additionalPairs.end(), addDirCheck); - //------------------------------------------------------------------------------------------ - - if (havePartialPair != haveFullPair) //either all pairs full or all half-filled -> validity check! - { - //potentially slow network access: give all checks 500ms to finish - const bool allFilesExist = findFirstMissing.timedWait(boost::posix_time::milliseconds(500)) && //true: have result - !findFirstMissing.get(); //no missing - if (allFilesExist) - if (wxEvtHandler* evtHandler = m_buttonCompare->GetEventHandler()) - { - wxCommandEvent dummy2(wxEVT_COMMAND_BUTTON_CLICKED); - evtHandler->AddPendingEvent(dummy2); //simulate button click on "compare" - } - } - } -} - - -MainDialog::~MainDialog() -{ - try //save "GlobalSettings.xml" - { - xmlAccess::writeConfig(getGlobalCfgBeforeExit()); //throw FfsXmlError - } - catch (const xmlAccess::FfsXmlError& e) - { - showNotificationDialog(this, DialogInfoType::ERROR2, PopupDialogCfg().setDetailInstructions(e.toString())); - } - - try //save "LastRun.ffs_gui" - { - xmlAccess::writeConfig(getConfig(), lastRunConfigName()); //throw FfsXmlError - } - //don't annoy users on read-only drives: it's enough to show a single error message when saving global config - catch (const xmlAccess::FfsXmlError&) {} - - //important! event source wxTheApp is NOT dependent on this instance -> disconnect! - wxTheApp->Disconnect(wxEVT_KEY_DOWN, wxKeyEventHandler(MainDialog::OnGlobalKeyEvent), nullptr, this); - wxTheApp->Disconnect(wxEVT_CHAR_HOOK, wxKeyEventHandler(MainDialog::OnGlobalKeyEvent), nullptr, this); - -#ifdef ZEN_MAC - //more (non-portable) wxWidgets crap: wxListBox leaks wxClientData, both of the following functions fail to clean up: - // src/common/ctrlsub.cpp:: wxItemContainer::~wxItemContainer() -> empty function body!!! - // src/osx/listbox_osx.cpp: wxListBox::~wxListBox() - //=> finally a manual wxItemContainer::Clear() will render itself useful: - m_listBoxHistory->Clear(); -#endif - - auiMgr.UnInit(); - - //no need for wxEventHandler::Disconnect() here; event sources are components of this window and are destroyed, too -} - -//------------------------------------------------------------------------------------------------------------------------------------- - -void MainDialog::onQueryEndSession() -{ - try { xmlAccess::writeConfig(getGlobalCfgBeforeExit()); } - catch (const xmlAccess::FfsXmlError&) {} //we try our best do to something useful in this extreme situation - no reason to notify or even log errors here! - - try { xmlAccess::writeConfig(getConfig(), lastRunConfigName()); } - catch (const xmlAccess::FfsXmlError&) {} -} - - -void MainDialog::setGlobalCfgOnInit(const xmlAccess::XmlGlobalSettings& globalSettings) -{ - globalCfg = globalSettings; - - //caveat set/get language asymmmetry! setLanguage(globalSettings.programLanguage); //throw FileError - //we need to set langugabe before creating this class! - - //set dialog size and position: - // - width/height are invalid if the window is minimized (eg x,y == -32000; height = 28, width = 160) - // - multi-monitor setups: dialog may be placed on second monitor which is currently turned off - if (globalSettings.gui.dlgSize.GetWidth () > 0 && - globalSettings.gui.dlgSize.GetHeight() > 0) - { - //calculate how much of the dialog will be visible on screen - const int dialogAreaTotal = globalSettings.gui.dlgSize.GetWidth() * globalSettings.gui.dlgSize.GetHeight(); - int dialogAreaVisible = 0; - - const int monitorCount = wxDisplay::GetCount(); - for (int i = 0; i < monitorCount; ++i) - { - wxRect intersection = wxDisplay(i).GetClientArea().Intersect(wxRect(globalSettings.gui.dlgPos, globalSettings.gui.dlgSize)); - dialogAreaVisible = std::max(dialogAreaVisible, intersection.GetWidth() * intersection.GetHeight()); - } - - if (dialogAreaVisible > 0.1 * dialogAreaTotal) //at least 10% of the dialog should be visible! - SetSize(wxRect(globalSettings.gui.dlgPos, globalSettings.gui.dlgSize)); - else - { - SetSize(wxRect(globalSettings.gui.dlgSize)); - Centre(); - } - } - else - Centre(); - - Maximize(globalSettings.gui.isMaximized); - - //set column attributes - m_gridMainL ->setColumnConfig(gridview::convertConfig(globalSettings.gui.columnAttribLeft)); - m_gridMainR ->setColumnConfig(gridview::convertConfig(globalSettings.gui.columnAttribRight)); - m_splitterMain->setSashOffset(globalSettings.gui.sashOffset); - - m_gridNavi->setColumnConfig(treeview::convertConfig(globalSettings.gui.columnAttribNavi)); - treeview::setShowPercentage(*m_gridNavi, globalSettings.gui.showPercentBar); - - treeDataView->setSortDirection(globalSettings.gui.naviLastSortColumn, globalSettings.gui.naviLastSortAscending); - - //-------------------------------------------------------------------------------- - //load list of last used configuration files - //config_history::setItems(*m_gridConfigHistory, globalSettings.gui.lastUsedConfigFiles2); - - std::vector<Zstring> cfgFileNames; - std::transform(globalSettings.gui.cfgFileHistory.rbegin(), globalSettings.gui.cfgFileHistory.rend(), std::back_inserter(cfgFileNames), - [](const ConfigHistoryItem& item) { return item.configFile; }); - //list is stored with last used files first in xml, however addFileToCfgHistory() needs them last!!! - - cfgFileNames.push_back(lastRunConfigName()); //make sure <Last session> is always part of history list (if existing) - addFileToCfgHistory(cfgFileNames); - - removeObsoleteCfgHistoryItems(cfgFileNames); //remove non-existent items (we need this only on startup) - //-------------------------------------------------------------------------------- - - //load list of last used folders - *folderHistoryLeft = FolderHistory(globalSettings.gui.folderHistoryLeft, globalSettings.gui.folderHistMax); - *folderHistoryRight = FolderHistory(globalSettings.gui.folderHistoryRight, globalSettings.gui.folderHistMax); - - //show/hide file icons - gridview::setupIcons(*m_gridMainL, *m_gridMainC, *m_gridMainR, globalSettings.gui.showIcons, convert(globalSettings.gui.iconSize)); - - //------------------------------------------------------------------------------------------------ - m_checkBoxMatchCase->SetValue(globalCfg.gui.textSearchRespectCase); - - //wxAuiManager erroneously loads panel captions, we don't want that - typedef std::vector<std::pair<wxString, wxString>> CaptionNameMapping; - CaptionNameMapping captionNameMap; - const wxAuiPaneInfoArray& paneArray = auiMgr.GetAllPanes(); - for (size_t i = 0; i < paneArray.size(); ++i) - captionNameMap.push_back(std::make_pair(paneArray[i].caption, paneArray[i].name)); - - auiMgr.LoadPerspective(globalSettings.gui.guiPerspectiveLast); - - //restore original captions - for (auto it = captionNameMap.begin(); it != captionNameMap.end(); ++it) - auiMgr.GetPane(it->second).Caption(it->first); - - //if MainDialog::onQueryEndSession() is called while comparison is active, this panel is saved and restored as "visible" - auiMgr.GetPane(compareStatus->getAsWindow()).Hide(); - - auiMgr.GetPane(m_panelSearch).Hide(); //no need to show it on startup - - m_menuItemCheckVersionAuto->Check(globalCfg.gui.lastUpdateCheck != -1); - - auiMgr.Update(); -} - - -xmlAccess::XmlGlobalSettings MainDialog::getGlobalCfgBeforeExit() -{ - Freeze(); //no need to Thaw() again!! - - xmlAccess::XmlGlobalSettings globalSettings = globalCfg; - - globalSettings.programLanguage = getLanguage(); - - //retrieve column attributes - globalSettings.gui.columnAttribLeft = gridview::convertConfig(m_gridMainL->getColumnConfig()); - globalSettings.gui.columnAttribRight = gridview::convertConfig(m_gridMainR->getColumnConfig()); - globalSettings.gui.sashOffset = m_splitterMain->getSashOffset(); - - globalSettings.gui.columnAttribNavi = treeview::convertConfig(m_gridNavi->getColumnConfig()); - globalSettings.gui.showPercentBar = treeview::getShowPercentage(*m_gridNavi); - - const std::pair<ColumnTypeNavi, bool> sortInfo = treeDataView->getSortDirection(); - globalSettings.gui.naviLastSortColumn = sortInfo.first; - globalSettings.gui.naviLastSortAscending = sortInfo.second; - - //-------------------------------------------------------------------------------- - //write list of last used configuration files - std::map<int, Zstring> historyDetail; //(cfg-file/last use index) - for (unsigned int i = 0; i < m_listBoxHistory->GetCount(); ++i) - if (auto clientString = dynamic_cast<const wxClientHistoryData*>(m_listBoxHistory->GetClientObject(i))) - historyDetail.insert(std::make_pair(clientString->lastUseIndex_, clientString->cfgFile_)); - else - assert(false); - - //sort by last use; put most recent items *first* (looks better in xml than the reverse) - std::vector<ConfigHistoryItem> history; - std::transform(historyDetail.rbegin(), historyDetail.rend(), std::back_inserter(history), [](const std::pair<const int, Zstring>& item) { return ConfigHistoryItem(item.second); }); - - if (history.size() > globalSettings.gui.cfgFileHistMax) //erase oldest elements - history.resize(globalSettings.gui.cfgFileHistMax); - - globalSettings.gui.cfgFileHistory = history; - //-------------------------------------------------------------------------------- - globalSettings.gui.lastUsedConfigFiles = activeConfigFiles; - //globalSettings.gui.lastUsedConfigFiles2 = config_history::getItems(*m_gridConfigHistory); - - //write list of last used folders - globalSettings.gui.folderHistoryLeft = folderHistoryLeft ->getList(); - globalSettings.gui.folderHistoryRight = folderHistoryRight->getList(); - - globalSettings.gui.textSearchRespectCase = m_checkBoxMatchCase->GetValue(); - - globalSettings.gui.guiPerspectiveLast = auiMgr.SavePerspective(); - - //we need to portably retrieve non-iconized, non-maximized size and position (non-portable: GetWindowPlacement()) - //call *after* wxAuiManager::SavePerspective()! - if (IsIconized()) - Iconize(false); - - globalSettings.gui.isMaximized = IsMaximized(); //evaluate AFTER uniconizing! - - if (IsMaximized()) - Maximize(false); - - globalSettings.gui.dlgSize = GetSize(); - globalSettings.gui.dlgPos = GetPosition(); - - return globalSettings; -} - - -void MainDialog::setSyncDirManually(const std::vector<FileSystemObject*>& selection, SyncDirection direction) -{ - if (!selection.empty()) - { - std::for_each(selection.begin(), selection.end(), - [&](FileSystemObject* fsObj) - { - setSyncDirectionRec(direction, *fsObj); //set new direction (recursively) - zen::setActiveStatus(true, *fsObj); //works recursively for directories - }); - - updateGui(); - } -} - - -void MainDialog::setFilterManually(const std::vector<FileSystemObject*>& selection, bool setIncluded) -{ - //if hidefiltered is active, there should be no filtered elements on screen => current element was filtered out - assert(!currentCfg.hideExcludedItems || !setIncluded); - - if (!selection.empty()) - { - std::for_each(selection.begin(), selection.end(), - [&](FileSystemObject* fsObj) { zen::setActiveStatus(setIncluded, *fsObj); }); //works recursively for directories - - updateGuiDelayedIf(currentCfg.hideExcludedItems); //show update GUI before removing rows - } -} - - -namespace -{ -//perf: wxString doesn't model exponential growth and so is unusable for large data sets -typedef Zbase<wchar_t> zxString; //guaranteed exponential growth -} - -void MainDialog::copySelectionToClipboard(const std::vector<const Grid*>& gridRefs) -{ - try - { - zxString clipboardString; - - auto addSelection = [&](const Grid& grid) - { - if (auto prov = grid.getDataProvider()) - { - std::vector<Grid::ColumnAttribute> colAttr = grid.getColumnConfig(); - vector_remove_if(colAttr, [](const Grid::ColumnAttribute& ca) { return !ca.visible_; }); - if (!colAttr.empty()) - { - const std::vector<size_t> selection = grid.getSelectedRows(); - std::for_each(selection.begin(), selection.end(), - [&](size_t row) - { - std::for_each(colAttr.begin(), colAttr.end() - 1, - [&](const Grid::ColumnAttribute& ca) - { - clipboardString += copyStringTo<zxString>(prov->getValue(row, ca.type_)); - clipboardString += L'\t'; - }); - clipboardString += copyStringTo<zxString>(prov->getValue(row, colAttr.back().type_)); - clipboardString += L'\n'; - }); - } - } - }; - - for (auto it = gridRefs.begin(); it != gridRefs.end(); ++it) - addSelection(**it); - - //finally write to clipboard - if (!clipboardString.empty()) - if (wxClipboard::Get()->Open()) - { - ZEN_ON_SCOPE_EXIT(wxClipboard::Get()->Close()); - wxClipboard::Get()->SetData(new wxTextDataObject(copyStringTo<wxString>(clipboardString))); //ownership passed - } - } - catch (const std::bad_alloc& e) - { - showNotificationDialog(this, DialogInfoType::ERROR2, PopupDialogCfg().setMainInstructions(_("Out of memory.") + L" " + utfCvrtTo<std::wstring>(e.what()))); - } -} - - -std::vector<FileSystemObject*> MainDialog::getGridSelection(bool fromLeft, bool fromRight) const -{ - std::set<size_t> selectedRows; - - auto addSelection = [&](const Grid& grid) - { - const std::vector<size_t>& sel = grid.getSelectedRows(); - selectedRows.insert(sel.begin(), sel.end()); - }; - - if (fromLeft) - addSelection(*m_gridMainL); - - if (fromRight) - addSelection(*m_gridMainR); - - return gridDataView->getAllFileRef(selectedRows); -} - - -std::vector<FileSystemObject*> MainDialog::getTreeSelection() const -{ - std::vector<FileSystemObject*> output; - - const std::vector<size_t>& sel = m_gridNavi->getSelectedRows(); - std::for_each(sel.begin(), sel.end(), - [&](size_t row) - { - if (std::unique_ptr<TreeView::Node> node = treeDataView->getLine(row)) - { - if (auto root = dynamic_cast<const TreeView::RootNode*>(node.get())) - { - //select first level of child elements - for (auto& fsObj : root->baseDirObj_.refSubDirs ()) output.push_back(&fsObj); - for (auto& fsObj : root->baseDirObj_.refSubFiles()) output.push_back(&fsObj); - for (auto& fsObj : root->baseDirObj_.refSubLinks()) output.push_back(&fsObj); - } - else if (auto dir = dynamic_cast<const TreeView::DirNode*>(node.get())) - output.push_back(&(dir->dirObj_)); - else if (auto file = dynamic_cast<const TreeView::FilesNode*>(node.get())) - output.insert(output.end(), file->filesAndLinks_.begin(), file->filesAndLinks_.end()); - } - }); - return output; -} - - -//Exception class used to abort the "compare" and "sync" process -class AbortDeleteProcess {}; - -class ManualDeletionHandler : private wxEvtHandler, public DeleteFilesHandler //throw AbortDeleteProcess -{ -public: - ManualDeletionHandler(MainDialog& main) : - mainDlg(main), - abortRequested(false), - ignoreErrors(false) - { - mainDlg.disableAllElements(true); //disable everything except abort button - - //register abort button - mainDlg.m_buttonCancel->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ManualDeletionHandler::OnAbortDeletion), nullptr, this ); - mainDlg.Connect(wxEVT_CHAR_HOOK, wxKeyEventHandler(ManualDeletionHandler::OnKeyPressed), nullptr, this); - } - - ~ManualDeletionHandler() - { - //de-register abort button - mainDlg.Disconnect(wxEVT_CHAR_HOOK, wxKeyEventHandler(ManualDeletionHandler::OnKeyPressed), nullptr, this); - mainDlg.m_buttonCancel->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ManualDeletionHandler::OnAbortDeletion ), nullptr, this ); - - mainDlg.enableAllElements(); - } - - virtual Response reportError(const std::wstring& msg) - { - if (ignoreErrors) - return DeleteFilesHandler::IGNORE_ERROR; - - forceUiRefresh(); - bool ignoreNextErrors = false; - switch (showConfirmationDialog3(&mainDlg, DialogInfoType::ERROR2, PopupDialogCfg3(). - setDetailInstructions(msg). - setCheckBox(ignoreNextErrors, _("&Ignore subsequent errors"), ConfirmationButton3::DONT_DO_IT), - _("&Ignore"), _("&Retry"))) - { - case ConfirmationButton3::DO_IT: //ignore - ignoreErrors = ignoreNextErrors; - return DeleteFilesHandler::IGNORE_ERROR; - - case ConfirmationButton3::DONT_DO_IT: //retry - return DeleteFilesHandler::RETRY; - - case ConfirmationButton3::CANCEL: - throw AbortDeleteProcess(); - } - - assert (false); - return DeleteFilesHandler::IGNORE_ERROR; //dummy return value - } - - virtual void reportWarning(const std::wstring& msg, bool& warningActive) - { - if (!warningActive || ignoreErrors) - return; - - forceUiRefresh(); - bool dontWarnAgain = false; - switch (showConfirmationDialog(&mainDlg, DialogInfoType::WARNING, PopupDialogCfg(). - setDetailInstructions(msg). - setCheckBox(dontWarnAgain, _("&Don't show this warning again")), _("&Ignore"))) - { - case ConfirmationButton::DO_IT: - warningActive = !dontWarnAgain; - break; - case ConfirmationButton::CANCEL: - throw AbortDeleteProcess(); - } - } - - virtual void reportStatus (const std::wstring& msg) - { - statusMsg = msg; - requestUiRefresh(); - } - -private: - virtual void requestUiRefresh() - { - if (updateUiIsAllowed()) //test if specific time span between ui updates is over - forceUiRefresh(); - - if (abortRequested) //test after (implicit) call to wxApp::Yield() - throw AbortDeleteProcess(); - } - - void forceUiRefresh() - { - //std::wstring msg = toGuiString(deletionCount) + - mainDlg.setStatusBarFullText(statusMsg); - wxTheApp->Yield(); - } - - //context: C callstack message loop => throw()! - void OnAbortDeletion(wxCommandEvent& event) //handle abort button click - { - abortRequested = true; //don't throw exceptions in a GUI-Callback!!! (throw zen::AbortThisProcess()) - } - - void OnKeyPressed(wxKeyEvent& event) - { - const int keyCode = event.GetKeyCode(); - if (keyCode == WXK_ESCAPE) - { - abortRequested = true; //don't throw exceptions in a GUI-Callback!!! (throw zen::AbortThisProcess()) - return; - } - - event.Skip(); - } - - MainDialog& mainDlg; - - bool abortRequested; - bool ignoreErrors; - //size_t deletionCount; // - std::wstring statusMsg; //status reporting -}; - - -void MainDialog::deleteSelectedFiles(const std::vector<FileSystemObject*>& selectionLeft, - const std::vector<FileSystemObject*>& selectionRight) -{ - bool deleteOnBothSides = false; //let's keep this disabled by default -> don't save - //=> clenup empty selection on either side: - std::vector<FileSystemObject*> selectionLeftTmp; - std::vector<FileSystemObject*> selectionRightTmp; - std::copy_if(selectionLeft .begin(), selectionLeft .end(), std::back_inserter(selectionLeftTmp ), [](const FileSystemObject* fsObj) { return !fsObj->isEmpty<LEFT_SIDE >(); }); - std::copy_if(selectionRight.begin(), selectionRight.end(), std::back_inserter(selectionRightTmp), [](const FileSystemObject* fsObj) { return !fsObj->isEmpty<RIGHT_SIDE>(); }); - - if (!selectionLeftTmp.empty() || !selectionRightTmp.empty()) - { - wxWindow* oldFocus = wxWindow::FindFocus(); - ZEN_ON_SCOPE_EXIT(if (oldFocus) oldFocus->SetFocus();) - - if (zen::showDeleteDialog(this, - selectionLeftTmp, - selectionRightTmp, - deleteOnBothSides, - globalCfg.gui.useRecyclerForManualDeletion) == ReturnSmallDlg::BUTTON_OKAY) - { - //wxBusyCursor dummy; -> redundant: progress already shown in status bar! - try - { - //handle errors when deleting files/folders - ManualDeletionHandler statusHandler(*this); - - zen::deleteFromGridAndHD(selectionLeftTmp, - selectionRightTmp, - folderCmp, - extractDirectionCfg(getConfig().mainCfg), - deleteOnBothSides, - globalCfg.gui.useRecyclerForManualDeletion, - statusHandler, - globalCfg.optDialogs.warningRecyclerMissing); - - gridview::clearSelection(*m_gridMainL, *m_gridMainC, *m_gridMainR); //do not clear, if aborted! - } - catch (AbortDeleteProcess&) {} - - //remove rows that are empty: just a beautification, invalid rows shouldn't cause issues - gridDataView->removeInvalidRows(); - - //redraw grid neccessary to update new dimensions and for UI-Backend data linkage - updateGui(); //call immediately after deleteFromGridAndHD!!! - } - } -} - - -namespace -{ -template <SelectedSide side> -Zstring getExistingParentFolder(const FileSystemObject& fsObj) -{ - const DirPair* dirObj = dynamic_cast<const DirPair*>(&fsObj); - if (!dirObj) - dirObj = dynamic_cast<const DirPair*>(&fsObj.parent()); - - while (dirObj) - { - if (!dirObj->isEmpty<side>()) - return dirObj->getFullName<side>(); - - dirObj = dynamic_cast<const DirPair*>(&dirObj->parent()); - } - return fsObj.getBaseDirPf<side>(); -} -} - -void MainDialog::openExternalApplication(const wxString& commandline, const std::vector<FileSystemObject*>& selection, bool leftSide) -{ - if (commandline.empty()) - return; - - auto selectionTmp = selection; - - const bool openFileBrowserRequested = [&]() -> bool - { - xmlAccess::XmlGlobalSettings::Gui dummy; - return !dummy.externelApplications.empty() && dummy.externelApplications[0].second == commandline; - }(); - - //support fallback instead of an error in this special case - if (openFileBrowserRequested) - { - if (selectionTmp.size() > 1) //do not open more than one explorer instance! - selectionTmp.resize(1); // - - if (selectionTmp.empty() || - (leftSide && selectionTmp[0]->isEmpty<LEFT_SIDE >()) || - (!leftSide && selectionTmp[0]->isEmpty<RIGHT_SIDE>())) - { - Zstring fallbackDir; - if (selectionTmp.empty()) - fallbackDir = leftSide ? - getFormattedDirectoryName(firstFolderPair->getLeftDir()) : - getFormattedDirectoryName(firstFolderPair->getRightDir()); - - else - fallbackDir = leftSide ? - getExistingParentFolder<LEFT_SIDE >(*selectionTmp[0]) : - getExistingParentFolder<RIGHT_SIDE>(*selectionTmp[0]); - - try - { -#ifdef ZEN_WIN - shellExecute2(L"\"" + fallbackDir + L"\"", EXEC_TYPE_ASYNC); //throw FileError -#elif defined ZEN_LINUX - shellExecute2("xdg-open \"" + fallbackDir + "\"", EXEC_TYPE_ASYNC); // -#elif defined ZEN_MAC - shellExecute2("open \"" + fallbackDir + "\"", EXEC_TYPE_ASYNC); // -#endif - } - catch (const FileError& e) { showNotificationDialog(this, DialogInfoType::ERROR2, PopupDialogCfg().setDetailInstructions(e.toString())); } - return; - } - } - - const size_t massInvokeThreshold = 10; //more than this is likely a user mistake - - if (selectionTmp.size() > massInvokeThreshold) - if (globalCfg.optDialogs.confirmExternalCommandMassInvoke) - { - bool dontAskAgain = false; - switch (showConfirmationDialog(this, DialogInfoType::WARNING, PopupDialogCfg(). - setTitle(_("Confirm")). - setMainInstructions(replaceCpy(_P("Do you really want to execute the command %y for one item?", - "Do you really want to execute the command %y for %x items?", selectionTmp.size()), - L"%y", L'\"' + commandline + L'\"')). - setCheckBox(dontAskAgain, _("&Don't show this warning again")), - _("&Execute"))) - { - case ConfirmationButton::DO_IT: - globalCfg.optDialogs.confirmExternalCommandMassInvoke = !dontAskAgain; - break; - case ConfirmationButton::CANCEL: - return; - } - } - - //regular command evaluation - for (const FileSystemObject* fsObj : selectionTmp) //context menu calls this function only if selection is not empty! - { - Zstring path1 = fsObj->getBaseDirPf<LEFT_SIDE>() + fsObj->getObjRelativeName(); //full path, even if item is not existing! - Zstring dir1 = beforeLast(path1, FILE_NAME_SEPARATOR); //Win: wrong for root paths like "C:\file.txt" - - Zstring path2 = fsObj->getBaseDirPf<RIGHT_SIDE>() + fsObj->getObjRelativeName(); - Zstring dir2 = beforeLast(path2, FILE_NAME_SEPARATOR); - - if (!leftSide) - { - std::swap(path1, path2); - std::swap(dir1, dir2); - } - - Zstring command = utfCvrtTo<Zstring>(commandline); - replace(command, Zstr("%item_path%"), path1); - replace(command, Zstr("%item2_path%"), path2); - replace(command, Zstr("%item_folder%"), dir1 ); - replace(command, Zstr("%item2_folder%"), dir2 ); - - auto cmdExp = expandMacros(command); - try - { - //caveat: spawning too many threads asynchronously can easily kill a user's desktop session on Ubuntu! - shellExecute2(cmdExp, selectionTmp.size() > massInvokeThreshold ? EXEC_TYPE_SYNC : EXEC_TYPE_ASYNC); //throw FileError - } - catch (const FileError& e) { showNotificationDialog(this, DialogInfoType::ERROR2, PopupDialogCfg().setDetailInstructions(e.toString())); } - } -} - - -void MainDialog::setStatusBarFileStatistics(size_t filesOnLeftView, - size_t foldersOnLeftView, - size_t filesOnRightView, - size_t foldersOnRightView, - zen::UInt64 filesizeLeftView, - zen::UInt64 filesizeRightView) -{ -#ifdef ZEN_WIN - wxWindowUpdateLocker dummy(m_panelStatusBar); //leads to GUI corruption problems on Linux/OS X! -#endif - - //select state - bSizerFileStatus->Show(true); - m_staticTextFullStatus->Hide(); - - //update status information - bSizerStatusLeftDirectories->Show(foldersOnLeftView > 0); - bSizerStatusLeftFiles ->Show(filesOnLeftView > 0); - - setText(*m_staticTextStatusLeftDirs, _P("1 directory", "%x directories", foldersOnLeftView)); - setText(*m_staticTextStatusLeftFiles, _P("1 file", "%x files", filesOnLeftView)); - setText(*m_staticTextStatusLeftBytes, L"(" + filesizeToShortString(to<Int64>(filesizeLeftView)) + L")"); - //------------------------------------------------------------------------------ - bSizerStatusRightDirectories->Show(foldersOnRightView > 0); - bSizerStatusRightFiles ->Show(filesOnRightView > 0); - - setText(*m_staticTextStatusRightDirs, _P("1 directory", "%x directories", foldersOnRightView)); - setText(*m_staticTextStatusRightFiles, _P("1 file", "%x files", filesOnRightView)); - setText(*m_staticTextStatusRightBytes, L"(" + filesizeToShortString(to<Int64>(filesizeRightView)) + L")"); - //------------------------------------------------------------------------------ - wxString statusMiddleNew; - if (gridDataView->rowsTotal() > 0) - { - statusMiddleNew = _P("%y of 1 row in view", "%y of %x rows in view", gridDataView->rowsTotal()); - replace(statusMiddleNew, L"%y", toGuiString(gridDataView->rowsOnView()), false); //%x is already used as plural form placeholder! - } - - //fill middle text (considering flashStatusInformation()) - if (oldStatusMsgs.empty()) - setText(*m_staticTextStatusMiddle, statusMiddleNew); - else - oldStatusMsgs.front() = statusMiddleNew; - - m_panelStatusBar->Layout(); -} - - -void MainDialog::setStatusBarFullText(const wxString& msg) -{ - const bool needLayoutUpdate = !m_staticTextFullStatus->IsShown(); - //select state - bSizerFileStatus->Show(false); - m_staticTextFullStatus->Show(); - - //update status information - setText(*m_staticTextFullStatus, msg); - m_panelStatusBar->Layout(); - - if (needLayoutUpdate) - auiMgr.Update(); //fix status bar height (needed on OS X) -} - - -void MainDialog::flashStatusInformation(const wxString& text) -{ - oldStatusMsgs.push_back(m_staticTextStatusMiddle->GetLabel()); - - m_staticTextStatusMiddle->SetLabel(text); - m_staticTextStatusMiddle->SetForegroundColour(wxColour(31, 57, 226)); //highlight color: blue - m_staticTextStatusMiddle->SetFont(m_staticTextStatusMiddle->GetFont().Bold()); - - m_panelStatusBar->Layout(); - //if (needLayoutUpdate) auiMgr.Update(); -> not needed here, this is called anyway in updateGui() - - processAsync2([] { boost::this_thread::sleep(boost::posix_time::millisec(2500)); }, - [this] { this->restoreStatusInformation(); }); -} - - -void MainDialog::restoreStatusInformation() -{ - if (!oldStatusMsgs.empty()) - { - wxString oldMsg = oldStatusMsgs.back(); - oldStatusMsgs.pop_back(); - - if (oldStatusMsgs.empty()) //restore original status text - { - m_staticTextStatusMiddle->SetLabel(oldMsg); - m_staticTextStatusMiddle->SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); //reset color - - wxFont fnt = m_staticTextStatusMiddle->GetFont(); - fnt.SetWeight(wxFONTWEIGHT_NORMAL); - m_staticTextStatusMiddle->SetFont(fnt); - - m_panelStatusBar->Layout(); - } - } -} - - -void MainDialog::onProcessAsyncTasks(wxEvent& event) -{ - //schedule and run long-running tasks asynchronously - asyncTasks.evalResults(); //process results on GUI queue - if (asyncTasks.empty()) - timerForAsyncTasks.Stop(); -} - - -void MainDialog::disableAllElements(bool enableAbort) -{ - //disables all elements (except abort button) that might receive user input during long-running processes: - //when changing consider: comparison, synchronization, manual deletion - - EnableCloseButton(false); //not allowed for synchronization! progress indicator is top window! -> not honored on OS X! - - //OS X: wxWidgets portability promise is again a mess: http://wxwidgets.10942.n7.nabble.com/Disable-panel-and-appropriate-children-windows-linux-macos-td35357.html - - m_menubar1->EnableTop(0, false); - m_menubar1->EnableTop(1, false); - m_menubar1->EnableTop(2, false); - m_bpButtonCmpConfig ->Disable(); - m_bpButtonSyncConfig ->Disable(); - m_buttonSync ->Disable(); - m_panelDirectoryPairs->Disable(); - m_splitterMain ->Disable(); //includes m_panelCenter, but not m_panelStatusBar! - m_panelViewFilter ->Disable(); - m_panelFilter ->Disable(); - m_panelConfig ->Disable(); - m_panelStatistics ->Disable(); - m_gridNavi ->Disable(); - - if (enableAbort) - { - //show abort button - m_buttonCancel->Enable(); - m_buttonCancel->Show(); - if (m_buttonCancel->IsShownOnScreen()) - m_buttonCancel->SetFocus(); - m_buttonCompare->Disable(); - m_buttonCompare->Hide(); - m_panelTopButtons->Layout(); - } - else - m_panelTopButtons->Disable(); -} - - -void MainDialog::enableAllElements() -{ - //wxGTK, yet another QOI issue: some stupid bug, keeps moving main dialog to top!! - - EnableCloseButton(true); - - m_menubar1->EnableTop(0, true); - m_menubar1->EnableTop(1, true); - m_menubar1->EnableTop(2, true); - m_bpButtonCmpConfig ->Enable(); - m_bpButtonSyncConfig ->Enable(); - m_buttonSync ->Enable(); - m_panelDirectoryPairs->Enable(); - m_splitterMain ->Enable(); - m_panelViewFilter ->Enable(); - m_panelFilter ->Enable(); - m_panelConfig ->Enable(); - m_panelStatistics ->Enable(); - m_gridNavi ->Enable(); - - //show compare button - m_buttonCancel->Disable(); - m_buttonCancel->Hide(); - m_buttonCompare->Enable(); - m_buttonCompare->Show(); - - m_panelTopButtons->Enable(); - m_panelTopButtons->Layout(); - - //at least wxWidgets on OS X fails to do this after enabling: - Refresh(); -} - - -namespace -{ -void updateSizerOrientation(wxBoxSizer& sizer, wxWindow& window, double horizontalWeight) -{ - const int newOrientation = window.GetSize().GetWidth() * horizontalWeight > window.GetSize().GetHeight() ? wxHORIZONTAL : wxVERTICAL; //check window NOT sizer width! - if (sizer.GetOrientation() != newOrientation) - { - sizer.SetOrientation(newOrientation); - window.Layout(); - } -} -} - - -void MainDialog::OnResizeConfigPanel(wxEvent& event) -{ - updateSizerOrientation(*bSizerConfig, *m_panelConfig, 0.5); - event.Skip(); -} - - -void MainDialog::OnResizeViewPanel(wxEvent& event) -{ - updateSizerOrientation(*bSizerViewFilter, *m_panelViewFilter, 1.0); - event.Skip(); -} - - -void MainDialog::OnResizeStatisticsPanel(wxEvent& event) -{ - //updateSizerOrientation(*bSizerStatistics, *m_panelStatistics); - - //we need something more fancy: - const int parentOrient = m_panelStatistics->GetSize().GetWidth() > m_panelStatistics->GetSize().GetHeight() ? wxHORIZONTAL : wxVERTICAL; //check window NOT sizer width! - if (bSizerStatistics->GetOrientation() != parentOrient) - { - bSizerStatistics->SetOrientation(parentOrient); - - //apply opposite orientation for child sizers - const int childOrient = parentOrient == wxHORIZONTAL ? wxVERTICAL : wxHORIZONTAL; - wxSizerItemList& sl = bSizerStatistics->GetChildren(); - for (auto it = sl.begin(); it != sl.end(); ++it) //yet another wxWidgets bug keeps us from using std::for_each - { - wxSizerItem& szItem = **it; - if (auto sizerChild = dynamic_cast<wxBoxSizer*>(szItem.GetSizer())) - if (sizerChild->GetOrientation() != childOrient) - sizerChild->SetOrientation(childOrient); - } - m_panelStatistics->Layout(); - } - - event.Skip(); -} - - -void MainDialog::OnResizeLeftFolderWidth(wxEvent& event) -{ - //adapt left-shift display distortion caused by scrollbars for multiple folder pairs - const int width = m_panelTopLeft->GetSize().GetWidth(); - std::for_each(additionalFolderPairs.begin(), additionalFolderPairs.end(), - [&](FolderPairPanel* panel) - { - panel->m_panelLeft->SetMinSize(wxSize(width, -1)); - }); - - event.Skip(); -} - - -void MainDialog::onTreeButtonEvent(wxKeyEvent& event) -{ - int keyCode = event.GetKeyCode(); - if (m_gridNavi->GetLayoutDirection() == wxLayout_RightToLeft) - { - if (keyCode == WXK_LEFT) - keyCode = WXK_RIGHT; - else if (keyCode == WXK_RIGHT) - keyCode = WXK_LEFT; - else if (keyCode == WXK_NUMPAD_LEFT) - keyCode = WXK_NUMPAD_RIGHT; - else if (keyCode == WXK_NUMPAD_RIGHT) - keyCode = WXK_NUMPAD_LEFT; - } - - if (event.ControlDown()) - switch (keyCode) - { - case 'C': - case WXK_INSERT: //CTRL + C || CTRL + INS - { - std::vector<const Grid*> gridRefs; - gridRefs.push_back(m_gridNavi); - copySelectionToClipboard(gridRefs); - } - return; - } - else if (event.AltDown()) - switch (keyCode) - { - case WXK_NUMPAD_LEFT: - case WXK_LEFT: //ALT + <- - setSyncDirManually(getTreeSelection(), SyncDirection::LEFT); - return; - - case WXK_NUMPAD_RIGHT: - case WXK_RIGHT: //ALT + -> - setSyncDirManually(getTreeSelection(), SyncDirection::RIGHT); - return; - - case WXK_NUMPAD_UP: - case WXK_NUMPAD_DOWN: - case WXK_UP: /* ALT + /|\ */ - case WXK_DOWN: /* ALT + \|/ */ - setSyncDirManually(getTreeSelection(), SyncDirection::NONE); - return; - } - - else - switch (keyCode) - { - case WXK_SPACE: - case WXK_NUMPAD_SPACE: - { - const std::vector<FileSystemObject*>& selection = getTreeSelection(); - if (!selection.empty()) - setFilterManually(selection, !selection[0]->isActive()); - } - return; - - case WXK_DELETE: - case WXK_NUMPAD_DELETE: - deleteSelectedFiles(getTreeSelection(), getTreeSelection()); - return; - } - - event.Skip(); //unknown keypress: propagate -} - - -void MainDialog::onGridButtonEventL(wxKeyEvent& event) -{ - onGridButtonEvent(event, *m_gridMainL, true); -} -void MainDialog::onGridButtonEventC(wxKeyEvent& event) -{ - onGridButtonEvent(event, *m_gridMainC, true); -} -void MainDialog::onGridButtonEventR(wxKeyEvent& event) -{ - onGridButtonEvent(event, *m_gridMainR, false); -} - -void MainDialog::onGridButtonEvent(wxKeyEvent& event, Grid& grid, bool leftSide) -{ - int keyCode = event.GetKeyCode(); - if (grid.GetLayoutDirection() == wxLayout_RightToLeft) - { - if (keyCode == WXK_LEFT) - keyCode = WXK_RIGHT; - else if (keyCode == WXK_RIGHT) - keyCode = WXK_LEFT; - else if (keyCode == WXK_NUMPAD_LEFT) - keyCode = WXK_NUMPAD_RIGHT; - else if (keyCode == WXK_NUMPAD_RIGHT) - keyCode = WXK_NUMPAD_LEFT; - } - - if (event.ControlDown()) - switch (keyCode) - { - case 'C': - case WXK_INSERT: //CTRL + C || CTRL + INS - { - std::vector<const Grid*> gridRefs; - gridRefs.push_back(m_gridMainL); - gridRefs.push_back(m_gridMainR); - copySelectionToClipboard(gridRefs); - } - return; // -> swallow event! don't allow default grid commands! - } - - else if (event.AltDown()) - switch (keyCode) - { - case WXK_NUMPAD_LEFT: - case WXK_LEFT: //ALT + <- - setSyncDirManually(getGridSelection(), SyncDirection::LEFT); - return; - - case WXK_NUMPAD_RIGHT: - case WXK_RIGHT: //ALT + -> - setSyncDirManually(getGridSelection(), SyncDirection::RIGHT); - return; - - case WXK_NUMPAD_UP: - case WXK_NUMPAD_DOWN: - case WXK_UP: /* ALT + /|\ */ - case WXK_DOWN: /* ALT + \|/ */ - setSyncDirManually(getGridSelection(), SyncDirection::NONE); - return; - } - - else - switch (keyCode) - { - case WXK_DELETE: - case WXK_NUMPAD_DELETE: - deleteSelectedFiles(getGridSelection(true, false), - getGridSelection(false, true)); - return; - - case WXK_SPACE: - case WXK_NUMPAD_SPACE: - { - const std::vector<FileSystemObject*>& selection = getGridSelection(); - if (!selection.empty()) - setFilterManually(selection, !selection[0]->isActive()); - } - return; - - case WXK_RETURN: - case WXK_NUMPAD_ENTER: - if (!globalCfg.gui.externelApplications.empty()) - openExternalApplication(globalCfg.gui.externelApplications[0].second, //open with first external application - getGridSelection(), leftSide); - return; - } - - event.Skip(); //unknown keypress: propagate -} - - -bool isComponentOf(const wxWindow* child, const wxWindow* top) -{ - for (const wxWindow* wnd = child; wnd != nullptr; wnd = wnd->GetParent()) - if (wnd == top) - return true; - return false; -} - - -void MainDialog::OnGlobalKeyEvent(wxKeyEvent& event) //process key events without explicit menu entry :) -{ - //avoid recursion!!! -> this ugly construct seems to be the only (portable) way to avoid re-entrancy - //recursion may happen in multiple situations: e.g. modal dialogs, wxGrid::ProcessEvent()! - if (processingGlobalKeyEvent || - !IsShown() || - !IsActive() || - !IsEnabled() || - !m_gridMainL->IsEnabled() || // - !m_gridMainC->IsEnabled() || //only handle if main window is in use - !m_gridMainR->IsEnabled()) // - { - event.Skip(); - return; - } - - processingGlobalKeyEvent = true; - ZEN_ON_SCOPE_EXIT(processingGlobalKeyEvent = false;) - //---------------------------------------------------- - - const int keyCode = event.GetKeyCode(); - - //CTRL + X - //if (event.ControlDown()) - // switch (keyCode) - // { - // case 'F': //CTRL + F - // showFindPanel(); - // return; //-> swallow event! - // } - - switch (keyCode) - { - case WXK_F3: - case WXK_NUMPAD_F3: - startFindNext(); - return; //-> swallow event! - - case WXK_F6: - { - wxCommandEvent dummy2(wxEVT_COMMAND_BUTTON_CLICKED); //simulate button click - if (wxEvtHandler* evtHandler = m_bpButtonCmpConfig->GetEventHandler()) - evtHandler->ProcessEvent(dummy2); //synchronous call - } - return; //-> swallow event! - - case WXK_F7: - { - wxCommandEvent dummy2(wxEVT_COMMAND_BUTTON_CLICKED); //simulate button click - if (wxEvtHandler* evtHandler = m_bpButtonSyncConfig->GetEventHandler()) - evtHandler->ProcessEvent(dummy2); //synchronous call - } - return; //-> swallow event! - - case WXK_F9: - setViewTypeSyncAction(!m_bpButtonViewTypeSyncAction->isActive()); - return; //-> swallow event! - - case WXK_F10: - { - wxCommandEvent dummy2(wxEVT_COMMAND_BUTTON_CLICKED); //simulate button click - if (wxEvtHandler* evtHandler = m_bpButtonFilter->GetEventHandler()) - evtHandler->ProcessEvent(dummy2); //synchronous call - } - return; //-> swallow event! - - //redirect certain (unhandled) keys directly to grid! - case WXK_UP: - case WXK_DOWN: - case WXK_LEFT: - case WXK_RIGHT: - case WXK_NUMPAD_UP: - case WXK_NUMPAD_DOWN: - case WXK_NUMPAD_LEFT: - case WXK_NUMPAD_RIGHT: - - case WXK_PAGEUP: - case WXK_PAGEDOWN: - case WXK_HOME: - case WXK_END: - case WXK_NUMPAD_PAGEUP: - case WXK_NUMPAD_PAGEDOWN: - case WXK_NUMPAD_HOME: - case WXK_NUMPAD_END: - { - const wxWindow* focus = wxWindow::FindFocus(); - if (!isComponentOf(focus, m_gridMainL ) && // - !isComponentOf(focus, m_gridMainC ) && //don't propagate keyboard commands if grid is already in focus - !isComponentOf(focus, m_gridMainR ) && // - !isComponentOf(focus, m_gridNavi ) && - !isComponentOf(focus, m_listBoxHistory) && //don't propagate if selecting config - !isComponentOf(focus, m_directoryLeft ) && //don't propagate if changing directory field - !isComponentOf(focus, m_directoryRight) && - !isComponentOf(focus, m_panelSearch ) && - !isComponentOf(focus, m_scrolledWindowFolderPairs)) - if (wxEvtHandler* evtHandler = m_gridMainL->getMainWin().GetEventHandler()) - { - m_gridMainL->SetFocus(); - - event.SetEventType(wxEVT_KEY_DOWN); //the grid event handler doesn't expect wxEVT_CHAR_HOOK! - evtHandler->ProcessEvent(event); //propagating event catched at wxTheApp to child leads to recursion, but we prevented it... - event.Skip(false); //definitively handled now! - return; - } - } - break; - } - - event.Skip(); -} - - -void MainDialog::onNaviSelection(GridRangeSelectEvent& event) -{ - //scroll m_gridMain to user's new selection on m_gridNavi - ptrdiff_t leadRow = -1; - if (event.rowFirst_ != event.rowLast_) - if (std::unique_ptr<TreeView::Node> node = treeDataView->getLine(event.rowFirst_)) - { - if (const TreeView::RootNode* root = dynamic_cast<const TreeView::RootNode*>(node.get())) - leadRow = gridDataView->findRowFirstChild(&(root->baseDirObj_)); - else if (const TreeView::DirNode* dir = dynamic_cast<const TreeView::DirNode*>(node.get())) - { - leadRow = gridDataView->findRowDirect(&(dir->dirObj_)); - if (leadRow < 0) //directory was filtered out! still on tree view (but NOT on grid view) - leadRow = gridDataView->findRowFirstChild(&(dir->dirObj_)); - } - else if (const TreeView::FilesNode* files = dynamic_cast<const TreeView::FilesNode*>(node.get())) - { - assert(!files->filesAndLinks_.empty()); - if (!files->filesAndLinks_.empty()) - leadRow = gridDataView->findRowDirect(files->filesAndLinks_[0]->getId()); - } - } - - if (leadRow >= 0) - { - leadRow = std::max<ptrdiff_t>(0, leadRow - 1); //scroll one more row - - m_gridMainL->scrollTo(leadRow); //scroll all of them (includes the "scroll master") - m_gridMainC->scrollTo(leadRow); // - m_gridMainR->scrollTo(leadRow); // - - m_gridNavi->getMainWin().Update(); //draw cursor immediately rather than on next idle event (required for slow CPUs, netbook) - } - - //get selection on navigation tree and set corresponding markers on main grid - hash_set<const FileSystemObject*> markedFilesAndLinks; //mark files/symlinks directly - hash_set<const HierarchyObject*> markedContainer; //mark full container including child-objects - - const std::vector<size_t>& selection = m_gridNavi->getSelectedRows(); - std::for_each(selection.begin(), selection.end(), - [&](size_t row) - { - if (std::unique_ptr<TreeView::Node> node = treeDataView->getLine(row)) - { - if (const TreeView::RootNode* root = dynamic_cast<const TreeView::RootNode*>(node.get())) - markedContainer.insert(&(root->baseDirObj_)); - else if (const TreeView::DirNode* dir = dynamic_cast<const TreeView::DirNode*>(node.get())) - markedContainer.insert(&(dir->dirObj_)); - else if (const TreeView::FilesNode* files = dynamic_cast<const TreeView::FilesNode*>(node.get())) - markedFilesAndLinks.insert(files->filesAndLinks_.begin(), files->filesAndLinks_.end()); - } - }); - - gridview::setNavigationMarker(*m_gridMainL, std::move(markedFilesAndLinks), std::move(markedContainer)); - - event.Skip(); -} - - -void MainDialog::onNaviGridContext(GridClickEvent& event) -{ - const auto& selection = getTreeSelection(); //referenced by lambdas! - ContextMenu menu; - - //---------------------------------------------------------------------------------------------------- - if (!selection.empty()) - //std::any_of(selection.begin(), selection.end(), [](const FileSystemObject* fsObj){ return fsObj->getSyncOperation() != SO_EQUAL; })) -> doesn't consider directories - { - auto getImage = [&](SyncDirection dir, SyncOperation soDefault) - { - return mirrorIfRtl(getSyncOpImage(selection[0]->getSyncOperation() != SO_EQUAL ? - selection[0]->testSyncOperation(dir) : soDefault)); - }; - const wxBitmap opRight = getImage(SyncDirection::RIGHT, SO_OVERWRITE_RIGHT); - const wxBitmap opNone = getImage(SyncDirection::NONE, SO_DO_NOTHING ); - const wxBitmap opLeft = getImage(SyncDirection::LEFT, SO_OVERWRITE_LEFT ); - - wxString shortCutLeft = L"\tAlt+Left"; - wxString shortCutRight = L"\tAlt+Right"; - if (wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft) - std::swap(shortCutLeft, shortCutRight); - - menu.addItem(_("Set direction:") + L" ->" + shortCutRight, [this, &selection] { setSyncDirManually(selection, SyncDirection::RIGHT); }, &opRight); - menu.addItem(_("Set direction:") + L" -" L"\tAlt+Down", [this, &selection] { setSyncDirManually(selection, SyncDirection::NONE); }, &opNone); - menu.addItem(_("Set direction:") + L" <-" + shortCutLeft, [this, &selection] { setSyncDirManually(selection, SyncDirection::LEFT); }, &opLeft); - //Gtk needs a direction, "<-", because it has no context menu icons! - //Gtk requires "no spaces" for shortcut identifiers! - menu.addSeparator(); - } - - //---------------------------------------------------------------------------------------------------- - //FILE FILTER - auto addFilterMenu = [&](const std::wstring& label, const wxString& iconName, bool include) - { - if (selection.size() == 1) - { - ContextMenu submenu; - - const bool isDir = dynamic_cast<const DirPair*>(selection[0]) != nullptr; - - //by short name - Zstring labelShort = Zstring(Zstr("*")) + FILE_NAME_SEPARATOR + selection[0]->getObjShortName(); - if (isDir) - labelShort += Zstring(FILE_NAME_SEPARATOR) + Zstr("*"); - submenu.addItem(utfCvrtTo<wxString>(labelShort), [this, &selection, include] { filterShortname(*selection[0], include); }); - - //by relative path - Zstring labelRel = FILE_NAME_SEPARATOR + selection[0]->getObjRelativeName(); - if (isDir) - labelRel += Zstring(FILE_NAME_SEPARATOR) + Zstr("*"); - submenu.addItem(utfCvrtTo<wxString>(labelRel), [this, &selection, include] { filterItems(selection, include); }); - - menu.addSubmenu(label, submenu, &getResourceImage(iconName)); - } - else if (selection.size() > 1) - { - //by relative path - menu.addItem(label + L" <" + _("multiple selection") + L">", - [this, &selection, include] { filterItems(selection, include); }, &getResourceImage(iconName)); - } - }; - addFilterMenu(_("Include via filter:"), L"filter_include_small", true); - addFilterMenu(_("Exclude via filter:"), L"filter_exclude_small", false); - - //---------------------------------------------------------------------------------------------------- - if (!selection.empty()) - { - if (selection[0]->isActive()) - menu.addItem(_("Exclude temporarily") + L"\tSpace", [this, &selection] { setFilterManually(selection, false); }, &getResourceImage(L"checkboxFalse")); - else - menu.addItem(_("Include temporarily") + L"\tSpace", [this, &selection] { setFilterManually(selection, true); }, &getResourceImage(L"checkboxTrue")); - } - else - menu.addItem(_("Exclude temporarily") + L"\tSpace", [] {}, nullptr, false); - - //---------------------------------------------------------------------------------------------------- - //CONTEXT_DELETE_FILES - menu.addSeparator(); - - menu.addItem(_("Delete") + L"\tDel", [&] { deleteSelectedFiles(selection, selection); }, nullptr, !selection.empty()); - - menu.popup(*this); -} - - -void MainDialog::onMainGridContextC(GridClickEvent& event) -{ - ContextMenu menu; - - menu.addItem(_("Include all"), [&] - { - zen::setActiveStatus(true, folderCmp); - updateGui(); - }, nullptr, gridDataView->rowsTotal() > 0); - - menu.addItem(_("Exclude all"), [&] - { - zen::setActiveStatus(false, folderCmp); - updateGuiDelayedIf(currentCfg.hideExcludedItems); //show update GUI before removing rows - }, nullptr, gridDataView->rowsTotal() > 0); - - menu.popup(*this); -} - -void MainDialog::onMainGridContextL(GridClickEvent& event) -{ - onMainGridContextRim(true); -} - -void MainDialog::onMainGridContextR(GridClickEvent& event) -{ - onMainGridContextRim(false); -} - -void MainDialog::onMainGridContextRim(bool leftSide) -{ - const auto& selection = getGridSelection(); //referenced by lambdas! - ContextMenu menu; - - if (!selection.empty()) - { - auto getImage = [&](SyncDirection dir, SyncOperation soDefault) - { - return mirrorIfRtl(getSyncOpImage(selection[0]->getSyncOperation() != SO_EQUAL ? - selection[0]->testSyncOperation(dir) : soDefault)); - }; - const wxBitmap opRight = getImage(SyncDirection::RIGHT, SO_OVERWRITE_RIGHT); - const wxBitmap opNone = getImage(SyncDirection::NONE, SO_DO_NOTHING ); - const wxBitmap opLeft = getImage(SyncDirection::LEFT, SO_OVERWRITE_LEFT ); - - wxString shortCutLeft = L"\tAlt+Left"; - wxString shortCutRight = L"\tAlt+Right"; - if (wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft) - std::swap(shortCutLeft, shortCutRight); - - menu.addItem(_("Set direction:") + L" ->" + shortCutRight, [this, &selection] { setSyncDirManually(selection, SyncDirection::RIGHT); }, &opRight); - menu.addItem(_("Set direction:") + L" -" L"\tAlt+Down", [this, &selection] { setSyncDirManually(selection, SyncDirection::NONE); }, &opNone); - menu.addItem(_("Set direction:") + L" <-" + shortCutLeft, [this, &selection] { setSyncDirManually(selection, SyncDirection::LEFT); }, &opLeft); - //Gtk needs a direction, "<-", because it has no context menu icons! - //Gtk requires "no spaces" for shortcut identifiers! - menu.addSeparator(); - } - - //---------------------------------------------------------------------------------------------------- - //FILE FILTER - auto addFilterMenu = [&](const wxString& label, const wxString& iconName, bool include) - { - if (selection.size() == 1) - { - ContextMenu submenu; - - const bool isDir = dynamic_cast<const DirPair*>(selection[0]) != nullptr; - - //by extension - if (!isDir) - { - const Zstring filename = afterLast(selection[0]->getObjRelativeName(), FILE_NAME_SEPARATOR); - if (contains(filename, Zchar('.'))) //be careful: afterLast returns the whole string if '.' is not found! - { - const Zstring extension = afterLast(filename, Zchar('.')); - submenu.addItem(L"*." + utfCvrtTo<wxString>(extension), - [this, extension, include] { filterExtension(extension, include); }); - } - } - - //by short name - Zstring labelShort = Zstring(Zstr("*")) + FILE_NAME_SEPARATOR + selection[0]->getObjShortName(); - if (isDir) - labelShort += Zstring(FILE_NAME_SEPARATOR) + Zstr("*"); - submenu.addItem(utfCvrtTo<wxString>(labelShort), [this, &selection, include] { filterShortname(*selection[0], include); }); - - //by relative path - Zstring labelRel = FILE_NAME_SEPARATOR + selection[0]->getObjRelativeName(); - if (isDir) - labelRel += Zstring(FILE_NAME_SEPARATOR) + Zstr("*"); - submenu.addItem(utfCvrtTo<wxString>(labelRel), [this, &selection, include] { filterItems(selection, include); }); - - menu.addSubmenu(label, submenu, &getResourceImage(iconName)); - } - else if (selection.size() > 1) - { - //by relative path - menu.addItem(label + L" <" + _("multiple selection") + L">", - [this, &selection, include] { filterItems(selection, include); }, &getResourceImage(iconName)); - } - }; - addFilterMenu(_("Include via filter:"), L"filter_include_small", true); - addFilterMenu(_("Exclude via filter:"), L"filter_exclude_small", false); - - //---------------------------------------------------------------------------------------------------- - if (!selection.empty()) - { - if (selection[0]->isActive()) - menu.addItem(_("Exclude temporarily") + L"\tSpace", [this, &selection] { setFilterManually(selection, false); }, &getResourceImage(L"checkboxFalse")); - else - menu.addItem(_("Include temporarily") + L"\tSpace", [this, &selection] { setFilterManually(selection, true); }, &getResourceImage(L"checkboxTrue")); - } - else - menu.addItem(_("Exclude temporarily") + L"\tSpace", [] {}, nullptr, false); - - //---------------------------------------------------------------------------------------------------- - //CONTEXT_EXTERNAL_APP - if (!globalCfg.gui.externelApplications.empty()) - { - menu.addSeparator(); - - for (auto it = globalCfg.gui.externelApplications.begin(); - it != globalCfg.gui.externelApplications.end(); - ++it) - { - //translate default external apps on the fly: 1. "open in explorer" 2. "start directly" - wxString description = zen::implementation::translate(it->first); - if (description.empty()) - description = L" "; //wxWidgets doesn't like empty items - - const wxString command = it->second; //COPY into lambda - - auto openApp = [this, &selection, leftSide, command] { openExternalApplication(command, selection, leftSide); }; - - if (it == globalCfg.gui.externelApplications.begin()) - description += L"\tEnter"; - - menu.addItem(description, openApp, nullptr, !selection.empty()); - } - } - //---------------------------------------------------------------------------------------------------- - //CONTEXT_DELETE_FILES - menu.addSeparator(); - - menu.addItem(_("Delete") + L"\tDel", [this] - { - deleteSelectedFiles( - getGridSelection(true, false), - getGridSelection(false, true)); - }, nullptr, !selection.empty()); - - menu.popup(*this); -} - - - -void MainDialog::filterPhrase(const Zstring& phrase, bool include, bool addNewLine) -{ - Zstring& filterString = [&]() -> Zstring& - { - if (include) - { - Zstring& includeFilter = currentCfg.mainCfg.globalFilter.includeFilter; - if (NameFilter::isNull(includeFilter, FilterConfig().excludeFilter)) //fancy way of checking for "*" include - includeFilter.clear(); - return includeFilter; - } - else - return currentCfg.mainCfg.globalFilter.excludeFilter; - }(); - - if (addNewLine) - { - if (!filterString.empty() && !endsWith(filterString, Zstr("\n"))) - filterString += Zstr("\n"); - filterString += phrase; - } - else - { - if (!filterString.empty() && !endsWith(filterString, Zstr("\n")) && !endsWith(filterString, Zstr(";"))) - filterString += Zstr("\n"); - filterString += phrase + Zstr(";"); //';' is appended to 'mark' that next exclude extension entry won't write to new line - } - - updateGlobalFilterButton(); - if (include) - applyFilterConfig(); //user's temporary exclusions lost! - else //do not fully apply filter, just exclude new items: preserve user's temporary exclusions - { - std::for_each(begin(folderCmp), end(folderCmp), [&](BaseDirPair& baseDirObj) { addHardFiltering(baseDirObj, phrase); }); - updateGui(); - } -} - - -void MainDialog::filterExtension(const Zstring& extension, bool include) -{ - assert(!extension.empty()); - filterPhrase(Zstr("*.") + extension, include, false); -} - - -void MainDialog::filterShortname(const FileSystemObject& fsObj, bool include) -{ - Zstring phrase = Zstring(Zstr("*")) + FILE_NAME_SEPARATOR + fsObj.getObjShortName(); - const bool isDir = dynamic_cast<const DirPair*>(&fsObj) != nullptr; - if (isDir) - phrase += Zstring(FILE_NAME_SEPARATOR) + Zstr("*"); //include filter: * required; exclude filter: * optional, but let's still apply it! - - filterPhrase(phrase, include, true); -} - - -void MainDialog::filterItems(const std::vector<FileSystemObject*>& selection, bool include) -{ - if (!selection.empty()) - { - Zstring phrase; - for (auto it = selection.begin(); it != selection.end(); ++it) - { - FileSystemObject* fsObj = *it; - - if (it != selection.begin()) - phrase += Zstr("\n"); - - //#pragma warning(suppress: 6011) -> fsObj bound in this context! - phrase += FILE_NAME_SEPARATOR + fsObj->getObjRelativeName(); - - const bool isDir = dynamic_cast<const DirPair*>(fsObj) != nullptr; - if (isDir) - phrase += Zstring(FILE_NAME_SEPARATOR) + Zstr("*"); //include filter: * required; exclude filter: * optional, but let's still apply it! - } - filterPhrase(phrase, include, true); - } -} - - -void MainDialog::onGridLabelContextC(GridClickEvent& event) -{ - ContextMenu menu; - - const bool actionView = m_bpButtonViewTypeSyncAction->isActive(); - menu.addRadio(_("Category") + (actionView ? L"\tF9" : L""), [&] { setViewTypeSyncAction(false); }, !actionView); - menu.addRadio(_("Action") + (!actionView ? L"\tF9" : L""), [&] { setViewTypeSyncAction(true ); }, actionView); - - //menu.addItem(_("Category") + L"\tF9", [&] { setViewTypeSyncAction(false); }, m_bpButtonViewTypeSyncAction->isActive() ? nullptr : &getResourceImage(L"compare_small")); - //menu.addItem(_("Action"), [&] { setViewTypeSyncAction(true ); }, m_bpButtonViewTypeSyncAction->isActive() ? &getResourceImage(L"sync_small") : nullptr); - menu.popup(*this); -} - - -void MainDialog::onGridLabelContextL(GridClickEvent& event) -{ - onGridLabelContext(*m_gridMainL, static_cast<ColumnTypeRim>(event.colType_), getDefaultColumnAttributesLeft()); -} -void MainDialog::onGridLabelContextR(GridClickEvent& event) -{ - onGridLabelContext(*m_gridMainR, static_cast<ColumnTypeRim>(event.colType_), getDefaultColumnAttributesRight()); -} - - -void MainDialog::onGridLabelContext(Grid& grid, ColumnTypeRim type, const std::vector<ColumnAttributeRim>& defaultColumnAttributes) -{ - ContextMenu menu; - - auto toggleColumn = [&](ColumnType ct) - { - auto colAttr = grid.getColumnConfig(); - - for (Grid::ColumnAttribute& ca : colAttr) - if (ca.type_ == ct) - { - ca.visible_ = !ca.visible_; - grid.setColumnConfig(colAttr); - return; - } - }; - - if (const GridData* prov = grid.getDataProvider()) - for (const Grid::ColumnAttribute& ca : grid.getColumnConfig()) - menu.addCheckBox(prov->getColumnLabel(ca.type_), [ca, toggleColumn] { toggleColumn(ca.type_); }, - ca.visible_, ca.type_ != static_cast<ColumnType>(COL_TYPE_FILENAME)); //do not allow user to hide file name column! - //---------------------------------------------------------------------------------------------- - menu.addSeparator(); - - auto setDefault = [&] - { - grid.setColumnConfig(gridview::convertConfig(defaultColumnAttributes)); - }; - menu.addItem(_("&Default"), setDefault); //'&' -> reuse text from "default" buttons elsewhere - //---------------------------------------------------------------------------------------------- - menu.addSeparator(); - menu.addCheckBox(_("Show icons:"), [&] - { - globalCfg.gui.showIcons = !globalCfg.gui.showIcons; - gridview::setupIcons(*m_gridMainL, *m_gridMainC, *m_gridMainR, globalCfg.gui.showIcons, convert(globalCfg.gui.iconSize)); - - }, globalCfg.gui.showIcons); - - auto setIconSize = [&](xmlAccess::FileIconSize sz) - { - globalCfg.gui.iconSize = sz; - gridview::setupIcons(*m_gridMainL, *m_gridMainC, *m_gridMainR, globalCfg.gui.showIcons, convert(sz)); - }; - auto addSizeEntry = [&](const wxString& label, xmlAccess::FileIconSize sz) - { - auto setIconSize2 = setIconSize; //bring into scope - menu.addRadio(label, [sz, setIconSize2] { setIconSize2(sz); }, globalCfg.gui.iconSize == sz, globalCfg.gui.showIcons); - }; - addSizeEntry(L" " + _("Small" ), xmlAccess::ICON_SIZE_SMALL ); - addSizeEntry(L" " + _("Medium"), xmlAccess::ICON_SIZE_MEDIUM); - addSizeEntry(L" " + _("Large" ), xmlAccess::ICON_SIZE_LARGE ); - //---------------------------------------------------------------------------------------------- - if (type == COL_TYPE_DATE) - { - menu.addSeparator(); - - auto selectTimeSpan = [&] - { - if (showSelectTimespanDlg(this, manualTimeSpanFrom, manualTimeSpanTo) == ReturnSmallDlg::BUTTON_OKAY) - { - applyTimeSpanFilter(folderCmp, manualTimeSpanFrom, manualTimeSpanTo); //overwrite current active/inactive settings - //updateGuiDelayedIf(currentCfg.hideExcludedItems); //show update GUI before removing rows - updateGui(); - } - }; - menu.addItem(_("Select time span..."), selectTimeSpan); - } - - menu.popup(*this); -} - - -void MainDialog::OnContextSetLayout(wxMouseEvent& event) -{ - ContextMenu menu; - - menu.addItem(_("Default view"), [&] - { - m_splitterMain->setSashOffset(0); - auiMgr.LoadPerspective(defaultPerspective); - updateGuiForFolderPair(); - }); - //---------------------------------------------------------------------------------------- - std::vector<std::pair<wxString, wxString>> captionNameMap; //(caption, identifier) - - const wxAuiPaneInfoArray& paneArray = auiMgr.GetAllPanes(); - for (size_t i = 0; i < paneArray.size(); ++i) - if (!paneArray[i].IsShown() && !paneArray[i].name.empty() && - paneArray[i].window != compareStatus->getAsWindow() && - paneArray[i].window != m_panelSearch) - captionNameMap.push_back(std::make_pair(paneArray[i].caption, paneArray[i].name)); - - if (!captionNameMap.empty()) - menu.addSeparator(); - - auto showPanel = [&](const wxString& identifier) - { - auiMgr.GetPane(identifier).Show(); - auiMgr.Update(); - }; - - for (auto it = captionNameMap.begin(); it != captionNameMap.end(); ++it) - { - const wxString label = replaceCpy(_("Show \"%x\""), L"%x", it->first); - const wxString identifier = it->second; - - menu.addItem(label, [showPanel, identifier] { showPanel(identifier); }); - } - - menu.popup(*this); -} - - -void MainDialog::OnCompSettingsContext(wxMouseEvent& event) -{ - ContextMenu menu; - - auto setVariant = [&](CompareVariant var) - { - currentCfg.mainCfg.cmpConfig.compareVar = var; - applyCompareConfig(true); //true: switchMiddleGrid - }; - - auto currentVar = getConfig().mainCfg.cmpConfig.compareVar; - - menu.addRadio(_("File time and size"), [&] { setVariant(CMP_BY_TIME_SIZE); }, currentVar == CMP_BY_TIME_SIZE); - menu.addRadio(_("File content" ), [&] { setVariant(CMP_BY_CONTENT); }, currentVar == CMP_BY_CONTENT); - - menu.popup(*this); -} - - -void MainDialog::OnSyncSettingsContext(wxMouseEvent& event) -{ - ContextMenu menu; - - auto setVariant = [&](DirectionConfig::Variant var) - { - currentCfg.mainCfg.syncCfg.directionCfg.var = var; - applySyncConfig(); - }; - - const auto currentVar = getConfig().mainCfg.syncCfg.directionCfg.var; - - menu.addRadio(L"<- " + _("Two way") + L" ->" , [&] { setVariant(DirectionConfig::TWOWAY); }, currentVar == DirectionConfig::TWOWAY); - menu.addRadio( _("Mirror") + L" ->>", [&] { setVariant(DirectionConfig::MIRROR); }, currentVar == DirectionConfig::MIRROR); - menu.addRadio( _("Update") + L" ->" , [&] { setVariant(DirectionConfig::UPDATE); }, currentVar == DirectionConfig::UPDATE); - menu.addRadio( _("Custom") , [&] { setVariant(DirectionConfig::CUSTOM); }, currentVar == DirectionConfig::CUSTOM); - - menu.popup(*this); -} - - -void MainDialog::onNaviPanelFilesDropped(FileDropEvent& event) -{ - loadConfiguration(toZ(event.getFiles())); - event.Skip(); -} - - -void MainDialog::onDirSelected(wxCommandEvent& event) -{ - //left and right directory text-control and dirpicker are synchronized by MainFolderDragDrop automatically - clearGrid(); //disable the sync button - event.Skip(); -} - - -void MainDialog::onDirManualCorrection(wxCommandEvent& event) -{ - updateUnsavedCfgStatus(); - event.Skip(); -} - - -wxString getFormattedHistoryElement(const Zstring& filename) -{ - Zstring output = afterLast(filename, FILE_NAME_SEPARATOR); - if (endsWith(output, Zstr(".ffs_gui"))) - output = beforeLast(output, Zstr('.')); - return utfCvrtTo<wxString>(output); -} - - -void MainDialog::addFileToCfgHistory(const std::vector<Zstring>& filenames) -{ - //determine highest "last use" index number of m_listBoxHistory - int lastUseIndexMax = 0; - for (unsigned int i = 0; i < m_listBoxHistory->GetCount(); ++i) - if (auto histData = dynamic_cast<const wxClientHistoryData*>(m_listBoxHistory->GetClientObject(i))) - if (histData->lastUseIndex_ > lastUseIndexMax) - lastUseIndexMax = histData->lastUseIndex_; - - std::deque<bool> selections(m_listBoxHistory->GetCount()); //items to select after update of history list - - for (auto it = filenames.begin(); it != filenames.end(); ++it) - { - const Zstring& filename = *it; - - //Do we need to additionally check for aliases of the same physical files here? (and aliases for lastRunConfigName?) - - const auto itemPos = [&]() -> std::pair<wxClientHistoryData*, unsigned int> - { - for (unsigned int i = 0; i < m_listBoxHistory->GetCount(); ++i) - if (auto histData = dynamic_cast<wxClientHistoryData*>(m_listBoxHistory->GetClientObject(i))) - { - if (EqualFilename()(filename, histData->cfgFile_)) - return std::make_pair(histData, i); - } - else - assert(false); - return std::make_pair(nullptr, 0); - }(); - - if (itemPos.first) //update - { - itemPos.first->lastUseIndex_ = ++lastUseIndexMax; - selections[itemPos.second] = true; - } - else //insert - { - wxString label; - unsigned int newPos = 0; - - if (EqualFilename()(filename, lastRunConfigName())) - label = L"<" + _("Last session") + L">"; - else - { - //workaround wxWidgets 2.9 bug on GTK screwing up the client data if the list box is sorted: - label = getFormattedHistoryElement(filename); - //"linear insertion sort": - for (; newPos < m_listBoxHistory->GetCount(); ++newPos) - if (label.CmpNoCase(m_listBoxHistory->GetString(newPos)) < 0) - break; - } - - assert(!m_listBoxHistory->IsSorted()); - m_listBoxHistory->Insert(label, newPos, new wxClientHistoryData(filename, ++lastUseIndexMax)); - - selections.insert(selections.begin() + newPos, true); - } - } - - assert(selections.size() == m_listBoxHistory->GetCount()); - - //do not apply selections immediately but only when needed! - //this prevents problems with m_listBoxHistory losing keyboard selection focus if identical selection is redundantly reapplied - for (int pos = 0; pos < static_cast<int>(selections.size()); ++pos) - if (m_listBoxHistory->IsSelected(pos) != selections[pos]) - m_listBoxHistory->SetSelection(pos, selections[pos]); -} - - -void MainDialog::removeObsoleteCfgHistoryItems(const std::vector<Zstring>& filenames) -{ - //don't use wxString: NOT thread-safe! (e.g. non-atomic ref-count) - - auto getMissingFilesAsync = [filenames]() -> std::vector<Zstring> - { - //boost::this_thread::sleep(boost::posix_time::millisec(5000)); - - //check existence of all config files in parallel! - std::list<boost::unique_future<bool>> fileEx; - - for (auto it = filenames.begin(); it != filenames.end(); ++it) //avoid VC11 compiler issues with std::for_each - { - const Zstring filename = *it; //don't reference iterator in lambda! - fileEx.push_back(zen::async2<bool>([=] { return fileExists(filename); })); - } - - //potentially slow network access => limit maximum wait time! - wait_for_all_timed(fileEx.begin(), fileEx.end(), boost::posix_time::milliseconds(1000)); - - std::vector<Zstring> missingFiles; - - auto itFut = fileEx.begin(); - for (auto it = filenames.begin(); it != filenames.end(); ++it, (void)++itFut) //void: prevent ADL from dragging in boost's ,-overload: "MSVC warning C4913: user defined binary operator ',' exists but no overload could convert all operands" - if (itFut->is_ready() && !itFut->get()) //remove only files that are confirmed to be non-existent - missingFiles.push_back(*it); - - return missingFiles; - }; - - processAsync(getMissingFilesAsync, [this](const std::vector<Zstring>& files) { removeCfgHistoryItems(files); }); -} - - -void MainDialog::removeCfgHistoryItems(const std::vector<Zstring>& filenames) -{ - std::for_each(filenames.begin(), filenames.end(), [&](const Zstring& filename) - { - const int histSize = m_listBoxHistory->GetCount(); - for (int i = 0; i < histSize; ++i) - if (auto histData = dynamic_cast<wxClientHistoryData*>(m_listBoxHistory->GetClientObject(i))) - if (EqualFilename()(filename, histData->cfgFile_)) - { - m_listBoxHistory->Delete(i); - break; - } - }); -} - - -void MainDialog::updateUnsavedCfgStatus() -{ - const Zstring activeCfgFilename = activeConfigFiles.size() == 1 && activeConfigFiles[0] != lastRunConfigName() ? activeConfigFiles[0] : Zstring(); - - const bool haveUnsavedCfg = lastConfigurationSaved != getConfig(); - - //update save config button - const bool allowSave = haveUnsavedCfg || - activeConfigFiles.size() > 1; - - auto makeBrightGrey = [](const wxBitmap& bmp) -> wxBitmap - { - wxImage img = bmp.ConvertToImage().ConvertToGreyscale(1.0/3, 1.0/3, 1.0/3); //treat all channels equally! - brighten(img, 80); - return img; - }; - //setImage(*m_bpButtonSave, greyScale(getResourceImage(L"save"))); - - setImage(*m_bpButtonSave, allowSave ? getResourceImage(L"save") : makeBrightGrey(getResourceImage(L"save"))); - m_bpButtonSave->Enable(allowSave); - m_menuItemSave->Enable(allowSave); //bitmap is automatically greyscaled on Win7 (introducing a crappy looking shift), but not on XP - - //set main dialog title - wxString title; - if (haveUnsavedCfg) - title += L'*'; - - if (!activeCfgFilename.empty()) - title += toWx(activeCfgFilename); - else if (activeConfigFiles.size() > 1) - { - const wchar_t* EM_DASH = L" \u2014 "; - title += xmlAccess::extractJobName(activeConfigFiles[0]); - std::for_each(activeConfigFiles.begin() + 1, activeConfigFiles.end(), [&](const Zstring& filename) { title += EM_DASH + xmlAccess::extractJobName(filename); }); - } - else - title += L"FreeFileSync - " + _("Folder Comparison and Synchronization"); - - SetTitle(title); -} - - -void MainDialog::OnConfigSave(wxCommandEvent& event) -{ - const Zstring activeCfgFilename = activeConfigFiles.size() == 1 && activeConfigFiles[0] != lastRunConfigName() ? activeConfigFiles[0] : Zstring(); - - //if we work on a single named configuration document: save directly if changed - //else: always show file dialog - if (!activeCfgFilename.empty()) - { - using namespace xmlAccess; - - switch (getXmlType(activeCfgFilename)) //throw() - { - case XML_TYPE_GUI: - trySaveConfig(&activeCfgFilename); - return; - case XML_TYPE_BATCH: - trySaveBatchConfig(&activeCfgFilename); - return; - case XML_TYPE_GLOBAL: - case XML_TYPE_OTHER: - assert(false); - return; - } - } - - trySaveConfig(nullptr); -} - - -void MainDialog::OnConfigSaveAs(wxCommandEvent& event) -{ - trySaveConfig(nullptr); -} - - -void MainDialog::OnSaveAsBatchJob(wxCommandEvent& event) -{ - trySaveBatchConfig(nullptr); -} - - -bool MainDialog::trySaveConfig(const Zstring* fileNameGui) //return true if saved successfully -{ - Zstring targetFilename; - - if (fileNameGui) - { - targetFilename = *fileNameGui; - assert(endsWith(targetFilename, Zstr(".ffs_gui"))); - } - else - { - Zstring defaultFileName = activeConfigFiles.size() == 1 && activeConfigFiles[0] != lastRunConfigName() ? activeConfigFiles[0] : Zstr("SyncSettings.ffs_gui"); - //attention: activeConfigFiles may be an imported *.ffs_batch file! We don't want to overwrite it with a GUI config! - if (endsWith(defaultFileName, Zstr(".ffs_batch"))) - replace(defaultFileName, Zstr(".ffs_batch"), Zstr(".ffs_gui"), false); - - wxFileDialog filePicker(this, //put modal dialog on stack: creating this on freestore leads to memleak! - wxEmptyString, - //OS X really needs dir/file separated like this: - utfCvrtTo<wxString>(beforeLast(defaultFileName, FILE_NAME_SEPARATOR)), //default dir; empty string if / not found - utfCvrtTo<wxString>(afterLast (defaultFileName, FILE_NAME_SEPARATOR)), //default file; whole string if / not found - wxString(L"FreeFileSync (*.ffs_gui)|*.ffs_gui") + L"|" +_("All files") + L" (*.*)|*", - wxFD_SAVE | wxFD_OVERWRITE_PROMPT); - if (filePicker.ShowModal() != wxID_OK) - return false; - targetFilename = toZ(filePicker.GetPath()); - } - - const xmlAccess::XmlGuiConfig guiCfg = getConfig(); - - try - { - xmlAccess::writeConfig(guiCfg, targetFilename); //throw FfsXmlError - setLastUsedConfig(targetFilename, guiCfg); - - flashStatusInformation(_("Configuration saved")); - return true; - } - catch (const xmlAccess::FfsXmlError& e) - { - showNotificationDialog(this, DialogInfoType::ERROR2, PopupDialogCfg().setDetailInstructions(e.toString())); - return false; - } -} - - -bool MainDialog::trySaveBatchConfig(const Zstring* fileNameBatch) -{ - //essentially behave like trySaveConfig(): the collateral damage of not saving GUI-only settings "hideExcludedItems, m_bpButtonViewTypeSyncAction" is negliable - - const xmlAccess::XmlGuiConfig guiCfg = getConfig(); - - Zstring targetFilename; - xmlAccess::XmlBatchConfig batchCfg; - - if (fileNameBatch) - { - targetFilename = *fileNameBatch; - batchCfg = convertGuiToBatchPreservingExistingBatch(guiCfg, *fileNameBatch); - assert(endsWith(targetFilename, Zstr(".ffs_batch"))); - } - else - { - const Zstring activeCfgFilename = activeConfigFiles.size() == 1 && activeConfigFiles[0] != lastRunConfigName() ? activeConfigFiles[0] : Zstring(); - batchCfg = convertGuiToBatchPreservingExistingBatch(guiCfg, activeCfgFilename); - - //let user change batch config: this should change batch-exclusive settings only, else the "setLastUsedConfig" below would be somewhat of a lie - if (!customizeBatchConfig(this, - batchCfg, //in/out - globalCfg.gui.onCompletionHistory, - globalCfg.gui.onCompletionHistoryMax)) - return false; - - Zstring defaultFileName = !activeCfgFilename.empty() ? activeCfgFilename : Zstr("BatchRun.ffs_batch"); - //attention: activeConfigFiles may be a *.ffs_gui file! We don't want to overwrite it with a BATCH config! - if (endsWith(defaultFileName, Zstr(".ffs_gui"))) - replace(defaultFileName, Zstr(".ffs_gui"), Zstr(".ffs_batch")); - - wxFileDialog filePicker(this, //put modal dialog on stack: creating this on freestore leads to memleak! - wxEmptyString, - //OS X really needs dir/file separated like this: - utfCvrtTo<wxString>(beforeLast(defaultFileName, FILE_NAME_SEPARATOR)), //default dir; empty string if / not found - utfCvrtTo<wxString>(afterLast (defaultFileName, FILE_NAME_SEPARATOR)), //default file; whole string if / not found - _("FreeFileSync batch") + L" (*.ffs_batch)|*.ffs_batch" + L"|" +_("All files") + L" (*.*)|*", - wxFD_SAVE | wxFD_OVERWRITE_PROMPT); - if (filePicker.ShowModal() != wxID_OK) - return false; - targetFilename = toZ(filePicker.GetPath()); - } - - try - { - xmlAccess::writeConfig(batchCfg, targetFilename); //throw FfsXmlError - - setLastUsedConfig(targetFilename, guiCfg); //[!] behave as if we had saved guiCfg - flashStatusInformation(_("Configuration saved")); - return true; - } - catch (const xmlAccess::FfsXmlError& e) - { - showNotificationDialog(this, DialogInfoType::ERROR2, PopupDialogCfg().setDetailInstructions(e.toString())); - return false; - } -} - - -bool MainDialog::saveOldConfig() //return false on user abort -{ - if (lastConfigurationSaved != getConfig()) - { - const Zstring activeCfgFilename = activeConfigFiles.size() == 1 && activeConfigFiles[0] != lastRunConfigName() ? activeConfigFiles[0] : Zstring(); - - //notify user about changed settings - if (globalCfg.optDialogs.popupOnConfigChange) - if (!activeCfgFilename.empty()) - //only if check is active and non-default config file loaded - { - bool neverSaveChanges = false; - switch (showConfirmationDialog3(this, DialogInfoType::INFO, PopupDialogCfg3(). - setTitle(toWx(activeCfgFilename)). - setMainInstructions(replaceCpy(_("Do you want to save changes to %x?"), L"%x", fmtFileName(afterLast(activeCfgFilename, FILE_NAME_SEPARATOR)))). - setCheckBox(neverSaveChanges, _("Never save &changes"), ConfirmationButton3::DO_IT), - _("&Save"), _("Do&n't save"))) - { - case ConfirmationButton3::DO_IT: //save - using namespace xmlAccess; - switch (getXmlType(activeCfgFilename)) //throw() - { - case XML_TYPE_GUI: - return trySaveConfig(&activeCfgFilename); - case XML_TYPE_BATCH: - return trySaveBatchConfig(&activeCfgFilename); - case XML_TYPE_GLOBAL: - case XML_TYPE_OTHER: - assert(false); - return false; - } - break; - - case ConfirmationButton3::DONT_DO_IT: //don't save - globalCfg.optDialogs.popupOnConfigChange = !neverSaveChanges; - break; - - case ConfirmationButton3::CANCEL: - return false; - } - } - - //discard current reference file(s), this ensures next app start will load <last session> instead of the original non-modified config selection - setLastUsedConfig(std::vector<Zstring>(), lastConfigurationSaved); - //this seems to make theoretical sense also: the job of this function is to make sure current (volatile) config and reference file name are in sync - // => if user does not save cfg, it is not attached to a physical file names anymore! - } - return true; -} - - -void MainDialog::OnConfigLoad(wxCommandEvent& event) -{ - const Zstring activeCfgFilename = activeConfigFiles.size() == 1 && activeConfigFiles[0] != lastRunConfigName() ? activeConfigFiles[0] : Zstring(); - - wxFileDialog filePicker(this, - wxEmptyString, - utfCvrtTo<wxString>(beforeLast(activeCfgFilename, FILE_NAME_SEPARATOR)), //set default dir: empty string if "activeConfigFiles" is empty or has no path separator - wxEmptyString, - 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::vector<wxString> filenames(tmp.begin(), tmp.end()); - - loadConfiguration(toZ(filenames)); - } -} - - -void MainDialog::OnConfigNew(wxCommandEvent& event) -{ - if (!saveOldConfig()) //notify user about changed settings - return; - - xmlAccess::XmlGuiConfig newConfig; - - //add default exclusion filter: this is only ever relevant when creating new configurations! - //a default XmlGuiConfig does not need these user-specific exclusions! - Zstring& excludeFilter = newConfig.mainCfg.globalFilter.excludeFilter; - if (!excludeFilter.empty() && !endsWith(excludeFilter, Zstr("\n"))) - excludeFilter += Zstr("\n"); - excludeFilter += globalCfg.gui.defaultExclusionFilter; - - setConfig(newConfig, std::vector<Zstring>()); -} - - -void MainDialog::OnLoadFromHistory(wxCommandEvent& event) -{ - wxArrayInt selections; - m_listBoxHistory->GetSelections(selections); - - std::vector<Zstring> filenames; - std::for_each(selections.begin(), selections.end(), - [&](int pos) - { - if (auto histData = dynamic_cast<const wxClientHistoryData*>(m_listBoxHistory->GetClientObject(pos))) - filenames.push_back(histData->cfgFile_); - else - assert(false); - }); - - if (!filenames.empty()) - loadConfiguration(filenames); - - //user changed m_listBoxHistory selection so it's this method's responsibility to synchronize with activeConfigFiles: - //- if user cancelled saving old config - //- there's an error loading new config - //- filenames is empty and user tried to unselect the current config - addFileToCfgHistory(activeConfigFiles); -} - - -void MainDialog::OnLoadFromHistoryDoubleClick(wxCommandEvent& event) -{ - wxArrayInt selections; - m_listBoxHistory->GetSelections(selections); - - std::vector<Zstring> filenames; - std::for_each(selections.begin(), selections.end(), [&](int pos) - { - if (auto histData = dynamic_cast<const wxClientHistoryData*>(m_listBoxHistory->GetClientObject(pos))) - filenames.push_back(histData->cfgFile_); - else - assert(false); - }); - - if (!filenames.empty()) - if (loadConfiguration(filenames)) - { - //simulate button click on "compare" - wxCommandEvent dummy2(wxEVT_COMMAND_BUTTON_CLICKED); - if (wxEvtHandler* evtHandler = m_buttonCompare->GetEventHandler()) - evtHandler->ProcessEvent(dummy2); //synchronous call - } - - //synchronize m_listBoxHistory and activeConfigFiles, see OnLoadFromHistory() - addFileToCfgHistory(activeConfigFiles); -} - - -bool MainDialog::loadConfiguration(const std::vector<Zstring>& filenames) -{ - if (filenames.empty()) - return true; - - if (!saveOldConfig()) - return false; //cancelled by user - - //load XML - xmlAccess::XmlGuiConfig newGuiCfg; //structure to receive gui settings, already defaulted!! - try - { - //allow reading batch configurations also - xmlAccess::readAnyConfig(filenames, newGuiCfg); //throw FfsXmlError - - setConfig(newGuiCfg, filenames); - //flashStatusInformation(("Configuration loaded")); -> irrelevant!? - return true; - } - catch (const xmlAccess::FfsXmlError& e) - { - if (e.getSeverity() == xmlAccess::FfsXmlError::WARNING) - { - showNotificationDialog(this, DialogInfoType::WARNING, PopupDialogCfg().setDetailInstructions(e.toString())); - setConfig(newGuiCfg, filenames); - setLastUsedConfig(filenames, xmlAccess::XmlGuiConfig()); //simulate changed config due to parsing errors - } - else - showNotificationDialog(this, DialogInfoType::ERROR2, PopupDialogCfg().setDetailInstructions(e.toString())); - return false; - } -} - -void MainDialog::deleteSelectedCfgHistoryItems() -{ - wxArrayInt tmp; - m_listBoxHistory->GetSelections(tmp); - - std::set<int> selections(tmp.begin(), tmp.end()); //sort ascending! - //delete starting with high positions: - std::for_each(selections.rbegin(), selections.rend(), [&](int pos) { m_listBoxHistory->Delete(pos); }); - - //set active selection on next element to allow "batch-deletion" by holding down DEL key - if (!selections.empty() && m_listBoxHistory->GetCount() > 0) - { - int newSelection = *selections.begin(); - if (newSelection >= static_cast<int>(m_listBoxHistory->GetCount())) - newSelection = m_listBoxHistory->GetCount() - 1; - m_listBoxHistory->SetSelection(newSelection); - } -} - - -void MainDialog::OnCfgHistoryRightClick(wxMouseEvent& event) -{ - ContextMenu menu; - menu.addItem(_("Delete") + L"\tDel", [this] { deleteSelectedCfgHistoryItems(); }); - menu.popup(*this); -} - - -void MainDialog::OnCfgHistoryKeyEvent(wxKeyEvent& event) -{ - const int keyCode = event.GetKeyCode(); - if (keyCode == WXK_DELETE || keyCode == WXK_NUMPAD_DELETE) - { - deleteSelectedCfgHistoryItems(); - return; //"swallow" event - } - event.Skip(); -} - - -void MainDialog::OnClose(wxCloseEvent& event) -{ - //attention: system shutdown: is handled in onQueryEndSession()! - - //regular destruction handling - if (event.CanVeto()) - { - const bool cancelled = !saveOldConfig(); //notify user about changed settings - if (cancelled) - { - //attention: this Veto() will NOT cancel system shutdown since saveOldConfig() blocks on modal dialog - - event.Veto(); - return; - } - } - - Destroy(); -} - - -void MainDialog::onCheckRows(CheckRowsEvent& event) -{ - std::set<size_t> selectedRows; - - const size_t rowLast = std::min(event.rowLast_, gridDataView->rowsOnView()); //consider dummy rows - for (size_t i = event.rowFirst_; i < rowLast; ++i) - selectedRows.insert(i); - - if (!selectedRows.empty()) - { - std::vector<FileSystemObject*> objects = gridDataView->getAllFileRef(selectedRows); - setFilterManually(objects, event.setIncluded_); - } -} - - -void MainDialog::onSetSyncDirection(SyncDirectionEvent& event) -{ - std::set<size_t> selectedRows; - - const size_t rowLast = std::min(event.rowLast_, gridDataView->rowsOnView()); //consider dummy rows - for (size_t i = event.rowFirst_; i < rowLast; ++i) - selectedRows.insert(i); - - if (!selectedRows.empty()) - { - std::vector<FileSystemObject*> objects = gridDataView->getAllFileRef(selectedRows); - setSyncDirManually(objects, event.direction_); - } -} - - -void MainDialog::setLastUsedConfig(const Zstring& filename, const xmlAccess::XmlGuiConfig& guiConfig) -{ - std::vector<Zstring> filenames; - filenames.push_back(filename); - setLastUsedConfig(filenames, guiConfig); -} - - -void MainDialog::setLastUsedConfig(const std::vector<Zstring>& filenames, - const xmlAccess::XmlGuiConfig& guiConfig) -{ - activeConfigFiles = filenames; - lastConfigurationSaved = guiConfig; - - addFileToCfgHistory(activeConfigFiles); //put filename on list of last used config files - - updateUnsavedCfgStatus(); -} - - -void MainDialog::setConfig(const xmlAccess::XmlGuiConfig& newGuiCfg, const std::vector<Zstring>& referenceFiles) -{ - currentCfg = newGuiCfg; - - //evaluate new settings... - - //(re-)set view filter buttons - setViewFilterDefault(); - - updateGlobalFilterButton(); - - //set first folder pair - firstFolderPair->setValues(currentCfg.mainCfg.firstPair.leftDirectory, - currentCfg.mainCfg.firstPair.rightDirectory, - currentCfg.mainCfg.firstPair.altCmpConfig, - currentCfg.mainCfg.firstPair.altSyncConfig, - currentCfg.mainCfg.firstPair.localFilter); - - //folderHistoryLeft->addItem(currentCfg.mainCfg.firstPair.leftDirectory); - //folderHistoryRight->addItem(currentCfg.mainCfg.firstPair.rightDirectory); - - setAddFolderPairs(currentCfg.mainCfg.additionalPairs); - - //read GUI layout - m_checkBoxHideExcluded->SetValue(currentCfg.hideExcludedItems); - - setViewTypeSyncAction(currentCfg.highlightSyncAction); - - clearGrid(); //+ update GUI! - - setLastUsedConfig(referenceFiles, newGuiCfg); -} - - -inline -FolderPairEnh getEnhancedPair(const FolderPairPanel* panel) -{ - return FolderPairEnh(panel->getLeftDir(), - panel->getRightDir(), - panel->getAltCompConfig(), - panel->getAltSyncConfig(), - panel->getAltFilterConfig()); -} - - -xmlAccess::XmlGuiConfig MainDialog::getConfig() const -{ - xmlAccess::XmlGuiConfig guiCfg = currentCfg; - - //load settings whose ownership lies not in currentCfg: - - //first folder pair - guiCfg.mainCfg.firstPair = FolderPairEnh(firstFolderPair->getLeftDir(), - firstFolderPair->getRightDir(), - firstFolderPair->getAltCompConfig(), - firstFolderPair->getAltSyncConfig(), - firstFolderPair->getAltFilterConfig()); - - //add additional pairs - guiCfg.mainCfg.additionalPairs.clear(); - std::transform(additionalFolderPairs.begin(), additionalFolderPairs.end(), - std::back_inserter(guiCfg.mainCfg.additionalPairs), getEnhancedPair); - - //sync preview - guiCfg.highlightSyncAction = m_bpButtonViewTypeSyncAction->isActive(); - - return guiCfg; -} - - -const Zstring& MainDialog::lastRunConfigName() -{ - static Zstring instance = zen::getConfigDir() + Zstr("LastRun.ffs_gui"); - return instance; -} - - -void MainDialog::updateGuiDelayedIf(bool condition) -{ - const int delay = 400; - - if (condition) - { - gridview::refresh(*m_gridMainL, *m_gridMainC, *m_gridMainR); - m_gridMainL->Update(); - m_gridMainC->Update(); - m_gridMainR->Update(); - - wxMilliSleep(delay); //some delay to show the changed GUI before removing rows from sight - } - - updateGui(); -} - - -void MainDialog::OnShowExcluded(wxCommandEvent& event) -{ - //toggle showing filtered rows - currentCfg.hideExcludedItems = !currentCfg.hideExcludedItems; - //make sure, checkbox and value are in sync - m_checkBoxHideExcluded->SetValue(currentCfg.hideExcludedItems); - - updateGui(); -} - - -void MainDialog::OnConfigureFilter(wxCommandEvent& event) -{ - if (showFilterDialog(this, currentCfg.mainCfg.globalFilter, _("Filter")) == ReturnSmallDlg::BUTTON_OKAY) - { - updateGlobalFilterButton(); //refresh global filter icon - applyFilterConfig(); //re-apply filter - } - - //event.Skip() -} - - -void MainDialog::OnGlobalFilterContext(wxMouseEvent& event) -{ - auto clearFilter = [&] - { - currentCfg.mainCfg.globalFilter = FilterConfig(); - updateGlobalFilterButton(); //refresh global filter icon - applyFilterConfig(); //re-apply filter - }; - auto copyFilter = [&] { filterCfgOnClipboard = make_unique<FilterConfig>(currentCfg.mainCfg.globalFilter); }; - auto pasteFilter = [&] - { - if (filterCfgOnClipboard) - { - currentCfg.mainCfg.globalFilter = *filterCfgOnClipboard; - updateGlobalFilterButton(); //refresh global filter icon - applyFilterConfig(); //re-apply filter - } - }; - - ContextMenu menu; - menu.addItem( _("Clear filter settings"), clearFilter, nullptr, !isNullFilter(currentCfg.mainCfg.globalFilter)); - menu.addSeparator(); - menu.addItem( _("Copy"), copyFilter, nullptr, !isNullFilter(currentCfg.mainCfg.globalFilter)); - menu.addItem( _("Paste"), pasteFilter, nullptr, filterCfgOnClipboard.get() != nullptr); - menu.popup(*this); -} - - -void MainDialog::OnToggleViewType(wxCommandEvent& event) -{ - setViewTypeSyncAction(!m_bpButtonViewTypeSyncAction->isActive()); //toggle view -} - - -void MainDialog::OnToggleViewButton(wxCommandEvent& event) -{ - if (auto button = dynamic_cast<ToggleButton*>(event.GetEventObject())) - { - button->toggle(); - updateGui(); - } - else - assert(false); -} - - -inline -wxBitmap buttonPressed(const std::string& name) -{ - wxBitmap background = getResourceImage(L"buttonPressed"); - return mirrorIfRtl( - layOver(getResourceImage(utfCvrtTo<wxString>(name)), background)); -} - - -inline -wxBitmap buttonReleased(const std::string& name) -{ - wxImage output = getResourceImage(utfCvrtTo<wxString>(name)).ConvertToImage().ConvertToGreyscale(1.0/3, 1.0/3, 1.0/3); //treat all channels equally! - //zen::moveImage(output, 1, 0); //move image right one pixel - - brighten(output, 80); - return mirrorIfRtl(output); -} - - -void MainDialog::initViewFilterButtons() -{ - m_bpButtonViewTypeSyncAction->init(getResourceImage(L"viewtype_sync_action"), getResourceImage(L"viewtype_cmp_result")); - //tooltip is updated dynamically in setViewTypeSyncAction() - - auto initButton = [](ToggleButton& btn, const char* imgName, const wxString& tooltip) { btn.init(buttonPressed(imgName), buttonReleased(imgName)); btn.SetToolTip(tooltip); }; - - //compare result buttons - initButton(*m_bpButtonShowLeftOnly, "cat_left_only", _("Show files that exist on left side only")); - initButton(*m_bpButtonShowRightOnly, "cat_right_only", _("Show files that exist on right side only")); - initButton(*m_bpButtonShowLeftNewer, "cat_left_newer", _("Show files that are newer on left")); - initButton(*m_bpButtonShowRightNewer, "cat_right_newer", _("Show files that are newer on right")); - initButton(*m_bpButtonShowEqual, "cat_equal", _("Show files that are equal")); - initButton(*m_bpButtonShowDifferent, "cat_different", _("Show files that are different")); - initButton(*m_bpButtonShowConflict, "cat_conflict", _("Show conflicts")); - - //sync preview buttons - initButton(*m_bpButtonShowCreateLeft, "so_create_left", _("Show files that will be created on the left side")); - initButton(*m_bpButtonShowCreateRight, "so_create_right", _("Show files that will be created on the right side")); - initButton(*m_bpButtonShowDeleteLeft, "so_delete_left", _("Show files that will be deleted on the left side")); - initButton(*m_bpButtonShowDeleteRight, "so_delete_right", _("Show files that will be deleted on the right side")); - initButton(*m_bpButtonShowUpdateLeft, "so_update_left", _("Show files that will be overwritten on left side")); - initButton(*m_bpButtonShowUpdateRight, "so_update_right", _("Show files that will be overwritten on right side")); - initButton(*m_bpButtonShowDoNothing, "so_none", _("Show files that won't be copied")); -} - - -void MainDialog::setViewFilterDefault() -{ - auto setButton = [](ToggleButton* tb, bool value) { tb->setActive(value); }; - - const auto& def = globalCfg.gui.viewFilterDefault; - setButton(m_bpButtonShowLeftOnly, def.leftOnly); - setButton(m_bpButtonShowRightOnly, def.rightOnly); - setButton(m_bpButtonShowLeftNewer, def.leftNewer); - setButton(m_bpButtonShowRightNewer, def.rightNewer); - setButton(m_bpButtonShowEqual, def.equal); - setButton(m_bpButtonShowDifferent, def.different); - setButton(m_bpButtonShowConflict, def.conflict); - - setButton(m_bpButtonShowCreateLeft, def.createLeft); - setButton(m_bpButtonShowCreateRight,def.createRight); - setButton(m_bpButtonShowUpdateLeft, def.updateLeft); - setButton(m_bpButtonShowUpdateRight,def.updateRight); - setButton(m_bpButtonShowDeleteLeft, def.deleteLeft); - setButton(m_bpButtonShowDeleteRight,def.deleteRight); - setButton(m_bpButtonShowDoNothing, def.doNothing); -} - - -void MainDialog::OnViewButtonRightClick(wxMouseEvent& event) -{ - auto setButtonDefault = [](const ToggleButton* tb, bool& defaultValue) - { - if (tb->IsShown()) - defaultValue = tb->isActive(); - }; - - auto setDefault = [&] - { - auto& def = globalCfg.gui.viewFilterDefault; - setButtonDefault(m_bpButtonShowLeftOnly, def.leftOnly); - setButtonDefault(m_bpButtonShowRightOnly, def.rightOnly); - setButtonDefault(m_bpButtonShowLeftNewer, def.leftNewer); - setButtonDefault(m_bpButtonShowRightNewer, def.rightNewer); - setButtonDefault(m_bpButtonShowEqual, def.equal); - setButtonDefault(m_bpButtonShowDifferent, def.different); - setButtonDefault(m_bpButtonShowConflict, def.conflict); - - setButtonDefault(m_bpButtonShowCreateLeft, def.createLeft); - setButtonDefault(m_bpButtonShowCreateRight, def.createRight); - setButtonDefault(m_bpButtonShowUpdateLeft, def.updateLeft); - setButtonDefault(m_bpButtonShowUpdateRight, def.updateRight); - setButtonDefault(m_bpButtonShowDeleteLeft, def.deleteLeft); - setButtonDefault(m_bpButtonShowDeleteRight, def.deleteRight); - setButtonDefault(m_bpButtonShowDoNothing, def.doNothing); - }; - - ContextMenu menu; - menu.addItem( _("Set as default"), setDefault); - menu.popup(*this); -} - - -void MainDialog::updateGlobalFilterButton() -{ - //global filter: test for Null-filter - if (!isNullFilter(currentCfg.mainCfg.globalFilter)) - { - setImage(*m_bpButtonFilter, getResourceImage(L"filter")); - m_bpButtonFilter->SetToolTip(_("Filter") + L" (F10) (" + _("Active") + L")"); - } - else - { - setImage(*m_bpButtonFilter, greyScale(getResourceImage(L"filter"))); - m_bpButtonFilter->SetToolTip(_("Filter") + L" (F10) (" + _("None") + L")"); - } -} - - -void MainDialog::OnCompare(wxCommandEvent& event) -{ - //PERF_START; - - //wxBusyCursor dummy; -> redundant: progress already shown in progress dialog! - - wxWindow* oldFocus = wxWindow::FindFocus(); - ZEN_ON_SCOPE_EXIT(if (oldFocus) oldFocus->SetFocus();); //e.g. keep focus on main grid after pressing F5 - - int scrollPosX = 0; - int scrollPosY = 0; - m_gridMainL->GetViewStart(&scrollPosX, &scrollPosY); //preserve current scroll position - ZEN_ON_SCOPE_EXIT( - m_gridMainL->Scroll(scrollPosX, scrollPosY); // - m_gridMainR->Scroll(scrollPosX, scrollPosY); //restore - m_gridMainC->Scroll(-1, scrollPosY); ) // - - clearGrid(); //avoid memory peak by clearing old data first - - disableAllElements(true); //CompareStatusHandler will internally process Window messages, so avoid unexpected callbacks! - auto app = wxTheApp; //fix lambda/wxWigets/VC fuck up - ZEN_ON_SCOPE_EXIT(app->Yield(); enableAllElements()); //ui update before enabling buttons again: prevent strange behaviour of delayed button clicks - - try - { - //class handling status display and error messages - CompareStatusHandler statusHandler(*this); - - const std::vector<zen::FolderPairCfg> cmpConfig = zen::extractCompareCfg(getConfig().mainCfg); - - //GUI mode: place directory locks on directories isolated(!) during both comparison and synchronization - std::unique_ptr<LockHolder> dirLocks; - - //COMPARE DIRECTORIES - compare(globalCfg.fileTimeTolerance, - globalCfg.optDialogs, - true, //allow pw prompt - globalCfg.runWithBackgroundPriority, - globalCfg.createLockFile, - dirLocks, - cmpConfig, - folderCmp, - statusHandler); //throw GuiAbortProcess - } - catch (GuiAbortProcess&) - { - // if (m_buttonCompare->IsShownOnScreen()) m_buttonCompare->SetFocus(); - updateGui(); //refresh grid in ANY case! (also on abort) - return; - } - - gridDataView->setData(folderCmp); //update view on data - treeDataView->setData(folderCmp); // - updateGui(); - - // if (m_buttonSync->IsShownOnScreen()) m_buttonSync->SetFocus(); - - gridview::clearSelection(*m_gridMainL, *m_gridMainC, *m_gridMainR); - m_gridNavi->clearSelection(); - - //play (optional) sound notification after sync has completed (GUI and batch mode) - //const Zstring soundFile = zen::getResourceDir() + Zstr("Compare_Complete.wav"); - //if (fileExists(soundFile)) - // wxSound::Play(toWx(soundFile), wxSOUND_ASYNC); - - //add to folder history after successful comparison only - folderHistoryLeft ->addItem(toZ(m_directoryLeft ->GetValue())); - folderHistoryRight->addItem(toZ(m_directoryRight->GetValue())); - - //prepare status information - if (allElementsEqual(folderCmp)) - flashStatusInformation(_("All folders are in sync")); -} - - -void MainDialog::updateTopButtonImages() -{ - auto updateButton = [&](wxBitmapButton& btn, const wxBitmap& bmp, const wxString& variantName, bool makeGrey) - { - wxImage labelImage = createImageFromText(btn.GetLabel(), btn.GetFont(), wxSystemSettings::GetColour(makeGrey ? wxSYS_COLOUR_GRAYTEXT : wxSYS_COLOUR_BTNTEXT)); - wxImage variantImage = createImageFromText(variantName, wxFont(wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxBOLD), wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT)); - - wxImage descrImage = stackImages(labelImage, variantImage, ImageStackLayout::VERTICAL, ImageStackAlignment::CENTER); - const wxImage& iconImage = makeGrey ? greyScale(bmp.ConvertToImage()) : bmp.ConvertToImage(); - - wxImage dynImage = btn.GetLayoutDirection() != wxLayout_RightToLeft ? - stackImages(iconImage, descrImage, ImageStackLayout::HORIZONTAL, ImageStackAlignment::CENTER, 5) : - stackImages(descrImage, iconImage, ImageStackLayout::HORIZONTAL, ImageStackAlignment::CENTER, 5); - - //SetMinSize() instead of SetSize() is needed here for wxWindows layout determination to work corretly - wxSize minSize = dynImage.GetSize() + wxSize(10, 10); //add border space - minSize.x = std::max(minSize.x, 180); - btn.SetMinSize(minSize); - - btn.SetBitmapLabel(wxBitmap(dynImage)); - //SetLabel() calls confuse wxBitmapButton in the disabled state and it won't show the image! workaround: - btn.SetBitmapDisabled(wxBitmap(dynImage.ConvertToDisabled())); - }; - - updateButton(*m_buttonCompare, getResourceImage(L"compare"), getConfig().mainCfg.getCompVariantName(), false); - updateButton(*m_buttonSync, getResourceImage(L"sync"), getConfig().mainCfg.getSyncVariantName(), folderCmp.empty()); - - m_panelTopButtons->Layout(); -} - - -void MainDialog::updateGui() -{ - updateGridViewData(); //update gridDataView and write status information - - updateStatistics(); - - updateUnsavedCfgStatus(); - - updateTopButtonImages(); - - auiMgr.Update(); //fix small display distortion, if view filter panel is empty -} - - -void MainDialog::clearGrid() -{ - folderCmp.clear(); - gridDataView->setData(folderCmp); - treeDataView->setData(folderCmp); - - updateGui(); -} - - -void MainDialog::updateStatistics() -{ - //update preview of item count and bytes to be transferred: - const SyncStatistics st(folderCmp); - - setText(*m_staticTextData, filesizeToShortString(st.getDataToProcess())); - if (st.getDataToProcess() == 0) - m_bitmapData->SetBitmap(greyScale(getResourceImage(L"data"))); - else - m_bitmapData->SetBitmap(getResourceImage(L"data")); - - auto setValue = [](wxStaticText& txtControl, int value, wxStaticBitmap& bmpControl, const wchar_t* bmpName) - { - setText(txtControl, toGuiString(value)); - - if (value == 0) - bmpControl.SetBitmap(greyScale(mirrorIfRtl(getResourceImage(bmpName)))); - else - bmpControl.SetBitmap(mirrorIfRtl(getResourceImage(bmpName))); - }; - - setValue(*m_staticTextCreateLeft, st.getCreate<LEFT_SIDE >(), *m_bitmapCreateLeft, L"so_create_left_small"); - setValue(*m_staticTextUpdateLeft, st.getUpdate<LEFT_SIDE >(), *m_bitmapUpdateLeft, L"so_update_left_small"); - setValue(*m_staticTextDeleteLeft, st.getDelete<LEFT_SIDE >(), *m_bitmapDeleteLeft, L"so_delete_left_small"); - setValue(*m_staticTextCreateRight, st.getCreate<RIGHT_SIDE>(), *m_bitmapCreateRight, L"so_create_right_small"); - setValue(*m_staticTextUpdateRight, st.getUpdate<RIGHT_SIDE>(), *m_bitmapUpdateRight, L"so_update_right_small"); - setValue(*m_staticTextDeleteRight, st.getDelete<RIGHT_SIDE>(), *m_bitmapDeleteRight, L"so_delete_right_small"); - - m_panelStatistics->Layout(); - m_panelStatistics->Refresh(); //fix small mess up on RTL layout -} - - -void MainDialog::OnSyncSettings(wxCommandEvent& event) -{ - ExecWhenFinishedCfg ewfCfg = { ¤tCfg.mainCfg.onCompletion, - &globalCfg.gui.onCompletionHistory, - globalCfg.gui.onCompletionHistoryMax - }; - - if (showSyncConfigDlg(this, - currentCfg.mainCfg.cmpConfig.compareVar, - currentCfg.mainCfg.syncCfg, - _("Synchronization Settings"), - ¤tCfg.handleError, - &ewfCfg) == ReturnSyncConfig::BUTTON_OKAY) //optional input parameter - { - applySyncConfig(); - } -} - - -void MainDialog::applyCompareConfig(bool switchMiddleGrid) -{ - clearGrid(); //+ GUI update - - //convenience: change sync view - if (switchMiddleGrid) - switch (currentCfg.mainCfg.cmpConfig.compareVar) - { - case CMP_BY_TIME_SIZE: - setViewTypeSyncAction(true); - break; - case CMP_BY_CONTENT: - setViewTypeSyncAction(false); - break; - } -} - - -void MainDialog::OnCmpSettings(wxCommandEvent& event) -{ - //show window right next to the compare-config button - //wxPoint windowPos = m_bpButtonCmpConfig->GetScreenPosition(); - //windowPos.x += m_bpButtonCmpConfig->GetSize().GetWidth() + 5; - - CompConfig cmpConfigNew = currentCfg.mainCfg.cmpConfig; - - if (zen::showCompareCfgDialog(this, cmpConfigNew, _("Comparison Settings")) == ReturnSmallDlg::BUTTON_OKAY && - //check if settings were changed at all - cmpConfigNew != currentCfg.mainCfg.cmpConfig) - { - currentCfg.mainCfg.cmpConfig = cmpConfigNew; - - applyCompareConfig(true); //true: switchMiddleGrid - } -} - - -void MainDialog::OnStartSync(wxCommandEvent& event) -{ - if (folderCmp.empty()) - { - //quick sync: simulate button click on "compare" - wxCommandEvent dummy2(wxEVT_COMMAND_BUTTON_CLICKED); - if (wxEvtHandler* evtHandler = m_buttonCompare->GetEventHandler()) - evtHandler->ProcessEvent(dummy2); //synchronous call - - if (folderCmp.empty()) //check if user aborted or error occurred, ect... - return; - } - - //show sync preview/confirmation dialog - if (globalCfg.optDialogs.confirmSyncStart) - { - bool dontShowAgain = false; - - if (zen::showSyncConfirmationDlg(this, - getConfig().mainCfg.getSyncVariantName(), - zen::SyncStatistics(folderCmp), - dontShowAgain) != ReturnSmallDlg::BUTTON_OKAY) - return; - - globalCfg.optDialogs.confirmSyncStart = !dontShowAgain; - } - - try - { - //PERF_START; - const Zstring activeCfgFilename = activeConfigFiles.size() == 1 && activeConfigFiles[0] != lastRunConfigName() ? activeConfigFiles[0] : Zstring(); - - const auto& guiCfg = getConfig(); - - disableAllElements(false); //SyncStatusHandler will internally process Window messages, so avoid unexpected callbacks! - ZEN_ON_SCOPE_EXIT(enableAllElements()); - - //class handling status updates and error messages - SyncStatusHandler statusHandler(this, //throw GuiAbortProcess - globalCfg.lastSyncsLogFileSizeMax, - currentCfg.handleError, - globalCfg.automaticRetryCount, - globalCfg.automaticRetryDelay, - xmlAccess::extractJobName(activeCfgFilename), - guiCfg.mainCfg.onCompletion, - globalCfg.gui.onCompletionHistory); - - //wxBusyCursor dummy; -> redundant: progress already shown in progress dialog! - - //GUI mode: place directory locks on directories isolated(!) during both comparison and synchronization - std::unique_ptr<LockHolder> dirLocks; - if (globalCfg.createLockFile) - { - std::set<Zstring, LessFilename> dirnamesExisting; - for (auto it = begin(folderCmp); it != end(folderCmp); ++it) - { - if (it->isExisting<LEFT_SIDE>()) //do NOT check directory existence again! - dirnamesExisting.insert(it->getBaseDirPf<LEFT_SIDE >()); - if (it->isExisting<RIGHT_SIDE>()) - dirnamesExisting.insert(it->getBaseDirPf<RIGHT_SIDE>()); - } - dirLocks = make_unique<LockHolder>(dirnamesExisting, globalCfg.optDialogs.warningDirectoryLockFailed, statusHandler); - } - - //START SYNCHRONIZATION - const std::vector<zen::FolderPairSyncCfg> syncProcessCfg = zen::extractSyncCfg(guiCfg.mainCfg); - if (syncProcessCfg.size() != folderCmp.size()) - throw std::logic_error("Programming Error: Contract violation! " + std::string(__FILE__) + ":" + numberTo<std::string>(__LINE__)); - //should never happen: sync button is deactivated if they are not in sync - - synchronize(localTime(), - globalCfg.optDialogs, - globalCfg.verifyFileCopy, - globalCfg.copyLockedFiles, - globalCfg.copyFilePermissions, - globalCfg.failsafeFileCopy, - globalCfg.runWithBackgroundPriority, - syncProcessCfg, - folderCmp, - statusHandler); - } - catch (GuiAbortProcess&) - { - //do NOT disable the sync button: user might want to try to sync the REMAINING rows - } //enableSynchronization(false); - - //remove empty rows: just a beautification, invalid rows shouldn't cause issues - gridDataView->removeInvalidRows(); - - updateGui(); -} - - -void MainDialog::onGridDoubleClickL(GridClickEvent& event) -{ - onGridDoubleClickRim(event.row_, true); -} -void MainDialog::onGridDoubleClickR(GridClickEvent& event) -{ - onGridDoubleClickRim(event.row_, false); -} - -void MainDialog::onGridDoubleClickRim(size_t row, bool leftSide) -{ - if (!globalCfg.gui.externelApplications.empty()) - { - std::vector<FileSystemObject*> selection; - if (FileSystemObject* fsObj = gridDataView->getObject(row)) //selection must be a list of BOUND pointers! - selection.push_back(fsObj); - - openExternalApplication(globalCfg.gui.externelApplications[0].second, selection, leftSide); - } -} - - -void MainDialog::onGridLabelLeftClick(bool onLeft, ColumnTypeRim type) -{ - auto sortInfo = gridDataView->getSortInfo(); - - bool sortAscending = GridView::getDefaultSortDirection(type); - if (sortInfo && sortInfo->onLeft_ == onLeft && sortInfo->type_ == type) - sortAscending = !sortInfo->ascending_; - - gridDataView->sortView(type, onLeft, sortAscending); - - gridview::clearSelection(*m_gridMainL, *m_gridMainC, *m_gridMainR); - updateGui(); //refresh gridDataView -} - -void MainDialog::onGridLabelLeftClickL(GridClickEvent& event) -{ - onGridLabelLeftClick(true, static_cast<ColumnTypeRim>(event.colType_)); -} -void MainDialog::onGridLabelLeftClickR(GridClickEvent& event) -{ - onGridLabelLeftClick(false, static_cast<ColumnTypeRim>(event.colType_)); -} - - -void MainDialog::onGridLabelLeftClickC(GridClickEvent& event) -{ - //sorting middle grid is more or less useless: therefore let's toggle view instead! - setViewTypeSyncAction(!m_bpButtonViewTypeSyncAction->isActive()); //toggle view -} - - -void MainDialog::OnSwapSides(wxCommandEvent& event) -{ - //swap directory names: first pair - firstFolderPair->setValues(firstFolderPair->getRightDir(), // swap directories - firstFolderPair->getLeftDir(), // - firstFolderPair->getAltCompConfig(), - firstFolderPair->getAltSyncConfig(), - firstFolderPair->getAltFilterConfig()); - - //additional pairs - for (auto it = additionalFolderPairs.begin(); it != additionalFolderPairs.end(); ++it) - { - FolderPairPanel* panel = *it; - panel->setValues(panel->getRightDir(), // swap directories - panel->getLeftDir(), // - panel->getAltCompConfig(), - panel->getAltSyncConfig(), - panel->getAltFilterConfig()); - } - - //swap view filter - bool tmp = m_bpButtonShowLeftOnly->isActive(); - m_bpButtonShowLeftOnly->setActive(m_bpButtonShowRightOnly->isActive()); - m_bpButtonShowRightOnly->setActive(tmp); - - tmp = m_bpButtonShowLeftNewer->isActive(); - m_bpButtonShowLeftNewer->setActive(m_bpButtonShowRightNewer->isActive()); - m_bpButtonShowRightNewer->setActive(tmp); - - /* for sync preview and "mirror" variant swapping may create strange effect: - tmp = m_bpButtonShowCreateLeft->isActive(); - m_bpButtonShowCreateLeft->setActive(m_bpButtonShowCreateRight->isActive()); - m_bpButtonShowCreateRight->setActive(tmp); - - tmp = m_bpButtonShowDeleteLeft->isActive(); - m_bpButtonShowDeleteLeft->setActive(m_bpButtonShowDeleteRight->isActive()); - m_bpButtonShowDeleteRight->setActive(tmp); - - tmp = m_bpButtonShowUpdateLeft->isActive(); - m_bpButtonShowUpdateLeft->setActive(m_bpButtonShowUpdateRight->isActive()); - m_bpButtonShowUpdateRight->setActive(tmp); - */ - - //swap grid information - zen::swapGrids(getConfig().mainCfg, folderCmp); - - updateGui(); -} - - -void MainDialog::updateGridViewData() -{ - size_t filesOnLeftView = 0; - size_t foldersOnLeftView = 0; - size_t filesOnRightView = 0; - size_t foldersOnRightView = 0; - zen::UInt64 filesizeLeftView; - zen::UInt64 filesizeRightView; - - //disable all buttons per default - m_bpButtonShowLeftOnly ->Show(false); - m_bpButtonShowRightOnly ->Show(false); - m_bpButtonShowLeftNewer ->Show(false); - m_bpButtonShowRightNewer->Show(false); - m_bpButtonShowDifferent ->Show(false); - m_bpButtonShowEqual ->Show(false); - m_bpButtonShowConflict ->Show(false); - - m_bpButtonShowCreateLeft ->Show(false); - m_bpButtonShowCreateRight->Show(false); - m_bpButtonShowDeleteLeft ->Show(false); - m_bpButtonShowDeleteRight->Show(false); - m_bpButtonShowUpdateLeft ->Show(false); - m_bpButtonShowUpdateRight->Show(false); - m_bpButtonShowDoNothing ->Show(false); - - if (m_bpButtonViewTypeSyncAction->isActive()) - { - const GridView::StatusSyncPreview result = gridDataView->updateSyncPreview(currentCfg.hideExcludedItems, - m_bpButtonShowCreateLeft ->isActive(), - m_bpButtonShowCreateRight->isActive(), - m_bpButtonShowDeleteLeft ->isActive(), - m_bpButtonShowDeleteRight->isActive(), - m_bpButtonShowUpdateLeft ->isActive(), - m_bpButtonShowUpdateRight->isActive(), - m_bpButtonShowDoNothing ->isActive(), - m_bpButtonShowEqual ->isActive(), - m_bpButtonShowConflict ->isActive()); - filesOnLeftView = result.filesOnLeftView; - foldersOnLeftView = result.foldersOnLeftView; - filesOnRightView = result.filesOnRightView; - foldersOnRightView = result.foldersOnRightView; - filesizeLeftView = result.filesizeLeftView; - filesizeRightView = result.filesizeRightView; - - - //sync preview buttons - m_bpButtonShowCreateLeft ->Show(result.existsSyncCreateLeft); - m_bpButtonShowCreateRight ->Show(result.existsSyncCreateRight); - m_bpButtonShowDeleteLeft ->Show(result.existsSyncDeleteLeft); - m_bpButtonShowDeleteRight ->Show(result.existsSyncDeleteRight); - m_bpButtonShowUpdateLeft ->Show(result.existsSyncDirLeft); - m_bpButtonShowUpdateRight ->Show(result.existsSyncDirRight); - m_bpButtonShowDoNothing ->Show(result.existsSyncDirNone); - m_bpButtonShowEqual ->Show(result.existsSyncEqual); - m_bpButtonShowConflict ->Show(result.existsConflict); - - const bool anyViewFilterButtonShown = m_bpButtonShowCreateLeft ->IsShown() || - m_bpButtonShowCreateRight->IsShown() || - m_bpButtonShowDeleteLeft ->IsShown() || - m_bpButtonShowDeleteRight->IsShown() || - m_bpButtonShowUpdateLeft ->IsShown() || - m_bpButtonShowUpdateRight->IsShown() || - m_bpButtonShowDoNothing ->IsShown() || - m_bpButtonShowEqual ->IsShown() || - m_bpButtonShowConflict ->IsShown(); - m_bpButtonViewTypeSyncAction->Show(anyViewFilterButtonShown); - - if (anyViewFilterButtonShown) - { - m_panelViewFilter->Show(); - m_panelViewFilter->Layout(); - } - else - m_panelViewFilter->Hide(); - } - else - { - const GridView::StatusCmpResult result = gridDataView->updateCmpResult(currentCfg.hideExcludedItems, - m_bpButtonShowLeftOnly ->isActive(), - m_bpButtonShowRightOnly ->isActive(), - m_bpButtonShowLeftNewer ->isActive(), - m_bpButtonShowRightNewer->isActive(), - m_bpButtonShowDifferent ->isActive(), - m_bpButtonShowEqual ->isActive(), - m_bpButtonShowConflict ->isActive()); - filesOnLeftView = result.filesOnLeftView; - foldersOnLeftView = result.foldersOnLeftView; - filesOnRightView = result.filesOnRightView; - foldersOnRightView = result.foldersOnRightView; - filesizeLeftView = result.filesizeLeftView; - filesizeRightView = result.filesizeRightView; - - //comparison result view buttons - m_bpButtonShowLeftOnly ->Show(result.existsLeftOnly); - m_bpButtonShowRightOnly ->Show(result.existsRightOnly); - m_bpButtonShowLeftNewer ->Show(result.existsLeftNewer); - m_bpButtonShowRightNewer->Show(result.existsRightNewer); - m_bpButtonShowDifferent ->Show(result.existsDifferent); - m_bpButtonShowEqual ->Show(result.existsEqual); - m_bpButtonShowConflict ->Show(result.existsConflict); - - const bool anyViewFilterButtonShown = m_bpButtonShowLeftOnly ->IsShown() || - m_bpButtonShowRightOnly ->IsShown() || - m_bpButtonShowLeftNewer ->IsShown() || - m_bpButtonShowRightNewer->IsShown() || - m_bpButtonShowDifferent ->IsShown() || - m_bpButtonShowEqual ->IsShown() || - m_bpButtonShowConflict ->IsShown(); - m_bpButtonViewTypeSyncAction->Show(anyViewFilterButtonShown); - - if (anyViewFilterButtonShown) - { - m_panelViewFilter->Show(); - m_panelViewFilter->Layout(); - } - else - m_panelViewFilter->Hide(); - } - //all three grids retrieve their data directly via gridDataView - gridview::refresh(*m_gridMainL, *m_gridMainC, *m_gridMainR); - - //navigation tree - if (m_bpButtonViewTypeSyncAction->isActive()) - treeDataView->updateSyncPreview(currentCfg.hideExcludedItems, - m_bpButtonShowCreateLeft ->isActive(), - m_bpButtonShowCreateRight->isActive(), - m_bpButtonShowDeleteLeft ->isActive(), - m_bpButtonShowDeleteRight->isActive(), - m_bpButtonShowUpdateLeft ->isActive(), - m_bpButtonShowUpdateRight->isActive(), - m_bpButtonShowDoNothing ->isActive(), - m_bpButtonShowEqual ->isActive(), - m_bpButtonShowConflict ->isActive()); - else - treeDataView->updateCmpResult(currentCfg.hideExcludedItems, - m_bpButtonShowLeftOnly ->isActive(), - m_bpButtonShowRightOnly ->isActive(), - m_bpButtonShowLeftNewer ->isActive(), - m_bpButtonShowRightNewer->isActive(), - m_bpButtonShowDifferent ->isActive(), - m_bpButtonShowEqual ->isActive(), - m_bpButtonShowConflict ->isActive()); - m_gridNavi->Refresh(); - - //update status bar information - setStatusBarFileStatistics(filesOnLeftView, - foldersOnLeftView, - filesOnRightView, - foldersOnRightView, - filesizeLeftView, - filesizeRightView); -} - - -void MainDialog::applyFilterConfig() -{ - applyFiltering(folderCmp, getConfig().mainCfg); - updateGui(); - //updateGuiDelayedIf(currentCfg.hideExcludedItems); //show update GUI before removing rows -} - - -void MainDialog::applySyncConfig() -{ - zen::redetermineSyncDirection(getConfig().mainCfg, folderCmp, - [&](const std::wstring& warning) - { - bool& warningActive = globalCfg.optDialogs.warningDatabaseError; - if (warningActive) - { - bool dontWarnAgain = false; - - showNotificationDialog(this, DialogInfoType::WARNING, PopupDialogCfg().setDetailInstructions(warning).setCheckBox(dontWarnAgain, _("&Don't show this warning again"))); - warningActive = !dontWarnAgain; - } - }); - - updateGui(); -} - - -void MainDialog::OnMenuFindItem(wxCommandEvent& event) //CTRL + F -{ - showFindPanel(); -} - - -void MainDialog::OnSearchGridEnter(wxCommandEvent& event) -{ - startFindNext(); -} - - -void MainDialog::OnHideSearchPanel(wxCommandEvent& event) -{ - hideFindPanel(); -} - - -void MainDialog::OnSearchPanelKeyPressed(wxKeyEvent& event) -{ - switch (event.GetKeyCode()) - { - case WXK_RETURN: - case WXK_NUMPAD_ENTER: //catches ENTER keys while focus is on *any* part of m_panelSearch! Seems to obsolete OnHideSearchPanel()! - startFindNext(); - return; - case WXK_ESCAPE: - hideFindPanel(); - return; - } - event.Skip(); -} - - -void MainDialog::showFindPanel() //CTRL + F or F3 with empty search phrase -{ - auiMgr.GetPane(m_panelSearch).Show(); - auiMgr.Update(); - - m_textCtrlSearchTxt->SelectAll(); - - wxWindow* focus = wxWindow::FindFocus(); //restore when closing panel! - if (!isComponentOf(focus, m_panelSearch)) - focusWindowAfterSearch = focus == &m_gridMainR->getMainWin() ? focus : &m_gridMainL->getMainWin(); - //don't save pointer to arbitrary window: it might not exist anymore when hideFindPanel() uses it!!! (e.g. some folder pair panel) - m_textCtrlSearchTxt->SetFocus(); -} - - -void MainDialog::hideFindPanel() -{ - auiMgr.GetPane(m_panelSearch).Hide(); - auiMgr.Update(); - - if (focusWindowAfterSearch) - { - focusWindowAfterSearch->SetFocus(); - focusWindowAfterSearch = nullptr; - } -} - - -void MainDialog::startFindNext() //F3 or ENTER in m_textCtrlSearchTxt -{ - const wxString& searchString = m_textCtrlSearchTxt->GetValue(); - - if (searchString.empty()) - showFindPanel(); - else - { - Grid* grid1 = m_gridMainL; - Grid* grid2 = m_gridMainR; - - wxWindow* focus = wxWindow::FindFocus(); - if ((isComponentOf(focus, m_panelSearch) ? focusWindowAfterSearch : focus) == &m_gridMainR->getMainWin()) - std::swap(grid1, grid2); //select side to start search at grid cursor position - - wxBeginBusyCursor(wxHOURGLASS_CURSOR); - const std::pair<const Grid*, ptrdiff_t> result = findGridMatch(*grid1, *grid2, searchString, - m_checkBoxMatchCase->GetValue()); //parameter owned by GUI, *not* globalCfg structure! => we should better implement a getGlocalCfg()! - wxEndBusyCursor(); - - if (Grid* grid = const_cast<Grid*>(result.first)) //grid wasn't const when passing to findAndSelectNext(), so this is safe - { - assert(result.second >= 0); - - gridview::setScrollMaster(*grid); - grid->setGridCursor(result.second); - - focusWindowAfterSearch = &grid->getMainWin(); - - if (!isComponentOf(wxWindow::FindFocus(), m_panelSearch)) - grid->getMainWin().SetFocus(); - } - else - { - showFindPanel(); - showNotificationDialog(this, DialogInfoType::INFO, PopupDialogCfg(). - setTitle(_("Find")). - setMainInstructions(replaceCpy(_("Cannot find %x"), L"%x", L"\"" + searchString + L"\"", false))); - } - } -} - - -void MainDialog::OnAddFolderPair(wxCommandEvent& event) -{ -#ifdef ZEN_WIN - wxWindowUpdateLocker dummy(this); //leads to GUI corruption problems on Linux/OS X! -#endif - - std::vector<FolderPairEnh> newPairs; - newPairs.push_back(getConfig().mainCfg.firstPair); - - //clear first pair - const FolderPairEnh cfgEmpty; - firstFolderPair->setValues(cfgEmpty.leftDirectory, - cfgEmpty.rightDirectory, - cfgEmpty.altCmpConfig, - cfgEmpty.altSyncConfig, - cfgEmpty.localFilter); - - //keep sequence to update GUI as last step - addAddFolderPair(newPairs, true); //add pair in front of additonal pairs -} - - -void MainDialog::OnRemoveTopFolderPair(wxCommandEvent& event) -{ - if (!additionalFolderPairs.empty()) - { -#ifdef ZEN_WIN - wxWindowUpdateLocker dummy(this); //leads to GUI corruption problems on Linux/OS X! -#endif - - //get settings from second folder pair - const FolderPairEnh cfgSecond = getEnhancedPair(additionalFolderPairs[0]); - - //reset first pair - firstFolderPair->setValues(cfgSecond.leftDirectory, - cfgSecond.rightDirectory, - cfgSecond.altCmpConfig, - cfgSecond.altSyncConfig, - cfgSecond.localFilter); - - removeAddFolderPair(0); //remove second folder pair (first of additional folder pairs) - } -} - - -void MainDialog::OnRemoveFolderPair(wxCommandEvent& event) -{ -#ifdef ZEN_WIN - wxWindowUpdateLocker dummy(this); //leads to GUI corruption problems on Linux/OS X! -#endif - - 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) - { - removeAddFolderPair(it - additionalFolderPairs.begin()); - break; - } -} - - -void MainDialog::updateGuiForFolderPair() -{ -#ifdef ZEN_WIN - wxWindowUpdateLocker dummy(this); //leads to GUI corruption problems on Linux/OS X! -#endif - - //adapt delete top folder pair button - m_bpButtonRemovePair->Show(!additionalFolderPairs.empty()); - m_panelTopLeft->Layout(); - - //adapt local filter and sync cfg for first folder pair - const bool showLocalCfgFirstPair = !additionalFolderPairs.empty() || - firstFolderPair->getAltCompConfig().get() != nullptr || - firstFolderPair->getAltSyncConfig().get() != nullptr || - !isNullFilter(firstFolderPair->getAltFilterConfig()); - - m_bpButtonAltCompCfg ->Show(showLocalCfgFirstPair); - m_bpButtonAltSyncCfg ->Show(showLocalCfgFirstPair); - m_bpButtonLocalFilter->Show(showLocalCfgFirstPair); - setImage(*m_bpButtonSwapSides, getResourceImage(showLocalCfgFirstPair ? L"swap_slim" : L"swap")); - - //update sub-panel sizes for calculations below!!! - m_panelTopMiddle->GetSizer()->SetSizeHints(m_panelTopMiddle); //~=Fit() + SetMinSize() - - int addPairMinimalHeight = 0; - int addPairOptimalHeight = 0; - if (!additionalFolderPairs.empty()) - { - const int pairHeight = additionalFolderPairs[0]->GetSize().GetHeight(); - addPairMinimalHeight = std::min<double>(1.5, additionalFolderPairs.size()) * pairHeight; //have 1.5 * height indicate that more folders are there - addPairOptimalHeight = std::min<double>(globalCfg.gui.maxFolderPairsVisible - 1 + 0.5, //subtract first/main folder pair and add 0.5 to indicate additional folders - additionalFolderPairs.size()) * pairHeight; - - addPairOptimalHeight = std::max(addPairOptimalHeight, addPairMinimalHeight); //implicitly handle corrupted values for "maxFolderPairsVisible" - } - - const int firstPairHeight = std::max(m_panelDirectoryPairs->ClientToWindowSize(m_panelTopLeft ->GetSize()).GetHeight(), //include m_panelDirectoryPairs window borders! - m_panelDirectoryPairs->ClientToWindowSize(m_panelTopMiddle->GetSize()).GetHeight()); // - - //######################################################################################################################## - //wxAUI hack: set minimum height to desired value, then call wxAuiPaneInfo::Fixed() to apply it - auiMgr.GetPane(m_panelDirectoryPairs).MinSize(-1, firstPairHeight + addPairOptimalHeight); - auiMgr.GetPane(m_panelDirectoryPairs).Fixed(); - auiMgr.Update(); - - //now make resizable again - auiMgr.GetPane(m_panelDirectoryPairs).Resizable(); - auiMgr.Update(); - //######################################################################################################################## - - //make sure user cannot fully shrink additional folder pairs - auiMgr.GetPane(m_panelDirectoryPairs).MinSize(-1, firstPairHeight + addPairMinimalHeight); - auiMgr.Update(); - - //it seems there is no GetSizer()->SetSizeHints(this)/Fit() required due to wxAui "magic" - //=> *massive* perf improvement on OS X! -} - - -void MainDialog::addAddFolderPair(const std::vector<FolderPairEnh>& newPairs, bool addFront) -{ -#ifdef ZEN_WIN - wxWindowUpdateLocker dummy(m_panelDirectoryPairs); //leads to GUI corruption problems on Linux/OS X! -#endif - - std::vector<FolderPairPanel*> newEntries; - - std::for_each(newPairs.begin(), newPairs.end(), - [&](const FolderPairEnh& enhPair) - { - //add new folder pair - FolderPairPanel* newPair = new FolderPairPanel(m_scrolledWindowFolderPairs, *this); - - //init dropdown history - newPair->m_directoryLeft ->init(folderHistoryLeft); - newPair->m_directoryRight->init(folderHistoryRight); - - //set width of left folder panel - const int width = m_panelTopLeft->GetSize().GetWidth(); - newPair->m_panelLeft->SetMinSize(wxSize(width, -1)); - - if (addFront) - { - bSizerAddFolderPairs->Insert(0, newPair, 0, wxEXPAND); - additionalFolderPairs.insert(additionalFolderPairs.begin(), newPair); - } - else - { - bSizerAddFolderPairs->Add(newPair, 0, wxEXPAND); - additionalFolderPairs.push_back(newPair); - } - newEntries.push_back(newPair); - - //register events - newPair->m_bpButtonRemovePair->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(MainDialog::OnRemoveFolderPair), nullptr, this); - }); - - updateGuiForFolderPair(); - - //wxComboBox screws up miserably if width/height is smaller than the magic number 4! Problem occurs when trying to set tooltip - //so we have to update window sizes before setting configuration: - for (auto it = newPairs.begin(); it != newPairs.end(); ++it)//set alternate configuration - newEntries[it - newPairs.begin()]->setValues(it->leftDirectory, - it->rightDirectory, - it->altCmpConfig, - it->altSyncConfig, - it->localFilter); - clearGrid(); //+ GUI update -} - - -void MainDialog::removeAddFolderPair(size_t pos) -{ -#ifdef ZEN_WIN - wxWindowUpdateLocker dummy(m_panelDirectoryPairs); //leads to GUI corruption problems on Linux/OS X! -#endif - - if (pos < additionalFolderPairs.size()) - { - FolderPairPanel* panel = additionalFolderPairs[pos]; - - bSizerAddFolderPairs->Detach(panel); //Remove() does not work on Window*, so do it manually - additionalFolderPairs.erase(additionalFolderPairs.begin() + pos); - //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 "panel" we need to delay deletion ourselves: - processAsync2([] {}, [panel] { panel->Destroy(); }); - } - - updateGuiForFolderPair(); - - clearGrid(); //+ GUI update -} - - -void MainDialog::setAddFolderPairs(const std::vector<zen::FolderPairEnh>& newPairs) -{ -#ifdef ZEN_WIN - wxWindowUpdateLocker dummy(m_panelDirectoryPairs); //leads to GUI corruption problems on Linux/OS X! -#endif - - additionalFolderPairs.clear(); - bSizerAddFolderPairs->Clear(true); - - //updateGuiForFolderPair(); -> already called in addAddFolderPair() - - addAddFolderPair(newPairs); -} - - -//######################################################################################################## - - -//menu events -void MainDialog::OnMenuGlobalSettings(wxCommandEvent& event) -{ - zen::showGlobalSettingsDlg(this, globalCfg); -} - - -void MainDialog::OnMenuExportFileList(wxCommandEvent& event) -{ - //get a filename - wxFileDialog filePicker(this, //creating this on freestore leads to memleak! - wxEmptyString, - wxEmptyString, - 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) - return; - - wxBusyCursor dummy; - - const Zstring filename = utfCvrtTo<Zstring>(filePicker.GetPath()); - - //http://en.wikipedia.org/wiki/Comma-separated_values - const lconv* localInfo = ::localeconv(); //always bound according to doc - const bool haveCommaAsDecimalSep = std::string(localInfo->decimal_point) == ","; - - const char CSV_SEP = haveCommaAsDecimalSep ? ';' : ','; - - auto fmtValue = [&](const wxString& val) -> Utf8String - { - Utf8String&& tmp = utfCvrtTo<Utf8String>(val); - - if (contains(tmp, CSV_SEP)) - return '\"' + tmp + '\"'; - else - return tmp; - }; - - Utf8String header; //perf: wxString doesn't model exponential growth and so is out, std::string doesn't give performance guarantee! - header += BYTE_ORDER_MARK_UTF8; - - //base folders - header += fmtValue(_("Folder Pairs")) + '\n' ; - std::for_each(begin(folderCmp), end(folderCmp), - [&](BaseDirPair& baseDirObj) - { - header += utfCvrtTo<Utf8String>(baseDirObj.getBaseDirPf<LEFT_SIDE >()) + CSV_SEP; - header += utfCvrtTo<Utf8String>(baseDirObj.getBaseDirPf<RIGHT_SIDE>()) + '\n'; - }); - header += '\n'; - - //write header - auto provLeft = m_gridMainL->getDataProvider(); - auto provMiddle = m_gridMainC->getDataProvider(); - auto provRight = m_gridMainR->getDataProvider(); - - auto colAttrLeft = m_gridMainL->getColumnConfig(); - auto colAttrMiddle = m_gridMainC->getColumnConfig(); - auto colAttrRight = m_gridMainR->getColumnConfig(); - - vector_remove_if(colAttrLeft , [](const Grid::ColumnAttribute& ca) { return !ca.visible_; }); - vector_remove_if(colAttrMiddle, [](const Grid::ColumnAttribute& ca) { return !ca.visible_ || static_cast<ColumnTypeMiddle>(ca.type_) == COL_TYPE_CHECKBOX; }); - vector_remove_if(colAttrRight , [](const Grid::ColumnAttribute& ca) { return !ca.visible_; }); - - if (provLeft && provMiddle && provRight) - { - std::for_each(colAttrLeft.begin(), colAttrLeft.end(), - [&](const Grid::ColumnAttribute& ca) - { - header += fmtValue(provLeft->getColumnLabel(ca.type_)); - header += CSV_SEP; - }); - std::for_each(colAttrMiddle.begin(), colAttrMiddle.end(), - [&](const Grid::ColumnAttribute& ca) - { - header += fmtValue(provMiddle->getColumnLabel(ca.type_)); - header += CSV_SEP; - }); - if (!colAttrRight.empty()) - { - std::for_each(colAttrRight.begin(), colAttrRight.end() - 1, - [&](const Grid::ColumnAttribute& ca) - { - header += fmtValue(provRight->getColumnLabel(ca.type_)); - header += CSV_SEP; - }); - header += fmtValue(provRight->getColumnLabel(colAttrRight.back().type_)); - } - header += '\n'; - - try - { - //write file - FileOutput fileOut(filename, zen::FileOutput::ACC_OVERWRITE); //throw FileError - - replace(header, '\n', LINE_BREAK); - fileOut.write(&*header.begin(), header.size()); //throw FileError - - //main grid: write rows one after the other instead of creating one big string: memory allocation might fail; think 1 million rows! - /* - performance test case "export 600.000 rows" to CSV: - aproach 1. assemble single temporary string, then write file: 4.6s - aproach 2. write to buffered file output directly for each row: 6.4s - */ - const size_t rowCount = m_gridMainL->getRowCount(); - for (size_t row = 0; row < rowCount; ++row) - { - Utf8String tmp; - - std::for_each(colAttrLeft.begin(), colAttrLeft.end(), - [&](const Grid::ColumnAttribute& ca) - { - tmp += fmtValue(provLeft->getValue(row, ca.type_)); - tmp += CSV_SEP; - }); - std::for_each(colAttrMiddle.begin(), colAttrMiddle.end(), - [&](const Grid::ColumnAttribute& ca) - { - tmp += fmtValue(provMiddle->getValue(row, ca.type_)); - tmp += CSV_SEP; - }); - std::for_each(colAttrRight.begin(), colAttrRight.end(), - [&](const Grid::ColumnAttribute& ca) - { - tmp += fmtValue(provRight->getValue(row, ca.type_)); - tmp += CSV_SEP; - }); - tmp += '\n'; - - replace(tmp, '\n', LINE_BREAK); - fileOut.write(&*tmp.begin(), tmp.size()); //throw FileError - } - - flashStatusInformation(_("File list exported")); - } - catch (const FileError& e) - { - showNotificationDialog(this, DialogInfoType::ERROR2, PopupDialogCfg().setDetailInstructions(e.toString())); - } - } -} - - -void MainDialog::OnMenuCheckVersion(wxCommandEvent& event) -{ - zen::checkForUpdateNow(this); -} - - -void MainDialog::OnMenuCheckVersionAutomatically(wxCommandEvent& event) -{ - globalCfg.gui.lastUpdateCheck = globalCfg.gui.lastUpdateCheck == -1 ? 0 : -1; - m_menuItemCheckVersionAuto->Check(globalCfg.gui.lastUpdateCheck != -1); - zen::checkForUpdatePeriodically(this, globalCfg.gui.lastUpdateCheck, [&] { flashStatusInformation(_("Searching for program updates...")); }); -} - - -void MainDialog::OnRegularUpdateCheck(wxIdleEvent& event) -{ - //execute just once per startup! - Disconnect(wxEVT_IDLE, wxIdleEventHandler(MainDialog::OnRegularUpdateCheck), nullptr, this); - - if (manualProgramUpdateRequired()) - zen::checkForUpdatePeriodically(this, globalCfg.gui.lastUpdateCheck, [&] { flashStatusInformation(_("Searching for program updates...")); }); -} - - -void MainDialog::OnLayoutWindowAsync(wxIdleEvent& event) -{ - //execute just once per startup! - Disconnect(wxEVT_IDLE, wxIdleEventHandler(MainDialog::OnLayoutWindowAsync), nullptr, this); - -#ifdef ZEN_WIN - wxWindowUpdateLocker dummy(this); //leads to GUI corruption problems on Linux/OS X! -#endif - - //adjust folder pair distortion on startup - std::for_each(additionalFolderPairs.begin(), additionalFolderPairs.end(), - [](FolderPairPanel* panel) { panel->Layout(); }); - - m_panelTopButtons->Layout(); - Layout(); //strangely this layout call works if called in next idle event only - auiMgr.Update(); //fix view filter distortion -} - - -void MainDialog::OnMenuAbout(wxCommandEvent& event) -{ - zen::showAboutDialog(this); -} - - -void MainDialog::OnShowHelp(wxCommandEvent& event) -{ - zen::displayHelpEntry(this); -} - -//######################################################################################################### - -//language selection -void MainDialog::switchProgramLanguage(int langID) -{ - //create new dialog with respect to new language - xmlAccess::XmlGlobalSettings newGlobalCfg = getGlobalCfgBeforeExit(); - newGlobalCfg.programLanguage = langID; - - //show new dialog, then delete old one - MainDialog::create(getConfig(), activeConfigFiles, &newGlobalCfg, false); - - //we don't use Close(): - //1. we don't want to show the prompt to save current config in OnClose() - //2. after getGlobalCfgBeforeExit() the old main dialog is invalid so we want to force deletion - Destroy(); -} - - -void MainDialog::OnMenuLanguageSwitch(wxCommandEvent& event) -{ - std::map<MenuItemID, LanguageID>::const_iterator it = languageMenuItemMap.find(event.GetId()); - if (it != languageMenuItemMap.end()) - switchProgramLanguage(it->second); -} - -//######################################################################################################### - -void MainDialog::setViewTypeSyncAction(bool value) -{ - //if (m_bpButtonViewTypeSyncAction->isActive() == value) return; support polling -> what about initialization? - - m_bpButtonViewTypeSyncAction->setActive(value); - m_bpButtonViewTypeSyncAction->SetToolTip((value ? _("Action") : _("Category")) + L" (F9)"); - - //toggle display of sync preview in middle grid - gridview::highlightSyncAction(*m_gridMainC, value); - - updateGui(); -} |