Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 111 additions & 13 deletions src/chatdlg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,16 +65,24 @@ CChatDlg::CChatDlg ( QWidget* parent ) : CBaseDlg ( parent, Qt::Window ) // use

edtLocalInputText->setAccessibleName ( tr ( "New chat text edit box" ) );

// clear chat window and edit line
txvChatWindow->clear();
// clear edit line
edtLocalInputText->clear();

// we do not want to show a cursor in the chat history
txvChatWindow->setCursorWidth ( 0 );

// set a placeholder text to make sure where to type the message in (#384)
edtLocalInputText->setPlaceholderText ( tr ( "Type a message here" ) );

// Set up the list model and delegate for accessible per-message rows ------
m_pChatModel = new QStandardItemModel ( this );
txvChatWindow->setModel ( m_pChatModel );
txvChatWindow->setItemDelegate ( new ChatDelegate ( txvChatWindow ) );
txvChatWindow->setSelectionMode ( QAbstractItemView::SingleSelection );
txvChatWindow->setEditTriggers ( QAbstractItemView::NoEditTriggers );
txvChatWindow->setWordWrap ( true );
txvChatWindow->setResizeMode ( QListView::Adjust );
txvChatWindow->setContextMenuPolicy ( Qt::CustomContextMenu );
txvChatWindow->installEventFilter ( this );
txvChatWindow->viewport()->installEventFilter ( this );

// Menu -------------------------------------------------------------------
QMenuBar* pMenu = new QMenuBar ( this );
QMenu* pEditMenu = new QMenu ( tr ( "&Edit" ), this );
Expand All @@ -98,7 +106,7 @@ CChatDlg::CChatDlg ( QWidget* parent ) : CBaseDlg ( parent, Qt::Window ) // use

QObject::connect ( butSend, &QPushButton::clicked, this, &CChatDlg::OnSendText );

QObject::connect ( txvChatWindow, &QTextBrowser::anchorClicked, this, &CChatDlg::OnAnchorClicked );
QObject::connect ( txvChatWindow, &QListView::customContextMenuRequested, this, &CChatDlg::OnChatContextMenu );

#if defined( Q_OS_IOS )
QObject::connect ( closeAction, &QAction::triggered, this, &CChatDlg::OnCloseClicked );
Expand Down Expand Up @@ -127,15 +135,11 @@ void CChatDlg::OnSendText()

void CChatDlg::OnClearChatHistory()
{
// clear chat window
txvChatWindow->clear();
m_pChatModel->clear();
}

void CChatDlg::AddChatText ( QString strChatText )
{
// notify accessibility plugin that text has changed
QAccessible::updateAccessibility ( new QAccessibleValueChangeEvent ( txvChatWindow, strChatText ) );

// analyze strChatText to check if hyperlink (limit ourselves to http(s)://) but do not
// replace the hyperlinks if any HTML code for a hyperlink was found (the user has done the HTML
// coding hisself and we should not mess with that)
Expand All @@ -156,8 +160,49 @@ void CChatDlg::AddChatText ( QString strChatText )
"<a href=\"\\1\">\\1</a>" );
}

// add new text in chat window
txvChatWindow->append ( strChatText );
// DisplayRole stores HTML; AccessibleTextRole gives screen readers clean text
// without angle-bracket markup (VoiceOver reads this role when narrating list items)
QTextDocument plainDoc;
plainDoc.setHtml ( strChatText );
QString strPlainText = plainDoc.toPlainText();

QStandardItem* pItem = new QStandardItem ( strChatText );
pItem->setData ( strPlainText, Qt::AccessibleTextRole );
m_pChatModel->appendRow ( pItem );
txvChatWindow->scrollToBottom();

// tell screen readers a new row was inserted, then announce its text as the
// list's current value — drives VoiceOver live-region-style announcement on macOS
int row = m_pChatModel->rowCount() - 1;
QAccessibleTableModelChangeEvent* pChangeEvent =
new QAccessibleTableModelChangeEvent ( txvChatWindow, QAccessibleTableModelChangeEvent::RowsInserted );
pChangeEvent->setFirstRow ( row );
pChangeEvent->setLastRow ( row );
pChangeEvent->setFirstColumn ( 0 );
pChangeEvent->setLastColumn ( 0 );
QAccessible::updateAccessibility ( pChangeEvent );
QAccessible::updateAccessibility ( new QAccessibleValueChangeEvent ( txvChatWindow, strPlainText ) );
}

void CChatDlg::OnCopyChatMessage()
{
QModelIndexList sel = txvChatWindow->selectionModel()->selectedIndexes();
if ( sel.isEmpty() )
return;
QTextDocument doc;
doc.setHtml ( sel.first().data ( Qt::DisplayRole ).toString() );
QApplication::clipboard()->setText ( doc.toPlainText() );
}

void CChatDlg::OnChatContextMenu ( const QPoint& pos )
{
QModelIndex idx = txvChatWindow->indexAt ( pos );
if ( !idx.isValid() )
return;
txvChatWindow->setCurrentIndex ( idx );
QMenu menu ( this );
menu.addAction ( tr ( "Copy message" ), this, &CChatDlg::OnCopyChatMessage );
menu.exec ( txvChatWindow->viewport()->mapToGlobal ( pos ) );
}

