<commit_before>using Stylet;
using SyncTrayzor.SyncThing;
using SyncTrayzor.Xaml;
using SyncTrayzor.Utils;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Navigation;
using CefSharp;
using CefSharp.Wpf;
using SyncTrayzor.Localization;
using SyncTrayzor.Services.Config;
using System.Threading;
using SyncTrayzor.Properties;
using SyncTrayzor.Services;

namespace SyncTrayzor.Pages
{
    public class ViewerViewModel : Screen, IRequestHandler, ILifeSpanHandler
    {
        private readonly IWindowManager windowManager;
        private readonly ISyncThingManager syncThingManager;
        private readonly IProcessStartProvider processStartProvider;
        private readonly IConfigurationProvider configurationProvider;

        private readonly object cultureLock = new object(); // This can be read from many threads
        private CultureInfo culture;

        public string Location { get; private set; }
        
        private SyncThingState syncThingState { get; set; }
        public bool ShowSyncThingStarting { get { return this.syncThingState == SyncThingState.Starting; } }
        public bool ShowSyncThingStopped { get { return this.syncThingState == SyncThingState.Stopped; ; } }

        public IWpfWebBrowser WebBrowser { get; set; }

        private JavascriptCallbackObject callback;

        public ViewerViewModel(
            IWindowManager windowManager,
            ISyncThingManager syncThingManager,
            IConfigurationProvider configurationProvider,
            IProcessStartProvider processStartProvider)
        {
            this.windowManager = windowManager;
            this.syncThingManager = syncThingManager;
            this.processStartProvider = processStartProvider;
            this.configurationProvider = configurationProvider;

            this.syncThingManager.StateChanged += (o, e) =>
            {
                this.syncThingState = e.NewState;
                this.RefreshBrowser();
            };

            this.callback = new JavascriptCallbackObject(this.OpenFolder);

            this.Bind(x => x.WebBrowser, (o, e) =>
            {
                if (e.NewValue != null)
                    this.InitializeBrowser(e.NewValue);
            });

            this.SetCulture(configurationProvider.Load());
            configurationProvider.ConfigurationChanged += (o, e) => this.SetCulture(e.NewConfiguration);
        }

        private void SetCulture(Configuration configuration)
        {
            lock (this.cultureLock)
            {
                this.culture = configuration.UseComputerCulture ? Thread.CurrentThread.CurrentUICulture : null;
            }
        }

        protected override void OnInitialActivate()
        {
            Cef.Initialize(new CefSettings()
            {
                RemoteDebuggingPort = Settings.Default.CefRemoteDebuggingPort,
            });
        }

        private void InitializeBrowser(IWpfWebBrowser webBrowser)
        {
            webBrowser.RequestHandler = this;
            webBrowser.LifeSpanHandler = this;
            webBrowser.RegisterJsObject("callbackObject", this.callback);

            webBrowser.FrameLoadEnd += (o, e) =>
            {
                if (e.IsMainFrame && e.Url != "about:blank")
                {
                    Execute.OnUIThread(() =>
                    {
                        // See https://github.com/cefsharp/CefSharp/issues/738#issuecomment-91099199
                        // ... Except that FrameLoadStart doesn't appear to work for me. FrameLoadEnd does though
                        webBrowser.ZoomLevel = this.configurationProvider.Load().SyncthingWebBrowserZoomLevel;
                    });

                    var script = @"$('#folders .panel-footer .pull-right').prepend(" +
                    @"'<button class=""btn btn-sm btn-default"" onclick=""callbackObject.openFolder(angular.element(this).scope().folder.ID)"">" +
                    @"<span class=""glyphicon glyphicon-folder-open""></span>" +
                    @"<span style=""margin-left: 12px"">" +
                    Localizer.Translate("ViewerView_OpenFolder") +
                    "</span></button>')";
                    webBrowser.ExecuteScriptAsync(script);
                }
            };
        }

        public void RefreshBrowser()
        {
            this.Location = "about:blank";
            if (this.syncThingManager.State == SyncThingState.Running)
                this.Location = this.syncThingManager.Address.NormalizeZeroHost().ToString();
        }

        public void ZoomIn()
        {
            this.ZoomBy(0.2);
        }

        public void ZoomOut()
        {
            this.ZoomBy(-0.2);
        }

        private void ZoomBy(double amount)
        {
            if (this.WebBrowser == null || this.syncThingState != SyncThingState.Running)
                return;

            this.WebBrowser.ZoomLevel += amount;
            this.configurationProvider.AtomicLoadAndSave(c => c.SyncthingWebBrowserZoomLevel = this.WebBrowser.ZoomLevel);
        }

        public void ZoomReset()
        {
            if (this.WebBrowser == null || this.syncThingState != SyncThingState.Running)
                return;

            this.WebBrowser.ZoomLevel = 0;
            this.configurationProvider.AtomicLoadAndSave(c => c.SyncthingWebBrowserZoomLevel = this.WebBrowser.ZoomLevel);
        }

        private void OpenFolder(string folderId)<commit_msg>Fix threading issues with CefSharp and ZoomLevel<commit_after>using Stylet;
using SyncTrayzor.SyncThing;
using SyncTrayzor.Xaml;
using SyncTrayzor.Utils;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Navigation;
using CefSharp;
using CefSharp.Wpf;
using SyncTrayzor.Localization;
using SyncTrayzor.Services.Config;
using System.Threading;
using SyncTrayzor.Properties;
using SyncTrayzor.Services;

namespace SyncTrayzor.Pages
{
    public class ViewerViewModel : Screen, IRequestHandler, ILifeSpanHandler
    {
        private readonly IWindowManager windowManager;
        private readonly ISyncThingManager syncThingManager;
        private readonly IProcessStartProvider processStartProvider;
        private readonly IConfigurationProvider configurationProvider;

