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 -