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 | |
22 | static const int s_defaultAutoHideTime = 6 * 1000; |
23 | |
24 | KateMessageWidget::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 | |
55 | void 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 | |
70 | void 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 | |
141 | void 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 | |
182 | void 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 | |
226 | void 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 | |
258 | void 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 | |
278 | void KateMessageWidget::linkHovered(const QString &link) |
279 | { |
280 | QToolTip::showText(pos: QCursor::pos(), text: link, w: m_messageWidget); |
281 | } |
282 | |
283 | QString KateMessageWidget::text() const |
284 | { |
285 | return m_messageWidget->text(); |
286 | } |
287 | |
288 | #include "moc_katemessagewidget.cpp" |
289 | |