        private readonly object cultureLock = new object(); // This can be read from many threads
        private CultureInfo culture;
        private double zoomLevel;

        public string Location { get; private set; }
        
        private SyncThingState syncThingState { get; set; }
        public bool ShowSyncThingStarting { get { return this.syncThingState == SyncThingState.Starting; } }
        public bool ShowSyncThingStopped { get { return this.syncThingState == SyncThingState.Stopped; ; } }

        public ChromiumWebBrowser WebBrowser { get; set; }

        private JavascriptCallbackObject callback;

        public ViewerViewModel(
            IWindowManager windowManager,
            ISyncThingManager syncThingManager,
            IConfigurationProvider configurationProvider,
            IProcessStartProvider processStartProvider)
        {
            this.windowManager = windowManager;
            this.syncThingManager = syncThingManager;
            this.processStartProvider = processStartProvider;
            this.configurationProvider = configurationProvider;

            var configuration = this.configurationProvider.Load();
            this.zoomLevel = configuration.SyncthingWebBrowserZoomLevel;

            this.syncThingManager.StateChanged += (o, e) =>
            {
                this.syncThingState = e.NewState;
                this.RefreshBrowser();
            };

            this.callback = new JavascriptCallbackObject(this.OpenFolder);

            this.Bind(x => x.WebBrowser, (o, e) =>
            {
                if (e.NewValue != null)
                    this.InitializeBrowser(e.NewValue);
            });

            this.SetCulture(configuration);
            configurationProvider.ConfigurationChanged += (o, e) => this.SetCulture(e.NewConfiguration);
        }

        private void SetCulture(Configuration configuration)
        {
            lock (this.cultureLock)
            {
                this.culture = configuration.UseComputerCulture ? Thread.CurrentThread.CurrentUICulture : null;
            }
        }

        protected override void OnInitialActivate()
        {
            Cef.Initialize(new CefSettings()
            {
                RemoteDebuggingPort = Settings.Default.CefRemoteDebuggingPort,
            });
        }

        private void InitializeBrowser(ChromiumWebBrowser webBrowser)
        {
            webBrowser.RequestHandler = this;
            webBrowser.LifeSpanHandler = this;
            webBrowser.RegisterJsObject("callbackObject", this.callback);

            // So. Fun story. From https://github.com/cefsharp/CefSharp/issues/738#issuecomment-91099199, we need to set the zoom level
            // in the FrameLoadStart event. However, the IWpfWebBrowser's ZoomLevel is a DependencyProperty, and it wraps
            // the SetZoomLevel method on the unmanaged browser (which is exposed directly by ChromiumWebBrowser, but not by IWpfWebBrowser).
            // Now, FrameLoadState and FrameLoadEnd are called on a background thread, and since ZoomLevel is a DP, it can only be changed
            // from the UI thread (it's "helpful" and does a dispatcher check for us). But, if we dispatch back to the UI thread to call
            // ZoomLevel = xxx, then CEF seems to hit threading issues, and can sometimes render things entirely badly (massive icons, no
            // localization, bad spacing, no JavaScript at all, etc).
            // So, in this case, we need to call SetZoomLevel directly, as we can do that from the thread on which FrameLoadStart is called,
            // and everything's happy.
            // However, this means that the DP value isn't updated... Which means we can't use the DP at all. We have to call SetZoomLevel
            // *everywhere*, and that means keeping a local field zoomLevel to track the current zoom level. Such is life

            webBrowser.FrameLoadStart += (o, e) => webBrowser.SetZoomLevel(this.zoomLevel);
            webBrowser.FrameLoadEnd += (o, e) =>
            {
                if (e.IsMainFrame && e.Url != "about:blank")
                {
                    var script = @"$('#folders .panel-footer .pull-right').prepend(" +
                    @"'<button class=""btn btn-sm btn-default"" onclick=""callbackObject.openFolder(angular.element(this).scope().folder.ID)"">" +
                    @"<span class=""glyphicon glyphicon-folder-open""></span>" +
                    @"<span style=""margin-left: 12px"">" +
                    Localizer.Translate("ViewerView_OpenFolder") +
                    "</span></button>')";
                    webBrowser.ExecuteScriptAsync(script);
                }
            };
        }

        public void RefreshBrowser()
        {
            this.Location = "about:blank";
            if (this.syncThingManager.State == SyncThingState.Running)
                this.Location = this.syncThingManager.Address.NormalizeZeroHost().ToString();
        }

        public void ZoomIn()
        {
            this.ZoomTo(this.zoomLevel + 0.2);
        }

        public void ZoomOut()
        {
            this.ZoomTo(this.zoomLevel - 0.2);
        }

        public void ZoomReset()
        {
            this.ZoomTo(0.0);
        }

        private void ZoomTo(double zoomLevel)
        {
            if (this.WebBrowser == null || this.syncThingState != SyncThingState.Running)
                return;

            this.zoomLevel = zoomLevel;
            this.WebBrowser.SetZoomLevel(zoomLevel);
            this.configurationProvider.AtomicLoadAndSave(c => c.SyncthingWebBrowserZoomLevel = zoomLevel);
        }

        private void OpenFolder(string folderId)
{'fraction_non_alphanumeric': 0.08619274339188968, 'fraction_numerical': 0.0030372681004761123, 'mean_word_length': 2.175964546402503, 'pattern_counts': {'":': 0, '<': 15, '<?xml version=': 0, '>': 30, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '399309', 'n_tokens_mistral': 3194, 'n_tokens_neox': 3024, 'n_words': 721}