diff --git a/R/GlobalSetter.R b/R/GlobalSetter.R new file mode 100644 index 0000000000..b3ef4e2bb0 --- /dev/null +++ b/R/GlobalSetter.R @@ -0,0 +1,94 @@ +#' Set global objects +#' +#' Class to manage global objects and bring them into scope of `shiny` modules. +#' +#' Singleton object to pass variables between modules. Instantiated in the package namespace. +#' Global objects are stored in the `session$userData` environment. +#' Every `shiny` module can register a setter functions for a global variable to be taken from storage +#' or created from scratch. Then, that function can be called anywhere in the app. +#' +#' @docType class +#' @name GlobalSetter +#' @rdname GlobalSetter +#' +#' @examples +#' GlobalSetter <- getFromNamespace("GlobalSetter", "teal") +#' setter <- GlobalSetter$new() +#' setter$add_setter("romans", function() { +#' assign("romans", .romans, envir = .GlobalEnv) +#' .romans +#' }) +#' setter +#' setter$set_global("romans") +#' exists("romans", envir = .GlobalEnv, inherits = TRUE) +#' +#' @keywords internal +#' +GlobalSetter <- R6::R6Class( + classname = "GlobalSetter", + + # __Public Methods ---- + public = list( + + #' @description + #' Set object. + #' @param obj (`character(1)`) Name of object to set. + #' @return + #' Whatever the particular setter function returns, usually the global object. + set_global = function(obj) { + if (!shiny::isRunning()) { + message("no shiny, no objects") + return(NULL) + } + checkmate::assert_string(obj) + checkmate::assert_choice(obj, private$objects) + + ind <- match(obj, private$objects) + private$functions[[ind]]() + }, + + #' @description + #' Add setter function for global object. + #' @param obj (`character(1)`) Name of object. + #' @param fun (`function`) Setter function. + add_setter = function(obj, fun) { + checkmate::assert_string(obj) + checkmate::assert_function(fun) + + ind <- match(obj, private$objects, nomatch = length(private$objects) + 1L) + + if (is.element(obj, private$objects)) { + if (identical(fun, private$functions[[ind]])) { + message("this setter already exists") + return(invisible(NULL)) + } else { + message(sprintf("overwriting setter function for object '%s'", obj)) + } + } + + private$objects[ind] <- obj + private$functions[[ind]] <- fun + invisible(NULL) + }, + + #' @noRd + format = function(...) { + paste( + c( + "Global Object Setter", + if (length(private$objects)) sprintf(" objects: %s", toString(private$objects)) else " empty" + ), + collapse = "\n" + ) + } + ), + + # __Private Members ---- + private = list( + objects = character(0L), # objects for which setter functions have been defined + functions = list() # setter functions + ), + cloneable = FALSE +) + +GS <- GlobalSetter$new() diff --git a/R/module_filter_manager.R b/R/module_filter_manager.R index fb735a2366..95e65e7589 100644 --- a/R/module_filter_manager.R +++ b/R/module_filter_manager.R @@ -113,43 +113,22 @@ filter_manager_srv <- function(id, filtered_data_list, filter) { is_module_specific <- isTRUE(attr(filter, "module_specific")) + # Register setters for global objects + GS$add_setter("slices_global", setter_slices_global) + GS$add_setter("filtered_data_list", setter_filtered_data_list) + GS$add_setter("mapping_matrix", setter_mapping_matrix) + # Create global list of slices. # Contains all available teal_slice objects available to all modules. # Passed whole to instances of FilteredData used for individual modules. # Down there a subset that pertains to the data sets used in that module is applied and displayed. - slices_global <- reactiveVal(filter) - - filtered_data_list <- - if (!is_module_specific) { - # Retrieve the first FilteredData from potentially nested list. - # List of length one is named "global_filters" because that name is forbidden for a module label. - list(global_filters = unlist(filtered_data_list)[[1]]) - } else { - # Flatten potentially nested list of FilteredData objects while maintaining useful names. - # Simply using `unlist` would result in concatenated names. - flatten_nested <- function(x, name = NULL) { - if (inherits(x, "FilteredData")) { - setNames(list(x), name) - } else { - unlist(lapply(names(x), function(name) flatten_nested(x[[name]], name))) - } - } - flatten_nested(filtered_data_list) - } - - # Create mapping fo filters to modules in matrix form (presented as data.frame). - # Modules get NAs for filters that cannot be set for them. - mapping_matrix <- reactive({ - state_ids_global <- vapply(slices_global(), `[[`, character(1L), "id") - mapping_smooth <- lapply(filtered_data_list, function(x) { - state_ids_local <- vapply(x$get_filter_state(), `[[`, character(1L), "id") - state_ids_allowed <- vapply(x$get_available_teal_slices()(), `[[`, character(1L), "id") - states_active <- state_ids_global %in% state_ids_local - ifelse(state_ids_global %in% state_ids_allowed, states_active, NA) - }) - - as.data.frame(mapping_smooth, row.names = state_ids_global, check.names = FALSE) - }) + slices_global <- GS$set_global("slices_global") + # Prepare FilteredData object according to app type (global/module-specific). + filtered_data_list <- GS$set_global("filtered_data_list") + # Represent mapping of modules to filters. + mapping_matrix <- GS$set_global("mapping_matrix") + # The three objects above are also stored in the app session's userData. + # They will be used in other modules and stored when bookmarking. output$slices_table <- renderTable( expr = { @@ -182,9 +161,9 @@ filter_manager_srv <- function(id, filtered_data_list, filter) { }) # Call snapshot manager. - snapshot_history <- snapshot_manager_srv("snapshot_manager", slices_global, mapping_matrix, filtered_data_list) + snapshot_manager_srv("snapshot_manager") # Call state manager. - state_manager_srv("state_manager", slices_global, mapping_matrix, filtered_data_list, snapshot_history) + state_manager_srv("state_manager") modules_out # returned for testing purpose }) @@ -252,3 +231,96 @@ filter_manager_module_srv <- function(id, module_fd, slices_global) { slices_module # returned for testing purpose }) } + + + +# utility functions ---- + +# Functions that prepare objects. +# Objects are first scoped in sesion (userData) and if not found, created and stored. + +# Obtain appropriate structure of `FilteredData` objects. +# For global app, list of length 1 named "global_filters". +# For module-specific app, list of length one-per-module, named after modules. +#' @keywords internal +#' @noRd +#' +create_filtered_data_list <- function(filtered_data_list, module_specific) { + if (!module_specific) { + # Retrieve the first FilteredData from potentially nested list. + # List of length one is named "global_filters" because that name is forbidden for a module label. + list(global_filters = unlist(filtered_data_list)[[1]]) + } else { + flatten_nested(filtered_data_list) + } +} +# Flatten potentially nested list of FilteredData objects while maintaining useful names. +# Simply using `unlist` would result in concatenated names. +#' @keywords internal +#' @noRd +#' +flatten_nested <- function(x, name = NULL) { + if (inherits(x, "FilteredData")) { + setNames(list(x), name) + } else { + unlist(lapply(names(x), function(name) flatten_nested(x[[name]], name))) + } +} + +# Create mapping fo filters to modules in matrix form (presented as data.frame). +# Modules get NAs for filters that cannot be set for them. +#' @keywords internal +#' @noRd +#' +create_mapping_matrix <- function(filtered_data_list, slices_global) { + reactive({ + state_ids_global <- vapply(slices_global(), `[[`, character(1L), "id") + mapping_smooth <- lapply(filtered_data_list, function(x) { + state_ids_local <- vapply(x$get_filter_state(), `[[`, character(1L), "id") + state_ids_allowed <- vapply(x$get_available_teal_slices()(), `[[`, character(1L), "id") + states_active <- state_ids_global %in% state_ids_local + ifelse(state_ids_global %in% state_ids_allowed, states_active, NA) + }) + + as.data.frame(mapping_smooth, row.names = state_ids_global, check.names = FALSE) + }) +} + + + +# setter for global variable ---- +#' @keywords internal +#' @noRd +setter_slices_global <- function() { + sesh <- getDefaultReactiveDomain() + if (is.null(sesh$userData$slices_global)) { + filter <- dynGet("filter") + sesh$userData$slices_global <- reactiveVal(filter) + } else { + sesh$userData$slices_global + } +} +#' @keywords internal +#' @noRd +setter_filtered_data_list <- function() { + sesh <- getDefaultReactiveDomain() + if (is.null(sesh$userData$filtered_data_list)) { + filtered_data_list <- dynGet("filtered_data_list") + is_module_specific <- dynGet("is_module_specific") + sesh$userData$filtered_data_list <- create_filtered_data_list(filtered_data_list, is_module_specific) + } else { + sesh$userData$filtered_data_list + } +} +#' @keywords internal +#' @noRd +setter_mapping_matrix <- function() { + sesh <- getDefaultReactiveDomain() + if (is.null(sesh$userData$mapping_matrix)) { + filtered_data_list <- dynGet("filtered_data_list") + slices_global <- dynGet("slices_global") + sesh$userData$mapping_matrix <- create_mapping_matrix(filtered_data_list, slices_global) + } else { + sesh$userData$mapping_matrix + } +} diff --git a/R/module_snapshot_manager.R b/R/module_snapshot_manager.R index 5f47c8597f..0a3e1abaa9 100644 --- a/R/module_snapshot_manager.R +++ b/R/module_snapshot_manager.R @@ -102,24 +102,21 @@ snapshot_manager_ui <- function(id) { #' @rdname snapshot_manager_module #' @keywords internal #' -snapshot_manager_srv <- function(id, slices_global, mapping_matrix, filtered_data_list) { +snapshot_manager_srv <- function(id) { checkmate::assert_character(id) - checkmate::assert_true(is.reactive(slices_global)) - checkmate::assert_class(isolate(slices_global()), "teal_slices") - checkmate::assert_true(is.reactive(mapping_matrix)) - checkmate::assert_data_frame(isolate(mapping_matrix()), null.ok = TRUE) - checkmate::assert_list(filtered_data_list, types = "FilteredData", any.missing = FALSE, names = "named") moduleServer(id, function(input, output, session) { ns <- session$ns + # Retrieve global objects ---- + slices_global <- GS$set_global("slices_global") + filtered_data_list <- GS$set_global("filtered_data_list") + mapping_matrix <- GS$set_global("mapping_matrix") + + # Register setter for global snapshot history ---- + GS$add_setter("snapshot_history", setter_snapshot_history) # Store global filter states ---- - filter <- isolate(slices_global()) - snapshot_history <- reactiveVal({ - list( - "Initial application state" = as.list(filter, recursive = TRUE) - ) - }) + snapshot_history <- GS$set_global("snapshot_history") # Snapshot current application state ---- # Name snaphsot. @@ -326,14 +323,13 @@ snapshot_manager_srv <- function(id, slices_global, mapping_matrix, filtered_dat } }) - return(snapshot_history) }) } -### utility functions ---- +# utility functions ---- #' Explicitly enumerate global filters. #' @@ -343,6 +339,7 @@ snapshot_manager_srv <- function(id, slices_global, mapping_matrix, filtered_dat #' @param module_names (`character`) vector containing names of all modules in the app #' @return A `named_list` with one element per module, each element containing all filters applied to that module. #' @keywords internal +#' @noRd #' unfold_mapping <- function(mapping, module_names) { module_names <- structure(module_names, names = module_names) @@ -360,6 +357,7 @@ unfold_mapping <- function(mapping, module_names) { #' columns represent modules and row represent `teal_slice`s #' @return `named list` like that in the `mapping` attribute of a `teal_slices` object. #' @keywords internal +#' @noRd #' matrix_to_mapping <- function(mapping_matrix) { mapping_matrix[] <- lapply(mapping_matrix, function(x) x | is.na(x)) @@ -370,3 +368,22 @@ matrix_to_mapping <- function(mapping_matrix) { mapping <- c(lapply(local_filters, function(x) rownames(local_filters)[x]), list(global_filters = global_filters)) Filter(function(x) length(x) != 0L, mapping) } + + + +# setter for global variable ---- +#' @keywords internal +#' @noRd +setter_snapshot_history <- function() { + sesh <- getDefaultReactiveDomain() + if (is.null(sesh$userData$snapshot_history)) { + slices_global <- dynGet("slices_global") + sesh$userData$snapshot_history <- reactiveVal({ + list( + "Initial application state" = as.list(isolate(slices_global()), recursive = TRUE) + ) + }) + } else { + sesh$userData$snapshot_history + } +} diff --git a/R/module_state_manager.R b/R/module_state_manager.R index 76a59dd477..38e9c67886 100644 --- a/R/module_state_manager.R +++ b/R/module_state_manager.R @@ -33,24 +33,24 @@ state_manager_ui <- function(id) { #' @rdname state_manager_module #' @keywords internal #' -state_manager_srv <- function(id, slices_global, mapping_matrix, filtered_data_list, snapshot_history) { +state_manager_srv <- function(id) { checkmate::assert_character(id) - checkmate::assert_true(is.reactive(slices_global)) - checkmate::assert_class(isolate(slices_global()), "teal_slices") - checkmate::assert_true(is.reactive(mapping_matrix)) - checkmate::assert_data_frame(isolate(mapping_matrix()), null.ok = TRUE) - checkmate::assert_list(filtered_data_list, types = "FilteredData", any.missing = FALSE, names = "named") - checkmate::assert_true(is.reactive(snapshot_history)) - checkmate::assert_list(isolate(snapshot_history()), names = "unique") moduleServer(id, function(input, output, session) { ns <- session$ns sesh <- get_master_session() - # Store input states. - grab_history <- reactiveVal({ - list() - }) + # Retrieve global objects ---- + slices_global <- GS$set_global("slices_global") + filtered_data_list <- GS$set_global("filtered_data_list") + mapping_matrix <- GS$set_global("mapping_matrix") + snapshot_history <- GS$set_global("snapshot_history") + + # Register setter for global grab history ---- + GS$add_setter("grab_history", setter_grab_history) + + # Store global filter states ---- + grab_history <- GS$set_global("grab_history") sesh$onBookmark(function(state) { # Add current filter state to bookmark. @@ -169,3 +169,19 @@ get_master_session <- function() { app_session } } + + + +# setter for global variable ---- +#' @keywords internal +#' @noRd +setter_grab_history <- function() { + sesh <- getDefaultReactiveDomain() + if (is.null(sesh$userData$grab_history)) { + sesh$userData$grab_history <- reactiveVal({ + list() + }) + } else { + sesh$userData$grab_history + } +}