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
16 changes: 16 additions & 0 deletions framework/toast/internal/toastprovider.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,22 @@ void ToastProvider::executeAction(int id, ToastActionCode actionCode)
dismissToast(id);
}

void ToastProvider::pauseToast(int id)
{
auto timerIt = m_progressTimers.find(id);
if (timerIt != m_progressTimers.end()) {
timerIt->second->stop();
}
}

void ToastProvider::resumeToast(int id)
{
auto timerIt = m_progressTimers.find(id);
if (timerIt != m_progressTimers.end() && !timerIt->second->isActive()) {
timerIt->second->start();
}
}

void ToastProvider::cleanup(int id)
{
auto timerIt = m_progressTimers.find(id);
Expand Down
3 changes: 3 additions & 0 deletions framework/toast/internal/toastprovider.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ class ToastProvider : public IToastProvider, public muse::async::Asyncable
void dismissToast(int id) override;
void executeAction(int id, ToastActionCode actionCode) override;

void pauseToast(int id) override;
void resumeToast(int id) override;

private:
void cleanup(int id);
void checkProgress(int id);
Expand Down
3 changes: 3 additions & 0 deletions framework/toast/itoastprovider.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,8 @@ class IToastProvider : MODULE_GLOBAL_INTERFACE

virtual void dismissToast(int id) = 0;
virtual void executeAction(int id, ToastActionCode actionCode) = 0;

virtual void pauseToast(int id) = 0;
virtual void resumeToast(int id) = 0;
};
}
60 changes: 60 additions & 0 deletions framework/toast/qml/Muse/Toast/ToastItem.qml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ Item {
property int actionButtonMargins: 6
property int actionButtonsSpacing: 8

property NavigationSection navigationSection: null
property int navigationOrder: -1
property int totalCount: 0
readonly property bool navigationFocused: toastNavPanel.active
readonly property bool shouldPauseTimer: hoverHandler.hovered || root.navigationFocused
property alias navigation: bodyNavCtrl

property string accessibleTitle: ""

signal dismissed
signal actionTriggered(string actionStr)

Expand All @@ -62,6 +71,41 @@ Item {

implicitHeight: mainContainer.childrenRect.height + root.verticalMargin * 2

NavigationPanel {
id: toastNavPanel

name: "Toast" + root.navigationOrder
section: root.navigationSection
order: root.navigationOrder
direction: NavigationPanel.Horizontal
enabled: root.enabled && root.visible

accessible.name: root.accessibleTitle
}

NavigationControl {
id: bodyNavCtrl

name: "ToastBody"
panel: toastNavPanel
order: 0
enabled: root.enabled && root.visible

accessible.role: MUAccessible.StaticText
accessible.name: {
var name = root.message.length > 0 ? root.accessibleTitle + ". " + root.message : root.accessibleTitle
if (root.totalCount > 0) {
name += ". " + qsTrc("toast", "%1 of %2").arg(root.navigationOrder + 1).arg(root.totalCount)
}
return name
}
accessible.visualItem: root
}

HoverHandler {
id: hoverHandler
}

StyledRectangularShadow {
anchors.fill: backgroundRect

Expand All @@ -76,6 +120,10 @@ Item {
color: ui.theme.popupBackgroundColor
radius: 8

NavigationFocusBorder {
navigationCtrl: bodyNavCtrl
}

Rectangle {
id: progressBar

Expand Down Expand Up @@ -182,6 +230,11 @@ Item {
height: root.actionButtonHeight
minWidth: root.actionButtonMinWidth
margins: root.actionButtonMargins

navigation.panel: toastNavPanel
navigation.order: 1 + index
navigation.name: "ToastAction" + index

onClicked: {
root.actionTriggered(root.actions[index].text)
}
Expand All @@ -200,6 +253,13 @@ Item {
icon: IconCode.CLOSE_X_ROUNDED
transparent: true

toolTipTitle: qsTrc("toast", "Dismiss")

navigation.panel: toastNavPanel
navigation.order: 1 + root.actions.length
navigation.name: "ToastDismiss"
navigation.accessible.name: dismissButton.toolTipTitle

onClicked: {
dismissed()
}
Expand Down
41 changes: 41 additions & 0 deletions framework/toast/qml/Muse/Toast/ToastProvider.qml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,26 @@ StyledListView {

visible: root.count > 0

property int navigationOrder: 0

property NavigationSection navigationSection: NavigationSection {
id: toastNavSec
name: "ToastNotifications"
enabled: root.enabled && root.visible
order: root.navigationOrder
}

QtObject {
id: prv

function restoreFocus() {
var newestItem = root.count > 0 ? root.itemAtIndex(root.count - 1) : null
if (newestItem) {
newestItem.navigation.requestActive()
}
}
}

ToastListModel {
id: toastmodel
}
Expand All @@ -69,6 +89,7 @@ StyledListView {
opacity: 0

title: model.title
accessibleTitle: model.accessibleTitle
iconCode: model.iconCode
message: model.message
actions: model.actions
Expand All @@ -78,13 +99,33 @@ StyledListView {
showProgressInfo: model.showProgressInfo
timeElapsed: model.timeElapsed

navigationSection: toastNavSec
navigationOrder: root.count - 1 - model.index
totalCount: root.count

onShouldPauseTimerChanged: {
if (shouldPauseTimer) {
toastmodel.pauseToast(model.id)
} else {
toastmodel.resumeToast(model.id)
}
}

onActionTriggered: function (actionStr) {
toastmodel.executeAction(model.id, actionStr)
}

Component.onCompleted: {
x = 0
opacity = 1

toastNavSec.requestPriority()
}

Component.onDestruction: {
if (itemRect.navigationFocused) {
Qt.callLater(prv.restoreFocus)
}
}

Behavior on x {
Expand Down
122 changes: 86 additions & 36 deletions framework/toast/qml/Muse/Toast/toastlistmodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,38 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <iterator>
#include "toastlistmodel.h"

#include <algorithm>

#include <QModelIndex>
#include <QString>

#include "toastlistmodel.h"
#include "global/translation.h"
#include "toast/toastitem.h"

using namespace muse::toast;

namespace {
constexpr int MAX_VISIBLE_TOASTS = 5;
constexpr int MAX_NAVIGATION_HINT_ANNOUNCEMENTS = 2;

QString accessibleTitle(const ToastItem& toast)
{
const QString title = QString::fromStdString(toast.title());
switch (toast.iconCode()) {
case muse::ui::IconCode::Code::ERROR:
return muse::qtrc("toast", "Error") + ": " + title;
case muse::ui::IconCode::Code::WARNING:
return muse::qtrc("toast", "Warning") + ": " + title;
case muse::ui::IconCode::Code::INFO:
return muse::qtrc("toast", "Information") + ": " + title;
case muse::ui::IconCode::Code::TICK:
return muse::qtrc("toast", "Success") + ": " + title;
default:
return title;
}
}
}

ToastListModel::ToastListModel(QObject* parent)
Expand All @@ -49,65 +69,92 @@ void ToastListModel::init()
m_toasts.emplace_back(toast);
endInsertRows();

announceToast(*toast);

int id = toast->id();
toast->progressChanged().onNotify(this, [this, id](){
int toastIndex = -1;
for (int i = 0; i < static_cast<int>(m_toasts.size()); ++i) {
if (m_toasts.at(i)->id() == id) {
toastIndex = i;
break;
}
}

if (toastIndex == -1) {
const std::optional<int> toastIndex = indexOfToast(id);
if (!toastIndex) {
return;
}

emit dataChanged(index(toastIndex), this->index(toastIndex), { ProgressRole, TimeElapsedRole });
emit dataChanged(index(toastIndex.value()), this->index(toastIndex.value()), { ProgressRole, TimeElapsedRole });
});
}, muse::async::Asyncable::Mode::SetReplace);

toastProvider()->toastDismissed().onReceive(this, [this](int id) {
int index = -1;
for (int i = 0; i < static_cast<int>(m_toasts.size()); ++i) {
if (m_toasts.at(i)->id() == id) {
index = i;
break;
}
}

if (index == -1) {
const std::optional<int> toastIndex = indexOfToast(id);
if (!toastIndex) {
return;
}

beginRemoveRows(QModelIndex(), index, index);
m_toasts.erase(m_toasts.begin() + index);
beginRemoveRows(QModelIndex(), toastIndex.value(), toastIndex.value());
m_toasts.erase(m_toasts.begin() + toastIndex.value());
endRemoveRows();
}, muse::async::Asyncable::Mode::SetReplace);
}

void ToastListModel::announceToast(const ToastItem& toast)
{
QString announcement = accessibleTitle(toast);

if (!toast.message().empty()) {
announcement += ": " + QString::fromStdString(toast.message());
}

if (!toast.actions().empty() && m_navigationHintShownCount < MAX_NAVIGATION_HINT_ANNOUNCEMENTS) {
announcement += " " + muse::qtrc("toast", "Press F6 to go to the notification.");
++m_navigationHintShownCount;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

accessibilityController()->announce(announcement);
}

std::optional<int> ToastListModel::indexOfToast(int id) const
{
const auto it = std::find_if(m_toasts.cbegin(), m_toasts.cend(), [id](const std::shared_ptr<ToastItem>& toast) {
return toast->id() == id;
});

if (it == m_toasts.cend()) {
return std::nullopt;
}

return static_cast<int>(std::distance(m_toasts.cbegin(), it));
}

void ToastListModel::dismissToast(int id)
{
toastProvider()->dismissToast(id);
}

void ToastListModel::pauseToast(int id)
{
toastProvider()->pauseToast(id);
}

void ToastListModel::resumeToast(int id)
{
toastProvider()->resumeToast(id);
}

void ToastListModel::executeAction(int id, QString actionStr)
{
for (int i = 0; i < static_cast<int>(m_toasts.size()); ++i) {
if (m_toasts.at(i)->id() == id) {
const auto& actions = m_toasts.at(i)->actions();
auto it = std::find_if(actions.cbegin(), actions.cend(), [&actionStr](const ToastAction& action) {
return action.text == actionStr.toStdString();
});

if (it == actions.cend()) {
return;
}
const std::optional<int> toastIndex = indexOfToast(id);
if (!toastIndex) {
return;
}

toastProvider()->executeAction(id, it->code);
return;
}
const auto& actions = m_toasts.at(toastIndex.value())->actions();
auto it = std::find_if(actions.cbegin(), actions.cend(), [&actionStr](const ToastAction& action) {
return action.text == actionStr.toStdString();
});

if (it == actions.cend()) {
return;
}

toastProvider()->executeAction(id, it->code);
}

int ToastListModel::rowCount(const QModelIndex& parent) const
Expand All @@ -133,6 +180,8 @@ QVariant ToastListModel::data(const QModelIndex& index, int role) const
return static_cast<int>(toast->iconCode());
case TitleRole:
return QString::fromStdString(toast->title());
case AccessibleTitleRole:
return accessibleTitle(*toast);
case MessageRole:
return QString::fromStdString(toast->message());
case DismissableRole:
Expand Down Expand Up @@ -164,6 +213,7 @@ QHash<int, QByteArray> ToastListModel::roleNames() const
{ IdRole, "id" },
{ IconCodeRole, "iconCode" },
{ TitleRole, "title" },
{ AccessibleTitleRole, "accessibleTitle" },
{ MessageRole, "message" },
{ DismissableRole, "dismissable" },
{ ActionRole, "actions" },
Expand Down
Loading