New feature: Opens file in its default viewer
This feature has the same effect as double-clicking this file in Windows Explorer. Closes #3577, fixes #3576
This commit is contained in:
parent
2593d64fbb
commit
fbbe9344d9
@ -91,7 +91,7 @@
|
|||||||
<Item id="41017" name="Rename..."/>
|
<Item id="41017" name="Rename..."/>
|
||||||
<Item id="41021" name="Restore Recent Closed File"/>
|
<Item id="41021" name="Restore Recent Closed File"/>
|
||||||
<Item id="41022" name="Open Folder as &Workspace"/>
|
<Item id="41022" name="Open Folder as &Workspace"/>
|
||||||
|
<Item id="41023" name="Open in Default Viewer"/>
|
||||||
<Item id="42001" name="Cu&t"/>
|
<Item id="42001" name="Cu&t"/>
|
||||||
<Item id="42002" name="&Copy"/>
|
<Item id="42002" name="&Copy"/>
|
||||||
<Item id="42003" name="&Undo"/>
|
<Item id="42003" name="&Undo"/>
|
||||||
@ -334,6 +334,7 @@
|
|||||||
<Item CMID="18" name="Close All to the Right"/>
|
<Item CMID="18" name="Close All to the Right"/>
|
||||||
<Item CMID="19" name="Open Containing Folder in Explorer"/>
|
<Item CMID="19" name="Open Containing Folder in Explorer"/>
|
||||||
<Item CMID="20" name="Open Containing Folder in cmd"/>
|
<Item CMID="20" name="Open Containing Folder in cmd"/>
|
||||||
|
<Item CMID="21" name="Open in Default Viewer"/>
|
||||||
</TabBar>
|
</TabBar>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
||||||
|
@ -95,6 +95,7 @@
|
|||||||
<Item id="41017" name="Renommer..."/>
|
<Item id="41017" name="Renommer..."/>
|
||||||
<Item id="41021" name="Ouvrir le dernier fichier fermé"/>
|
<Item id="41021" name="Ouvrir le dernier fichier fermé"/>
|
||||||
<Item id="41022" name="Ouvrir Dossier en tant qu'Espace de travail"/>
|
<Item id="41022" name="Ouvrir Dossier en tant qu'Espace de travail"/>
|
||||||
|
<Item id="41023" name="Ouvrir dans l'application par défaut"/>
|
||||||
<Item id="42001" name="Cou&per"/>
|
<Item id="42001" name="Cou&per"/>
|
||||||
<Item id="42002" name="&Copier"/>
|
<Item id="42002" name="&Copier"/>
|
||||||
<Item id="42003" name="&Annuler"/>
|
<Item id="42003" name="&Annuler"/>
|
||||||
@ -331,6 +332,7 @@
|
|||||||
<Item CMID="18" name="Fermer les onglets sur la droite"/>
|
<Item CMID="18" name="Fermer les onglets sur la droite"/>
|
||||||
<Item CMID="19" name="Ouvrir le répertoire du fichier en cours dans Explorer"/>
|
<Item CMID="19" name="Ouvrir le répertoire du fichier en cours dans Explorer"/>
|
||||||
<Item CMID="20" name="Ouvrir le répertoire du fichier en cours dans cmd"/>
|
<Item CMID="20" name="Ouvrir le répertoire du fichier en cours dans cmd"/>
|
||||||
|
<Item CMID="21" name="Ouvrir dans l'application par défaut"/>
|
||||||
</TabBar>
|
</TabBar>
|
||||||
</Menu>
|
</Menu>
|
||||||
<Dialog>
|
<Dialog>
|
||||||
|
@ -1101,3 +1101,28 @@ bool isCertificateValidated(const generic_string & fullFilePath, const generic_s
|
|||||||
|
|
||||||
return isOK;
|
return isOK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isAssoCommandExisting(LPCTSTR FullPathName)
|
||||||
|
{
|
||||||
|
bool isAssoCommandExisting = false;
|
||||||
|
|
||||||
|
bool isFileExisting = PathFileExists(FullPathName) != FALSE;
|
||||||
|
|
||||||
|
if (isFileExisting)
|
||||||
|
{
|
||||||
|
PTSTR ext = PathFindExtension(FullPathName);
|
||||||
|
|
||||||
|
HRESULT hres;
|
||||||
|
wchar_t buffer[MAX_PATH] = TEXT("");
|
||||||
|
DWORD bufferLen = MAX_PATH;
|
||||||
|
|
||||||
|
// check if association exist
|
||||||
|
hres = AssocQueryString(ASSOCF_VERIFY|ASSOCF_INIT_IGNOREUNKNOWN, ASSOCSTR_COMMAND, ext, NULL, buffer, &bufferLen);
|
||||||
|
|
||||||
|
isAssoCommandExisting = (hres == S_OK) // check if association exist and no error
|
||||||
|
&& (buffer != NULL) // check if buffer is not NULL
|
||||||
|
&& (wcsstr(buffer, TEXT("notepad++.exe")) == NULL); // check association with notepad++
|
||||||
|
|
||||||
|
}
|
||||||
|
return isAssoCommandExisting;
|
||||||
|
}
|
@ -187,3 +187,4 @@ generic_string uintToString(unsigned int val);
|
|||||||
HWND CreateToolTip(int toolID, HWND hDlg, HINSTANCE hInst, const PTSTR pszText);
|
HWND CreateToolTip(int toolID, HWND hDlg, HINSTANCE hInst, const PTSTR pszText);
|
||||||
|
|
||||||
bool isCertificateValidated(const generic_string & fullFilePath, const generic_string & subjectName2check);
|
bool isCertificateValidated(const generic_string & fullFilePath, const generic_string & subjectName2check);
|
||||||
|
bool isAssoCommandExisting(LPCTSTR FullPathName);
|
||||||
|
@ -49,6 +49,7 @@
|
|||||||
#include "functionListPanel.h"
|
#include "functionListPanel.h"
|
||||||
#include "fileBrowser.h"
|
#include "fileBrowser.h"
|
||||||
#include "LongRunningOperation.h"
|
#include "LongRunningOperation.h"
|
||||||
|
#include "Common.h"
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
|
|
||||||
@ -1943,6 +1944,8 @@ void Notepad_plus::checkDocState()
|
|||||||
enableCommand(IDM_FILE_OPEN_FOLDER, isFileExisting, MENU);
|
enableCommand(IDM_FILE_OPEN_FOLDER, isFileExisting, MENU);
|
||||||
enableCommand(IDM_FILE_RELOAD, isFileExisting, MENU);
|
enableCommand(IDM_FILE_RELOAD, isFileExisting, MENU);
|
||||||
|
|
||||||
|
enableCommand(IDM_FILE_OPEN_DEFAULT_VIEWER, isAssoCommandExisting(curBuf->getFullPathName()), MENU);
|
||||||
|
|
||||||
enableConvertMenuItems(curBuf->getEolFormat());
|
enableConvertMenuItems(curBuf->getEolFormat());
|
||||||
checkUnicodeMenuItems();
|
checkUnicodeMenuItems();
|
||||||
checkLangsMenu(-1);
|
checkLangsMenu(-1);
|
||||||
|
@ -222,6 +222,7 @@ BEGIN
|
|||||||
MENUITEM "Explorer", IDM_FILE_OPEN_FOLDER
|
MENUITEM "Explorer", IDM_FILE_OPEN_FOLDER
|
||||||
MENUITEM "cmd", IDM_FILE_OPEN_CMD
|
MENUITEM "cmd", IDM_FILE_OPEN_CMD
|
||||||
END
|
END
|
||||||
|
MENUITEM "Open in Default Viewer" IDM_FILE_OPEN_DEFAULT_VIEWER
|
||||||
MENUITEM "Open Folder as Workspace...", IDM_FILE_OPENFOLDERASWORSPACE
|
MENUITEM "Open Folder as Workspace...", IDM_FILE_OPENFOLDERASWORSPACE
|
||||||
MENUITEM "Re&load from Disk", IDM_FILE_RELOAD
|
MENUITEM "Re&load from Disk", IDM_FILE_RELOAD
|
||||||
MENUITEM "&Save", IDM_FILE_SAVE
|
MENUITEM "&Save", IDM_FILE_SAVE
|
||||||
|
@ -105,6 +105,34 @@ void Notepad_plus::command(int id)
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case IDM_FILE_OPEN_DEFAULT_VIEWER:
|
||||||
|
{
|
||||||
|
// Opens file in its default viewer.
|
||||||
|
// Has the same effect as double–clicking this file in Windows Explorer.
|
||||||
|
BufferID buf = _pEditView->getCurrentBufferID();
|
||||||
|
HINSTANCE res = ::ShellExecute(NULL, TEXT("open"), buf->getFullPathName(), NULL, NULL, SW_SHOW);
|
||||||
|
|
||||||
|
// As per MSDN (https://msdn.microsoft.com/en-us/library/windows/desktop/bb762153(v=vs.85).aspx)
|
||||||
|
// If the function succeeds, it returns a value greater than 32.
|
||||||
|
// If the function fails, it returns an error value that indicates the cause of the failure.
|
||||||
|
int retResult = reinterpret_cast<int>(res);
|
||||||
|
if (retResult <= 32)
|
||||||
|
{
|
||||||
|
generic_string errorMsg;
|
||||||
|
errorMsg += GetLastErrorAsString(retResult);
|
||||||
|
errorMsg += TEXT("An attempt was made to execute the below command.");
|
||||||
|
errorMsg += TEXT("\n----------------------------------------------------------");
|
||||||
|
errorMsg += TEXT("\nCommand: ");
|
||||||
|
errorMsg += buf->getFullPathName();
|
||||||
|
errorMsg += TEXT("\nError Code: ");
|
||||||
|
errorMsg += intToString(retResult);
|
||||||
|
errorMsg += TEXT("\n----------------------------------------------------------");
|
||||||
|
|
||||||
|
::MessageBox(_pPublicInterface->getHSelf(), errorMsg.c_str(), TEXT("ShellExecute - ERROR"), MB_ICONINFORMATION | MB_APPLMODAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case IDM_FILE_OPENFOLDERASWORSPACE:
|
case IDM_FILE_OPENFOLDERASWORSPACE:
|
||||||
{
|
{
|
||||||
generic_string folderPath = folderBrowser(_pPublicInterface->getHSelf(), TEXT("Select a folder to add in Folder as Workspace panel"));
|
generic_string folderPath = folderBrowser(_pPublicInterface->getHSelf(), TEXT("Select a folder to add in Folder as Workspace panel"));
|
||||||
|
@ -32,6 +32,7 @@
|
|||||||
#include "VerticalFileSwitcher.h"
|
#include "VerticalFileSwitcher.h"
|
||||||
#include "ProjectPanel.h"
|
#include "ProjectPanel.h"
|
||||||
#include "documentMap.h"
|
#include "documentMap.h"
|
||||||
|
#include "Common.h"
|
||||||
#include <stack>
|
#include <stack>
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
@ -514,7 +515,8 @@ BOOL Notepad_plus::notify(SCNotification *notification)
|
|||||||
|
|
||||||
if (!_tabPopupMenu.isCreated())
|
if (!_tabPopupMenu.isCreated())
|
||||||
{
|
{
|
||||||
std::vector<MenuItemUnit> itemUnitArray;
|
// IMPORTANT: If list below is modified, you have to change the value of tabContextMenuItemPos[] in localization.cpp file
|
||||||
|
std::vector<MenuItemUnit> itemUnitArray;
|
||||||
itemUnitArray.push_back(MenuItemUnit(IDM_FILE_CLOSE, TEXT("Close")));
|
itemUnitArray.push_back(MenuItemUnit(IDM_FILE_CLOSE, TEXT("Close")));
|
||||||
itemUnitArray.push_back(MenuItemUnit(IDM_FILE_CLOSEALL_BUT_CURRENT, TEXT("Close All BUT This")));
|
itemUnitArray.push_back(MenuItemUnit(IDM_FILE_CLOSEALL_BUT_CURRENT, TEXT("Close All BUT This")));
|
||||||
itemUnitArray.push_back(MenuItemUnit(IDM_FILE_CLOSEALL_TOLEFT, TEXT("Close All to the Left")));
|
itemUnitArray.push_back(MenuItemUnit(IDM_FILE_CLOSEALL_TOLEFT, TEXT("Close All to the Left")));
|
||||||
@ -529,6 +531,8 @@ BOOL Notepad_plus::notify(SCNotification *notification)
|
|||||||
itemUnitArray.push_back(MenuItemUnit(IDM_FILE_OPEN_FOLDER, TEXT("Open Containing Folder in Explorer")));
|
itemUnitArray.push_back(MenuItemUnit(IDM_FILE_OPEN_FOLDER, TEXT("Open Containing Folder in Explorer")));
|
||||||
itemUnitArray.push_back(MenuItemUnit(IDM_FILE_OPEN_CMD, TEXT("Open Containing Folder in cmd")));
|
itemUnitArray.push_back(MenuItemUnit(IDM_FILE_OPEN_CMD, TEXT("Open Containing Folder in cmd")));
|
||||||
itemUnitArray.push_back(MenuItemUnit(0, NULL));
|
itemUnitArray.push_back(MenuItemUnit(0, NULL));
|
||||||
|
itemUnitArray.push_back(MenuItemUnit(IDM_FILE_OPEN_DEFAULT_VIEWER, TEXT("Open in Default Viewer")));
|
||||||
|
itemUnitArray.push_back(MenuItemUnit(0, NULL));
|
||||||
itemUnitArray.push_back(MenuItemUnit(IDM_EDIT_SETREADONLY, TEXT("Read-Only")));
|
itemUnitArray.push_back(MenuItemUnit(IDM_EDIT_SETREADONLY, TEXT("Read-Only")));
|
||||||
itemUnitArray.push_back(MenuItemUnit(IDM_EDIT_CLEARREADONLY, TEXT("Clear Read-Only Flag")));
|
itemUnitArray.push_back(MenuItemUnit(IDM_EDIT_CLEARREADONLY, TEXT("Clear Read-Only Flag")));
|
||||||
itemUnitArray.push_back(MenuItemUnit(0, NULL));
|
itemUnitArray.push_back(MenuItemUnit(0, NULL));
|
||||||
@ -540,6 +544,7 @@ BOOL Notepad_plus::notify(SCNotification *notification)
|
|||||||
itemUnitArray.push_back(MenuItemUnit(IDM_VIEW_CLONE_TO_ANOTHER_VIEW, TEXT("Clone to Other View")));
|
itemUnitArray.push_back(MenuItemUnit(IDM_VIEW_CLONE_TO_ANOTHER_VIEW, TEXT("Clone to Other View")));
|
||||||
itemUnitArray.push_back(MenuItemUnit(IDM_VIEW_GOTO_NEW_INSTANCE, TEXT("Move to New Instance")));
|
itemUnitArray.push_back(MenuItemUnit(IDM_VIEW_GOTO_NEW_INSTANCE, TEXT("Move to New Instance")));
|
||||||
itemUnitArray.push_back(MenuItemUnit(IDM_VIEW_LOAD_IN_NEW_INSTANCE, TEXT("Open in New Instance")));
|
itemUnitArray.push_back(MenuItemUnit(IDM_VIEW_LOAD_IN_NEW_INSTANCE, TEXT("Open in New Instance")));
|
||||||
|
// IMPORTANT: If list above is modified, you have to change the value of tabContextMenuItemPos[] in localization.cpp file
|
||||||
|
|
||||||
_tabPopupMenu.create(_pPublicInterface->getHSelf(), itemUnitArray);
|
_tabPopupMenu.create(_pPublicInterface->getHSelf(), itemUnitArray);
|
||||||
_nativeLangSpeaker.changeLangTabContextMenu(_tabPopupMenu.getMenuHandle());
|
_nativeLangSpeaker.changeLangTabContextMenu(_tabPopupMenu.getMenuHandle());
|
||||||
@ -560,6 +565,8 @@ BOOL Notepad_plus::notify(SCNotification *notification)
|
|||||||
_tabPopupMenu.enableItem(IDM_FILE_DELETE, isFileExisting);
|
_tabPopupMenu.enableItem(IDM_FILE_DELETE, isFileExisting);
|
||||||
_tabPopupMenu.enableItem(IDM_FILE_RENAME, isFileExisting);
|
_tabPopupMenu.enableItem(IDM_FILE_RENAME, isFileExisting);
|
||||||
|
|
||||||
|
_tabPopupMenu.enableItem(IDM_FILE_OPEN_DEFAULT_VIEWER, isAssoCommandExisting(buf->getFullPathName()));
|
||||||
|
|
||||||
bool isDirty = buf->isDirty();
|
bool isDirty = buf->isDirty();
|
||||||
bool isUntitled = buf->isUntitled();
|
bool isUntitled = buf->isUntitled();
|
||||||
_tabPopupMenu.enableItem(IDM_VIEW_GOTO_NEW_INSTANCE, !(isDirty||isUntitled));
|
_tabPopupMenu.enableItem(IDM_VIEW_GOTO_NEW_INSTANCE, !(isDirty||isUntitled));
|
||||||
|
@ -81,6 +81,7 @@ static const WinMenuKeyDefinition winKeyDefs[] =
|
|||||||
{ VK_O, IDM_FILE_OPEN, true, false, false, nullptr },
|
{ VK_O, IDM_FILE_OPEN, true, false, false, nullptr },
|
||||||
{ VK_NULL, IDM_FILE_OPEN_FOLDER, false, false, false, nullptr },
|
{ VK_NULL, IDM_FILE_OPEN_FOLDER, false, false, false, nullptr },
|
||||||
{ VK_NULL, IDM_FILE_OPEN_CMD, false, false, false, nullptr },
|
{ VK_NULL, IDM_FILE_OPEN_CMD, false, false, false, nullptr },
|
||||||
|
{ VK_NULL, IDM_FILE_OPEN_DEFAULT_VIEWER, false, false, false, nullptr },
|
||||||
{ VK_NULL, IDM_FILE_OPENFOLDERASWORSPACE, false, false, false, nullptr },
|
{ VK_NULL, IDM_FILE_OPENFOLDERASWORSPACE, false, false, false, nullptr },
|
||||||
{ VK_NULL, IDM_FILE_RELOAD, false, false, false, nullptr },
|
{ VK_NULL, IDM_FILE_RELOAD, false, false, false, nullptr },
|
||||||
{ VK_S, IDM_FILE_SAVE, true, false, false, nullptr },
|
{ VK_S, IDM_FILE_SAVE, true, false, false, nullptr },
|
||||||
|
@ -364,27 +364,32 @@ void NativeLangSpeaker::changeMenuLang(HMENU menuHandle, generic_string & plugin
|
|||||||
|
|
||||||
static const int tabContextMenuItemPos[] =
|
static const int tabContextMenuItemPos[] =
|
||||||
{
|
{
|
||||||
|
// +-------------- The order in tab menu (NppNotification.cpp : if (!_tabPopupMenu.isCreated())
|
||||||
|
// |
|
||||||
|
// | +------ Number in english.xml (<language>.xml) : <TabBar>
|
||||||
|
// | |
|
||||||
0, // 0 : Close
|
0, // 0 : Close
|
||||||
1, // 1 : Close ALL BUT This
|
1, // 1 : Close ALL BUT This
|
||||||
4, // 2 : Save
|
4, // 2 : Save
|
||||||
5, // 3 : Save As
|
5, // 3 : Save As
|
||||||
9, // 4 : Print
|
9, // 4 : Print
|
||||||
21, // 5 : Move to Other View
|
23, // 5 : Move to Other View
|
||||||
22, // 6 : Clone to Other View
|
24, // 6 : Clone to Other View
|
||||||
17, // 7 : Full File Path to Clipboard
|
19, // 7 : Full File Path to Clipboard
|
||||||
18, // 8 : Filename to Clipboard
|
20, // 8 : Filename to Clipboard
|
||||||
19, // 9 : Current Dir. Path to Clipboard
|
21, // 9 : Current Dir. Path to Clipboard
|
||||||
6, // 10: Rename
|
6, // 10: Rename
|
||||||
7, // 11: Move to Recycle Bin
|
7, // 11: Move to Recycle Bin
|
||||||
14, // 12: Read-Only
|
16, // 12: Read-Only
|
||||||
15, // 13: Clear Read-Only Flag
|
17, // 13: Clear Read-Only Flag
|
||||||
23, // 14: Move to New Instance
|
25, // 14: Move to New Instance
|
||||||
24, // 15: Open to New Instance
|
26, // 15: Open to New Instance
|
||||||
8, // 16: Reload
|
8, // 16: Reload
|
||||||
2, // 17: Close ALL to the Left
|
2, // 17: Close ALL to the Left
|
||||||
3, // 18: Close ALL to the Right
|
3, // 18: Close ALL to the Right
|
||||||
11, // 19: Open Containing Folder in Explorer
|
11, // 19: Open Containing Folder in Explorer
|
||||||
12, // 20: Open Containing Folder in cmd
|
12, // 20: Open Containing Folder in cmd
|
||||||
|
14, // 21: Open in Default Viewer
|
||||||
-1 //-------End
|
-1 //-------End
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -56,10 +56,11 @@
|
|||||||
#define IDM_FILE_OPEN_CMD (IDM_FILE + 20)
|
#define IDM_FILE_OPEN_CMD (IDM_FILE + 20)
|
||||||
#define IDM_FILE_RESTORELASTCLOSEDFILE (IDM_FILE + 21)
|
#define IDM_FILE_RESTORELASTCLOSEDFILE (IDM_FILE + 21)
|
||||||
#define IDM_FILE_OPENFOLDERASWORSPACE (IDM_FILE + 22)
|
#define IDM_FILE_OPENFOLDERASWORSPACE (IDM_FILE + 22)
|
||||||
|
#define IDM_FILE_OPEN_DEFAULT_VIEWER (IDM_FILE + 23)
|
||||||
// IMPORTANT: If list above is modified, you have to change the following values:
|
// IMPORTANT: If list above is modified, you have to change the following values:
|
||||||
|
|
||||||
// To be updated if new menu item(s) is (are) added in menu "File"
|
// To be updated if new menu item(s) is (are) added in menu "File"
|
||||||
#define IDM_FILEMENU_LASTONE IDM_FILE_OPENFOLDERASWORSPACE
|
#define IDM_FILEMENU_LASTONE IDM_FILE_OPEN_DEFAULT_VIEWER
|
||||||
|
|
||||||
// 0 based position of command "Exit" including the bars in the file menu
|
// 0 based position of command "Exit" including the bars in the file menu
|
||||||
// and without counting "Recent files history" items
|
// and without counting "Recent files history" items
|
||||||
@ -68,25 +69,26 @@
|
|||||||
// 1 Open...
|
// 1 Open...
|
||||||
// 2 Open Containing Folder
|
// 2 Open Containing Folder
|
||||||
// 3 Open Folder as Workspace
|
// 3 Open Folder as Workspace
|
||||||
// 4 Reload from Disk
|
// 4 Open in Default Viewer
|
||||||
// 5 Save
|
// 5 Reload from Disk
|
||||||
// 6 Save As...
|
// 6 Save
|
||||||
// 7 Save a Copy As...
|
// 7 Save As...
|
||||||
// 8 Save All
|
// 8 Save a Copy As...
|
||||||
// 9 Rename...
|
// 9 Save All
|
||||||
//10 Close
|
//10 Rename...
|
||||||
//11 Close All
|
//11 Close
|
||||||
//12 Close More
|
//12 Close All
|
||||||
//13 Move to Recycle Bin
|
//13 Close More
|
||||||
//14 --------
|
//14 Move to Recycle Bin
|
||||||
//15 Load Session...
|
//15 --------
|
||||||
//16 Save Session...
|
//16 Load Session...
|
||||||
//17 --------
|
//17 Save Session...
|
||||||
//18 Print...
|
//18 --------
|
||||||
//19 Print Now
|
//19 Print...
|
||||||
//20 --------
|
//20 Print Now
|
||||||
//21 Exit
|
//21 --------
|
||||||
#define IDM_FILEMENU_EXISTCMDPOSITION 21
|
//22 Exit
|
||||||
|
#define IDM_FILEMENU_EXISTCMDPOSITION 22
|
||||||
|
|
||||||
|
|
||||||
#define IDM_EDIT (IDM + 2000)
|
#define IDM_EDIT (IDM + 2000)
|
||||||
|
Loading…
Reference in New Issue
Block a user