/*************************************************************************** * __________ __ ___. * Open \______ \ ____ ____ | | _\_ |__ _______ ___ * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ * \/ \/ \/ \/ \/ * $Id$ * * Copyright (C) 2007 Nicolas Pennequin, Dan Everton, Matthias Mohr * 2010 Jonathan Gordon * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY * KIND, either express or implied. * ****************************************************************************/ #include #include #include #include "config.h" #include "file.h" #include "misc.h" #include "plugin.h" #include "viewport.h" #include "skin_buffer.h" #include "skin_parser.h" #include "tag_table.h" #ifdef __PCTOOL__ #ifdef WPSEDITOR #include "proxy.h" #include "sysfont.h" #else #include "action.h" #include "checkwps.h" #include "audio.h" #define lang_is_rtl() (false) #define DEBUGF printf #endif /*WPSEDITOR*/ #else #include "debug.h" #include "language.h" #endif /*__PCTOOL__*/ #include #include #include "font.h" #include "wps_internals.h" #include "skin_engine.h" #include "settings.h" #include "settings_list.h" #if CONFIG_TUNER #include "radio.h" #include "tuner.h" #endif #include "skin_fonts.h" #ifdef HAVE_LCD_BITMAP #include "bmp.h" #endif #ifdef HAVE_ALBUMART #include "playback.h" #endif #include "backdrop.h" #include "statusbar-skinned.h" #define WPS_ERROR_INVALID_PARAM -1 static bool isdefault(struct skin_tag_parameter *param) { return param->type == DEFAULT; } /* which screen are we parsing for? */ static enum screen_type curr_screen; /* the current viewport */ static struct skin_element *curr_viewport_element; static struct skin_viewport *curr_vp; static struct line *curr_line; static int follow_lang_direction = 0; typedef int (*parse_function)(struct skin_element *element, struct wps_token *token, struct wps_data *wps_data); #ifdef HAVE_LCD_BITMAP /* add a skin_token_list item to the list chain. ALWAYS appended because some of the * chains require the order to be kept. */ static void add_to_ll_chain(struct skin_token_list **list, struct skin_token_list *item) { if (*list == NULL) *list = item; else { struct skin_token_list *t = *list; while (t->next) t = t->next; t->next = item; } } #endif void *skin_find_item(const char *label, enum skin_find_what what, struct wps_data *data) { const char *itemlabel = NULL; union { struct skin_token_list *linkedlist; struct skin_element *vplist; } list = {NULL}; bool isvplist = false; void *ret = NULL; switch (what) { case SKIN_FIND_UIVP: case SKIN_FIND_VP: list.vplist = data->tree; isvplist = true; break; #ifdef HAVE_LCD_BITMAP case SKIN_FIND_IMAGE: list.linkedlist = data->images; break; #endif #ifdef HAVE_TOUCHSCREEN case SKIN_FIND_TOUCHREGION: list.linkedlist = data->touchregions; break; #endif #ifdef HAVE_SKIN_VARIABLES case SKIN_VARIABLE: list.linkedlist = data->skinvars; break; #endif } while (list.linkedlist) { bool skip = false; switch (what) { case SKIN_FIND_UIVP: case SKIN_FIND_VP: ret = list.vplist->data; itemlabel = ((struct skin_viewport *)ret)->label; skip = !(((struct skin_viewport *)ret)->is_infovp == (what==SKIN_FIND_UIVP)); break; #ifdef HAVE_LCD_BITMAP case SKIN_FIND_IMAGE: ret = list.linkedlist->token->value.data; itemlabel = ((struct gui_img *)ret)->label; break; #endif #ifdef HAVE_TOUCHSCREEN case SKIN_FIND_TOUCHREGION: ret = list.linkedlist->token->value.data; itemlabel = ((struct touchregion *)ret)->label; break; #endif #ifdef HAVE_SKIN_VARIABLES case SKIN_VARIABLE: ret = list.linkedlist->token->value.data; itemlabel = ((struct skin_var *)ret)->label; break; #endif } if (!skip && itemlabel && !strcmp(itemlabel, label)) return ret; if (isvplist) list.vplist = list.vplist->next; else list.linkedlist = list.linkedlist->next; } return NULL; } #ifdef HAVE_LCD_BITMAP /* create and init a new wpsll item. * passing NULL to token will alloc a new one. * You should only pass NULL for the token when the token type (table above) * is WPS_NO_TOKEN which means it is not stored automatically in the skins token array */ static struct skin_token_list *new_skin_token_list_item(struct wps_token *token, void* token_data) { struct skin_token_list *llitem = (struct skin_token_list *)skin_buffer_alloc(sizeof(struct skin_token_list)); if (!token) token = (struct wps_token*)skin_buffer_alloc(sizeof(struct wps_token)); if (!llitem || !token) return NULL; llitem->next = NULL; llitem->token = token; if (token_data) llitem->token->value.data = token_data; return llitem; } static int parse_statusbar_tags(struct skin_element* element, struct wps_token *token, struct wps_data *wps_data) { (void)element; if (token->type == SKIN_TOKEN_DRAW_INBUILTBAR) { token->value.data = (void*)&curr_vp->vp; } else { struct skin_element *def_vp = wps_data->tree; struct skin_viewport *default_vp = def_vp->data; if (def_vp->params_count == 0) { wps_data->wps_sb_tag = true; wps_data->show_sb_on_wps = (token->type == SKIN_TOKEN_ENABLE_THEME); } if (wps_data->show_sb_on_wps) { viewport_set_defaults(&default_vp->vp, curr_screen); } else { viewport_set_fullscreen(&default_vp->vp, curr_screen); } #ifdef HAVE_REMOTE_LCD /* viewport_set_defaults() sets the font to FONT_UI+curr_screen. * This parser requires font 1 to always be the UI font, * so force it back to FONT_UI and handle the screen number at the end */ default_vp->vp.font = FONT_UI; #endif } return 0; } static int get_image_id(int c) { if(c >= 'a' && c <= 'z') return c - 'a'; else if(c >= 'A' && c <= 'Z') return c - 'A' + 26; else return -1; } char *get_image_filename(const char *start, const char* bmpdir, char *buf, int buf_size) { snprintf(buf, buf_size, "%s/%s", bmpdir, start); return buf; } static int parse_image_display(struct skin_element *element, struct wps_token *token, struct wps_data *wps_data) { char *label = element->params[0].data.text; char sublabel = '\0'; int subimage; struct gui_img *img; struct image_display *id = skin_buffer_alloc(sizeof(struct image_display)); if (element->params_count == 1 && strlen(label) <= 2) { /* backwards compatability. Allow %xd(Aa) to still work */ sublabel = label[1]; label[1] = '\0'; } /* sanity check */ img = skin_find_item(label, SKIN_FIND_IMAGE, wps_data); if (!img || !id) { return WPS_ERROR_INVALID_PARAM; } id->label = label; id->offset = 0; id->token = NULL; if (img->using_preloaded_icons) { token->type = SKIN_TOKEN_IMAGE_DISPLAY_LISTICON; } if (element->params_count > 1) { if (element->params[1].type == CODE) id->token = element->params[1].data.code->data; /* specify a number. 1 being the first subimage (i.e top) NOT 0 */ else if (element->params[1].type == INTEGER) id->subimage = element->params[1].data.number - 1; if (element->params_count > 2) id->offset = element->params[2].data.number; } else { if ((subimage = get_image_id(sublabel)) != -1) { if (subimage >= img->num_subimages) return WPS_ERROR_INVALID_PARAM; id->subimage = subimage; } else { id->subimage = 0; } } token->value.data = id; return 0; } static int parse_image_load(struct skin_element *element, struct wps_token *token, struct wps_data *wps_data) { const char* filename; const char* id; int x,y; struct gui_img *img; /* format: %x(n,filename.bmp,x,y) or %xl(n,filename.bmp,x,y) or %xl(n,filename.bmp,x,y,num_subimages) */ id = element->params[0].data.text; filename = element->params[1].data.text; x = element->params[2].data.number; y = element->params[3].data.number; /* check the image number and load state */ if(skin_find_item(id, SKIN_FIND_IMAGE, wps_data)) { /* Invalid image ID */ return WPS_ERROR_INVALID_PARAM; } img = (struct gui_img*)skin_buffer_alloc(sizeof(struct gui_img)); if (!img) return WPS_ERROR_INVALID_PARAM; /* save a pointer to the filename */ img->bm.data = (char*)filename; img->label = id; img->x = x; img->y = y; img->num_subimages = 1; img->always_display = false; img->display = -1; img->using_preloaded_icons = false; /* save current viewport */ img->vp = &curr_vp->vp; if (token->type == SKIN_TOKEN_IMAGE_DISPLAY) { img->always_display = true; } else if (element->params_count == 5) { img->num_subimages = element->params[4].data.number; if (img->num_subimages <= 0) return WPS_ERROR_INVALID_PARAM; } if (!strcmp(img->bm.data, "__list_icons__")) { img->num_subimages = Icon_Last_Themeable; img->using_preloaded_icons = true; } struct skin_token_list *item = (struct skin_token_list *)new_skin_token_list_item(NULL, img); if (!item) return WPS_ERROR_INVALID_PARAM; add_to_ll_chain(&wps_data->images, item); return 0; } struct skin_font { int id; /* the id from font_load */ char *name; /* filename without path and extension */ int glyphs; /* how many glyphs to reserve room for */ }; static struct skin_font skinfonts[MAXUSERFONTS]; static int parse_font_load(struct skin_element *element, struct wps_token *token, struct wps_data *wps_data) { (void)wps_data; (void)token; int id = element->params[0].data.number; char *filename = element->params[1].data.text; int glyphs; char *ptr; if(element->params_count > 2) glyphs = element->params[2].data.number; else glyphs = GLYPHS_TO_CACHE; #if defined(DEBUG) || defined(SIMULATOR) if (skinfonts[id-FONT_FIRSTUSERFONT].name != NULL) { DEBUGF("font id %d already being used\n", id); } #endif /* make sure the filename contains .fnt, * we dont actually use it, but require it anyway */ ptr = strchr(filename, '.'); if (!ptr || strncmp(ptr, ".fnt", 4)) return WPS_ERROR_INVALID_PARAM; skinfonts[id-FONT_FIRSTUSERFONT].id = -1; skinfonts[id-FONT_FIRSTUSERFONT].name = filename; skinfonts[id-FONT_FIRSTUSERFONT].glyphs = glyphs; return 0; } #ifdef HAVE_LCD_BITMAP static int parse_playlistview(struct skin_element *element, struct wps_token *token, struct wps_data *wps_data) { (void)wps_data; struct playlistviewer *viewer = (struct playlistviewer *)skin_buffer_alloc(sizeof(struct playlistviewer)); if (!viewer) return WPS_ERROR_INVALID_PARAM; viewer->vp = &curr_vp->vp; viewer->show_icons = true; viewer->start_offset = element->params[0].data.number; viewer->line = element->params[1].data.code; token->value.data = (void*)viewer; return 0; } #endif #if (LCD_DEPTH > 1) || (defined(HAVE_REMOTE_LCD) && (LCD_REMOTE_DEPTH > 1)) static int parse_viewportcolour(struct skin_element *element, struct wps_token *token, struct wps_data *wps_data) { (void)wps_data; struct skin_tag_parameter *param = element->params; struct viewport_colour *colour = (struct viewport_colour *)skin_buffer_alloc(sizeof(struct viewport_colour)); if (!colour) return -1; if (isdefault(param)) { colour->colour = get_viewport_default_colour(curr_screen, token->type == SKIN_TOKEN_VIEWPORT_FGCOLOUR); } else { if (!parse_color(curr_screen, param->data.text, &colour->colour)) return -1; } colour->vp = &curr_vp->vp; token->value.data = colour; if (element->line == curr_viewport_element->line) { if (token->type == SKIN_TOKEN_VIEWPORT_FGCOLOUR) { curr_vp->start_fgcolour = colour->colour; curr_vp->vp.fg_pattern = colour->colour; } else { curr_vp->start_bgcolour = colour->colour; curr_vp->vp.bg_pattern = colour->colour; } } return 0; } static int parse_image_special(struct skin_element *element, struct wps_token *token, struct wps_data *wps_data) { (void)wps_data; /* kill warning */ (void)token; #if LCD_DEPTH > 1 char *filename; if (token->type == SKIN_TOKEN_IMAGE_BACKDROP) { if (isdefault(&element->params[0])) { filename = "-"; } else { filename = element->params[0].data.text; /* format: %X(filename.bmp) or %X(d) */ if (!strcmp(filename, "d")) filename = NULL; } wps_data->backdrop = filename; } #endif return 0; } #endif #endif /* HAVE_LCD_BITMAP */ static int parse_setting_and_lang(struct skin_element *element, struct wps_token *token, struct wps_data *wps_data) { /* NOTE: both the string validations that happen in here will * automatically PASS on checkwps because its too hard to get * settings_list.c and english.lang built for it. * If that ever changes remove the #ifndef __PCTOOL__'s here */ (void)wps_data; char *temp = element->params[0].data.text; int i; if (token->type == SKIN_TOKEN_TRANSLATEDSTRING) { #ifndef __PCTOOL__ i = lang_english_to_id(temp); if (i < 0) return WPS_ERROR_INVALID_PARAM; #endif } else { /* Find the setting */ for (i=0; ivalue.i = i; return 0; } static int parse_logical_if(struct skin_element *element, struct wps_token *token, struct wps_data *wps_data) { (void)wps_data; char *op = element->params[1].data.text; struct logical_if *lif = skin_buffer_alloc(sizeof(struct logical_if)); if (!lif) return -1; token->value.data = lif; lif->token = element->params[0].data.code->data; if (!strncmp(op, "=", 1)) lif->op = IF_EQUALS; else if (!strncmp(op, "!=", 2)) lif->op = IF_NOTEQUALS; else if (!strncmp(op, ">=", 2)) lif->op = IF_GREATERTHAN_EQ; else if (!strncmp(op, "<=", 2)) lif->op = IF_LESSTHAN_EQ; else if (!strncmp(op, ">", 2)) lif->op = IF_GREATERTHAN; else if (!strncmp(op, "<", 1)) lif->op = IF_LESSTHAN; memcpy(&lif->operand, &element->params[2], sizeof(lif->operand)); if (element->params_count > 3) lif->num_options = element->params[3].data.number; else lif->num_options = TOKEN_VALUE_ONLY; return 0; } static int parse_timeout_tag(struct skin_element *element, struct wps_token *token, struct wps_data *wps_data) { (void)wps_data; int val = 0; if (element->params_count == 0) { switch (token->type) { case SKIN_TOKEN_SUBLINE_TIMEOUT: return -1; case SKIN_TOKEN_BUTTON_VOLUME: case SKIN_TOKEN_TRACK_STARTING: case SKIN_TOKEN_TRACK_ENDING: val = 10; break; default: break; } } else val = element->params[0].data.number; token->value.i = val * TIMEOUT_UNIT; return 0; } static int parse_progressbar_tag(struct skin_element* element, struct wps_token *token, struct wps_data *wps_data) { #ifdef HAVE_LCD_BITMAP struct progressbar *pb; struct viewport *vp = &curr_vp->vp; struct skin_tag_parameter *param = element->params; int curr_param = 0; char *image_filename = NULL; if (element->params_count == 0 && element->tag->type != SKIN_TOKEN_PROGRESSBAR) return 0; /* nothing to do */ pb = (struct progressbar*)skin_buffer_alloc(sizeof(struct progressbar)); token->value.data = pb; if (!pb) return WPS_ERROR_INVALID_PARAM; pb->vp = vp; pb->follow_lang_direction = follow_lang_direction > 0; pb->nofill = false; pb->nobar = false; pb->image = NULL; pb->slider = NULL; pb->backdrop = NULL; pb->invert_fill_direction = false; pb->horizontal = true; if (element->params_count == 0) { pb->x = 0; pb->width = vp->width; pb->height = SYSFONT_HEIGHT-2; pb->y = -1; /* Will be computed during the rendering */ pb->type = element->tag->type; return 0; } /* (x, y, width, height, ...) */ if (!isdefault(param)) pb->x = param->data.number; else pb->x = 0; param++; if (!isdefault(param)) pb->y = param->data.number; else pb->y = -1; /* computed at rendering */ param++; if (!isdefault(param)) pb->width = param->data.number; else pb->width = vp->width - pb->x; param++; if (!isdefault(param)) { /* A zero height makes no sense - reject it */ if (param->data.number == 0) return WPS_ERROR_INVALID_PARAM; pb->height = param->data.number; } else { if (vp->font > FONT_UI) pb->height = -1; /* calculate at display time */ else { #ifndef __PCTOOL__ pb->height = font_get(vp->font)->height; #else pb->height = 8; #endif } } /* optional params, first is the image filename if it isnt recognised as a keyword */ curr_param = 4; if (isdefault(&element->params[curr_param])) { param++; curr_param++; } pb->horizontal = pb->width > pb->height; while (curr_param < element->params_count) { param++; if (!strcmp(param->data.text, "invert")) pb->invert_fill_direction = true; else if (!strcmp(param->data.text, "nofill")) pb->nofill = true; else if (!strcmp(param->data.text, "nobar")) pb->nobar = true; else if (!strcmp(param->data.text, "slider")) { if (curr_param+1 < element->params_count) { curr_param++; param++; pb->slider = skin_find_item(param->data.text, SKIN_FIND_IMAGE, wps_data); } else /* option needs the next param */ return -1; } else if (!strcmp(param->data.text, "image")) { if (curr_param+1 < element->params_count) { curr_param++; param++; image_filename = param->data.text; } else /* option needs the next param */ return -1; } else if (!strcmp(param->data.text, "backdrop")) { if (curr_param+1 < element->params_count) { curr_param++; param++; pb->backdrop = skin_find_item(param->data.text, SKIN_FIND_IMAGE, wps_data); } else /* option needs the next param */ return -1; } else if (!strcmp(param->data.text, "vertical")) { pb->horizontal = false; if (isdefault(&element->params[3])) pb->height = vp->height - pb->y; } else if (!strcmp(param->data.text, "horizontal")) pb->horizontal = true; else if (curr_param == 4) image_filename = param->data.text; curr_param++; } if (image_filename) { pb->image = skin_find_item(image_filename, SKIN_FIND_IMAGE, wps_data); if (!pb->image) /* load later */ { struct gui_img* img = (struct gui_img*)skin_buffer_alloc(sizeof(struct gui_img)); if (!img) return WPS_ERROR_INVALID_PARAM; /* save a pointer to the filename */ img->bm.data = (char*)image_filename; img->label = image_filename; img->x = 0; img->y = 0; img->num_subimages = 1; img->always_display = false; img->display = -1; img->using_preloaded_icons = false; img->vp = &curr_vp->vp; struct skin_token_list *item = (struct skin_token_list *)new_skin_token_list_item(NULL, img); if (!item) return WPS_ERROR_INVALID_PARAM; add_to_ll_chain(&wps_data->images, item); pb->image = img; } } if (token->type == SKIN_TOKEN_VOLUME) token->type = SKIN_TOKEN_VOLUMEBAR; else if (token->type == SKIN_TOKEN_BATTERY_PERCENT) token->type = SKIN_TOKEN_BATTERY_PERCENTBAR; else if (token->type == SKIN_TOKEN_TUNER_RSSI) token->type = SKIN_TOKEN_TUNER_RSSI_BAR; else if (token->type == SKIN_TOKEN_PEAKMETER_LEFT) token->type = SKIN_TOKEN_PEAKMETER_LEFTBAR; else if (token->type == SKIN_TOKEN_PEAKMETER_RIGHT) token->type = SKIN_TOKEN_PEAKMETER_RIGHTBAR; pb->type = token->type; return 0; #else (void)element; if (token->type == SKIN_TOKEN_PROGRESSBAR || token->type == SKIN_TOKEN_PLAYER_PROGRESSBAR) { wps_data->full_line_progressbar = token->type == SKIN_TOKEN_PLAYER_PROGRESSBAR; } return 0; #endif } #ifdef HAVE_ALBUMART static int parse_albumart_load(struct skin_element* element, struct wps_token *token, struct wps_data *wps_data) { struct dim dimensions; int albumart_slot; bool swap_for_rtl = lang_is_rtl() && follow_lang_direction; struct skin_albumart *aa = (struct skin_albumart *)skin_buffer_alloc(sizeof(struct skin_albumart)); (void)token; /* silence warning */ if (!aa) return -1; /* reset albumart info in wps */ aa->width = -1; aa->height = -1; aa->xalign = WPS_ALBUMART_ALIGN_CENTER; /* default */ aa->yalign = WPS_ALBUMART_ALIGN_CENTER; /* default */ aa->x = element->params[0].data.number; aa->y = element->params[1].data.number; aa->width = element->params[2].data.number; aa->height = element->params[3].data.number; aa->vp = &curr_vp->vp; aa->draw_handle = -1; /* if we got here, we parsed everything ok .. ! */ if (aa->width < 0) aa->width = 0; else if (aa->width > LCD_WIDTH) aa->width = LCD_WIDTH; if (aa->height < 0) aa->height = 0; else if (aa->height > LCD_HEIGHT) aa->height = LCD_HEIGHT; if (swap_for_rtl) aa->x = LCD_WIDTH - (aa->x + aa->width); aa->state = WPS_ALBUMART_LOAD; wps_data->albumart = aa; dimensions.width = aa->width; dimensions.height = aa->height; albumart_slot = playback_claim_aa_slot(&dimensions); if (0 <= albumart_slot) wps_data->playback_aa_slot = albumart_slot; if (element->params_count > 4 && !isdefault(&element->params[4])) { switch (*element->params[4].data.text) { case 'l': case 'L': if (swap_for_rtl) aa->xalign = WPS_ALBUMART_ALIGN_RIGHT; else aa->xalign = WPS_ALBUMART_ALIGN_LEFT; break; case 'c': case 'C': aa->xalign = WPS_ALBUMART_ALIGN_CENTER; break; case 'r': case 'R': if (swap_for_rtl) aa->xalign = WPS_ALBUMART_ALIGN_LEFT; else aa->xalign = WPS_ALBUMART_ALIGN_RIGHT; break; } } if (element->params_count > 5 && !isdefault(&element->params[5])) { switch (*element->params[5].data.text) { case 't': case 'T': aa->yalign = WPS_ALBUMART_ALIGN_TOP; break; case 'c': case 'C': aa->yalign = WPS_ALBUMART_ALIGN_CENTER; break; case 'b': case 'B': aa->yalign = WPS_ALBUMART_ALIGN_BOTTOM; break; } } return 0; } #endif /* HAVE_ALBUMART */ #ifdef HAVE_SKIN_VARIABLES static struct skin_var* find_or_add_var(const char* label, struct wps_data *data) { struct skin_var* ret = skin_find_item(label, SKIN_VARIABLE, data); if (!ret) { ret = (struct skin_var*)skin_buffer_alloc(sizeof(struct skin_var)); if (!ret) return NULL; ret->label = label; ret->value = 1; ret->last_changed = 0xffff; struct skin_token_list *item = new_skin_token_list_item(NULL, ret); if (!item) return NULL; add_to_ll_chain(&data->skinvars, item); } return ret; } static int parse_skinvar( struct skin_element *element, struct wps_token *token, struct wps_data *wps_data) { const char* label = element->params[0].data.text; struct skin_var* var = find_or_add_var(label, wps_data); if (!var) return WPS_ERROR_INVALID_PARAM; switch (token->type) { case SKIN_TOKEN_VAR_GETVAL: token->value.data = var; break; case SKIN_TOKEN_VAR_SET: { struct skin_var_changer *data = (struct skin_var_changer*)skin_buffer_alloc( sizeof(struct skin_var_changer)); if (!data) return WPS_ERROR_INVALID_PARAM; data->var = var; data->newval = element->params[2].data.number; data->max = 0; if (!strcmp(element->params[1].data.text, "set")) data->direct = true; else if (!strcmp(element->params[1].data.text, "inc")) { data->direct = false; } else if (!strcmp(element->params[1].data.text, "dec")) { data->direct = false; data->newval *= -1; } if (element->params_count > 3) data->max = element->params[3].data.number; token->value.data = data; } break; case SKIN_TOKEN_VAR_TIMEOUT: { struct skin_var_lastchange *data = (struct skin_var_lastchange*)skin_buffer_alloc( sizeof(struct skin_var_lastchange)); if (!data) return WPS_ERROR_INVALID_PARAM; data->var = var; data->timeout = 10; if (element->params_count > 1) data->timeout = element->params[1].data.number; data->timeout *= TIMEOUT_UNIT; token->value.data = data; } break; default: /* kill the warning */ break; } return 0; } #endif /* HAVE_SKIN_VARIABLES */ #ifdef HAVE_TOUCHSCREEN static int parse_lasttouch(struct skin_element *element, struct wps_token *token, struct wps_data *wps_data) { struct touchregion_lastpress *data = (struct touchregion_lastpress*)skin_buffer_alloc( sizeof(struct touchregion_lastpress)); int i; if (!data) return WPS_ERROR_INVALID_PARAM; data->region = NULL; data->timeout = 10; for (i=0; iparams_count; i++) { if (element->params[i].type == STRING) data->region = skin_find_item(element->params[i].data.text, SKIN_FIND_TOUCHREGION, wps_data); else if (element->params[i].type == INTEGER) data->timeout = element->params[i].data.number; } data->timeout *= TIMEOUT_UNIT; token->value.data = data; return 0; } struct touchaction {const char* s; int action;}; static const struct touchaction touchactions[] = { /* generic actions, convert to screen actions on use */ {"none", ACTION_TOUCHSCREEN}, {"prev", ACTION_STD_PREV }, {"next", ACTION_STD_NEXT }, {"rwd", ACTION_STD_PREVREPEAT }, {"ffwd", ACTION_STD_NEXTREPEAT }, {"hotkey", ACTION_STD_HOTKEY}, {"select", ACTION_STD_OK }, {"menu", ACTION_STD_MENU }, {"cancel", ACTION_STD_CANCEL }, {"contextmenu", ACTION_STD_CONTEXT},{"quickscreen", ACTION_STD_QUICKSCREEN }, /* list/tree actions */ { "resumeplayback", ACTION_TREE_WPS}, /* returns to previous music, WPS/FM */ /* not really WPS specific, but no equivilant ACTION_STD_* */ {"voldown", ACTION_WPS_VOLDOWN}, {"volup", ACTION_WPS_VOLUP}, {"mute", ACTION_TOUCH_MUTE }, /* generic settings changers */ {"setting_inc", ACTION_SETTINGS_INC}, {"setting_dec", ACTION_SETTINGS_DEC}, {"setting_set", ACTION_SETTINGS_SET}, /* WPS specific actions */ {"wps_prev", ACTION_WPS_SKIPPREV }, {"wps_next", ACTION_WPS_SKIPNEXT }, {"browse", ACTION_WPS_BROWSE }, {"play", ACTION_WPS_PLAY }, {"stop", ACTION_WPS_STOP }, {"shuffle", ACTION_TOUCH_SHUFFLE }, {"repmode", ACTION_TOUCH_REPMODE }, {"pitch", ACTION_WPS_PITCHSCREEN}, {"playlist", ACTION_WPS_VIEW_PLAYLIST }, #if CONFIG_TUNER /* FM screen actions */ /* Also allow browse, play, stop from WPS codes */ {"mode", ACTION_FM_MODE }, {"record", ACTION_FM_RECORD }, {"presets", ACTION_FM_PRESET}, #endif }; bool cfg_string_to_int(int setting_id, int* out, const char* str); static int parse_touchregion(struct skin_element *element, struct wps_token *token, struct wps_data *wps_data) { (void)token; unsigned i, imax; int p; struct touchregion *region = NULL; const char *action; const char pb_string[] = "progressbar"; const char vol_string[] = "volume"; char temp[20]; /* format: %T([label,], x,y,width,height,action[, ...]) * if action starts with & the area must be held to happen */ region = (struct touchregion*)skin_buffer_alloc(sizeof(struct touchregion)); if (!region) return WPS_ERROR_INVALID_PARAM; /* should probably do some bounds checking here with the viewport... but later */ region->action = ACTION_NONE; if (element->params[0].type == STRING) { region->label = element->params[0].data.text; p = 1; /* "[SI]III[SI]|SS" is the param list. There MUST be 4 numbers * followed by at least one string. Verify that here */ if (element->params_count < 6 || element->params[4].type != INTEGER) return WPS_ERROR_INVALID_PARAM; } else { region->label = NULL; p = 0; } region->x = element->params[p++].data.number; region->y = element->params[p++].data.number; region->width = element->params[p++].data.number; region->height = element->params[p++].data.number; region->wvp = curr_vp; region->armed = false; region->reverse_bar = false; region->value = 0; region->last_press = 0xffff; region->press_length = PRESS; action = element->params[p++].data.text; strcpy(temp, action); action = temp; if (*action == '!') { region->reverse_bar = true; action++; } if(!strcmp(pb_string, action)) region->action = ACTION_TOUCH_SCROLLBAR; else if(!strcmp(vol_string, action)) region->action = ACTION_TOUCH_VOLUME; else { if (*action == '&') { action++; region->press_length = LONG_PRESS; } else if(*action == '*') { action++; region->press_length = REPEAT; } else region->press_length = PRESS; imax = ARRAYLEN(touchactions); for (i = 0; i < imax; i++) { /* try to match with one of our touchregion screens */ if (!strcmp(touchactions[i].s, action)) { region->action = touchactions[i].action; if (region->action == ACTION_SETTINGS_INC || region->action == ACTION_SETTINGS_DEC || region->action == ACTION_SETTINGS_SET) { if (element->params_count < p+1) { return WPS_ERROR_INVALID_PARAM; } else { char *name = element->params[p].data.text; int j; /* Find the setting */ for (j=0; jsetting_data.setting = (void*)&settings[j]; if (region->action == ACTION_SETTINGS_SET) { char* text; int temp; struct touchsetting *setting = ®ion->setting_data; if (element->params_count < p+2) return WPS_ERROR_INVALID_PARAM; #ifndef __PCTOOL__ text = element->params[p+1].data.text; switch (settings[j].flags&F_T_MASK) { case F_T_CUSTOM: setting->value.text = text; break; case F_T_INT: case F_T_UINT: if (settings[j].cfg_vals == NULL) { setting->value.number = atoi(text); } else if (cfg_string_to_int(j, &temp, text)) { if (settings[j].flags&F_TABLE_SETTING) setting->value.number = settings[j].table_setting->values[temp]; else setting->value.number = temp; } else return WPS_ERROR_INVALID_PARAM; break; case F_T_BOOL: if (cfg_string_to_int(j, &temp, text)) { setting->value.number = temp; } else return WPS_ERROR_INVALID_PARAM; break; default: return WPS_ERROR_INVALID_PARAM; } #endif /* __PCTOOL__ */ } } } break; } } if (region->action == ACTION_NONE) return WPS_ERROR_INVALID_PARAM; } struct skin_token_list *item = new_skin_token_list_item(NULL, region); if (!item) return WPS_ERROR_INVALID_PARAM; add_to_ll_chain(&wps_data->touchregions, item); if (region->action == ACTION_TOUCH_MUTE) { region->value = global_settings.volume; } return 0; } #endif static bool check_feature_tag(const int type) { switch (type) { case SKIN_TOKEN_RTC_PRESENT: #if CONFIG_RTC return true; #else return false; #endif case SKIN_TOKEN_HAVE_RECORDING: #ifdef HAVE_RECORDING return true; #else return false; #endif case SKIN_TOKEN_HAVE_TUNER: #if CONFIG_TUNER if (radio_hardware_present()) return true; #endif return false; case SKIN_TOKEN_HAVE_TOUCH: #ifdef HAVE_TOUCHSCREEN return true; #else return false; #endif #if CONFIG_TUNER case SKIN_TOKEN_HAVE_RDS: #ifdef HAVE_RDS_CAP return true; #else return false; #endif /* HAVE_RDS_CAP */ #endif /* CONFIG_TUNER */ default: /* not a tag we care about, just don't skip */ return true; } } /* * initial setup of wps_data; does reset everything * except fields which need to survive, i.e. * **/ static void skin_data_reset(struct wps_data *wps_data) { wps_data->tree = NULL; #ifdef HAVE_LCD_BITMAP wps_data->images = NULL; #endif #if LCD_DEPTH > 1 || defined(HAVE_REMOTE_LCD) && LCD_REMOTE_DEPTH > 1 if (wps_data->backdrop_id >= 0) skin_backdrop_unload(wps_data->backdrop_id); wps_data->backdrop = NULL; #endif #ifdef HAVE_TOUCHSCREEN wps_data->touchregions = NULL; #endif #ifdef HAVE_SKIN_VARIABLES wps_data->skinvars = NULL; #endif #ifdef HAVE_ALBUMART wps_data->albumart = NULL; if (wps_data->playback_aa_slot >= 0) { playback_release_aa_slot(wps_data->playback_aa_slot); wps_data->playback_aa_slot = -1; } #endif #ifdef HAVE_LCD_BITMAP wps_data->peak_meter_enabled = false; wps_data->wps_sb_tag = false; wps_data->show_sb_on_wps = false; #else /* HAVE_LCD_CHARCELLS */ /* progress bars */ int i; for (i = 0; i < 8; i++) { wps_data->wps_progress_pat[i] = 0; } wps_data->full_line_progressbar = false; #endif wps_data->wps_loaded = false; } #ifdef HAVE_LCD_BITMAP static bool load_skin_bmp(struct wps_data *wps_data, struct bitmap *bitmap, char* bmpdir) { (void)wps_data; /* only needed for remote targets */ char img_path[MAX_PATH]; int fd; get_image_filename(bitmap->data, bmpdir, img_path, sizeof(img_path)); /* load the image */ int format; #ifdef HAVE_REMOTE_LCD if (curr_screen == SCREEN_REMOTE) format = FORMAT_ANY|FORMAT_REMOTE; else #endif format = FORMAT_ANY|FORMAT_TRANSPARENT; fd = open(img_path, O_RDONLY); if (fd < 0) { DEBUGF("Couldn't open %s\n", img_path); return false; } size_t buf_size = read_bmp_fd(fd, bitmap, 0, format|FORMAT_RETURN_SIZE, NULL); char* imgbuf = (char*)skin_buffer_alloc(buf_size); if (!imgbuf) { #ifndef APPLICATION DEBUGF("Not enough skin buffer: need %zd more.\n", buf_size - skin_buffer_freespace()); #endif close(fd); return NULL; } lseek(fd, 0, SEEK_SET); bitmap->data = imgbuf; int ret = read_bmp_fd(fd, bitmap, buf_size, format, NULL); close(fd); if (ret > 0) { return true; } else { /* Abort if we can't load an image */ DEBUGF("Couldn't load '%s'\n", img_path); return false; } } static bool load_skin_bitmaps(struct wps_data *wps_data, char *bmpdir) { struct skin_token_list *list; bool retval = true; /* return false if a single image failed to load */ /* regular images */ list = wps_data->images; while (list) { struct gui_img *img = (struct gui_img*)list->token->value.data; if (img->bm.data) { if (img->using_preloaded_icons) { img->loaded = true; list->token->type = SKIN_TOKEN_IMAGE_DISPLAY_LISTICON; } else { img->loaded = load_skin_bmp(wps_data, &img->bm, bmpdir); if (img->loaded) img->subimage_height = img->bm.height / img->num_subimages; else retval = false; } } list = list->next; } #if (LCD_DEPTH > 1) || (defined(HAVE_REMOTE_LCD) && (LCD_REMOTE_DEPTH > 1)) wps_data->backdrop_id = skin_backdrop_assign(wps_data->backdrop, bmpdir, curr_screen); #endif /* has backdrop support */ return retval; } static bool skin_load_fonts(struct wps_data *data) { /* don't spit out after the first failue to aid debugging */ bool success = true; struct skin_element *vp_list; int font_id; /* walk though each viewport and assign its font */ for(vp_list = data->tree; vp_list; vp_list = vp_list->next) { /* first, find the viewports that have a non-sys/ui-font font */ struct skin_viewport *skin_vp = (struct skin_viewport*)vp_list->data; struct viewport *vp = &skin_vp->vp; if (vp->font <= FONT_UI) { /* the usual case -> built-in fonts */ #ifdef HAVE_REMOTE_LCD if (vp->font == FONT_UI) vp->font += curr_screen; #endif continue; } font_id = vp->font; /* now find the corresponding skin_font */ struct skin_font *font = &skinfonts[font_id-FONT_FIRSTUSERFONT]; if (!font->name) { if (success) { DEBUGF("font %d not specified\n", font_id); } success = false; continue; } /* load the font - will handle loading the same font again if * multiple viewports use the same */ if (font->id < 0) { char *dot = strchr(font->name, '.'); *dot = '\0'; font->id = skin_font_load(font->name, skinfonts[font_id-FONT_FIRSTUSERFONT].glyphs); } if (font->id < 0) { DEBUGF("Unable to load font %d: '%s.fnt'\n", font_id, font->name); font->name = NULL; /* to stop trying to load it again if we fail */ success = false; font->name = NULL; continue; } /* finally, assign the font_id to the viewport */ vp->font = font->id; } return success; } #endif /* HAVE_LCD_BITMAP */ static int convert_viewport(struct wps_data *data, struct skin_element* element) { struct skin_viewport *skin_vp = (struct skin_viewport *)skin_buffer_alloc(sizeof(struct skin_viewport)); struct screen *display = &screens[curr_screen]; if (!skin_vp) return CALLBACK_ERROR; skin_vp->hidden_flags = 0; skin_vp->label = NULL; skin_vp->is_infovp = false; element->data = skin_vp; curr_vp = skin_vp; curr_viewport_element = element; viewport_set_defaults(&skin_vp->vp, curr_screen); #ifdef HAVE_REMOTE_LCD /* viewport_set_defaults() sets the font to FONT_UI+curr_screen. * This parser requires font 1 to always be the UI font, * so force it back to FONT_UI and handle the screen number at the end */ skin_vp->vp.font = FONT_UI; #endif #if (LCD_DEPTH > 1) || (defined(HAVE_REMOTE_LCD) && (LCD_REMOTE_DEPTH > 1)) skin_vp->start_fgcolour = skin_vp->vp.fg_pattern; skin_vp->start_bgcolour = skin_vp->vp.bg_pattern; #endif struct skin_tag_parameter *param = element->params; if (element->params_count == 0) /* default viewport */ { if (!data->tree) /* first viewport in the skin */ data->tree = element; skin_vp->label = VP_DEFAULT_LABEL; return CALLBACK_OK; } if (element->params_count == 6) { if (element->tag->type == SKIN_TOKEN_UIVIEWPORT_LOAD) { skin_vp->is_infovp = true; if (isdefault(param)) { skin_vp->hidden_flags = VP_NEVER_VISIBLE; skin_vp->label = VP_DEFAULT_LABEL; } else { skin_vp->hidden_flags = VP_NEVER_VISIBLE; skin_vp->label = param->data.text; } } else { skin_vp->hidden_flags = VP_DRAW_HIDEABLE|VP_DRAW_HIDDEN; skin_vp->label = param->data.text; } param++; } /* x */ if (!isdefault(param)) { skin_vp->vp.x = param->data.number; if (param->data.number < 0) skin_vp->vp.x += display->lcdwidth; } param++; /* y */ if (!isdefault(param)) { skin_vp->vp.y = param->data.number; if (param->data.number < 0) skin_vp->vp.y += display->lcdheight; } param++; /* width */ if (!isdefault(param)) { skin_vp->vp.width = param->data.number; if (param->data.number < 0) skin_vp->vp.width = (skin_vp->vp.width + display->lcdwidth) - skin_vp->vp.x; } else { skin_vp->vp.width = display->lcdwidth - skin_vp->vp.x; } param++; /* height */ if (!isdefault(param)) { skin_vp->vp.height = param->data.number; if (param->data.number < 0) skin_vp->vp.height = (skin_vp->vp.height + display->lcdheight) - skin_vp->vp.y; } else { skin_vp->vp.height = display->lcdheight - skin_vp->vp.y; } param++; #ifdef HAVE_LCD_BITMAP /* font */ if (!isdefault(param)) { skin_vp->vp.font = param->data.number; } #endif if ((unsigned) skin_vp->vp.x >= (unsigned) display->lcdwidth || skin_vp->vp.width + skin_vp->vp.x > display->lcdwidth || (unsigned) skin_vp->vp.y >= (unsigned) display->lcdheight || skin_vp->vp.height + skin_vp->vp.y > display->lcdheight) return CALLBACK_ERROR; return CALLBACK_OK; } static int skin_element_callback(struct skin_element* element, void* data) { struct wps_data *wps_data = (struct wps_data *)data; struct wps_token *token; parse_function function = NULL; switch (element->type) { /* IMPORTANT: element params are shared, so copy them if needed * or use then NOW, dont presume they have a long lifespan */ case TAG: { token = (struct wps_token*)skin_buffer_alloc(sizeof(struct wps_token)); memset(token, 0, sizeof(*token)); token->type = element->tag->type; if (element->tag->flags&SKIN_RTC_REFRESH) { #if CONFIG_RTC curr_line->update_mode |= SKIN_REFRESH_DYNAMIC; #else curr_line->update_mode |= SKIN_REFRESH_STATIC; #endif } else curr_line->update_mode |= element->tag->flags&SKIN_REFRESH_ALL; element->data = token; /* Some tags need special handling for the tag, so add them here */ switch (token->type) { case SKIN_TOKEN_ALIGN_LANGDIRECTION: follow_lang_direction = 2; break; case SKIN_TOKEN_LOGICAL_IF: function = parse_logical_if; break; case SKIN_TOKEN_PROGRESSBAR: case SKIN_TOKEN_VOLUME: case SKIN_TOKEN_BATTERY_PERCENT: case SKIN_TOKEN_PLAYER_PROGRESSBAR: case SKIN_TOKEN_PEAKMETER_LEFT: case SKIN_TOKEN_PEAKMETER_RIGHT: #ifdef HAVE_RADIO_RSSI case SKIN_TOKEN_TUNER_RSSI: #endif function = parse_progressbar_tag; break; case SKIN_TOKEN_SUBLINE_TIMEOUT: case SKIN_TOKEN_BUTTON_VOLUME: case SKIN_TOKEN_TRACK_STARTING: case SKIN_TOKEN_TRACK_ENDING: function = parse_timeout_tag; break; #ifdef HAVE_LCD_BITMAP case SKIN_TOKEN_DISABLE_THEME: case SKIN_TOKEN_ENABLE_THEME: case SKIN_TOKEN_DRAW_INBUILTBAR: function = parse_statusbar_tags; break; case SKIN_TOKEN_LIST_TITLE_TEXT: #ifndef __PCTOOL__ sb_skin_has_title(curr_screen); #endif break; #endif case SKIN_TOKEN_FILE_DIRECTORY: token->value.i = element->params[0].data.number; break; #if (LCD_DEPTH > 1) || (defined(HAVE_REMOTE_LCD) && (LCD_REMOTE_DEPTH > 1)) case SKIN_TOKEN_VIEWPORT_FGCOLOUR: case SKIN_TOKEN_VIEWPORT_BGCOLOUR: function = parse_viewportcolour; break; case SKIN_TOKEN_IMAGE_BACKDROP: function = parse_image_special; break; #endif case SKIN_TOKEN_TRANSLATEDSTRING: case SKIN_TOKEN_SETTING: function = parse_setting_and_lang; break; #ifdef HAVE_LCD_BITMAP case SKIN_TOKEN_VIEWPORT_CUSTOMLIST: function = parse_playlistview; break; case SKIN_TOKEN_LOAD_FONT: function = parse_font_load; break; case SKIN_TOKEN_VIEWPORT_ENABLE: case SKIN_TOKEN_UIVIEWPORT_ENABLE: token->value.data = element->params[0].data.text; break; case SKIN_TOKEN_IMAGE_PRELOAD_DISPLAY: function = parse_image_display; break; case SKIN_TOKEN_IMAGE_PRELOAD: case SKIN_TOKEN_IMAGE_DISPLAY: function = parse_image_load; break; #endif #ifdef HAVE_TOUCHSCREEN case SKIN_TOKEN_TOUCHREGION: function = parse_touchregion; break; case SKIN_TOKEN_LASTTOUCH: function = parse_lasttouch; break; #endif #ifdef HAVE_ALBUMART case SKIN_TOKEN_ALBUMART_DISPLAY: if (wps_data->albumart) wps_data->albumart->vp = &curr_vp->vp; break; case SKIN_TOKEN_ALBUMART_LOAD: function = parse_albumart_load; break; #endif #ifdef HAVE_SKIN_VARIABLES case SKIN_TOKEN_VAR_SET: case SKIN_TOKEN_VAR_GETVAL: case SKIN_TOKEN_VAR_TIMEOUT: function = parse_skinvar; break; #endif default: break; } if (function) { if (function(element, token, wps_data) < 0) return CALLBACK_ERROR; } /* tags that start with 'F', 'I' or 'D' are for the next file */ if ( *(element->tag->name) == 'I' || *(element->tag->name) == 'F' || *(element->tag->name) == 'D') token->next = true; if (follow_lang_direction > 0 ) follow_lang_direction--; break; } case VIEWPORT: return convert_viewport(wps_data, element); case LINE: { struct line *line = (struct line *)skin_buffer_alloc(sizeof(struct line)); line->update_mode = SKIN_REFRESH_STATIC; curr_line = line; element->data = line; } break; case LINE_ALTERNATOR: { struct line_alternator *alternator = (struct line_alternator *)skin_buffer_alloc(sizeof(struct line_alternator)); alternator->current_line = 0; #ifndef __PCTOOL__ alternator->next_change_tick = current_tick; #endif element->data = alternator; } break; case CONDITIONAL: { struct conditional *conditional = (struct conditional *)skin_buffer_alloc(sizeof(struct conditional)); conditional->last_value = -1; conditional->token = element->data; element->data = conditional; if (!check_feature_tag(element->tag->type)) { return FEATURE_NOT_AVAILABLE; } return CALLBACK_OK; } case TEXT: curr_line->update_mode |= SKIN_REFRESH_STATIC; break; default: break; } return CALLBACK_OK; } /* to setup up the wps-data from a format-buffer (isfile = false) from a (wps-)file (isfile = true)*/ bool skin_data_load(enum screen_type screen, struct wps_data *wps_data, const char *buf, bool isfile) { char *wps_buffer = NULL; if (!wps_data || !buf) return false; #ifdef HAVE_ALBUMART int status; struct mp3entry *curtrack; long offset; struct skin_albumart old_aa = {.state = WPS_ALBUMART_NONE}; if (wps_data->albumart) { old_aa.state = wps_data->albumart->state; old_aa.height = wps_data->albumart->height; old_aa.width = wps_data->albumart->width; } #endif #ifdef HAVE_LCD_BITMAP int i; for (i=0;iwps_loaded = false; curr_screen = screen; curr_line = NULL; curr_vp = NULL; curr_viewport_element = NULL; if (isfile) { int fd = open_utf8(buf, O_RDONLY); if (fd < 0) return false; /* get buffer space from the plugin buffer */ size_t buffersize = 0; wps_buffer = (char *)plugin_get_buffer(&buffersize); if (!wps_buffer) return false; /* copy the file's content to the buffer for parsing, ensuring that every line ends with a newline char. */ unsigned int start = 0; while(read_line(fd, wps_buffer + start, buffersize - start) > 0) { start += strlen(wps_buffer + start); if (start < buffersize - 1) { wps_buffer[start++] = '\n'; wps_buffer[start] = 0; } } close(fd); if (start <= 0) return false; } else { wps_buffer = (char*)buf; } #if LCD_DEPTH > 1 || defined(HAVE_REMOTE_LCD) && LCD_REMOTE_DEPTH > 1 wps_data->backdrop = "-"; wps_data->backdrop_id = -1; #endif /* parse the skin source */ #ifndef APPLICATION skin_buffer_save_position(); #endif wps_data->tree = skin_parse(wps_buffer, skin_element_callback, wps_data); if (!wps_data->tree) { skin_data_reset(wps_data); #ifndef APPLICATION skin_buffer_restore_position(); #endif return false; } #ifdef HAVE_LCD_BITMAP char bmpdir[MAX_PATH]; if (isfile) { /* get the bitmap dir */ char *dot = strrchr(buf, '.'); strlcpy(bmpdir, buf, dot - buf + 1); } else { snprintf(bmpdir, MAX_PATH, "%s", BACKDROP_DIR); } /* load the bitmaps that were found by the parsing */ if (!load_skin_bitmaps(wps_data, bmpdir) || !skin_load_fonts(wps_data)) { skin_data_reset(wps_data); #ifndef APPLICATION skin_buffer_restore_position(); #endif return false; } #endif #if defined(HAVE_ALBUMART) && !defined(__PCTOOL__) status = audio_status(); if (status & AUDIO_STATUS_PLAY) { struct skin_albumart *aa = wps_data->albumart; if (aa && ((aa->state && !old_aa.state) || (aa->state && (((old_aa.height != aa->height) || (old_aa.width != aa->width)))))) { curtrack = audio_current_track(); offset = curtrack->offset; audio_stop(); if (!(status & AUDIO_STATUS_PAUSE)) audio_play(offset); } } #endif wps_data->wps_loaded = true; #ifdef DEBUG_SKIN_ENGINE // if (isfile && debug_wps) // debug_skin_usage(); #endif return true; }