From e3dbeda4c99e441f6bf2f77f717703f0dd4b798b Mon Sep 17 00:00:00 2001 From: mere-human <9664141+mere-human@users.noreply.github.com> Date: Sun, 4 Apr 2021 11:04:09 +0300 Subject: [PATCH] Add "Append extension" checkbox to Save As dialog Add "Append extension" checkbox to Save As dialog for replacing option "Save dialog file extension filter to *.*" in Preferences dialog. Fix #9515, close #9732 --- PowerEditor/installer/nativeLang/english.xml | 2 +- PowerEditor/src/NppIO.cpp | 14 +- .../OpenSaveFileDialog/CustomFileDialog.cpp | 242 ++++++++++++++---- .../OpenSaveFileDialog/CustomFileDialog.h | 3 + .../src/WinControls/Preference/preference.rc | 5 +- .../WinControls/Preference/preferenceDlg.cpp | 5 - .../WinControls/Preference/preference_rc.h | 1 - 7 files changed, 203 insertions(+), 69 deletions(-) diff --git a/PowerEditor/installer/nativeLang/english.xml b/PowerEditor/installer/nativeLang/english.xml index f8ff3f6d9..d371a19dc 100644 --- a/PowerEditor/installer/nativeLang/english.xml +++ b/PowerEditor/installer/nativeLang/english.xml @@ -1061,7 +1061,6 @@ You can define several column markers by using white space to separate the diffe - @@ -1422,6 +1421,7 @@ Find in all files except exe, obj && log: + diff --git a/PowerEditor/src/NppIO.cpp b/PowerEditor/src/NppIO.cpp index 391880d5c..b07fe57bb 100644 --- a/PowerEditor/src/NppIO.cpp +++ b/PowerEditor/src/NppIO.cpp @@ -1637,16 +1637,17 @@ bool Notepad_plus::fileSaveAs(BufferID id, bool isSaveCopy) LangType langType = buf->getLangType(); - int langTypeIndex = 0; - if (!((NppParameters::getInstance()).getNppGUI()._setSaveDlgExtFiltToAllTypes)) - { - langTypeIndex = setFileOpenSaveDlgFilters(fDlg, false, langType); - } + const bool defaultAllTypes = NppParameters::getInstance().getNppGUI()._setSaveDlgExtFiltToAllTypes; + const int langTypeIndex = setFileOpenSaveDlgFilters(fDlg, false, langType); fDlg.setDefFileName(buf->getFileName()); fDlg.setExtIndex(langTypeIndex + 1); // +1 for "All types" + const generic_string checkboxLabel = _nativeLangSpeaker.getLocalizedStrFromID("file-save-assign-type", + TEXT("&Append extension")); + fDlg.enableFileTypeCheckbox(checkboxLabel, !defaultAllTypes); + // Disable file autodetection before opening save dialog to prevent use-after-delete bug. NppParameters& nppParam = NppParameters::getInstance(); auto cdBefore = nppParam.getNppGUI()._fileAutoDetection; @@ -1654,6 +1655,9 @@ bool Notepad_plus::fileSaveAs(BufferID id, bool isSaveCopy) generic_string fn = fDlg.doSaveDlg(); + // Remember the selected state + const_cast(nppParam.getNppGUI())._setSaveDlgExtFiltToAllTypes = !fDlg.getFileTypeCheckboxValue(); + // Enable file autodetection again. (const_cast(nppParam.getNppGUI()))._fileAutoDetection = cdBefore; diff --git a/PowerEditor/src/WinControls/OpenSaveFileDialog/CustomFileDialog.cpp b/PowerEditor/src/WinControls/OpenSaveFileDialog/CustomFileDialog.cpp index a4a8085a0..7b4888d28 100644 --- a/PowerEditor/src/WinControls/OpenSaveFileDialog/CustomFileDialog.cpp +++ b/PowerEditor/src/WinControls/OpenSaveFileDialog/CustomFileDialog.cpp @@ -50,6 +50,9 @@ namespace // anonymous generic_string ext; }; + static const int IDC_FILE_CUSTOM_CHECKBOX = 4; + static const int IDC_FILE_TYPE_CHECKBOX = IDC_FILE_CUSTOM_CHECKBOX + 1; + // Returns a first extension from the extension specification string. // Multiple extensions are separated with ';'. // Example: input - ".c;.cpp;.h", output - ".c" @@ -151,12 +154,15 @@ namespace // anonymous generic_string getDialogFileName(IFileDialog* dialog) { generic_string fileName; - PWSTR pszFilePath = nullptr; - HRESULT hr = dialog->GetFileName(&pszFilePath); - if (SUCCEEDED(hr) && pszFilePath) + if (dialog) { - fileName = pszFilePath; - CoTaskMemFree(pszFilePath); + PWSTR pszFilePath = nullptr; + HRESULT hr = dialog->GetFileName(&pszFilePath); + if (SUCCEEDED(hr) && pszFilePath) + { + fileName = pszFilePath; + CoTaskMemFree(pszFilePath); + } } return fileName; } @@ -170,7 +176,7 @@ namespace // anonymous return {}; } - // Backups the current directory in constructor and restores it in destructor. + // Backs up the current directory in constructor and restores it in destructor. // This is needed in case dialog changes the current directory. class CurrentDirBackup { @@ -197,13 +203,15 @@ namespace // anonymous /////////////////////////////////////////////////////////////////////////////// -class FileDialogEventHandler : public IFileDialogEvents +class FileDialogEventHandler : public IFileDialogEvents, public IFileDialogControlEvents { public: - static HRESULT createInstance(const std::vector& filterSpec, REFIID riid, void **ppv) + static HRESULT createInstance(IFileDialog* dlg, const std::vector& filterSpec, + int fileIndex, int wildcardIndex, REFIID riid, void **ppv) { *ppv = nullptr; - FileDialogEventHandler *pDialogEventHandler = new (std::nothrow) FileDialogEventHandler(filterSpec); + FileDialogEventHandler* pDialogEventHandler = + new (std::nothrow) FileDialogEventHandler(dlg, filterSpec, fileIndex, wildcardIndex); HRESULT hr = pDialogEventHandler ? S_OK : E_OUTOFMEMORY; if (SUCCEEDED(hr)) { @@ -228,6 +236,13 @@ public: AddRef(); return NOERROR; } + else if (riid == __uuidof(IFileDialogControlEvents)) + { + // Increment the reference count and return the pointer. + *ppv = static_cast(this); + AddRef(); + return NOERROR; + } return E_NOINTERFACE; } @@ -260,34 +275,91 @@ public: // First launch order: 2. Buttons are added, correct window title. return S_OK; } - IFACEMETHODIMP OnSelectionChange(IFileDialog* dlg) override + IFACEMETHODIMP OnSelectionChange(IFileDialog*) override { // First launch order: 4. Main window is shown. - if (!_dialog) - initDialog(dlg); + if (shouldInitControls()) + initControls(); return S_OK; } IFACEMETHODIMP OnShareViolation(IFileDialog*, IShellItem*, FDE_SHAREVIOLATION_RESPONSE*) override { return S_OK; } - IFACEMETHODIMP OnTypeChange(IFileDialog* dlg) override + IFACEMETHODIMP OnTypeChange(IFileDialog*) override { // First launch order: 1. Inactive, window title might be wrong. - generic_string name = getDialogFileName(dlg); - if (changeExt(name, dlg)) - dlg->SetFileName(name.c_str()); + UINT dialogIndex = 0; + if (SUCCEEDED(_dialog->GetFileTypeIndex(&dialogIndex))) + { + // Enable checkbox if type was changed. + if (OnTypeChange(dialogIndex)) + _customize->SetCheckButtonState(IDC_FILE_TYPE_CHECKBOX, TRUE); + } return S_OK; } + + bool OnTypeChange(UINT dialogIndex) + { + if (dialogIndex == 0) + return false; + generic_string name = getDialogFileName(_dialog); + if (changeExt(name, dialogIndex - 1)) + return SUCCEEDED(_dialog->SetFileName(name.c_str())); + return false; + } + IFACEMETHODIMP OnOverwrite(IFileDialog*, IShellItem*, FDE_OVERWRITE_RESPONSE*) override { return S_OK; } + // IFileDialogControlEvents methods + + IFACEMETHODIMP OnItemSelected(IFileDialogCustomize*, DWORD, DWORD) override + { + return E_NOTIMPL; + } + + IFACEMETHODIMP OnButtonClicked(IFileDialogCustomize*, DWORD) override + { + return E_NOTIMPL; + } + + IFACEMETHODIMP OnCheckButtonToggled(IFileDialogCustomize*, DWORD id, BOOL bChecked) override + { + if (id == IDC_FILE_TYPE_CHECKBOX) + { + bool ok = false; + if (bChecked) + { + ok = SUCCEEDED(_dialog->SetFileTypeIndex(_lastSelectedType)); + ok &= OnTypeChange(_lastSelectedType); + } + else + { + UINT currentIndex = 0; + ok = SUCCEEDED(_dialog->GetFileTypeIndex(¤tIndex)); + if (ok && currentIndex != 0 && currentIndex != _wildcardType) + _lastSelectedType = currentIndex; + ok &= SUCCEEDED(_dialog->SetFileTypeIndex(_wildcardType)); + } + return S_OK; + } + return E_NOTIMPL; + } + + IFACEMETHODIMP OnControlActivating(IFileDialogCustomize*, DWORD) override + { + return E_NOTIMPL; + } + private: // Use createInstance() instead - FileDialogEventHandler(const std::vector& filterSpec) : _cRef(1), _filterSpec(filterSpec) + FileDialogEventHandler(IFileDialog* dlg, const std::vector& filterSpec, int fileIndex, int wildcardIndex) + : _cRef(1), _dialog(dlg), _customize(dlg), _filterSpec(filterSpec), _lastSelectedType(fileIndex + 1), + _wildcardType(wildcardIndex >= 0 ? wildcardIndex + 1 : 0) { _staticThis = this; } @@ -300,14 +372,13 @@ private: FileDialogEventHandler(FileDialogEventHandler&&) = delete; FileDialogEventHandler& operator=(FileDialogEventHandler&&) = delete; - // Inits dialog pointer and overrides window procedures for file name edit and ok button. + // Overrides window procedures for file name edit and ok button. // Call this as late as possible to ensure all the controls of the dialog are created. - void initDialog(IFileDialog * d) + void initControls() { - assert(!_dialog); - _dialog = d; _okButtonProc = nullptr; _fileNameProc = nullptr; + assert(_dialog); com_ptr pOleWnd = _dialog; if (pOleWnd) { @@ -320,16 +391,27 @@ private: } } + bool shouldInitControls() const + { + return !_okButtonProc && !_fileNameProc; + } + // Changes the name extension according to currently selected file type index. - bool changeExt(generic_string& name, IFileDialog* dlg) + bool changeExt(generic_string& name) { - UINT typeIndex = 0; - if (FAILED(dlg->GetFileTypeIndex(&typeIndex))) + if (!_dialog) + return false; + UINT dialogIndex = 0; + if (FAILED(_dialog->GetFileTypeIndex(&dialogIndex)) || dialogIndex == 0) return false; - // Index starts from 1 - if (typeIndex > 0 && typeIndex - 1 < _filterSpec.size()) + return changeExt(name, dialogIndex - 1); + } + + bool changeExt(generic_string& name, size_t extIndex) + { + if (extIndex >= 0 && extIndex < _filterSpec.size()) { - const generic_string ext = get1stExt(_filterSpec[typeIndex - 1].ext); + const generic_string ext = get1stExt(_filterSpec[extIndex].ext); if (!endsWith(ext, _T(".*"))) return replaceExt(name, ext); } @@ -365,7 +447,7 @@ private: // Name is a file path. // Add file extension if missing. if (!hasExt(fileName)) - nameChanged |= changeExt(fileName, _dialog); + nameChanged |= changeExt(fileName); } // Update the edit box text. // It will update the address if the path is a directory. @@ -487,10 +569,13 @@ private: static FileDialogEventHandler* _staticThis; long _cRef; - IFileDialog* _dialog = nullptr; + com_ptr _dialog; + com_ptr _customize; const std::vector _filterSpec; HWND _hwndNameEdit = nullptr; bool _monitorKeyboard = true; + UINT _lastSelectedType = 0; + UINT _wildcardType = 0; }; WNDPROC FileDialogEventHandler::_okButtonProc; @@ -520,6 +605,16 @@ public: NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&_dialog)); + _customize = _dialog; + + // Init the event handler. + // Pass the initially selected file type. + if (SUCCEEDED(hr)) + hr = FileDialogEventHandler::createInstance(_dialog, _filterSpec, _fileTypeIndex, _wildcardIndex, IID_PPV_ARGS(&_events)); + + // If "assign type" is OFF, then change the file type to *.* + if (_enableFileTypeCheckbox && !_fileTypeCheckboxValue && _wildcardIndex >= 0) + _fileTypeIndex = _wildcardIndex; if (SUCCEEDED(hr) && _title) hr = _dialog->SetTitle(_title); @@ -564,7 +659,10 @@ public: // The selected index should be set after the file types are set. if (SUCCEEDED(hr) && _fileTypeIndex >= 0) - hr = _dialog->SetFileTypeIndex(_fileTypeIndex + 1); // This index is 1-based + hr = _dialog->SetFileTypeIndex(_fileTypeIndex + 1); // This index is 1-based. + + if (_enableFileTypeCheckbox) + addCheckbox(IDC_FILE_TYPE_CHECKBOX, _fileTypeCheckboxLabel.c_str(), _fileTypeCheckboxValue); if (SUCCEEDED(hr)) return addControls(); @@ -595,47 +693,62 @@ public: bool addControls() { - _customize = _dialog; if (!_customize) return false; if (_checkboxLabel && _checkboxLabel[0] != '\0') { - const BOOL isChecked = FALSE; - HRESULT hr = _customize->AddCheckButton(IDC_FILE_CHECKBOX, _checkboxLabel, isChecked); - if (SUCCEEDED(hr) && !_isCheckboxActive) - { - hr = _customize->SetControlState(IDC_FILE_CHECKBOX, CDCS_INACTIVE | CDCS_VISIBLE); - return SUCCEEDED(hr); - } + return addCheckbox(IDC_FILE_CUSTOM_CHECKBOX, _checkboxLabel, false, _isCheckboxActive); } return true; } + bool addCheckbox(int id, const TCHAR* label, bool value, bool enabled = true) + { + if (!_customize) + return false; + HRESULT hr = _customize->AddCheckButton(id, label, value ? TRUE : FALSE); + if (SUCCEEDED(hr) && !enabled) + { + hr = _customize->SetControlState(id, CDCS_INACTIVE | CDCS_VISIBLE); + return SUCCEEDED(hr); + } + return SUCCEEDED(hr); + } + bool show() { - bool okPressed = false; - HRESULT hr = FileDialogEventHandler::createInstance(_filterSpec, IID_PPV_ARGS(&_events)); - if (SUCCEEDED(hr)) + assert(_dialog); + if (!_dialog) + return false; + + HRESULT hr = S_OK; + DWORD dwCookie = 0; + if (_events) { - DWORD dwCookie; hr = _dialog->Advise(_events, &dwCookie); - if (SUCCEEDED(hr)) - { - hr = _dialog->Show(_hwndOwner); - okPressed = SUCCEEDED(hr); + if (FAILED(hr)) + _events.Release(); + } - _dialog->Unadvise(dwCookie); - } + bool okPressed = false; + if (SUCCEEDED(hr)) + { + hr = _dialog->Show(_hwndOwner); + okPressed = SUCCEEDED(hr); } + + if (_events) + _dialog->Unadvise(dwCookie); + return okPressed; } - BOOL getCheckboxState() const + BOOL getCheckboxState(int id) const { if (_customize) { BOOL bChecked = FALSE; - HRESULT hr = _customize->GetCheckButtonState(IDC_FILE_CHECKBOX, &bChecked); + HRESULT hr = _customize->GetCheckButtonState(id, &bChecked); if (SUCCEEDED(hr)) return bChecked; } @@ -695,8 +808,6 @@ public: return result; } - static const int IDC_FILE_CHECKBOX = 4; - HWND _hwndOwner = nullptr; const TCHAR* _title = nullptr; const TCHAR* _defExt = nullptr; @@ -706,8 +817,12 @@ public: const TCHAR* _initialFileName = nullptr; bool _isCheckboxActive = true; std::vector _filterSpec; - int _fileTypeIndex = -1; + int _fileTypeIndex = -1; // preferred file type index + int _wildcardIndex = -1; // *.* file type index bool _hasReadonly = false; // set during the result handling + bool _enableFileTypeCheckbox = false; + bool _fileTypeCheckboxValue = false; // initial value + generic_string _fileTypeCheckboxLabel; private: com_ptr _dialog; @@ -750,6 +865,9 @@ void CustomFileDialog::setExtFilter(const TCHAR *extText, const TCHAR *exts) } } + if (newExts.find(_T("*.*")) == 0) + _impl->_wildcardIndex = static_cast(_impl->_filterSpec.size()); + _impl->_filterSpec.push_back({ extText, newExts }); } @@ -793,7 +911,7 @@ void CustomFileDialog::setExtIndex(int extTypeIndex) bool CustomFileDialog::getCheckboxState() const { - return _impl->getCheckboxState(); + return _impl->getCheckboxState(IDC_FILE_CUSTOM_CHECKBOX); } bool CustomFileDialog::isReadOnly() const @@ -801,6 +919,22 @@ bool CustomFileDialog::isReadOnly() const return _impl->_hasReadonly; } +void CustomFileDialog::enableFileTypeCheckbox(const generic_string& text, bool value) +{ + assert(!text.empty()); + if (!text.empty()) + { + _impl->_fileTypeCheckboxLabel = text; + _impl->_enableFileTypeCheckbox = true; + _impl->_fileTypeCheckboxValue = value; + } +} + +bool CustomFileDialog::getFileTypeCheckboxValue() const +{ + return _impl->getCheckboxState(IDC_FILE_TYPE_CHECKBOX); +} + generic_string CustomFileDialog::doSaveDlg() { if (!_impl->initSave()) diff --git a/PowerEditor/src/WinControls/OpenSaveFileDialog/CustomFileDialog.h b/PowerEditor/src/WinControls/OpenSaveFileDialog/CustomFileDialog.h index b04f86801..3d221a230 100644 --- a/PowerEditor/src/WinControls/OpenSaveFileDialog/CustomFileDialog.h +++ b/PowerEditor/src/WinControls/OpenSaveFileDialog/CustomFileDialog.h @@ -38,6 +38,9 @@ public: void setCheckbox(const TCHAR* text, bool isActive = true); void setExtIndex(int extTypeIndex); + void enableFileTypeCheckbox(const generic_string& text, bool value); + bool getFileTypeCheckboxValue() const; + // Empty string is not a valid file name and may signal that the dialog was canceled. generic_string doSaveDlg(); generic_string pickFolder(); diff --git a/PowerEditor/src/WinControls/Preference/preference.rc b/PowerEditor/src/WinControls/Preference/preference.rc index d4631cdbf..1de4f08c4 100644 --- a/PowerEditor/src/WinControls/Preference/preference.rc +++ b/PowerEditor/src/WinControls/Preference/preference.rc @@ -426,9 +426,8 @@ BEGIN CONTROL "Peek on document map",IDC_CHECK_ENABLEDOCPEEKONMAP,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,269,71,140,10 // "Enable Notepad++ auto-updater" should be always the 1st item, because it'll be hidden if GUP.exe is absent - CONTROL "Mute all sounds", IDC_CHECK_MUTE_SOUNDS, "Button", BS_AUTOCHECKBOX | BS_MULTILINE | WS_TABSTOP, 37, 94, 217, 10 - CONTROL "Enable Notepad++ auto-updater",IDC_CHECK_AUTOUPDATE,"Button",BS_AUTOCHECKBOX | BS_MULTILINE | WS_TABSTOP, 37, 78, 210, 10 - CONTROL "Set Save dialog file extension filter to *.*",IDC_CHECK_SAVEDLGEXTFILTALLTYPES,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,37,109,267,10 + CONTROL "Enable Notepad++ auto-updater",IDC_CHECK_AUTOUPDATE,"Button",BS_AUTOCHECKBOX | BS_MULTILINE | WS_TABSTOP, 37, 94, 210, 10 + CONTROL "Mute all sounds", IDC_CHECK_MUTE_SOUNDS, "Button", BS_AUTOCHECKBOX | BS_MULTILINE | WS_TABSTOP, 37, 109, 217, 10 CONTROL "Autodetect character encoding",IDC_CHECK_DETECTENCODING,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,37,124,217,10 CONTROL "Minimize to system tray",IDC_CHECK_MIN2SYSTRAY,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,37,139,217,10 CONTROL "Show only filename in title bar",IDC_CHECK_SHORTTITLE,"Button",BS_AUTOCHECKBOX | BS_MULTILINE | WS_TABSTOP,37,154,217,10 diff --git a/PowerEditor/src/WinControls/Preference/preferenceDlg.cpp b/PowerEditor/src/WinControls/Preference/preferenceDlg.cpp index 60d6cfa0a..92fecd15b 100644 --- a/PowerEditor/src/WinControls/Preference/preferenceDlg.cpp +++ b/PowerEditor/src/WinControls/Preference/preferenceDlg.cpp @@ -1025,7 +1025,6 @@ INT_PTR CALLBACK MiscSubDlg::run_dlgProc(UINT message, WPARAM wParam, LPARAM) ::SendDlgItemMessage(_hSelf, IDC_CHECK_MIN2SYSTRAY, BM_SETCHECK, nppGUI._isMinimizedToTray, 0); ::SendDlgItemMessage(_hSelf, IDC_CHECK_DETECTENCODING, BM_SETCHECK, nppGUI._detectEncoding, 0); ::SendDlgItemMessage(_hSelf, IDC_CHECK_AUTOUPDATE, BM_SETCHECK, nppGUI._autoUpdateOpt._doAutoUpdate, 0); - ::SendDlgItemMessage(_hSelf, IDC_CHECK_SAVEDLGEXTFILTALLTYPES, BM_SETCHECK, nppGUI._setSaveDlgExtFiltToAllTypes, 0); ::SendDlgItemMessage(_hSelf, IDC_CHECK_DIRECTWRITE_ENABLE, BM_SETCHECK, nppGUI._writeTechnologyEngine == directWriteTechnology, 0); ::SendDlgItemMessage(_hSelf, IDC_CHECK_ENABLEDOCPEEKER, BM_SETCHECK, nppGUI._isDocPeekOnTab ? BST_CHECKED : BST_UNCHECKED, 0); ::SendDlgItemMessage(_hSelf, IDC_CHECK_ENABLEDOCPEEKONMAP, BM_SETCHECK, nppGUI._isDocPeekOnMap ? BST_CHECKED : BST_UNCHECKED, 0); @@ -1103,10 +1102,6 @@ INT_PTR CALLBACK MiscSubDlg::run_dlgProc(UINT message, WPARAM wParam, LPARAM) nppGUI._autoUpdateOpt._doAutoUpdate = isCheckedOrNot(static_cast(wParam)); return TRUE; - case IDC_CHECK_SAVEDLGEXTFILTALLTYPES: - nppGUI._setSaveDlgExtFiltToAllTypes = isCheckedOrNot(static_cast(wParam)); - return TRUE; - case IDC_CHECK_MIN2SYSTRAY: nppGUI._isMinimizedToTray = isCheckedOrNot(static_cast(wParam)); return TRUE; diff --git a/PowerEditor/src/WinControls/Preference/preference_rc.h b/PowerEditor/src/WinControls/Preference/preference_rc.h index 3df46e4eb..0d4159ce3 100644 --- a/PowerEditor/src/WinControls/Preference/preference_rc.h +++ b/PowerEditor/src/WinControls/Preference/preference_rc.h @@ -198,7 +198,6 @@ #define IDC_CHECK_SMARTHILITEWHOLEWORDONLY (IDD_PREFERENCE_SUB_MISC + 38) #define IDC_CHECK_SMARTHILITEUSEFINDSETTINGS (IDD_PREFERENCE_SUB_MISC + 39) #define IDC_CHECK_SMARTHILITEANOTHERRVIEW (IDD_PREFERENCE_SUB_MISC + 40) - #define IDC_CHECK_SAVEDLGEXTFILTALLTYPES (IDD_PREFERENCE_SUB_MISC + 51) //-- xFileEditViewHistoryParameterGUI: Additional Checkbox for enabling the history for restoring the edit view per file. #define IDC_CHECK_REMEMBEREDITVIEWPERFILE (IDD_PREFERENCE_SUB_MISC + 41)