// ************************************************************************** // * 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 #include #include //#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 #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 { 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& droppedFiles, const wxPoint& clientPos, const wxWindow& wnd) { if (std::any_of(droppedFiles.begin(), droppedFiles.end(), [](const wxString& filename) { using namespace xmlAccess; switch (getXmlType(utfCvrtTo(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 FolderPairCallback : public FolderPairPanelBasic //implements callback functionality to MainDialog as imposed by FolderPairPanelBasic { public: FolderPairCallback(GuiPanel& basicPanel, MainDialog& mainDialog) : FolderPairPanelBasic(basicPanel), //pass FolderPairPanelGenerated part... mainDlg(mainDialog) {} private: virtual MainConfiguration getMainConfig() const { return mainDlg.getConfig().mainCfg; } virtual wxWindow* getParentWindow() { return &mainDlg; } virtual std::unique_ptr& 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 { public: FolderPairPanel(wxWindow* parent, MainDialog& mainDialog) : FolderPairPanelGenerated(parent), FolderPairCallback(static_cast(*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 dirNameLeft; DirectoryName dirNameRight; }; class FolderPairFirst : public FolderPairCallback { public: FolderPairFirst(MainDialog& mainDialog) : FolderPairCallback(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(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 filenames = globalSettings.gui.lastUsedConfigFiles; //2. now try last used files //------------------------------------------------------------------------------------------ //check existence of all files in parallel: RunUntilFirstHit findFirstMissing; std::for_each(filenames.begin(), filenames.end(), [&](const Zstring& filename) { findFirstMissing.addJob([=] { return filename.empty() /*ever empty??*/ || !fileExists(filename) ? zen::make_unique() : 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& 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& referenceFiles, const xmlAccess::XmlGlobalSettings& globalSettings, bool startComparison) : MainDialogGenerated(nullptr), folderHistoryLeft (std::make_shared()), //make sure it is always bound folderHistoryRight(std::make_shared()), // 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(*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(); treeDataView = std::make_shared(); 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(*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 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() : nullptr; }); if (!dirRight.empty()) findFirstMissing.addJob([=] { return !dirExists(dirRight) ? zen::make_unique() : 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 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 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> 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 sortInfo = treeDataView->getSortDirection(); globalSettings.gui.naviLastSortColumn = sortInfo.first; globalSettings.gui.naviLastSortAscending = sortInfo.second; //-------------------------------------------------------------------------------- //write list of last used configuration files std::map historyDetail; //(cfg-file/last use index) for (unsigned int i = 0; i < m_listBoxHistory->GetCount(); ++i) if (auto clientString = dynamic_cast(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 history; std::transform(historyDetail.rbegin(), historyDetail.rend(), std::back_inserter(history), [](const std::pair& 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& 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& 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 zxString; //guaranteed exponential growth } void MainDialog::copySelectionToClipboard(const std::vector& gridRefs) { try { zxString clipboardString; auto addSelection = [&](const Grid& grid) { if (auto prov = grid.getDataProvider()) { std::vector colAttr = grid.getColumnConfig(); vector_remove_if(colAttr, [](const Grid::ColumnAttribute& ca) { return !ca.visible_; }); if (!colAttr.empty()) { const std::vector 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(prov->getValue(row, ca.type_)); clipboardString += L'\t'; }); clipboardString += copyStringTo(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(clipboardString))); //ownership passed } } catch (const std::bad_alloc& e) { showNotificationDialog(this, DialogInfoType::ERROR2, PopupDialogCfg().setMainInstructions(_("Out of memory.") + L" " + utfCvrtTo(e.what()))); } } std::vector MainDialog::getGridSelection(bool fromLeft, bool fromRight) const { std::set selectedRows; auto addSelection = [&](const Grid& grid) { const std::vector& 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 MainDialog::getTreeSelection() const { std::vector output; const std::vector& sel = m_gridNavi->getSelectedRows(); std::for_each(sel.begin(), sel.end(), [&](size_t row) { if (std::unique_ptr node = treeDataView->getLine(row)) { if (auto root = dynamic_cast(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(node.get())) output.push_back(&(dir->dirObj_)); else if (auto file = dynamic_cast(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& selectionLeft, const std::vector& selectionRight) { bool deleteOnBothSides = false; //let's keep this disabled by default -> don't save //=> clenup empty selection on either side: std::vector selectionLeftTmp; std::vector selectionRightTmp; std::copy_if(selectionLeft .begin(), selectionLeft .end(), std::back_inserter(selectionLeftTmp ), [](const FileSystemObject* fsObj) { return !fsObj->isEmpty(); }); std::copy_if(selectionRight.begin(), selectionRight.end(), std::back_inserter(selectionRightTmp), [](const FileSystemObject* fsObj) { return !fsObj->isEmpty(); }); 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 Zstring getExistingParentFolder(const FileSystemObject& fsObj) { const DirPair* dirObj = dynamic_cast(&fsObj); if (!dirObj) dirObj = dynamic_cast(&fsObj.parent()); while (dirObj) { if (!dirObj->isEmpty()) return dirObj->getFullName(); dirObj = dynamic_cast(&dirObj->parent()); } return fsObj.getBaseDirPf(); } } void MainDialog::openExternalApplication(const wxString& commandline, const std::vector& 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()) || (!leftSide && selectionTmp[0]->isEmpty())) { Zstring fallbackDir; if (selectionTmp.empty()) fallbackDir = leftSide ? getFormattedDirectoryName(firstFolderPair->getLeftDir()) : getFormattedDirectoryName(firstFolderPair->getRightDir()); else fallbackDir = leftSide ? getExistingParentFolder(*selectionTmp[0]) : getExistingParentFolder(*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() + 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() + fsObj->getObjRelativeName(); Zstring dir2 = beforeLast(path2, FILE_NAME_SEPARATOR); if (!leftSide) { std::swap(path1, path2); std::swap(dir1, dir2); } Zstring command = utfCvrtTo(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(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(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(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 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& 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 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& 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 node = treeDataView->getLine(event.rowFirst_)) { if (const TreeView::RootNode* root = dynamic_cast(node.get())) leadRow = gridDataView->findRowFirstChild(&(root->baseDirObj_)); else if (const TreeView::DirNode* dir = dynamic_cast(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(node.get())) { assert(!files->filesAndLinks_.empty()); if (!files->filesAndLinks_.empty()) leadRow = gridDataView->findRowDirect(files->filesAndLinks_[0]->getId()); } } if (leadRow >= 0) { leadRow = std::max(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 markedFilesAndLinks; //mark files/symlinks directly hash_set markedContainer; //mark full container including child-objects const std::vector& selection = m_gridNavi->getSelectedRows(); std::for_each(selection.begin(), selection.end(), [&](size_t row) { if (std::unique_ptr node = treeDataView->getLine(row)) { if (const TreeView::RootNode* root = dynamic_cast(node.get())) markedContainer.insert(&(root->baseDirObj_)); else if (const TreeView::DirNode* dir = dynamic_cast(node.get())) markedContainer.insert(&(dir->dirObj_)); else if (const TreeView::FilesNode* files = dynamic_cast(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(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(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(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(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(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(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(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(&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& 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(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(event.colType_), getDefaultColumnAttributesLeft()); } void MainDialog::onGridLabelContextR(GridClickEvent& event) { onGridLabelContext(*m_gridMainR, static_cast(event.colType_), getDefaultColumnAttributesRight()); } void MainDialog::onGridLabelContext(Grid& grid, ColumnTypeRim type, const std::vector& 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(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> 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(output); } void MainDialog::addFileToCfgHistory(const std::vector& 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(m_listBoxHistory->GetClientObject(i))) if (histData->lastUseIndex_ > lastUseIndexMax) lastUseIndexMax = histData->lastUseIndex_; std::deque 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 { for (unsigned int i = 0; i < m_listBoxHistory->GetCount(); ++i) if (auto histData = dynamic_cast(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(selections.size()); ++pos) if (m_listBoxHistory->IsSelected(pos) != selections[pos]) m_listBoxHistory->SetSelection(pos, selections[pos]); } void MainDialog::removeObsoleteCfgHistoryItems(const std::vector& filenames) { //don't use wxString: NOT thread-safe! (e.g. non-atomic ref-count) auto getMissingFilesAsync = [filenames]() -> std::vector { //boost::this_thread::sleep(boost::posix_time::millisec(5000)); //check existence of all config files in parallel! std::list> 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([=] { 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 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& files) { removeCfgHistoryItems(files); }); } void MainDialog::removeCfgHistoryItems(const std::vector& 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(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(beforeLast(defaultFileName, FILE_NAME_SEPARATOR)), //default dir; empty string if / not found utfCvrtTo(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(beforeLast(defaultFileName, FILE_NAME_SEPARATOR)), //default dir; empty string if / not found utfCvrtTo(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 instead of the original non-modified config selection setLastUsedConfig(std::vector(), 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(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 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()); } void MainDialog::OnLoadFromHistory(wxCommandEvent& event) { wxArrayInt selections; m_listBoxHistory->GetSelections(selections); std::vector filenames; std::for_each(selections.begin(), selections.end(), [&](int pos) { if (auto histData = dynamic_cast(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 filenames; std::for_each(selections.begin(), selections.end(), [&](int pos) { if (auto histData = dynamic_cast(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& 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 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(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 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 objects = gridDataView->getAllFileRef(selectedRows); setFilterManually(objects, event.setIncluded_); } } void MainDialog::onSetSyncDirection(SyncDirectionEvent& event) { std::set 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 objects = gridDataView->getAllFileRef(selectedRows); setSyncDirManually(objects, event.direction_); } } void MainDialog::setLastUsedConfig(const Zstring& filename, const xmlAccess::XmlGuiConfig& guiConfig) { std::vector filenames; filenames.push_back(filename); setLastUsedConfig(filenames, guiConfig); } void MainDialog::setLastUsedConfig(const std::vector& 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& 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(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(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(name)), background)); } inline wxBitmap buttonReleased(const std::string& name) { wxImage output = getResourceImage(utfCvrtTo(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 cmpConfig = zen::extractCompareCfg(getConfig().mainCfg); //GUI mode: place directory locks on directories isolated(!) during both comparison and synchronization std::unique_ptr 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(), *m_bitmapCreateLeft, L"so_create_left_small"); setValue(*m_staticTextUpdateLeft, st.getUpdate(), *m_bitmapUpdateLeft, L"so_update_left_small"); setValue(*m_staticTextDeleteLeft, st.getDelete(), *m_bitmapDeleteLeft, L"so_delete_left_small"); setValue(*m_staticTextCreateRight, st.getCreate(), *m_bitmapCreateRight, L"so_create_right_small"); setValue(*m_staticTextUpdateRight, st.getUpdate(), *m_bitmapUpdateRight, L"so_update_right_small"); setValue(*m_staticTextDeleteRight, st.getDelete(), *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 dirLocks; if (globalCfg.createLockFile) { std::set dirnamesExisting; for (auto it = begin(folderCmp); it != end(folderCmp); ++it) { if (it->isExisting()) //do NOT check directory existence again! dirnamesExisting.insert(it->getBaseDirPf()); if (it->isExisting()) dirnamesExisting.insert(it->getBaseDirPf()); } dirLocks = make_unique(dirnamesExisting, globalCfg.optDialogs.warningDirectoryLockFailed, statusHandler); } //START SYNCHRONIZATION const std::vector syncProcessCfg = zen::extractSyncCfg(guiCfg.mainCfg); if (syncProcessCfg.size() != folderCmp.size()) throw std::logic_error("Programming Error: Contract violation! " + std::string(__FILE__) + ":" + numberTo(__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 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(event.colType_)); } void MainDialog::onGridLabelLeftClickR(GridClickEvent& event) { onGridLabelLeftClick(false, static_cast(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 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(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 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(1.5, additionalFolderPairs.size()) * pairHeight; //have 1.5 * height indicate that more folders are there addPairOptimalHeight = std::min(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& newPairs, bool addFront) { #ifdef ZEN_WIN wxWindowUpdateLocker dummy(m_panelDirectoryPairs); //leads to GUI corruption problems on Linux/OS X! #endif std::vector 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& 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(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(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(baseDirObj.getBaseDirPf()) + CSV_SEP; header += utfCvrtTo(baseDirObj.getBaseDirPf()) + '\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(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::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(); }