void CChatDlg::OnAnchorClicked ( const QUrl& Url )
Expand All @@ -175,6 +220,59 @@ void CChatDlg::OnAnchorClicked ( const QUrl& Url )
}
}

bool CChatDlg::eventFilter ( QObject* obj, QEvent* event )
{
if ( obj == txvChatWindow && event->type() == QEvent::KeyPress )
{
QKeyEvent* ke = static_cast<QKeyEvent*> ( event );
if ( ke->matches ( QKeySequence::Copy ) )
{
OnCopyChatMessage();
return true;
}
}
if ( obj == txvChatWindow->viewport() )
{
if ( event->type() == QEvent::MouseMove )
{
QMouseEvent* me = static_cast<QMouseEvent*> ( event );
QModelIndex idx = txvChatWindow->indexAt ( me->pos() );
if ( idx.isValid() )
{
QRect rect = txvChatWindow->visualRect ( idx );
QTextDocument doc;
doc.setHtml ( idx.data ( Qt::DisplayRole ).toString() );
doc.setTextWidth ( rect.width() );
QString anchor = doc.documentLayout()->anchorAt ( me->pos() - rect.topLeft() );
txvChatWindow->viewport()->setCursor ( anchor.isEmpty() ? Qt::ArrowCursor : Qt::PointingHandCursor );
}
else
{
txvChatWindow->viewport()->setCursor ( Qt::ArrowCursor );
}
}
if ( event->type() == QEvent::MouseButtonPress )
{
QMouseEvent* me = static_cast<QMouseEvent*> ( event );
QModelIndex idx = txvChatWindow->indexAt ( me->pos() );
if ( idx.isValid() )
{
QRect rect = txvChatWindow->visualRect ( idx );
QTextDocument doc;
doc.setHtml ( idx.data ( Qt::DisplayRole ).toString() );
doc.setTextWidth ( rect.width() );
QString anchor = doc.documentLayout()->anchorAt ( me->pos() - rect.topLeft() );
if ( !anchor.isEmpty() )
{
OnAnchorClicked ( QUrl ( anchor ) );
return true;
}
}
}
}
return CBaseDlg::eventFilter ( obj, event );
}

#if defined( Q_OS_IOS ) || defined( ANDROID ) || defined( Q_OS_ANDROID )
void CChatDlg::OnCloseClicked()
{
Expand Down
50 changes: 50 additions & 0 deletions src/chatdlg.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,52 @@
#include <QDesktopServices>
#include <QMessageBox>
#include <QRegularExpression>
#include <QListView>
#include <QStandardItemModel>
#include <QStyledItemDelegate>
#include <QTextDocument>
#include <QAbstractTextDocumentLayout>
#include <QPainter>
#include <QStyle>
#include <QMouseEvent>
#include <QKeyEvent>
#include <QMenu>
#include <QClipboard>
#include <QApplication>
#include "global.h"
#include "util.h"
#include "ui_chatdlgbase.h"

class ChatDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
explicit ChatDelegate ( QObject* parent = nullptr ) : QStyledItemDelegate ( parent ) {}

void paint ( QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index ) const override
{
painter->save();
if ( option.state & QStyle::State_Selected )
painter->fillRect ( option.rect, option.palette.highlight() );
QTextDocument doc;
doc.setHtml ( index.data ( Qt::DisplayRole ).toString() );
doc.setTextWidth ( option.rect.width() );
painter->translate ( option.rect.topLeft() );
doc.drawContents ( painter, option.rect.translated ( -option.rect.topLeft() ) );
painter->restore();
}

QSize sizeHint ( const QStyleOptionViewItem& option, const QModelIndex& index ) const override
{
QListView* view = qobject_cast<QListView*> ( parent() );
int w = ( view && view->viewport()->width() > 0 ) ? view->viewport()->width() : option.rect.width();
QTextDocument doc;
doc.setHtml ( index.data ( Qt::DisplayRole ).toString() );
doc.setTextWidth ( w > 0 ? w : 200 );
return QSize ( w, static_cast<int> ( doc.size().height() ) );
}
};

/* Classes ********************************************************************/
class CChatDlg : public CBaseDlg, private Ui_CChatDlgBase
{
Expand All @@ -76,10 +118,18 @@ public slots:
void OnLocalInputTextTextChanged ( const QString& strNewText );
void OnClearChatHistory();
void OnAnchorClicked ( const QUrl& Url );
void OnCopyChatMessage();
void OnChatContextMenu ( const QPoint& pos );
#if defined( Q_OS_IOS ) || defined( ANDROID ) || defined( Q_OS_ANDROID )
void OnCloseClicked();
#endif

signals:
void NewLocalInputText ( QString strNewText );

protected:
bool eventFilter ( QObject* obj, QEvent* event ) override;

private:
QStandardItemModel* m_pChatModel;
};
11 changes: 1 addition & 10 deletions src/chatdlgbase.ui
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,10 @@
</property>
<layout class="QVBoxLayout">
<item>
<widget class="QTextBrowser" name="txvChatWindow">
<widget class="QListView" name="txvChatWindow">
<property name="acceptDrops">
<bool>false</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
<property name="openExternalLinks">
<bool>false</bool>
</property>
<property name="openLinks">
<bool>false</bool>
</property>
</widget>
</item>
<item>
Expand Down
Loading