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
pull/9778/head
mere-human 4 years ago committed by Don HO
parent af56713079
commit e3dbeda4c9

@ -1061,7 +1061,6 @@ You can define several column markers by using white space to separate the diffe
<Item id="6325" name="Scroll to the last line after update"/>
<Item id="6322" name="Session file ext.:"/>
<Item id="6323" name="Enable Notepad++ auto-updater"/>
<Item id="6351" name="Set Save dialog file extension filter to *.*"/>
<Item id="6324" name="Document Switcher (Ctrl+TAB)"/>
<Item id="6331" name="Show only filename in title bar"/>
<Item id="6334" name="Autodetect character encoding"/>
@ -1422,6 +1421,7 @@ Find in all files except exe, obj &amp;&amp; log:
<find-regex-zero-length-match value="zero length match" />
<session-save-folder-as-workspace value="Save Folder as Workspace" />
<tab-untitled-string value="new " />
<file-save-assign-type value="&amp;Append extension" />
</MiscStrings>
</Native-Langue>
</NotepadPlus>

@ -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<NppGUI&>(nppParam.getNppGUI())._setSaveDlgExtFiltToAllTypes = !fDlg.getFileTypeCheckboxValue();
// Enable file autodetection again.
(const_cast<NppGUI &>(nppParam.getNppGUI()))._fileAutoDetection = cdBefore;

@ -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<Filter>& filterSpec, REFIID riid, void **ppv)
static HRESULT createInstance(IFileDialog* dlg, const std::vector<Filter>& 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<IFileDialogControlEvents*>(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(&currentIndex));
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<Filter>& filterSpec) : _cRef(1), _filterSpec(filterSpec)
FileDialogEventHandler(IFileDialog* dlg, const std::vector<Filter>& 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<IOleWindow> 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<IFileDialog> _dialog;
com_ptr<IFileDialogCustomize> _customize;
const std::vector<Filter> _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<Filter> _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<IFileDialog> _dialog;
@ -750,6 +865,9 @@ void CustomFileDialog::setExtFilter(const TCHAR *extText, const TCHAR *exts)
}
}
if (newExts.find(_T("*.*")) == 0)
_impl->_wildcardIndex = static_cast<int>(_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())

@ -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();

@ -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

@ -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<int32_t>(wParam));
return TRUE;
case IDC_CHECK_SAVEDLGEXTFILTALLTYPES:
nppGUI._setSaveDlgExtFiltToAllTypes = isCheckedOrNot(static_cast<int32_t>(wParam));
return TRUE;
case IDC_CHECK_MIN2SYSTRAY:
nppGUI._isMinimizedToTray = isCheckedOrNot(static_cast<int32_t>(wParam));
return TRUE;

@ -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)

Loading…
Cancel
Save