1 /*
2 SPDX-FileCopyrightText: 2020 Michail Vourlakos <mvourlakos@gmail.com>
3 SPDX-License-Identifier: GPL-2.0-or-later
4 */
5
6 #include "panelbackground.h"
7
8 // local
9 #include "theme.h"
10
11 // Qt
12 #include <QDebug>
13 #include <QImage>
14 #include <QtGlobal>
15
16 #define CENTERWIDTH 100
17 #define CENTERHEIGHT 50
18
19 #define BASELINESHADOWTHRESHOLD 5
20
21 namespace Latte {
22 namespace PlasmaExtended {
23
PanelBackground(Plasma::Types::Location edge,Theme * parent)24 PanelBackground::PanelBackground(Plasma::Types::Location edge, Theme *parent)
25 : QObject(parent),
26 m_location(edge),
27 m_parentTheme(parent)
28 {
29 }
30
~PanelBackground()31 PanelBackground::~PanelBackground()
32 {
33 }
34
hasMask(Plasma::Svg * svg) const35 bool PanelBackground::hasMask(Plasma::Svg *svg) const
36 {
37 if (!svg) {
38 return false;
39 }
40
41 return svg->hasElement("mask-topleft");
42 }
43
paddingTop() const44 int PanelBackground::paddingTop() const
45 {
46 return m_paddingTop;
47 }
48
paddingLeft() const49 int PanelBackground::paddingLeft() const
50 {
51 return m_paddingLeft;
52 }
53
paddingBottom() const54 int PanelBackground::paddingBottom() const
55 {
56 return m_paddingBottom;
57 }
58
paddingRight() const59 int PanelBackground::paddingRight() const
60 {
61 return m_paddingRight;
62 }
63
roundness() const64 int PanelBackground::roundness() const
65 {
66 return m_roundness;
67 }
68
shadowSize() const69 int PanelBackground::shadowSize() const
70 {
71 return m_shadowSize;
72 }
73
maxOpacity() const74 float PanelBackground::maxOpacity() const
75 {
76 return m_maxOpacity;
77 }
78
shadowColor() const79 QColor PanelBackground::shadowColor() const
80 {
81 return m_shadowColor;
82 }
83
prefixed(const QString & id)84 QString PanelBackground::prefixed(const QString &id)
85 {
86 if (m_location == Plasma::Types::TopEdge) {
87 return QString("north-"+id);
88 } else if (m_location == Plasma::Types::LeftEdge) {
89 return QString("west-"+id);
90 } else if (m_location == Plasma::Types::BottomEdge) {
91 return QString("south-"+id);
92 } else if (m_location == Plasma::Types::RightEdge) {
93 return QString("east-"+id);
94 }
95
96 return id;
97 }
98
element(Plasma::Svg * svg,const QString & id)99 QString PanelBackground::element(Plasma::Svg *svg, const QString &id)
100 {
101 if (!svg) {
102 return "";
103 }
104
105 if (svg->hasElement(prefixed(id))) {
106 return prefixed(id);
107 }
108
109 if (svg->hasElement(id)) {
110 return id;
111 }
112
113 return "";
114 }
115
updateMaxOpacity(Plasma::Svg * svg)116 void PanelBackground::updateMaxOpacity(Plasma::Svg *svg)
117 {
118 if (!svg) {
119 return;
120 }
121
122 QImage center = svg->image(QSize(CENTERWIDTH, CENTERHEIGHT), element(svg, "center"));
123
124 if (center.format() != QImage::Format_ARGB32_Premultiplied) {
125 #if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
126 center.convertTo(QImage::Format_ARGB32_Premultiplied);
127 #else
128 center = center.convertToFormat(QImage::Format_ARGB32_Premultiplied);
129 #endif
130 }
131
132 float alphasum{0};
133
134 //! calculating the mid opacity (this is needed in order to handle Oxygen
135 //! that has different opacity levels in the same center element)
136 for (int row=0; row<2; ++row) {
137 QRgb *line = (QRgb *)center.scanLine(row);
138
139 for (int col=0; col<CENTERWIDTH; ++col) {
140 QRgb pixelData = line[col];
141 alphasum += ((float)qAlpha(pixelData)/(float)255);
142 }
143 }
144
145 m_maxOpacity = alphasum / (float)(2 * CENTERWIDTH);
146
147 emit maxOpacityChanged();
148 }
149
updatePaddings(Plasma::Svg * svg)150 void PanelBackground::updatePaddings(Plasma::Svg *svg)
151 {
152 if (!svg) {
153 return;
154 }
155
156 m_paddingTop = svg->elementSize(element(svg, "top")).height();
157 m_paddingLeft = svg->elementSize(element(svg, "left")).width();
158 m_paddingBottom = svg->elementSize(element(svg, "bottom")).height();
159 m_paddingRight = svg->elementSize(element(svg, "right")).width();
160
161 emit paddingsChanged();
162 }
163
updateRoundnessFromMask(Plasma::Svg * svg)164 void PanelBackground::updateRoundnessFromMask(Plasma::Svg *svg)
165 {
166 if (!svg) {
167 return;
168 }
169
170 bool topLeftCorner = (m_location == Plasma::Types::BottomEdge || m_location == Plasma::Types::RightEdge);
171
172 QString cornerId = (topLeftCorner ? "mask-topleft" : "mask-bottomright");
173 QImage corner = svg->image(svg->elementSize(cornerId), cornerId);
174
175 if (corner.format() != QImage::Format_ARGB32_Premultiplied) {
176 #if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
177 corner.convertTo(QImage::Format_ARGB32_Premultiplied);
178 #else
179 corner = corner.convertToFormat(QImage::Format_ARGB32_Premultiplied);
180 #endif
181 }
182
183 int baseRow = (topLeftCorner ? corner.height()-1 : 0);
184 int baseCol = (topLeftCorner ? corner.width()-1 : 0);
185
186 int baseLineLength = 0;
187 int roundnessLines = 0;
188
189 if (topLeftCorner) {
190 //! TOPLEFT corner
191 QRgb *line = (QRgb *)corner.scanLine(baseRow);
192 QRgb basePoint = line[baseCol];
193
194 QRgb *isRoundedLine = (QRgb *)corner.scanLine(0);
195 QRgb isRoundedPoint = isRoundedLine[0];
196
197 //! If there is roundness, if that point is not fully transparent then
198 //! there is no roundness
199 if (qAlpha(isRoundedPoint) == 0) {
200
201 if (qAlpha(basePoint) > 0) {
202 //! calculate the mask baseLine length
203 for(int c = baseCol; c>=0; --c) {
204 QRgb *l = (QRgb *)corner.scanLine(baseRow);
205 QRgb point = line[c];
206
207 if (qAlpha(point) > 0) {
208 baseLineLength ++;
209 } else {
210 break;
211 }
212 }
213 }
214
215 qDebug() << " TOP LEFT CORNER MASK base line length :: " << baseLineLength;
216
217 if (baseLineLength>0) {
218 int headLimitR = baseRow;
219 int tailLimitR = baseRow;
220
221 for (int r = baseRow-1; r>=0; --r) {
222 QRgb *line = (QRgb *)corner.scanLine(r);
223 QRgb fpoint = line[baseCol];
224 if (qAlpha(fpoint) == 0) {
225 //! a line that is not part of the roundness because its first pixel is fully transparent
226 break;
227 }
228
229 headLimitR = r;
230 }
231
232 int c = qMax(0, corner.width() - baseLineLength);
233
234 for (int r = baseRow-1; r>=0; --r) {
235 QRgb *line = (QRgb *)corner.scanLine(r);
236 QRgb point = line[c];
237
238 if (qAlpha(point) != 255) {
239 tailLimitR = r;
240 break;
241 }
242 }
243
244 //qDebug() << " -> calculations: " << ", tail row :" << tailLimitR << " | head row: " << headLimitR;
245
246 if (headLimitR != tailLimitR) {
247 roundnessLines = tailLimitR - headLimitR + 1;
248 }
249 }
250 }
251 } else {
252 //! BOTTOMRIGHT CORNER
253 //! it should be TOPRIGHT corner in that case
254 QRgb *line = (QRgb *)corner.scanLine(baseRow);
255 QRgb basePoint = line[baseCol];
256
257 QRgb *isRoundedLine = (QRgb *)corner.scanLine(corner.height()-1);
258 QRgb isRoundedPoint = isRoundedLine[corner.width()-1];
259
260 //! If there is roundness, if that point is not fully transparent then
261 //! there is no roundness
262 if (qAlpha(isRoundedPoint) == 0) {
263
264 if (qAlpha(basePoint) > 0) {
265 //! calculate the mask baseLine length
266 for(int c = baseCol; c<corner.width(); ++c) {
267 QRgb *l = (QRgb *)corner.scanLine(baseRow);
268 QRgb point = line[c];
269
270 if (qAlpha(point) > 0) {
271 baseLineLength ++;
272 } else {
273 break;
274 }
275 }
276 }
277
278 qDebug() << " BOTTOM RIGHT CORNER MASK base line length :: " << baseLineLength;
279
280 if (baseLineLength>0) {
281 int headLimitR = 0;
282 int tailLimitR = 0;
283
284 for (int r = baseRow+1; r<=corner.height(); ++r) {
285 QRgb *line = (QRgb *)corner.scanLine(r);
286 QRgb fpoint = line[baseCol];
287 if (qAlpha(fpoint) == 0) {
288 //! a line that is not part of the roundness because its first pixel is not trasparent
289 break;
290 }
291
292 headLimitR = r;
293 }
294
295 int c = baseLineLength - 1;
296
297 for (int r = baseRow+1; r<=corner.height(); ++r) {
298 QRgb *line = (QRgb *)corner.scanLine(r);
299 QRgb point = line[c];
300
301 if (qAlpha(point) != 255) {
302 tailLimitR = r;
303 break;
304 }
305 }
306
307 //qDebug() << " -> calculations: " << ", tail row :" << tailLimitR << " | head row: " << headLimitR;
308
309 if (headLimitR != tailLimitR) {
310 roundnessLines = headLimitR - tailLimitR + 1;
311 }
312 }
313 }
314 }
315
316 m_roundness = roundnessLines;
317 emit roundnessChanged();
318 }
319
320
321
updateRoundnessFromShadows(Plasma::Svg * svg)322 void PanelBackground::updateRoundnessFromShadows(Plasma::Svg *svg)
323 {
324 //! 1. Algorithm is choosing which corner shadow based on panel location
325 //! 2. For that corner discovers the maxOpacity (most solid shadow point) and
326 //! how pixels (distance) is to the most solid point, that is called [baseLineLength]
327 //! 3. After [2] the algorigthm for each next line calculates the maxOpacity
328 //! for that line and how many points are needed to reach there. If the points
329 //! to reach the line max opacity are shorter than baseLineLength then that line
330 //! is considered part of the roundness
331 //! 3.1 Avoid zig-zag cases such as the Air plasma theme case. When the shadow is not
332 //! following a straight line until reaching the rounded part the algorithm is
333 //! considering as valid roundness only the last part of the discovered roundness and
334 //! ignores all the previous.
335 //! 4. Calculating the lines that are shorter than the baseline provides
336 //! the discovered roundness
337
338 if (!svg) {
339 return;
340 }
341
342 bool topLeftCorner = (m_location == Plasma::Types::BottomEdge || m_location == Plasma::Types::RightEdge);
343
344 QString cornerId = (topLeftCorner ? "shadow-topleft" : "shadow-bottomright");
345 QImage corner = svg->image(svg->elementSize(cornerId), cornerId);
346
347 if (corner.format() != QImage::Format_ARGB32_Premultiplied) {
348 #if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
349 corner.convertTo(QImage::Format_ARGB32_Premultiplied);
350 #else
351 corner = corner.convertToFormat(QImage::Format_ARGB32_Premultiplied);
352 #endif
353 }
354
355 int baseRow = (topLeftCorner ? corner.height()-1 : 0);
356 int baseCol = (topLeftCorner ? corner.width()-1 : 0);
357
358 int baseLineLength = 0;
359 int roundnessLines = 0;
360
361 if (topLeftCorner) {
362 //! TOPLEFT corner
363 QRgb *line = (QRgb *)corner.scanLine(baseRow);
364 QRgb basePoint = line[baseCol];
365
366 int baseShadowMaxOpacity = 0;
367
368 if (qAlpha(basePoint) == 0) {
369 //! calculate the shadow maxOpacity in the base line
370 //! and number of pixels to reach there
371 for(int c = baseCol; c>=0; --c) {
372 QRgb *l = (QRgb *)corner.scanLine(baseRow);
373 QRgb point = line[c];
374
375 if (qAlpha(point) > baseShadowMaxOpacity) {
376 baseShadowMaxOpacity = qAlpha(point);
377 baseLineLength = (baseCol - c + 1);
378 }
379 }
380 }
381
382 qDebug() << " TOP LEFT CORNER SHADOW base line length :: " << baseLineLength << " with max shadow opacity : " << baseShadowMaxOpacity;
383
384 if (baseLineLength>0) {
385 for (int r = baseRow-1; r>=0; --r) {
386 QRgb *line = (QRgb *)corner.scanLine(r);
387 QRgb fpoint = line[baseCol];
388 if (qAlpha(fpoint) != 0) {
389 //! a line that is not part of the roundness because its first pixel is not trasparent
390 break;
391 }
392
393 int transPixels = 0;
394 int rowMaxOpacity = 0;
395
396 for(int c = baseCol; c>=0; --c) {
397 QRgb *l = (QRgb *)corner.scanLine(r);
398 QRgb point = line[c];
399
400 if (qAlpha(point) > rowMaxOpacity) {
401 rowMaxOpacity = qAlpha(point);
402 continue;
403 }
404 }
405
406 for(int c = baseCol; c>=(baseCol - baseLineLength + 1); --c) {
407 QRgb *l = (QRgb *)corner.scanLine(r);
408 QRgb point = line[c];
409
410 if (qAlpha(point) != rowMaxOpacity) {
411 transPixels++;
412 continue;
413 }
414
415 if (transPixels != baseLineLength) {
416 roundnessLines++;
417 break;
418 }
419 }
420
421 if (transPixels == baseLineLength) {
422 //! 3.1 avoid zig-zag shadows Air plasma theme case
423 roundnessLines = 0;
424 }
425
426 //qDebug() << " -> line: " << r << ", low transparency pixels :" << transPixels << " | " << " rowMaxOpacity :"<< rowMaxOpacity << ", " << (transPixels != baseLineLength);
427 }
428 }
429 } else {
430 //! BOTTOMRIGHT CORNER
431 //! it should be TOPRIGHT corner in that case
432 QRgb *line = (QRgb *)corner.scanLine(baseRow);
433 QRgb basePoint = line[baseCol];
434
435 int baseShadowMaxOpacity = 0;
436
437 if (qAlpha(basePoint) == 0) {
438 //! calculate the base line transparent pixels
439 for(int c = baseCol; c<corner.width(); ++c) {
440 QRgb *l = (QRgb *)corner.scanLine(baseRow);
441 QRgb point = line[c];
442
443 if (qAlpha(point) > baseShadowMaxOpacity) {
444 baseShadowMaxOpacity = qAlpha(point);
445 baseLineLength = c + 1;
446 }
447 }
448 }
449
450 qDebug() << " BOTTOM RIGHT CORNER SHADOW base line length :: " << baseLineLength << " with max shadow opacity : " << baseShadowMaxOpacity;
451
452 if (baseLineLength>0) {
453 for (int r = baseRow+1; r<=corner.height(); ++r) {
454 QRgb *line = (QRgb *)corner.scanLine(r);
455 QRgb fpoint = line[baseCol];
456 if (qAlpha(fpoint) != 0) {
457 //! a line that is not part of the roundness because its first pixel is not trasparent
458 break;
459 }
460
461 int transPixels = 0;
462 int rowMaxOpacity = 0;
463
464 for(int c = baseCol; c<corner.width(); ++c) {
465 QRgb *l = (QRgb *)corner.scanLine(r);
466 QRgb point = line[c];
467
468 if (qAlpha(point) > rowMaxOpacity) {
469 rowMaxOpacity = qAlpha(point);
470 baseLineLength = c + 1;
471 }
472 }
473
474 for(int c = baseCol; c<baseLineLength; ++c) {
475 QRgb *l = (QRgb *)corner.scanLine(r);
476 QRgb point = line[c];
477
478 if (qAlpha(point) != rowMaxOpacity) {
479 transPixels++;
480 continue;
481 }
482
483 if (transPixels != baseLineLength) {
484 roundnessLines++;
485 break;
486 }
487 }
488
489 if (transPixels == baseLineLength) {
490 //! 3.1 avoid zig-zag shadows Air plasma theme case
491 roundnessLines = 0;
492 }
493
494 //qDebug() << " -> line: " << r << ", low transparency pixels :" << transPixels << " | " << " rowMaxOpacity :"<< rowMaxOpacity << ", " << (transPixels != baseLineLength);
495 }
496 }
497 }
498
499 m_roundness = roundnessLines;
500 emit roundnessChanged();
501 }
502
updateRoundnessFallback(Plasma::Svg * svg)503 void PanelBackground::updateRoundnessFallback(Plasma::Svg *svg)
504 {
505 if (!svg) {
506 return;
507 }
508
509 QString cornerId = element(svg, (m_location == Plasma::Types::LeftEdge ? "bottomright" : "topleft"));
510 QImage corner = svg->image(svg->elementSize(cornerId), cornerId);
511
512 if (corner.format() != QImage::Format_ARGB32_Premultiplied) {
513 #if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
514 corner.convertTo(QImage::Format_ARGB32_Premultiplied);
515 #else
516 corner = corner.convertToFormat(QImage::Format_ARGB32_Premultiplied);
517 #endif
518 }
519
520 int discovRow = (m_location == Plasma::Types::LeftEdge ? corner.height()-1 : 0);
521 int discovCol{0};
522 //int discovCol = (m_location == Plasma::Types::LeftEdge ? corner.width()-1 : 0);
523 int round{0};
524
525 int minOpacity = m_maxOpacity * 255;
526
527 if (m_location == Plasma::Types::BottomEdge || m_location == Plasma::Types::RightEdge || m_location == Plasma::Types::TopEdge) {
528 //! TOPLEFT corner
529 //! first LEFT pixel found
530 QRgb *line = (QRgb *)corner.scanLine(discovRow);
531
532 for (int col=0; col<corner.width() - 1; ++col) {
533 QRgb pixelData = line[col];
534
535 if (qAlpha(pixelData) < minOpacity) {
536 discovCol++;
537 round++;
538 } else {
539 break;
540 }
541 }
542 } else if (m_location == Plasma::Types::LeftEdge) {
543 //! it should be TOPRIGHT corner in that case
544 //! first RIGHT pixel found
545 QRgb *line = (QRgb *)corner.scanLine(discovRow);
546 for (int col=corner.width()-1; col>0; --col) {
547 QRgb pixelData = line[col];
548
549 if (qAlpha(pixelData) < minOpacity) {
550 discovCol--;
551 round++;
552 } else {
553 break;
554 }
555 }
556 }
557
558 m_roundness = round;
559 emit roundnessChanged();
560 }
561
updateShadow(Plasma::Svg * svg)562 void PanelBackground::updateShadow(Plasma::Svg *svg)
563 {
564 if (!svg) {
565 return;
566 }
567
568 if (!m_parentTheme->hasShadow()) {
569 m_shadowSize = 0;
570 m_shadowColor = Qt::black;
571 return;
572 }
573
574 bool horizontal = (m_location == Plasma::Types::BottomEdge || m_location == Plasma::Types::TopEdge);
575
576 QString borderId{"shadow-top"};
577
578 if (m_location == Plasma::Types::TopEdge) {
579 borderId = "shadow-bottom";
580 } else if (m_location == Plasma::Types::LeftEdge) {
581 borderId = "shadow-right";
582 } else if (m_location == Plasma::Types::RightEdge) {
583 borderId = "shadow-left";
584 }
585
586 QImage border = svg->image(svg->elementSize(borderId), borderId);
587
588 if (border.format() != QImage::Format_ARGB32_Premultiplied) {
589 #if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
590 border.convertTo(QImage::Format_ARGB32_Premultiplied);
591 #else
592 border = border.convertToFormat(QImage::Format_ARGB32_Premultiplied);
593 #endif
594 }
595
596 //! find shadow size through, plasma theme
597 int themeshadowsize{0};
598
599 if (m_location == Plasma::Types::TopEdge) {
600 themeshadowsize = svg->elementSize(element(svg, "shadow-hint-bottom-margin")).height();
601 } else if (m_location == Plasma::Types::LeftEdge) {
602 themeshadowsize = svg->elementSize(element(svg, "shadow-hint-right-margin")).width();
603 } else if (m_location == Plasma::Types::RightEdge) {
604 themeshadowsize = svg->elementSize(element(svg, "shadow-hint-left-margin")).width();
605 } else {
606 themeshadowsize = svg->elementSize(element(svg, "shadow-hint-top-margin")).height();
607 }
608
609 //! find shadow size through heuristics, elementsize provided through svg may not be valid because it could contain
610 //! many fully transparent pixels in its edges
611 int discoveredshadowsize{0};
612 int firstPixel{-1};
613 int lastPixel{-1};
614
615 if (horizontal) {
616 for(int y = 0; y<border.height(); ++y) {
617 QRgb *line = (QRgb *)border.scanLine(y);
618 QRgb pixel = line[0];
619
620 if (qAlpha(pixel) > 0) {
621 if (firstPixel < 0) {
622 firstPixel = y;
623 lastPixel = y;
624 } else {
625 lastPixel = y;
626 }
627 }
628 }
629 } else {
630 QRgb *line = (QRgb *)border.scanLine(0);
631 for(int x = 0; x<border.width(); ++x) {
632 QRgb pixel = line[x];
633
634 if (qAlpha(pixel) > 0) {
635 if (firstPixel < 0) {
636 firstPixel = x;
637 lastPixel = x;
638 } else {
639 lastPixel = x;
640 }
641 }
642 }
643 }
644
645 discoveredshadowsize = (firstPixel>=0 ? qMax(0, lastPixel - firstPixel + 1) : 0);
646
647 m_shadowSize = qMax(themeshadowsize, discoveredshadowsize);
648
649 //! find maximum shadow color applied
650 int maxopacity{0};
651
652 for (int r=0; r<border.height(); ++r) {
653 QRgb *line = (QRgb *)border.scanLine(r);
654
655 for(int c = 0; c<border.width(); ++c) {
656 QRgb pixel = line[c];
657
658 if (qAlpha(pixel) > maxopacity) {
659 maxopacity = qAlpha(pixel);
660 m_shadowColor = QColor(pixel);
661 m_shadowColor.setAlpha(qMin(255, maxopacity));
662 }
663 }
664 }
665 }
666
667
updateRoundness(Plasma::Svg * svg)668 void PanelBackground::updateRoundness(Plasma::Svg *svg)
669 {
670 if (!svg) {
671 return;
672 }
673
674 if (hasMask(svg)) {
675 qDebug() << "PLASMA THEME, calculating roundness from mask...";
676 updateRoundnessFromMask(svg);
677 } else if (m_parentTheme->hasShadow()) {
678 qDebug() << "PLASMA THEME, calculating roundness from shadows...";
679 updateRoundnessFromShadows(svg);
680 } else {
681 qDebug() << "PLASMA THEME, calculating roundness from fallback code...";
682 updateRoundnessFallback(svg);
683 }
684 }
685
update()686 void PanelBackground::update()
687 {
688 Plasma::Svg *backSvg = new Plasma::Svg(this);
689 backSvg->setImagePath(QStringLiteral("widgets/panel-background"));
690 backSvg->resize();
691
692 updateMaxOpacity(backSvg);
693 updatePaddings(backSvg);
694 updateRoundness(backSvg);
695 updateShadow(backSvg);
696
697 qDebug() << " PLASMA THEME EXTENDED :: " << m_location << " | roundness:" << m_roundness << " center_max_opacity:" << m_maxOpacity;
698 qDebug() << " PLASMA THEME EXTENDED :: " << m_location
699 << " | padtop:" << m_paddingTop << " padleft:" << m_paddingLeft
700 << " padbottom:" << m_paddingBottom << " padright:" << m_paddingRight;
701 qDebug() << " PLASMA THEME EXTENDED :: " << m_location << " | shadowsize:" << m_shadowSize << " shadowcolor:" << m_shadowColor;
702
703 backSvg->deleteLater();
704 }
705
706 }
707 }
708