mirror of
https://github.com/google/pebble.git
synced 2026-02-14 09:27:20 -05:00
Import of the watch repository from Pebble
This commit is contained in:
544
src/fw/process_management/app_menu_data_source.c
Normal file
544
src/fw/process_management/app_menu_data_source.c
Normal file
@@ -0,0 +1,544 @@
|
||||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "app_menu_data_source.h"
|
||||
|
||||
#include "applib/fonts/fonts.h"
|
||||
#include "applib/ui/menu_layer.h"
|
||||
#include "apps/system_app_ids.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "process_management/app_manager.h"
|
||||
#include "resource/resource_ids.auto.h"
|
||||
#include "services/common/i18n/i18n.h"
|
||||
#include "services/normal/process_management/app_order_storage.h"
|
||||
#include "system/passert.h"
|
||||
#include "util/size.h"
|
||||
|
||||
#include "apps/system_apps/activity_demo_app.h"
|
||||
#include "shell/prefs.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
static void add_app_with_install_id(const AppInstallEntry *entry, AppMenuDataSource *source);
|
||||
static bool remove_app_with_install_id(const AppInstallId install_id, AppMenuDataSource *source);
|
||||
static AppMenuNode * prv_find_node_with_install_id(const AppInstallId install_id,
|
||||
const AppMenuDataSource * const source);
|
||||
static void prv_unload_node(const AppMenuDataSource *source, AppMenuNode *node);
|
||||
|
||||
////////////////////////////////
|
||||
// List helper functions
|
||||
////////////////////////////////
|
||||
|
||||
static bool prv_is_app_filtered_out(AppInstallEntry *entry, AppMenuDataSource * const source) {
|
||||
return (source->callbacks.filter && (source->callbacks.filter(source, entry) == false));
|
||||
}
|
||||
|
||||
/////////////////////////
|
||||
// Order List helpers
|
||||
/////////////////////////
|
||||
|
||||
//! Place these in the order that is desired in the Launcher.
|
||||
//! Set `move_on_activity` to true if you only want the item to jump to the top during communication
|
||||
//! The movement will not happen while looking at the launcher, it will only refresh on a
|
||||
//! close->open
|
||||
static const struct {
|
||||
AppInstallId install_id;
|
||||
bool move_on_activity;
|
||||
} s_override_table[] = {
|
||||
{ APP_ID_SPORTS, false },
|
||||
{ APP_ID_GOLF, false },
|
||||
#ifdef APP_ID_WORKOUT
|
||||
{ APP_ID_WORKOUT, true },
|
||||
#endif
|
||||
{ APP_ID_MUSIC, true },
|
||||
};
|
||||
|
||||
// Returns 0 if not in table. Otherwise, returns the rank in `s_override_table`. Rank is
|
||||
// where the lowest index returns the highest rank
|
||||
static int prv_override_index(AppInstallId app_id) {
|
||||
const uint32_t num_overrides = ARRAY_LENGTH(s_override_table);
|
||||
for (uint32_t i = 0; i < num_overrides; i++) {
|
||||
AppInstallId cur_id = s_override_table[i].install_id;
|
||||
if (cur_id == app_id) {
|
||||
const bool should_move = (!s_override_table[i].move_on_activity ||
|
||||
app_install_is_prioritized(cur_id));
|
||||
return (should_move) ? (num_overrides - i + 1) : 0;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int prv_app_override_comparator(AppInstallId app_id, AppInstallId new_id) {
|
||||
return (prv_override_index(app_id) - prv_override_index(new_id));
|
||||
}
|
||||
|
||||
static int prv_comparator_ascending_zero_last(unsigned int a, unsigned int b) {
|
||||
return ((a != 0) && (b != 0)) ? (b - a) : // Sort in ascending order
|
||||
(a - b); // 0 should be sorted last so invert the sort
|
||||
}
|
||||
|
||||
T_STATIC int prv_app_node_comparator(void *app_node_ref, void *new_node_ref) {
|
||||
const AppMenuNode *app_node = app_node_ref;
|
||||
const AppMenuNode *new_node = new_node_ref;
|
||||
|
||||
const bool is_app_quick_launch = (app_node->visibility == ProcessVisibilityQuickLaunch);
|
||||
const bool is_new_quick_launch = (new_node->visibility == ProcessVisibilityQuickLaunch);
|
||||
const int override_cmp_rv = prv_app_override_comparator(app_node->install_id,
|
||||
new_node->install_id);
|
||||
if (is_app_quick_launch != is_new_quick_launch) {
|
||||
// Quick Launch only apps are first
|
||||
return (is_app_quick_launch ? 1 : 0) - (is_new_quick_launch ? 1 : 0);
|
||||
} else if (override_cmp_rv) {
|
||||
// Apps that override storage, record, and install order
|
||||
return (override_cmp_rv);
|
||||
} else if (app_node->storage_order != new_node->storage_order) {
|
||||
// Storage order (smallest first)
|
||||
return prv_comparator_ascending_zero_last(app_node->storage_order, new_node->storage_order);
|
||||
} else if (app_node->record_order != new_node->record_order) {
|
||||
// Record order (smallest first)
|
||||
return prv_comparator_ascending_zero_last(app_node->record_order, new_node->record_order);
|
||||
} else {
|
||||
// AppInstallId (smallest first)
|
||||
return new_node->install_id - app_node->install_id;
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_set_storage_order(AppMenuDataSource *source, AppMenuNode *menu_node,
|
||||
AppMenuOrderStorage *storage, bool update_and_take_ownership) {
|
||||
if (!storage) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < storage->list_length; i++) {
|
||||
const AppInstallId storage_app_id = storage->id_list[i];
|
||||
if (storage_app_id == INSTALL_ID_INVALID) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const AppMenuStorageOrder new_storage_order =
|
||||
(AppMenuStorageOrder)i + AppMenuStorageOrderGeneralOrderOffset;
|
||||
|
||||
if (menu_node && (menu_node->install_id == storage_app_id)) {
|
||||
menu_node->storage_order = new_storage_order;
|
||||
if (!update_and_take_ownership) {
|
||||
break;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (update_and_take_ownership) {
|
||||
AppMenuNode *other_node = prv_find_node_with_install_id(storage_app_id, source);
|
||||
if (other_node) {
|
||||
other_node->storage_order = new_storage_order;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (update_and_take_ownership) {
|
||||
app_free(storage);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_sorted_add(AppMenuDataSource *source, AppMenuNode *menu_node) {
|
||||
// Update the entire list order only if we've just read the order in this context. If we haven't
|
||||
// just read the order, then we're building a list starting from an empty list, so just set the
|
||||
// order for the new node.
|
||||
prv_set_storage_order(source, menu_node, source->order_storage ?: app_order_read_order(),
|
||||
(source->order_storage == NULL));
|
||||
|
||||
// If we're adding the Settings app node to the list and it hasn't received a storage order,
|
||||
// then give it its default order
|
||||
if ((menu_node->install_id == APP_ID_SETTINGS) &&
|
||||
(menu_node->storage_order == AppMenuStorageOrder_NoOrder)) {
|
||||
menu_node->storage_order = AppMenuStorageOrder_SettingsDefaultOrder;
|
||||
}
|
||||
|
||||
source->list = (AppMenuNode *)list_sorted_add(&source->list->node, &menu_node->node,
|
||||
prv_app_node_comparator, true /* ascending */);
|
||||
}
|
||||
|
||||
////////////////////////////////
|
||||
// AppInstallManager Callbacks
|
||||
////////////////////////////////
|
||||
|
||||
typedef struct InstallData {
|
||||
AppInstallId id;
|
||||
AppMenuDataSource *source;
|
||||
InstallEventType event_type;
|
||||
} InstallData;
|
||||
|
||||
void prv_alert_data_source_changed(AppMenuDataSource *data_source) {
|
||||
if (data_source->callbacks.changed) {
|
||||
data_source->callbacks.changed(data_source->callback_context);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_do_app_added(AppMenuDataSource *source, AppInstallId install_id);
|
||||
static void prv_do_app_removed(AppMenuDataSource *source, AppInstallId install_id);
|
||||
static void prv_do_app_icon_name_updated(AppMenuDataSource *source, AppInstallId install_id);
|
||||
static void prv_do_app_db_cleared(AppMenuDataSource *source);
|
||||
|
||||
void prv_handle_app_event(void *data) {
|
||||
InstallData *install_data = data;
|
||||
AppMenuDataSource *source = install_data->source;
|
||||
AppInstallId install_id = install_data->id;
|
||||
|
||||
switch (install_data->event_type) {
|
||||
case APP_AVAILABLE:
|
||||
prv_do_app_added(source, install_id);
|
||||
break;
|
||||
case APP_REMOVED:
|
||||
prv_do_app_removed(source, install_id);
|
||||
break;
|
||||
case APP_ICON_NAME_UPDATED:
|
||||
prv_do_app_icon_name_updated(source, install_id);
|
||||
break;
|
||||
case APP_DB_CLEARED:
|
||||
prv_do_app_db_cleared(source);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
kernel_free(install_data);
|
||||
}
|
||||
|
||||
void prv_send_callback_to_app(AppMenuDataSource *data_source, AppInstallId install_id,
|
||||
InstallEventType event_type) {
|
||||
InstallData *install_data = kernel_malloc_check(sizeof(InstallData));
|
||||
*install_data = (InstallData) {
|
||||
.id = install_id,
|
||||
.source = data_source,
|
||||
.event_type = event_type,
|
||||
};
|
||||
process_manager_send_callback_event_to_process(PebbleTask_App,
|
||||
prv_handle_app_event,
|
||||
install_data);
|
||||
}
|
||||
|
||||
//! Must be run from the app task
|
||||
static void prv_do_app_added(AppMenuDataSource *source, AppInstallId install_id) {
|
||||
AppInstallEntry entry;
|
||||
if (!app_install_get_entry_for_install_id(install_id, &entry) ||
|
||||
prv_is_app_filtered_out(&entry, source)) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_app_with_install_id(&entry, source);
|
||||
prv_alert_data_source_changed(source);
|
||||
}
|
||||
|
||||
//! Called when an application is installed
|
||||
static void prv_app_added_callback(const AppInstallId install_id, void *data) {
|
||||
AppMenuDataSource *data_source = data;
|
||||
prv_send_callback_to_app(data_source, install_id, APP_AVAILABLE);
|
||||
}
|
||||
|
||||
//! Must be run from the app task
|
||||
static void prv_do_app_removed(AppMenuDataSource *source, AppInstallId install_id) {
|
||||
// Don't filter, just always try removing from the list:
|
||||
const bool is_removed = remove_app_with_install_id(install_id, source);
|
||||
if (is_removed) {
|
||||
prv_alert_data_source_changed(source);
|
||||
}
|
||||
}
|
||||
|
||||
//! Called when an application is uninstalled
|
||||
static void prv_app_removed_callback(const AppInstallId install_id, void *data) {
|
||||
AppMenuDataSource *data_source = data;
|
||||
prv_send_callback_to_app(data_source, install_id, APP_REMOVED);
|
||||
}
|
||||
|
||||
//! Must be run from the app task
|
||||
static void prv_do_app_icon_name_updated(AppMenuDataSource *source, AppInstallId install_id) {
|
||||
AppInstallEntry entry;
|
||||
if (!app_install_get_entry_for_install_id(install_id, &entry)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AppMenuNode *node = prv_find_node_with_install_id(install_id, source);
|
||||
if (prv_is_app_filtered_out(&entry, source)) {
|
||||
if (node == NULL) {
|
||||
// Changed and still excluded:
|
||||
return;
|
||||
}
|
||||
// Changed and is now excluded:
|
||||
prv_unload_node(source, node);
|
||||
} else {
|
||||
// Changed and is now included:
|
||||
if (node == NULL) {
|
||||
add_app_with_install_id(&entry, source);
|
||||
}
|
||||
}
|
||||
prv_alert_data_source_changed(source);
|
||||
}
|
||||
|
||||
static void prv_app_icon_name_updated_callback(const AppInstallId install_id, void *data) {
|
||||
AppMenuDataSource *data_source = data;
|
||||
prv_send_callback_to_app(data_source, install_id, APP_ICON_NAME_UPDATED);
|
||||
}
|
||||
|
||||
//! Must be run from the app task
|
||||
static void prv_do_app_db_cleared(AppMenuDataSource *source) {
|
||||
AppMenuNode *iter = source->list;
|
||||
while (iter) {
|
||||
AppMenuNode *temp = (AppMenuNode *)list_get_next((ListNode *)iter);
|
||||
|
||||
// if the node belonged to the app_db, remove
|
||||
if (app_install_id_from_app_db(iter->install_id)) {
|
||||
prv_unload_node(source, iter);
|
||||
}
|
||||
iter = temp;
|
||||
}
|
||||
|
||||
prv_alert_data_source_changed(source);
|
||||
}
|
||||
|
||||
static void prv_app_db_cleared_callback(const AppInstallId install_id, void *data) {
|
||||
// data is just a pointer to the AppMenuDataSource
|
||||
prv_send_callback_to_app((AppMenuDataSource *)data, INSTALL_ID_INVALID, APP_DB_CLEARED);
|
||||
}
|
||||
|
||||
static bool prv_app_enumerate_callback(AppInstallEntry *entry, void *data) {
|
||||
if (prv_is_app_filtered_out(entry, (AppMenuDataSource *)data)) {
|
||||
return true; // continue
|
||||
}
|
||||
add_app_with_install_id(entry, data);
|
||||
return true; // continue
|
||||
}
|
||||
|
||||
////////////////////
|
||||
// Add / remove helper functions:
|
||||
|
||||
// This function should only be called once per app entry. The icon from the app will either be
|
||||
// loaded and cached or we will load the default system icon that is set by the client.
|
||||
static void prv_load_list_item_icon(AppMenuDataSource *source, AppMenuNode *node) {
|
||||
// Should only call this function if the icon has not been loaded
|
||||
PBL_ASSERTN(node->icon == NULL);
|
||||
|
||||
if (node->icon_resource_id != RESOURCE_ID_INVALID) {
|
||||
// If we have some sort of valid resource_id, try loading it
|
||||
node->icon = gbitmap_create_with_resource_system(node->app_num, node->icon_resource_id);
|
||||
}
|
||||
|
||||
if (!node->icon) {
|
||||
// If we failed to load the app's icon or it didn't have one, use the default. This will either
|
||||
// be NULL or an actual icon...both are fine. And no need to clip the default icon
|
||||
node->icon = source->default_icon;
|
||||
return;
|
||||
}
|
||||
|
||||
// Clip the icon down if needed
|
||||
static const GRect icon_clip = {{0, 0}, {32, 32}};
|
||||
grect_clip(&node->icon->bounds, &icon_clip);
|
||||
}
|
||||
|
||||
static void prv_unload_list_item_icon(const AppMenuDataSource *source, AppMenuNode *node) {
|
||||
// Don't destroy the default icon here, we'll destroy it later.
|
||||
if (node->icon && node->icon != source->default_icon) {
|
||||
gbitmap_destroy(node->icon);
|
||||
node->icon = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_load_list_if_needed(AppMenuDataSource *source) {
|
||||
if (source->is_list_loaded) {
|
||||
return;
|
||||
}
|
||||
source->is_list_loaded = true;
|
||||
|
||||
PBL_ASSERTN(!source->order_storage);
|
||||
source->order_storage = app_order_read_order();
|
||||
|
||||
app_install_enumerate_entries(prv_app_enumerate_callback, source);
|
||||
|
||||
app_free(source->order_storage);
|
||||
source->order_storage = NULL;
|
||||
}
|
||||
|
||||
static void prv_unload_node(const AppMenuDataSource *source, AppMenuNode *node) {
|
||||
prv_unload_list_item_icon(source, node);
|
||||
list_remove((ListNode*)node, (ListNode**)&source->list, NULL);
|
||||
app_free(node->name);
|
||||
app_free(node);
|
||||
}
|
||||
|
||||
static void add_app_with_install_id(const AppInstallEntry *entry, AppMenuDataSource *source) {
|
||||
if (source->is_list_loaded == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
AppMenuNode *node = app_malloc_check(sizeof(AppMenuNode));
|
||||
*node = (AppMenuNode) {
|
||||
.install_id = entry->install_id,
|
||||
.app_num = app_install_get_app_icon_bank(entry),
|
||||
.icon_resource_id = app_install_entry_get_icon_resource_id(entry),
|
||||
.uuid = entry->uuid,
|
||||
.color = entry->color,
|
||||
.visibility = entry->visibility,
|
||||
.sdk_version = entry->sdk_version,
|
||||
.record_order = entry->record_order,
|
||||
};
|
||||
|
||||
uint8_t len;
|
||||
const char *app_name = app_install_get_custom_app_name(node->install_id);
|
||||
if (!app_name) {
|
||||
app_name = entry->name;
|
||||
}
|
||||
|
||||
len = strlen(app_name) + 1;
|
||||
node->name = app_malloc_check(len);
|
||||
strncpy(node->name, app_name, len);
|
||||
|
||||
prv_sorted_add(source, node);
|
||||
}
|
||||
|
||||
static AppMenuNode * prv_find_node_with_install_id(const AppInstallId install_id,
|
||||
const AppMenuDataSource * const source) {
|
||||
AppMenuNode *node = source->list;
|
||||
while (node != NULL) {
|
||||
if (node->install_id == install_id) {
|
||||
return node;
|
||||
} else {
|
||||
node = (AppMenuNode*)list_get_next((ListNode*)node);
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
//! @return True if there was app with install_id was found and removed from the list.
|
||||
static bool remove_app_with_install_id(const AppInstallId install_id, AppMenuDataSource *source) {
|
||||
if (source->is_list_loaded == false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
AppMenuNode *node = prv_find_node_with_install_id(install_id, source);
|
||||
if (node == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
prv_unload_node(source, node);
|
||||
return true;
|
||||
}
|
||||
|
||||
////////////////////
|
||||
// public interface
|
||||
|
||||
void app_menu_data_source_init(AppMenuDataSource *source,
|
||||
const AppMenuDataSourceCallbacks *callbacks,
|
||||
void *callback_context) {
|
||||
PBL_ASSERTN(source != NULL);
|
||||
*source = (AppMenuDataSource) {
|
||||
.callback_context = callback_context,
|
||||
};
|
||||
if (callbacks) {
|
||||
source->callbacks = *callbacks;
|
||||
}
|
||||
|
||||
// Register callbacks for app_install_manager updates:
|
||||
static const AppInstallCallback s_app_install_callbacks[NUM_INSTALL_EVENT_TYPES] = {
|
||||
[APP_AVAILABLE] = prv_app_added_callback,
|
||||
[APP_REMOVED] = prv_app_removed_callback,
|
||||
[APP_UPGRADED] = prv_app_removed_callback,
|
||||
[APP_ICON_NAME_UPDATED] = prv_app_icon_name_updated_callback,
|
||||
[APP_DB_CLEARED] = prv_app_db_cleared_callback,
|
||||
};
|
||||
source->app_install_callback_node = (struct AppInstallCallbackNode) {
|
||||
.data = source,
|
||||
.callbacks = s_app_install_callbacks,
|
||||
};
|
||||
app_install_register_callback(&source->app_install_callback_node);
|
||||
}
|
||||
|
||||
void app_menu_data_source_deinit(AppMenuDataSource *source) {
|
||||
app_install_deregister_callback(&source->app_install_callback_node);
|
||||
// Free the AppMenuNodes:
|
||||
AppMenuNode *node = source->list;
|
||||
while (node != NULL) {
|
||||
AppMenuNode * const next = (AppMenuNode*)list_get_next((ListNode*)node);
|
||||
prv_unload_node(source, node);
|
||||
node = next;
|
||||
}
|
||||
|
||||
if (source->default_icon) {
|
||||
gbitmap_destroy(source->default_icon);
|
||||
}
|
||||
|
||||
source->callbacks.changed = NULL;
|
||||
source->is_list_loaded = false;
|
||||
}
|
||||
|
||||
void app_menu_data_source_enable_icons(AppMenuDataSource *source, uint32_t fallback_icon_id) {
|
||||
// should only call this once, and should be passed in a valid resource id.
|
||||
PBL_ASSERTN(source->default_icon == NULL && fallback_icon_id != RESOURCE_ID_INVALID);
|
||||
|
||||
source->show_icons = true;
|
||||
// The return value will be a valid GBitmap* or NULL (because of an OOM that shouldn't ever happen
|
||||
// We will handle both gracefully.
|
||||
source->default_icon = gbitmap_create_with_resource_system(SYSTEM_APP, fallback_icon_id);
|
||||
}
|
||||
|
||||
static uint16_t prv_transform_index(AppMenuDataSource *source, uint16_t index) {
|
||||
if (source->callbacks.transform_index) {
|
||||
return source->callbacks.transform_index(source, index, source->callback_context);
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
AppMenuNode* app_menu_data_source_get_node_at_index(AppMenuDataSource *source, uint16_t row_index) {
|
||||
prv_load_list_if_needed(source);
|
||||
return (AppMenuNode*)list_get_at((ListNode*)source->list,
|
||||
prv_transform_index(source, row_index));
|
||||
}
|
||||
|
||||
uint16_t app_menu_data_source_get_count(AppMenuDataSource *source) {
|
||||
prv_load_list_if_needed(source);
|
||||
return list_count((ListNode*)source->list);
|
||||
}
|
||||
|
||||
uint16_t app_menu_data_source_get_index_of_app_with_install_id(AppMenuDataSource *source,
|
||||
AppInstallId install_id) {
|
||||
prv_load_list_if_needed(source);
|
||||
AppMenuNode *node = source->list;
|
||||
uint16_t index = 0;
|
||||
while (node != NULL) {
|
||||
if (node->install_id == install_id) {
|
||||
return prv_transform_index(source, index);
|
||||
}
|
||||
node = (AppMenuNode*)list_get_next((ListNode*)node);
|
||||
++index;
|
||||
}
|
||||
return MENU_INDEX_NOT_FOUND;
|
||||
}
|
||||
|
||||
GBitmap *app_menu_data_source_get_node_icon(AppMenuDataSource *source, AppMenuNode *node) {
|
||||
if (!node->icon && source->show_icons) {
|
||||
// If the icon is currently NULL and we should be showing icons, load the icon
|
||||
prv_load_list_item_icon(source, node);
|
||||
}
|
||||
// Will return the icon if it exists, or NULL if one doesn't.
|
||||
return node->icon;
|
||||
}
|
||||
|
||||
void app_menu_data_source_draw_row(AppMenuDataSource *source, GContext *ctx, Layer *cell_layer,
|
||||
MenuIndex *cell_index) {
|
||||
AppMenuNode *node = app_menu_data_source_get_node_at_index(source, cell_index->row);
|
||||
// Will return an icon or NULL depending on if icons are enabled.
|
||||
GBitmap *bitmap = app_menu_data_source_get_node_icon(source, node);
|
||||
const GCompOp op = (gbitmap_get_format(bitmap) == GBitmapFormat1Bit) ? GCompOpTint : GCompOpSet;
|
||||
graphics_context_set_compositing_mode(ctx, op);
|
||||
menu_cell_basic_draw(ctx, cell_layer, node->name, NULL, bitmap);
|
||||
}
|
||||
Reference in New Issue
Block a user