1 | // Aseprite |
2 | // Copyright (C) 2019-2022 Igara Studio S.A. |
3 | // Copyright (C) 2001-2018 David Capello |
4 | // |
5 | // This program is distributed under the terms of |
6 | // the End-User License Agreement for Aseprite. |
7 | |
8 | #ifdef HAVE_CONFIG_H |
9 | #include "config.h" |
10 | #endif |
11 | |
12 | #include "app/ui/skin/skin_theme.h" |
13 | |
14 | #include "app/app.h" |
15 | #include "app/console.h" |
16 | #include "app/extensions.h" |
17 | #include "app/font_path.h" |
18 | #include "app/modules/gui.h" |
19 | #include "app/pref/preferences.h" |
20 | #include "app/resource_finder.h" |
21 | #include "app/ui/app_menuitem.h" |
22 | #include "app/ui/keyboard_shortcuts.h" |
23 | #include "app/ui/skin/font_data.h" |
24 | #include "app/ui/skin/skin_property.h" |
25 | #include "app/ui/skin/skin_slider_property.h" |
26 | #include "app/xml_document.h" |
27 | #include "app/xml_exception.h" |
28 | #include "base/fs.h" |
29 | #include "base/log.h" |
30 | #include "base/string.h" |
31 | #include "base/utf8_decode.h" |
32 | #include "gfx/border.h" |
33 | #include "gfx/point.h" |
34 | #include "gfx/rect.h" |
35 | #include "gfx/size.h" |
36 | #include "os/draw_text.h" |
37 | #include "os/font.h" |
38 | #include "os/surface.h" |
39 | #include "os/system.h" |
40 | #include "ui/intern.h" |
41 | #include "ui/ui.h" |
42 | |
43 | #include "tinyxml.h" |
44 | |
45 | #include <algorithm> |
46 | #include <cstring> |
47 | |
48 | #define BGCOLOR (getWidgetBgColor(widget)) |
49 | |
50 | namespace app { |
51 | namespace skin { |
52 | |
53 | using namespace gfx; |
54 | using namespace ui; |
55 | |
56 | // TODO For backward compatibility, in future versions we should remove this (extensions are preferred) |
57 | const char* SkinTheme::kThemesFolderName = "themes" ; |
58 | |
59 | // This class offer backward compatibility with old themes, completing |
60 | // or changing styles from the default theme to match the default |
61 | // theme of previous versions, so third-party themes can look like |
62 | // they are running in the old Aseprite without any modification. |
63 | struct app::skin::SkinTheme::BackwardCompatibility { |
64 | bool hasSliderStyle = false; |
65 | void notifyStyleExistence(const char* styleId) { |
66 | if (std::strcmp(styleId, "slider" ) == 0) |
67 | hasSliderStyle = true; |
68 | } |
69 | void createMissingStyles(SkinTheme* theme) { |
70 | if (!hasSliderStyle && |
71 | theme->styles.slider() && |
72 | theme->styles.miniSlider()) { |
73 | // Old slider style |
74 | ui::Style style(nullptr); |
75 | os::Font* font = theme->getDefaultFont(); |
76 | const int h = font->height(); |
77 | |
78 | style.setId(theme->styles.slider()->id()); |
79 | style.setFont(AddRef(font)); |
80 | |
81 | auto part = theme->parts.sliderEmpty(); |
82 | style.setBorder( |
83 | gfx::Border(part->bitmapW()->width()-1*guiscale(), |
84 | part->bitmapN()->height()+h/2, |
85 | part->bitmapE()->width()-1*guiscale(), |
86 | part->bitmapS()->height()-1*guiscale()+h/2)); |
87 | |
88 | *theme->styles.slider() = style; |
89 | *theme->styles.miniSlider() = style; |
90 | } |
91 | } |
92 | }; |
93 | |
94 | static const char* g_cursor_names[kCursorTypes] = { |
95 | "null" , // kNoCursor |
96 | "normal" , // kArrowCursor |
97 | "normal_add" , // kArrowPlusCursor |
98 | "crosshair" , // kCrosshairCursor |
99 | "forbidden" , // kForbiddenCursor |
100 | "hand" , // kHandCursor |
101 | "scroll" , // kScrollCursor |
102 | "move" , // kMoveCursor |
103 | "size_ns" , // kSizeNSCursor |
104 | "size_we" , // kSizeWECursor |
105 | "size_n" , // kSizeNCursor |
106 | "size_ne" , // kSizeNECursor |
107 | "size_e" , // kSizeECursor |
108 | "size_se" , // kSizeSECursor |
109 | "size_s" , // kSizeSCursor |
110 | "size_sw" , // kSizeSWCursor |
111 | "size_w" , // kSizeWCursor |
112 | "size_nw" , // kSizeNWCursor |
113 | }; |
114 | |
115 | static FontData* load_font(std::map<std::string, FontData*>& fonts, |
116 | const TiXmlElement* xmlFont, |
117 | const std::string& xmlFilename) |
118 | { |
119 | const char* fontRef = xmlFont->Attribute("font" ); |
120 | if (fontRef) { |
121 | auto it = fonts.find(fontRef); |
122 | if (it == fonts.end()) |
123 | throw base::Exception("Font named '%s' not found\n" , fontRef); |
124 | return it->second; |
125 | } |
126 | |
127 | const char* nameStr = xmlFont->Attribute("name" ); |
128 | if (!nameStr) |
129 | throw base::Exception("No \"name\" or \"font\" attributes specified on <font>" ); |
130 | |
131 | std::string name(nameStr); |
132 | |
133 | // Use cached font data |
134 | auto it = fonts.find(name); |
135 | if (it != fonts.end()) |
136 | return it->second; |
137 | |
138 | LOG(VERBOSE, "THEME: Loading font '%s'\n" , name.c_str()); |
139 | |
140 | const char* typeStr = xmlFont->Attribute("type" ); |
141 | if (!typeStr) |
142 | throw base::Exception("<font> without 'type' attribute in '%s'\n" , |
143 | xmlFilename.c_str()); |
144 | |
145 | std::string type(typeStr); |
146 | std::string xmlDir(base::get_file_path(xmlFilename)); |
147 | std::unique_ptr<FontData> font(nullptr); |
148 | |
149 | if (type == "spritesheet" ) { |
150 | const char* fileStr = xmlFont->Attribute("file" ); |
151 | if (fileStr) { |
152 | font.reset(new FontData(os::FontType::SpriteSheet)); |
153 | font->setFilename(base::join_path(xmlDir, fileStr)); |
154 | } |
155 | } |
156 | else if (type == "truetype" ) { |
157 | const char* platformFileAttrName = |
158 | #ifdef _WIN32 |
159 | "file_win" |
160 | #elif defined __APPLE__ |
161 | "file_mac" |
162 | #else |
163 | "file_linux" |
164 | #endif |
165 | ; |
166 | |
167 | const char* platformFileStr = xmlFont->Attribute(platformFileAttrName); |
168 | const char* fileStr = xmlFont->Attribute("file" ); |
169 | bool antialias = true; |
170 | if (xmlFont->Attribute("antialias" )) |
171 | antialias = bool_attr(xmlFont, "antialias" , false); |
172 | |
173 | std::string fontFilename; |
174 | if (platformFileStr) |
175 | fontFilename = app::find_font(xmlDir, platformFileStr); |
176 | if (fileStr && fontFilename.empty()) |
177 | fontFilename = app::find_font(xmlDir, fileStr); |
178 | |
179 | // The filename can be empty if the font was not found, anyway we |
180 | // want to keep the font information (e.g. to use the fallback |
181 | // information of this font). |
182 | font.reset(new FontData(os::FontType::FreeType)); |
183 | font->setFilename(fontFilename); |
184 | font->setAntialias(antialias); |
185 | |
186 | if (!fontFilename.empty()) |
187 | LOG(VERBOSE, "THEME: Font file '%s' found\n" , fontFilename.c_str()); |
188 | } |
189 | else { |
190 | throw base::Exception("Invalid type=\"%s\" in '%s' for <font name=\"%s\" ...>\n" , |
191 | type.c_str(), xmlFilename.c_str(), name.c_str()); |
192 | } |
193 | |
194 | FontData* result = nullptr; |
195 | if (font) { |
196 | fonts[name] = result = font.get(); |
197 | font.release(); |
198 | |
199 | // Fallback font |
200 | const TiXmlElement* xmlFallback = |
201 | (const TiXmlElement*)xmlFont->FirstChild("fallback" ); |
202 | if (xmlFallback) { |
203 | FontData* fallback = load_font(fonts, xmlFallback, xmlFilename); |
204 | if (fallback) { |
205 | int size = 10; |
206 | const char* sizeStr = xmlFont->Attribute("size" ); |
207 | if (sizeStr) |
208 | size = std::strtol(sizeStr, nullptr, 10); |
209 | |
210 | result->setFallback(fallback, size); |
211 | } |
212 | } |
213 | } |
214 | return result; |
215 | } |
216 | |
217 | // static |
218 | SkinTheme* SkinTheme::instance() |
219 | { |
220 | if (auto mgr = ui::Manager::getDefault()) |
221 | return SkinTheme::get(mgr); |
222 | else |
223 | return nullptr; |
224 | } |
225 | |
226 | // static |
227 | SkinTheme* SkinTheme::get(const ui::Widget* widget) |
228 | { |
229 | ASSERT(widget); |
230 | ASSERT(widget->theme()); |
231 | ASSERT(dynamic_cast<SkinTheme*>(widget->theme())); |
232 | return static_cast<SkinTheme*>(widget->theme()); |
233 | } |
234 | |
235 | SkinTheme::SkinTheme() |
236 | : m_sheet(nullptr) |
237 | , m_defaultFont(nullptr) |
238 | , m_miniFont(nullptr) |
239 | , m_preferredScreenScaling(-1) |
240 | , m_preferredUIScaling(-1) |
241 | { |
242 | m_standardCursors.fill(nullptr); |
243 | } |
244 | |
245 | SkinTheme::~SkinTheme() |
246 | { |
247 | // Delete all cursors. |
248 | for (auto& it : m_cursors) |
249 | delete it.second; // Delete cursor |
250 | |
251 | m_sheet.reset(); |
252 | m_parts_by_id.clear(); |
253 | |
254 | // Delete all styles. |
255 | for (auto style : m_styles) |
256 | delete style.second; |
257 | m_styles.clear(); |
258 | |
259 | // Destroy fonts |
260 | for (auto& kv : m_fonts) |
261 | delete kv.second; // Delete all FontDatas |
262 | m_fonts.clear(); |
263 | } |
264 | |
265 | void SkinTheme::onRegenerateTheme() |
266 | { |
267 | Preferences& pref = Preferences::instance(); |
268 | |
269 | // First we load the skin from default theme, which is more proper |
270 | // to have every single needed skin part/color/dimension. |
271 | loadAll(pref.theme.selected.defaultValue()); |
272 | |
273 | // Then we load the selected theme to redefine default theme parts. |
274 | if (pref.theme.selected.defaultValue() != pref.theme.selected()) { |
275 | try { |
276 | BackwardCompatibility backward; |
277 | loadAll(pref.theme.selected(), &backward); |
278 | } |
279 | catch (const std::exception& e) { |
280 | LOG("THEME: Error loading user-theme: %s\n" , e.what()); |
281 | |
282 | // Load default theme again |
283 | loadAll(pref.theme.selected.defaultValue()); |
284 | |
285 | if (ui::get_theme()) |
286 | Console::showException(e); |
287 | |
288 | // We can continue, as we've already loaded the default theme |
289 | // anyway. Here we restore the setting to its default value. |
290 | pref.theme.selected(pref.theme.selected.defaultValue()); |
291 | } |
292 | } |
293 | } |
294 | |
295 | void SkinTheme::loadFontData() |
296 | { |
297 | LOG("THEME: Loading fonts\n" ); |
298 | |
299 | std::string fonstFilename("fonts/fonts.xml" ); |
300 | |
301 | ResourceFinder rf; |
302 | rf.includeDataDir(fonstFilename.c_str()); |
303 | if (!rf.findFirst()) |
304 | throw base::Exception("File %s not found" , fonstFilename.c_str()); |
305 | |
306 | XmlDocumentRef doc = open_xml(rf.filename()); |
307 | TiXmlHandle handle(doc.get()); |
308 | |
309 | TiXmlElement* xmlFont = handle |
310 | .FirstChild("fonts" ) |
311 | .FirstChild("font" ).ToElement(); |
312 | while (xmlFont) { |
313 | load_font(m_fonts, xmlFont, rf.filename()); |
314 | xmlFont = xmlFont->NextSiblingElement(); |
315 | } |
316 | } |
317 | |
318 | void SkinTheme::loadAll(const std::string& themeId, |
319 | BackwardCompatibility* backward) |
320 | { |
321 | LOG("THEME: Loading theme %s\n" , themeId.c_str()); |
322 | |
323 | if (m_fonts.empty()) |
324 | loadFontData(); |
325 | |
326 | m_path = findThemePath(themeId); |
327 | if (m_path.empty()) |
328 | throw base::Exception("Theme %s not found" , themeId.c_str()); |
329 | |
330 | loadSheet(); |
331 | loadXml(backward); |
332 | } |
333 | |
334 | void SkinTheme::loadSheet() |
335 | { |
336 | // Load the skin sheet |
337 | std::string sheet_filename(base::join_path(m_path, "sheet.png" )); |
338 | os::SurfaceRef newSheet; |
339 | try { |
340 | newSheet = os::instance()->loadRgbaSurface(sheet_filename.c_str()); |
341 | } |
342 | catch (...) { |
343 | // Ignore the error, newSheet is nullptr and we will throw our own |
344 | // exception. |
345 | } |
346 | if (!newSheet) |
347 | throw base::Exception("Error loading %s file" , sheet_filename.c_str()); |
348 | |
349 | // Replace the sprite sheet |
350 | if (m_sheet) |
351 | m_sheet.reset(); |
352 | m_sheet = newSheet; |
353 | if (m_sheet) |
354 | m_sheet->applyScale(guiscale()); |
355 | m_sheet->setImmutable(); |
356 | |
357 | // Reset sprite sheet and font of all layer styles (to avoid |
358 | // dangling pointers to os::Surface or os::Font). |
359 | for (auto& it : m_styles) { |
360 | for (auto& layer : it.second->layers()) { |
361 | layer.setIcon(nullptr); |
362 | layer.setSpriteSheet(nullptr); |
363 | } |
364 | it.second->setFont(nullptr); |
365 | } |
366 | } |
367 | |
368 | void SkinTheme::loadXml(BackwardCompatibility* backward) |
369 | { |
370 | const int scale = guiscale(); |
371 | |
372 | // Load the skin XML |
373 | std::string xml_filename(base::join_path(m_path, "theme.xml" )); |
374 | |
375 | XmlDocumentRef doc = open_xml(xml_filename); |
376 | TiXmlHandle handle(doc.get()); |
377 | |
378 | // Load Preferred scaling |
379 | m_preferredScreenScaling = -1; |
380 | m_preferredUIScaling = -1; |
381 | { |
382 | TiXmlElement* xmlTheme = handle |
383 | .FirstChild("theme" ).ToElement(); |
384 | if (xmlTheme) { |
385 | const char* screenScaling = xmlTheme->Attribute("screenscaling" ); |
386 | const char* uiScaling = xmlTheme->Attribute("uiscaling" ); |
387 | if (screenScaling) |
388 | m_preferredScreenScaling = std::strtol(screenScaling, nullptr, 10); |
389 | if (uiScaling) |
390 | m_preferredUIScaling = std::strtol(uiScaling, nullptr, 10); |
391 | } |
392 | } |
393 | |
394 | // Load fonts |
395 | { |
396 | TiXmlElement* xmlFont = handle |
397 | .FirstChild("theme" ) |
398 | .FirstChild("fonts" ) |
399 | .FirstChild("font" ).ToElement(); |
400 | while (xmlFont) { |
401 | const char* idStr = xmlFont->Attribute("id" ); |
402 | FontData* fontData = load_font(m_fonts, xmlFont, xml_filename); |
403 | if (idStr && fontData) { |
404 | std::string id(idStr); |
405 | LOG(VERBOSE, "THEME: Loading theme font %s\n" , idStr); |
406 | |
407 | int size = 10; |
408 | const char* sizeStr = xmlFont->Attribute("size" ); |
409 | if (sizeStr) |
410 | size = std::strtol(sizeStr, nullptr, 10); |
411 | |
412 | os::FontRef font = fontData->getFont(size); |
413 | m_themeFonts[idStr] = font; |
414 | |
415 | if (id == "default" ) |
416 | m_defaultFont = font; |
417 | else if (id == "mini" ) |
418 | m_miniFont = font; |
419 | } |
420 | |
421 | xmlFont = xmlFont->NextSiblingElement(); |
422 | } |
423 | } |
424 | |
425 | // No available font to run the program |
426 | if (!m_defaultFont) |
427 | throw base::Exception("There is no default font" ); |
428 | if (!m_miniFont) |
429 | m_miniFont = m_defaultFont; |
430 | |
431 | // Load dimension |
432 | { |
433 | TiXmlElement* xmlDim = handle |
434 | .FirstChild("theme" ) |
435 | .FirstChild("dimensions" ) |
436 | .FirstChild("dim" ).ToElement(); |
437 | while (xmlDim) { |
438 | std::string id = xmlDim->Attribute("id" ); |
439 | uint32_t value = strtol(xmlDim->Attribute("value" ), NULL, 10); |
440 | |
441 | LOG(VERBOSE, "THEME: Loading dimension %s\n" , id.c_str()); |
442 | |
443 | m_dimensions_by_id[id] = value; |
444 | xmlDim = xmlDim->NextSiblingElement(); |
445 | } |
446 | } |
447 | |
448 | // Load colors |
449 | { |
450 | TiXmlElement* xmlColor = handle |
451 | .FirstChild("theme" ) |
452 | .FirstChild("colors" ) |
453 | .FirstChild("color" ).ToElement(); |
454 | while (xmlColor) { |
455 | std::string id = xmlColor->Attribute("id" ); |
456 | uint32_t value = strtol(xmlColor->Attribute("value" )+1, NULL, 16); |
457 | gfx::Color color = gfx::rgba( |
458 | (value & 0xff0000) >> 16, |
459 | (value & 0xff00) >> 8, |
460 | (value & 0xff)); |
461 | |
462 | LOG(VERBOSE, "THEME: Loading color %s\n" , id.c_str()); |
463 | |
464 | m_colors_by_id[id] = color; |
465 | xmlColor = xmlColor->NextSiblingElement(); |
466 | } |
467 | } |
468 | |
469 | // Load parts |
470 | { |
471 | TiXmlElement* xmlPart = handle |
472 | .FirstChild("theme" ) |
473 | .FirstChild("parts" ) |
474 | .FirstChild("part" ).ToElement(); |
475 | while (xmlPart) { |
476 | // Get the tool-icon rectangle |
477 | const char* part_id = xmlPart->Attribute("id" ); |
478 | int x = scale*strtol(xmlPart->Attribute("x" ), nullptr, 10); |
479 | int y = scale*strtol(xmlPart->Attribute("y" ), nullptr, 10); |
480 | int w = (xmlPart->Attribute("w" ) ? scale*strtol(xmlPart->Attribute("w" ), nullptr, 10): 0); |
481 | int h = (xmlPart->Attribute("h" ) ? scale*strtol(xmlPart->Attribute("h" ), nullptr, 10): 0); |
482 | |
483 | LOG(VERBOSE, "THEME: Loading part %s\n" , part_id); |
484 | |
485 | SkinPartPtr part = m_parts_by_id[part_id]; |
486 | if (!part) |
487 | part = m_parts_by_id[part_id] = SkinPartPtr(new SkinPart); |
488 | |
489 | if (w > 0 && h > 0) { |
490 | part->setSpriteBounds(gfx::Rect(x, y, w, h)); |
491 | part->setBitmap(0, sliceSheet(part->bitmapRef(0), gfx::Rect(x, y, w, h))); |
492 | } |
493 | else if (xmlPart->Attribute("w1" )) { // 3x3-1 part (NW, N, NE, E, SE, S, SW, W) |
494 | int w1 = scale*strtol(xmlPart->Attribute("w1" ), nullptr, 10); |
495 | int w2 = scale*strtol(xmlPart->Attribute("w2" ), nullptr, 10); |
496 | int w3 = scale*strtol(xmlPart->Attribute("w3" ), nullptr, 10); |
497 | int h1 = scale*strtol(xmlPart->Attribute("h1" ), nullptr, 10); |
498 | int h2 = scale*strtol(xmlPart->Attribute("h2" ), nullptr, 10); |
499 | int h3 = scale*strtol(xmlPart->Attribute("h3" ), nullptr, 10); |
500 | |
501 | part->setSpriteBounds(gfx::Rect(x, y, w1+w2+w3, h1+h2+h3)); |
502 | part->setSlicesBounds(gfx::Rect(w1, h1, w2, h2)); |
503 | |
504 | part->setBitmap(0, sliceSheet(part->bitmapRef(0), gfx::Rect(x, y, w1, h1))); // NW |
505 | part->setBitmap(1, sliceSheet(part->bitmapRef(1), gfx::Rect(x+w1, y, w2, h1))); // N |
506 | part->setBitmap(2, sliceSheet(part->bitmapRef(2), gfx::Rect(x+w1+w2, y, w3, h1))); // NE |
507 | part->setBitmap(3, sliceSheet(part->bitmapRef(3), gfx::Rect(x+w1+w2, y+h1, w3, h2))); // E |
508 | part->setBitmap(4, sliceSheet(part->bitmapRef(4), gfx::Rect(x+w1+w2, y+h1+h2, w3, h3))); // SE |
509 | part->setBitmap(5, sliceSheet(part->bitmapRef(5), gfx::Rect(x+w1, y+h1+h2, w2, h3))); // S |
510 | part->setBitmap(6, sliceSheet(part->bitmapRef(6), gfx::Rect(x, y+h1+h2, w1, h3))); // SW |
511 | part->setBitmap(7, sliceSheet(part->bitmapRef(7), gfx::Rect(x, y+h1, w1, h2))); // W |
512 | } |
513 | |
514 | // Is it a mouse cursor? |
515 | if (std::strncmp(part_id, "cursor_" , 7) == 0) { |
516 | std::string cursorName = std::string(part_id).substr(7); |
517 | int focusx = scale*std::strtol(xmlPart->Attribute("focusx" ), NULL, 10); |
518 | int focusy = scale*std::strtol(xmlPart->Attribute("focusy" ), NULL, 10); |
519 | |
520 | LOG(VERBOSE, "THEME: Loading cursor '%s'\n" , cursorName.c_str()); |
521 | |
522 | auto it = m_cursors.find(cursorName); |
523 | if (it != m_cursors.end() && it->second != nullptr) { |
524 | delete it->second; |
525 | it->second = nullptr; |
526 | } |
527 | |
528 | os::SurfaceRef slice = sliceSheet(nullptr, gfx::Rect(x, y, w, h)); |
529 | Cursor* cursor = new Cursor(slice, gfx::Point(focusx, focusy)); |
530 | m_cursors[cursorName] = cursor; |
531 | |
532 | for (int c=0; c<kCursorTypes; ++c) { |
533 | if (cursorName == g_cursor_names[c]) { |
534 | m_standardCursors[c] = cursor; |
535 | break; |
536 | } |
537 | } |
538 | } |
539 | |
540 | xmlPart = xmlPart->NextSiblingElement(); |
541 | } |
542 | } |
543 | |
544 | // Load styles |
545 | { |
546 | TiXmlElement* xmlStyle = handle |
547 | .FirstChild("theme" ) |
548 | .FirstChild("styles" ) |
549 | .FirstChild("style" ).ToElement(); |
550 | |
551 | if (!xmlStyle) // Without styles? |
552 | throw base::Exception("There are no styles" ); |
553 | |
554 | while (xmlStyle) { |
555 | const char* style_id = xmlStyle->Attribute("id" ); |
556 | if (!style_id) { |
557 | throw base::Exception("<style> without 'id' attribute in '%s'\n" , |
558 | xml_filename.c_str()); |
559 | } |
560 | |
561 | const char* extends_id = xmlStyle->Attribute("extends" ); |
562 | const ui::Style* base = nullptr; |
563 | if (extends_id) |
564 | base = m_styles[extends_id]; |
565 | |
566 | if (backward) |
567 | backward->notifyStyleExistence(style_id); |
568 | |
569 | ui::Style* style = m_styles[style_id]; |
570 | if (!style) { |
571 | m_styles[style_id] = style = new ui::Style(base); |
572 | } |
573 | else { |
574 | *style = ui::Style(base); |
575 | } |
576 | style->setId(style_id); |
577 | |
578 | // Margin |
579 | { |
580 | const char* m = xmlStyle->Attribute("margin" ); |
581 | const char* l = xmlStyle->Attribute("margin-left" ); |
582 | const char* t = xmlStyle->Attribute("margin-top" ); |
583 | const char* r = xmlStyle->Attribute("margin-right" ); |
584 | const char* b = xmlStyle->Attribute("margin-bottom" ); |
585 | gfx::Border margin = ui::Style::UndefinedBorder(); |
586 | if (m || l) margin.left(scale*std::strtol(l ? l: m, nullptr, 10)); |
587 | if (m || t) margin.top(scale*std::strtol(t ? t: m, nullptr, 10)); |
588 | if (m || r) margin.right(scale*std::strtol(r ? r: m, nullptr, 10)); |
589 | if (m || b) margin.bottom(scale*std::strtol(b ? b: m, nullptr, 10)); |
590 | style->setMargin(margin); |
591 | } |
592 | |
593 | // Border |
594 | { |
595 | const char* m = xmlStyle->Attribute("border" ); |
596 | const char* l = xmlStyle->Attribute("border-left" ); |
597 | const char* t = xmlStyle->Attribute("border-top" ); |
598 | const char* r = xmlStyle->Attribute("border-right" ); |
599 | const char* b = xmlStyle->Attribute("border-bottom" ); |
600 | gfx::Border border = ui::Style::UndefinedBorder(); |
601 | if (m || l) border.left(scale*std::strtol(l ? l: m, nullptr, 10)); |
602 | if (m || t) border.top(scale*std::strtol(t ? t: m, nullptr, 10)); |
603 | if (m || r) border.right(scale*std::strtol(r ? r: m, nullptr, 10)); |
604 | if (m || b) border.bottom(scale*std::strtol(b ? b: m, nullptr, 10)); |
605 | style->setBorder(border); |
606 | } |
607 | |
608 | // Padding |
609 | { |
610 | const char* m = xmlStyle->Attribute("padding" ); |
611 | const char* l = xmlStyle->Attribute("padding-left" ); |
612 | const char* t = xmlStyle->Attribute("padding-top" ); |
613 | const char* r = xmlStyle->Attribute("padding-right" ); |
614 | const char* b = xmlStyle->Attribute("padding-bottom" ); |
615 | gfx::Border padding = ui::Style::UndefinedBorder(); |
616 | if (m || l) padding.left(scale*std::strtol(l ? l: m, nullptr, 10)); |
617 | if (m || t) padding.top(scale*std::strtol(t ? t: m, nullptr, 10)); |
618 | if (m || r) padding.right(scale*std::strtol(r ? r: m, nullptr, 10)); |
619 | if (m || b) padding.bottom(scale*std::strtol(b ? b: m, nullptr, 10)); |
620 | style->setPadding(padding); |
621 | } |
622 | |
623 | // Font |
624 | { |
625 | const char* fontId = xmlStyle->Attribute("font" ); |
626 | if (fontId) { |
627 | os::FontRef font = m_themeFonts[fontId]; |
628 | style->setFont(font); |
629 | } |
630 | } |
631 | |
632 | TiXmlElement* xmlLayer = xmlStyle->FirstChildElement(); |
633 | while (xmlLayer) { |
634 | const std::string layerName = xmlLayer->Value(); |
635 | |
636 | LOG(VERBOSE, "THEME: Layer %s for %s\n" , layerName.c_str(), style_id); |
637 | |
638 | ui::Style::Layer layer; |
639 | |
640 | // Layer type |
641 | if (layerName == "background" ) { |
642 | layer.setType(ui::Style::Layer::Type::kBackground); |
643 | } |
644 | else if (layerName == "background-border" ) { |
645 | layer.setType(ui::Style::Layer::Type::kBackgroundBorder); |
646 | } |
647 | else if (layerName == "border" ) { |
648 | layer.setType(ui::Style::Layer::Type::kBorder); |
649 | } |
650 | else if (layerName == "icon" ) { |
651 | layer.setType(ui::Style::Layer::Type::kIcon); |
652 | } |
653 | else if (layerName == "text" ) { |
654 | layer.setType(ui::Style::Layer::Type::kText); |
655 | } |
656 | else if (layerName == "newlayer" ) { |
657 | layer.setType(ui::Style::Layer::Type::kNewLayer); |
658 | } |
659 | |
660 | // Parse state condition |
661 | const char* stateValue = xmlLayer->Attribute("state" ); |
662 | if (stateValue) { |
663 | std::string state(stateValue); |
664 | int flags = 0; |
665 | if (state.find("disabled" ) != std::string::npos) flags |= ui::Style::Layer::kDisabled; |
666 | if (state.find("selected" ) != std::string::npos) flags |= ui::Style::Layer::kSelected; |
667 | if (state.find("focus" ) != std::string::npos) flags |= ui::Style::Layer::kFocus; |
668 | if (state.find("mouse" ) != std::string::npos) flags |= ui::Style::Layer::kMouse; |
669 | layer.setFlags(flags); |
670 | } |
671 | |
672 | // Align |
673 | const char* alignValue = xmlLayer->Attribute("align" ); |
674 | if (alignValue) { |
675 | std::string alignString(alignValue); |
676 | int align = 0; |
677 | if (alignString.find("left" ) != std::string::npos) align |= LEFT; |
678 | if (alignString.find("center" ) != std::string::npos) align |= CENTER; |
679 | if (alignString.find("right" ) != std::string::npos) align |= RIGHT; |
680 | if (alignString.find("top" ) != std::string::npos) align |= TOP; |
681 | if (alignString.find("middle" ) != std::string::npos) align |= MIDDLE; |
682 | if (alignString.find("bottom" ) != std::string::npos) align |= BOTTOM; |
683 | if (alignString.find("wordwrap" ) != std::string::npos) align |= WORDWRAP; |
684 | layer.setAlign(align); |
685 | } |
686 | |
687 | // Color |
688 | const char* colorId = xmlLayer->Attribute("color" ); |
689 | if (colorId) { |
690 | auto it = m_colors_by_id.find(colorId); |
691 | if (it != m_colors_by_id.end()) |
692 | layer.setColor(it->second); |
693 | else if (std::strcmp(colorId, "none" ) == 0) { |
694 | layer.setColor(gfx::ColorNone); |
695 | } |
696 | else { |
697 | throw base::Exception("Color <%s color='%s' ...> was not found in '%s'\n" , |
698 | layerName.c_str(), colorId, |
699 | xml_filename.c_str()); |
700 | } |
701 | } |
702 | |
703 | // Offset |
704 | const char* x = xmlLayer->Attribute("x" ); |
705 | const char* y = xmlLayer->Attribute("y" ); |
706 | if (x || y) { |
707 | gfx::Point offset(0, 0); |
708 | if (x) offset.x = std::strtol(x, nullptr, 10); |
709 | if (y) offset.y = std::strtol(y, nullptr, 10); |
710 | layer.setOffset(offset*scale); |
711 | } |
712 | |
713 | // Sprite sheet |
714 | const char* partId = xmlLayer->Attribute("part" ); |
715 | if (partId) { |
716 | auto it = m_parts_by_id.find(partId); |
717 | if (it != m_parts_by_id.end()) { |
718 | SkinPartPtr part = it->second; |
719 | if (part) { |
720 | if (layer.type() == ui::Style::Layer::Type::kIcon) |
721 | layer.setIcon(AddRef(part->bitmap(0))); |
722 | else { |
723 | layer.setSpriteSheet(m_sheet); |
724 | layer.setSpriteBounds(part->spriteBounds()); |
725 | layer.setSlicesBounds(part->slicesBounds()); |
726 | } |
727 | } |
728 | } |
729 | else if (std::strcmp(partId, "none" ) == 0) { |
730 | layer.setIcon(nullptr); |
731 | layer.setSpriteSheet(nullptr); |
732 | layer.setSpriteBounds(gfx::Rect(0, 0, 0, 0)); |
733 | layer.setSlicesBounds(gfx::Rect(0, 0, 0, 0)); |
734 | } |
735 | else { |
736 | throw base::Exception("Part <%s part='%s' ...> was not found in '%s'\n" , |
737 | layerName.c_str(), partId, |
738 | xml_filename.c_str()); |
739 | } |
740 | } |
741 | |
742 | if (layer.type() != ui::Style::Layer::Type::kNone) |
743 | style->addLayer(layer); |
744 | |
745 | xmlLayer = xmlLayer->NextSiblingElement(); |
746 | } |
747 | |
748 | xmlStyle = xmlStyle->NextSiblingElement(); |
749 | } |
750 | } |
751 | |
752 | if (backward) |
753 | backward->createMissingStyles(this); |
754 | |
755 | ThemeFile<SkinTheme>::updateInternals(); |
756 | } |
757 | |
758 | os::SurfaceRef SkinTheme::sliceSheet(os::SurfaceRef sur, const gfx::Rect& bounds) |
759 | { |
760 | if (sur && (sur->width() != bounds.w || |
761 | sur->height() != bounds.h)) { |
762 | sur = nullptr; |
763 | } |
764 | |
765 | if (!bounds.isEmpty()) { |
766 | if (!sur) |
767 | sur = os::instance()->makeRgbaSurface(bounds.w, bounds.h); |
768 | |
769 | os::SurfaceLock lockSrc(m_sheet.get()); |
770 | os::SurfaceLock lockDst(sur.get()); |
771 | m_sheet->blitTo(sur.get(), bounds.x, bounds.y, 0, 0, bounds.w, bounds.h); |
772 | |
773 | // The new surface is immutable because we're going to re-use the |
774 | // surface if we reload the theme. |
775 | // |
776 | // TODO Add sub-surfaces (SkBitmap::extractSubset()) |
777 | //sur->setImmutable(); |
778 | } |
779 | else { |
780 | ASSERT(!sur); |
781 | } |
782 | |
783 | return sur; |
784 | } |
785 | |
786 | os::Font* SkinTheme::getWidgetFont(const Widget* widget) const |
787 | { |
788 | auto skinPropery = std::static_pointer_cast<SkinProperty>(widget->getProperty(SkinProperty::Name)); |
789 | if (skinPropery && skinPropery->hasMiniFont()) |
790 | return getMiniFont(); |
791 | else |
792 | return getDefaultFont(); |
793 | } |
794 | |
795 | Cursor* SkinTheme::getStandardCursor(CursorType type) |
796 | { |
797 | if (type >= kFirstCursorType && type <= kLastCursorType) |
798 | return m_standardCursors[type]; |
799 | else |
800 | return nullptr; |
801 | } |
802 | |
803 | void SkinTheme::initWidget(Widget* widget) |
804 | { |
805 | #define BORDER(n) \ |
806 | widget->setBorder(gfx::Border(n)) |
807 | |
808 | #define BORDER4(L,T,R,B) \ |
809 | widget->setBorder(gfx::Border((L), (T), (R), (B))) |
810 | |
811 | const int scale = guiscale(); |
812 | |
813 | switch (widget->type()) { |
814 | |
815 | case kBoxWidget: |
816 | widget->setStyle(styles.box()); |
817 | BORDER(0); |
818 | widget->setChildSpacing(4 * scale); |
819 | break; |
820 | |
821 | case kButtonWidget: |
822 | widget->setStyle(styles.button()); |
823 | break; |
824 | |
825 | case kCheckWidget: |
826 | widget->setStyle(styles.checkBox()); |
827 | break; |
828 | |
829 | case kRadioWidget: |
830 | widget->setStyle(styles.radioButton()); |
831 | break; |
832 | |
833 | case kEntryWidget: |
834 | BORDER4( |
835 | parts.sunkenNormal()->bitmapW()->width(), |
836 | parts.sunkenNormal()->bitmapN()->height(), |
837 | parts.sunkenNormal()->bitmapE()->width(), |
838 | parts.sunkenNormal()->bitmapS()->height()); |
839 | widget->setChildSpacing(3 * scale); |
840 | break; |
841 | |
842 | case kGridWidget: |
843 | widget->setStyle(styles.grid()); |
844 | BORDER(0); |
845 | widget->setChildSpacing(4 * scale); |
846 | break; |
847 | |
848 | case kLabelWidget: |
849 | widget->setStyle(styles.label()); |
850 | break; |
851 | |
852 | case kLinkLabelWidget: |
853 | widget->setStyle(styles.link()); |
854 | break; |
855 | |
856 | case kListBoxWidget: |
857 | BORDER(0); |
858 | widget->setChildSpacing(0); |
859 | break; |
860 | |
861 | case kListItemWidget: |
862 | widget->setStyle(styles.listItem()); |
863 | break; |
864 | |
865 | case kComboBoxWidget: { |
866 | ComboBox* combobox = static_cast<ComboBox*>(widget); |
867 | Button* button = combobox->getButtonWidget(); |
868 | combobox->setChildSpacing(0); |
869 | button->setStyle(styles.comboboxButton()); |
870 | break; |
871 | } |
872 | |
873 | case kMenuWidget: |
874 | widget->setStyle(styles.menu()); |
875 | break; |
876 | |
877 | case kMenuBarWidget: |
878 | widget->setStyle(styles.menubar()); |
879 | break; |
880 | |
881 | case kMenuBoxWidget: |
882 | widget->setStyle(styles.menubox()); |
883 | break; |
884 | |
885 | case kMenuItemWidget: |
886 | BORDER(2 * scale); |
887 | widget->setChildSpacing(18 * scale); |
888 | break; |
889 | |
890 | case kSplitterWidget: |
891 | widget->setChildSpacing(3 * scale); |
892 | widget->setStyle(styles.splitter()); |
893 | break; |
894 | |
895 | case kSeparatorWidget: |
896 | // Horizontal bar |
897 | if (widget->align() & HORIZONTAL) { |
898 | if (dynamic_cast<MenuSeparator*>(widget)) { |
899 | widget->setStyle(styles.menuSeparator()); |
900 | BORDER(2 * scale); |
901 | } |
902 | else |
903 | widget->setStyle(styles.horizontalSeparator()); |
904 | } |
905 | // Vertical bar |
906 | else { |
907 | widget->setStyle(styles.verticalSeparator()); |
908 | } |
909 | break; |
910 | |
911 | case kSliderWidget: |
912 | widget->setStyle(styles.slider()); |
913 | break; |
914 | |
915 | case kTextBoxWidget: |
916 | widget->setChildSpacing(0); |
917 | widget->setStyle(styles.textboxText()); |
918 | break; |
919 | |
920 | case kViewWidget: |
921 | widget->setChildSpacing(0); |
922 | widget->setBgColor(colors.windowFace()); |
923 | widget->setStyle(styles.view()); |
924 | break; |
925 | |
926 | case kViewScrollbarWidget: |
927 | widget->setStyle(styles.scrollbar()); |
928 | static_cast<ScrollBar*>(widget)->setThumbStyle(styles.scrollbarThumb()); |
929 | break; |
930 | |
931 | case kViewViewportWidget: |
932 | BORDER(0); |
933 | widget->setChildSpacing(0); |
934 | break; |
935 | |
936 | case kManagerWidget: |
937 | widget->setStyle(styles.desktop()); |
938 | break; |
939 | |
940 | case kWindowWidget: |
941 | if (TipWindow* window = dynamic_cast<TipWindow*>(widget)) { |
942 | window->setStyle(styles.tooltipWindow()); |
943 | window->setArrowStyle(styles.tooltipWindowArrow()); |
944 | window->textBox()->setStyle(styles.tooltipText()); |
945 | } |
946 | else if (dynamic_cast<TransparentPopupWindow*>(widget)) { |
947 | widget->setStyle(styles.transparentPopupWindow()); |
948 | } |
949 | else if (dynamic_cast<PopupWindow*>(widget)) { |
950 | widget->setStyle(styles.popupWindow()); |
951 | } |
952 | else if (static_cast<Window*>(widget)->isDesktop()) { |
953 | widget->setStyle(styles.desktop()); |
954 | } |
955 | else { |
956 | if (widget->hasText()) { |
957 | widget->setStyle(styles.windowWithTitle()); |
958 | } |
959 | else { |
960 | widget->setStyle(styles.windowWithoutTitle()); |
961 | } |
962 | } |
963 | break; |
964 | |
965 | case kWindowTitleLabelWidget: |
966 | widget->setStyle(styles.windowTitleLabel()); |
967 | break; |
968 | |
969 | case kWindowCloseButtonWidget: |
970 | widget->setStyle(styles.windowCloseButton()); |
971 | break; |
972 | |
973 | default: |
974 | break; |
975 | } |
976 | } |
977 | |
978 | void SkinTheme::getWindowMask(Widget* widget, Region& region) |
979 | { |
980 | region = widget->bounds(); |
981 | } |
982 | |
983 | int SkinTheme::getScrollbarSize() |
984 | { |
985 | return dimensions.scrollbarSize(); |
986 | } |
987 | |
988 | gfx::Size SkinTheme::getEntryCaretSize(Widget* widget) |
989 | { |
990 | if (widget->font()->type() == os::FontType::FreeType) |
991 | return gfx::Size(2*guiscale(), widget->textHeight()); |
992 | else |
993 | return gfx::Size(2*guiscale(), widget->textHeight()+2*guiscale()); |
994 | } |
995 | |
996 | void SkinTheme::paintEntry(PaintEvent& ev) |
997 | { |
998 | Graphics* g = ev.graphics(); |
999 | Entry* widget = static_cast<Entry*>(ev.getSource()); |
1000 | gfx::Rect bounds = widget->clientBounds(); |
1001 | |
1002 | // Outside borders |
1003 | g->fillRect(BGCOLOR, bounds); |
1004 | |
1005 | bool isMiniLook = false; |
1006 | auto skinPropery = std::static_pointer_cast<SkinProperty>(widget->getProperty(SkinProperty::Name)); |
1007 | if (skinPropery) |
1008 | isMiniLook = (skinPropery->getLook() == MiniLook); |
1009 | |
1010 | drawRect(g, bounds, |
1011 | (widget->hasFocus() ? |
1012 | (isMiniLook ? parts.sunkenMiniFocused().get(): parts.sunkenFocused().get()): |
1013 | (isMiniLook ? parts.sunkenMiniNormal().get() : parts.sunkenNormal().get()))); |
1014 | |
1015 | drawEntryText(g, widget); |
1016 | } |
1017 | |
1018 | namespace { |
1019 | |
1020 | class DrawEntryTextDelegate : public os::DrawTextDelegate { |
1021 | public: |
1022 | DrawEntryTextDelegate(Entry* widget, Graphics* graphics, |
1023 | const gfx::Point& pos, const int h) |
1024 | : m_widget(widget) |
1025 | , m_graphics(graphics) |
1026 | , m_caretDrawn(false) |
1027 | // m_lastX is an absolute position on screen |
1028 | , m_lastX(pos.x+m_widget->bounds().x) |
1029 | , m_y(pos.y) |
1030 | , m_h(h) |
1031 | { |
1032 | m_widget->getEntryThemeInfo(&m_index, &m_caret, &m_state, &m_range); |
1033 | } |
1034 | |
1035 | int index() const { return m_index; } |
1036 | bool caretDrawn() const { return m_caretDrawn; } |
1037 | const gfx::Rect& textBounds() const { return m_textBounds; } |
1038 | |
1039 | void preProcessChar(const int index, |
1040 | const int codepoint, |
1041 | gfx::Color& fg, |
1042 | gfx::Color& bg, |
1043 | const gfx::Rect& charBounds) override { |
1044 | auto theme = SkinTheme::get(m_widget); |
1045 | |
1046 | // Normal text |
1047 | auto& colors = theme->colors; |
1048 | bg = ColorNone; |
1049 | fg = colors.text(); |
1050 | |
1051 | // Selected |
1052 | if ((m_index >= m_range.from) && |
1053 | (m_index < m_range.to)) { |
1054 | if (m_widget->hasFocus()) |
1055 | bg = colors.selected(); |
1056 | else |
1057 | bg = colors.disabled(); |
1058 | fg = colors.selectedText(); |
1059 | } |
1060 | |
1061 | // Disabled |
1062 | if (!m_widget->isEnabled()) { |
1063 | bg = ColorNone; |
1064 | fg = colors.disabled(); |
1065 | } |
1066 | |
1067 | m_bg = bg; |
1068 | } |
1069 | |
1070 | bool preDrawChar(const gfx::Rect& charBounds) override { |
1071 | m_textBounds |= charBounds; |
1072 | m_charStartX = charBounds.x; |
1073 | |
1074 | if (charBounds.x2()-m_widget->bounds().x < m_widget->clientBounds().x2()) { |
1075 | if (m_bg != ColorNone) { |
1076 | // Fill background e.g. needed for selected/highlighted |
1077 | // regions with TTF fonts where the char is smaller than the |
1078 | // text bounds [m_y,m_y+m_h) |
1079 | gfx::Rect fillThisRect(m_lastX-m_widget->bounds().x, |
1080 | m_y, charBounds.x2()-m_lastX, m_h); |
1081 | if (charBounds != fillThisRect) |
1082 | m_graphics->fillRect(m_bg, fillThisRect); |
1083 | } |
1084 | m_lastX = charBounds.x2(); |
1085 | return true; |
1086 | } |
1087 | else |
1088 | return false; |
1089 | } |
1090 | |
1091 | void postDrawChar(const gfx::Rect& charBounds) override { |
1092 | // Caret |
1093 | if (m_state && |
1094 | m_index == m_caret && |
1095 | m_widget->hasFocus() && |
1096 | m_widget->isEnabled()) { |
1097 | auto theme = SkinTheme::get(m_widget); |
1098 | theme->drawEntryCaret( |
1099 | m_graphics, m_widget, |
1100 | m_charStartX-m_widget->bounds().x, m_y); |
1101 | m_caretDrawn = true; |
1102 | } |
1103 | |
1104 | ++m_index; |
1105 | } |
1106 | |
1107 | private: |
1108 | Entry* m_widget; |
1109 | Graphics* m_graphics; |
1110 | int m_index; |
1111 | int m_caret; |
1112 | int m_state; |
1113 | Entry::Range m_range; |
1114 | gfx::Rect m_textBounds; |
1115 | bool m_caretDrawn; |
1116 | gfx::Color m_bg; |
1117 | int m_lastX; // Last position used to fill the background |
1118 | int m_y, m_h; |
1119 | int m_charStartX; |
1120 | }; |
1121 | |
1122 | } // anonymous namespace |
1123 | |
1124 | void SkinTheme::drawEntryText(ui::Graphics* g, ui::Entry* widget) |
1125 | { |
1126 | // Draw the text |
1127 | gfx::Rect bounds = widget->getEntryTextBounds(); |
1128 | |
1129 | DrawEntryTextDelegate delegate(widget, g, bounds.origin(), widget->textHeight()); |
1130 | int scroll = delegate.index(); |
1131 | |
1132 | const std::string& textString = widget->text(); |
1133 | base::utf8_decode dec(textString); |
1134 | auto pos = dec.pos(); |
1135 | for (int i=0; i<scroll && dec.next(); ++i) |
1136 | pos = dec.pos(); |
1137 | |
1138 | // TODO use a string_view() |
1139 | g->drawText(std::string(pos, textString.end()), |
1140 | colors.text(), ColorNone, |
1141 | bounds.origin(), &delegate); |
1142 | |
1143 | bounds.x += delegate.textBounds().w; |
1144 | |
1145 | // Draw suffix if there is enough space |
1146 | if (!widget->getSuffix().empty()) { |
1147 | Rect sufBounds(bounds.x, bounds.y, |
1148 | bounds.x2()-widget->childSpacing()*guiscale()-bounds.x, |
1149 | widget->textHeight()); |
1150 | IntersectClip clip(g, sufBounds & widget->clientChildrenBounds()); |
1151 | if (clip) { |
1152 | drawText( |
1153 | g, widget->getSuffix().c_str(), |
1154 | colors.entrySuffix(), ColorNone, |
1155 | widget, sufBounds, widget->align(), 0); |
1156 | } |
1157 | } |
1158 | |
1159 | // Draw caret at the end of the text |
1160 | if (!delegate.caretDrawn()) { |
1161 | gfx::Rect charBounds(bounds.x+widget->bounds().x, |
1162 | bounds.y+widget->bounds().y, 0, widget->textHeight()); |
1163 | delegate.preDrawChar(charBounds); |
1164 | delegate.postDrawChar(charBounds); |
1165 | } |
1166 | } |
1167 | |
1168 | void SkinTheme::paintListBox(PaintEvent& ev) |
1169 | { |
1170 | Graphics* g = ev.graphics(); |
1171 | |
1172 | g->fillRect(colors.background(), g->getClipBounds()); |
1173 | } |
1174 | |
1175 | void SkinTheme::(PaintEvent& ev) |
1176 | { |
1177 | Widget* widget = static_cast<Widget*>(ev.getSource()); |
1178 | Graphics* g = ev.graphics(); |
1179 | |
1180 | g->fillRect(BGCOLOR, g->getClipBounds()); |
1181 | } |
1182 | |
1183 | void SkinTheme::(ui::PaintEvent& ev) |
1184 | { |
1185 | int scale = guiscale(); |
1186 | Graphics* g = ev.graphics(); |
1187 | MenuItem* widget = static_cast<MenuItem*>(ev.getSource()); |
1188 | gfx::Rect bounds = widget->clientBounds(); |
1189 | gfx::Color fg, bg; |
1190 | int c, bar; |
1191 | |
1192 | // TODO ASSERT? |
1193 | if (!widget->parent()->parent()) |
1194 | return; |
1195 | |
1196 | bar = (widget->parent()->parent()->type() == kMenuBarWidget); |
1197 | |
1198 | // Colors |
1199 | if (!widget->isEnabled()) { |
1200 | fg = ColorNone; |
1201 | bg = colors.menuitemNormalFace(); |
1202 | } |
1203 | else { |
1204 | if (widget->isHighlighted()) { |
1205 | fg = colors.menuitemHighlightText(); |
1206 | bg = colors.menuitemHighlightFace(); |
1207 | } |
1208 | else if (widget->hasMouse()) { |
1209 | fg = colors.menuitemHotText(); |
1210 | bg = colors.menuitemHotFace(); |
1211 | } |
1212 | else { |
1213 | fg = colors.menuitemNormalText(); |
1214 | bg = colors.menuitemNormalFace(); |
1215 | } |
1216 | } |
1217 | |
1218 | // Background |
1219 | g->fillRect(bg, bounds); |
1220 | |
1221 | // Draw an indicator for selected items |
1222 | if (widget->isSelected()) { |
1223 | os::Surface* icon = |
1224 | (widget->isEnabled() ? |
1225 | parts.checkSelected()->bitmap(0): |
1226 | parts.checkDisabled()->bitmap(0)); |
1227 | |
1228 | int x = bounds.x+4*scale-icon->width()/2; |
1229 | int y = bounds.y+bounds.h/2-icon->height()/2; |
1230 | g->drawRgbaSurface(icon, x, y); |
1231 | } |
1232 | |
1233 | // Text |
1234 | if (bar) |
1235 | widget->setAlign(CENTER | MIDDLE); |
1236 | else |
1237 | widget->setAlign(LEFT | MIDDLE); |
1238 | |
1239 | Rect pos = bounds; |
1240 | if (!bar) |
1241 | pos.offset(widget->childSpacing()/2, 0); |
1242 | drawText(g, nullptr, fg, ColorNone, widget, pos, |
1243 | widget->align(), widget->mnemonic()); |
1244 | |
1245 | // For menu-box |
1246 | if (!bar) { |
1247 | // Draw the arrown (to indicate which this menu has a sub-menu) |
1248 | if (widget->getSubmenu()) { |
1249 | // Enabled |
1250 | if (widget->isEnabled()) { |
1251 | for (c=0; c<3*scale; c++) |
1252 | g->drawVLine(fg, |
1253 | bounds.x2()-3*scale-c, |
1254 | bounds.y+bounds.h/2-c, 2*c+1); |
1255 | } |
1256 | // Disabled |
1257 | else { |
1258 | for (c=0; c<3*scale; c++) |
1259 | g->drawVLine(colors.background(), |
1260 | bounds.x2()-3*scale-c+1, |
1261 | bounds.y+bounds.h/2-c+1, 2*c+1); |
1262 | |
1263 | for (c=0; c<3*scale; c++) |
1264 | g->drawVLine(colors.disabled(), |
1265 | bounds.x2()-3*scale-c, |
1266 | bounds.y+bounds.h/2-c, 2*c+1); |
1267 | } |
1268 | } |
1269 | // Draw the keyboard shortcut |
1270 | else if (AppMenuItem* = dynamic_cast<AppMenuItem*>(widget)) { |
1271 | if (appMenuItem->key() && !appMenuItem->key()->accels().empty()) { |
1272 | int old_align = appMenuItem->align(); |
1273 | |
1274 | pos = bounds; |
1275 | pos.w -= widget->childSpacing()/4; |
1276 | |
1277 | std::string buf = appMenuItem->key()->accels().front().toString(); |
1278 | |
1279 | widget->setAlign(RIGHT | MIDDLE); |
1280 | drawText(g, buf.c_str(), fg, ColorNone, widget, pos, widget->align(), 0); |
1281 | widget->setAlign(old_align); |
1282 | } |
1283 | } |
1284 | } |
1285 | } |
1286 | |
1287 | void SkinTheme::paintSlider(PaintEvent& ev) |
1288 | { |
1289 | Graphics* g = ev.graphics(); |
1290 | Slider* widget = static_cast<Slider*>(ev.getSource()); |
1291 | const Rect bounds = widget->clientBounds(); |
1292 | int min, max, value; |
1293 | |
1294 | // Outside borders |
1295 | gfx::Color bgcolor = widget->bgColor(); |
1296 | if (!is_transparent(bgcolor)) |
1297 | g->fillRect(bgcolor, bounds); |
1298 | |
1299 | widget->getSliderThemeInfo(&min, &max, &value); |
1300 | |
1301 | Rect rc = bounds; |
1302 | rc.shrink(widget->border()); |
1303 | int x; |
1304 | if (min != max) |
1305 | x = rc.x + rc.w * (value-min) / (max-min); |
1306 | else |
1307 | x = rc.x; |
1308 | |
1309 | rc = bounds; |
1310 | |
1311 | // The mini-look is used for sliders with tiny borders. |
1312 | bool isMiniLook = false; |
1313 | |
1314 | // The BG painter is used for sliders without a number-indicator and |
1315 | // customized background (e.g. RGB sliders) |
1316 | ISliderBgPainter* bgPainter = NULL; |
1317 | |
1318 | const auto skinPropery = std::static_pointer_cast<SkinProperty>(widget->getProperty(SkinProperty::Name)); |
1319 | if (skinPropery) |
1320 | isMiniLook = (skinPropery->getLook() == MiniLook); |
1321 | |
1322 | const auto skinSliderPropery = std::static_pointer_cast<SkinSliderProperty>(widget->getProperty(SkinSliderProperty::Name)); |
1323 | if (skinSliderPropery) |
1324 | bgPainter = skinSliderPropery->getBgPainter(); |
1325 | |
1326 | // Draw customized background |
1327 | if (bgPainter) { |
1328 | SkinPartPtr nw = parts.miniSliderEmpty(); |
1329 | os::Surface* thumb = |
1330 | (widget->hasFocus() ? parts.miniSliderThumbFocused()->bitmap(0): |
1331 | parts.miniSliderThumb()->bitmap(0)); |
1332 | |
1333 | // Draw background |
1334 | g->fillRect(BGCOLOR, rc); |
1335 | |
1336 | // Draw thumb |
1337 | int thumb_y = rc.y; |
1338 | if (rc.h > thumb->height()*3) |
1339 | rc.shrink(Border(0, thumb->height(), 0, 0)); |
1340 | |
1341 | // Draw borders |
1342 | if (rc.h > 4*guiscale()) { |
1343 | rc.shrink(Border(3, 0, 3, 1) * guiscale()); |
1344 | drawRect(g, rc, nw.get()); |
1345 | } |
1346 | |
1347 | // Draw background (using the customized ISliderBgPainter implementation) |
1348 | rc.shrink(Border(1, 1, 1, 2) * guiscale()); |
1349 | if (!rc.isEmpty()) |
1350 | bgPainter->paint(widget, g, rc); |
1351 | |
1352 | g->drawRgbaSurface(thumb, x-thumb->width()/2, thumb_y); |
1353 | } |
1354 | else { |
1355 | // Draw borders |
1356 | SkinPartPtr full_part; |
1357 | SkinPartPtr empty_part; |
1358 | |
1359 | if (isMiniLook) { |
1360 | full_part = widget->hasMouseOver() ? parts.miniSliderFullFocused(): |
1361 | parts.miniSliderFull(); |
1362 | empty_part = widget->hasMouseOver() ? parts.miniSliderEmptyFocused(): |
1363 | parts.miniSliderEmpty(); |
1364 | } |
1365 | else { |
1366 | full_part = widget->hasFocus() ? parts.sliderFullFocused(): |
1367 | parts.sliderFull(); |
1368 | empty_part = widget->hasFocus() ? parts.sliderEmptyFocused(): |
1369 | parts.sliderEmpty(); |
1370 | } |
1371 | |
1372 | if (value == min) |
1373 | drawRect(g, rc, empty_part.get()); |
1374 | else if (value == max) |
1375 | drawRect(g, rc, full_part.get()); |
1376 | else |
1377 | drawRect2(g, rc, x, |
1378 | full_part.get(), empty_part.get()); |
1379 | |
1380 | // Draw text |
1381 | std::string old_text = widget->text(); |
1382 | widget->setTextQuiet(widget->convertValueToText(value)); |
1383 | |
1384 | gfx::Rect textrc; |
1385 | int textAlign; |
1386 | calcTextInfo(widget, widget->style(), bounds, textrc, textAlign); |
1387 | |
1388 | { |
1389 | IntersectClip clip(g, Rect(rc.x, rc.y, x-rc.x+1, rc.h)); |
1390 | if (clip) { |
1391 | drawText(g, nullptr, |
1392 | colors.sliderFullText(), ColorNone, |
1393 | widget, textrc, textAlign, widget->mnemonic()); |
1394 | } |
1395 | } |
1396 | |
1397 | { |
1398 | IntersectClip clip(g, Rect(x+1, rc.y, rc.w-(x-rc.x+1), rc.h)); |
1399 | if (clip) { |
1400 | drawText(g, nullptr, |
1401 | colors.sliderEmptyText(), |
1402 | ColorNone, widget, textrc, textAlign, widget->mnemonic()); |
1403 | } |
1404 | } |
1405 | |
1406 | widget->setTextQuiet(old_text.c_str()); |
1407 | } |
1408 | } |
1409 | |
1410 | void SkinTheme::paintComboBoxEntry(ui::PaintEvent& ev) |
1411 | { |
1412 | Graphics* g = ev.graphics(); |
1413 | Entry* widget = static_cast<Entry*>(ev.getSource()); |
1414 | gfx::Rect bounds = widget->clientBounds(); |
1415 | |
1416 | // Outside borders |
1417 | g->fillRect(BGCOLOR, bounds); |
1418 | |
1419 | drawRect(g, bounds, |
1420 | (widget->hasFocus() ? |
1421 | parts.sunken2Focused().get(): |
1422 | parts.sunken2Normal().get())); |
1423 | |
1424 | drawEntryText(g, widget); |
1425 | } |
1426 | |
1427 | void SkinTheme::paintTextBox(ui::PaintEvent& ev) |
1428 | { |
1429 | Graphics* g = ev.graphics(); |
1430 | Widget* widget = static_cast<Widget*>(ev.getSource()); |
1431 | |
1432 | Theme::paintTextBoxWithStyle(g, widget); |
1433 | } |
1434 | |
1435 | void SkinTheme::paintViewViewport(PaintEvent& ev) |
1436 | { |
1437 | Viewport* widget = static_cast<Viewport*>(ev.getSource()); |
1438 | Graphics* g = ev.graphics(); |
1439 | gfx::Color bg = BGCOLOR; |
1440 | |
1441 | if (!is_transparent(bg)) |
1442 | g->fillRect(bg, widget->clientBounds()); |
1443 | } |
1444 | |
1445 | gfx::Color SkinTheme::getWidgetBgColor(Widget* widget) |
1446 | { |
1447 | gfx::Color c = widget->bgColor(); |
1448 | bool decorative = widget->isDecorative(); |
1449 | |
1450 | if (!is_transparent(c) || |
1451 | widget->type() == kWindowWidget) |
1452 | return (widget->isTransparent() ? gfx::ColorNone: c); |
1453 | else if (decorative) |
1454 | return colors.selected(); |
1455 | else |
1456 | return colors.face(); |
1457 | } |
1458 | |
1459 | void SkinTheme::drawText(Graphics* g, const char* t, |
1460 | const gfx::Color fgColor, |
1461 | const gfx::Color bgColor, |
1462 | const Widget* widget, |
1463 | const Rect& rc, |
1464 | const int textAlign, |
1465 | const int mnemonic) |
1466 | { |
1467 | if (t || widget->hasText()) { |
1468 | Rect textrc; |
1469 | |
1470 | g->setFont(AddRef(widget->font())); |
1471 | |
1472 | if (!t) |
1473 | t = widget->text().c_str(); |
1474 | |
1475 | textrc.setSize(g->measureUIText(t)); |
1476 | |
1477 | // Horizontally text alignment |
1478 | |
1479 | if (textAlign & RIGHT) |
1480 | textrc.x = rc.x + rc.w - textrc.w - 1; |
1481 | else if (textAlign & CENTER) |
1482 | textrc.x = rc.center().x - textrc.w/2; |
1483 | else |
1484 | textrc.x = rc.x; |
1485 | |
1486 | // Vertically text alignment |
1487 | |
1488 | if (textAlign & BOTTOM) |
1489 | textrc.y = rc.y + rc.h - textrc.h - 1; |
1490 | else if (textAlign & MIDDLE) |
1491 | textrc.y = rc.center().y - textrc.h/2; |
1492 | else |
1493 | textrc.y = rc.y; |
1494 | |
1495 | // Background |
1496 | if (!is_transparent(bgColor)) { |
1497 | if (!widget->isEnabled()) |
1498 | g->fillRect(bgColor, Rect(textrc).inflate(guiscale(), guiscale())); |
1499 | else |
1500 | g->fillRect(bgColor, textrc); |
1501 | } |
1502 | |
1503 | // Text |
1504 | Rect textWrap = textrc.createIntersection( |
1505 | // TODO add ui::Widget::getPadding() property |
1506 | // Rect(widget->clientBounds()).shrink(widget->border())); |
1507 | widget->clientBounds()).inflate(0, 1*guiscale()); |
1508 | |
1509 | IntersectClip clip(g, textWrap); |
1510 | if (clip) { |
1511 | if (!widget->isEnabled()) { |
1512 | // Draw white part |
1513 | g->drawUIText( |
1514 | t, |
1515 | colors.background(), |
1516 | gfx::ColorNone, |
1517 | textrc.origin() + Point(guiscale(), guiscale()), |
1518 | mnemonic); |
1519 | } |
1520 | |
1521 | g->drawUIText( |
1522 | t, |
1523 | (!widget->isEnabled() ? |
1524 | colors.disabled(): |
1525 | (gfx::geta(fgColor) > 0 ? fgColor : |
1526 | colors.text())), |
1527 | bgColor, textrc.origin(), |
1528 | mnemonic); |
1529 | } |
1530 | } |
1531 | } |
1532 | |
1533 | void SkinTheme::drawEntryCaret(ui::Graphics* g, Entry* widget, int x, int y) |
1534 | { |
1535 | gfx::Color color = colors.text(); |
1536 | int textHeight = widget->textHeight(); |
1537 | gfx::Size caretSize = getEntryCaretSize(widget); |
1538 | |
1539 | for (int u=x; u<x+caretSize.w; ++u) |
1540 | g->drawVLine(color, u, y+textHeight/2-caretSize.h/2, caretSize.h); |
1541 | } |
1542 | |
1543 | SkinPartPtr SkinTheme::getToolPart(const char* toolId) const |
1544 | { |
1545 | return getPartById(std::string("tool_" ) + toolId); |
1546 | } |
1547 | |
1548 | os::Surface* SkinTheme::getToolIcon(const char* toolId) const |
1549 | { |
1550 | SkinPartPtr part = getToolPart(toolId); |
1551 | if (part) |
1552 | return part->bitmap(0); |
1553 | else |
1554 | return nullptr; |
1555 | } |
1556 | |
1557 | void SkinTheme::drawRect(Graphics* g, const Rect& rc, |
1558 | os::Surface* nw, os::Surface* n, os::Surface* ne, |
1559 | os::Surface* e, os::Surface* se, os::Surface* s, |
1560 | os::Surface* sw, os::Surface* w) |
1561 | { |
1562 | int x, y; |
1563 | |
1564 | // Top |
1565 | |
1566 | g->drawRgbaSurface(nw, rc.x, rc.y); |
1567 | { |
1568 | IntersectClip clip(g, Rect(rc.x+nw->width(), rc.y, |
1569 | rc.w-nw->width()-ne->width(), rc.h)); |
1570 | if (clip) { |
1571 | for (x = rc.x+nw->width(); |
1572 | x < rc.x+rc.w-ne->width(); |
1573 | x += n->width()) { |
1574 | g->drawRgbaSurface(n, x, rc.y); |
1575 | } |
1576 | } |
1577 | } |
1578 | |
1579 | g->drawRgbaSurface(ne, rc.x+rc.w-ne->width(), rc.y); |
1580 | |
1581 | // Bottom |
1582 | |
1583 | g->drawRgbaSurface(sw, rc.x, rc.y+rc.h-sw->height()); |
1584 | { |
1585 | IntersectClip clip(g, Rect(rc.x+sw->width(), rc.y, |
1586 | rc.w-sw->width()-se->width(), rc.h)); |
1587 | if (clip) { |
1588 | for (x = rc.x+sw->width(); |
1589 | x < rc.x+rc.w-se->width(); |
1590 | x += s->width()) { |
1591 | g->drawRgbaSurface(s, x, rc.y+rc.h-s->height()); |
1592 | } |
1593 | } |
1594 | } |
1595 | |
1596 | g->drawRgbaSurface(se, rc.x+rc.w-se->width(), rc.y+rc.h-se->height()); |
1597 | { |
1598 | IntersectClip clip(g, Rect(rc.x, rc.y+nw->height(), |
1599 | rc.w, rc.h-nw->height()-sw->height())); |
1600 | if (clip) { |
1601 | // Left |
1602 | for (y = rc.y+nw->height(); |
1603 | y < rc.y+rc.h-sw->height(); |
1604 | y += w->height()) { |
1605 | g->drawRgbaSurface(w, rc.x, y); |
1606 | } |
1607 | |
1608 | // Right |
1609 | for (y = rc.y+ne->height(); |
1610 | y < rc.y+rc.h-se->height(); |
1611 | y += e->height()) { |
1612 | g->drawRgbaSurface(e, rc.x+rc.w-e->width(), y); |
1613 | } |
1614 | } |
1615 | } |
1616 | } |
1617 | |
1618 | void SkinTheme::drawRect(ui::Graphics* g, const gfx::Rect& rc, |
1619 | SkinPart* skinPart, const bool drawCenter) |
1620 | { |
1621 | Theme::drawSlices(g, m_sheet.get(), rc, |
1622 | skinPart->spriteBounds(), |
1623 | skinPart->slicesBounds(), |
1624 | gfx::ColorNone, |
1625 | drawCenter); |
1626 | } |
1627 | |
1628 | void SkinTheme::drawRect2(Graphics* g, const Rect& rc, int x_mid, |
1629 | SkinPart* nw1, SkinPart* nw2) |
1630 | { |
1631 | Rect rc2(rc.x, rc.y, x_mid-rc.x+1, rc.h); |
1632 | { |
1633 | IntersectClip clip(g, rc2); |
1634 | if (clip) |
1635 | drawRect(g, rc, nw1); |
1636 | } |
1637 | |
1638 | rc2.x += rc2.w; |
1639 | rc2.w = rc.w - rc2.w; |
1640 | |
1641 | IntersectClip clip(g, rc2); |
1642 | if (clip) |
1643 | drawRect(g, rc, nw2); |
1644 | } |
1645 | |
1646 | void SkinTheme::drawHline(ui::Graphics* g, const gfx::Rect& rc, SkinPart* part) |
1647 | { |
1648 | int x; |
1649 | |
1650 | for (x = rc.x; |
1651 | x < rc.x2()-part->size().w; |
1652 | x += part->size().w) { |
1653 | g->drawRgbaSurface(part->bitmap(0), x, rc.y); |
1654 | } |
1655 | |
1656 | if (x < rc.x2()) { |
1657 | Rect rc2(x, rc.y, rc.w-(x-rc.x), part->size().h); |
1658 | IntersectClip clip(g, rc2); |
1659 | if (clip) |
1660 | g->drawRgbaSurface(part->bitmap(0), x, rc.y); |
1661 | } |
1662 | } |
1663 | |
1664 | void SkinTheme::drawVline(ui::Graphics* g, const gfx::Rect& rc, SkinPart* part) |
1665 | { |
1666 | int y; |
1667 | |
1668 | for (y = rc.y; |
1669 | y < rc.y2()-part->size().h; |
1670 | y += part->size().h) { |
1671 | g->drawRgbaSurface(part->bitmap(0), rc.x, y); |
1672 | } |
1673 | |
1674 | if (y < rc.y2()) { |
1675 | Rect rc2(rc.x, y, part->size().w, rc.h-(y-rc.y)); |
1676 | IntersectClip clip(g, rc2); |
1677 | if (clip) |
1678 | g->drawRgbaSurface(part->bitmap(0), rc.x, y); |
1679 | } |
1680 | } |
1681 | |
1682 | void SkinTheme::paintProgressBar(ui::Graphics* g, const gfx::Rect& rc0, double progress) |
1683 | { |
1684 | gfx::Color border = colors.text(); |
1685 | border = gfx::rgba(gfx::getr(border), |
1686 | gfx::getg(border), |
1687 | gfx::getb(border), 64); |
1688 | g->drawRect(border, rc0); |
1689 | |
1690 | gfx::Rect rc = rc0; |
1691 | rc.shrink(1); |
1692 | |
1693 | int u = (int)((double)rc.w*progress); |
1694 | u = std::clamp(u, 0, rc.w); |
1695 | |
1696 | if (u > 0) |
1697 | g->fillRect(colors.selected(), gfx::Rect(rc.x, rc.y, u, rc.h)); |
1698 | |
1699 | if (1+u < rc.w) |
1700 | g->fillRect(colors.background(), gfx::Rect(rc.x+u, rc.y, rc.w-u, rc.h)); |
1701 | } |
1702 | |
1703 | std::string SkinTheme::findThemePath(const std::string& themeId) const |
1704 | { |
1705 | // First we try to find the theme on an extensions |
1706 | std::string path = App::instance()->extensions().themePath(themeId); |
1707 | if (path.empty()) { |
1708 | // Then we try a theme in the old themes/ folder |
1709 | path = base::join_path(SkinTheme::kThemesFolderName, themeId); |
1710 | path = base::join_path(path, "theme.xml" ); |
1711 | |
1712 | ResourceFinder rf; |
1713 | rf.includeDataDir(path.c_str()); |
1714 | if (!rf.findFirst()) |
1715 | return std::string(); |
1716 | |
1717 | path = base::get_file_path(rf.filename()); |
1718 | } |
1719 | return base::normalize_path(path); |
1720 | } |
1721 | |
1722 | } // namespace skin |
1723 | } // namespace app |
1724 | |