1/*
2 SPDX-FileCopyrightText: 2012 Dominik Haumann <dhaumann@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "katemessagewidget.h"
8
9#include "katepartdebug.h"
10
11#include <KTextEditor/Message>
12
13#include <KMessageWidget>
14#include <kateanimation.h>
15
16#include <QEvent>
17#include <QShowEvent>
18#include <QTimer>
19#include <QToolTip>
20#include <QVBoxLayout>
21
22static const int s_defaultAutoHideTime = 6 * 1000;
23
24KateMessageWidget::KateMessageWidget(QWidget *parent, bool applyFadeEffect)
25 : QWidget(parent)
26 , m_animation(nullptr)
27 , m_autoHideTimer(new QTimer(this))
28 , m_autoHideTime(-1)
29{
30 QVBoxLayout *l = new QVBoxLayout(this);
31 l->setContentsMargins(left: 0, top: 0, right: 0, bottom: 0);
32
33 m_messageWidget = new KMessageWidget(this);
34 m_messageWidget->setCloseButtonVisible(false);
35
36 l->addWidget(m_messageWidget);
37
38 // tell the widget to always use the minimum size.
39 setSizePolicy(hor: QSizePolicy::Preferred, ver: QSizePolicy::Minimum);
40
41 // by default, hide widgets
42 m_messageWidget->hide();
43 hide();
44
45 // create animation controller, and connect widgetHidden() to showNextMessage()
46 m_animation = new KateAnimation(m_messageWidget, applyFadeEffect ? KateAnimation::FadeEffect : KateAnimation::GrowEffect);
47 connect(sender: m_animation, signal: &KateAnimation::widgetHidden, context: this, slot: &KateMessageWidget::showNextMessage);
48
49 // setup autoHide timer details
50 m_autoHideTimer->setSingleShot(true);
51
52 connect(sender: m_messageWidget, signal: &KMessageWidget::linkHovered, context: this, slot: &KateMessageWidget::linkHovered);
53}
54
55void KateMessageWidget::setPosition(KateMessageWidget::Position position)
56{
57 switch (position) {
58 case Position::Inline:
59 m_messageWidget->setPosition(KMessageWidget::Inline);
60 return;
61 case Position::Header:
62 m_messageWidget->setPosition(KMessageWidget::Header);
63 return;
64 case Position::Footer:
65 m_messageWidget->setPosition(KMessageWidget::Footer);
66 return;
67 }
68}
69
70void KateMessageWidget::showNextMessage()
71{
72 // at this point, we should not have a currently shown message
73 Q_ASSERT(m_currentMessage == nullptr);
74
75 // if not message to show, just stop
76 if (m_messageQueue.size() == 0) {
77 hide();
78 return;
79 }
80
81 // track current message
82 m_currentMessage = m_messageQueue[0];
83
84 // set text etc.
85 m_messageWidget->setText(m_currentMessage->text());
86 m_messageWidget->setIcon(m_currentMessage->icon());
87
88 // connect textChanged() and iconChanged(), so it's possible to change this on the fly
89 connect(sender: m_currentMessage, signal: &KTextEditor::Message::textChanged, context: m_messageWidget, slot: &KMessageWidget::setText, type: Qt::UniqueConnection);
90 connect(sender: m_currentMessage, signal: &KTextEditor::Message::iconChanged, context: m_messageWidget, slot: &KMessageWidget::setIcon, type: Qt::UniqueConnection);
91
92 // the enums values do not necessarily match, hence translate with switch
93 switch (m_currentMessage->messageType()) {
94 case KTextEditor::Message::Positive:
95 m_messageWidget->setMessageType(KMessageWidget::Positive);
96 break;
97 case KTextEditor::Message::Information:
98 m_messageWidget->setMessageType(KMessageWidget::Information);
99 break;
100 case KTextEditor::Message::Warning:
101 m_messageWidget->setMessageType(KMessageWidget::Warning);
102 break;
103 case KTextEditor::Message::Error:
104 m_messageWidget->setMessageType(KMessageWidget::Error);
105 break;
106 default:
107 m_messageWidget->setMessageType(KMessageWidget::Information);
108 break;
109 }
110
111 // remove all actions from the message widget
112 const auto messageWidgetActions = m_messageWidget->actions();
113 for (QAction *a : messageWidgetActions) {
114 m_messageWidget->removeAction(action: a);
115 }
116
117 // add new actions to the message widget
118 const auto m_currentMessageActions = m_currentMessage->actions();
119 for (QAction *a : m_currentMessageActions) {
120 m_messageWidget->addAction(action: a);
121 }
122
123 // set word wrap of the message
124 setWordWrap(m_currentMessage);
125
126 // setup auto-hide timer, and start if requested
127 m_autoHideTime = m_currentMessage->autoHide();
128 m_autoHideTimer->stop();
129 if (m_autoHideTime >= 0) {
130 connect(sender: m_autoHideTimer, signal: &QTimer::timeout, context: m_currentMessage, slot: &QObject::deleteLater, type: Qt::UniqueConnection);
131 if (m_currentMessage->autoHideMode() == KTextEditor::Message::Immediate) {
132 m_autoHideTimer->start(msec: m_autoHideTime == 0 ? s_defaultAutoHideTime : m_autoHideTime);
133 }
134 }
135
136 // finally show
137 show();
138 m_animation->show();
139}
140
141void KateMessageWidget::setWordWrap(KTextEditor::Message *message)
142{
143 // want word wrap anyway? -> ok
144 if (message->wordWrap()) {
145 m_messageWidget->setWordWrap(message->wordWrap());
146 return;
147 }
148
149 // word wrap not wanted, that's ok if a parent widget does not exist
150 if (!parentWidget()) {
151 m_messageWidget->setWordWrap(false);
152 return;
153 }
154
155 // word wrap not wanted -> enable word wrap if it breaks the layout otherwise
156 int margin = 0;
157 if (parentWidget()->layout()) {
158 // get left/right margin of the layout, since we need to subtract these
159 int leftMargin = 0;
160 int rightMargin = 0;
161 parentWidget()->layout()->getContentsMargins(left: &leftMargin, top: nullptr, right: &rightMargin, bottom: nullptr);
162 margin = leftMargin + rightMargin;
163 }
164
165 // if word wrap enabled, first disable it
166 if (m_messageWidget->wordWrap()) {
167 m_messageWidget->setWordWrap(false);
168 }
169
170 // make sure the widget's size is up-to-date in its hidden state
171 m_messageWidget->ensurePolished();
172 m_messageWidget->adjustSize();
173
174 // finally enable word wrap, if there is not enough free horizontal space
175 const int freeSpace = (parentWidget()->width() - margin) - m_messageWidget->width();
176 if (freeSpace < 0) {
177 // qCDebug(LOG_KTE) << "force word wrap to avoid breaking the layout" << freeSpace;
178 m_messageWidget->setWordWrap(true);
179 }
180}
181
182void KateMessageWidget::postMessage(KTextEditor::Message *message, QList<std::shared_ptr<QAction>> actions)
183{
184 Q_ASSERT(!m_messageHash.contains(message));
185 m_messageHash[message] = std::move(actions);
186
187 // insert message sorted after priority
188 int i = 0;
189 for (; i < m_messageQueue.count(); ++i) {
190 if (message->priority() > m_messageQueue[i]->priority()) {
191 break;
192 }
193 }
194
195 // queue message
196 m_messageQueue.insert(i, t: message);
197
198 // catch if the message gets deleted
199 connect(sender: message, signal: &KTextEditor::Message::closed, context: this, slot: &KateMessageWidget::messageDestroyed);
200
201 if (i == 0 && !m_animation->isHideAnimationRunning()) {
202 // if message has higher priority than the one currently shown,
203 // then hide the current one and then show the new one.
204 if (m_currentMessage) {
205 // autoHide timer may be running for currently shown message, therefore
206 // simply disconnect autoHide timer to all timeout() receivers
207 disconnect(sender: m_autoHideTimer, signal: &QTimer::timeout, receiver: nullptr, zero: nullptr);
208 m_autoHideTimer->stop();
209
210 // if there is a current message, the message queue must contain 2 messages
211 Q_ASSERT(m_messageQueue.size() > 1);
212 Q_ASSERT(m_currentMessage == m_messageQueue[1]);
213
214 // a bit unnice: disconnect textChanged() and iconChanged() signals of previously visible message
215 disconnect(sender: m_currentMessage, signal: &KTextEditor::Message::textChanged, receiver: m_messageWidget, slot: &KMessageWidget::setText);
216 disconnect(sender: m_currentMessage, signal: &KTextEditor::Message::iconChanged, receiver: m_messageWidget, slot: &KMessageWidget::setIcon);
217
218 m_currentMessage = nullptr;
219 m_animation->hide();
220 } else {
221 showNextMessage();
222 }
223 }
224}
225
226void KateMessageWidget::messageDestroyed(KTextEditor::Message *message)
227{
228 // last moment when message is valid, since KTE::Message is already in
229 // destructor we have to do the following:
230 // 1. remove message from m_messageQueue, so we don't care about it anymore
231 // 2. activate hide animation or show a new message()
232
233 // remove widget from m_messageQueue
234 int i = 0;
235 for (; i < m_messageQueue.count(); ++i) {
236 if (m_messageQueue[i] == message) {
237 break;
238 }
239 }
240
241 // the message must be in the list
242 Q_ASSERT(i < m_messageQueue.count());
243
244 // remove message
245 m_messageQueue.removeAt(i);
246
247 // remove message from hash -> release QActions
248 Q_ASSERT(m_messageHash.contains(message));
249 m_messageHash.remove(key: message);
250
251 // if deleted message is the current message, launch hide animation
252 if (message == m_currentMessage) {
253 m_currentMessage = nullptr;
254 m_animation->hide();
255 }
256}
257
258void KateMessageWidget::startAutoHideTimer()
259{
260 // message does not want autohide, or timer already running
261 if (!m_currentMessage // no message, nothing to do
262 || m_autoHideTime < 0 // message does not want auto-hide
263 || m_autoHideTimer->isActive() // auto-hide timer is already active
264 || m_animation->isHideAnimationRunning() // widget is in hide animation phase
265 || m_animation->isShowAnimationRunning() // widget is in show animation phase
266 ) {
267 return;
268 }
269
270 // safety checks: the message must still be valid
271 Q_ASSERT(m_messageQueue.size());
272 Q_ASSERT(m_currentMessage->autoHide() == m_autoHideTime);
273
274 // start autoHide timer as requested
275 m_autoHideTimer->start(msec: m_autoHideTime == 0 ? s_defaultAutoHideTime : m_autoHideTime);
276}
277
278void KateMessageWidget::linkHovered(const QString &link)
279{
280 QToolTip::showText(pos: QCursor::pos(), text: link, w: m_messageWidget);
281}
282
283QString KateMessageWidget::text() const
284{
285 return m_messageWidget->text();
286}
287
288#include "moc_katemessagewidget.cpp"
289

source code of ktexteditor/src/view/katemessagewidget.cpp