diff options
author | Daniel Wilhelm <daniel@wili.li> | 2014-04-18 17:24:35 +0200 |
---|---|---|
committer | Daniel Wilhelm <daniel@wili.li> | 2014-04-18 17:24:35 +0200 |
commit | 460091fb0b2ff114cc741372f15bb43b702ea3b1 (patch) | |
tree | 0562c2eda4c66969c6e6d0910080db9f5b0def3e /ui | |
parent | 5.15 (diff) | |
download | FreeFileSync-460091fb0b2ff114cc741372f15bb43b702ea3b1.tar.gz FreeFileSync-460091fb0b2ff114cc741372f15bb43b702ea3b1.tar.bz2 FreeFileSync-460091fb0b2ff114cc741372f15bb43b702ea3b1.zip |
5.16
Diffstat (limited to 'ui')
-rw-r--r-- | ui/batch_status_handler.cpp | 128 | ||||
-rw-r--r-- | ui/batch_status_handler.h | 6 | ||||
-rw-r--r-- | ui/column_attr.h | 5 | ||||
-rw-r--r-- | ui/custom_grid.cpp | 493 | ||||
-rw-r--r-- | ui/custom_grid.h | 2 | ||||
-rw-r--r-- | ui/dir_name.cpp | 5 | ||||
-rw-r--r-- | ui/grid_view.cpp | 1 | ||||
-rw-r--r-- | ui/gui_generated.cpp | 30 | ||||
-rw-r--r-- | ui/gui_generated.h | 5 | ||||
-rw-r--r-- | ui/gui_status_handler.cpp | 120 | ||||
-rw-r--r-- | ui/gui_status_handler.h | 14 | ||||
-rw-r--r-- | ui/main_dlg.cpp | 346 | ||||
-rw-r--r-- | ui/main_dlg.h | 14 | ||||
-rw-r--r-- | ui/msg_popup.cpp | 68 | ||||
-rw-r--r-- | ui/msg_popup.h | 17 | ||||
-rw-r--r-- | ui/progress_indicator.cpp | 314 | ||||
-rw-r--r-- | ui/progress_indicator.h | 66 | ||||
-rw-r--r-- | ui/small_dlgs.cpp | 17 | ||||
-rw-r--r-- | ui/sorting.h | 2 | ||||
-rw-r--r-- | ui/sync_cfg.cpp | 4 | ||||
-rw-r--r-- | ui/tree_view.cpp | 105 |
21 files changed, 995 insertions, 767 deletions
diff --git a/ui/batch_status_handler.cpp b/ui/batch_status_handler.cpp index 0edd289f..e06a0000 100644 --- a/ui/batch_status_handler.cpp +++ b/ui/batch_status_handler.cpp @@ -38,7 +38,8 @@ private: virtual std::shared_ptr<TraverseCallback> onDir (const Zchar* shortName, const Zstring& fullName) { return nullptr; } //DON'T traverse into subdirs virtual HandleLink onSymlink(const Zchar* shortName, const Zstring& fullName, const SymlinkInfo& details) { return LINK_SKIP; } - virtual HandleError onError (const std::wstring& msg) { assert(false); return ON_ERROR_IGNORE; } //errors are not really critical in this context + virtual HandleError reportDirError (const std::wstring& msg) { assert(false); return ON_ERROR_IGNORE; } //errors are not really critical in this context + virtual HandleError reportItemError(const std::wstring& msg, const Zchar* shortName) { assert(false); return ON_ERROR_IGNORE; } // const Zstring prefix_; std::vector<Zstring>& logfiles_; @@ -112,13 +113,14 @@ BatchStatusHandler::BatchStatusHandler(bool showProgress, lastSyncsLogFileSizeMax_(lastSyncsLogFileSizeMax), handleError_(handleError), returnCode_(returnCode), - syncStatusFrame(*this, *this, nullptr, showProgress, jobName, execWhenFinished, execFinishedHistory), - jobName_(jobName) + progressDlg(createProgressDialog(*this, [this] { this->onProgressDialogTerminate(); }, *this, nullptr, showProgress, jobName, execWhenFinished, execFinishedHistory)), + jobName_(jobName) { if (logfilesCountLimit != 0) //init log file: starts internal timer! { - if (!tryReportingError([&] { logFile = prepareNewLogfile(logfileDirectory, jobName, timeStamp); }, //throw FileError; return value always bound! - *this)) + zen::Opt<std::wstring> errMsg = tryReportingError2([&] { logFile = prepareNewLogfile(logfileDirectory, jobName, timeStamp); }, //throw FileError; return value always bound! + *this); + if (errMsg) { raiseReturnCode(returnCode_, FFS_RC_ABORTED); throw BatchAbortProcess(); @@ -202,59 +204,73 @@ BatchStatusHandler::~BatchStatusHandler() } catch (FileError&) {} - //decide whether to stay on status screen or exit immediately... - if (switchToGuiRequested) //-> avoid recursive yield() calls, thous switch not before ending batch mode + if (progressDlg) { - try + //decide whether to stay on status screen or exit immediately... + if (switchToGuiRequested) //-> avoid recursive yield() calls, thous switch not before ending batch mode { - switchBatchToGui_.execute(); //open FreeFileSync GUI + try + { + switchBatchToGui_.execute(); //open FreeFileSync GUI + } + catch (...) {} + progressDlg->closeWindowDirectly(); //progressDlg is not main window anymore } - catch (...) {} - syncStatusFrame.closeWindowDirectly(); //syncStatusFrame is not main window anymore - } - else - { - if (syncStatusFrame.getAsWindow()->IsShown()) - showFinalResults = true; - - //execute "on completion" command (even in case of ignored errors) - if (!abortIsRequested()) //if aborted (manually), we don't execute the command + else { - const std::wstring finalCommand = syncStatusFrame.getExecWhenFinishedCommand(); //final value (after possible user modification) - if (isCloseProgressDlgCommand(finalCommand)) - showFinalResults = false; //take precedence over current visibility status - else if (!finalCommand.empty()) + if (progressDlg->getAsWindow()->IsShown()) + showFinalResults = true; + + //execute "on completion" command (even in case of ignored errors) + if (!abortIsRequested()) //if aborted (manually), we don't execute the command { - auto cmdexp = expandMacros(utfCvrtTo<Zstring>(finalCommand)); - shellExecute(cmdexp); + const std::wstring finalCommand = progressDlg->getExecWhenFinishedCommand(); //final value (after possible user modification) + if (isCloseProgressDlgCommand(finalCommand)) + showFinalResults = false; //take precedence over current visibility status + else if (!finalCommand.empty()) + { + auto cmdexp = expandMacros(utfCvrtTo<Zstring>(finalCommand)); + shellExecute(cmdexp); + } } + + if (showFinalResults) //warning: wxWindow::Show() is called within processHasFinished()! + { + //notify about (logical) application main window => program won't quit, but stay on this dialog + setMainWindow(progressDlg->getAsWindow()); + + //notify to progressDlg that current process has ended + if (abortIsRequested()) + progressDlg->processHasFinished(SyncProgressDialog::RESULT_ABORTED, errorLog); //enable okay and close events + else if (totalErrors > 0) + progressDlg->processHasFinished(SyncProgressDialog::RESULT_FINISHED_WITH_ERROR, errorLog); + else if (totalWarnings > 0) + progressDlg->processHasFinished(SyncProgressDialog::RESULT_FINISHED_WITH_WARNINGS, errorLog); + else + progressDlg->processHasFinished(SyncProgressDialog::RESULT_FINISHED_WITH_SUCCESS, errorLog); + } + else + progressDlg->closeWindowDirectly(); //progressDlg is main window => program will quit directly } - if (showFinalResults) //warning: wxWindow::Show() is called within processHasFinished()! + //wait until progress dialog notified shutdown via onProgressDialogTerminate() + //-> required since it has our "this" pointer captured in lambda "notifyWindowTerminate"! + //-> nicely manages dialog lifetime + while (progressDlg) { - //notify about (logical) application main window => program won't quit, but stay on this dialog - setMainWindow(syncStatusFrame.getAsWindow()); - - //notify to syncStatusFrame that current process has ended - if (abortIsRequested()) - syncStatusFrame.processHasFinished(SyncProgressDialog::RESULT_ABORTED, errorLog); //enable okay and close events - else if (totalErrors > 0) - syncStatusFrame.processHasFinished(SyncProgressDialog::RESULT_FINISHED_WITH_ERROR, errorLog); - else if (totalWarnings > 0) - syncStatusFrame.processHasFinished(SyncProgressDialog::RESULT_FINISHED_WITH_WARNINGS, errorLog); - else - syncStatusFrame.processHasFinished(SyncProgressDialog::RESULT_FINISHED_WITH_SUCCESS, errorLog); + boost::this_thread::sleep(boost::posix_time::milliseconds(UI_UPDATE_INTERVAL)); + updateUiNow(); } - else - syncStatusFrame.closeWindowDirectly(); //syncStatusFrame is main window => program will quit directly } } + void BatchStatusHandler::initNewPhase(int objectsTotal, Int64 dataTotal, ProcessCallback::Phase phaseID) { StatusHandler::initNewPhase(objectsTotal, dataTotal, phaseID); - syncStatusFrame.initNewPhase(); //call after "StatusHandler::initNewPhase" + if (progressDlg) + progressDlg->initNewPhase(); //call after "StatusHandler::initNewPhase" } @@ -262,7 +278,8 @@ void BatchStatusHandler::updateProcessedData(int objectsDelta, Int64 dataDelta) { StatusHandler::updateProcessedData(objectsDelta, dataDelta); - syncStatusFrame.notifyProgressChange(); //noexcept + if (progressDlg) + progressDlg->notifyProgressChange(); //noexcept //note: this method should NOT throw in order to properly allow undoing setting of statistics! } @@ -285,11 +302,12 @@ void BatchStatusHandler::reportWarning(const std::wstring& warningMessage, bool& { case xmlAccess::ON_ERROR_POPUP: { - PauseTimers dummy(syncStatusFrame); + if (!progressDlg) abortThisProcess(); + PauseTimers dummy(*progressDlg); forceUiRefresh(); bool dontWarnAgain = false; - switch (showWarningDlg(syncStatusFrame.getAsWindow(), + switch (showWarningDlg(progressDlg->getAsWindow(), ReturnWarningDlg::BUTTON_IGNORE | ReturnWarningDlg::BUTTON_SWITCH | ReturnWarningDlg::BUTTON_CANCEL, warningMessage + L"\n\n" + _("Press \"Switch\" to resolve issues in FreeFileSync main dialog."), dontWarnAgain)) @@ -329,11 +347,12 @@ ProcessCallback::Response BatchStatusHandler::reportError(const std::wstring& er { case xmlAccess::ON_ERROR_POPUP: { - PauseTimers dummy(syncStatusFrame); + if (!progressDlg) abortThisProcess(); + PauseTimers dummy(*progressDlg); forceUiRefresh(); bool ignoreNextErrors = false; - switch (showErrorDlg(syncStatusFrame.getAsWindow(), + switch (showErrorDlg(progressDlg->getAsWindow(), ReturnErrorDlg::BUTTON_IGNORE | ReturnErrorDlg::BUTTON_RETRY | ReturnErrorDlg::BUTTON_CANCEL, errorMessage, &ignoreNextErrors)) { @@ -373,11 +392,12 @@ void BatchStatusHandler::reportFatalError(const std::wstring& errorMessage) { case xmlAccess::ON_ERROR_POPUP: { - PauseTimers dummy(syncStatusFrame); + if (!progressDlg) abortThisProcess(); + PauseTimers dummy(*progressDlg); forceUiRefresh(); bool ignoreNextErrors = false; - switch (showFatalErrorDlg(syncStatusFrame.getAsWindow(), + switch (showFatalErrorDlg(progressDlg->getAsWindow(), ReturnFatalErrorDlg::BUTTON_IGNORE | ReturnFatalErrorDlg::BUTTON_CANCEL, errorMessage, &ignoreNextErrors)) { @@ -405,12 +425,20 @@ void BatchStatusHandler::reportFatalError(const std::wstring& errorMessage) void BatchStatusHandler::forceUiRefresh() { - syncStatusFrame.updateGui(); + if (progressDlg) + progressDlg->updateGui(); } void BatchStatusHandler::abortThisProcess() { requestAbortion(); //just make sure... - throw BatchAbortProcess(); //abort can be triggered by syncStatusFrame + throw BatchAbortProcess(); //abort can be triggered by progressDlg +} + + +void BatchStatusHandler::onProgressDialogTerminate() +{ + //it's responsibility of "progressDlg" to call requestAbortion() when closing dialog + progressDlg = nullptr; } diff --git a/ui/batch_status_handler.h b/ui/batch_status_handler.h index dff2f7b3..7a77785b 100644 --- a/ui/batch_status_handler.h +++ b/ui/batch_status_handler.h @@ -21,6 +21,7 @@ class BatchAbortProcess {}; +//BatchStatusHandler(SyncProgressDialog) will internally process Window messages! disable GUI controls to avoid unexpected callbacks! class BatchStatusHandler : public zen::StatusHandler //throw BatchAbortProcess { public: @@ -37,7 +38,7 @@ public: std::vector<std::wstring>& execFinishedHistory); ~BatchStatusHandler(); - virtual void initNewPhase (int objectsTotal, zen::Int64 dataTotal, Phase phaseID); + virtual void initNewPhase (int objectsTotal, zen::Int64 dataTotal, Phase phaseID); virtual void updateProcessedData(int objectsDelta, zen::Int64 dataDelta); virtual void reportInfo(const std::wstring& text); virtual void forceUiRefresh(); @@ -48,6 +49,7 @@ public: private: virtual void abortThisProcess(); //throw BatchAbortProcess + void onProgressDialogTerminate(); const zen::SwitchToGui& switchBatchToGui_; //functionality to change from batch mode to GUI mode bool showFinalResults; @@ -57,7 +59,7 @@ private: zen::ErrorLog errorLog; //list of non-resolved errors and warnings zen::FfsReturnCode& returnCode_; - SyncProgressDialog syncStatusFrame; //the window managed by SyncStatus has longer lifetime than this handler! + SyncProgressDialog* progressDlg; //managed to have shorter lifetime than this handler! std::unique_ptr<zen::FileOutput> logFile; //optional! const std::wstring jobName_; diff --git a/ui/column_attr.h b/ui/column_attr.h index 5f0d79aa..399f553b 100644 --- a/ui/column_attr.h +++ b/ui/column_attr.h @@ -67,8 +67,9 @@ std::vector<ColumnAttributeRim> getDefaultColumnAttributesRight() enum ColumnTypeMiddle { - COL_TYPE_MIDDLE_VALUE, - COL_TYPE_BORDER + COL_TYPE_CHECKBOX, + COL_TYPE_CMP_CATEGORY, + COL_TYPE_SYNC_ACTION, }; //------------------------------------------------------------------ diff --git a/ui/custom_grid.cpp b/ui/custom_grid.cpp index 35d3248d..c1a39a0c 100644 --- a/ui/custom_grid.cpp +++ b/ui/custom_grid.cpp @@ -39,11 +39,6 @@ const wxColour COLOR_NOT_ACTIVE (228, 228, 228); //light grey const Zstring ICON_FILE_FOLDER = Zstr("folder"); - -const int CHECK_BOX_IMAGE = 12; //width of checkbox image -const int CHECK_BOX_SPACE_LEFT = 2; -const int CHECK_BOX_WIDTH = CHECK_BOX_SPACE_LEFT + CHECK_BOX_IMAGE; //width of first block - const size_t ROW_COUNT_NO_DATA = 10; /* @@ -95,6 +90,51 @@ std::pair<ptrdiff_t, ptrdiff_t> getVisibleRows(const Grid& grid) //returns range } +void fillBackgroundDefaultColorAlternating(wxDC& dc, const wxRect& rect, bool evenRowNumber) +{ + //alternate background color to improve readability (while lacking cell borders) + if (!evenRowNumber) + { + //accessibility, support high-contrast schemes => work with user-defined background color! + const auto backCol = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); + + auto incChannel = [](unsigned char c, int diff) { return static_cast<unsigned char>(std::max(0, std::min(255, c + diff))); }; + + auto getAdjustedColor = [&](int diff) + { + return wxColor(incChannel(backCol.Red (), diff), + incChannel(backCol.Green(), diff), + incChannel(backCol.Blue (), diff)); + }; + + auto colorDist = [](const wxColor& lhs, const wxColor& rhs) //just some metric + { + return numeric::power<2>(static_cast<int>(lhs.Red ()) - static_cast<int>(rhs.Red ())) + + numeric::power<2>(static_cast<int>(lhs.Green()) - static_cast<int>(rhs.Green())) + + numeric::power<2>(static_cast<int>(lhs.Blue ()) - static_cast<int>(rhs.Blue ())); + }; + + const int signLevel = colorDist(backCol, *wxBLACK) < colorDist(backCol, *wxWHITE) ? 1 : -1; //brighten or darken + + const wxColor colOutter = getAdjustedColor(signLevel * 14); //just some very faint gradient to avoid visual distraction + const wxColor colInner = getAdjustedColor(signLevel * 11); // + + //clearArea(dc, rect, backColAlt); + + //add some nice background gradient + wxRect rectUpper = rect; + rectUpper.height /= 2; + wxRect rectLower = rect; + rectLower.y += rectUpper.height; + rectLower.height -= rectUpper.height; + dc.GradientFillLinear(rectUpper, colOutter, colInner, wxSOUTH); + dc.GradientFillLinear(rectLower, colOutter, colInner, wxNORTH); + } + else + clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); +} + + Zstring getExtension(const Zstring& shortName) { return contains(shortName, Zchar('.')) ? afterLast(shortName, Zchar('.')) : Zstring(); @@ -235,43 +275,8 @@ protected: else { //alternate background color to improve readability (while lacking cell borders) - if (getRowDisplayType(row) == DISP_TYPE_NORMAL && row % 2 == 1) - { - //accessibility, support high-contrast schemes => work with user-defined background color! - const auto backCol = getBackGroundColor(row); - - auto incChannel = [](unsigned char c, int diff) { return static_cast<unsigned char>(std::max(0, std::min(255, c + diff))); }; - - auto getAdjustedColor = [&](int diff) - { - return wxColor(incChannel(backCol.Red (), diff), - incChannel(backCol.Green(), diff), - incChannel(backCol.Blue (), diff)); - }; - - auto colorDist = [](const wxColor& lhs, const wxColor& rhs) //just some metric - { - return numeric::power<2>(static_cast<int>(lhs.Red ()) - static_cast<int>(rhs.Red ())) + - numeric::power<2>(static_cast<int>(lhs.Green()) - static_cast<int>(rhs.Green())) + - numeric::power<2>(static_cast<int>(lhs.Blue ()) - static_cast<int>(rhs.Blue ())); - }; - - const int signLevel = colorDist(backCol, *wxBLACK) < colorDist(backCol, *wxWHITE) ? 1 : -1; //brighten or darken - - const wxColor colOutter = getAdjustedColor(signLevel * 14); //just some very faint gradient to avoid visual distraction - const wxColor colInner = getAdjustedColor(signLevel * 11); // - - //clearArea(dc, rect, backColAlt); - - //add some nice background gradient - wxRect rectUpper = rect; - rectUpper.height /= 2; - wxRect rectLower = rect; - rectLower.y += rectUpper.height; - rectLower.height -= rectUpper.height; - dc.GradientFillLinear(rectUpper, colOutter, colInner, wxSOUTH); - dc.GradientFillLinear(rectLower, colOutter, colInner, wxNORTH); - } + if (getRowDisplayType(row) == DISP_TYPE_NORMAL) + fillBackgroundDefaultColorAlternating(dc, rect, row % 2 == 0); else clearArea(dc, rect, getBackGroundColor(row)); } @@ -295,7 +300,6 @@ protected: return COLOR_ORANGE; case DISP_TYPE_INACTIVE: return COLOR_NOT_ACTIVE; - } return wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); } @@ -630,9 +634,7 @@ private: } virtual void visit(const SymLinkMapping& linkObj) { - iconName = linkObj.getLinkType<side>() == LinkDescriptor::TYPE_DIR ? - ICON_FILE_FOLDER : - linkObj.getFullName<side>(); + iconName = linkObj.getFullName<side>(); } virtual void visit(const DirMapping& dirObj) { @@ -770,16 +772,16 @@ class GridDataMiddle : public GridDataBase public: GridDataMiddle(const std::shared_ptr<const zen::GridView>& gridDataView, Grid& grid) : GridDataBase(grid, gridDataView), - showSyncAction_(true), - toolTip(grid) {} //tool tip must not live longer than grid! + highlightSyncAction_(false), + toolTip(grid), //tool tip must not live longer than grid! + notch(getResourceImage(L"notch").ConvertToImage()) {} void onSelectBegin(const wxPoint& clientPos, size_t row, ColumnType colType) { - if (static_cast<ColumnTypeMiddle>(colType) == COL_TYPE_MIDDLE_VALUE && - row < refGrid().getRowCount()) + if (row < refGrid().getRowCount()) { refGrid().clearSelection(false); //don't emit event, prevent recursion! - dragSelection = make_unique<std::pair<size_t, BlockPosition>>(row, mousePosToBlock(clientPos, row)); + dragSelection = make_unique<std::pair<size_t, BlockPosition>>(row, mousePosToBlock(clientPos, row, static_cast<ColumnTypeMiddle>(colType))); toolTip.hide(); //handle custom tooltip } } @@ -839,22 +841,27 @@ public: //manage block highlighting and custom tooltip if (!dragSelection) { + auto refreshHighlight = [&](size_t row) + { + refreshCell(refGrid(), row, static_cast<ColumnType>(COL_TYPE_CHECKBOX)); + refreshCell(refGrid(), row, static_cast<ColumnType>(COL_TYPE_SYNC_ACTION)); + }; + const wxPoint& topLeftAbs = refGrid().CalcUnscrolledPosition(clientPos); const size_t row = refGrid().getRowAtPos(topLeftAbs.y); //return -1 for invalid position, rowCount if one past the end auto colInfo = refGrid().getColumnAtPos(topLeftAbs.x); //(column type, component position) - if (row < refGrid().getRowCount() && - colInfo && static_cast<ColumnTypeMiddle>(colInfo->first) == COL_TYPE_MIDDLE_VALUE) + if (row < refGrid().getRowCount() && colInfo) { - if (highlight) //refresh old highlight - refreshCell(refGrid(), highlight->row_, static_cast<ColumnType>(COL_TYPE_MIDDLE_VALUE)); + if (highlight) refreshHighlight(highlight->row_); //refresh old highlight + + highlight = make_unique<MouseHighlight>(row, mousePosToBlock(clientPos, row, static_cast<ColumnTypeMiddle>(colInfo->first))); - highlight = make_unique<MouseHighlight>(row, mousePosToBlock(clientPos, row)); - refreshCell(refGrid(), highlight->row_, static_cast<ColumnType>(COL_TYPE_MIDDLE_VALUE)); + refreshHighlight(highlight->row_); //show custom tooltip if (refGrid().getMainWin().GetClientRect().Contains(clientPos)) //cursor might have moved outside visible client area - showToolTip(row, refGrid().getMainWin().ClientToScreen(clientPos)); + showToolTip(row, static_cast<ColumnTypeMiddle>(colInfo->first), refGrid().getMainWin().ClientToScreen(clientPos)); } else onMouseLeave(); @@ -867,184 +874,231 @@ public: { if (highlight) { - refreshCell(refGrid(), highlight->row_, static_cast<ColumnType>(COL_TYPE_MIDDLE_VALUE)); + refreshCell(refGrid(), highlight->row_, static_cast<ColumnType>(COL_TYPE_CHECKBOX)); + refreshCell(refGrid(), highlight->row_, static_cast<ColumnType>(COL_TYPE_SYNC_ACTION)); highlight.reset(); } toolTip.hide(); //handle custom tooltip } } - void showSyncAction(bool value) { showSyncAction_ = value; } + void highlightSyncAction(bool value) { highlightSyncAction_ = value; } private: virtual wxString getValue(size_t row, ColumnType colType) const { - if (static_cast<ColumnTypeMiddle>(colType) == COL_TYPE_MIDDLE_VALUE) - { - if (const FileSystemObject* fsObj = getRawData(row)) - return showSyncAction_ ? getSymbol(fsObj->getSyncOperation()) : getSymbol(fsObj->getCategory()); - } - return wxEmptyString; + if (const FileSystemObject* fsObj = getRawData(row)) + switch (static_cast<ColumnTypeMiddle>(colType)) + { + case COL_TYPE_CHECKBOX: + break; + case COL_TYPE_CMP_CATEGORY: + return getSymbol(fsObj->getCategory()); + case COL_TYPE_SYNC_ACTION: + return getSymbol(fsObj->getSyncOperation()); + } + return wxString(); } - virtual void renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected, bool hasFocus) { - drawCellBackground(dc, rect, enabled, selected, hasFocus, getBackGroundColor(row)); + const FileSystemObject* fsObj = getRawData(row); + GridData::drawCellBackground(dc, rect, enabled, selected, hasFocus, highlightSyncAction_ ? + getBackGroundColorSyncAction(fsObj) : + getBackGroundColorCmpCategory(fsObj)); } virtual void renderCell(Grid& grid, wxDC& dc, const wxRect& rect, size_t row, ColumnType colType) { switch (static_cast<ColumnTypeMiddle>(colType)) { - case COL_TYPE_MIDDLE_VALUE: - { + case COL_TYPE_CHECKBOX: if (const FileSystemObject* fsObj = getRawData(row)) { - //wxRect rectInside = drawCellBorder(dc, rect); wxRect rectInside = rect; - rectInside.width -= CHECK_BOX_SPACE_LEFT; - rectInside.x += CHECK_BOX_SPACE_LEFT; - - //draw checkbox - wxRect checkBoxArea = rectInside; - checkBoxArea.SetWidth(CHECK_BOX_IMAGE); + //if sync action is shown draw notch on left side, right side otherwise + if (notch.GetHeight() != rectInside.GetHeight()) + notch.Rescale(notch.GetWidth(), rectInside.GetHeight()); + if (highlightSyncAction_) + { + drawBitmapRtlMirror(dc, notch, rectInside, wxALIGN_LEFT, buffer); + rectInside.x += notch.GetWidth(); + rectInside.width -= notch.GetWidth(); + } + else + { + //wxWidgets screws up again and has wxALIGN_RIGHT off by one pixel! -> use wxALIGN_LEFT instead + wxRect rectNotch(rectInside.x + rectInside.width - notch.GetWidth(), rectInside.y, + notch.GetWidth(), rectInside.height); + drawBitmapRtlMirror(dc, notch, rectNotch, wxALIGN_LEFT, buffer); + rectInside.width -= notch.GetWidth(); + } const bool rowHighlighted = dragSelection ? row == dragSelection->first : highlight ? row == highlight->row_ : false; const BlockPosition highlightBlock = dragSelection ? dragSelection->second : highlight ? highlight->blockPos_ : BLOCKPOS_CHECK_BOX; if (rowHighlighted && highlightBlock == BLOCKPOS_CHECK_BOX) - drawBitmapRtlMirror(dc, getResourceImage(fsObj->isActive() ? L"checkboxTrueFocus" : L"checkboxFalseFocus"), checkBoxArea, wxALIGN_CENTER, buffer); + drawBitmapRtlMirror(dc, getResourceImage(fsObj->isActive() ? L"checkboxTrueFocus" : L"checkboxFalseFocus"), rectInside, wxALIGN_CENTER, buffer); else //default - drawBitmapRtlMirror(dc, getResourceImage(fsObj->isActive() ? L"checkboxTrue" : L"checkboxFalse" ), checkBoxArea, wxALIGN_CENTER, buffer); + drawBitmapRtlMirror(dc, getResourceImage(fsObj->isActive() ? L"checkboxTrue" : L"checkboxFalse" ), rectInside, wxALIGN_CENTER, buffer); + } + break; - rectInside.width -= CHECK_BOX_IMAGE; - rectInside.x += CHECK_BOX_IMAGE; + case COL_TYPE_CMP_CATEGORY: + if (const FileSystemObject* fsObj = getRawData(row)) + { + if (highlightSyncAction_) + fillBackgroundDefaultColorAlternating(dc, rect, row % 2 == 0); + + const wxBitmap& cmpImg = getCmpResultImage(fsObj->getCategory()); + drawBitmapRtlMirror(dc, highlightSyncAction_ ? greyScale(cmpImg) : cmpImg, rect, wxALIGN_CENTER, buffer); + } + break; + + case COL_TYPE_SYNC_ACTION: + if (const FileSystemObject* fsObj = getRawData(row)) + { + if (!highlightSyncAction_) + fillBackgroundDefaultColorAlternating(dc, rect, row % 2 == 0); + + const bool rowHighlighted = dragSelection ? row == dragSelection->first : highlight ? row == highlight->row_ : false; + const BlockPosition highlightBlock = dragSelection ? dragSelection->second : highlight ? highlight->blockPos_ : BLOCKPOS_CHECK_BOX; //synchronization preview - if (showSyncAction_) + if (rowHighlighted && highlightBlock != BLOCKPOS_CHECK_BOX) + switch (highlightBlock) + { + case BLOCKPOS_CHECK_BOX: + break; + case BLOCKPOS_LEFT: + drawBitmapRtlMirror(dc, getSyncOpImage(fsObj->testSyncOperation(SYNC_DIR_LEFT)), rect, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, buffer); + break; + case BLOCKPOS_MIDDLE: + drawBitmapRtlMirror(dc, getSyncOpImage(fsObj->testSyncOperation(SYNC_DIR_NONE)), rect, wxALIGN_CENTER, buffer); + break; + case BLOCKPOS_RIGHT: + drawBitmapRtlMirror(dc, getSyncOpImage(fsObj->testSyncOperation(SYNC_DIR_RIGHT)), rect, wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL, buffer); + break; + } + else //default { - if (rowHighlighted && highlightBlock != BLOCKPOS_CHECK_BOX) - switch (highlightBlock) - { - case BLOCKPOS_CHECK_BOX: - break; - case BLOCKPOS_LEFT: - drawBitmapRtlMirror(dc, getSyncOpImage(fsObj->testSyncOperation(SYNC_DIR_LEFT)), rectInside, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, buffer); - break; - case BLOCKPOS_MIDDLE: - drawBitmapRtlMirror(dc, getSyncOpImage(fsObj->testSyncOperation(SYNC_DIR_NONE)), rectInside, wxALIGN_CENTER, buffer); - break; - case BLOCKPOS_RIGHT: - drawBitmapRtlMirror(dc, getSyncOpImage(fsObj->testSyncOperation(SYNC_DIR_RIGHT)), rectInside, wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL, buffer); - break; - } - else //default - drawBitmapRtlMirror(dc, getSyncOpImage(fsObj->getSyncOperation()), rectInside, wxALIGN_CENTER, buffer); + const wxBitmap& opImg = getSyncOpImage(fsObj->getSyncOperation()); + drawBitmapRtlMirror(dc, highlightSyncAction_ ? opImg : greyScale(opImg), rect, wxALIGN_CENTER, buffer); } - else //comparison results view - drawBitmapRtlMirror(dc, getCmpResultImage(fsObj->getCategory()), rectInside, wxALIGN_CENTER, buffer); } - } - break; - case COL_TYPE_BORDER: - clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_3DSHADOW)); break; } } - virtual wxString getColumnLabel(ColumnType colType) const { return wxEmptyString; } + virtual wxString getColumnLabel(ColumnType colType) const + { + switch (static_cast<ColumnTypeMiddle>(colType)) + { + case COL_TYPE_CHECKBOX: + break; + case COL_TYPE_CMP_CATEGORY: + return _("Category"); + case COL_TYPE_SYNC_ACTION: + return _("Action"); + } + return wxEmptyString; + } + + virtual wxString getToolTip(ColumnType colType) const { return getColumnLabel(colType); } virtual void renderColumnLabel(Grid& tree, wxDC& dc, const wxRect& rect, ColumnType colType, bool highlighted) { switch (static_cast<ColumnTypeMiddle>(colType)) { - case COL_TYPE_MIDDLE_VALUE: + case COL_TYPE_CHECKBOX: + drawColumnLabelBackground(dc, rect, false); + break; + + case COL_TYPE_CMP_CATEGORY: { wxRect rectInside = drawColumnLabelBorder(dc, rect); drawColumnLabelBackground(dc, rectInside, highlighted); - const wxBitmap& cmpIcon = getResourceImage(L"compareSmall"); - const wxBitmap& syncIcon = getResourceImage(L"syncSmall"); - - const int space = 8; - const int imageWidthTotal = cmpIcon.GetWidth() + space + syncIcon.GetWidth(); + const wxBitmap& cmpIcon = getResourceImage(L"compareSmall"); + drawBitmapRtlNoMirror(dc, highlightSyncAction_ ? greyScale(cmpIcon) : cmpIcon, rectInside, wxALIGN_CENTER, buffer); + } + break; - const int posX = (rectInside.GetWidth () - imageWidthTotal) / 2; - const int posY = (rectInside.GetHeight() - cmpIcon.GetHeight()) / 2; - assert(cmpIcon.GetHeight() == syncIcon.GetHeight()); + case COL_TYPE_SYNC_ACTION: + { + wxRect rectInside = drawColumnLabelBorder(dc, rect); + drawColumnLabelBackground(dc, rectInside, highlighted); - dc.DrawBitmap(showSyncAction_ ? greyScale(cmpIcon) : cmpIcon, posX, posY, true); - dc.DrawBitmap(showSyncAction_ ? syncIcon : greyScale(syncIcon), posX + cmpIcon.GetWidth() + space, posY, true); + const wxBitmap& syncIcon = getResourceImage(L"syncSmall"); + drawBitmapRtlNoMirror(dc, highlightSyncAction_ ? syncIcon : greyScale(syncIcon), rectInside, wxALIGN_CENTER, buffer); } break; - - case COL_TYPE_BORDER: - drawCellBackground(dc, rect, true, false, true, wxSystemSettings::GetColour(wxSYS_COLOUR_3DSHADOW)); - break; } } - wxColor getBackGroundColor(size_t row) const + static wxColor getBackGroundColorSyncAction(const FileSystemObject* fsObj) { - if (const FileSystemObject* fsObj = getRawData(row)) + if (fsObj) { if (!fsObj->isActive()) return COLOR_NOT_ACTIVE; - else - { - if (showSyncAction_) //synchronization preview - { - switch (fsObj->getSyncOperation()) //evaluate comparison result and sync direction - { - case SO_DO_NOTHING: - //return COLOR_NOT_ACTIVE; - case SO_EQUAL: - break; //usually white - - case SO_CREATE_NEW_LEFT: - case SO_OVERWRITE_LEFT: - case SO_DELETE_LEFT: - case SO_MOVE_LEFT_SOURCE: - case SO_MOVE_LEFT_TARGET: - case SO_COPY_METADATA_TO_LEFT: - return COLOR_SYNC_BLUE; - case SO_CREATE_NEW_RIGHT: - case SO_OVERWRITE_RIGHT: - case SO_DELETE_RIGHT: - case SO_MOVE_RIGHT_SOURCE: - case SO_MOVE_RIGHT_TARGET: - case SO_COPY_METADATA_TO_RIGHT: - return COLOR_SYNC_GREEN; - - case SO_UNRESOLVED_CONFLICT: - return COLOR_YELLOW; - } - } - else //comparison results view - { - switch (fsObj->getCategory()) - { - case FILE_LEFT_SIDE_ONLY: - case FILE_LEFT_NEWER: - return COLOR_SYNC_BLUE; //COLOR_CMP_BLUE; + switch (fsObj->getSyncOperation()) //evaluate comparison result and sync direction + { + case SO_DO_NOTHING: + return COLOR_NOT_ACTIVE; + case SO_EQUAL: + break; //usually white + + case SO_CREATE_NEW_LEFT: + case SO_OVERWRITE_LEFT: + case SO_DELETE_LEFT: + case SO_MOVE_LEFT_SOURCE: + case SO_MOVE_LEFT_TARGET: + case SO_COPY_METADATA_TO_LEFT: + return COLOR_SYNC_BLUE; + + case SO_CREATE_NEW_RIGHT: + case SO_OVERWRITE_RIGHT: + case SO_DELETE_RIGHT: + case SO_MOVE_RIGHT_SOURCE: + case SO_MOVE_RIGHT_TARGET: + case SO_COPY_METADATA_TO_RIGHT: + return COLOR_SYNC_GREEN; + + case SO_UNRESOLVED_CONFLICT: + return COLOR_YELLOW; + } + } + return wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); + } - case FILE_RIGHT_SIDE_ONLY: - case FILE_RIGHT_NEWER: - return COLOR_SYNC_GREEN; //COLOR_CMP_GREEN; + static wxColor getBackGroundColorCmpCategory(const FileSystemObject* fsObj) + { + if (fsObj) + { + if (!fsObj->isActive()) + return COLOR_NOT_ACTIVE; - case FILE_DIFFERENT: - return COLOR_CMP_RED; - case FILE_EQUAL: - break; //usually white - case FILE_CONFLICT: - case FILE_DIFFERENT_METADATA: //= sub-category of equal, but hint via background that sync direction follows conflict-setting - return COLOR_YELLOW; - //return COLOR_YELLOW_LIGHT; - } - } + switch (fsObj->getCategory()) + { + case FILE_LEFT_SIDE_ONLY: + case FILE_LEFT_NEWER: + return COLOR_SYNC_BLUE; //COLOR_CMP_BLUE; + + case FILE_RIGHT_SIDE_ONLY: + case FILE_RIGHT_NEWER: + return COLOR_SYNC_GREEN; //COLOR_CMP_GREEN; + + case FILE_DIFFERENT: + return COLOR_CMP_RED; + case FILE_EQUAL: + break; //usually white + case FILE_CONFLICT: + case FILE_DIFFERENT_METADATA: //= sub-category of equal, but hint via background that sync direction follows conflict-setting + return COLOR_YELLOW; + //return COLOR_YELLOW_LIGHT; } } return wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); @@ -1058,44 +1112,60 @@ private: BLOCKPOS_RIGHT }; - //determine blockposition within cell - BlockPosition mousePosToBlock(const wxPoint& clientPos, size_t row) const + //determine block position within cell + BlockPosition mousePosToBlock(const wxPoint& clientPos, size_t row, ColumnTypeMiddle colType) const { - const int absX = refGrid().CalcUnscrolledPosition(clientPos).x; - - const wxRect rect = refGrid().getCellArea(row, static_cast<ColumnType>(COL_TYPE_MIDDLE_VALUE)); //returns empty rect if column not found; absolute coordinates! - if (rect.width > CHECK_BOX_WIDTH && rect.height > 0) + switch (static_cast<ColumnTypeMiddle>(colType)) { - const FileSystemObject* const fsObj = getRawData(row); - if (fsObj && showSyncAction_ && - fsObj->getSyncOperation() != SO_EQUAL) //in sync-preview equal files shall be treated as in cmp result, i.e. as full checkbox - { - // cell: - // ---------------------------------- - // | checkbox | left | middle | right| - // ---------------------------------- + case COL_TYPE_CHECKBOX: + case COL_TYPE_CMP_CATEGORY: + break; - const double blockWidth = (rect.GetWidth() - CHECK_BOX_WIDTH) / 3.0; - if (rect.GetX() + CHECK_BOX_WIDTH <= absX && absX < rect.GetX() + rect.GetWidth()) - { - if (absX < rect.GetX() + CHECK_BOX_WIDTH + blockWidth) - return BLOCKPOS_LEFT; - else if (absX < rect.GetX() + CHECK_BOX_WIDTH + 2 * blockWidth) - return BLOCKPOS_MIDDLE; - else - return BLOCKPOS_RIGHT; - } + case COL_TYPE_SYNC_ACTION: + { + const int absX = refGrid().CalcUnscrolledPosition(clientPos).x; + + const wxRect rect = refGrid().getCellArea(row, static_cast<ColumnType>(COL_TYPE_SYNC_ACTION)); //returns empty rect if column not found; absolute coordinates! + if (rect.width > 0 && rect.height > 0) + if (const FileSystemObject* const fsObj = getRawData(row)) + if (fsObj->getSyncOperation() != SO_EQUAL) //in sync-preview equal files shall be treated like a checkbox + // cell: + // ----------------------- + // | left | middle | right| + // ----------------------- + if (rect.GetX() <= absX) + { + if (absX < rect.GetX() + rect.GetWidth() / 3) + return BLOCKPOS_LEFT; + else if (absX < rect.GetX() + 2 * rect.GetWidth() / 3) + return BLOCKPOS_MIDDLE; + else if (absX < rect.GetX() + rect.GetWidth()) + return BLOCKPOS_RIGHT; + } } - + break; } return BLOCKPOS_CHECK_BOX; } - void showToolTip(size_t row, wxPoint posScreen) + void showToolTip(size_t row, ColumnTypeMiddle colType, wxPoint posScreen) { if (const FileSystemObject* fsObj = getRawData(row)) { - if (showSyncAction_) //synchronization preview + bool showTooltipSyncAction = true; + switch (colType) + { + case COL_TYPE_CHECKBOX: + showTooltipSyncAction = highlightSyncAction_; + break; + case COL_TYPE_CMP_CATEGORY: + showTooltipSyncAction = false; + break; + case COL_TYPE_SYNC_ACTION: + break; + } + + if (showTooltipSyncAction) //synchronization preview { const wchar_t* imageName = [&]() -> const wchar_t* { @@ -1173,9 +1243,7 @@ private: toolTip.hide(); //if invalid row... } - virtual wxString getToolTip(ColumnType colType) const { return showSyncAction_ ? _("Action") + L" (F8)" : _("Category") + L" (F8)"; } - - bool showSyncAction_; + bool highlightSyncAction_; struct MouseHighlight { @@ -1184,9 +1252,10 @@ private: const BlockPosition blockPos_; }; std::unique_ptr<MouseHighlight> highlight; //current mouse highlight - std::unique_ptr<std::pair<size_t, BlockPosition>> dragSelection; //(row, block) + std::unique_ptr<std::pair<size_t, BlockPosition>> dragSelection; //(row, block); area clicked when beginning selection std::unique_ptr<wxBitmap> buffer; //avoid costs of recreating this temporal variable Tooltip toolTip; + wxImage notch; }; //######################################################################################################## @@ -1478,11 +1547,15 @@ void gridview::init(Grid& gridLeft, Grid& gridCenter, Grid& gridRight, const std //gridLeft .showScrollBars(Grid::SB_SHOW_AUTOMATIC, Grid::SB_SHOW_NEVER); -> redundant: configuration happens in GridEventManager::onAlignScrollBars() //gridCenter.showScrollBars(Grid::SB_SHOW_NEVER, Grid::SB_SHOW_NEVER); - gridCenter.SetSize(60 /*+ 2 * 5*/, -1); + const int widthCategory = 30; + const int widthCheckbox = getResourceImage(L"checkboxTrue").GetWidth() + 4 + getResourceImage(L"notch").GetWidth(); + const int widthAction = 45; + gridCenter.SetSize(widthCategory + widthCheckbox + widthAction, -1); + std::vector<Grid::ColumnAttribute> attribMiddle; - //attribMiddle.push_back(Grid::ColumnAttribute(static_cast<ColumnType>(COL_TYPE_BORDER), 5, 0, true)); - attribMiddle.push_back(Grid::ColumnAttribute(static_cast<ColumnType>(COL_TYPE_MIDDLE_VALUE), 60, 0, true)); - //attribMiddle.push_back(Grid::ColumnAttribute(static_cast<ColumnType>(COL_TYPE_BORDER), 5, 0, true)); + attribMiddle.push_back(Grid::ColumnAttribute(static_cast<ColumnType>(COL_TYPE_CMP_CATEGORY), widthCategory, 0, true)); + attribMiddle.push_back(Grid::ColumnAttribute(static_cast<ColumnType>(COL_TYPE_CHECKBOX ), widthCheckbox, 0, true)); + attribMiddle.push_back(Grid::ColumnAttribute(static_cast<ColumnType>(COL_TYPE_SYNC_ACTION ), widthAction, 0, true)); gridCenter.setColumnConfig(attribMiddle); } @@ -1629,10 +1702,10 @@ void gridview::setNavigationMarker(Grid& gridLeft, } -void gridview::showSyncAction(Grid& gridCenter, bool value) +void gridview::highlightSyncAction(Grid& gridCenter, bool value) { if (auto* provMiddle = dynamic_cast<GridDataMiddle*>(gridCenter.getDataProvider())) - provMiddle->showSyncAction(value); + provMiddle->highlightSyncAction(value); else assert(false); } diff --git a/ui/custom_grid.h b/ui/custom_grid.h index c2e653ef..da6387f3 100644 --- a/ui/custom_grid.h +++ b/ui/custom_grid.h @@ -22,7 +22,7 @@ void init(Grid& gridLeft, Grid& gridCenter, Grid& gridRight, const std::shared_p std::vector<Grid::ColumnAttribute> convertConfig(const std::vector<ColumnAttributeRim>& attribs); //+ make consistent std::vector<ColumnAttributeRim> convertConfig(const std::vector<Grid::ColumnAttribute>& attribs); // -void showSyncAction(Grid& gridCenter, bool value); +void highlightSyncAction(Grid& gridCenter, bool value); void setupIcons(Grid& gridLeft, Grid& gridCenter, Grid& gridRight, bool show, IconBuffer::IconSize sz); diff --git a/ui/dir_name.cpp b/ui/dir_name.cpp index a35de5fc..2cfc8a3d 100644 --- a/ui/dir_name.cpp +++ b/ui/dir_name.cpp @@ -44,8 +44,9 @@ void setDirectoryNameImpl(const wxString& dirname, wxWindow& tooltipWnd, wxStati //change static box label only if there is a real difference to what is shown in wxTextCtrl anyway wxString dirNormalized = dirname; trim(dirNormalized); - if (!dirNormalized.empty() && !endsWith(dirNormalized, FILE_NAME_SEPARATOR)) - dirNormalized += FILE_NAME_SEPARATOR; + if (!dirNormalized.empty()) + if (!endsWith(dirNormalized, FILE_NAME_SEPARATOR)) + dirNormalized += FILE_NAME_SEPARATOR; staticText->SetLabel(dirNormalized == dirFormatted ? wxString(_("Drag && drop")) : dirFormatted); } diff --git a/ui/grid_view.cpp b/ui/grid_view.cpp index 9a5143c1..a28a7ea4 100644 --- a/ui/grid_view.cpp +++ b/ui/grid_view.cpp @@ -554,4 +554,3 @@ void GridView::sortView(ColumnTypeRim type, bool onLeft, bool ascending) break; } } - diff --git a/ui/gui_generated.cpp b/ui/gui_generated.cpp index db1f03da..d2c886d8 100644 --- a/ui/gui_generated.cpp +++ b/ui/gui_generated.cpp @@ -790,6 +790,9 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const m_panelViewFilter = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); bSizerViewFilter = new wxBoxSizer( wxHORIZONTAL ); + m_bpButtonViewTypeSyncAction = new ToggleButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( 42,42 ), wxBU_AUTODRAW ); + bSizerViewFilter->Add( m_bpButtonViewTypeSyncAction, 0, wxALIGN_CENTER_VERTICAL, 5 ); + bSizerViewFilter->Add( 0, 0, 1, wxEXPAND, 5 ); @@ -883,6 +886,7 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const m_bpButtonFilter->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::OnConfigureFilter ), NULL, this ); m_bpButtonFilter->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::OnGlobalFilterContext ), NULL, this ); m_checkBoxHideExcluded->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( MainDialogGenerated::OnShowExcluded ), NULL, this ); + m_bpButtonViewTypeSyncAction->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::OnToggleViewType ), NULL, this ); m_bpButtonShowCreateLeft->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::OnToggleViewButton ), NULL, this ); m_bpButtonShowCreateLeft->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::OnViewButtonRightClick ), NULL, this ); m_bpButtonShowUpdateLeft->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::OnToggleViewButton ), NULL, this ); @@ -950,6 +954,7 @@ MainDialogGenerated::~MainDialogGenerated() m_bpButtonFilter->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::OnConfigureFilter ), NULL, this ); m_bpButtonFilter->Disconnect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::OnGlobalFilterContext ), NULL, this ); m_checkBoxHideExcluded->Disconnect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( MainDialogGenerated::OnShowExcluded ), NULL, this ); + m_bpButtonViewTypeSyncAction->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::OnToggleViewType ), NULL, this ); m_bpButtonShowCreateLeft->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::OnToggleViewButton ), NULL, this ); m_bpButtonShowCreateLeft->Disconnect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::OnViewButtonRightClick ), NULL, this ); m_bpButtonShowUpdateLeft->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::OnToggleViewButton ), NULL, this ); @@ -1355,7 +1360,7 @@ SyncProgressDlgGenerated::SyncProgressDlgGenerated( wxWindow* parent, wxWindowID bSizerRoot->Add( m_panelProgress, 1, wxEXPAND, 5 ); m_listbookResult = new wxListbook( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLB_TOP ); - wxSize m_listbookResultImageSize = wxSize( 180,1 ); + wxSize m_listbookResultImageSize = wxSize( 170,1 ); int m_listbookResultIndex = 0; wxImageList* m_listbookResultImages = new wxImageList( m_listbookResultImageSize.GetWidth(), m_listbookResultImageSize.GetHeight() ); m_listbookResult->AssignImageList( m_listbookResultImages ); @@ -1406,7 +1411,7 @@ SyncProgressDlgGenerated::SyncProgressDlgGenerated( wxWindow* parent, wxWindowID this->Centre( wxBOTH ); // Connect Events - this->Connect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( SyncProgressDlgGenerated::OnCloseBtn ) ); + this->Connect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( SyncProgressDlgGenerated::OnClose ) ); this->Connect( wxEVT_ICONIZE, wxIconizeEventHandler( SyncProgressDlgGenerated::OnIconize ) ); m_buttonClose->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( SyncProgressDlgGenerated::OnOkay ), NULL, this ); m_buttonPause->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( SyncProgressDlgGenerated::OnPause ), NULL, this ); @@ -1416,7 +1421,7 @@ SyncProgressDlgGenerated::SyncProgressDlgGenerated( wxWindow* parent, wxWindowID SyncProgressDlgGenerated::~SyncProgressDlgGenerated() { // Disconnect Events - this->Disconnect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( SyncProgressDlgGenerated::OnCloseBtn ) ); + this->Disconnect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( SyncProgressDlgGenerated::OnClose ) ); this->Disconnect( wxEVT_ICONIZE, wxIconizeEventHandler( SyncProgressDlgGenerated::OnIconize ) ); m_buttonClose->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( SyncProgressDlgGenerated::OnOkay ), NULL, this ); m_buttonPause->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( SyncProgressDlgGenerated::OnPause ), NULL, this ); @@ -1998,20 +2003,20 @@ SyncCfgDlgGenerated::SyncCfgDlgGenerated( wxWindow* parent, wxWindowID id, const m_bitmapDatabase = new wxStaticBitmap( m_panel37, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1,-1 ), 0 ); bSizerConfig->Add( m_bitmapDatabase, 0, wxALIGN_CENTER_HORIZONTAL|wxTOP, 10 ); - wxBoxSizer* sbSizerKeepWidthStable; - sbSizerKeepWidthStable = new wxBoxSizer( wxHORIZONTAL ); + wxBoxSizer* sbSizerKeepWidthStableIfSyncDirsNotShown; + sbSizerKeepWidthStableIfSyncDirsNotShown = new wxBoxSizer( wxHORIZONTAL ); - sbSizerKeepWidthStable->Add( 45, 0, 0, 0, 5 ); + sbSizerKeepWidthStableIfSyncDirsNotShown->Add( 45, 0, 0, 0, 5 ); - sbSizerKeepWidthStable->Add( 5, 0, 0, 0, 5 ); + sbSizerKeepWidthStableIfSyncDirsNotShown->Add( 5, 0, 0, 0, 5 ); - sbSizerKeepWidthStable->Add( 46, 0, 0, 0, 5 ); + sbSizerKeepWidthStableIfSyncDirsNotShown->Add( 46, 0, 0, 0, 5 ); - bSizerConfig->Add( sbSizerKeepWidthStable, 0, 0, 5 ); + bSizerConfig->Add( sbSizerKeepWidthStableIfSyncDirsNotShown, 0, 0, 5 ); sbSizerSyncDirections = new wxBoxSizer( wxVERTICAL ); @@ -2626,6 +2631,7 @@ MessageDlgGenerated::MessageDlgGenerated( wxWindow* parent, wxWindowID id, const // Connect Events this->Connect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( MessageDlgGenerated::OnClose ) ); + m_checkBoxCustom->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( MessageDlgGenerated::OnCheckBoxClick ), NULL, this ); m_buttonCustom1->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MessageDlgGenerated::OnButton1 ), NULL, this ); m_buttonCustom2->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MessageDlgGenerated::OnButton2 ), NULL, this ); m_buttonCancel->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MessageDlgGenerated::OnCancel ), NULL, this ); @@ -2635,6 +2641,7 @@ MessageDlgGenerated::~MessageDlgGenerated() { // Disconnect Events this->Disconnect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( MessageDlgGenerated::OnClose ) ); + m_checkBoxCustom->Disconnect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( MessageDlgGenerated::OnCheckBoxClick ), NULL, this ); m_buttonCustom1->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MessageDlgGenerated::OnButton1 ), NULL, this ); m_buttonCustom2->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MessageDlgGenerated::OnButton2 ), NULL, this ); m_buttonCancel->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MessageDlgGenerated::OnCancel ), NULL, this ); @@ -2653,14 +2660,14 @@ DeleteDlgGenerated::DeleteDlgGenerated( wxWindow* parent, wxWindowID id, const w bSizer41 = new wxBoxSizer( wxHORIZONTAL ); m_bitmapDeleteType = new wxStaticBitmap( this, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1,-1 ), 0 ); - bSizer41->Add( m_bitmapDeleteType, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 ); + bSizer41->Add( m_bitmapDeleteType, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); m_staticTextHeader = new wxStaticText( this, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); m_staticTextHeader->Wrap( -1 ); bSizer41->Add( m_staticTextHeader, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); - bSizer24->Add( bSizer41, 0, wxALIGN_CENTER_HORIZONTAL|wxALL, 5 ); + bSizer24->Add( bSizer41, 0, 0, 5 ); m_staticline91 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); bSizer24->Add( m_staticline91, 0, wxEXPAND, 5 ); @@ -2682,6 +2689,7 @@ DeleteDlgGenerated::DeleteDlgGenerated( wxWindow* parent, wxWindowID id, const w bSizer99->Add( m_checkBoxUseRecycler, 0, wxALIGN_CENTER_VERTICAL|wxEXPAND|wxALL, 5 ); m_checkBoxDeleteBothSides = new wxCheckBox( this, wxID_ANY, _("Delete on both sides"), wxDefaultPosition, wxDefaultSize, 0 ); + m_checkBoxDeleteBothSides->Hide(); m_checkBoxDeleteBothSides->SetToolTip( _("Delete on both sides even if the file is selected on one side only") ); bSizer99->Add( m_checkBoxDeleteBothSides, 0, wxALIGN_CENTER_VERTICAL|wxEXPAND|wxBOTTOM|wxRIGHT|wxLEFT, 5 ); diff --git a/ui/gui_generated.h b/ui/gui_generated.h index 1f034dc2..4a7857fa 100644 --- a/ui/gui_generated.h +++ b/ui/gui_generated.h @@ -161,6 +161,7 @@ protected: wxStaticText* m_staticTextCreateRight; wxPanel* m_panelViewFilter; wxBoxSizer* bSizerViewFilter; + ToggleButton* m_bpButtonViewTypeSyncAction; ToggleButton* m_bpButtonShowCreateLeft; ToggleButton* m_bpButtonShowUpdateLeft; ToggleButton* m_bpButtonShowDeleteLeft; @@ -206,6 +207,7 @@ protected: virtual void OnConfigureFilter( wxCommandEvent& event ) { event.Skip(); } virtual void OnGlobalFilterContext( wxMouseEvent& event ) { event.Skip(); } virtual void OnShowExcluded( wxCommandEvent& event ) { event.Skip(); } + virtual void OnToggleViewType( wxCommandEvent& event ) { event.Skip(); } virtual void OnToggleViewButton( wxCommandEvent& event ) { event.Skip(); } virtual void OnViewButtonRightClick( wxMouseEvent& event ) { event.Skip(); } @@ -331,7 +333,7 @@ protected: wxButton* m_buttonAbort; // Virtual event handlers, overide them in your derived class - virtual void OnCloseBtn( wxCloseEvent& event ) { event.Skip(); } + virtual void OnClose( wxCloseEvent& event ) { event.Skip(); } virtual void OnIconize( wxIconizeEvent& event ) { event.Skip(); } virtual void OnOkay( wxCommandEvent& event ) { event.Skip(); } virtual void OnPause( wxCommandEvent& event ) { event.Skip(); } @@ -651,6 +653,7 @@ protected: // Virtual event handlers, overide them in your derived class virtual void OnClose( wxCloseEvent& event ) { event.Skip(); } + virtual void OnCheckBoxClick( wxCommandEvent& event ) { event.Skip(); } virtual void OnButton1( wxCommandEvent& event ) { event.Skip(); } virtual void OnButton2( wxCommandEvent& event ) { event.Skip(); } virtual void OnCancel( wxCommandEvent& event ) { event.Skip(); } diff --git a/ui/gui_status_handler.cpp b/ui/gui_status_handler.cpp index fa327136..c740bd09 100644 --- a/ui/gui_status_handler.cpp +++ b/ui/gui_status_handler.cpp @@ -25,39 +25,29 @@ CompareStatusHandler::CompareStatusHandler(MainDialog& dlg) : { wxWindowUpdateLocker dummy(&mainDlg); //avoid display distortion - //prevent user input during "compare", do not disable maindialog since abort-button would also be disabled - mainDlg.disableAllElements(true); - //display status panel during compare mainDlg.compareStatus->init(*this); //clear old values before showing panel mainDlg.auiMgr.GetPane(mainDlg.compareStatus->getAsWindow()).Show(); mainDlg.auiMgr.Update(); - - //register keys - mainDlg.Connect(wxEVT_CHAR_HOOK, wxKeyEventHandler(CompareStatusHandler::OnKeyPressed), nullptr, this); - mainDlg.m_buttonCancel->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(CompareStatusHandler::OnAbortCompare), nullptr, this); } + mainDlg.Update(); //don't wait until idle event! + + //register keys + mainDlg.Connect(wxEVT_CHAR_HOOK, wxKeyEventHandler(CompareStatusHandler::OnKeyPressed), nullptr, this); + mainDlg.m_buttonCancel->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(CompareStatusHandler::OnAbortCompare), nullptr, this); } CompareStatusHandler::~CompareStatusHandler() { - updateUiNow(); //ui update before enabling buttons again: prevent strange behaviour of delayed button clicks - - //reenable complete main dialog - mainDlg.enableAllElements(); - - mainDlg.compareStatus->finalize(); - mainDlg.auiMgr.GetPane(mainDlg.compareStatus->getAsWindow()).Hide(); - mainDlg.auiMgr.Update(); - //unregister keys mainDlg.Disconnect(wxEVT_CHAR_HOOK, wxKeyEventHandler(CompareStatusHandler::OnKeyPressed), nullptr, this); mainDlg.m_buttonCancel->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(CompareStatusHandler::OnAbortCompare), nullptr, this); - if (abortIsRequested()) - mainDlg.flashStatusInformation(_("Operation aborted!")); + mainDlg.compareStatus->finalize(); + mainDlg.auiMgr.GetPane(mainDlg.compareStatus->getAsWindow()).Hide(); + mainDlg.auiMgr.Update(); } @@ -179,17 +169,17 @@ void CompareStatusHandler::abortThisProcess() //######################################################################################################## -SyncStatusHandler::SyncStatusHandler(MainDialog* parentDlg, +SyncStatusHandler::SyncStatusHandler(wxTopLevelWindow* parentDlg, size_t lastSyncsLogFileSizeMax, OnGuiError handleError, const std::wstring& jobName, const std::wstring& execWhenFinished, std::vector<std::wstring>& execFinishedHistory) : parentDlg_(parentDlg), - syncStatusFrame(*this, *this, parentDlg, true, jobName, execWhenFinished, execFinishedHistory), - lastSyncsLogFileSizeMax_(lastSyncsLogFileSizeMax), - handleError_(handleError), - jobName_(jobName) + progressDlg(createProgressDialog(*this, [this] { this->onProgressDialogTerminate(); }, *this, parentDlg, true, jobName, execWhenFinished, execFinishedHistory)), + lastSyncsLogFileSizeMax_(lastSyncsLogFileSizeMax), + handleError_(handleError), + jobName_(jobName) { totalTime.Start(); //measure total time } @@ -241,35 +231,46 @@ SyncStatusHandler::~SyncStatusHandler() } catch (FileError&) {} - bool showFinalResults = true; - - //execute "on completion" command (even in case of ignored errors) - if (!abortIsRequested()) //if aborted (manually), we don't execute the command + if (progressDlg) { - const std::wstring finalCommand = syncStatusFrame.getExecWhenFinishedCommand(); //final value (after possible user modification) - if (isCloseProgressDlgCommand(finalCommand)) - showFinalResults = false; //take precedence over current visibility status - else if (!finalCommand.empty()) + bool showFinalResults = true; + //execute "on completion" command (even in case of ignored errors) + if (!abortIsRequested()) //if aborted (manually), we don't execute the command { - auto cmdexp = expandMacros(utfCvrtTo<Zstring>(finalCommand)); - shellExecute(cmdexp); + const std::wstring finalCommand = progressDlg->getExecWhenFinishedCommand(); //final value (after possible user modification) + if (isCloseProgressDlgCommand(finalCommand)) + showFinalResults = false; //take precedence over current visibility status + else if (!finalCommand.empty()) + { + auto cmdexp = expandMacros(utfCvrtTo<Zstring>(finalCommand)); + shellExecute(cmdexp); + } } - } - //notify to syncStatusFrame that current process has ended - if (showFinalResults) - { - if (abortIsRequested()) - syncStatusFrame.processHasFinished(SyncProgressDialog::RESULT_ABORTED, errorLog); //enable okay and close events - else if (totalErrors > 0) - syncStatusFrame.processHasFinished(SyncProgressDialog::RESULT_FINISHED_WITH_ERROR, errorLog); - else if (totalWarnings > 0) - syncStatusFrame.processHasFinished(SyncProgressDialog::RESULT_FINISHED_WITH_WARNINGS, errorLog); + //notify to progressDlg that current process has ended + if (showFinalResults) + { + if (abortIsRequested()) + progressDlg->processHasFinished(SyncProgressDialog::RESULT_ABORTED, errorLog); //enable okay and close events + else if (totalErrors > 0) + progressDlg->processHasFinished(SyncProgressDialog::RESULT_FINISHED_WITH_ERROR, errorLog); + else if (totalWarnings > 0) + progressDlg->processHasFinished(SyncProgressDialog::RESULT_FINISHED_WITH_WARNINGS, errorLog); + else + progressDlg->processHasFinished(SyncProgressDialog::RESULT_FINISHED_WITH_SUCCESS, errorLog); + } else - syncStatusFrame.processHasFinished(SyncProgressDialog::RESULT_FINISHED_WITH_SUCCESS, errorLog); + progressDlg->closeWindowDirectly(); + + //wait until progress dialog notified shutdown via onProgressDialogTerminate() + //-> required since it has our "this" pointer captured in lambda "notifyWindowTerminate"! + //-> nicely manages dialog lifetime + while (progressDlg) + { + boost::this_thread::sleep(boost::posix_time::milliseconds(UI_UPDATE_INTERVAL)); + updateUiNow(); + } } - else - syncStatusFrame.closeWindowDirectly(); //syncStatusFrame is main window => program will quit directly } @@ -277,14 +278,16 @@ void SyncStatusHandler::initNewPhase(int objectsTotal, Int64 dataTotal, Phase ph { assert(phaseID == PHASE_SYNCHRONIZING); StatusHandler::initNewPhase(objectsTotal, dataTotal, phaseID); - syncStatusFrame.initNewPhase(); //call after "StatusHandler::initNewPhase" + if (progressDlg) + progressDlg->initNewPhase(); //call after "StatusHandler::initNewPhase" } void SyncStatusHandler::updateProcessedData(int objectsDelta, Int64 dataDelta) { StatusHandler::updateProcessedData(objectsDelta, dataDelta); - syncStatusFrame.notifyProgressChange(); //noexcept + if (progressDlg) + progressDlg->notifyProgressChange(); //noexcept //note: this method should NOT throw in order to properly allow undoing setting of statistics! } @@ -304,7 +307,8 @@ ProcessCallback::Response SyncStatusHandler::reportError(const std::wstring& err { case ON_GUIERROR_POPUP: { - PauseTimers dummy(syncStatusFrame); + if (!progressDlg) abortThisProcess(); + PauseTimers dummy(*progressDlg); forceUiRefresh(); bool ignoreNextErrors = false; @@ -345,7 +349,8 @@ void SyncStatusHandler::reportFatalError(const std::wstring& errorMessage) { case ON_GUIERROR_POPUP: { - PauseTimers dummy(syncStatusFrame); + if (!progressDlg) abortThisProcess(); + PauseTimers dummy(*progressDlg); forceUiRefresh(); bool ignoreNextErrors = false; @@ -382,7 +387,8 @@ void SyncStatusHandler::reportWarning(const std::wstring& warningMessage, bool& { case ON_GUIERROR_POPUP: { - PauseTimers dummy(syncStatusFrame); + if (!progressDlg) abortThisProcess(); + PauseTimers dummy(*progressDlg); forceUiRefresh(); bool dontWarnAgain = false; @@ -411,12 +417,20 @@ void SyncStatusHandler::reportWarning(const std::wstring& warningMessage, bool& void SyncStatusHandler::forceUiRefresh() { - syncStatusFrame.updateGui(); + if (progressDlg) + progressDlg->updateGui(); } void SyncStatusHandler::abortThisProcess() { requestAbortion(); //just make sure... - throw GuiAbortProcess(); //abort can be triggered by syncStatusFrame + throw GuiAbortProcess(); //abort can be triggered by progressDlg +} + + +void SyncStatusHandler::onProgressDialogTerminate() +{ + //it's responsibility of "progressDlg" to call requestAbortion() when closing dialog + progressDlg = nullptr; } diff --git a/ui/gui_status_handler.h b/ui/gui_status_handler.h index 312206b6..f7b58f05 100644 --- a/ui/gui_status_handler.h +++ b/ui/gui_status_handler.h @@ -20,13 +20,15 @@ class GuiAbortProcess {}; //classes handling sync and compare error as well as status information + +//CompareStatusHandler(CompareProgressDialog) will internally process Window messages! disable GUI controls to avoid unexpected callbacks! class CompareStatusHandler : private wxEvtHandler, public zen::StatusHandler //throw GuiAbortProcess { public: CompareStatusHandler(MainDialog& dlg); ~CompareStatusHandler(); - virtual void initNewPhase (int objectsTotal, zen::Int64 dataTotal, Phase phaseID); + virtual void initNewPhase(int objectsTotal, zen::Int64 dataTotal, Phase phaseID); virtual void forceUiRefresh(); virtual Response reportError(const std::wstring& text); @@ -43,10 +45,11 @@ private: }; +//SyncStatusHandler(SyncProgressDialog) will internally process Window messages! disable GUI controls to avoid unexpected callbacks! class SyncStatusHandler : public zen::StatusHandler { public: - SyncStatusHandler(MainDialog* parentDlg, + SyncStatusHandler(wxTopLevelWindow* parentDlg, size_t lastSyncsLogFileSizeMax, xmlAccess::OnGuiError handleError, const std::wstring& jobName, @@ -54,7 +57,7 @@ public: std::vector<std::wstring>& execFinishedHistory); ~SyncStatusHandler(); - virtual void initNewPhase (int objectsTotal, zen::Int64 dataTotal, Phase phaseID); + virtual void initNewPhase (int objectsTotal, zen::Int64 dataTotal, Phase phaseID); virtual void updateProcessedData(int objectsDelta, zen::Int64 dataDelta); virtual void reportInfo(const std::wstring& text); virtual void forceUiRefresh(); @@ -65,9 +68,10 @@ public: private: virtual void abortThisProcess(); //throw GuiAbortProcess + void onProgressDialogTerminate(); - MainDialog* parentDlg_; - SyncProgressDialog syncStatusFrame; //the window managed by SyncStatus has longer lifetime than this handler! + wxTopLevelWindow* parentDlg_; + SyncProgressDialog* progressDlg; //managed to have shorter lifetime than this handler! const size_t lastSyncsLogFileSizeMax_; xmlAccess::OnGuiError handleError_; zen::ErrorLog errorLog; diff --git a/ui/main_dlg.cpp b/ui/main_dlg.cpp index cb05e086..bb1e0c86 100644 --- a/ui/main_dlg.cpp +++ b/ui/main_dlg.cpp @@ -5,16 +5,18 @@ // ************************************************************************** #include "main_dlg.h" -#include <wx/clipbrd.h> -#include <wx/wupdlock.h> -#include <wx/msgdlg.h> -#include <wx/sound.h> -#include <wx/filedlg.h> #include <zen/format_unit.h> #include <zen/file_handling.h> #include <zen/serialize.h> //#include <zen/file_id.h> +//#include <zen/perf.h> #include <zen/thread.h> +#include <wx/clipbrd.h> +#include <wx/wupdlock.h> +#include <wx/msgdlg.h> +#include <wx/sound.h> +#include <wx/filedlg.h> +#include <wx/display.h> #include <wx+/context_menu.h> #include <wx+/string_conv.h> #include <wx+/button.h> @@ -44,7 +46,6 @@ #include "../lib/help_provider.h" #include "../lib/lock_holder.h" #include "../lib/localization.h" -#include <zen/perf.h> using namespace zen; using namespace std::rel_ops; @@ -52,10 +53,15 @@ using namespace std::rel_ops; namespace { -struct wxClientHistoryData: public wxClientData //we need a wxClientData derived class to tell wxWidgets to take object ownership! +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) {} + //~wxClientHistoryData() + //{ + // std::cerr << cfgFile_.c_str() << "\n"; + //} + Zstring cfgFile_; int lastUseIndex_; //support sorting history by last usage, the higher the index the more recent the usage }; @@ -434,7 +440,6 @@ MainDialog::MainDialog(const xmlAccess::XmlGuiConfig& guiCfg, const xmlAccess::XmlGlobalSettings& globalSettings, bool startComparison) : MainDialogGenerated(nullptr), - showSyncAction_(false), folderHistoryLeft (std::make_shared<FolderHistory>()), //make sure it is always bound folderHistoryRight(std::make_shared<FolderHistory>()) // { @@ -465,36 +470,36 @@ MainDialog::MainDialog(const xmlAccess::XmlGuiConfig& guiCfg, auiMgr.SetManagedWindow(this); auiMgr.SetFlags(wxAUI_MGR_DEFAULT | wxAUI_MGR_LIVE_RESIZE); - //caption required for all panes that can be manipulated by the users => used by context menu - auiMgr.AddPane(m_panelTopButtons, - wxAuiPaneInfo().Name(L"Panel1").Layer(4).Top().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 - compareStatus = make_unique<CompareProgressDialog>(*this); //integrate the compare status panel (in hidden state) - auiMgr.AddPane(compareStatus->getAsWindow(), - wxAuiPaneInfo().Name(L"Panel9").Layer(4).Top().Row(1).CaptionVisible(false).PaneBorder(false).Hide()); //name "CmpStatus" used by context menu - - auiMgr.AddPane(m_panelDirectoryPairs, - wxAuiPaneInfo().Name(L"Panel2").Layer(2).Top().Row(2).Caption(_("Folder pairs")).CaptionVisible(false).PaneBorder(false).Gripper()); - + //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_gridNavi, - wxAuiPaneInfo().Name(L"Panel10").Left().Layer(3).Caption(_("Overview")).MinSize(300, m_gridNavi->GetSize().GetHeight())); //MinSize(): just default size, see comment below + wxAuiPaneInfo().Name(L"Panel10").Layer(3).Left().Caption(_("Overview")).MinSize(300, m_gridNavi->GetSize().GetHeight())); //MinSize(): just default size, see comment below auiMgr.AddPane(m_panelConfig, - wxAuiPaneInfo().Name(L"Panel4").Layer(4).Bottom().Row(1).Position(0).Caption(_("Configuration")).MinSize(m_listBoxHistory->GetSize().GetWidth(), m_panelConfig->GetSize().GetHeight())); + wxAuiPaneInfo().Name(L"Panel4").Layer(3).Left().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().Row(1).Position(1).Caption(_("Filter files")).MinSize(m_bpButtonFilter->GetSize().GetWidth(), m_panelFilter->GetSize().GetHeight())); + 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().Row(1).Position(2).Caption(_("Select view")).MinSize(m_bpButtonShowDoNothing->GetSize().GetWidth(), m_panelViewFilter->GetSize().GetHeight())); + 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().Row(1).Position(3).Caption(_("Statistics")).MinSize(m_bitmapData->GetSize().GetWidth() + m_staticTextData->GetSize().GetWidth(), m_panelStatistics->GetSize().GetHeight())); + 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(); @@ -560,7 +565,7 @@ MainDialog::MainDialog(const xmlAccess::XmlGuiConfig& guiCfg, new PanelMoveWindow(*this); //allow moving main dialog by clicking (nearly) anywhere... //ownership passed to "this" #endif - SetIcon(GlobalResources::instance().programIcon); //set application icon + SetIcon(GlobalResources::instance().programIconFFS); //set application icon //notify about (logical) application main window => program won't quit, but stay on this dialog zen::setMainWindow(this); @@ -766,6 +771,14 @@ MainDialog::~MainDialog() wxTheApp->Disconnect(wxEVT_KEY_DOWN, wxKeyEventHandler(MainDialog::OnGlobalKeyEvent), nullptr, this); wxTheApp->Disconnect(wxEVT_CHAR_HOOK, wxKeyEventHandler(MainDialog::OnGlobalKeyEvent), nullptr, this); +#ifdef FFS_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 @@ -790,14 +803,31 @@ void MainDialog::setGlobalCfgOnInit(const xmlAccess::XmlGlobalSettings& globalSe //caveat set/get language asymmmetry! setLanguage(globalSettings.programLanguage); //throw FileError //we need to set langugabe before creating this class! - //set dialog size and position: test ALL parameters at once, since width/height are invalid if the window is minimized (eg x,y == -32000; height = 28, width = 160) - //note: negative values for x and y are possible when using multiple monitors! + //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 && - globalSettings.gui.dlgPos.x >= -3360 && - globalSettings.gui.dlgPos.y >= -200) - //wxDisplay::GetFromPoint(globalSettings.gui.dlgPos) != wxNOT_FOUND) //make sure upper left corner is in visible view -> not required - SetSize(wxRect(globalSettings.gui.dlgPos, globalSettings.gui.dlgSize)); + 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(); @@ -1333,9 +1363,9 @@ void MainDialog::setStatusBarFileStatistics(size_t filesOnLeftView, wxString statusMiddleNew; if (gridDataView->rowsTotal() > 0) { - statusMiddleNew = _P("%x of 1 row in view", "%x of %y rows in view", gridDataView->rowsTotal()); - replace(statusMiddleNew, L"%x", toGuiString(gridDataView->rowsOnView()), false); - replace(statusMiddleNew, L"%y", toGuiString(gridDataView->rowsTotal ()), false); + statusMiddleNew = _P("%y of 1 row in view", "%y of %x rows in view", gridDataView->rowsTotal()); + replace(statusMiddleNew, L"%x", toGuiString(gridDataView->rowsTotal ()), false); //%x is required to be the plural form placeholder! + replace(statusMiddleNew, L"%y", toGuiString(gridDataView->rowsOnView()), false); //%y is secondary placeholder } bSizerStatusRightDirectories->Show(foldersOnRightView > 0); @@ -1380,9 +1410,8 @@ void MainDialog::flashStatusInformation(const wxString& text) m_panelStatusBar->Layout(); //if (needLayoutUpdate) auiMgr.Update(); -> not needed here, this is called anyway in updateGui() - asyncTasks.add2([] { boost::this_thread::sleep(boost::posix_time::millisec(2500)); }, - [this] { this->restoreStatusInformation(); }); - startProcessingAsyncTasks(); + processAsync2([] { boost::this_thread::sleep(boost::posix_time::millisec(2500)); }, + [this] { this->restoreStatusInformation(); }); } @@ -1416,7 +1445,7 @@ void MainDialog::disableAllElements(bool enableAbort) { //when changing consider: comparison, synchronization, manual deletion - EnableCloseButton(false); //not allowed for synchronization! progress indicator is top window! + EnableCloseButton(false); //not allowed for synchronization! progress indicator is top window! -> not honored on OS X! //disables all elements (except abort button) that might receive user input during long-running processes: comparison, deletion m_panelViewFilter ->Disable(); @@ -1764,7 +1793,7 @@ void MainDialog::OnGlobalKeyEvent(wxKeyEvent& event) //process key events withou return; //-> swallow event! case WXK_F8: //F8 - showSyncAction(!showSyncAction_); + setViewTypeSyncAction(!m_bpButtonViewTypeSyncAction->isActive()); return; //-> swallow event! //redirect certain (unhandled) keys directly to grid! @@ -1878,7 +1907,7 @@ void MainDialog::onNaviGridContext(GridClickEvent& event) ContextMenu menu; //---------------------------------------------------------------------------------------------------- - if (showSyncAction_ && !selection.empty()) + 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) @@ -1971,7 +2000,7 @@ void MainDialog::onMainGridContextRim(bool leftSide) const auto& selection = getGridSelection(); //referenced by lambdas! ContextMenu menu; - if (showSyncAction_ && !selection.empty()) + if (!selection.empty()) { auto getImage = [&](SyncDirection dir, SyncOperation soDefault) { @@ -2158,8 +2187,13 @@ void MainDialog::excludeItems(const std::vector<FileSystemObject*>& selection) void MainDialog::onGridLabelContextC(GridClickEvent& event) { ContextMenu menu; - menu.addItem(_("Category") + L"\tF8", [&] { showSyncAction(false); }, showSyncAction_ ? nullptr : &getResourceImage(L"compareSmall")); - menu.addItem(_("Action"), [&] { showSyncAction(true ); }, showSyncAction_ ? &getResourceImage(L"syncSmall") : nullptr); + + const bool actionView = m_bpButtonViewTypeSyncAction->isActive(); + menu.addRadio(_("Category") + (actionView ? L"\tF8" : L""), [&] { setViewTypeSyncAction(false); }, !actionView); + menu.addRadio(_("Action") + (!actionView ? L"\tF8" : L""), [&] { setViewTypeSyncAction(true ); }, actionView); + + //menu.addItem(_("Category") + L"\tF8", [&] { setViewTypeSyncAction(false); }, m_bpButtonViewTypeSyncAction->isActive() ? nullptr : &getResourceImage(L"compareSmall")); + //menu.addItem(_("Action"), [&] { setViewTypeSyncAction(true ); }, m_bpButtonViewTypeSyncAction->isActive() ? &getResourceImage(L"syncSmall") : nullptr); menu.popup(*this); } @@ -2450,9 +2484,7 @@ void MainDialog::removeObsoleteCfgHistoryItems(const std::vector<Zstring>& filen return missingFiles; }; - asyncTasks.add(getMissingFilesAsync, - [this](const std::vector<Zstring>& files) { removeCfgHistoryItems(files); }); - startProcessingAsyncTasks(); + processAsync(getMissingFilesAsync, [this](const std::vector<Zstring>& files) { removeCfgHistoryItems(files); }); } @@ -2605,7 +2637,7 @@ bool MainDialog::trySaveConfig(const Zstring* fileNameGui) //return true if save bool MainDialog::trySaveBatchConfig(const Zstring* fileNameBatch) { - //essentially behave like trySaveConfig(): the collateral damage of not saving GUI-only settings "hideExcludedItems, showSyncAction" is negliable + //essentially behave like trySaveConfig(): the collateral damage of not saving GUI-only settings "hideExcludedItems, m_bpButtonViewTypeSyncAction" is negliable const xmlAccess::XmlGuiConfig guiCfg = getConfig(); @@ -2681,7 +2713,7 @@ bool MainDialog::saveOldConfig() //return false on user abort QuestConfig().setCaption(toWx(activeCfgFilename)). setLabelYes(_("&Save")). setLabelNo(_("Do&n't save")). - showCheckBox(neverSave, _("Never save changes")))) + showCheckBox(neverSave, _("Never save changes"), ReturnQuestionDlg::BUTTON_YES))) { case ReturnQuestionDlg::BUTTON_YES: @@ -2983,7 +3015,7 @@ void MainDialog::setConfig(const xmlAccess::XmlGuiConfig& newGuiCfg, const std:: //read GUI layout m_checkBoxHideExcluded->SetValue(currentCfg.hideExcludedItems); - showSyncAction(currentCfg.showSyncAction); + setViewTypeSyncAction(currentCfg.highlightSyncAction); //########################################################### //update compare variant name @@ -3029,7 +3061,7 @@ xmlAccess::XmlGuiConfig MainDialog::getConfig() const std::back_inserter(guiCfg.mainCfg.additionalPairs), getEnhancedPair); //sync preview - guiCfg.showSyncAction = showSyncAction_; + guiCfg.highlightSyncAction = m_bpButtonViewTypeSyncAction->isActive(); return guiCfg; } @@ -3113,6 +3145,12 @@ void MainDialog::OnGlobalFilterContext(wxMouseEvent& event) } +void MainDialog::OnToggleViewType(wxCommandEvent& event) +{ + setViewTypeSyncAction(!m_bpButtonViewTypeSyncAction->isActive()); //toggle view +} + + void MainDialog::OnToggleViewButton(wxCommandEvent& event) { if (auto button = dynamic_cast<ToggleButton*>(event.GetEventObject())) @@ -3147,63 +3185,28 @@ wxBitmap buttonReleased(const std::string& name) void MainDialog::initViewFilterButtons() { - //compare result buttons - m_bpButtonShowLeftOnly->init(buttonPressed("leftOnly"), - buttonReleased("leftOnly"), - _("Show files that exist on left side only")); - - m_bpButtonShowRightOnly->init(buttonPressed("rightOnly"), - buttonReleased("rightOnly"), - _("Show files that exist on right side only")); - - m_bpButtonShowLeftNewer->init(buttonPressed("leftNewer"), - buttonReleased("leftNewer"), - _("Show files that are newer on left")); - - m_bpButtonShowRightNewer->init(buttonPressed("rightNewer"), - buttonReleased("rightNewer"), - _("Show files that are newer on right")); + m_bpButtonViewTypeSyncAction->init(getResourceImage(L"viewTypeSyncAction"), getResourceImage(L"viewTypeCmpResult")); + //tooltip is updated dynamically in setViewTypeSyncAction() - m_bpButtonShowEqual->init(buttonPressed("equal"), - buttonReleased("equal"), - _("Show files that are equal")); + auto initButton = [](ToggleButton& btn, const char* imgName, const wxString& tooltip) { btn.init(buttonPressed(imgName), buttonReleased(imgName)); btn.SetToolTip(tooltip); }; - m_bpButtonShowDifferent->init(buttonPressed("different"), - buttonReleased("different"), - _("Show files that are different")); - - m_bpButtonShowConflict->init(buttonPressed("conflict"), - buttonReleased("conflict"), - _("Show conflicts")); + //compare result buttons + initButton(*m_bpButtonShowLeftOnly, "leftOnly", _("Show files that exist on left side only")); + initButton(*m_bpButtonShowRightOnly, "rightOnly", _("Show files that exist on right side only")); + initButton(*m_bpButtonShowLeftNewer, "leftNewer", _("Show files that are newer on left")); + initButton(*m_bpButtonShowRightNewer, "rightNewer", _("Show files that are newer on right")); + initButton(*m_bpButtonShowEqual, "equal", _("Show files that are equal")); + initButton(*m_bpButtonShowDifferent, "different", _("Show files that are different")); + initButton(*m_bpButtonShowConflict, "conflict", _("Show conflicts")); //sync preview buttons - m_bpButtonShowCreateLeft->init(buttonPressed("createLeft"), - buttonReleased("createLeft"), - _("Show files that will be created on the left side")); - - m_bpButtonShowCreateRight->init(buttonPressed("createRight"), - buttonReleased("createRight"), - _("Show files that will be created on the right side")); - - m_bpButtonShowDeleteLeft->init(buttonPressed("deleteLeft"), - buttonReleased("deleteLeft"), - _("Show files that will be deleted on the left side")); - - m_bpButtonShowDeleteRight->init(buttonPressed("deleteRight"), - buttonReleased("deleteRight"), - _("Show files that will be deleted on the right side")); - - m_bpButtonShowUpdateLeft->init(buttonPressed("updateLeft"), - buttonReleased("updateLeft"), - _("Show files that will be overwritten on left side")); - - m_bpButtonShowUpdateRight->init(buttonPressed("updateRight"), - buttonReleased("updateRight"), - _("Show files that will be overwritten on right side")); - - m_bpButtonShowDoNothing->init(buttonPressed("none"), - buttonReleased("none"), - _("Show files that won't be copied")); + initButton(*m_bpButtonShowCreateLeft, "createLeft", _("Show files that will be created on the left side")); + initButton(*m_bpButtonShowCreateRight, "createRight", _("Show files that will be created on the right side")); + initButton(*m_bpButtonShowDeleteLeft, "deleteLeft", _("Show files that will be deleted on the left side")); + initButton(*m_bpButtonShowDeleteRight, "deleteRight", _("Show files that will be deleted on the right side")); + initButton(*m_bpButtonShowUpdateLeft, "updateLeft", _("Show files that will be overwritten on left side")); + initButton(*m_bpButtonShowUpdateRight, "updateRight", _("Show files that will be overwritten on right side")); + initButton(*m_bpButtonShowDoNothing, "none", _("Show files that won't be copied")); } @@ -3299,6 +3302,9 @@ void MainDialog::OnCompare(wxCommandEvent& event) clearGrid(); //avoid memory peak by clearing old data first + disableAllElements(true); //CompareStatusHandler will internally process Window messages, so avoid unexpected callbacks! + ZEN_ON_SCOPE_EXIT(updateUiNow(); enableAllElements()); //ui update before enabling buttons again: prevent strange behaviour of delayed button clicks + try { //class handling status display and error messages @@ -3322,6 +3328,7 @@ void MainDialog::OnCompare(wxCommandEvent& event) } catch (GuiAbortProcess&) { + flashStatusInformation(_("Operation aborted!")); // if (m_buttonCompare->IsShownOnScreen()) m_buttonCompare->SetFocus(); updateGui(); //refresh grid in ANY case! (also on abort) return; @@ -3337,9 +3344,9 @@ void MainDialog::OnCompare(wxCommandEvent& event) 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); + //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())); @@ -3451,10 +3458,10 @@ void MainDialog::applyCompareConfig(bool switchMiddleGrid) switch (currentCfg.mainCfg.cmpConfig.compareVar) { case CMP_BY_TIME_SIZE: - showSyncAction(true); + setViewTypeSyncAction(true); break; case CMP_BY_CONTENT: - showSyncAction(false); + setViewTypeSyncAction(false); break; } } @@ -3515,6 +3522,9 @@ void MainDialog::OnStartSync(wxCommandEvent& event) 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, @@ -3560,7 +3570,7 @@ void MainDialog::OnStartSync(wxCommandEvent& event) //do NOT disable the sync button: user might want to try to sync the REMAINING rows } //enableSynchronization(false); - //remove rows that empty: just a beautification, invalid rows shouldn't cause issues + //remove empty rows: just a beautification, invalid rows shouldn't cause issues gridDataView->removeInvalidRows(); updateGui(); @@ -3616,7 +3626,7 @@ void MainDialog::onGridLabelLeftClickR(GridClickEvent& event) void MainDialog::onGridLabelLeftClickC(GridClickEvent& event) { //sorting middle grid is more or less useless: therefore let's toggle view instead! - showSyncAction(!showSyncAction_); //toggle view + setViewTypeSyncAction(!m_bpButtonViewTypeSyncAction->isActive()); //toggle view } @@ -3696,7 +3706,7 @@ void MainDialog::updateGridViewData() m_bpButtonShowUpdateRight->Show(false); m_bpButtonShowDoNothing ->Show(false); - if (showSyncAction_) + if (m_bpButtonViewTypeSyncAction->isActive()) { const GridView::StatusSyncPreview result = gridDataView->updateSyncPreview(currentCfg.hideExcludedItems, m_bpButtonShowCreateLeft ->isActive(), @@ -3727,22 +3737,24 @@ void MainDialog::updateGridViewData() m_bpButtonShowEqual ->Show(result.existsSyncEqual); m_bpButtonShowConflict ->Show(result.existsConflict); - if (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()) + 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 { @@ -3770,13 +3782,16 @@ void MainDialog::updateGridViewData() m_bpButtonShowEqual ->Show(result.existsEqual); m_bpButtonShowConflict ->Show(result.existsConflict); - if (m_bpButtonShowLeftOnly ->IsShown() || - m_bpButtonShowRightOnly ->IsShown() || - m_bpButtonShowLeftNewer ->IsShown() || - m_bpButtonShowRightNewer->IsShown() || - m_bpButtonShowDifferent ->IsShown() || - m_bpButtonShowEqual ->IsShown() || - m_bpButtonShowConflict ->IsShown()) + 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(); @@ -3788,7 +3803,7 @@ void MainDialog::updateGridViewData() gridview::refresh(*m_gridMainL, *m_gridMainC, *m_gridMainR); //navigation tree - if (showSyncAction_) + if (m_bpButtonViewTypeSyncAction->isActive()) treeDataView->updateSyncPreview(currentCfg.hideExcludedItems, m_bpButtonShowCreateLeft ->isActive(), m_bpButtonShowCreateRight->isActive(), @@ -3894,11 +3909,11 @@ void MainDialog::OnRemoveFolderPair(wxCommandEvent& event) wxWindowUpdateLocker dummy(this); //avoid display distortion const wxObject* const eventObj = event.GetEventObject(); //find folder pair originating the event - for (std::vector<DirectoryPair*>::const_iterator it = additionalFolderPairs.begin(); it != additionalFolderPairs.end(); ++it) + for (auto it = additionalFolderPairs.begin(); it != additionalFolderPairs.end(); ++it) if (eventObj == (*it)->m_bpButtonRemovePair) { removeAddFolderPair(it - additionalFolderPairs.begin()); - return; + break; } } @@ -3998,10 +4013,10 @@ void MainDialog::addFolderPair(const std::vector<FolderPairEnh>& newPairs, bool newPair->m_bpButtonRemovePair->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(MainDialog::OnRemoveFolderPair), nullptr, this); }); - //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 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, @@ -4024,8 +4039,12 @@ void MainDialog::removeAddFolderPair(size_t pos) //const int pairHeight = pairToDelete->GetSize().GetHeight(); bSizerAddFolderPairs->Detach(pairToDelete); //Remove() does not work on Window*, so do it manually - pairToDelete->Destroy(); // additionalFolderPairs.erase(additionalFolderPairs.begin() + pos); //remove element from vector + //more (non-portable) wxWidgets bullshit: on OS X wxWindow::Destroy() screws up and calls "operator delete" directly rather than + //the deferred deletion it is expected to do (and which is implemented correctly on Windows and Linux) + //http://bb10.com/python-wxpython-devel/2012-09/msg00004.html + //=> since we're in a mouse button callback of a sub-component of "pairToDelete" we need to delay deletion ourselves: + processAsync2([] {}, [pairToDelete] { pairToDelete->Destroy(); }); //set size of scrolled window //const size_t additionalRows = additionalFolderPairs.size(); @@ -4083,7 +4102,7 @@ void MainDialog::OnMenuExportFileList(wxCommandEvent& event) //http://en.wikipedia.org/wiki/Comma-separated_values const lconv* localInfo = ::localeconv(); //always bound according to doc - const bool haveCommaAsDecimalSep = strcmp(localInfo->decimal_point, ",") == 0; + const bool haveCommaAsDecimalSep = std::string(localInfo->decimal_point) == ","; const char CSV_SEP = haveCommaAsDecimalSep ? ';' : ','; @@ -4100,32 +4119,6 @@ void MainDialog::OnMenuExportFileList(wxCommandEvent& event) 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; - //write legend - header += fmtValue(_("Legend")) + '\n'; - if (showSyncAction_) - { - header += fmtValue(getSyncOpDescription(SO_EQUAL)) + CSV_SEP + fmtValue(getSymbol(SO_EQUAL)) + '\n'; - header += fmtValue(getSyncOpDescription(SO_CREATE_NEW_LEFT)) + CSV_SEP + fmtValue(getSymbol(SO_CREATE_NEW_LEFT)) + '\n'; - header += fmtValue(getSyncOpDescription(SO_CREATE_NEW_RIGHT)) + CSV_SEP + fmtValue(getSymbol(SO_CREATE_NEW_RIGHT)) + '\n'; - header += fmtValue(getSyncOpDescription(SO_OVERWRITE_LEFT)) + CSV_SEP + fmtValue(getSymbol(SO_OVERWRITE_LEFT)) + '\n'; - header += fmtValue(getSyncOpDescription(SO_OVERWRITE_RIGHT)) + CSV_SEP + fmtValue(getSymbol(SO_OVERWRITE_RIGHT)) + '\n'; - header += fmtValue(getSyncOpDescription(SO_DELETE_LEFT)) + CSV_SEP + fmtValue(getSymbol(SO_DELETE_LEFT)) + '\n'; - header += fmtValue(getSyncOpDescription(SO_DELETE_RIGHT)) + CSV_SEP + fmtValue(getSymbol(SO_DELETE_RIGHT)) + '\n'; - header += fmtValue(getSyncOpDescription(SO_DO_NOTHING)) + CSV_SEP + fmtValue(getSymbol(SO_DO_NOTHING)) + '\n'; - header += fmtValue(getSyncOpDescription(SO_UNRESOLVED_CONFLICT)) + CSV_SEP + fmtValue(getSymbol(SO_UNRESOLVED_CONFLICT)) + '\n'; - } - else - { - header += fmtValue(getCategoryDescription(FILE_EQUAL)) + CSV_SEP + fmtValue(getSymbol(FILE_EQUAL)) + '\n'; - header += fmtValue(getCategoryDescription(FILE_DIFFERENT)) + CSV_SEP + fmtValue(getSymbol(FILE_DIFFERENT)) + '\n'; - header += fmtValue(getCategoryDescription(FILE_LEFT_SIDE_ONLY)) + CSV_SEP + fmtValue(getSymbol(FILE_LEFT_SIDE_ONLY)) + '\n'; - header += fmtValue(getCategoryDescription(FILE_RIGHT_SIDE_ONLY)) + CSV_SEP + fmtValue(getSymbol(FILE_RIGHT_SIDE_ONLY)) + '\n'; - header += fmtValue(getCategoryDescription(FILE_LEFT_NEWER)) + CSV_SEP + fmtValue(getSymbol(FILE_LEFT_NEWER)) + '\n'; - header += fmtValue(getCategoryDescription(FILE_RIGHT_NEWER)) + CSV_SEP + fmtValue(getSymbol(FILE_RIGHT_NEWER)) + '\n'; - header += fmtValue(getCategoryDescription(FILE_CONFLICT)) + CSV_SEP + fmtValue(getSymbol(FILE_CONFLICT)) + '\n'; - } - header += '\n'; - //base folders header += fmtValue(_("Folder pairs")) + '\n' ; std::for_each(begin(folderCmp), end(folderCmp), @@ -4146,7 +4139,7 @@ void MainDialog::OnMenuExportFileList(wxCommandEvent& event) 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_; }); + vector_remove_if(colAttrMiddle, [](const Grid::ColumnAttribute& ca) { return !ca.visible_ || static_cast<ColumnTypeMiddle>(ca.type_) == COL_TYPE_CHECKBOX; }); vector_remove_if(colAttrRight , [](const Grid::ColumnAttribute& ca) { return !ca.visible_; }); if (provLeft && provMiddle && provRight) @@ -4206,16 +4199,12 @@ void MainDialog::OnMenuExportFileList(wxCommandEvent& event) tmp += fmtValue(provMiddle->getValue(row, ca.type_)); tmp += CSV_SEP; }); - if (!colAttrRight.empty()) + std::for_each(colAttrRight.begin(), colAttrRight.end(), + [&](const Grid::ColumnAttribute& ca) { - std::for_each(colAttrRight.begin(), colAttrRight.end() - 1, - [&](const Grid::ColumnAttribute& ca) - { - tmp += fmtValue(provRight->getValue(row, ca.type_)); - tmp += CSV_SEP; - }); - tmp += fmtValue(provRight->getValue(row, colAttrRight.back().type_)); - } + tmp += fmtValue(provRight->getValue(row, ca.type_)); + tmp += CSV_SEP; + }); tmp += '\n'; replace(tmp, '\n', LINE_BREAK); @@ -4295,6 +4284,10 @@ void MainDialog::switchProgramLanguage(int 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(); } @@ -4308,12 +4301,15 @@ void MainDialog::OnMenuLanguageSwitch(wxCommandEvent& event) //######################################################################################################### -void MainDialog::showSyncAction(bool value) +void MainDialog::setViewTypeSyncAction(bool value) { - showSyncAction_ = value; + //if (m_bpButtonViewTypeSyncAction->isActive() == value) return; support polling -> what about initialization? + + m_bpButtonViewTypeSyncAction->setActive(value); + m_bpButtonViewTypeSyncAction->SetToolTip((value ? _("Action") : _("Category")) + L" (F8)"); //toggle display of sync preview in middle grid - gridview::showSyncAction(*m_gridMainC, value); + gridview::highlightSyncAction(*m_gridMainC, value); updateGui(); } diff --git a/ui/main_dlg.h b/ui/main_dlg.h index 70f78e93..2d132814 100644 --- a/ui/main_dlg.h +++ b/ui/main_dlg.h @@ -127,9 +127,13 @@ private: void openExternalApplication(const wxString& commandline, const std::vector<zen::FileSystemObject*>& selection, bool leftSide); //selection may be empty //don't use wxWidgets idle handling => repeated idle requests/consumption hogs 100% cpu! - void startProcessingAsyncTasks() { timerForAsyncTasks.Start(50); } //timer interval in [ms] void onProcessAsyncTasks(wxEvent& event); + template <class Fun, class Fun2> + void processAsync(Fun doAsync, Fun2 evalOnGui) { asyncTasks.add(doAsync, evalOnGui); timerForAsyncTasks.Start(50); /*timer interval in [ms] */ } + template <class Fun, class Fun2> + void processAsync2(Fun doAsync, Fun2 evalOnGui) { asyncTasks.add2(doAsync, evalOnGui); timerForAsyncTasks.Start(50); /*timer interval in [ms] */ } + //status bar supports one of the following two states at a time: void setStatusBarFullText(const wxString& msg); void setStatusBarFileStatistics(size_t filesOnLeftView, size_t foldersOnLeftView, size_t filesOnRightView, size_t foldersOnRightView, zen::UInt64 filesizeLeftView, zen::UInt64 filesizeRightView); @@ -186,6 +190,7 @@ private: void onGridLabelContextR(zen::GridClickEvent& event); void onGridLabelContext(zen::Grid& grid, zen::ColumnTypeRim type, const std::vector<zen::ColumnAttributeRim>& defaultColumnAttributes); + void OnToggleViewType (wxCommandEvent& event); void OnToggleViewButton(wxCommandEvent& event); void OnConfigNew (wxCommandEvent& event); @@ -279,9 +284,10 @@ private: bool processingGlobalKeyEvent; //indicator to notify recursion in OnGlobalKeyEvent() - bool showSyncAction_; //toggle to display configuration preview instead of comparison result - //use this methods when changing values! - void showSyncAction(bool value); + //toggle to display configuration preview instead of comparison result: + //for read access use: m_bpButtonViewTypeSyncAction->isActive() + //when changing value use: + void setViewTypeSyncAction(bool value); wxAuiManager auiMgr; //implement dockable GUI design diff --git a/ui/msg_popup.cpp b/ui/msg_popup.cpp index 940163af..59908cf9 100644 --- a/ui/msg_popup.cpp +++ b/ui/msg_popup.cpp @@ -35,6 +35,8 @@ private: void OnCancel(wxCommandEvent& event) { EndModal(ReturnErrorDlg::BUTTON_CANCEL); } void OnButton1(wxCommandEvent& event); void OnButton2(wxCommandEvent& event); + void OnCheckBoxClick(wxCommandEvent& event) { updateGui(); event.Skip(); } + void updateGui(); bool* ignoreErrors; wxButton& buttonRetry; // @@ -87,9 +89,16 @@ ErrorDlg::ErrorDlg(wxWindow* parent, int activeButtons, const wxString& messageT else if (activeButtons & ReturnErrorDlg::BUTTON_CANCEL) setAsStandard(*m_buttonCancel); + updateGui(); Fit(); //child-element widths have changed: image was set } +void ErrorDlg::updateGui() +{ + //button doesn't make sense when checkbox is set! + buttonRetry.Enable(!checkBoxIgnoreErrors.GetValue()); +} + void ErrorDlg::OnButton1(wxCommandEvent& event) //retry { @@ -135,6 +144,8 @@ private: void OnCancel(wxCommandEvent& event) { EndModal(ReturnWarningDlg::BUTTON_CANCEL); } void OnButton1(wxCommandEvent& event); void OnButton2(wxCommandEvent& event); + void OnCheckBoxClick(wxCommandEvent& event) { updateGui(); event.Skip(); } + void updateGui(); bool& dontShowAgain; wxButton& buttonIgnore; // @@ -182,10 +193,18 @@ WarningDlg::WarningDlg(wxWindow* parent, int activeButtons, const wxString& mes else if (activeButtons & ReturnWarningDlg::BUTTON_CANCEL) setAsStandard(*m_buttonCancel); + updateGui(); Fit(); //child-element widths have changed: image was set } +void WarningDlg::updateGui() +{ + //button doesn't make sense when checkbox is set! + buttonSwitch.Enable(!checkBoxDontShowAgain.GetValue()); +} + + void WarningDlg::OnButton1(wxCommandEvent& event) //ignore { dontShowAgain = checkBoxDontShowAgain.GetValue(); @@ -212,49 +231,49 @@ ReturnWarningDlg::ButtonPressed zen::showWarningDlg(wxWindow* parent, int active class QuestionDlg : public MessageDlgGenerated { public: - QuestionDlg(wxWindow* parent, int activeButtons, const wxString& messageText, const wxString& caption, const wxString& labelYes, const wxString& labelNo, bool* checkBoxValue, const wxString& checkBoxLabel); + QuestionDlg(wxWindow* parent, int activeButtons, const wxString& messageText, const QuestConfig& cfg); private: void OnClose (wxCloseEvent& event) { EndModal(ReturnQuestionDlg::BUTTON_CANCEL); } void OnCancel(wxCommandEvent& event) { EndModal(ReturnQuestionDlg::BUTTON_CANCEL); } void OnButton1(wxCommandEvent& event); void OnButton2(wxCommandEvent& event); + void OnCheckBoxClick(wxCommandEvent& event) { updateGui(); event.Skip(); } + void updateGui(); - bool* checkBoxValue_; //optional wxButton& buttonYes; // map generic controls wxButton& buttonNo; // + + bool* const checkBoxValue_; //optional + const int disabledButtonsWhenChecked_; }; QuestionDlg::QuestionDlg(wxWindow* parent, int activeButtons, const wxString& messageText, - const wxString& caption, //optional - const wxString& labelYes, - const wxString& labelNo, - //optional checkbox: - bool* checkBoxValue, - const wxString& checkBoxLabel) : + const QuestConfig& cfg) : MessageDlgGenerated(parent), - checkBoxValue_(checkBoxValue), buttonYes(*m_buttonCustom1), - buttonNo (*m_buttonCustom2) + buttonNo (*m_buttonCustom2), + checkBoxValue_(cfg.checkBoxValue), + disabledButtonsWhenChecked_(cfg.disabledButtonsWhenChecked_) { #ifdef FFS_WIN new zen::MouseMoveWindow(*this); //allow moving main dialog by clicking (nearly) anywhere...; ownership passed to "this" #endif - SetTitle(!caption.empty()? caption : _("Question")); + SetTitle(!cfg.caption.empty()? cfg.caption : _("Question")); m_bitmapMsgType->SetBitmap(getResourceImage(L"msg_question")); m_textCtrlMessage->SetValue(messageText); - buttonYes.SetLabel(!labelYes.empty() ? labelYes : _("&Yes")); - buttonNo .SetLabel(!labelNo .empty() ? labelNo : _("&No")); + buttonYes.SetLabel(!cfg.labelYes.empty() ? cfg.labelYes : _("&Yes")); + buttonNo .SetLabel(!cfg.labelNo .empty() ? cfg.labelNo : _("&No")); //buttonYes.SetId(wxID_YES); -> see comment in ErrorDlg //buttonNo .SetId(wxID_NO); - if (checkBoxValue) + if (cfg.checkBoxValue) { - m_checkBoxCustom->SetValue(*checkBoxValue); - m_checkBoxCustom->SetLabel(checkBoxLabel); + m_checkBoxCustom->SetValue(*cfg.checkBoxValue); + m_checkBoxCustom->SetLabel(cfg.checkBoxLabel); } else m_checkBoxCustom->Hide(); @@ -276,9 +295,24 @@ QuestionDlg::QuestionDlg(wxWindow* parent, else if (activeButtons & ReturnQuestionDlg::BUTTON_CANCEL) setAsStandard(*m_buttonCancel); + updateGui(); Fit(); //child-element widths have changed: image was set } + +void QuestionDlg::updateGui() +{ + auto updateEnabledStatus = [&](wxButton& btn, ReturnQuestionDlg::ButtonPressed btnId) + { + if (disabledButtonsWhenChecked_ & btnId) + btn.Enable(!m_checkBoxCustom->GetValue()); + }; + updateEnabledStatus(buttonYes, ReturnQuestionDlg::BUTTON_YES); + updateEnabledStatus(buttonNo, ReturnQuestionDlg::BUTTON_NO); + updateEnabledStatus(*m_buttonCancel, ReturnQuestionDlg::BUTTON_CANCEL); +} + + void QuestionDlg::OnButton1(wxCommandEvent& event) //yes { if (checkBoxValue_) @@ -299,7 +333,7 @@ ReturnQuestionDlg::ButtonPressed zen::showQuestionDlg(wxWindow* parent, const wxString& messageText, const QuestConfig& cfg) { - QuestionDlg qtnDlg(parent, activeButtons, messageText, cfg.caption, cfg.labelYes, cfg.labelNo, cfg.checkBoxValue, cfg.checkBoxLabel); + QuestionDlg qtnDlg(parent, activeButtons, messageText, cfg); qtnDlg.Raise(); return static_cast<ReturnQuestionDlg::ButtonPressed>(qtnDlg.ShowModal()); } diff --git a/ui/msg_popup.h b/ui/msg_popup.h index 5e59eb6b..5e7ea97f 100644 --- a/ui/msg_popup.h +++ b/ui/msg_popup.h @@ -4,12 +4,13 @@ // * Copyright (C) Zenju (zenju AT gmx DOT de) - All Rights Reserved * // ************************************************************************** -#ifndef MESSAGEPOPUP_H_INCLUDED -#define MESSAGEPOPUP_H_INCLUDED +#ifndef MESSAGEPOPUP_H_820780154723456 +#define MESSAGEPOPUP_H_820780154723456 #include <wx/window.h> #include <wx/string.h> +class QuestionDlg; namespace zen { @@ -60,20 +61,17 @@ struct ReturnQuestionDlg }; }; -class QuestConfig; -ReturnQuestionDlg::ButtonPressed showQuestionDlg(wxWindow* parent, int activeButtons, const wxString& messageText, const QuestConfig& cfg); - class QuestConfig { public: - QuestConfig() : checkBoxValue() {} + QuestConfig() : checkBoxValue(), disabledButtonsWhenChecked_() {} QuestConfig& setCaption (const wxString& label) { caption = label; return *this; } QuestConfig& setLabelYes(const wxString& label) { labelYes = label; return *this; } QuestConfig& setLabelNo (const wxString& label) { labelNo = label; return *this; } - QuestConfig& showCheckBox(bool& value, const wxString& label) { checkBoxLabel = label; checkBoxValue = &value; return *this; } + QuestConfig& showCheckBox(bool& value, const wxString& label, int disabledButtonsWhenChecked) { checkBoxValue = &value; checkBoxLabel = label; disabledButtonsWhenChecked_ = disabledButtonsWhenChecked; return *this; } private: - friend ReturnQuestionDlg::ButtonPressed showQuestionDlg(wxWindow* parent, int activeButtons, const wxString& messageText, const QuestConfig& cfg); + friend class ::QuestionDlg; wxString caption; wxString labelYes; //overwrite default "Yes, No" labels @@ -81,9 +79,10 @@ private: //optional checkbox: bool* checkBoxValue; //in/out wxString checkBoxLabel; //in + int disabledButtonsWhenChecked_; }; ReturnQuestionDlg::ButtonPressed showQuestionDlg(wxWindow* parent, int activeButtons, const wxString& messageText, const QuestConfig& cfg = QuestConfig()); } -#endif // MESSAGEPOPUP_H_INCLUDED +#endif //MESSAGEPOPUP_H_820780154723456 diff --git a/ui/progress_indicator.cpp b/ui/progress_indicator.cpp index d23de61a..37237861 100644 --- a/ui/progress_indicator.cpp +++ b/ui/progress_indicator.cpp @@ -12,8 +12,11 @@ #include <wx/sound.h> #include <wx/clipbrd.h> #include <wx/msgdlg.h> +#include <wx/dataobj.h> //wxTextDataObject #include <zen/basic_math.h> #include <zen/format_unit.h> +#include <zen/scope_guard.h> +#include <wx+/grid.h> #include <wx+/mouse_move_dlg.h> #include <wx+/toggle_button.h> #include <wx+/image_tools.h> @@ -38,7 +41,7 @@ namespace const int GAUGE_FULL_RANGE = 50000; //window size used for statistics in milliseconds -const int WINDOW_REMAINING_TIME = 60000; //some use cases have dropouts of 40 seconds -> 60 sec. window size handles them well +const int WINDOW_REMAINING_TIME = 60000; //some scenarios have dropouts of 40 seconds -> 60 sec. window size handles them well const int WINDOW_BYTES_PER_SEC = 5000; // } @@ -59,6 +62,7 @@ private: wxString titleTextBackup; wxStopWatch timeElapsed; + long binCompStartMs; //begin of binary comparison phase in [ms] const Statistics* syncStat_; //only bound while sync is running @@ -72,8 +76,9 @@ private: CompareProgressDialog::Pimpl::Pimpl(wxTopLevelWindow& parentWindow) : CompareProgressDlgGenerated(&parentWindow), parentWindow_(parentWindow), + binCompStartMs(0), syncStat_(nullptr), - lastStatCallSpeed (-1000000) //some big number + lastStatCallSpeed(-1000000) //some big number { //init(); -> needed? } @@ -125,6 +130,8 @@ void CompareProgressDialog::Pimpl::switchToCompareBytewise() perf = make_unique<PerfCheck>(WINDOW_REMAINING_TIME, WINDOW_BYTES_PER_SEC); lastStatCallSpeed = -1000000; //some big number + binCompStartMs = timeElapsed.Time(); + //show status for comparing bytewise bSizerFilesFound ->Show(false); bSizerFilesRemaining->Show(true); @@ -153,6 +160,7 @@ void CompareProgressDialog::Pimpl::updateStatusPanelNow() }; bool layoutChanged = false; //avoid screen flicker by calling layout() only if necessary + const long timeNow = timeElapsed.Time(); //status texts setText(*m_textCtrlStatus, replaceCpy(syncStat_->currentStatusText(), L'\n', L' ')); //no layout update for status texts! @@ -194,22 +202,22 @@ void CompareProgressDialog::Pimpl::updateStatusPanelNow() setText(*m_staticTextDataRemaining, L"(" + filesizeToShortString(dataTotal - dataCurrent) + L")", &layoutChanged); //remaining time and speed: only visible during binary comparison + assert(perf); if (perf) - { - const long timeNow = timeElapsed.Time(); if (numeric::dist(lastStatCallSpeed, timeNow) >= 500) { lastStatCallSpeed = timeNow; - perf->addSample(objectsCurrent, to<double>(dataCurrent), timeNow); + if (numeric::dist(binCompStartMs, timeNow) >= 1000) //discard stats for first second: probably messy + perf->addSample(objectsCurrent, to<double>(dataCurrent), timeNow); - //current speed -> Win 7 copy uses 1 sec update interval + //current speed -> Win 7 copy uses 1 sec update interval instead setText(*m_staticTextSpeed, perf->getBytesPerSecond(), &layoutChanged); - } - //remaining time - setText(*m_staticTextRemTime, perf->getRemainingTime(to<double>(dataTotal - dataCurrent)), &layoutChanged); - } + //remaining time: display with relative error of 10% - based on samples taken every 0.5 sec only + //-> call more often than once per second to correctly show last few seconds countdown, but don't call too often to avoid occasional jitter + setText(*m_staticTextRemTime, perf->getRemainingTime(to<double>(dataTotal - dataCurrent)), &layoutChanged); + } } break; @@ -222,7 +230,7 @@ void CompareProgressDialog::Pimpl::updateStatusPanelNow() setText(*m_staticTextScanned, scannedObjects, &layoutChanged); //time elapsed - const long timeElapSec = timeElapsed.Time() / 1000; + const long timeElapSec = timeNow / 1000; setText(*m_staticTextTimeElapsed, timeElapSec < 3600 ? wxTimeSpan::Seconds(timeElapSec).Format( L"%M:%S") : @@ -565,9 +573,11 @@ public: const int warningCount = log.getItemCount(TYPE_WARNING); const int infoCount = log.getItemCount(TYPE_INFO); - m_bpButtonErrors ->init(buttonPressed ("msg_error" ), buttonReleased("msg_error" ), _("Error" ) + wxString::Format(L" (%d)", errorCount )); - m_bpButtonWarnings->init(buttonPressed ("msg_warning"), buttonReleased("msg_warning"), _("Warning") + wxString::Format(L" (%d)", warningCount)); - m_bpButtonInfo ->init(buttonPressed ("msg_info" ), buttonReleased("msg_info" ), _("Info" ) + wxString::Format(L" (%d)", infoCount )); + auto initButton = [](ToggleButton& btn, const char* imgName, const wxString& tooltip) { btn.init(buttonPressed(imgName), buttonReleased(imgName)); btn.SetToolTip(tooltip); }; + + initButton(*m_bpButtonErrors, "msg_error", _("Error" ) + wxString::Format(L" (%d)", errorCount )); + initButton(*m_bpButtonWarnings, "msg_warning", _("Warning") + wxString::Format(L" (%d)", warningCount)); + initButton(*m_bpButtonInfo, "msg_info", _("Info" ) + wxString::Format(L" (%d)", infoCount )); m_bpButtonErrors ->setActive(true); m_bpButtonWarnings->setActive(true); @@ -832,41 +842,45 @@ struct LabelFormatterTimeElapsed : public LabelFormatter } -class SyncProgressDialog::Pimpl : public SyncProgressDlgGenerated +class SyncProgressDialogImpl : private SyncProgressDlgGenerated, public SyncProgressDialog { public: - Pimpl(AbortCallback& abortCb, - const Statistics& syncStat, - MainDialog* parentWindow, - const wxString& jobName, - const std::wstring& execWhenFinished, - std::vector<std::wstring>& execFinishedHistory); - ~Pimpl(); - - void initNewPhase(); - void notifyProgressChange(); - void updateGui(bool allowYield = true); + SyncProgressDialogImpl(AbortCallback& abortCb, + const std::function<void()>& notifyWindowTerminate, + const Statistics& syncStat, + wxTopLevelWindow* parentWindow, + bool showProgress, + const wxString& jobName, + const std::wstring& execWhenFinished, + std::vector<std::wstring>& execFinishedHistory); + ~SyncProgressDialogImpl(); //call this in StatusUpdater derived class destructor at the LATEST(!) to prevent access to currentStatusUpdater - void processHasFinished(SyncResult resultId, const ErrorLog& log); - void closeWindowDirectly(); + virtual void processHasFinished(SyncResult resultId, const ErrorLog& log); + virtual void closeWindowDirectly(); + + virtual wxWindow* getAsWindow() { return this; } + virtual void initNewPhase(); + virtual void notifyProgressChange(); + virtual void updateGui() { updateGuiInt(true); } - std::wstring getExecWhenFinishedCommand() const; + virtual std::wstring getExecWhenFinishedCommand() const; - void stopTimer() //halt all internal counters! + virtual void stopTimer() //halt all internal counters! { m_animationControl1->Stop(); timeElapsed.Pause (); } - void resumeTimer() + virtual void resumeTimer() { m_animationControl1->Play(); timeElapsed.Resume(); } +private: + void updateGuiInt(bool allowYield); void minimizeToTray(); -private: void OnKeyPressed(wxKeyEvent& event); virtual void OnOkay (wxCommandEvent& event); virtual void OnPause (wxCommandEvent& event); @@ -884,11 +898,13 @@ private: const wxString jobName_; wxStopWatch timeElapsed; - MainDialog* mainDialog; //optional + wxTopLevelWindow* mainDialog; //optional + + std::function<void()> notifyWindowTerminate_; //call once in OnClose(), NOT in destructor which is called far too late somewhere in wxWidgets main loop! //status variables - AbortCallback* abortCb_; //temporarily bound while sync is running - const Statistics* syncStat_; // + const Statistics* syncStat_; // + AbortCallback* abortCb_; //valid only while sync is running bool paused_; //valid only while sync is running SyncResult finalResult; //set after sync @@ -909,12 +925,14 @@ private: }; -SyncProgressDialog::Pimpl::Pimpl(AbortCallback& abortCb, - const Statistics& syncStat, - MainDialog* parentWindow, - const wxString& jobName, - const std::wstring& execWhenFinished, - std::vector<std::wstring>& execFinishedHistory) : +SyncProgressDialogImpl::SyncProgressDialogImpl(AbortCallback& abortCb, + const std::function<void()>& notifyWindowTerminate, + const Statistics& syncStat, + wxTopLevelWindow* parentWindow, + bool showProgress, + const wxString& jobName, + const std::wstring& execWhenFinished, + std::vector<std::wstring>& execFinishedHistory) : SyncProgressDlgGenerated(parentWindow, wxID_ANY, parentWindow ? wxString() : (wxString(L"FreeFileSync - ") + _("Folder Comparison and Synchronization")), @@ -924,8 +942,9 @@ SyncProgressDialog::Pimpl::Pimpl(AbortCallback& abortCb, wxDEFAULT_FRAME_STYLE | wxTAB_TRAVERSAL), jobName_ (jobName), mainDialog(parentWindow), - abortCb_ (&abortCb), + notifyWindowTerminate_(notifyWindowTerminate), syncStat_ (&syncStat), + abortCb_ (&abortCb), paused_ (false), finalResult(RESULT_ABORTED), //dummy value isZombie(false), @@ -941,23 +960,18 @@ SyncProgressDialog::Pimpl::Pimpl(AbortCallback& abortCb, setRelativeFontSize(*m_staticTextPhase, 1.5); if (mainDialog) - { titelTextBackup = mainDialog->GetTitle(); //save old title (will be used as progress indicator) - mainDialog->disableAllElements(false); //disable all child elements - } m_animationControl1->SetAnimation(GlobalResources::instance().aniSync); m_animationControl1->Play(); - SetIcon(GlobalResources::instance().programIcon); + SetIcon(GlobalResources::instance().programIconFFS); //initialize gauge m_gauge1->SetRange(GAUGE_FULL_RANGE); m_gauge1->SetValue(0); - warn_static("not honored on osx") - - EnableCloseButton(false); + EnableCloseButton(false); //this is NOT honored on OS X or during system shutdown on Windows! if (IsShown()) //don't steal focus when starting in sys-tray! m_buttonAbort->SetFocus(); @@ -978,7 +992,7 @@ SyncProgressDialog::Pimpl::Pimpl(AbortCallback& abortCb, Layout(); //register key event - Connect(wxEVT_CHAR_HOOK, wxKeyEventHandler(Pimpl::OnKeyPressed), nullptr, this); + Connect(wxEVT_CHAR_HOOK, wxKeyEventHandler(SyncProgressDialogImpl::OnKeyPressed), nullptr, this); //init graph graphDataBytes = std::make_shared<GraphDataBytes>(); @@ -1003,14 +1017,22 @@ SyncProgressDialog::Pimpl::Pimpl(AbortCallback& abortCb, updateDialogStatus(); //null-status will be shown while waiting for dir locks (if at all) Fit(); + + if (showProgress) + { + Show(); + //clear gui flicker, remove dummy texts: window must be visible to make this work! + updateGuiInt(true); //at least on OS X a real Yield() is required to flush pending GUI updates; Update() is not enough + } + else + minimizeToTray(); } -SyncProgressDialog::Pimpl::~Pimpl() +SyncProgressDialogImpl::~SyncProgressDialogImpl() { if (mainDialog) { - mainDialog->enableAllElements(); mainDialog->SetTitle(titelTextBackup); //restore title text //make sure main dialog is shown again if still "minimized to systray"! see SyncProgressDialog::closeWindowDirectly() @@ -1019,10 +1041,12 @@ SyncProgressDialog::Pimpl::~Pimpl() mainDialog->Show(); } + + //our client is NOT expecting a second call vianotifyWindowTerminate_()! } -void SyncProgressDialog::Pimpl::OnKeyPressed(wxKeyEvent& event) +void SyncProgressDialogImpl::OnKeyPressed(wxKeyEvent& event) { const int keyCode = event.GetKeyCode(); if (keyCode == WXK_ESCAPE) @@ -1048,7 +1072,7 @@ void SyncProgressDialog::Pimpl::OnKeyPressed(wxKeyEvent& event) } -void SyncProgressDialog::Pimpl::initNewPhase() +void SyncProgressDialogImpl::initNewPhase() { updateDialogStatus(); //evaluates "syncStat_->currentPhase()" @@ -1066,14 +1090,13 @@ void SyncProgressDialog::Pimpl::initNewPhase() //so give updateGui() a chance to set a different value m_gauge1->SetValue(0); - updateGui(false); + updateGuiInt(false); } -void SyncProgressDialog::Pimpl::notifyProgressChange() //noexcept! +void SyncProgressDialogImpl::notifyProgressChange() //noexcept! { - if (syncStat_) - { + if (syncStat_) //sync running switch (syncStat_->currentPhase()) { case ProcessCallback::PHASE_NONE: @@ -1090,7 +1113,6 @@ void SyncProgressDialog::Pimpl::notifyProgressChange() //noexcept! } break; } - } } @@ -1157,7 +1179,7 @@ std::wstring getDialogPhaseText(const Statistics* syncStat, bool paused, SyncPro } -void SyncProgressDialog::Pimpl::setExternalStatus(const wxString& status, const wxString& progress) //progress may be empty! +void SyncProgressDialogImpl::setExternalStatus(const wxString& status, const wxString& progress) //progress may be empty! { //sys tray: order "top-down": jobname, status, progress wxString newTrayInfo = jobName_.empty() ? status : L"\"" + jobName_ + L"\"\n" + status; @@ -1186,15 +1208,15 @@ void SyncProgressDialog::Pimpl::setExternalStatus(const wxString& status, const } -void SyncProgressDialog::Pimpl::updateGui(bool allowYield) +void SyncProgressDialogImpl::updateGuiInt(bool allowYield) { - assert(syncStat_); - if (!syncStat_) //no sync running!! + if (!syncStat_) //sync not running return; //wxWindowUpdateLocker dummy(this); -> not needed bool layoutChanged = false; //avoid screen flicker by calling layout() only if necessary + const long timeNow = timeElapsed.Time(); //sync status text setText(*m_staticTextStatus, replaceCpy(syncStat_->currentStatusText(), L'\n', L' ')); //no layout update for status texts! @@ -1256,20 +1278,20 @@ void SyncProgressDialog::Pimpl::updateGui(bool allowYield) //remaining time and speed assert(perf); if (perf) - { - const long timeNow = timeElapsed.Time(); if (numeric::dist(lastStatCallSpeed, timeNow) >= 500) { lastStatCallSpeed = timeNow; - perf->addSample(objectsCurrent, to<double>(dataCurrent), timeNow); + if (numeric::dist(phaseStartMs, timeNow) >= 1000) //discard stats for first second: probably messy + perf->addSample(objectsCurrent, to<double>(dataCurrent), timeNow); - //current speed -> Win 7 copy uses 1 sec update interval + //current speed -> Win 7 copy uses 1 sec update interval instead setText(*m_staticTextSpeed, perf->getBytesPerSecond(), &layoutChanged); + + //remaining time: display with relative error of 10% - based on samples taken every 0.5 sec only + //-> call more often than once per second to correctly show last few seconds countdown, but don't call too often to avoid occasional jitter + setText(*m_staticTextRemTime, perf->getRemainingTime(to<double>(dataTotal - dataCurrent)), &layoutChanged); } - //remaining time: display with relative error of 10% - based on samples taken every 0.5 sec only - accuracy of prediction grows with time - setText(*m_staticTextRemTime, perf->getRemainingTime(to<double>(dataTotal - dataCurrent)), &layoutChanged); - } } break; } @@ -1279,7 +1301,7 @@ void SyncProgressDialog::Pimpl::updateGui(bool allowYield) m_panelGraph->Refresh(); //time elapsed - const long timeElapSec = timeElapsed.Time() / 1000; + const long timeElapSec = timeNow / 1000; setText(*m_staticTextTimeElapsed, timeElapSec < 3600 ? wxTimeSpan::Seconds(timeElapSec).Format( L"%M:%S") : @@ -1336,13 +1358,13 @@ void SyncProgressDialog::Pimpl::updateGui(bool allowYield) } -std::wstring SyncProgressDialog::Pimpl::getExecWhenFinishedCommand() const +std::wstring SyncProgressDialogImpl::getExecWhenFinishedCommand() const { return m_comboBoxExecFinished->getValue(); } -void SyncProgressDialog::Pimpl::updateDialogStatus() //depends on "syncStat_, paused_, finalResult" +void SyncProgressDialogImpl::updateDialogStatus() //depends on "syncStat_, paused_, finalResult" { const wxString dlgStatusTxt = getDialogPhaseText(syncStat_, paused_, finalResult); @@ -1449,25 +1471,20 @@ void SyncProgressDialog::Pimpl::updateDialogStatus() //depends on "syncStat_, pa warn_static("osx: minimize to systray?") -void SyncProgressDialog::Pimpl::closeWindowDirectly() //this should really be called: do not call back + schedule deletion +void SyncProgressDialogImpl::closeWindowDirectly() //this should really be called: do not call back + schedule deletion { paused_ = false; //you never know? - - //ATTENTION: dialog may live a little longer, so cut callbacks! + //ATTENTION: dialog may live a little longer, so watch callbacks! //e.g. wxGTK calls OnIconize after wxWindow::Close() (better not ask why) and before physical destruction! => indirectly calls updateDialogStatus(), which reads syncStat_!!! + syncStat_ = nullptr; + abortCb_ = nullptr; + //resumeFromSystray(); -> NO, instead ~SyncProgressDialogImpl() makes sure that main dialog is shown again! - //------- change class state ------- - abortCb_ = nullptr; //avoid callback to (maybe) deleted parent process - syncStat_ = nullptr; //set *after* last call to "updateGui" - //---------------------------------- - - //resumeFromSystray(); -> NO, instead ~Pimpl() makes sure that main dialog is shown again! - - Close(); + Close(); //generate close event: do NOT destroy window unconditionally! } -void SyncProgressDialog::Pimpl::processHasFinished(SyncResult resultId, const ErrorLog& log) //essential to call this in StatusHandler derived class destructor +void SyncProgressDialogImpl::processHasFinished(SyncResult resultId, const ErrorLog& log) //essential to call this in StatusHandler derived class destructor { //at the LATEST(!) to prevent access to currentStatusHandler //enable okay and close events; may be set in this method ONLY @@ -1478,7 +1495,7 @@ void SyncProgressDialog::Pimpl::processHasFinished(SyncResult resultId, const Er //update numbers one last time (as if sync were still running) notifyProgressChange(); //make one last graph entry at the *current* time - updateGui(false); + updateGuiInt(false); switch (syncStat_->currentPhase()) //no matter if paused or not { @@ -1526,9 +1543,10 @@ void SyncProgressDialog::Pimpl::processHasFinished(SyncResult resultId, const Er } //------- change class state ------- - abortCb_ = nullptr; //avoid callback to (maybe) deleted parent process - syncStat_ = nullptr; //set *after* last call to "updateGui" finalResult = resultId; + + syncStat_ = nullptr; + abortCb_ = nullptr; //---------------------------------- updateDialogStatus(); @@ -1573,7 +1591,7 @@ void SyncProgressDialog::Pimpl::processHasFinished(SyncResult resultId, const Er //workaround wxListBox bug on Windows XP: labels are drawn on top of each other assert(m_listbookResult->GetImageList()); //make sure listbook keeps *any* image list //due to some crazy reasons that aren't worth debugging, this needs to be done directly in wxFormBuilder, - //the following call is *not* sufficient: m_listbookResult->AssignImageList(new wxImageList(180, 1)); + //the following call is *not* sufficient: m_listbookResult->AssignImageList(new wxImageList(170, 1)); //note: alternative solutions involving wxLC_LIST, wxLC_REPORT and SetWindowStyleFlag() do not work portably! wxListBook using wxLC_ICON is obviously a class invariant! //1. re-arrange graph into results listbook @@ -1595,7 +1613,6 @@ void SyncProgressDialog::Pimpl::processHasFinished(SyncResult resultId, const Er m_listbookResult->ChangeSelection(posLog); Layout(); - //play (optional) sound notification after sync has completed -> only play when waiting on results dialog, seems to be pointless otherwise! switch (finalResult) { @@ -1616,14 +1633,14 @@ void SyncProgressDialog::Pimpl::processHasFinished(SyncResult resultId, const Er } -void SyncProgressDialog::Pimpl::OnOkay(wxCommandEvent& event) +void SyncProgressDialogImpl::OnOkay(wxCommandEvent& event) { - isZombie = true; //on Fedora the iconize event is executed *before* entering SyncProgressDialog::Pimpl::OnClose()!!! + isZombie = true; //on Fedora an iconize event is issued *before* entering SyncProgressDialogImpl::OnClose()!!! Close(); //generate close event: do NOT destroy window unconditionally! } -void SyncProgressDialog::Pimpl::OnAbort(wxCommandEvent& event) +void SyncProgressDialogImpl::OnAbort(wxCommandEvent& event) { paused_ = false; updateDialogStatus(); //update status + pause button @@ -1634,27 +1651,36 @@ void SyncProgressDialog::Pimpl::OnAbort(wxCommandEvent& event) } -void SyncProgressDialog::Pimpl::OnPause(wxCommandEvent& event) +void SyncProgressDialogImpl::OnPause(wxCommandEvent& event) { paused_ = !paused_; updateDialogStatus(); //update status + pause button } -void SyncProgressDialog::Pimpl::OnClose(wxCloseEvent& event) +void SyncProgressDialogImpl::OnClose(wxCloseEvent& event) { - //this event handler may be called due to a system shutdown DURING synchronization! + //this event handler may be called *during* sync, e.g. due to a system shutdown (Windows), anytime (OS X) //try to stop sync gracefully and cross fingers: if (abortCb_) abortCb_->requestAbortion(); - //Note: we must NOT veto dialog destruction, else we will cancel system shutdown if this dialog is application main window (as in batch mode) + //Note: we must NOT veto dialog destruction, else we will cancel system shutdown if this dialog is application main window (like in batch mode) + + notifyWindowTerminate_(); //don't wait until delayed "Destroy()" finally calls destructor -> avoid calls to processHasFinished()/closeWindowDirectly() + + paused_ = false; //[!] we could be pausing here! + + //now that we notified window termination prematurely, and since processHasFinished()/closeWindowDirectly() won't be called, make sure we don't call back, too! + //e.g. the second notifyWindowTerminate_() in ~SyncProgressDialogImpl()!!! + syncStat_ = nullptr; + abortCb_ = nullptr; isZombie = true; //it "lives" until cleanup in next idle event Destroy(); } -void SyncProgressDialog::Pimpl::OnIconize(wxIconizeEvent& event) +void SyncProgressDialogImpl::OnIconize(wxIconizeEvent& event) { if (isZombie) return; //wxGTK sends iconize event *after* wxWindow::Destroy, sigh... @@ -1665,23 +1691,22 @@ void SyncProgressDialog::Pimpl::OnIconize(wxIconizeEvent& event) } -void SyncProgressDialog::Pimpl::OnResumeFromTray(wxCommandEvent& event) +void SyncProgressDialogImpl::OnResumeFromTray(wxCommandEvent& event) { resumeFromSystray(); } -void SyncProgressDialog::Pimpl::minimizeToTray() +void SyncProgressDialogImpl::minimizeToTray() { if (!trayIcon.get()) { trayIcon = make_unique<FfsTrayIcon>(); - trayIcon->Connect(FFS_REQUEST_RESUME_TRAY_EVENT, wxCommandEventHandler(SyncProgressDialog::Pimpl::OnResumeFromTray), nullptr, this); + trayIcon->Connect(FFS_REQUEST_RESUME_TRAY_EVENT, wxCommandEventHandler(SyncProgressDialogImpl::OnResumeFromTray), nullptr, this); //tray icon has shorter lifetime than this => no need to disconnect event later } - if (syncStat_) - updateGui(false); //set tray tooltip + progress: e.g. no updates while paused + updateGuiInt(false); //set tray tooltip + progress: e.g. no updates while paused Hide(); if (mainDialog) @@ -1689,7 +1714,7 @@ void SyncProgressDialog::Pimpl::minimizeToTray() } -void SyncProgressDialog::Pimpl::resumeFromSystray() +void SyncProgressDialogImpl::resumeFromSystray() { trayIcon.reset(); @@ -1708,80 +1733,19 @@ void SyncProgressDialog::Pimpl::resumeFromSystray() SetFocus(); updateDialogStatus(); //restore Windows 7 task bar status (e.g. required in pause mode) - if (syncStat_) - updateGui(false); //restore Windows 7 task bar progress (e.g. required in pause mode) + updateGuiInt(false); //restore Windows 7 task bar progress (e.g. required in pause mode) } - //######################################################################################## -//redirect to implementation -SyncProgressDialog::SyncProgressDialog(AbortCallback& abortCb, - const Statistics& syncStat, - MainDialog* parentWindow, - bool showProgress, - const wxString& jobName, - const std::wstring& execWhenFinished, - std::vector<std::wstring>& execFinishedHistory) : - pimpl(new Pimpl(abortCb, syncStat, parentWindow, jobName, execWhenFinished, execFinishedHistory)) -{ - if (showProgress) - { - pimpl->Show(); - warn_static("problem??") - //clear gui flicker, remove dummy texts: window must be visible to make this work! - pimpl->updateGui(true); //at least on OS X a real Yield() is required to flush pending GUI updates; Update() is not enough - } - else - pimpl->minimizeToTray(); -} - -SyncProgressDialog::~SyncProgressDialog() -{ - //DON'T delete pimpl! it will be deleted by the user clicking "OK/Cancel" -> (wxWindow::Destroy()) -} - -wxWindow* SyncProgressDialog::getAsWindow() -{ - return pimpl; -} - -void SyncProgressDialog::initNewPhase() -{ - pimpl->initNewPhase(); -} - -void SyncProgressDialog::notifyProgressChange() -{ - pimpl->notifyProgressChange(); -} - -void SyncProgressDialog::updateGui() -{ - pimpl->updateGui(); -} - -std::wstring SyncProgressDialog::getExecWhenFinishedCommand() const -{ - return pimpl->getExecWhenFinishedCommand(); -} - -void SyncProgressDialog::stopTimer() -{ - return pimpl->stopTimer(); -} - -void SyncProgressDialog::resumeTimer() -{ - return pimpl->resumeTimer(); -} - -void SyncProgressDialog::processHasFinished(SyncResult resultId, const ErrorLog& log) -{ - pimpl->processHasFinished(resultId, log); -} - -void SyncProgressDialog::closeWindowDirectly() //don't wait for user (silent mode) +SyncProgressDialog* createProgressDialog(zen::AbortCallback& abortCb, + const std::function<void()>& notifyWindowTerminate, //note: user closing window cannot be prevented on OS X! (And neither on Windows during system shutdown!) + const zen::Statistics& syncStat, + wxTopLevelWindow* parentWindow, //may be nullptr + bool showProgress, + const wxString& jobName, + const std::wstring& execWhenFinished, + std::vector<std::wstring>& execFinishedHistory) { - pimpl->closeWindowDirectly(); + return new SyncProgressDialogImpl(abortCb, notifyWindowTerminate, syncStat, parentWindow, showProgress, jobName, execWhenFinished, execFinishedHistory); } diff --git a/ui/progress_indicator.h b/ui/progress_indicator.h index d659af6e..0789ca80 100644 --- a/ui/progress_indicator.h +++ b/ui/progress_indicator.h @@ -7,11 +7,12 @@ #ifndef PROGRESSINDICATOR_H_INCLUDED #define PROGRESSINDICATOR_H_INCLUDED +#include <functional> #include <zen/error_log.h> -#include <zen/zstring.h> +//#include <zen/zstring.h> #include <wx/toplevel.h> #include "../lib/status_handler.h" -#include "main_dlg.h" +//#include "main_dlg.h" class CompareProgressDialog @@ -33,30 +34,10 @@ private: }; -class SyncProgressDialog -{ -public: - SyncProgressDialog(zen::AbortCallback& abortCb, - const zen::Statistics& syncStat, - MainDialog* parentWindow, //may be nullptr - bool showProgress, - const wxString& jobName, - const std::wstring& execWhenFinished, - std::vector<std::wstring>& execFinishedHistory); //changing parameter! - ~SyncProgressDialog(); - - wxWindow* getAsWindow(); //convenience! don't abuse! - - void initNewPhase(); //call after "StatusHandler::initNewPhase" - - void notifyProgressChange(); //throw (), required by graph! - void updateGui(); - - std::wstring getExecWhenFinishedCommand() const; //final value (after possible user modification) - - void stopTimer(); //halt all internal counters! - void resumeTimer(); // +//SyncStatusHandler will internally process Window messages => disable GUI controls to avoid unexpected callbacks! +struct SyncProgressDialog +{ enum SyncResult { RESULT_ABORTED, @@ -64,17 +45,40 @@ public: RESULT_FINISHED_WITH_WARNINGS, RESULT_FINISHED_WITH_SUCCESS }; - //essential to call one of these two methods in StatusUpdater derived class destructor at the LATEST(!) + //essential to call one of these two methods in StatusUpdater derived class' destructor at the LATEST(!) //to prevent access to callback to updater (e.g. request abort) - void processHasFinished(SyncResult resultId, const zen::ErrorLog& log); //sync finished, still dialog may live on - void closeWindowDirectly(); //don't wait for user + virtual void processHasFinished(SyncResult resultId, const zen::ErrorLog& log) = 0; //sync finished, still dialog may live on + virtual void closeWindowDirectly() = 0; //don't wait for user -private: - class Pimpl; - Pimpl* const pimpl; + //--------------------------------------------------------------------------- + + virtual wxWindow* getAsWindow() = 0; //convenience! don't abuse! + + virtual void initNewPhase() = 0; //call after "StatusHandler::initNewPhase" + virtual void notifyProgressChange() = 0; //throw (), required by graph! + virtual void updateGui() = 0; //update GUI and process Window messages + + virtual std::wstring getExecWhenFinishedCommand() const = 0; //final value (after possible user modification) + + virtual void stopTimer() = 0; //halt all internal timers! + virtual void resumeTimer() = 0; // + +protected: + ~SyncProgressDialog() {} }; +SyncProgressDialog* createProgressDialog(zen::AbortCallback& abortCb, + const std::function<void()>& notifyWindowTerminate, //note: user closing window cannot be prevented on OS X! (And neither on Windows during system shutdown!) + const zen::Statistics& syncStat, + wxTopLevelWindow* parentWindow, //may be nullptr + bool showProgress, + const wxString& jobName, + const std::wstring& execWhenFinished, + std::vector<std::wstring>& execFinishedHistory); //changing parameter! +//DON'T delete the pointer! it will be deleted by the user clicking "OK/Cancel"/wxWindow::Destroy() after processHasFinished() or closeWindowDirectly() + + class PauseTimers { public: diff --git a/ui/small_dlgs.cpp b/ui/small_dlgs.cpp index 3827c837..32bb3e85 100644 --- a/ui/small_dlgs.cpp +++ b/ui/small_dlgs.cpp @@ -8,6 +8,7 @@ #include <wx/wupdlock.h> #include <zen/format_unit.h> #include <zen/build_info.h> +#include <zen/tick_count.h> #include <zen/stl_tools.h> #include <wx+/choice_enum.h> #include <wx+/button.h> @@ -306,8 +307,8 @@ ReturnSmallDlg::ButtonPressed zen::showFilterDialog(wxWindow* parent, bool isGlo filter); return static_cast<ReturnSmallDlg::ButtonPressed>(filterDlg.ShowModal()); } -//######################################################################################## +//######################################################################################## class DeleteDialog : public DeleteDlgGenerated { @@ -331,6 +332,8 @@ private: const std::vector<zen::FileSystemObject*>& rowsToDeleteOnRight; bool& outRefdeleteOnBothSides; bool& outRefuseRecycleBin; + const TickVal tickCountStartup; + const std::int64_t ticksPerSec_; }; @@ -343,7 +346,9 @@ DeleteDialog::DeleteDialog(wxWindow* parent, rowsToDeleteOnLeft(rowsOnLeft), rowsToDeleteOnRight(rowsOnRight), outRefdeleteOnBothSides(deleteOnBothSides), - outRefuseRecycleBin(useRecycleBin) + outRefuseRecycleBin(useRecycleBin), + tickCountStartup(getTicks()), + ticksPerSec_(ticksPerSec()) { #ifdef FFS_WIN new zen::MouseMoveWindow(*this); //allow moving main dialog by clicking (nearly) anywhere...; ownership passed to "this" @@ -403,6 +408,12 @@ void DeleteDialog::updateGui() void DeleteDialog::OnOK(wxCommandEvent& event) { + //additional safety net, similar to Windows Explorer: time delta between DEL and ENTER must be at least 50ms to avoid accidental deletion! + const TickVal now = getTicks(); //0 on error + if (now.isValid() && tickCountStartup.isValid() && ticksPerSec_ != 0) + if (dist(tickCountStartup, now) * 1000 / ticksPerSec_ < 50) + return; + outRefuseRecycleBin = m_checkBoxUseRecycler->GetValue(); if (rowsToDeleteOnLeft != rowsToDeleteOnRight) outRefdeleteOnBothSides = m_checkBoxDeleteBothSides->GetValue(); @@ -746,7 +757,7 @@ void GlobalSettingsDlg::OnOkay(wxCommandEvent& event) void GlobalSettingsDlg::OnResetDialogs(wxCommandEvent& event) { if (showQuestionDlg(this, ReturnQuestionDlg::BUTTON_YES | ReturnQuestionDlg::BUTTON_CANCEL, - _("Make hidden warnings and dialogs visible again?")) == ReturnQuestionDlg::BUTTON_YES) + _("Make hidden warnings and dialogs visible again?"), QuestConfig().setLabelYes(_("&Restore"))) == ReturnQuestionDlg::BUTTON_YES) settings.optDialogs.resetDialogs(); } diff --git a/ui/sorting.h b/ui/sorting.h index 75b2593a..28d2f931 100644 --- a/ui/sorting.h +++ b/ui/sorting.h @@ -20,7 +20,7 @@ struct CompileTimeReminder : public FSObjectVisitor virtual void visit(const FileMapping& fileObj) {} virtual void visit(const SymLinkMapping& linkObj) {} virtual void visit(const DirMapping& dirObj ) {} -} checkDymanicCasts; //just a compile-time reminder to check dynamic casts in this file +} checkDymanicCasts; //just a compile-time reminder to manually check dynamic casts in this file when needed } inline diff --git a/ui/sync_cfg.cpp b/ui/sync_cfg.cpp index dbef3619..dc62b04a 100644 --- a/ui/sync_cfg.cpp +++ b/ui/sync_cfg.cpp @@ -235,8 +235,8 @@ SyncCfgDialog::SyncCfgDialog(wxWindow* parent, setRelativeFontSize(*m_toggleBtnCustom, 1.25); enumVersioningStyle. - add(VER_STYLE_ADD_TIMESTAMP, _("Time stamp"), _("Append a timestamp to each file name")). - add(VER_STYLE_REPLACE, _("Replace"), _("Move files and replace if existing")); + add(VER_STYLE_REPLACE, _("Replace"), _("Move files and replace if existing")). + add(VER_STYLE_ADD_TIMESTAMP, _("Time stamp"), _("Append a timestamp to each file name")); //hide controls for optional parameters if (!handleError && !execWhenFinished) //currently either both or neither are bound! diff --git a/ui/tree_view.cpp b/ui/tree_view.cpp index 07f7a942..cf866a71 100644 --- a/ui/tree_view.cpp +++ b/ui/tree_view.cpp @@ -664,6 +664,70 @@ std::unique_ptr<TreeView::Node> TreeView::getLine(size_t row) const namespace { +#ifdef _MSC_VER +#pragma warning(disable:4428) // VC wrongly issues warning C4428: universal-character-name encountered in source +#endif + +wxString getShortDisplayNameForFolderPair(const Zstring& dirLeftPf, const Zstring& dirRightPf) //post-fixed with separator +{ + assert(endsWith(dirLeftPf, FILE_NAME_SEPARATOR) || dirLeftPf .empty()); + assert(endsWith(dirRightPf, FILE_NAME_SEPARATOR) || dirRightPf.empty()); + + auto itL = dirLeftPf .end(); + auto itR = dirRightPf.end(); + + for (;;) + { + auto itLPrev = find_last(dirLeftPf .begin(), itL, FILE_NAME_SEPARATOR); + auto itRPrev = find_last(dirRightPf.begin(), itR, FILE_NAME_SEPARATOR); + + if (itLPrev == itL || + itRPrev == itR) + { + if (itLPrev == itL) + itLPrev = dirLeftPf.begin(); + else + ++itLPrev; //skip separator + if (itRPrev == itR) + itRPrev = dirRightPf.begin(); + else + ++itRPrev; + + if (equal(itLPrev, itL, itRPrev, itR)) + { + itL = itLPrev; + itR = itRPrev; + } + break; + } + + if (!equal(itLPrev, itL, itRPrev, itR)) + break; + itL = itLPrev; + itR = itRPrev; + } + + Zstring commonPostfix(itL, dirLeftPf.end()); + if (startsWith(commonPostfix, FILE_NAME_SEPARATOR)) + commonPostfix = afterFirst(commonPostfix, FILE_NAME_SEPARATOR); + if (endsWith(commonPostfix, FILE_NAME_SEPARATOR)) + commonPostfix.resize(commonPostfix.size() - 1); + + if (commonPostfix.empty()) + { + auto getLastComponent = [](const Zstring& dirPf) { return utfCvrtTo<wxString>(afterLast(beforeLast(dirPf, FILE_NAME_SEPARATOR), FILE_NAME_SEPARATOR)); }; //returns the whole string if term not found + if (dirLeftPf.empty()) + return getLastComponent(dirRightPf); + else if (dirRightPf.empty()) + return getLastComponent(dirLeftPf); + else + return getLastComponent(dirLeftPf) + L" \u2212 " + //= unicode minus + getLastComponent(dirRightPf); + } + return utfCvrtTo<wxString>(commonPostfix); +} + + const wxColour COLOR_LEVEL0(0xcc, 0xcc, 0xff); const wxColour COLOR_LEVEL1(0xcc, 0xff, 0xcc); const wxColour COLOR_LEVEL2(0xff, 0xff, 0x99); @@ -716,6 +780,32 @@ public: private: virtual size_t getRowCount() const { return treeDataView_ ? treeDataView_->linesTotal() : 0; } + virtual wxString getToolTip(size_t row, ColumnType colType) const + { + switch (static_cast<ColumnTypeNavi>(colType)) + { + case COL_TYPE_NAVI_BYTES: + case COL_TYPE_NAVI_ITEM_COUNT: + break; + + case COL_TYPE_NAVI_DIRECTORY: + if (treeDataView_) + if (std::unique_ptr<TreeView::Node> node = treeDataView_->getLine(row)) + if (const TreeView::RootNode* root = dynamic_cast<const TreeView::RootNode*>(node.get())) + { + const wxString& dirLeft = utfCvrtTo<wxString>(root->baseMap_.getBaseDirPf<LEFT_SIDE >()); + const wxString& dirRight = utfCvrtTo<wxString>(root->baseMap_.getBaseDirPf<RIGHT_SIDE>()); + if (dirLeft.empty()) + return dirRight; + else if (dirRight.empty()) + return dirLeft; + return dirLeft + L" \u2212 \n" + dirRight; //\u2212 = unicode minus + } + break; + } + return wxString(); + } + virtual wxString getValue(size_t row, ColumnType colType) const { if (treeDataView_) @@ -728,17 +818,8 @@ private: case COL_TYPE_NAVI_DIRECTORY: if (const TreeView::RootNode* root = dynamic_cast<const TreeView::RootNode*>(node.get())) - { - const wxString dirLeft = utfCvrtTo<wxString>(beforeLast(root->baseMap_.getBaseDirPf<LEFT_SIDE >(), FILE_NAME_SEPARATOR)); - const wxString dirRight = utfCvrtTo<wxString>(beforeLast(root->baseMap_.getBaseDirPf<RIGHT_SIDE>(), FILE_NAME_SEPARATOR)); - - if (dirLeft.empty()) - return dirRight; - else if (dirRight.empty()) - return dirLeft; - else - return dirLeft + L" \x2212 " + dirRight; //\x2212 = unicode minus - } + return getShortDisplayNameForFolderPair(root->baseMap_.getBaseDirPf<LEFT_SIDE >(), + root->baseMap_.getBaseDirPf<RIGHT_SIDE>()); else if (const TreeView::DirNode* dir = dynamic_cast<const TreeView::DirNode*>(node.get())) return utfCvrtTo<wxString>(dir->dirObj_.getObjShortName()); else if (dynamic_cast<const TreeView::FilesNode*>(node.get())) @@ -749,7 +830,7 @@ private: return toGuiString(node->itemCount_); } } - return wxEmptyString; + return wxString(); } virtual void renderColumnLabel(Grid& tree, wxDC& dc, const wxRect& rect, ColumnType colType, bool highlighted) |