mirror of
https://invent.kde.org/network/falkon.git
synced 2024-12-20 02:36:34 +01:00
VerticalTabs: Show pinned tabs in horizontal list above normal tabs
This commit is contained in:
parent
801c1845b5
commit
072d3c28ee
@ -6,6 +6,9 @@ set( VerticalTabs_SRCS
|
||||
tabtreeview.cpp
|
||||
tabtreedelegate.cpp
|
||||
loadinganimator.cpp
|
||||
tabfiltermodel.cpp
|
||||
tablistview.cpp
|
||||
tablistdelegate.cpp
|
||||
)
|
||||
|
||||
set( VerticalTabs_UIS
|
||||
|
49
src/plugins/VerticalTabs/tabfiltermodel.cpp
Normal file
49
src/plugins/VerticalTabs/tabfiltermodel.cpp
Normal file
@ -0,0 +1,49 @@
|
||||
/* ============================================================
|
||||
* VerticalTabs plugin for Falkon
|
||||
* Copyright (C) 2018 David Rosca <nowrep@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
* ============================================================ */
|
||||
#include "tabfiltermodel.h"
|
||||
|
||||
#include "tabmodel.h"
|
||||
|
||||
TabFilterModel::TabFilterModel(QObject *parent)
|
||||
: QSortFilterProxyModel(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void TabFilterModel::resetFilter()
|
||||
{
|
||||
m_mode = NoFilter;
|
||||
invalidateFilter();
|
||||
}
|
||||
|
||||
void TabFilterModel::setFilterPinnedTabs(bool filter)
|
||||
{
|
||||
m_mode = FilterPinnedTabs;
|
||||
m_filterPinnedTabs = filter;
|
||||
invalidateFilter();
|
||||
}
|
||||
|
||||
bool TabFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
|
||||
{
|
||||
if (m_mode == NoFilter) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
|
||||
return index.data(TabModel::PinnedRole).toBool() != m_filterPinnedTabs;
|
||||
}
|
||||
|
43
src/plugins/VerticalTabs/tabfiltermodel.h
Normal file
43
src/plugins/VerticalTabs/tabfiltermodel.h
Normal file
@ -0,0 +1,43 @@
|
||||
/* ============================================================
|
||||
* VerticalTabs plugin for Falkon
|
||||
* Copyright (C) 2018 David Rosca <nowrep@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
* ============================================================ */
|
||||
#pragma once
|
||||
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
class TabFilterModel : public QSortFilterProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TabFilterModel(QObject *parent = nullptr);
|
||||
|
||||
void resetFilter();
|
||||
|
||||
void setFilterPinnedTabs(bool pinned);
|
||||
|
||||
private:
|
||||
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const;
|
||||
|
||||
enum Mode {
|
||||
NoFilter,
|
||||
FilterPinnedTabs
|
||||
};
|
||||
|
||||
Mode m_mode = NoFilter;
|
||||
bool m_filterPinnedTabs = false;
|
||||
};
|
104
src/plugins/VerticalTabs/tablistdelegate.cpp
Normal file
104
src/plugins/VerticalTabs/tablistdelegate.cpp
Normal file
@ -0,0 +1,104 @@
|
||||
/* ============================================================
|
||||
* VerticalTabs plugin for Falkon
|
||||
* Copyright (C) 2018 David Rosca <nowrep@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
* ============================================================ */
|
||||
#include "tablistdelegate.h"
|
||||
#include "tablistview.h"
|
||||
#include "loadinganimator.h"
|
||||
|
||||
#include "tabmodel.h"
|
||||
#include "tabicon.h"
|
||||
|
||||
#include <QPainter>
|
||||
|
||||
TabListDelegate::TabListDelegate(TabListView *view)
|
||||
: QStyledItemDelegate()
|
||||
, m_view(view)
|
||||
{
|
||||
m_padding = qMax(5, m_view->style()->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1);
|
||||
|
||||
m_loadingAnimator = new LoadingAnimator(this);
|
||||
connect(m_loadingAnimator, &LoadingAnimator::updateIndex, this, [this](const QModelIndex &index) {
|
||||
m_view->update(index);
|
||||
});
|
||||
}
|
||||
|
||||
QRect TabListDelegate::audioButtonRect(const QModelIndex &index) const
|
||||
{
|
||||
if (!index.data(TabModel::AudioPlayingRole).toBool() && !index.data(TabModel::AudioMutedRole).toBool()) {
|
||||
return QRect();
|
||||
}
|
||||
const QRect rect = m_view->visualRect(index);
|
||||
const int center = rect.height() / 2 + rect.top();
|
||||
return QRect(rect.right() - 16, center - 16 / 2, 16, 16);
|
||||
}
|
||||
|
||||
void TabListDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
|
||||
{
|
||||
const QWidget *w = option.widget;
|
||||
const QStyle *style = w ? w->style() : m_view->style();
|
||||
|
||||
QStyleOptionViewItem opt = option;
|
||||
opt.state.setFlag(QStyle::State_Active, true);
|
||||
opt.state.setFlag(QStyle::State_HasFocus, false);
|
||||
opt.state.setFlag(QStyle::State_Selected, index.data(TabModel::CurrentTabRole).toBool());
|
||||
|
||||
const int height = opt.rect.height();
|
||||
const int center = height / 2 + opt.rect.top();
|
||||
|
||||
const QIcon::Mode iconMode = opt.state & QStyle::State_Selected ? QIcon::Selected : QIcon::Normal;
|
||||
|
||||
// Draw background
|
||||
style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, w);
|
||||
|
||||
// Draw icon
|
||||
const int iconSize = 16;
|
||||
const int iconYPos = center - (iconSize / 2);
|
||||
QRect iconRect(opt.rect.left() + (opt.rect.width() - iconSize) / 2, iconYPos, iconSize, iconSize);
|
||||
QPixmap pixmap;
|
||||
if (index.data(TabModel::LoadingRole).toBool()) {
|
||||
pixmap = m_loadingAnimator->pixmap(index);
|
||||
} else {
|
||||
pixmap = index.data(Qt::DecorationRole).value<QIcon>().pixmap(iconSize, iconMode);
|
||||
}
|
||||
painter->drawPixmap(iconRect, pixmap);
|
||||
|
||||
// Draw audio icon
|
||||
const bool audioMuted = index.data(TabModel::AudioMutedRole).toBool();
|
||||
const bool audioPlaying = index.data(TabModel::AudioPlayingRole).toBool();
|
||||
if (audioMuted || audioPlaying) {
|
||||
QSize audioSize(16, 16);
|
||||
QPoint pos(opt.rect.right() - audioSize.width(), center - audioSize.height() / 2);
|
||||
QRect audioRect(pos, audioSize);
|
||||
|
||||
QColor c = opt.palette.color(QPalette::Window);
|
||||
c.setAlpha(180);
|
||||
painter->setPen(c);
|
||||
painter->setBrush(c);
|
||||
painter->drawEllipse(audioRect);
|
||||
|
||||
painter->drawPixmap(audioRect, audioMuted ? TabIcon::data()->audioMutedPixmap : TabIcon::data()->audioPlayingPixmap);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
QSize TabListDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
|
||||
{
|
||||
QStyleOptionViewItem opt(option);
|
||||
initStyleOption(&opt, index);
|
||||
|
||||
return QSize(m_padding * 4 + 16, m_padding * 2 + opt.fontMetrics.height());
|
||||
}
|
39
src/plugins/VerticalTabs/tablistdelegate.h
Normal file
39
src/plugins/VerticalTabs/tablistdelegate.h
Normal file
@ -0,0 +1,39 @@
|
||||
/* ============================================================
|
||||
* VerticalTabs plugin for Falkon
|
||||
* Copyright (C) 2018 David Rosca <nowrep@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
* ============================================================ */
|
||||
#pragma once
|
||||
|
||||
#include <QStyledItemDelegate>
|
||||
|
||||
class TabListView;
|
||||
class LoadingAnimator;
|
||||
|
||||
class TabListDelegate : public QStyledItemDelegate
|
||||
{
|
||||
public:
|
||||
explicit TabListDelegate(TabListView *view);
|
||||
|
||||
QRect audioButtonRect(const QModelIndex &index) const;
|
||||
|
||||
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
||||
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
||||
|
||||
private:
|
||||
TabListView *m_view;
|
||||
LoadingAnimator *m_loadingAnimator;
|
||||
int m_padding;
|
||||
};
|
143
src/plugins/VerticalTabs/tablistview.cpp
Normal file
143
src/plugins/VerticalTabs/tablistview.cpp
Normal file
@ -0,0 +1,143 @@
|
||||
/* ============================================================
|
||||
* VerticalTabs plugin for Falkon
|
||||
* Copyright (C) 2018 David Rosca <nowrep@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
* ============================================================ */
|
||||
#include "tablistview.h"
|
||||
#include "tablistdelegate.h"
|
||||
#include "loadinganimator.h"
|
||||
|
||||
#include "tabmodel.h"
|
||||
#include "webtab.h"
|
||||
#include "tabcontextmenu.h"
|
||||
|
||||
#include <QToolTip>
|
||||
#include <QHoverEvent>
|
||||
|
||||
TabListView::TabListView(QWidget *parent)
|
||||
: QListView(parent)
|
||||
{
|
||||
setDragEnabled(true);
|
||||
setAcceptDrops(true);
|
||||
setUniformItemSizes(true);
|
||||
setDropIndicatorShown(true);
|
||||
setMouseTracking(true);
|
||||
setFlow(QListView::LeftToRight);
|
||||
setFrameShape(QFrame::NoFrame);
|
||||
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
|
||||
|
||||
m_delegate = new TabListDelegate(this);
|
||||
setItemDelegate(m_delegate);
|
||||
|
||||
setFixedHeight(m_delegate->sizeHint(viewOptions(), QModelIndex()).height());
|
||||
}
|
||||
|
||||
void TabListView::currentChanged(const QModelIndex ¤t, const QModelIndex &previous)
|
||||
{
|
||||
if (current.data(TabModel::CurrentTabRole).toBool()) {
|
||||
QListView::currentChanged(current, previous);
|
||||
} else if (previous.data(TabModel::CurrentTabRole).toBool()) {
|
||||
setCurrentIndex(previous);
|
||||
}
|
||||
}
|
||||
|
||||
void TabListView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles)
|
||||
{
|
||||
QListView::dataChanged(topLeft, bottomRight, roles);
|
||||
|
||||
if (roles.size() == 1 && roles.at(0) == TabModel::CurrentTabRole && topLeft.data(TabModel::CurrentTabRole).toBool()) {
|
||||
setCurrentIndex(topLeft);
|
||||
}
|
||||
}
|
||||
|
||||
bool TabListView::viewportEvent(QEvent *event)
|
||||
{
|
||||
switch (event->type()) {
|
||||
case QEvent::MouseButtonPress: {
|
||||
QMouseEvent *me = static_cast<QMouseEvent*>(event);
|
||||
const QModelIndex index = indexAt(me->pos());
|
||||
WebTab *tab = index.data(TabModel::WebTabRole).value<WebTab*>();
|
||||
if (me->buttons() == Qt::MiddleButton && tab) {
|
||||
tab->closeTab();
|
||||
}
|
||||
if (me->buttons() != Qt::LeftButton) {
|
||||
m_pressedIndex = QModelIndex();
|
||||
m_pressedButton = NoButton;
|
||||
break;
|
||||
}
|
||||
m_pressedIndex = index;
|
||||
m_pressedButton = buttonAt(me->pos(), m_pressedIndex);
|
||||
if (m_pressedButton == NoButton && tab) {
|
||||
tab->makeCurrentTab();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case QEvent::MouseButtonRelease: {
|
||||
QMouseEvent *me = static_cast<QMouseEvent*>(event);
|
||||
if (me->buttons() != Qt::NoButton) {
|
||||
break;
|
||||
}
|
||||
const QModelIndex index = indexAt(me->pos());
|
||||
if (m_pressedIndex != index) {
|
||||
break;
|
||||
}
|
||||
DelegateButton button = buttonAt(me->pos(), index);
|
||||
if (m_pressedButton == button) {
|
||||
WebTab *tab = index.data(TabModel::WebTabRole).value<WebTab*>();
|
||||
if (tab && m_pressedButton == AudioButton) {
|
||||
tab->toggleMuted();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case QEvent::ToolTip: {
|
||||
QHelpEvent *he = static_cast<QHelpEvent*>(event);
|
||||
const QModelIndex index = indexAt(he->pos());
|
||||
DelegateButton button = buttonAt(he->pos(), index);
|
||||
if (button == AudioButton) {
|
||||
const bool muted = index.data(TabModel::AudioMutedRole).toBool();
|
||||
QToolTip::showText(he->globalPos(), muted ? tr("Unmute Tab") : tr("Mute Tab"), this, visualRect(index));
|
||||
he->accept();
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case QEvent::ContextMenu: {
|
||||
QContextMenuEvent *ce = static_cast<QContextMenuEvent*>(event);
|
||||
const QModelIndex index = indexAt(ce->pos());
|
||||
WebTab *tab = index.data(TabModel::WebTabRole).value<WebTab*>();
|
||||
if (tab) {
|
||||
TabContextMenu menu(tab, Qt::Horizontal, false);
|
||||
menu.exec(ce->globalPos());
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return QListView::viewportEvent(event);
|
||||
}
|
||||
|
||||
TabListView::DelegateButton TabListView::buttonAt(const QPoint &pos, const QModelIndex &index) const
|
||||
{
|
||||
if (m_delegate->audioButtonRect(index).contains(pos)) {
|
||||
return AudioButton;
|
||||
}
|
||||
return NoButton;
|
||||
}
|
47
src/plugins/VerticalTabs/tablistview.h
Normal file
47
src/plugins/VerticalTabs/tablistview.h
Normal file
@ -0,0 +1,47 @@
|
||||
/* ============================================================
|
||||
* VerticalTabs plugin for Falkon
|
||||
* Copyright (C) 2018 David Rosca <nowrep@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
* ============================================================ */
|
||||
#pragma once
|
||||
|
||||
#include <QListView>
|
||||
|
||||
class TabListDelegate;
|
||||
|
||||
class TabListView : public QListView
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TabListView(QWidget *parent = nullptr);
|
||||
|
||||
private:
|
||||
void currentChanged(const QModelIndex ¤t, const QModelIndex &previous) override;
|
||||
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles = QVector<int>()) override;
|
||||
bool viewportEvent(QEvent *event) override;
|
||||
|
||||
TabListDelegate *m_delegate;
|
||||
|
||||
enum DelegateButton {
|
||||
NoButton,
|
||||
AudioButton
|
||||
};
|
||||
|
||||
DelegateButton buttonAt(const QPoint &pos, const QModelIndex &index) const;
|
||||
|
||||
DelegateButton m_pressedButton = NoButton;
|
||||
QModelIndex m_pressedIndex;
|
||||
};
|
@ -36,6 +36,7 @@ TabTreeView::TabTreeView(QWidget *parent)
|
||||
setDropIndicatorShown(true);
|
||||
setAllColumnsShowFocus(true);
|
||||
setMouseTracking(true);
|
||||
setFrameShape(QFrame::NoFrame);
|
||||
setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
|
||||
setIndentation(0);
|
||||
|
||||
|
@ -17,12 +17,15 @@
|
||||
* ============================================================ */
|
||||
#include "verticaltabswidget.h"
|
||||
#include "tabtreeview.h"
|
||||
#include "tablistview.h"
|
||||
#include "tabfiltermodel.h"
|
||||
|
||||
#include "tabmodel.h"
|
||||
#include "tabtreemodel.h"
|
||||
#include "browserwindow.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QListView>
|
||||
|
||||
VerticalTabsWidget::VerticalTabsWidget(BrowserWindow *window)
|
||||
: QWidget()
|
||||
@ -32,24 +35,38 @@ VerticalTabsWidget::VerticalTabsWidget(BrowserWindow *window)
|
||||
layout->setSpacing(0);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
m_view = new TabTreeView(this);
|
||||
layout->addWidget(m_view);
|
||||
TabListView *m_pinnedView = new TabListView(this);
|
||||
m_normalView = new TabTreeView(this);
|
||||
layout->addWidget(m_pinnedView);
|
||||
layout->addWidget(m_normalView);
|
||||
|
||||
TabFilterModel *model = new TabFilterModel(m_pinnedView);
|
||||
model->setFilterPinnedTabs(false);
|
||||
model->setSourceModel(m_window->tabModel());
|
||||
m_pinnedView->setModel(model);
|
||||
|
||||
m_pinnedView->setFocusProxy(m_normalView);
|
||||
}
|
||||
|
||||
void VerticalTabsWidget::setViewType(VerticalTabsPlugin::ViewType type)
|
||||
{
|
||||
TabFilterModel *model = new TabFilterModel(m_normalView);
|
||||
model->setFilterPinnedTabs(true);
|
||||
|
||||
switch (type) {
|
||||
case VerticalTabsPlugin::TabListView:
|
||||
m_view->setModel(m_window->tabModel());
|
||||
m_view->setTabsInOrder(true);
|
||||
model->setSourceModel(m_window->tabModel());
|
||||
m_normalView->setModel(model);
|
||||
m_normalView->setTabsInOrder(true);
|
||||
break;
|
||||
|
||||
case VerticalTabsPlugin::TabTreeView:
|
||||
delete m_treeModel;
|
||||
m_treeModel = new TabTreeModel(this);
|
||||
m_treeModel->setSourceModel(m_window->tabModel());
|
||||
m_view->setModel(m_treeModel);
|
||||
m_view->setTabsInOrder(false);
|
||||
model->setSourceModel(m_treeModel);
|
||||
m_normalView->setModel(model);
|
||||
m_normalView->setTabsInOrder(false);
|
||||
break;
|
||||
|
||||
default:
|
||||
|
@ -24,6 +24,7 @@
|
||||
class BrowserWindow;
|
||||
class TabTreeModel;
|
||||
|
||||
class TabListView;
|
||||
class TabTreeView;
|
||||
|
||||
class VerticalTabsWidget : public QWidget
|
||||
@ -36,6 +37,7 @@ public:
|
||||
|
||||
private:
|
||||
BrowserWindow *m_window;
|
||||
TabTreeView *m_view;
|
||||
TabListView *m_pinnedView;
|
||||
TabTreeView *m_normalView;
|
||||
TabTreeModel *m_treeModel = nullptr;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user