1 /*
2  * SPDX-FileCopyrightText: 2021~2021 CSSlayer <wengxt@gmail.com>
3  *
4  * SPDX-License-Identifier: BSD-3-Clause
5  *
6  */
7 #include "fcitxtheme.h"
8 #include "font.h"
9 #include <QDebug>
10 #include <QMargins>
11 #include <QPixmap>
12 #include <QSettings>
13 #include <QStandardPaths>
14 
15 namespace fcitx {
16 
readBool(const QSettings & settings,const QString & name,bool defaultValue)17 bool readBool(const QSettings &settings, const QString &name,
18               bool defaultValue) {
19     return settings.value(name, defaultValue ? "True" : "False").toString() ==
20            "True";
21 }
22 
readMargin(const QSettings & settings)23 QMargins readMargin(const QSettings &settings) {
24     settings.allKeys();
25     return QMargins(settings.value("Left", 0).toInt(),
26                     settings.value("Top", 0).toInt(),
27                     settings.value("Right", 0).toInt(),
28                     settings.value("Bottom", 0).toInt());
29 }
30 
readColor(const QSettings & settings,const QString & name,const QString & defaultValue)31 QColor readColor(const QSettings &settings, const QString &name,
32                  const QString &defaultValue) {
33     QString colorString = settings.value(name, defaultValue).toString();
34     QColor color;
35     color.setNamedColor(defaultValue);
36     if (colorString.startsWith("#")) {
37         if (colorString.size() == 7) {
38             // Parse #RRGGBB
39             color.setNamedColor(colorString.toUpper());
40         } else if (colorString.size() == 9) {
41             // Qt accept "#AARRGGBB"
42             auto newColorString =
43                 QString("#%1%2")
44                     .arg(colorString.mid(7, 2), colorString.mid(1, 6))
45                     .toUpper();
46             color.setNamedColor(newColorString);
47         }
48     }
49     return color;
50 }
51 
load(const QString & name,QSettings & settings)52 void BackgroundImage::load(const QString &name, QSettings &settings) {
53     settings.allKeys();
54     image_ = QPixmap();
55     overlay_ = QPixmap();
56     if (auto image = settings.value("Image").toString(); !image.isEmpty()) {
57         auto file = QStandardPaths::locate(
58             QStandardPaths::GenericDataLocation,
59             QString("fcitx5/themes/%1/%2").arg(name, image));
60         image_.load(file);
61     }
62     if (auto image = settings.value("Overlay").toString(); !image.isEmpty()) {
63         auto file = QStandardPaths::locate(
64             QStandardPaths::GenericDataLocation,
65             QString("fcitx5/themes/%1/%2").arg(name, image));
66         overlay_.load(file);
67     }
68 
69     settings.beginGroup("Margin");
70     margin_ = readMargin(settings);
71     settings.endGroup();
72 
73     if (image_.isNull()) {
74         QColor color = readColor(settings, "Color", "#ffffff");
75         QColor borderColor = readColor(settings, "BorderColor", "#00ffffff");
76         int borderWidth = settings.value("BorderWidth", 0).toInt();
77         fillBackground(borderColor, color, borderWidth);
78     }
79 
80     settings.beginGroup("OverlayClipMargin");
81     overlayClipMargin_ = readMargin(settings);
82     settings.endGroup();
83 
84     hideOverlayIfOversize_ =
85         settings.value("HideOverlayIfOversize", "False").toString() == "True";
86     overlayOffsetX_ = settings.value("OverlayOffsetX", 0).toInt();
87     overlayOffsetY_ = settings.value("OverlayOffsetY", 0).toInt();
88     gravity_ = settings.value("Gravity", "TopLeft").toString();
89 }
90 
loadFromValue(const QColor & border,const QColor & background,QMargins margin,int borderWidth)91 void BackgroundImage::loadFromValue(const QColor &border,
92                                     const QColor &background, QMargins margin,
93                                     int borderWidth) {
94     image_ = QPixmap();
95     overlay_ = QPixmap();
96     margin_ = margin;
97     fillBackground(border, background, borderWidth);
98     overlayClipMargin_ = QMargins();
99     hideOverlayIfOversize_ = false;
100     overlayOffsetX_ = 0;
101     overlayOffsetY_ = 0;
102     gravity_ = QString();
103 }
104 
fillBackground(const QColor & border,const QColor & background,int borderWidth)105 void BackgroundImage::fillBackground(const QColor &border,
106                                      const QColor &background,
107                                      int borderWidth) {
108     image_ = QPixmap(margin_.left() + margin_.right() + 1,
109                      margin_.top() + margin_.bottom() + 1);
110     borderWidth = std::min({borderWidth, margin_.left(), margin_.right(),
111                             margin_.top(), margin_.bottom()});
112     borderWidth = std::max(0, borderWidth);
113 
114     QPainter painter;
115     painter.begin(&image_);
116     painter.setCompositionMode(QPainter::CompositionMode_Source);
117     if (borderWidth) {
118         painter.fillRect(image_.rect(), border);
119     }
120     painter.fillRect(QRect(borderWidth, borderWidth,
121                            image_.width() - borderWidth * 2,
122                            image_.height() - borderWidth * 2),
123                      background);
124     painter.end();
125 }
126 
load(const QString & name,QSettings & settings)127 void ActionImage::load(const QString &name, QSettings &settings) {
128     settings.allKeys();
129     image_ = QPixmap();
130     valid_ = false;
131     if (auto image = settings.value("Image").toString(); !image.isEmpty()) {
132         auto file = QStandardPaths::locate(
133             QStandardPaths::GenericDataLocation,
134             QString("fcitx5/themes/%1/%2").arg(name, image));
135         image_.load(file);
136         valid_ = !image_.isNull();
137     }
138 
139     settings.beginGroup("ClickMargin");
140     margin_ = readMargin(settings);
141     settings.endGroup();
142 }
143 
reset()144 void ActionImage::reset() {
145     image_ = QPixmap();
146     valid_ = false;
147     margin_ = QMargins(0, 0, 0, 0);
148 }
149 
FcitxTheme(QObject * parent)150 FcitxTheme::FcitxTheme(QObject *parent)
151     : QObject(parent), configPath_(QStandardPaths::writableLocation(
152                                        QStandardPaths::GenericConfigLocation)
153                                        .append("/fcitx5/conf/classicui.conf")),
154       watcher_(new QFileSystemWatcher) {
155     connect(watcher_, &QFileSystemWatcher::fileChanged, this,
156             &FcitxTheme::configChanged);
157     watcher_->addPath(configPath_);
158 
159     configChanged();
160 }
161 
~FcitxTheme()162 FcitxTheme::~FcitxTheme() {}
163 
configChanged()164 void FcitxTheme::configChanged() {
165     // Since fcitx is doing things like delete and move, we need to re-add the
166     // path.
167     watcher_->removePath(configPath_);
168     watcher_->addPath(configPath_);
169     QSettings settings(configPath_, QSettings::IniFormat);
170     settings.childGroups();
171     font_ = parseFont(settings.value("Font", "Sans Serif 9").toString());
172     fontMetrics_ = QFontMetrics(font_);
173     vertical_ =
174         settings.value("Vertical Candidate List", "False").toString() == "True";
175     wheelForPaging_ =
176         settings.value("WheelForPaging", "True").toString() == "True";
177     theme_ = settings.value("Theme", "default").toString();
178 
179     themeChanged();
180 }
181 
themeChanged()182 void FcitxTheme::themeChanged() {
183     if (!themeConfigPath_.isEmpty()) {
184         watcher_->removePath(themeConfigPath_);
185     }
186     auto themeConfig = QString("/fcitx5/themes/%1/theme.conf").arg(theme_);
187     themeConfigPath_ =
188         QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation)
189             .append(themeConfig);
190     auto file = QStandardPaths::locate(QStandardPaths::GenericDataLocation,
191                                        themeConfig);
192     if (file.isEmpty()) {
193         file = QStandardPaths::locate(QStandardPaths::GenericDataLocation,
194                                       "fcitx5/themes/default/theme.conf");
195         themeConfigPath_ = QStandardPaths::writableLocation(
196                                QStandardPaths::GenericDataLocation)
197                                .append("fcitx5/themes/default/theme.conf");
198         theme_ = "default";
199     }
200 
201     watcher_->addPath(themeConfigPath_);
202 
203     // We can not locate default theme.
204     if (file.isEmpty()) {
205         normalColor_.setNamedColor("#000000");
206         highlightCandidateColor_.setNamedColor("#ffffff");
207         fullWidthHighlight_ = true;
208         highlightColor_.setNamedColor("#ffffff");
209         highlightBackgroundColor_.setNamedColor("#a5a5a5");
210         contentMargin_ = QMargins{2, 2, 2, 2};
211         textMargin_ = QMargins{5, 5, 5, 5};
212         highlightClickMargin_ = QMargins{0, 0, 0, 0};
213         background_.loadFromValue(highlightBackgroundColor_, highlightColor_,
214                                   contentMargin_, 2);
215         highlight_.loadFromValue(highlightBackgroundColor_,
216                                  highlightBackgroundColor_, textMargin_, 0);
217         prev_.reset();
218         next_.reset();
219         return;
220     }
221 
222     QSettings settings(file, QSettings::IniFormat);
223     settings.childGroups();
224     settings.beginGroup("InputPanel");
225     normalColor_ = readColor(settings, "NormalColor", "#000000");
226     highlightCandidateColor_ =
227         readColor(settings, "HighlightCandidateColor", "#ffffff");
228     fullWidthHighlight_ = readBool(settings, "FullWidthHighlight", true);
229     highlightColor_ = readColor(settings, "HighlightColor", "#ffffff");
230     highlightBackgroundColor_ =
231         readColor(settings, "HighlightColor", "#a5a5a5");
232 
233     settings.beginGroup("ContentMargin");
234     contentMargin_ = readMargin(settings);
235     settings.endGroup();
236     settings.beginGroup("TextMargin");
237     textMargin_ = readMargin(settings);
238     settings.endGroup();
239 
240     settings.beginGroup("Background");
241     background_.load(theme_, settings);
242     settings.endGroup();
243 
244     settings.beginGroup("Highlight");
245     highlight_.load(theme_, settings);
246     settings.beginGroup("HighlightClickMargin");
247     highlightClickMargin_ = readMargin(settings);
248     settings.endGroup();
249     settings.endGroup();
250 
251     settings.beginGroup("PrevPage");
252     prev_.load(theme_, settings);
253     settings.endGroup();
254 
255     settings.beginGroup("NextPage");
256     next_.load(theme_, settings);
257     settings.endGroup();
258 }
259 
260 } // namespace fcitx
261 
paint(QPainter * painter,const fcitx::BackgroundImage & image,QRect region)262 void fcitx::FcitxTheme::paint(QPainter *painter,
263                               const fcitx::BackgroundImage &image,
264                               QRect region) {
265     auto marginTop = image.margin_.top();
266     auto marginBottom = image.margin_.bottom();
267     auto marginLeft = image.margin_.left();
268     auto marginRight = image.margin_.right();
269     int resizeHeight = image.image_.height() - marginTop - marginBottom;
270     int resizeWidth = image.image_.width() - marginLeft - marginRight;
271 
272     if (resizeHeight <= 0) {
273         resizeHeight = 1;
274     }
275 
276     if (resizeWidth <= 0) {
277         resizeWidth = 1;
278     }
279 
280     if (region.height() < 0) {
281         region.setHeight(resizeHeight);
282     }
283 
284     if (region.width() < 0) {
285         region.setWidth(resizeWidth);
286     }
287 
288     /*
289      * 7 8 9
290      * 4 5 6
291      * 1 2 3
292      */
293 
294     if (marginLeft && marginBottom) {
295         /* part 1 */
296         painter->drawPixmap(
297             QRect(0, region.height() - marginBottom, marginLeft, marginBottom)
298                 .translated(region.topLeft()),
299             image.image_,
300             QRect(0, marginTop + resizeHeight, marginLeft, marginBottom));
301     }
302 
303     if (marginRight && marginBottom) {
304         /* part 3 */
305         painter->drawPixmap(
306             QRect(region.width() - marginRight, region.height() - marginBottom,
307                   marginRight, marginBottom)
308                 .translated(region.topLeft()),
309             image.image_,
310             QRect(marginLeft + resizeWidth, marginTop + resizeHeight,
311                   marginRight, marginBottom));
312     }
313 
314     if (marginLeft && marginTop) {
315         /* part 7 */
316         painter->drawPixmap(
317             QRect(0, 0, marginLeft, marginTop).translated(region.topLeft()),
318             image.image_, QRect(0, 0, marginLeft, marginTop));
319     }
320 
321     if (marginRight && marginTop) {
322         /* part 9 */
323         painter->drawPixmap(
324             QRect(region.width() - marginRight, 0, marginRight, marginTop)
325                 .translated(region.topLeft()),
326             image.image_,
327             QRect(marginLeft + resizeWidth, 0, marginRight, marginTop));
328     }
329 
330     /* part 2 & 8 */
331     if (marginTop) {
332         painter->drawPixmap(
333             QRect(marginLeft, 0, region.width() - marginLeft - marginRight,
334                   marginTop)
335                 .translated(region.topLeft()),
336             image.image_, QRect(marginLeft, 0, resizeWidth, marginTop));
337     }
338 
339     if (marginBottom) {
340         painter->drawPixmap(QRect(marginLeft, region.height() - marginBottom,
341                                   region.width() - marginLeft - marginRight,
342                                   marginBottom)
343                                 .translated(region.topLeft()),
344                             image.image_,
345                             QRect(marginLeft, marginTop + resizeHeight,
346                                   resizeWidth, marginBottom));
347     }
348 
349     /* part 4 & 6 */
350     if (marginLeft) {
351         painter->drawPixmap(QRect(0, marginTop, marginLeft,
352                                   region.height() - marginTop - marginBottom)
353                                 .translated(region.topLeft()),
354                             image.image_,
355                             QRect(0, marginTop, marginLeft, resizeHeight));
356     }
357 
358     if (marginRight) {
359         painter->drawPixmap(QRect(region.width() - marginRight, marginTop,
360                                   marginRight,
361                                   region.height() - marginTop - marginBottom)
362                                 .translated(region.topLeft()),
363                             image.image_,
364                             QRect(marginLeft + resizeWidth, marginTop,
365                                   marginRight, resizeHeight));
366     }
367 
368     /* part 5 */
369     {
370         painter->drawPixmap(
371             QRect(marginLeft, marginTop,
372                   region.width() - marginLeft - marginRight,
373                   region.height() - marginTop - marginBottom)
374                 .translated(region.topLeft()),
375             image.image_,
376             QRect(marginLeft, marginTop, resizeWidth, resizeHeight));
377     }
378 
379     if (image.overlay_.isNull()) {
380         return;
381     }
382 
383     auto clipWidth = region.width() - image.overlayClipMargin_.left() -
384                      image.overlayClipMargin_.right();
385     auto clipHeight = region.height() - image.overlayClipMargin_.top() -
386                       image.overlayClipMargin_.bottom();
387     if (clipWidth <= 0 || clipHeight <= 0) {
388         return;
389     }
390     QRect clipRect(region.topLeft() + QPoint(image.overlayClipMargin_.left(),
391                                              image.overlayClipMargin_.top()),
392                    QSize(clipWidth, clipHeight));
393 
394     int x = 0, y = 0;
395     if (image.gravity_ == "Top Left" || image.gravity_ == "Center Left" ||
396         image.gravity_ == "Bottom Left") {
397         x = image.overlayOffsetX_;
398     } else if (image.gravity_ == "Top Center" || image.gravity_ == "Center" ||
399                image.gravity_ == "Bottom Center") {
400         x = (region.width() - image.overlay_.width()) / 2 +
401             image.overlayOffsetX_;
402     } else {
403         x = region.width() - image.overlay_.width() - image.overlayOffsetX_;
404     }
405 
406     if (image.gravity_ == "Top Left" || image.gravity_ == "Top Center" ||
407         image.gravity_ == "Top Right") {
408         y = image.overlayOffsetY_;
409     } else if (image.gravity_ == "Center Left" || image.gravity_ == "Center" ||
410                image.gravity_ == "Center Right") {
411         y = (region.height() - image.overlay_.height()) / 2 +
412             image.overlayOffsetY_;
413     } else {
414         y = region.height() - image.overlay_.height() - image.overlayOffsetY_;
415     }
416     QRect rect(QPoint(x, y) + region.topLeft(), image.overlay_.size());
417     QRect finalRect = rect.intersected(clipRect);
418     if (finalRect.isEmpty()) {
419         return;
420     }
421 
422     if (image.hideOverlayIfOversize_ && !clipRect.contains(rect)) {
423         return;
424     }
425 
426     painter->save();
427     painter->setClipRect(clipRect);
428     painter->drawPixmap(rect, image.overlay_);
429     painter->restore();
430 }
431 
paint(QPainter * painter,const fcitx::ActionImage & image,QPoint position,float alpha)432 void fcitx::FcitxTheme::paint(QPainter *painter,
433                               const fcitx::ActionImage &image, QPoint position,
434                               float alpha) {
435     painter->save();
436     painter->setOpacity(alpha);
437     painter->drawPixmap(position, image.image_);
438     painter->restore();
439 }
440 
highlightMargin() const441 QMargins fcitx::FcitxTheme::highlightMargin() const {
442     return highlight_.margin_;
443 }
444