Patch level : 10.0 220
Files correlati : ve0.exe lv3.exe co0.exe Ricompilazione Demo : [ ] Commento : Implementata la possibilità di colorare le righe dei documenti definendo nella maschera (file maschera.ini) delle regle come quelle sottoesposte ; RuleName(1) = Merce Cara Rule(1) = PREZZO>1000|N BgCol(1) = 0,192,192 FgCol(1) = 0,0,0 RuleName(2) = Merce Carissima Rule(2) = PREZZO>10000|N BgCol(2) = 0,0,192 FgCol(2) = 0,0,0 RuleName(3) = Riga vuota Rule(3) = STR(TIPORIGA!="05")&&(QTA==0)&&(PREZZO==0)|N BgCol(3) = 128,0,128 FgCol(3) = 255,255,255 I nnumeri di riga cominciano da 1, nella regola !N significa formula numerica, i colori sono definiti come r,g,b. Si possono utilizzare campi della maschera di riga (#id camop) DELLATESTATA (#-campo) della testata documento (33.nomecampo) della riga documeno (34.nome campo) e DIRTY che significa riga modificata git-svn-id: svn://10.65.10.50/trunk@18205 c028cbd2-c16b-5b4b-a496-9718f37d4682
This commit is contained in:
parent
1f94a1de94
commit
06339d0074
@ -21,7 +21,7 @@ class TSelect_color_mask : public TVariable_mask
|
|||||||
|
|
||||||
HIDDEN TMask * get_mask(int, TMask&);
|
HIDDEN TMask * get_mask(int, TMask&);
|
||||||
TColor_object_props * row(int r) const { return (TColor_object_props *) _color_defs.objptr(r);}
|
TColor_object_props * row(int r) const { return (TColor_object_props *) _color_defs.objptr(r);}
|
||||||
int items() { return _color_defs.items();}
|
int items() const { return _color_defs.items();}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void init(const char * mask_name, const char * para = NULL);
|
void init(const char * mask_name, const char * para = NULL);
|
||||||
|
@ -184,8 +184,8 @@ bool TSelect_color_mask::color_handler(TMask_field& f, KEY k)
|
|||||||
if (use_back)
|
if (use_back)
|
||||||
p.set_back(col);
|
p.set_back(col);
|
||||||
else
|
else
|
||||||
p.set_fore(col);
|
p.set_fore(col);
|
||||||
cm._sheet_mask->update();
|
xvt_dwin_invalidate_rect(f.parent(), NULL);
|
||||||
m.set_focus();
|
m.set_focus();
|
||||||
s.set_back_and_fore_color(p.get_back(), p.get_fore(), cur);
|
s.set_back_and_fore_color(p.get_back(), p.get_fore(), cur);
|
||||||
s.force_update();
|
s.force_update();
|
||||||
@ -238,13 +238,13 @@ int TSelect_color_mask::add_color_def(const char * key, const char * prompt, COL
|
|||||||
TString tmp(_mask_name) ; tmp << "_" << _paragraph;
|
TString tmp(_mask_name) ; tmp << "_" << _paragraph;
|
||||||
TConfig conf(CONFIG_GUI, tmp);
|
TConfig conf(CONFIG_GUI, tmp);
|
||||||
|
|
||||||
tmp = p->get_key();
|
tmp = p->get_key();
|
||||||
if (conf.exist(tmp))
|
if (conf.exist(tmp))
|
||||||
p->set_fore(conf.get_color(tmp));
|
p->set_fore(conf.get_color(tmp));
|
||||||
|
|
||||||
tmp << "_Bg";
|
tmp << "_Bg";
|
||||||
if (conf.exist(tmp))
|
if (conf.exist(tmp))
|
||||||
p->set_back(conf.get_color(tmp));
|
p->set_back(conf.get_color(tmp));
|
||||||
|
|
||||||
s.set_back_and_fore_color(p->get_back(), p->get_fore(), row);
|
s.set_back_and_fore_color(p->get_back(), p->get_fore(), row);
|
||||||
return row;
|
return row;
|
||||||
|
@ -91,7 +91,7 @@ void TRow_property::set(int col, COLOR back, COLOR fore)
|
|||||||
|
|
||||||
void TRow_property::get(int col, COLOR & back, COLOR & fore) const
|
void TRow_property::get(int col, COLOR & back, COLOR & fore) const
|
||||||
{
|
{
|
||||||
if (col > 0) // Lascia stare la colonna del numero riga
|
if (col >= 0) // Lascia stare la colonna del numero riga
|
||||||
{
|
{
|
||||||
back = _back;
|
back = _back;
|
||||||
fore = _fore;
|
fore = _fore;
|
||||||
|
@ -53,6 +53,7 @@ bool TMotore_application::save_and_new() const
|
|||||||
void TMotore_application::init_query_mode( TMask& m )
|
void TMotore_application::init_query_mode( TMask& m )
|
||||||
{
|
{
|
||||||
disable_menu_item(M_FILE_PRINT);
|
disable_menu_item(M_FILE_PRINT);
|
||||||
|
disable_menu_item(MENU_ITEM(1));
|
||||||
|
|
||||||
TEdit_field& cn = _msk->efield(F_CODNUM);
|
TEdit_field& cn = _msk->efield(F_CODNUM);
|
||||||
|
|
||||||
@ -81,6 +82,7 @@ void TMotore_application::init_insert_mode( TMask& m )
|
|||||||
TDocumento_mask& dm = (TDocumento_mask&)m;
|
TDocumento_mask& dm = (TDocumento_mask&)m;
|
||||||
TString4 codnum(m.get(F_CODNUM));
|
TString4 codnum(m.get(F_CODNUM));
|
||||||
|
|
||||||
|
enable_menu_item(MENU_ITEM(1));
|
||||||
dm.reset_father_rows();
|
dm.reset_father_rows();
|
||||||
if (codnum.empty())
|
if (codnum.empty())
|
||||||
{
|
{
|
||||||
@ -171,6 +173,7 @@ void TMotore_application::init_modify_mode( TMask& m )
|
|||||||
const bool enable_print = edit_mask().doc().tipo().printable();
|
const bool enable_print = edit_mask().doc().tipo().printable();
|
||||||
const TString4 provv = m.get(F_PROVV);
|
const TString4 provv = m.get(F_PROVV);
|
||||||
enable_menu_item(M_FILE_PRINT, provv == "D" && enable_print);
|
enable_menu_item(M_FILE_PRINT, provv == "D" && enable_print);
|
||||||
|
enable_menu_item(MENU_ITEM(1));
|
||||||
if (provv[0] == 'P')
|
if (provv[0] == 'P')
|
||||||
{
|
{
|
||||||
m.disable(DLG_PRINT);
|
m.disable(DLG_PRINT);
|
||||||
@ -334,10 +337,7 @@ int TMotore_application::read( TMask& m )
|
|||||||
_tipodoc = m.get(F_TIPODOC);
|
_tipodoc = m.get(F_TIPODOC);
|
||||||
|
|
||||||
mask.doc2mask();
|
mask.doc2mask();
|
||||||
COLOR high_back_color = _sel_color->get_back_color(_link_pos);
|
mask.highlight();
|
||||||
COLOR high_color = _sel_color->get_fore_color(_link_pos);
|
|
||||||
|
|
||||||
mask.highlight(high_back_color, high_color);
|
|
||||||
|
|
||||||
return NOERR;
|
return NOERR;
|
||||||
}
|
}
|
||||||
@ -347,13 +347,8 @@ bool TMotore_application::menu(MENU_TAG mt)
|
|||||||
bool ok = true;
|
bool ok = true;
|
||||||
if (mt == MENU_ITEM(1))
|
if (mt == MENU_ITEM(1))
|
||||||
{
|
{
|
||||||
if (_sel_color->run() != K_ESC && _docmsk != NULL)
|
if (_docmsk != NULL)
|
||||||
{
|
_docmsk->sel_color();
|
||||||
COLOR high_back_color = _sel_color->get_back_color(_link_pos);
|
|
||||||
COLOR high_color = _sel_color->get_fore_color(_link_pos);
|
|
||||||
_docmsk->highlight(high_back_color, high_color);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
ok = TRelation_application::menu(mt);
|
ok = TRelation_application::menu(mt);
|
||||||
@ -498,9 +493,7 @@ bool TMotore_application::user_create( )
|
|||||||
_rel->lfile().set_curr(new TDocumento);
|
_rel->lfile().set_curr(new TDocumento);
|
||||||
|
|
||||||
_msk = new TMask("ve0100a");
|
_msk = new TMask("ve0100a");
|
||||||
_sel_color = new TSelect_color_mask("ve0100a", "0");
|
|
||||||
set_search_field(F_NDOC);
|
set_search_field(F_NDOC);
|
||||||
_link_pos =_sel_color->add_color_def("LINKED", "Righe collegate a documento", COLOR_YELLOW, COLOR_BLACK);
|
|
||||||
|
|
||||||
_msk->set_handler( F_ANNO, TDocumento_mask::anno_handler );
|
_msk->set_handler( F_ANNO, TDocumento_mask::anno_handler );
|
||||||
_msk->set_handler( F_CODNUM, TDocumento_mask::num_handler );
|
_msk->set_handler( F_CODNUM, TDocumento_mask::num_handler );
|
||||||
@ -598,9 +591,6 @@ bool TMotore_application::user_destroy( )
|
|||||||
if ( _msk != NULL )
|
if ( _msk != NULL )
|
||||||
delete _msk;
|
delete _msk;
|
||||||
|
|
||||||
if ( _sel_color != NULL )
|
|
||||||
delete _sel_color;
|
|
||||||
|
|
||||||
// Distruggo la relazione
|
// Distruggo la relazione
|
||||||
delete _rel;
|
delete _rel;
|
||||||
// delete _condv;
|
// delete _condv;
|
||||||
|
@ -30,8 +30,6 @@ class TMotore_application : public TRelation_application
|
|||||||
TString4 _tipodoc;
|
TString4 _tipodoc;
|
||||||
TString80 __last_key;
|
TString80 __last_key;
|
||||||
|
|
||||||
TSelect_color_mask * _sel_color;
|
|
||||||
int _link_pos;
|
|
||||||
bool _print_directly;
|
bool _print_directly;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
32
ve/velib.h
32
ve/velib.h
@ -839,6 +839,25 @@ public:
|
|||||||
virtual ~TStati() { }
|
virtual ~TStati() { }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class TRow_colors : public TObject
|
||||||
|
{
|
||||||
|
COLOR _back;
|
||||||
|
COLOR _fore;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
|
||||||
|
virtual TObject* dup() const { return new TRow_colors(*this); }
|
||||||
|
virtual TRow_colors & copy(const TRow_colors & c) { set(c.back(), c.fore()); return *this; }
|
||||||
|
|
||||||
|
public:
|
||||||
|
void set(COLOR back, COLOR fore) { _back = back; _fore = fore;}
|
||||||
|
COLOR back() const { return _back; }
|
||||||
|
COLOR fore() const { return _fore; }
|
||||||
|
|
||||||
|
TRow_colors(COLOR back, COLOR fore) { set(back, fore);}
|
||||||
|
~TRow_colors() {}
|
||||||
|
};
|
||||||
|
|
||||||
class TDocumento_mask : public TVariable_mask // velib06
|
class TDocumento_mask : public TVariable_mask // velib06
|
||||||
{
|
{
|
||||||
int _progs_page; // pagina in cui cominciano i progressivi
|
int _progs_page; // pagina in cui cominciano i progressivi
|
||||||
@ -867,6 +886,10 @@ class TDocumento_mask : public TVariable_mask // velib06
|
|||||||
short _cms_end_sh;
|
short _cms_end_sh;
|
||||||
TString _codcms;
|
TString _codcms;
|
||||||
TString _codcms_sh;
|
TString _codcms_sh;
|
||||||
|
TArray _colors;
|
||||||
|
TArray _def_colors;
|
||||||
|
TArray _color_rules;
|
||||||
|
TString_array _color_rule_names;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual void next_page(int p);
|
virtual void next_page(int p);
|
||||||
@ -940,7 +963,8 @@ public:
|
|||||||
static bool ragsoc_search_handler( TMask_field& f, KEY key );
|
static bool ragsoc_search_handler( TMask_field& f, KEY key );
|
||||||
static bool datadocrif_handler(TMask_field& f, KEY key);
|
static bool datadocrif_handler(TMask_field& f, KEY key);
|
||||||
|
|
||||||
void highlight(COLOR high_back_color, COLOR high_color);
|
void highlight_row(int row, bool dirty = true, bool update = true);
|
||||||
|
void highlight();
|
||||||
|
|
||||||
bool is_calculated_page(int p) const { return _calculated_pages[p]; }
|
bool is_calculated_page(int p) const { return _calculated_pages[p]; }
|
||||||
TSmart_card* smartcard() const { return _smartcard;}
|
TSmart_card* smartcard() const { return _smartcard;}
|
||||||
@ -958,6 +982,12 @@ public:
|
|||||||
TString & codcms_sh() { return _codcms_sh;}
|
TString & codcms_sh() { return _codcms_sh;}
|
||||||
const TString & codcms_sh() const { return _codcms_sh;}
|
const TString & codcms_sh() const { return _codcms_sh;}
|
||||||
|
|
||||||
|
TArray & colors() { return _colors; }
|
||||||
|
TArray & color_rules() { return _color_rules; }
|
||||||
|
TString_array & color_rule_names() { return _color_rule_names; }
|
||||||
|
void sel_color();
|
||||||
|
|
||||||
|
|
||||||
TDocumento_mask(const char* tipodoc);
|
TDocumento_mask(const char* tipodoc);
|
||||||
virtual ~TDocumento_mask();
|
virtual ~TDocumento_mask();
|
||||||
};
|
};
|
||||||
|
176
ve/velib06.cpp
176
ve/velib06.cpp
@ -266,6 +266,57 @@ TDocumento_mask::TDocumento_mask(const char* td)
|
|||||||
if (f->dlg() > BASE_PIEDE)
|
if (f->dlg() > BASE_PIEDE)
|
||||||
_calculated_pages.set(f->page());
|
_calculated_pages.set(f->page());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TFilename pn;
|
||||||
|
d.tipo().profile_name(pn);
|
||||||
|
TConfig prof(pn, "Colors");
|
||||||
|
|
||||||
|
prof.write_protect(true);
|
||||||
|
color_rule_names().add("Righe collegate a documento", 0);
|
||||||
|
color_rules().add(new TExpression(""), 0);
|
||||||
|
colors().add(new TRow_colors(prof.get_color("BgCol", NULL, 0, COLOR_YELLOW), prof.get_color("fore", NULL, 0, COLOR_BLACK)));
|
||||||
|
|
||||||
|
for(int i = 1; ; i++)
|
||||||
|
{
|
||||||
|
const TString & name = prof.get("RuleName", NULL, i);
|
||||||
|
|
||||||
|
if (name.full())
|
||||||
|
{
|
||||||
|
color_rule_names().add(name, i);
|
||||||
|
TToken_string se(prof.get("Rule", NULL, i));
|
||||||
|
const bool numeric = se.get_char(1) == 'N';
|
||||||
|
const TString expr(se.get(0));
|
||||||
|
|
||||||
|
color_rules().add(new TExpression(expr, numeric ? _numexpr : _strexpr), i);
|
||||||
|
TToken_string sb(prof.get("BgCol", NULL, i), ',');
|
||||||
|
COLOR back = RGB2COLOR(sb.get_int(0), sb.get_int(1), sb.get_int(2));
|
||||||
|
TToken_string sf(prof.get_color("FgCol", NULL, i), ',');
|
||||||
|
COLOR fore = RGB2COLOR(sf.get_int(0), sf.get_int(1), sf.get_int(2));
|
||||||
|
colors().add(new TRow_colors(back, fore));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_def_colors = colors();
|
||||||
|
TString tmp(doc().tipo().mask_name()); tmp << "_0"; tmp.lower();
|
||||||
|
TConfig conf(CONFIG_GUI, tmp);
|
||||||
|
const int items = colors().items();
|
||||||
|
|
||||||
|
conf.write_protect(true);
|
||||||
|
for (int i = 0; i < items; i++)
|
||||||
|
{
|
||||||
|
tmp = color_rule_names().row(i);
|
||||||
|
TRow_colors * c = (TRow_colors *) colors().objptr(i);
|
||||||
|
if (c != NULL)
|
||||||
|
{
|
||||||
|
COLOR fore = conf.get_color(tmp);
|
||||||
|
tmp << "_Bg";
|
||||||
|
COLOR back = conf.get_color(tmp);
|
||||||
|
|
||||||
|
if (back != 0L || fore != 0L)
|
||||||
|
c->set(back, fore);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TDocumento_mask::~TDocumento_mask()
|
TDocumento_mask::~TDocumento_mask()
|
||||||
@ -1326,6 +1377,7 @@ bool TDocumento_mask::ss_notify( TSheet_field& ss, int r, KEY key )
|
|||||||
|
|
||||||
if (m.is_calculated_page(m.curr_page()))
|
if (m.is_calculated_page(m.curr_page()))
|
||||||
m.update_progs();
|
m.update_progs();
|
||||||
|
m.highlight_row(r);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case K_CTRL + K_ENTER: // inizio modifica
|
case K_CTRL + K_ENTER: // inizio modifica
|
||||||
@ -1406,6 +1458,7 @@ bool TDocumento_mask::ss_notify( TSheet_field& ss, int r, KEY key )
|
|||||||
riga.autoload(ss);
|
riga.autoload(ss);
|
||||||
ss.check_row(r);
|
ss.check_row(r);
|
||||||
t.set_defaults(ss, r + 1);
|
t.set_defaults(ss, r + 1);
|
||||||
|
m.highlight_row(r);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case K_TAB: // ingresso nella riga
|
case K_TAB: // ingresso nella riga
|
||||||
@ -1617,21 +1670,91 @@ bool TDocumento_mask::clifo_handler( TMask_field& f, KEY key )
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TDocumento_mask::highlight(COLOR high_back_color, COLOR high_color)
|
void TDocumento_mask::highlight_row(int row, bool dirty, bool update)
|
||||||
|
{
|
||||||
|
TSheet_field & sf = sfield(F_SHEET);
|
||||||
|
const int items = sf.items();
|
||||||
|
COLOR back = NORMAL_BACK_COLOR;
|
||||||
|
COLOR fore = NORMAL_COLOR;
|
||||||
|
const int last_rule = colors().items() - 1;
|
||||||
|
bool on = false;
|
||||||
|
const TRiga_documento & rigadoc = doc()[row + 1];
|
||||||
|
TRow_colors * c = NULL;
|
||||||
|
|
||||||
|
for (int rule = last_rule; !on && rule >= 0; rule--)
|
||||||
|
{
|
||||||
|
c = (TRow_colors *) colors().objptr(rule);
|
||||||
|
|
||||||
|
if (c != NULL)
|
||||||
|
{
|
||||||
|
if (rule == 0)
|
||||||
|
on = rigadoc.linked();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TExpression * expr = (TExpression *) color_rules().objptr(rule);
|
||||||
|
|
||||||
|
if (expr != NULL)
|
||||||
|
{
|
||||||
|
// SET VARS
|
||||||
|
const int vars = expr->numvar();
|
||||||
|
|
||||||
|
for (int i = 0; i < vars; i++)
|
||||||
|
{
|
||||||
|
const TString name(expr->varname(i));
|
||||||
|
|
||||||
|
if (name.starts_with("DIRTY"))
|
||||||
|
expr->setvar(i, dirty ? UNO : ZERO);
|
||||||
|
else
|
||||||
|
if (name.starts_with("#"))
|
||||||
|
{
|
||||||
|
const short id = atoi(name.mid(1));
|
||||||
|
|
||||||
|
if (id > 0)
|
||||||
|
{
|
||||||
|
TToken_string & sheet_row = sf.row(row);
|
||||||
|
|
||||||
|
expr->setvar(i, sheet_row.get(sf.cid2index(id)));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
expr->setvar(i, get(-id));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
if (name.starts_with("33.") || name.starts_with("DOC."))
|
||||||
|
{
|
||||||
|
const TString & fldname = name.after('.');
|
||||||
|
|
||||||
|
expr->setvar(i, doc().get(fldname));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const TString & fldname = name.find('.') > 0 ? name.after('.') : name;
|
||||||
|
|
||||||
|
expr->setvar(i, rigadoc.get(fldname));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
on = expr->as_bool();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (on && c != NULL)
|
||||||
|
{
|
||||||
|
back = c->back();
|
||||||
|
fore = c->fore();
|
||||||
|
}
|
||||||
|
sf.set_back_and_fore_color(back, fore, row);
|
||||||
|
if (update)
|
||||||
|
sf.force_update(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TDocumento_mask::highlight()
|
||||||
{
|
{
|
||||||
TSheet_field & sf = sfield(F_SHEET);
|
TSheet_field & sf = sfield(F_SHEET);
|
||||||
const int items = sf.items();
|
const int items = sf.items();
|
||||||
|
|
||||||
for (int i = 0 ; i < items; i++)
|
for (int i = 0 ; i < items; i++)
|
||||||
{
|
highlight_row(i, false, false);
|
||||||
const bool on = doc()[i + 1].linked();
|
sf.force_update();
|
||||||
|
|
||||||
COLOR back = on ? high_back_color : -1;
|
|
||||||
COLOR fore = on ? high_color : -1;
|
|
||||||
|
|
||||||
sf.set_back_and_fore_color(back, fore, i);
|
|
||||||
sf.force_update(i);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////
|
||||||
@ -3554,3 +3677,36 @@ bool TDocumento_mask::confirm_handler( TMask_field& f, KEY key )
|
|||||||
}
|
}
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TDocumento_mask::sel_color()
|
||||||
|
{
|
||||||
|
TString tmp(doc().tipo().mask_name()); tmp.lower();
|
||||||
|
TSelect_color_mask sel(tmp, "0");
|
||||||
|
|
||||||
|
const int items = color_rule_names().items();
|
||||||
|
|
||||||
|
for(int i = 0; i < items; i++)
|
||||||
|
{
|
||||||
|
const TString & name = color_rule_names().row(i);
|
||||||
|
TRow_colors * cd = (TRow_colors *) _def_colors.objptr(i);
|
||||||
|
|
||||||
|
if (cd != NULL)
|
||||||
|
sel.add_color_def(name, name, cd->back(), cd->fore());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sel.run() != K_ESC)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < items; i++)
|
||||||
|
{
|
||||||
|
COLOR back;
|
||||||
|
COLOR fore;
|
||||||
|
TRow_colors * c = (TRow_colors *) colors().objptr(i);
|
||||||
|
const TString & name = color_rule_names().row(i);
|
||||||
|
|
||||||
|
sel.get_color_def(name, back, fore);
|
||||||
|
if (c != NULL)
|
||||||
|
c->set(back, fore);
|
||||||
|
}
|
||||||
|
highlight();
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user