From e00396fc2ae6cd43eb4cc0e1e99794af486ddb7b Mon Sep 17 00:00:00 2001 From: Evan Date: Tue, 7 Feb 2023 20:10:25 -0500 Subject: [PATCH] Use different default ace theme depending on site theme --- django_ace/static/django_ace/widget.js | 17 +++++++++++++++ django_ace/widgets.py | 2 ++ dmoj/settings.py | 9 ++++++++ judge/admin/contest.py | 4 +++- judge/admin/profile.py | 4 +++- judge/admin/runtime.py | 4 +++- judge/admin/submission.py | 5 +++-- judge/forms.py | 2 +- judge/migrations/0138_dark_ace_theme.py | 29 +++++++++++++++++++++++++ judge/models/choices.py | 1 + judge/models/profile.py | 11 +++++++++- judge/views/problem.py | 2 +- 12 files changed, 82 insertions(+), 8 deletions(-) create mode 100644 judge/migrations/0138_dark_ace_theme.py diff --git a/django_ace/static/django_ace/widget.js b/django_ace/static/django_ace/widget.js index ce5cd830c..7da6f3243 100644 --- a/django_ace/static/django_ace/widget.js +++ b/django_ace/static/django_ace/widget.js @@ -76,6 +76,8 @@ editor = ace.edit(div), mode = widget.getAttribute('data-mode'), theme = widget.getAttribute('data-theme'), + default_light_theme = widget.getAttribute('data-default-light-theme'), + default_dark_theme = widget.getAttribute('data-default-dark-theme'), wordwrap = widget.getAttribute('data-wordwrap'), toolbar = prev(widget), main_block = toolbar.parentNode; @@ -98,6 +100,21 @@ } if (theme) { editor.setTheme("ace/theme/" + theme); + } else { + if (window.matchMedia) { + const setEditorTheme = function (is_dark) { + if (is_dark) { + editor.setTheme("ace/theme/" + default_dark_theme); + } else { + editor.setTheme("ace/theme/" + default_light_theme); + } + } + + setEditorTheme(window.matchMedia('(prefers-color-scheme: dark)').matches); + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(ev) { + setEditorTheme(ev.matches); + }) + } } if (wordwrap == "true") { editor.getSession().setUseWrapMode(true); diff --git a/django_ace/widgets.py b/django_ace/widgets.py index 91369d039..2f521bf7f 100644 --- a/django_ace/widgets.py +++ b/django_ace/widgets.py @@ -42,6 +42,8 @@ class AceWidget(forms.Textarea): ace_attrs['data-mode'] = self.mode if self.theme: ace_attrs['data-theme'] = self.theme + ace_attrs['data-default-light-theme'] = settings.ACE_DEFAULT_LIGHT_THEME + ace_attrs['data-default-dark-theme'] = settings.ACE_DEFAULT_DARK_THEME if self.wordwrap: ace_attrs['data-wordwrap'] = 'true' diff --git a/dmoj/settings.py b/dmoj/settings.py index 8eb6c5e38..222f578a6 100644 --- a/dmoj/settings.py +++ b/dmoj/settings.py @@ -106,6 +106,11 @@ DMOJ_THEME_CSS = { 'light': 'style.css', 'dark': 'dark/style.css', } +# At the bare minimum, dark and light ace themes must be declared +DMOJ_THEME_DEFAULT_ACE_THEME = { + 'light': 'github', + 'dark': 'twilight', +} DMOJ_SELECT2_THEME = 'dmoj' MARKDOWN_STYLES = {} @@ -601,3 +606,7 @@ except IOError: # Check settings are consistent assert DMOJ_PROBLEM_MIN_USER_POINTS_VOTE >= DMOJ_PROBLEM_MIN_PROBLEM_POINTS + +# Compute these values after local_settings.py is loaded +ACE_DEFAULT_LIGHT_THEME = DMOJ_THEME_DEFAULT_ACE_THEME['light'] +ACE_DEFAULT_DARK_THEME = DMOJ_THEME_DEFAULT_ACE_THEME['dark'] diff --git a/judge/admin/contest.py b/judge/admin/contest.py index 4bb3604ab..3f8d6f8f1 100644 --- a/judge/admin/contest.py +++ b/judge/admin/contest.py @@ -308,7 +308,9 @@ class ContestAdmin(NoBatchDeleteMixin, VersionAdmin): if 'problem_label_script' in form.base_fields: # form.base_fields['problem_label_script'] does not exist when the user has only view permission # on the model. - form.base_fields['problem_label_script'].widget = AceWidget('lua', request.profile.ace_theme) + form.base_fields['problem_label_script'].widget = AceWidget( + mode='lua', theme=request.profile.resolved_ace_theme, + ) perms = ('edit_own_contest', 'edit_all_contest') form.base_fields['curators'].queryset = Profile.objects.filter( diff --git a/judge/admin/profile.py b/judge/admin/profile.py index bf9a3364d..d839b0b47 100644 --- a/judge/admin/profile.py +++ b/judge/admin/profile.py @@ -126,5 +126,7 @@ class ProfileAdmin(NoBatchDeleteMixin, VersionAdmin): form = super(ProfileAdmin, self).get_form(request, obj, **kwargs) if 'user_script' in form.base_fields: # form.base_fields['user_script'] does not exist when the user has only view permission on the model. - form.base_fields['user_script'].widget = AceWidget('javascript', request.profile.ace_theme) + form.base_fields['user_script'].widget = AceWidget( + mode='javascript', theme=request.profile.resolved_ace_theme, + ) return form diff --git a/judge/admin/runtime.py b/judge/admin/runtime.py index 02010bd1d..3c756527c 100644 --- a/judge/admin/runtime.py +++ b/judge/admin/runtime.py @@ -27,7 +27,9 @@ class LanguageAdmin(VersionAdmin): def get_form(self, request, obj=None, **kwargs): form = super(LanguageAdmin, self).get_form(request, obj, **kwargs) if obj is not None: - form.base_fields['template'].widget = AceWidget(obj.ace, request.profile.ace_theme) + form.base_fields['template'].widget = AceWidget( + mode=obj.ace, theme=request.profile.resolved_ace_theme, + ) return form diff --git a/judge/admin/submission.py b/judge/admin/submission.py index 591a2eb10..64b675e49 100644 --- a/judge/admin/submission.py +++ b/judge/admin/submission.py @@ -107,8 +107,9 @@ class SubmissionSourceInline(admin.StackedInline): extra = 0 def get_formset(self, request, obj=None, **kwargs): - kwargs.setdefault('widgets', {})['source'] = AceWidget(mode=obj and obj.language.ace, - theme=request.profile.ace_theme) + kwargs.setdefault('widgets', {})['source'] = AceWidget( + mode=obj and obj.language.ace, theme=request.profile.resolved_ace_theme, + ) return super().get_formset(request, obj, **kwargs) diff --git a/judge/forms.py b/judge/forms.py index 70c3aceb7..5a328235b 100644 --- a/judge/forms.py +++ b/judge/forms.py @@ -92,7 +92,7 @@ class ProfileForm(ModelForm): ) if not self.fields['organizations'].queryset: self.fields.pop('organizations') - self.fields['user_script'].widget = AceWidget(theme=user.profile.ace_theme, mode='javascript') + self.fields['user_script'].widget = AceWidget(mode='javascript', theme=user.profile.resolved_ace_theme) class DownloadDataForm(Form): diff --git a/judge/migrations/0138_dark_ace_theme.py b/judge/migrations/0138_dark_ace_theme.py new file mode 100644 index 000000000..110e873f8 --- /dev/null +++ b/judge/migrations/0138_dark_ace_theme.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2.16 on 2023-02-08 01:11 + +from django.db import migrations, models + + +def github_to_auto(apps, schema_editor): + Profile = apps.get_model('judge', 'Profile') + Profile.objects.filter(ace_theme='github').update(ace_theme='auto') + + +def auto_to_github(apps, schema_editor): + Profile = apps.get_model('judge', 'Profile') + Profile.objects.filter(ace_theme='auto').update(ace_theme='github') + + +class Migration(migrations.Migration): + + dependencies = [ + ('judge', '0137_profile_site_theme'), + ] + + operations = [ + migrations.AlterField( + model_name='profile', + name='ace_theme', + field=models.CharField(choices=[('auto', 'Follow theme default'), ('ambiance', 'Ambiance'), ('chaos', 'Chaos'), ('chrome', 'Chrome'), ('clouds', 'Clouds'), ('clouds_midnight', 'Clouds Midnight'), ('cobalt', 'Cobalt'), ('crimson_editor', 'Crimson Editor'), ('dawn', 'Dawn'), ('dreamweaver', 'Dreamweaver'), ('eclipse', 'Eclipse'), ('github', 'Github'), ('idle_fingers', 'Idle Fingers'), ('katzenmilch', 'Katzenmilch'), ('kr_theme', 'KR Theme'), ('kuroir', 'Kuroir'), ('merbivore', 'Merbivore'), ('merbivore_soft', 'Merbivore Soft'), ('mono_industrial', 'Mono Industrial'), ('monokai', 'Monokai'), ('pastel_on_dark', 'Pastel on Dark'), ('solarized_dark', 'Solarized Dark'), ('solarized_light', 'Solarized Light'), ('terminal', 'Terminal'), ('textmate', 'Textmate'), ('tomorrow', 'Tomorrow'), ('tomorrow_night', 'Tomorrow Night'), ('tomorrow_night_blue', 'Tomorrow Night Blue'), ('tomorrow_night_bright', 'Tomorrow Night Bright'), ('tomorrow_night_eighties', 'Tomorrow Night Eighties'), ('twilight', 'Twilight'), ('vibrant_ink', 'Vibrant Ink'), ('xcode', 'XCode')], default='auto', max_length=30, verbose_name='Ace theme'), + ), + migrations.RunPython(github_to_auto, auto_to_github, atomic=True), + ] diff --git a/judge/models/choices.py b/judge/models/choices.py index c7b73375c..e8c7852c4 100644 --- a/judge/models/choices.py +++ b/judge/models/choices.py @@ -21,6 +21,7 @@ TIMEZONE = make_timezones() del make_timezones ACE_THEMES = ( + ('auto', _('Follow theme default')), ('ambiance', 'Ambiance'), ('chaos', 'Chaos'), ('chrome', 'Chrome'), diff --git a/judge/models/profile.py b/judge/models/profile.py index 09685d542..7efd1036e 100644 --- a/judge/models/profile.py +++ b/judge/models/profile.py @@ -154,7 +154,7 @@ class Profile(models.Model): points = models.FloatField(default=0, db_index=True) performance_points = models.FloatField(default=0, db_index=True) problem_count = models.IntegerField(default=0, db_index=True) - ace_theme = models.CharField(max_length=30, verbose_name=_('Ace theme'), choices=ACE_THEMES, default='github') + ace_theme = models.CharField(max_length=30, verbose_name=_('Ace theme'), choices=ACE_THEMES, default='auto') site_theme = models.CharField(max_length=10, verbose_name=_('site theme'), choices=SITE_THEMES, default='auto') last_access = models.DateTimeField(verbose_name=_('last access time'), default=now) ip = models.GenericIPAddressField(verbose_name=_('last IP'), blank=True, null=True) @@ -226,6 +226,15 @@ class Profile(models.Model): def has_any_solves(self): return self.submission_set.filter(result='AC', case_points__gte=F('case_total')).exists() + @cached_property + def resolved_ace_theme(self): + if self.ace_theme != 'auto': + return self.ace_theme + if self.site_theme != 'auto': + return settings.DMOJ_THEME_DEFAULT_ACE_THEME.get(self.site_theme) + # This must be resolved client-side using prefers-color-scheme. + return None + _pp_table = [pow(settings.DMOJ_PP_STEP, i) for i in range(settings.DMOJ_PP_ENTRIES)] def calculate_points(self, table=_pp_table): diff --git a/judge/views/problem.py b/judge/views/problem.py index 07fec5f76..f8d2e559a 100644 --- a/judge/views/problem.py +++ b/judge/views/problem.py @@ -691,7 +691,7 @@ class ProblemSubmit(LoginRequiredMixin, ProblemMixin, TitleMixin, SingleObjectFo form_data = getattr(form, 'cleaned_data', form.initial) if 'language' in form_data: form.fields['source'].widget.mode = form_data['language'].ace - form.fields['source'].widget.theme = self.request.profile.ace_theme + form.fields['source'].widget.theme = self.request.profile.resolved_ace_theme return form