diff --git a/blocks/recentlyaccesseditems/classes/external/recentlyaccesseditems_item_exporter.php b/blocks/recentlyaccesseditems/classes/external/recentlyaccesseditems_item_exporter.php index bff39633dbc..549a2ec9d50 100644 --- a/blocks/recentlyaccesseditems/classes/external/recentlyaccesseditems_item_exporter.php +++ b/blocks/recentlyaccesseditems/classes/external/recentlyaccesseditems_item_exporter.php @@ -51,17 +51,18 @@ class recentlyaccesseditems_item_exporter extends \core\external\exporter { protected function get_other_values(renderer_base $output) { global $CFG; require_once($CFG->libdir.'/modinfolib.php'); - + $iconurl = get_fast_modinfo($this->data->courseid)->cms[$this->data->cmid]->get_icon_url(); + $iconclass = $iconurl->get_param('filtericon') ? '' : 'nofilter'; return array( 'viewurl' => (new moodle_url('/mod/'.$this->data->modname.'/view.php', array('id' => $this->data->cmid)))->out(false), 'courseviewurl' => (new moodle_url('/course/view.php', array('id' => $this->data->courseid)))->out(false), 'icon' => \html_writer::img( - get_fast_modinfo($this->data->courseid)->cms[$this->data->cmid]->get_icon_url(), + $iconurl, get_string('pluginname', $this->data->modname), - ['title' => get_string('pluginname', $this->data->modname), 'class' => 'icon'] + ['title' => get_string('pluginname', $this->data->modname), 'class' => "icon $iconclass"] ), - 'purpose' => plugin_supports('mod', $this->data->modname, FEATURE_MOD_PURPOSE, MOD_PURPOSE_OTHER) + 'purpose' => plugin_supports('mod', $this->data->modname, FEATURE_MOD_PURPOSE, MOD_PURPOSE_OTHER), ); } diff --git a/blocks/timeline/templates/event-list-item.mustache b/blocks/timeline/templates/event-list-item.mustache index 7c442c13075..13f28089897 100644 --- a/blocks/timeline/templates/event-list-item.mustache +++ b/blocks/timeline/templates/event-list-item.mustache @@ -57,7 +57,7 @@
{{#icon}} {{#iconurl}} - {{alttext}} + {{alttext}} {{/iconurl}} {{^iconurl}} {{#pix}} {{key}}, {{component}}, {{alttext}} {{/pix}} diff --git a/calendar/classes/external/event_icon_exporter.php b/calendar/classes/external/event_icon_exporter.php index f6bdf4c12f0..bdc8b6386d3 100644 --- a/calendar/classes/external/event_icon_exporter.php +++ b/calendar/classes/external/event_icon_exporter.php @@ -62,12 +62,15 @@ class event_icon_exporter extends exporter { $isgroupevent = ($group && !empty($groupid)); $isuserevent = ($user && !empty($userid)); $iconurl = ''; + $iconclass = ''; if ($isactivityevent) { $key = 'monologo'; $component = $coursemodule->get('modname'); - $iconurl = get_fast_modinfo($courseid)->get_cm($coursemodule->get('id'))->get_icon_url()->out(false); + $iconurl = get_fast_modinfo($courseid)->get_cm($coursemodule->get('id'))->get_icon_url(); + $iconclass = $iconurl->get_param('filtericon') ? '' : 'nofilter'; + $iconurl = $iconurl->out(false); if (get_string_manager()->string_exists($event->get_type(), $component)) { $alttext = get_string($event->get_type(), $component); } else { @@ -121,6 +124,7 @@ class event_icon_exporter extends exporter { $data->component = $component; $data->alttext = $alttext; $data->iconurl = $iconurl; + $data->iconclass = $iconclass; parent::__construct($data, $related); } @@ -136,6 +140,7 @@ class event_icon_exporter extends exporter { 'component' => ['type' => PARAM_TEXT], 'alttext' => ['type' => PARAM_TEXT], 'iconurl' => ['type' => PARAM_TEXT], + 'iconclass' => ['type' => PARAM_TEXT], ]; } diff --git a/course/classes/local/repository/content_item_readonly_repository.php b/course/classes/local/repository/content_item_readonly_repository.php index 32bd29d2880..ae4c64861fb 100644 --- a/course/classes/local/repository/content_item_readonly_repository.php +++ b/course/classes/local/repository/content_item_readonly_repository.php @@ -26,6 +26,7 @@ namespace core_course\local\repository; defined('MOODLE_INTERNAL') || die(); +use core_component; use core_course\local\entity\content_item; use core_course\local\entity\lang_string_title; use core_course\local\entity\string_title; @@ -195,12 +196,20 @@ class content_item_readonly_repository implements content_item_readonly_reposito $archetype = plugin_supports('mod', $mod->name, FEATURE_MOD_ARCHETYPE, MOD_ARCHETYPE_OTHER); $purpose = plugin_supports('mod', $mod->name, FEATURE_MOD_PURPOSE, MOD_PURPOSE_OTHER); + $icon = 'monologo'; + // Quick check for monologo icons. + // Plugins that don't have monologo icons will be displayed as is and CSS filter will not be applied. + $hasmonologoicons = core_component::has_monologo_icon('mod', $mod->name); + $iconclass = ''; + if (!$hasmonologoicons) { + $iconclass = 'nofilter'; + } $contentitem = new content_item( $mod->id, $mod->name, new lang_string_title("modulename", $mod->name), new \moodle_url('/course/mod.php', ['id' => $course->id, 'add' => $mod->name]), - $OUTPUT->pix_icon('monologo', '', $mod->name, ['class' => 'icon activityicon']), + $OUTPUT->pix_icon($icon, '', $mod->name, ['class' => "activityicon $iconclass"]), $help, $archetype, 'mod_' . $mod->name, diff --git a/course/format/classes/output/local/content/cm/cmname.php b/course/format/classes/output/local/content/cm/cmname.php index 0aa979d2e9a..9c7c9ad3210 100644 --- a/course/format/classes/output/local/content/cm/cmname.php +++ b/course/format/classes/output/local/content/cm/cmname.php @@ -102,9 +102,12 @@ class cmname implements named_templatable, renderable { return []; } + $iconurl = $mod->get_icon_url(); + $iconclass = $iconurl->get_param('filtericon') ? '' : 'nofilter'; $data = [ 'url' => $mod->url, - 'icon' => $mod->get_icon_url(), + 'icon' => $iconurl, + 'iconclass' => $iconclass, 'modname' => $mod->modname, 'textclasses' => $displayoptions['textclasses'] ?? '', 'purpose' => plugin_supports('mod', $mod->modname, FEATURE_MOD_PURPOSE, MOD_PURPOSE_OTHER), diff --git a/course/format/templates/local/content/cm/cmname.mustache b/course/format/templates/local/content/cm/cmname.mustache index 5c364dbebb3..62389885190 100644 --- a/course/format/templates/local/content/cm/cmname.mustache +++ b/course/format/templates/local/content/cm/cmname.mustache @@ -25,6 +25,7 @@ { "url": "#", "icon": "../../../pix/help.svg", + "iconclass": "", "pluginname": "File", "textclasses": "", "purpose": "content", @@ -47,7 +48,7 @@
- {{{modname}}} icon + {{{modname}}} icon
{{#pluginname}} diff --git a/lib/classes/component.php b/lib/classes/component.php index fb6fc9012b0..65e63a88369 100644 --- a/lib/classes/component.php +++ b/lib/classes/component.php @@ -1282,4 +1282,22 @@ $cache = '.var_export($cache, true).'; } return $componentnames; } + + /** + * Checks for the presence of monologo icons within a plugin. + * + * Only checks monologo icons in PNG and SVG formats as they are + * formats that can have transparent background. + * + * @param string $plugintype The plugin type. + * @param string $pluginname The plugin name. + * @return bool True if the plugin has a monologo icon + */ + public static function has_monologo_icon(string $plugintype, string $pluginname): bool { + $plugindir = core_component::get_plugin_directory($plugintype, $pluginname); + if ($plugindir === null) { + return false; + } + return file_exists("$plugindir/pix/monologo.svg") || file_exists("$plugindir/pix/monologo.png"); + } } diff --git a/lib/modinfolib.php b/lib/modinfolib.php index d0eb3e29b8b..bb5b3bc3543 100644 --- a/lib/modinfolib.php +++ b/lib/modinfolib.php @@ -1763,7 +1763,15 @@ class cm_info implements IteratorAggregate { } /** - * @param moodle_core_renderer $output Output render to use, or null for default (global) + * Fetch the module's icon URL. + * + * This function fetches the course module instance's icon URL. + * This method adds a `filtericon` parameter in the URL when rendering the monologo version of the course module icon or when + * the plugin declares, via its `filtericon` custom data, that the icon needs to be filtered. + * This additional information can be used by plugins when rendering the module icon to determine whether to apply + * CSS filtering to the icon. + * + * @param core_renderer $output Output render to use, or null for default (global) * @return moodle_url Icon URL for a suitable icon to put beside this cm */ public function get_icon_url($output = null) { @@ -1773,6 +1781,7 @@ class cm_info implements IteratorAggregate { $output = $OUTPUT; } + $ismonologo = false; if (!empty($this->iconurl)) { // Support modules setting their own, external, icon image. $icon = $this->iconurl; @@ -1792,6 +1801,17 @@ class cm_info implements IteratorAggregate { } } else { $icon = $output->image_url('monologo', $this->modname); + // Activity modules may only have an `icon` icon instead of a `monologo` icon. + // So we need to determine if the module really has a `monologo` icon. + $ismonologo = core_component::has_monologo_icon('mod', $this->modname); + } + + // Determine whether the icon will be filtered in the CSS. + // This can be controlled by the module by declaring a 'filtericon' custom data. + // If the 'filtericon' custom data is not set, icon filtering will be determined whether the module has a `monologo` icon. + $filtericon = $this->customdata['filtericon'] ?? $ismonologo; + if ($filtericon) { + $icon->param('filtericon', 1); } return $icon; } diff --git a/lib/tests/component_test.php b/lib/tests/component_test.php index 2d0cc23632c..03ffe440714 100644 --- a/lib/tests/component_test.php +++ b/lib/tests/component_test.php @@ -861,4 +861,19 @@ class component_test extends advanced_testcase { $this->assertContains('tool_usertours', $componentnames); $this->assertContains('core_favourites', $componentnames); } + + /** + * Test for monologo icons check in plugins. + * + * @covers core_component::has_monologo_icon + * @return void + */ + public function test_has_monologo_icon(): void { + // The Forum activity plugin has monologo icons. + $this->assertTrue(core_component::has_monologo_icon('mod', 'forum')); + // The core H5P subsystem doesn't have monologo icons. + $this->assertFalse(core_component::has_monologo_icon('core', 'h5p')); + // The function will return false for a non-existent component. + $this->assertFalse(core_component::has_monologo_icon('randomcomponent', 'h5p')); + } } diff --git a/lib/upgrade.txt b/lib/upgrade.txt index 36463e05d6e..cba147fd7aa 100644 --- a/lib/upgrade.txt +++ b/lib/upgrade.txt @@ -5,6 +5,17 @@ information provided here is intended especially for developers. * Added 'extrainfo' in the DB options config. Its extra information for the DB driver, e.g. SQL Server, has additional configuration according to its environment, which the administrator can specify to alter and override any connection options. +* New \core_component::has_monologo_icon() that determines whether a plugin has monologo icons. This can be used to + determine whether to apply CSS filtering to the plugin's icon when rendering it. +* \cm_info::get_icon_url() resolves the icon's file type and adds a `filtericon` parameter in the URL when rendering the monologo + version of the course module icon or when the plugin declares, via its `filtericon` custom data, that the icon needs to be + filtered. This additional information can be used by plugins when rendering the module icon to determine whether to apply + CSS filtering to the icon. +* Activity plugins displaying activity module icons using \cm_info::get_icon_url() can declare the `filtericon` custom data in their + `get_coursemodule_info()` callback. If set, this will be used by \cm_info::get_icon_url() to set the icon URL's `filtericon` + parameter. This information can be used by the plugins when enclosing the icons in `.activityiconcontainer .icon` or + `.activityiconcontainer .activityicon` containers to determine whether CSS filtering should be applied to the icon. If the icon + needs to be rendered as is and not whitened out, the `.nofilter` CSS class needs to be applied to the icon. === 4.1.2 === diff --git a/mod/lti/locallib.php b/mod/lti/locallib.php index 1ced06aeea0..8c1728fc95b 100644 --- a/mod/lti/locallib.php +++ b/mod/lti/locallib.php @@ -2387,7 +2387,15 @@ function lti_get_configured_types($courseid, $sectionreturn = 0) { $type->help = clean_param($trimmeddescription, PARAM_NOTAGS); $type->helplink = get_string('modulename_shortcut_link', 'lti'); } - $type->icon = html_writer::empty_tag('img', ['src' => get_tool_type_icon_url($ltitype), 'alt' => '', 'class' => 'icon']); + + $iconurl = get_tool_type_icon_url($ltitype); + $iconclass = ''; + if ($iconurl !== $OUTPUT->image_url('monologo', 'lti')->out()) { + // Do not filter the icon if it is not the default LTI activity icon. + $iconclass = 'nofilter'; + } + $type->icon = html_writer::empty_tag('img', ['src' => $iconurl, 'alt' => '', 'class' => "icon $iconclass"]); + $type->link = new moodle_url('/course/modedit.php', array('add' => 'lti', 'return' => 0, 'course' => $courseid, 'sr' => $sectionreturn, 'typeid' => $ltitype->id)); $types[] = $type; diff --git a/mod/url/lib.php b/mod/url/lib.php index dae393ac800..256c3eb516d 100644 --- a/mod/url/lib.php +++ b/mod/url/lib.php @@ -245,6 +245,8 @@ function url_get_coursemodule_info($coursemodule) { } $info->customdata['display'] = $display; + // The icon will be filtered if it will be the default module icon. + $info->customdata['filtericon'] = empty($info->icon); return $info; } diff --git a/theme/boost/classes/output/core_renderer.php b/theme/boost/classes/output/core_renderer.php index d5123d02749..7f6520b812d 100644 --- a/theme/boost/classes/output/core_renderer.php +++ b/theme/boost/classes/output/core_renderer.php @@ -161,8 +161,13 @@ class core_renderer extends \core_renderer { $heading = format_string($this->page->course->fullname, true, ['context' => $context]); } else { $heading = $this->page->cm->get_formatted_name(); - $imagedata = html_writer::img($this->page->cm->get_icon_url()->out(false), '', - ['class' => 'icon activityicon', 'aria-hidden' => 'true']); + $iconurl = $this->page->cm->get_icon_url(); + $iconclass = $iconurl->get_param('filtericon') ? '' : 'nofilter'; + $iconattrs = [ + 'class' => "icon activityicon $iconclass", + 'aria-hidden' => 'true' + ]; + $imagedata = html_writer::img($iconurl->out(false), '', $iconattrs); $purposeclass = plugin_supports('mod', $this->page->activityname, FEATURE_MOD_PURPOSE); $purposeclass .= ' activityiconcontainer'; $purposeclass .= ' modicon_' . $this->page->activityname; diff --git a/theme/boost/scss/moodle/icons.scss b/theme/boost/scss/moodle/icons.scss index 17bf8fe0cae..fa6baf1999c 100644 --- a/theme/boost/scss/moodle/icons.scss +++ b/theme/boost/scss/moodle/icons.scss @@ -151,7 +151,9 @@ $iconsizes: map-merge(( background-color: $value; .activityicon, .icon { - filter: brightness(0) invert(1); + &:not(.nofilter) { + filter: brightness(0) invert(1); + } } } } diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index b1b96fab2c3..e7e8fe341f7 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -12506,38 +12506,38 @@ blockquote { .activityiconcontainer.administration { background-color: #5d63f6; } - .activityiconcontainer.administration .activityicon, - .activityiconcontainer.administration .icon { + .activityiconcontainer.administration .activityicon:not(.nofilter), + .activityiconcontainer.administration .icon:not(.nofilter) { filter: brightness(0) invert(1); } .activityiconcontainer.assessment { background-color: #eb66a2; } - .activityiconcontainer.assessment .activityicon, - .activityiconcontainer.assessment .icon { + .activityiconcontainer.assessment .activityicon:not(.nofilter), + .activityiconcontainer.assessment .icon:not(.nofilter) { filter: brightness(0) invert(1); } .activityiconcontainer.collaboration { background-color: #f7634d; } - .activityiconcontainer.collaboration .activityicon, - .activityiconcontainer.collaboration .icon { + .activityiconcontainer.collaboration .activityicon:not(.nofilter), + .activityiconcontainer.collaboration .icon:not(.nofilter) { filter: brightness(0) invert(1); } .activityiconcontainer.communication { background-color: #11a676; } - .activityiconcontainer.communication .activityicon, - .activityiconcontainer.communication .icon { + .activityiconcontainer.communication .activityicon:not(.nofilter), + .activityiconcontainer.communication .icon:not(.nofilter) { filter: brightness(0) invert(1); } .activityiconcontainer.content { background-color: #399be2; } - .activityiconcontainer.content .activityicon, - .activityiconcontainer.content .icon { + .activityiconcontainer.content .activityicon:not(.nofilter), + .activityiconcontainer.content .icon:not(.nofilter) { filter: brightness(0) invert(1); } .activityiconcontainer.interface { background-color: #a378ff; } - .activityiconcontainer.interface .activityicon, - .activityiconcontainer.interface .icon { + .activityiconcontainer.interface .activityicon:not(.nofilter), + .activityiconcontainer.interface .icon:not(.nofilter) { filter: brightness(0) invert(1); } :root { diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css index 63c38fdc2e2..9cbf8056dba 100644 --- a/theme/classic/style/moodle.css +++ b/theme/classic/style/moodle.css @@ -12506,38 +12506,38 @@ blockquote { .activityiconcontainer.administration { background-color: #5d63f6; } - .activityiconcontainer.administration .activityicon, - .activityiconcontainer.administration .icon { + .activityiconcontainer.administration .activityicon:not(.nofilter), + .activityiconcontainer.administration .icon:not(.nofilter) { filter: brightness(0) invert(1); } .activityiconcontainer.assessment { background-color: #eb66a2; } - .activityiconcontainer.assessment .activityicon, - .activityiconcontainer.assessment .icon { + .activityiconcontainer.assessment .activityicon:not(.nofilter), + .activityiconcontainer.assessment .icon:not(.nofilter) { filter: brightness(0) invert(1); } .activityiconcontainer.collaboration { background-color: #f7634d; } - .activityiconcontainer.collaboration .activityicon, - .activityiconcontainer.collaboration .icon { + .activityiconcontainer.collaboration .activityicon:not(.nofilter), + .activityiconcontainer.collaboration .icon:not(.nofilter) { filter: brightness(0) invert(1); } .activityiconcontainer.communication { background-color: #11a676; } - .activityiconcontainer.communication .activityicon, - .activityiconcontainer.communication .icon { + .activityiconcontainer.communication .activityicon:not(.nofilter), + .activityiconcontainer.communication .icon:not(.nofilter) { filter: brightness(0) invert(1); } .activityiconcontainer.content { background-color: #399be2; } - .activityiconcontainer.content .activityicon, - .activityiconcontainer.content .icon { + .activityiconcontainer.content .activityicon:not(.nofilter), + .activityiconcontainer.content .icon:not(.nofilter) { filter: brightness(0) invert(1); } .activityiconcontainer.interface { background-color: #a378ff; } - .activityiconcontainer.interface .activityicon, - .activityiconcontainer.interface .icon { + .activityiconcontainer.interface .activityicon:not(.nofilter), + .activityiconcontainer.interface .icon:not(.nofilter) { filter: brightness(0) invert(1); } :root {