diff --git a/src/chatdlg.cpp b/src/chatdlg.cpp
index d49a716fb9..9e484c762e 100644
--- a/src/chatdlg.cpp
+++ b/src/chatdlg.cpp
@@ -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 );
@@ -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 );
@@ -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)
@@ -156,8 +160,49 @@ void CChatDlg::AddChatText ( QString strChatText )
"\\1" );
}
- // 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 )
@@ -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 ( event );
+ if ( ke->matches ( QKeySequence::Copy ) )
+ {
+ OnCopyChatMessage();
+ return true;
+ }
+ }
+ if ( obj == txvChatWindow->viewport() )
+ {
+ if ( event->type() == QEvent::MouseMove )
+ {
+ QMouseEvent* me = static_cast ( 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 ( 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()
{
diff --git a/src/chatdlg.h b/src/chatdlg.h
index eaad2c1cbe..e9cba2d02b 100644
--- a/src/chatdlg.h
+++ b/src/chatdlg.h
@@ -57,10 +57,52 @@
#include
#include
#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
#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 ( 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 ( doc.size().height() ) );
+ }
+};
+
/* Classes ********************************************************************/
class CChatDlg : public CBaseDlg, private Ui_CChatDlgBase
{
@@ -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;
};
diff --git a/src/chatdlgbase.ui b/src/chatdlgbase.ui
index 7d59db9661..0b4eb659a4 100644
--- a/src/chatdlgbase.ui
+++ b/src/chatdlgbase.ui
@@ -28,19 +28,10 @@
-
-
+
false
-
- Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse
-
-
- false
-
-
- false
-
-