mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Add filtering for the Tag Editor
This commit is contained in:
parent
b8c52790bc
commit
2ef9ea3566
@ -53,6 +53,13 @@ class TagEditor(QDialog, Ui_TagEditor):
|
|||||||
self.connect(self.add_tag_button, SIGNAL('clicked()'), self.add_tag)
|
self.connect(self.add_tag_button, SIGNAL('clicked()'), self.add_tag)
|
||||||
self.connect(self.delete_button, SIGNAL('clicked()'), self.delete_tags)
|
self.connect(self.delete_button, SIGNAL('clicked()'), self.delete_tags)
|
||||||
self.connect(self.add_tag_input, SIGNAL('returnPressed()'), self.add_tag)
|
self.connect(self.add_tag_input, SIGNAL('returnPressed()'), self.add_tag)
|
||||||
|
# add the handlers for the filter input clear buttons
|
||||||
|
self.connect(self.available_filter_input_clear_btn, SIGNAL('clicked()'), self.clear_available_filter)
|
||||||
|
self.connect(self.applied_filter_input_clear_btn, SIGNAL('clicked()'), self.clear_applied_filter)
|
||||||
|
# add the handlers for the filter input fields
|
||||||
|
self.available_filter_input.textChanged.connect(self.filter_available)
|
||||||
|
self.applied_filter_input.textChanged.connect(self.filter_applied)
|
||||||
|
|
||||||
if islinux:
|
if islinux:
|
||||||
self.available_tags.itemDoubleClicked.connect(self.apply_tags)
|
self.available_tags.itemDoubleClicked.connect(self.apply_tags)
|
||||||
else:
|
else:
|
||||||
@ -116,6 +123,9 @@ class TagEditor(QDialog, Ui_TagEditor):
|
|||||||
item = self.available_tags.item(row)
|
item = self.available_tags.item(row)
|
||||||
self.available_tags.scrollToItem(item)
|
self.available_tags.scrollToItem(item)
|
||||||
|
|
||||||
|
# use the filter again when the applied tags were changed
|
||||||
|
self.filter_applied(self.applied_filter_input.text())
|
||||||
|
|
||||||
def unapply_tags(self, item=None):
|
def unapply_tags(self, item=None):
|
||||||
items = self.applied_tags.selectedItems() if item is None else [item]
|
items = self.applied_tags.selectedItems() if item is None else [item]
|
||||||
for item in items:
|
for item in items:
|
||||||
@ -135,6 +145,10 @@ class TagEditor(QDialog, Ui_TagEditor):
|
|||||||
for item in items:
|
for item in items:
|
||||||
self.available_tags.addItem(item)
|
self.available_tags.addItem(item)
|
||||||
|
|
||||||
|
# use the filter again when the applied tags were changed
|
||||||
|
self.filter_applied(self.applied_filter_input.text())
|
||||||
|
self.filter_available(self.available_filter_input.text())
|
||||||
|
|
||||||
def add_tag(self):
|
def add_tag(self):
|
||||||
tags = unicode(self.add_tag_input.text()).split(',')
|
tags = unicode(self.add_tag_input.text()).split(',')
|
||||||
for tag in tags:
|
for tag in tags:
|
||||||
@ -152,6 +166,39 @@ class TagEditor(QDialog, Ui_TagEditor):
|
|||||||
self.applied_tags.addItem(tag)
|
self.applied_tags.addItem(tag)
|
||||||
|
|
||||||
self.add_tag_input.setText('')
|
self.add_tag_input.setText('')
|
||||||
|
# use the filter again when the applied tags were changed
|
||||||
|
self.filter_applied(self.applied_filter_input.text())
|
||||||
|
|
||||||
|
|
||||||
|
# do the filtering on the available tags
|
||||||
|
def filter_available(self, filter_value):
|
||||||
|
itemCount = self.available_tags.count()
|
||||||
|
for i in range(0, itemCount): # on every available tag
|
||||||
|
if str(filter_value).lower() in str(self.available_tags.item(i).text()).lower():
|
||||||
|
# if contains the filter text, then show it
|
||||||
|
self.available_tags.item(i).setHidden(False)
|
||||||
|
else:
|
||||||
|
# if not then hide it from the list
|
||||||
|
self.available_tags.item(i).setHidden(True)
|
||||||
|
|
||||||
|
# do the filtering on the applied tags
|
||||||
|
def filter_applied(self, filter_value):
|
||||||
|
itemCount = self.applied_tags.count()
|
||||||
|
for i in range(0, itemCount): # on every applied tag
|
||||||
|
if str(filter_value).lower() in str(self.applied_tags.item(i).text()).lower():
|
||||||
|
# if contains the filter text, then show it
|
||||||
|
self.applied_tags.item(i).setHidden(False)
|
||||||
|
else:
|
||||||
|
# if not then hide it from the list
|
||||||
|
self.applied_tags.item(i).setHidden(True)
|
||||||
|
|
||||||
|
# clears the available tags filter input
|
||||||
|
def clear_available_filter(self):
|
||||||
|
self.available_filter_input.setText('');
|
||||||
|
|
||||||
|
# clears the applied tags filter input
|
||||||
|
def clear_applied_filter(self):
|
||||||
|
self.applied_filter_input.setText('');
|
||||||
|
|
||||||
def accept(self):
|
def accept(self):
|
||||||
self.save_state()
|
self.save_state()
|
||||||
@ -163,4 +210,3 @@ class TagEditor(QDialog, Ui_TagEditor):
|
|||||||
|
|
||||||
def save_state(self):
|
def save_state(self):
|
||||||
gprefs['tag_editor_geometry'] = bytearray(self.saveGeometry())
|
gprefs['tag_editor_geometry'] = bytearray(self.saveGeometry())
|
||||||
|
|
||||||
|
@ -6,213 +6,24 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>588</width>
|
<width>600</width>
|
||||||
<height>335</height>
|
<height>360</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
<string>Tag Editor</string>
|
<string>Tag Editor</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowIcon">
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
<iconset resource="../../../../resources/images.qrc">
|
|
||||||
<normaloff>:/images/chapters.png</normaloff>:/images/chapters.png</iconset>
|
|
||||||
</property>
|
|
||||||
<layout class="QGridLayout">
|
|
||||||
<item row="0" column="0">
|
|
||||||
<layout class="QVBoxLayout">
|
|
||||||
<item>
|
<item>
|
||||||
<layout class="QHBoxLayout">
|
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||||
|
<property name="leftMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="rightMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="label">
|
<spacer name="horizontalSpacer">
|
||||||
<property name="text">
|
|
||||||
<string>A&vailable tags</string>
|
|
||||||
</property>
|
|
||||||
<property name="buddy">
|
|
||||||
<cstring>available_tags</cstring>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<spacer>
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>40</width>
|
|
||||||
<height>20</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<layout class="QHBoxLayout">
|
|
||||||
<item>
|
|
||||||
<widget class="QToolButton" name="delete_button">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>Delete tag from database. This will unapply the tag from all books and then remove it from the database.</string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>...</string>
|
|
||||||
</property>
|
|
||||||
<property name="icon">
|
|
||||||
<iconset resource="../../../../resources/images.qrc">
|
|
||||||
<normaloff>:/images/trash.png</normaloff>:/images/trash.png</iconset>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QListWidget" name="available_tags">
|
|
||||||
<property name="alternatingRowColors">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
<property name="selectionMode">
|
|
||||||
<enum>QAbstractItemView::MultiSelection</enum>
|
|
||||||
</property>
|
|
||||||
<property name="selectionBehavior">
|
|
||||||
<enum>QAbstractItemView::SelectRows</enum>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="1">
|
|
||||||
<layout class="QVBoxLayout">
|
|
||||||
<item>
|
|
||||||
<spacer>
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Vertical</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>20</width>
|
|
||||||
<height>40</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QToolButton" name="apply_button">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>Apply tag to current book</string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>...</string>
|
|
||||||
</property>
|
|
||||||
<property name="icon">
|
|
||||||
<iconset resource="../../../../resources/images.qrc">
|
|
||||||
<normaloff>:/images/forward.png</normaloff>:/images/forward.png</iconset>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<spacer>
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Vertical</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>20</width>
|
|
||||||
<height>40</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="2">
|
|
||||||
<layout class="QVBoxLayout">
|
|
||||||
<item>
|
|
||||||
<layout class="QHBoxLayout">
|
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="label_2">
|
|
||||||
<property name="text">
|
|
||||||
<string>A&pplied tags</string>
|
|
||||||
</property>
|
|
||||||
<property name="buddy">
|
|
||||||
<cstring>applied_tags</cstring>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<spacer>
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>40</width>
|
|
||||||
<height>20</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QListWidget" name="applied_tags">
|
|
||||||
<property name="alternatingRowColors">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
<property name="selectionMode">
|
|
||||||
<enum>QAbstractItemView::MultiSelection</enum>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="3">
|
|
||||||
<layout class="QVBoxLayout">
|
|
||||||
<item>
|
|
||||||
<spacer>
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Vertical</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>20</width>
|
|
||||||
<height>40</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QToolButton" name="unapply_button">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>Unapply (remove) tag from current book</string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>...</string>
|
|
||||||
</property>
|
|
||||||
<property name="icon">
|
|
||||||
<iconset resource="../../../../resources/images.qrc">
|
|
||||||
<normaloff>:/images/list_remove.png</normaloff>:/images/list_remove.png</iconset>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<spacer>
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Vertical</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>20</width>
|
|
||||||
<height>40</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="0" colspan="4">
|
|
||||||
<layout class="QHBoxLayout">
|
|
||||||
<item>
|
|
||||||
<spacer>
|
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Horizontal</enum>
|
<enum>Qt::Horizontal</enum>
|
||||||
</property>
|
</property>
|
||||||
@ -256,7 +67,7 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<spacer>
|
<spacer name="horizontalSpacer_2">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Horizontal</enum>
|
<enum>Qt::Horizontal</enum>
|
||||||
</property>
|
</property>
|
||||||
@ -270,11 +81,175 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="0" colspan="4">
|
<item>
|
||||||
<widget class="QDialogButtonBox" name="buttonBox">
|
<widget class="Line" name="line_2">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Horizontal</enum>
|
<enum>Qt::Horizontal</enum>
|
||||||
</property>
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||||
|
<property name="bottomMargin">
|
||||||
|
<number>8</number>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="text">
|
||||||
|
<string>A&vailable tags</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>available_tags</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="available_filter_input">
|
||||||
|
<property name="placeholderText">
|
||||||
|
<string>filter the available tags</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="available_filter_input_clear_btn">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Clear the available tags filter input field</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string></string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../../../../resources/images.qrc">
|
||||||
|
<normaloff>:/images/clear_left.png</normaloff>:/images/clear_left.png</iconset>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QListWidget" name="available_tags">
|
||||||
|
<property name="alternatingRowColors">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="selectionMode">
|
||||||
|
<enum>QAbstractItemView::MultiSelection</enum>
|
||||||
|
</property>
|
||||||
|
<property name="selectionBehavior">
|
||||||
|
<enum>QAbstractItemView::SelectRows</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="delete_button">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Delete tag from database. This will unapply the tag from all books and then remove it from the database.</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Delete tag</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../../../../resources/images.qrc">
|
||||||
|
<normaloff>:/images/trash.png</normaloff>:/images/trash.png</iconset>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="apply_button">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Apply tag to current book</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../../../../resources/images.qrc">
|
||||||
|
<normaloff>:/images/forward.png</normaloff>:/images/forward.png</iconset>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_2">
|
||||||
|
<property name="text">
|
||||||
|
<string>A&pplied tags</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>applied_tags</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="applied_filter_input">
|
||||||
|
<property name="placeholderText">
|
||||||
|
<string>filter the applied tags</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="applied_filter_input_clear_btn">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Clear the applied tags filter input field</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string></string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../../../../resources/images.qrc">
|
||||||
|
<normaloff>:/images/clear_left.png</normaloff>:/images/clear_left.png</iconset>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QListWidget" name="applied_tags">
|
||||||
|
<property name="alternatingRowColors">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="selectionMode">
|
||||||
|
<enum>QAbstractItemView::MultiSelection</enum>
|
||||||
|
</property>
|
||||||
|
<property name="selectionBehavior">
|
||||||
|
<enum>QAbstractItemView::SelectRows</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="unapply_button">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Unapply (remove) tag from current book</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Unapply tag</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../../../../resources/images.qrc">
|
||||||
|
<normaloff>:/images/list_remove.png</normaloff>:/images/list_remove.png</iconset>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="Line" name="line">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
<property name="standardButtons">
|
<property name="standardButtons">
|
||||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||||
</property>
|
</property>
|
||||||
@ -282,6 +257,7 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
|
<layoutdefault spacing="6" margin="11"/>
|
||||||
<customwidgets>
|
<customwidgets>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
<class>EnLineEdit</class>
|
<class>EnLineEdit</class>
|
||||||
@ -289,9 +265,8 @@
|
|||||||
<header>widgets.h</header>
|
<header>widgets.h</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
</customwidgets>
|
</customwidgets>
|
||||||
<resources>
|
<resources/>
|
||||||
<include location="../../../../resources/images.qrc"/>
|
|
||||||
</resources>
|
|
||||||
<connections>
|
<connections>
|
||||||
<connection>
|
<connection>
|
||||||
<sender>buttonBox</sender>
|
<sender>buttonBox</sender>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user