public SearchInfoCollection(ISearchMetadataCollection searchMetadataCollection, ISearchMetadataFactory searchMetadataFactory, IFileWatcher fileWatcher) { _metadataCollection = searchMetadataCollection; _searchMetadataFactory = searchMetadataFactory; _fileWatcher = fileWatcher; //Add a complete file display All = _fileWatcher.Latest.Index().Replay(1).RefCount(); //create a collection with 1 item, which is used to show entire file var systemSearches = new SourceCache<SearchInfo, string>(t => t.SearchText); systemSearches.AddOrUpdate(new SearchInfo("<All>", All, SearchType.All)); //create a collection of all possible user filters var userSearches = searchMetadataCollection.Metadata .Connect(meta => meta.Filter) .IgnoreUpdateWhen((current,previous)=> SearchMetadata.EffectsFilterComparer.Equals(current, previous)) .Transform(meta => { var latest = _fileWatcher.Latest .Search(meta.BuildPredicate()) .Replay(1).RefCount(); return new SearchInfo(meta.SearchText, latest, SearchType.User); }); //combine te results into a single collection Searches = systemSearches.Connect() .Or(userSearches) .AsObservableCache(); _cleanUp = new CompositeDisposable(Searches, systemSearches); }
public SearchInfoCollection(ISearchMetadataCollection searchMetadataCollection, IFileWatcher fileWatcher) { _metadataCollection = searchMetadataCollection; _fileWatcher = fileWatcher; //Add a complete file display All = fileWatcher.Latest.Index().Replay(1).RefCount(); //create a collection with 1 item, which is used to show entire file var systemSearches = new SourceCache <SearchInfo, string>(t => t.SearchText); systemSearches.AddOrUpdate(new SearchInfo("<All>", All, SearchType.All)); //create a collection of all possible user filters var userSearches = searchMetadataCollection.Metadata .Connect(meta => meta.Filter) .IgnoreUpdateWhen((current, previous) => SearchMetadata.EffectsFilterComparer.Equals(current, previous)) .Transform(meta => { var latest = _fileWatcher.Latest .Search(meta.BuildPredicate()) .Replay(1).RefCount(); return(new SearchInfo(meta.SearchText, latest, SearchType.User)); }); //combine te results into a single collection Searches = systemSearches.Connect() .Or(userSearches) .AsObservableCache(); _cleanUp = new CompositeDisposable(Searches, systemSearches); }
public SearchInfoCollection(ISearchMetadataCollection searchMetadataCollection, IFileWatcher fileWatcher) { _metadataCollection = searchMetadataCollection; _fileWatcher = fileWatcher; //Add a complete file display All = fileWatcher.Latest.Index().Replay(1).RefCount(); //create a collection with 1 item, which is used to show entire file var systemSearches = new SourceCache<SearchInfo, CaseInsensitiveString>(t => (CaseInsensitiveString)t.SearchText); systemSearches.AddOrUpdate(new SearchInfo("<All>", All, SearchType.All)); //create a collection of all possible user filters var userSearches = searchMetadataCollection.Metadata .Connect(meta => meta.Filter) .IgnoreUpdateWhen((current,previous)=>current.Filter == previous.Filter) .Transform(meta => { var latest = _fileWatcher.Latest .Search(s => s.Contains(meta.SearchText, StringComparison.OrdinalIgnoreCase)) .Replay(1).RefCount(); return new SearchInfo(meta.SearchText, latest, SearchType.User); }); //combine te results into a single collection Searches = systemSearches.Connect() .Or(userSearches) .AsObservableCache(); _cleanUp = new CompositeDisposable(Searches, systemSearches); }
public CombinedSearchMetadataCollection([NotNull] ISearchMetadataCollection metadataCollection, [NotNull] IGlobalSearchOptions globalSearchOptions) { if (metadataCollection == null) { throw new ArgumentNullException(nameof(metadataCollection)); } if (globalSearchOptions == null) { throw new ArgumentNullException(nameof(globalSearchOptions)); } Local = metadataCollection; Global = globalSearchOptions.Metadata; var cache = new SourceCache <SearchMetadata, string>(t => t.SearchText); ////Prioritise local before global and renumber var localItems = metadataCollection.Metadata.Connect().ToCollection().Select(items => items.ToArray()).StartWith(Enumerable.Empty <SearchMetadata>()); var globalItems = globalSearchOptions.Metadata.Metadata.Connect().ToCollection().Select(items => items.ToArray()).StartWith(Enumerable.Empty <SearchMetadata>()); var combiner = localItems.CombineLatest(globalItems, (local, global) => new { local, global }).Select(x => Combine(x.local, x.global)).Subscribe(uppdatedItems => { cache.Edit(innerCache => { var toRemove = innerCache.Items.Except(uppdatedItems, SearchMetadata.SearchTextComparer).ToArray(); innerCache.Remove(toRemove); innerCache.AddOrUpdate(uppdatedItems); }); }); Combined = cache.Connect().IgnoreUpdateWhen((current, previous) => current.Equals(previous)).AsObservableCache(); _cleanUp = new CompositeDisposable(Combined, cache, combiner); }
public InlineViewer Create(IObservable<ILineProvider> lineProvider, IObservable<LineProxy> selectedChanged, ISearchMetadataCollection searchMetadataCollection) { var args = new InlineViewerArgs(lineProvider, selectedChanged, searchMetadataCollection); return _objectProvider.Get<InlineViewer>(new ExplictArg("args", args)); }
public InlineViewer Create(IObservable <ILineProvider> lineProvider, IObservable <LineProxy> selectedChanged, ISearchMetadataCollection searchMetadataCollection) { var args = new InlineViewerArgs(lineProvider, selectedChanged, searchMetadataCollection); return(_objectProvider.Get <InlineViewer>(new ExplictArg("args", args))); }
public SearchInfoCollection(ISearchMetadataCollection metadataCollection) { //var filters = metadataCollection.Metadata // .Connect(m=>m.Filter) // .Transform(meta=>new SearchInfo(meta.SearchText,)) Searches = _searches.AsObservableCache(); _cleanUp = new CompositeDisposable(_searches, Searches); }
public ISearchProxyCollection Create([NotNull] ISearchMetadataCollection metadataCollection, Guid id, Action <SearchMetadata> changeScopeAction) { if (metadataCollection == null) { throw new ArgumentNullException(nameof(metadataCollection)); } return(new SearchProxyCollection(metadataCollection, id, changeScopeAction, SchedulerProvider, ColourProvider, IconsProvider, TextAssociationCollection, ThemeProvider)); }
public TextFormatter(ISearchMetadataCollection searchMetadataCollection) { _strings = searchMetadataCollection.Metadata .Connect(meta => meta.Highlight) .IgnoreUpdateWhen((current, previous) => SearchMetadata.EffectsHighlightComparer.Equals(current, previous)) .QueryWhenChanged(query => query.Items.OrderBy(m => m.Position)) .Replay(1) .RefCount(); }
public TextFormatter(ISearchMetadataCollection searchMetadataCollection) { _strings = searchMetadataCollection.Metadata .Connect(meta => meta.Highlight) .IgnoreUpdateWhen((current, previous) => current.Highlight == previous.Highlight) .QueryWhenChanged(query => query.Items.Select(si => si.SearchText)) .Replay(1) .RefCount(); }
public LineMatches(ISearchMetadataCollection searchMetadataCollection) { _strings = searchMetadataCollection.Metadata .Connect() .IgnoreUpdateWhen((current, previous) => SearchMetadata.EffectsHighlightComparer.Equals(current, previous)) .QueryWhenChanged(query => query.Items.OrderBy(si => si.Position)) .Replay(1) .RefCount(); }
public LineMatches(ISearchMetadataCollection searchMetadataCollection) { _strings = searchMetadataCollection.Metadata .Connect(meta => meta.Filter) .IgnoreUpdateWhen((current, previous) => SearchMetadata.EffectsHighlightComparer.Equals(current, previous)) .QueryWhenChanged(query => query.Items.Select(si => si)) .Replay(1) .RefCount(); }
public SearchOptionsViewModel(ISearchMetadataCollection metadataCollection, ISchedulerProvider schedulerProvider) { //TODO: options for colour var swatches = new SwatchesProvider().Swatches; ReadOnlyObservableCollection<SearchOptionsProxy> data; var userOptions = metadataCollection.Metadata.Connect() .WhereReasonsAre(ChangeReason.Add, ChangeReason.Remove) //ignore updates because we update from here .Transform(meta => new SearchOptionsProxy(meta, swatches,m => metadataCollection.Remove(m.SearchText))) .SubscribeMany(so => { //when a value changes, write the original value back to the cache return so.WhenAnyPropertyChanged() .Subscribe(_ => metadataCollection.Add(new SearchMetadata(so.Text, so.Filter, so.Highlight))); }) .Sort(SortExpressionComparer<SearchOptionsProxy>.Ascending(proxy=>proxy.Text)) .ObserveOn(schedulerProvider.MainThread) .Bind(out data) .Subscribe(); Data = data; AddSearchCommand = new Command(() => { metadataCollection.Add(new SearchMetadata(SearchText,false,true)); SearchText = string.Empty; }, () => SearchText.IsLongerThanOrEqualTo(3) && !metadataCollection.Metadata.Lookup((CaseInsensitiveString)SearchText).HasValue); var commandRefresher = this.WhenValueChanged(vm => vm.SearchText) .Subscribe(_ => ((Command) AddSearchCommand).Refresh()); //User feedback to guide them whilst typing SearchHint = this.WhenValueChanged(vm => vm.SearchText) .Select(text => { if (string.IsNullOrEmpty(text)) return "Type to highlight"; return text.Length < 3 ? "Enter at least 3 characters" : "Hit enter for more options"; }).ForBinding(); _cleanUp = new CompositeDisposable(commandRefresher, userOptions, SearchHint); }
public GlobalSearchOptions(ISearchMetadataCollection metadata, ISearchStateToMetadataMapper converter, ISetting <SearchState[]> searchStateSettings) { Metadata = metadata; var loader = searchStateSettings.Value .Take(1) .Select(items => items.Select(state => converter.Map(state, true))) .Subscribe(metadata.Add); var writer = metadata.Metadata.Connect() .ToCollection() .Select(metaData => metaData.ToArray()) .Throttle(TimeSpan.FromMilliseconds(250)) .Select(searchStateItems => searchStateItems.Select(converter.Map).ToArray()) .Subscribe(searchStateSettings.Write); _cleanUp = new CompositeDisposable(loader, writer); }
public GlobalSearchOptions(ISearchMetadataCollection metadata, ISearchStateToMetadataMapper converter, ISetting<SearchState[]> searchStateSettings) { Metadata = metadata; var loader = searchStateSettings.Value .Take(1) .Select(items => items.Select(state => converter.Map(state,true))) .Subscribe(metadata.Add); var writer = metadata.Metadata.Connect() .ToCollection() .Select(metaData => metaData.ToArray()) .Throttle(TimeSpan.FromMilliseconds(250)) .Select(searchStateItems => searchStateItems.Select(converter.Map).ToArray()) .Subscribe(searchStateSettings.Write); _cleanUp = new CompositeDisposable(loader, writer); }
public InlineViewerArgs([NotNull] IObservable <ILineProvider> lineProvider, [NotNull] IObservable <LineProxy> selectedChanged, [NotNull] ISearchMetadataCollection searchMetadataCollection) { if (lineProvider == null) { throw new ArgumentNullException(nameof(lineProvider)); } if (selectedChanged == null) { throw new ArgumentNullException(nameof(selectedChanged)); } if (searchMetadataCollection == null) { throw new ArgumentNullException(nameof(searchMetadataCollection)); } LineProvider = lineProvider; SelectedChanged = selectedChanged; SearchMetadataCollection = searchMetadataCollection; }
public SearchOptionsViewModel(ISearchMetadataCollection metadataCollection, ISchedulerProvider schedulerProvider, SearchHints searchHints) { SearchHints = searchHints; //TODO: options for colour var swatches = new SwatchesProvider().Swatches; ReadOnlyObservableCollection<SearchOptionsProxy> data; var userOptions = metadataCollection.Metadata.Connect() .WhereReasonsAre(ChangeReason.Add, ChangeReason.Remove) //ignore updates because we update from here .Transform(meta => new SearchOptionsProxy(meta, swatches,m => metadataCollection.Remove(m.SearchText))) .SubscribeMany(so => { //when a value changes, write the original value back to the cache return so.WhenAnyPropertyChanged() .Subscribe(_ => metadataCollection.Add(new SearchMetadata(so.Text, so.Filter, so.Highlight,so.UseRegex,so.IgnoreCase))); }) .Sort(SortExpressionComparer<SearchOptionsProxy>.Ascending(proxy=>proxy.Text)) .ObserveOn(schedulerProvider.MainThread) .Bind(out data) .Subscribe(); Data = data; //command to add the current search to the tail collection var searchInvoker = SearchHints.SearchRequested.Subscribe(request => { metadataCollection.Add(new SearchMetadata(request.Text, false, true, request.UseRegEx, true)); }); _cleanUp = new CompositeDisposable(searchInvoker, userOptions, searchInvoker); }
public SearchInfoCollection(ICombinedSearchMetadataCollection combinedSearchMetadataCollection, ISearchMetadataFactory searchMetadataFactory, IFileWatcher fileWatcher) { _localMetadataCollection = combinedSearchMetadataCollection.Local; _combinedSearchMetadataCollection = combinedSearchMetadataCollection; _searchMetadataFactory = searchMetadataFactory; _fileWatcher = fileWatcher; var exclusionPredicate = combinedSearchMetadataCollection.Combined.Connect() .IncludeUpdateWhen((current, previous) => !SearchMetadata.EffectsFilterComparer.Equals(current, previous)) .Filter(meta=> meta.IsExclusion) .ToCollection() .Select(searchMetadataItems => { Func<string, bool> predicate = null; if (searchMetadataItems.Count == 0) return predicate; var predicates = searchMetadataItems.Select(meta => meta.BuildPredicate()).ToArray(); predicate = str => { return !predicates.Any(item => item(str)); }; return predicate; }).StartWith((Func<string, bool>)null) .Replay(1).RefCount(); All = exclusionPredicate.Select(predicate => { if (predicate==null) return _fileWatcher.Latest.Index(); return _fileWatcher.Latest.Search(predicate); }).Switch().Replay(1).RefCount(); //create a collection with 1 item, which is used to show entire file var systemSearches = new SourceCache<SearchInfo, string>(t => t.SearchText); systemSearches.AddOrUpdate(new SearchInfo("<All>", false, All, SearchType.All)); //create a collection of all possible user filters var userSearches = combinedSearchMetadataCollection.Combined .Connect(meta => meta.Filter) .IgnoreUpdateWhen((current,previous)=> SearchMetadata.EffectsFilterComparer.Equals(current, previous)) .Transform(meta => { var latest = exclusionPredicate .Select(exclpredicate => { Func<string, bool> resultingPredicate; if (exclpredicate == null) { resultingPredicate = meta.BuildPredicate(); } else { var toMatch = meta.BuildPredicate(); resultingPredicate = str=> toMatch(str) && exclpredicate(str); } return _fileWatcher.Latest.Search(resultingPredicate); }) .Switch() .Replay(1).RefCount(); return new SearchInfo(meta.SearchText, meta.IsGlobal, latest, SearchType.User); }); //combine te results into a single collection Searches = systemSearches.Connect() .Or(userSearches) .AsObservableCache(); _cleanUp = new CompositeDisposable(Searches, systemSearches); }
public GlobalSearchInfoCollection(ISearchMetadataCollection searchMetadataCollection, ILogger logger) { _searchMetadataCollection = searchMetadataCollection; }
public SearchInfoCollection(ICombinedSearchMetadataCollection combinedSearchMetadataCollection, ISearchMetadataFactory searchMetadataFactory, IFileWatcher fileWatcher) { _localMetadataCollection = combinedSearchMetadataCollection.Local; _combinedSearchMetadataCollection = combinedSearchMetadataCollection; _searchMetadataFactory = searchMetadataFactory; _fileWatcher = fileWatcher; var exclusionPredicate = combinedSearchMetadataCollection.Combined.Connect() .IncludeUpdateWhen((current, previous) => !SearchMetadata.EffectsFilterComparer.Equals(current, previous)) .Filter(meta => meta.IsExclusion) .ToCollection() .Select(searchMetadataItems => { Func <string, bool> predicate = null; if (searchMetadataItems.Count == 0) { return(predicate); } var predicates = searchMetadataItems.Select(meta => meta.BuildPredicate()).ToArray(); predicate = str => { return(!predicates.Any(item => item(str))); }; return(predicate); }).StartWith((Func <string, bool>)null) .Replay(1).RefCount(); All = exclusionPredicate.Select(predicate => { if (predicate == null) { return(_fileWatcher.Latest.Index()); } return(_fileWatcher.Latest.Search(predicate)); }).Switch().Replay(1).RefCount(); //create a collection with 1 item, which is used to show entire file var systemSearches = new SourceCache <SearchInfo, string>(t => t.SearchText); systemSearches.AddOrUpdate(new SearchInfo("<All>", false, All, SearchType.All)); //create a collection of all possible user filters var userSearches = combinedSearchMetadataCollection.Combined .Connect(meta => meta.Filter) .IgnoreUpdateWhen((current, previous) => SearchMetadata.EffectsFilterComparer.Equals(current, previous)) .Transform(meta => { var latest = exclusionPredicate .Select(exclpredicate => { Func <string, bool> resultingPredicate; if (exclpredicate == null) { resultingPredicate = meta.BuildPredicate(); } else { var toMatch = meta.BuildPredicate(); resultingPredicate = str => toMatch(str) && exclpredicate(str); } return(_fileWatcher.Latest.Search(resultingPredicate)); }) .Switch() .Replay(1).RefCount(); return(new SearchInfo(meta.SearchText, meta.IsGlobal, latest, SearchType.User)); }); //combine the results into a single collection Searches = systemSearches.Connect() .Or(userSearches) .AsObservableCache(); _cleanUp = new CompositeDisposable(Searches, systemSearches); }
public TailViewModel([NotNull] ILogger logger, [NotNull] ISchedulerProvider schedulerProvider, [NotNull] IEnumerable <IFileWatcher> fileWatcher, [NotNull] ISelectionMonitor selectionMonitor, [NotNull] IClipboardHandler clipboardHandler, [NotNull] ISearchInfoCollection searchInfoCollection, [NotNull] IInlineViewerFactory inlineViewerFactory, [NotNull] ISetting <GeneralOptions> generalOptions, [NotNull] ISearchMetadataCollection searchMetadataCollection, [NotNull] SearchOptionsViewModel searchOptionsViewModel, [NotNull] SearchHints searchHints) { if (logger == null) { throw new ArgumentNullException(nameof(logger)); } if (schedulerProvider == null) { throw new ArgumentNullException(nameof(schedulerProvider)); } if (fileWatcher == null) { throw new ArgumentNullException(nameof(fileWatcher)); } if (selectionMonitor == null) { throw new ArgumentNullException(nameof(selectionMonitor)); } if (clipboardHandler == null) { throw new ArgumentNullException(nameof(clipboardHandler)); } if (searchInfoCollection == null) { throw new ArgumentNullException(nameof(searchInfoCollection)); } if (inlineViewerFactory == null) { throw new ArgumentNullException(nameof(inlineViewerFactory)); } if (generalOptions == null) { throw new ArgumentNullException(nameof(generalOptions)); } if (searchMetadataCollection == null) { throw new ArgumentNullException(nameof(searchMetadataCollection)); } if (searchOptionsViewModel == null) { throw new ArgumentNullException(nameof(searchOptionsViewModel)); } if (searchHints == null) { throw new ArgumentNullException(nameof(searchHints)); } _stateProvider = new TailViewPersister(this); var enumerable = fileWatcher as IFileWatcher[] ?? fileWatcher.ToArray(); Names = enumerable.Select(t => t.FullName); SelectionMonitor = selectionMonitor; SearchOptions = searchOptionsViewModel; SearchHints = searchHints; SearchCollection = new SearchCollection(searchInfoCollection, schedulerProvider); CopyToClipboardCommand = new Command(() => clipboardHandler.WriteToClipboard(selectionMonitor.GetSelectedText())); OpenFileCommand = new Command(() => Process.Start(enumerable[0].FullName)); OpenFolderCommand = new Command(() => Process.Start(enumerable[0].Folder)); UsingDarkTheme = generalOptions.Value .ObserveOn(schedulerProvider.MainThread) .Select(options => options.Theme == Theme.Dark) .ForBinding(); HighlightTail = generalOptions.Value .ObserveOn(schedulerProvider.MainThread) .Select(options => options.HighlightTail) .ForBinding(); HighlightDuration = generalOptions.Value .ObserveOn(schedulerProvider.MainThread) .Select(options => new Duration(TimeSpan.FromSeconds(options.HighlightDuration))) .ForBinding(); //An observable which acts as a scroll command var autoChanged = this.WhenValueChanged(vm => vm.AutoTail); var scroller = _userScrollRequested.CombineLatest(autoChanged, (user, auto) => { var mode = AutoTail ? ScrollReason.Tail : ScrollReason.User; return(new ScrollRequest(mode, user.PageSize, user.FirstIndex)); }) .Do(x => logger.Info("Scrolling to {0}/{1}", x.FirstIndex, x.PageSize)) .DistinctUntilChanged(); FileStatus = enumerable .Select(t => t.Status) .Merge() .Scan(default(FileStatus), (status, fileStatus) => status | fileStatus) .ForBinding(); //command to add the current search to the tail collection var searchInvoker = SearchHints.SearchRequested.Subscribe( request => { searchInfoCollection.Add(request.Text, request.UseRegEx); }); //User feedback to show file size FileSizeText = enumerable .Select(t => t.Latest) .Merge() .Select(t => t.Size) .Scan(0f, (previousSize, currentSize) => previousSize + currentSize / 2f) .Select(t => ((long)t).FormatWithAbbreviation()) .DistinctUntilChanged() .ForBinding(); //tailer is the main object used to tail, scroll and filter in a file var lineScroller = new LineScroller(SearchCollection.Latest.ObserveOn(schedulerProvider.Background), scroller); //load lines into observable collection var lineProxyFactory = new LineProxyFactory(new TextFormatter(searchMetadataCollection), new LineMatches(searchMetadataCollection)); var loader = lineScroller.Lines.Connect() .RecordChanges(logger, "Received") .Transform(lineProxyFactory.Create, new ParallelisationOptions(ParallelType.Ordered, 3)) .Sort(SortExpressionComparer <LineProxy> .Ascending(proxy => proxy)) .ObserveOn(schedulerProvider.MainThread) .Bind(out _data, 100) .DisposeMany() .LogErrors(logger) .Subscribe(); //monitor matching lines and start index, Count = searchInfoCollection.All .GroupBy(t => t) .Select(groupedProvider => groupedProvider.Key.Count) .Scan(0, (i, providerCount) => i + providerCount) .ForBinding(); CountText = searchInfoCollection.All .GroupBy(t => t) .Select(groupedProvider => groupedProvider.Key.Count) .Scan(0, (i, providerCount) => i + providerCount) .Select(latestCount => $"{latestCount.ToString("##,###")} lines") .ForBinding(); //iterate over every items to evaluate the lines' count LatestCount = SearchCollection.Latest .GroupBy(t => t) .Do(Console.WriteLine) .Scan(0, (acc, provider) => { if (provider.Key is IndexCollection && provider.Key.NumberOfPreviousProvider == 0) { acc = 0; } else if (provider.Key is FileSearchResult && provider.Key.NumberOfPreviousProvider == 0) { acc = 0; } return(provider.Key.Count + acc); }) .ForBinding(); ////track first visible index var firstIndexMonitor = lineScroller.Lines.Connect() .Buffer(TimeSpan.FromMilliseconds(25)).FlattenBufferResult() .ToCollection() .Select(lines => lines.Count == 0 ? 0 : lines.Select(l => l.Index).Max() - lines.Count + 1) .ObserveOn(schedulerProvider.MainThread) .Subscribe(first => { FirstIndex = first; }); //Create objects required for inline viewing var isUserDefinedChanged = SearchCollection.WhenValueChanged(sc => sc.Selected) .Where(selected => selected != null) .Select(selected => selected.IsUserDefined) .DistinctUntilChanged() .Replay(1) .RefCount(); var inlineViewerVisible = isUserDefinedChanged.CombineLatest(this.WhenValueChanged(vm => vm.ShowInline), (userDefined, showInline) => userDefined && showInline); CanViewInline = isUserDefinedChanged.ForBinding(); InlineViewerVisible = inlineViewerVisible.ForBinding(); //return an empty line provider unless user is viewing inline - this saves needless trips to the file var inline = searchInfoCollection.All.CombineLatest(inlineViewerVisible, (index, ud) => ud ? index : new EmptyLineProvider()); var firstVisibleRow = _data.ToObservableChangeSet().ToCollection() .Select(collection => collection.FirstOrDefault()); //var itemToSelect = this.WhenValueChanged(vm => vm.SelectedItem) // .CombineLatest(firstVisibleRow, (selected, first) => selected ?? first); //// InlineViewer = inlineViewerFactory.Create(inline, this.WhenValueChanged(vm => vm.SelectedItem), lineProxyFactory); _cleanUp = new CompositeDisposable(lineScroller, loader, firstIndexMonitor, FileStatus, Count, LatestCount, FileSizeText, CanViewInline, InlineViewer, InlineViewerVisible, SearchCollection, searchInfoCollection, HighlightTail, UsingDarkTheme, searchHints, searchMetadataCollection, searchMetadataCollection, SelectionMonitor, SearchOptions, searchInvoker, _userScrollRequested.SetAsComplete()); }
public SearchOptionsViewModel(ISearchMetadataCollection metadataCollection, ISearchMetadataFactory searchMetadataFactory, ISchedulerProvider schedulerProvider, IColourProvider colourProvider, IIconProvider iconsProvider, SearchHints searchHints) { SearchHints = searchHints; var proxyItems = metadataCollection.Metadata.Connect() .WhereReasonsAre(ChangeReason.Add, ChangeReason.Remove) //ignore updates because we update from here .Transform(meta => { return(new SearchOptionsProxy(meta, colourProvider, new IconSelector(iconsProvider, schedulerProvider), m => metadataCollection.Remove(m.SearchText), iconsProvider.DefaultIconSelector, Id)); }) .SubscribeMany(so => { //when a value changes, write the original value back to the cache return(so.WhenAnyPropertyChanged() .Select(_ => (SearchMetadata)so) .Subscribe(metadataCollection.AddorUpdate)); }) .AsObservableCache(); var monitor = MonitorPositionalChanges() .Subscribe(positionChangedArgs => { positionChangedArgs.ForEach(metadataCollection.AddorUpdate); }); //load data onto grid var collection = new ObservableCollectionExtended <SearchOptionsProxy>(); var userOptions = proxyItems.Connect() .Sort(SortExpressionComparer <SearchOptionsProxy> .Ascending(proxy => proxy.Position)) .ObserveOn(schedulerProvider.MainThread) //force reset for each new or removed item dues to a bug in the underlying dragablz control which inserts in an incorrect position .Bind(collection, new ObservableCollectionAdaptor <SearchOptionsProxy, string>(0)) .DisposeMany() .Subscribe(); Data = new ReadOnlyObservableCollection <SearchOptionsProxy>(collection); //command to add the current search to the tail collection var searchInvoker = SearchHints.SearchRequested.Subscribe(request => { schedulerProvider.Background.Schedule(() => { var meta = searchMetadataFactory.Create(request.Text, request.UseRegEx, metadataCollection.NextIndex(), false); metadataCollection.AddorUpdate(meta); }); }); _cleanUp = new CompositeDisposable(searchInvoker, userOptions, searchInvoker, monitor, SearchHints); }
public SearchProxyCollection(ISearchMetadataCollection metadataCollection, Guid id, Action<SearchMetadata> changeScopeAction, ISchedulerProvider schedulerProvider, IColourProvider colourProvider, IIconProvider iconsProvider, ITextAssociationCollection textAssociationCollection, IThemeProvider themeProvider) { var proxyItems = metadataCollection.Metadata.Connect() .WhereReasonsAre(ChangeReason.Add, ChangeReason.Remove) //ignore updates because we update from here .Transform(meta => { return new SearchOptionsProxy(meta, changeScopeAction, colourProvider, themeProvider, new IconSelector(iconsProvider, schedulerProvider), m => metadataCollection.Remove(m.SearchText), iconsProvider.DefaultIconSelector, id); }) .SubscribeMany(so => { //when a value changes, write the original value back to the metadata collection var anyPropertyHasChanged = so.WhenAnyPropertyChanged() .Select(_ => (SearchMetadata) so) .Subscribe(metadataCollection.AddorUpdate); //when an icon or colour has changed we need to record user choice so //the same choice can be used again var iconChanged = so.WhenValueChanged(proxy => proxy.IconKind, false).ToUnit(); var colourChanged = so.WhenValueChanged(proxy => proxy.HighlightHue, false).ToUnit(); var ignoreCaseChanged = so.WhenValueChanged(proxy => proxy.CaseSensitive, false).ToUnit(); var textAssociationChanged = iconChanged.Merge(colourChanged).Merge(ignoreCaseChanged) .Throttle(TimeSpan.FromMilliseconds(250)) .Select(_ => new TextAssociation(so.Text, so.CaseSensitive, so.UseRegex, so.HighlightHue.Swatch, so.IconKind.ToString(), so.HighlightHue.Name, DateTime.UtcNow)) .Subscribe(textAssociationCollection.MarkAsChanged); return new CompositeDisposable(anyPropertyHasChanged, textAssociationChanged); }) .AsObservableCache(); Count = proxyItems.CountChanged.StartWith(0).ForBinding(); var monitor = MonitorPositionalChanges().Subscribe(metadataCollection.Add); //load data onto grid var collection = new ObservableCollectionExtended<SearchOptionsProxy>(); var includedLoader = proxyItems .Connect(proxy => !proxy.IsExclusion) .Sort(SortExpressionComparer<SearchOptionsProxy>.Ascending(proxy => proxy.Position)) .ObserveOn(schedulerProvider.MainThread) //force reset for each new or removed item dues to a bug in the underlying dragablz control which inserts in an incorrect position .Bind(collection, new ObservableCollectionAdaptor<SearchOptionsProxy, string>(0)) .DisposeMany() .Subscribe(); ReadOnlyObservableCollection<SearchOptionsProxy> excluded; var excludedLoader = proxyItems .Connect(proxy => proxy.IsExclusion) .Sort(SortExpressionComparer<SearchOptionsProxy>.Ascending(proxy => proxy.Text)) .ObserveOn(schedulerProvider.MainThread) //force reset for each new or removed item dues to a bug in the underlying dragablz control which inserts in an incorrect position .Bind(out excluded) .DisposeMany() .Subscribe(); Excluded = excluded; Included = new ReadOnlyObservableCollection<SearchOptionsProxy>(collection); _cleanUp = new CompositeDisposable(proxyItems, includedLoader, excludedLoader, monitor); }
public SearchProxyCollection(ISearchMetadataCollection metadataCollection, Guid id, Action<SearchMetadata> changeScopeAction, ISchedulerProvider schedulerProvider, IColourProvider colourProvider, IIconProvider iconsProvider, ITextAssociationCollection textAssociationCollection, IThemeProvider themeProvider) { var proxyItems = metadataCollection.Metadata.Connect() .WhereReasonsAre(ChangeReason.Add, ChangeReason.Remove) //ignore updates because we update from here .Transform(meta => { return new SearchOptionsProxy(meta, changeScopeAction, colourProvider, themeProvider, new IconSelector(iconsProvider, schedulerProvider), m => metadataCollection.Remove(m.SearchText), iconsProvider.DefaultIconSelector, id); }) .SubscribeMany(so => { //when a value changes, write the original value back to the metadata collection var anyPropertyHasChanged = so.WhenAnyPropertyChanged() .Select(_ => (SearchMetadata) so) .Subscribe(metadataCollection.AddorUpdate); //when an icon or colour has changed we need to record user choice so //the same choice can be used again var iconChanged = so.WhenValueChanged(proxy => proxy.IconKind, false).ToUnit(); var colourChanged = so.WhenValueChanged(proxy => proxy.HighlightHue, false).ToUnit(); var ignoreCaseChanged = so.WhenValueChanged(proxy => proxy.CaseSensitive, false).ToUnit(); var textAssociationChanged = iconChanged.Merge(colourChanged).Merge(ignoreCaseChanged) .Throttle(TimeSpan.FromMilliseconds(250)) .Select(_ => new TextAssociation(so.Text, so.CaseSensitive, so.UseRegex, so.HighlightHue.Swatch, so.IconKind.ToString(), so.HighlightHue.Name, DateTime.UtcNow)) .Subscribe(textAssociationCollection.MarkAsChanged); return new CompositeDisposable(anyPropertyHasChanged, textAssociationChanged); }) .AsObservableCache(); Count = proxyItems.CountChanged.StartWith(0).ForBinding(); var monitor = MonitorPositionalChanges().Subscribe(metadataCollection.Add); //load data onto grid var collection = new ObservableCollectionExtended<SearchOptionsProxy>(); var includedLoader = proxyItems .Connect(proxy => !proxy.IsExclusion) .Sort(SortExpressionComparer<SearchOptionsProxy>.Ascending(proxy => proxy.Position)) .ObserveOn(schedulerProvider.MainThread) //force reset for each new or removed item dues to a bug in the underlying dragablz control which inserts in an incorrect position .Bind(collection, new ObservableCollectionAdaptor<SearchOptionsProxy, string>(0)) .DisposeMany() .Subscribe(); var excludedLoader = proxyItems .Connect(proxy => proxy.IsExclusion) .Sort(SortExpressionComparer<SearchOptionsProxy>.Ascending(proxy => proxy.Text)) .ObserveOn(schedulerProvider.MainThread) //force reset for each new or removed item dues to a bug in the underlying dragablz control which inserts in an incorrect position .Bind(out var excluded) .DisposeMany() .Subscribe(); Excluded = excluded; Included = new ReadOnlyObservableCollection<SearchOptionsProxy>(collection); _cleanUp = new CompositeDisposable(proxyItems, includedLoader, excludedLoader, monitor); }
public SearchOptionsViewModel(ISearchMetadataCollection metadataCollection, ISearchMetadataFactory searchMetadataFactory, ISchedulerProvider schedulerProvider, IColourProvider colourProvider, IIconProvider iconsProvider, ITextAssociationCollection textAssociationCollection, SearchHints searchHints, IThemeProvider themeProvider) { SearchHints = searchHints; var proxyItems = metadataCollection.Metadata.Connect() .WhereReasonsAre(ChangeReason.Add, ChangeReason.Remove) //ignore updates because we update from here .Transform(meta => { return new SearchOptionsProxy(meta, colourProvider, themeProvider, new IconSelector(iconsProvider, schedulerProvider), m => metadataCollection.Remove(m.SearchText), iconsProvider.DefaultIconSelector, Id); }) .SubscribeMany(so => { //when a value changes, write the original value back to the metadata collection var anyPropertyHasChanged = so.WhenAnyPropertyChanged() .Select(_ => (SearchMetadata) so) .Subscribe(metadataCollection.AddorUpdate); //when an icon or colour has changed we need to record user choice so //the same choice can be used again var iconChanged = so.WhenValueChanged(proxy => proxy.IconKind,false).ToUnit(); var colourChanged = so.WhenValueChanged(proxy => proxy.HighlightHue, false).ToUnit(); var ignoreCaseChanged = so.WhenValueChanged(proxy => proxy.IgnoreCase, false).ToUnit(); var textAssociationChanged = iconChanged.Merge(colourChanged).Merge(ignoreCaseChanged) .Throttle(TimeSpan.FromMilliseconds(250)) .Select(_=> new TextAssociation(so.Text, so.IgnoreCase, so.UseRegex, so.HighlightHue.Swatch, so.IconKind.ToString(), so.HighlightHue.Name, DateTime.Now)) .Subscribe(textAssociationCollection.MarkAsChanged); return new CompositeDisposable(anyPropertyHasChanged, textAssociationChanged); }) .AsObservableCache(); var monitor = MonitorPositionalChanges() .Subscribe(metadataCollection.Add); //load data onto grid var collection = new ObservableCollectionExtended<SearchOptionsProxy>(); var userOptions = proxyItems.Connect() .Sort(SortExpressionComparer<SearchOptionsProxy>.Ascending(proxy => proxy.Position)) .ObserveOn(schedulerProvider.MainThread) //force reset for each new or removed item dues to a bug in the underlying dragablz control which inserts in an incorrect position .Bind(collection, new ObservableCollectionAdaptor<SearchOptionsProxy, string>(0)) .DisposeMany() .Subscribe(); Data = new ReadOnlyObservableCollection<SearchOptionsProxy>(collection); //command to add the current search to the tail collection var searchInvoker = SearchHints.SearchRequested .ObserveOn(schedulerProvider.Background) .Subscribe(request => { var meta = searchMetadataFactory.Create(request.Text, request.UseRegEx, metadataCollection.NextIndex(), false); metadataCollection.AddorUpdate(meta); }); _cleanUp = new CompositeDisposable(searchInvoker, userOptions, searchInvoker, monitor, SearchHints); }
public TailViewModel([NotNull] ILogger logger, [NotNull] ISchedulerProvider schedulerProvider, [NotNull] IFileWatcher fileWatcher, [NotNull] ISelectionMonitor selectionMonitor, [NotNull] IClipboardHandler clipboardHandler, [NotNull] ISearchInfoCollection searchInfoCollection, [NotNull] IInlineViewerFactory inlineViewerFactory, [NotNull] ISetting <GeneralOptions> generalOptions, [NotNull] ISearchMetadataCollection searchMetadataCollection, [NotNull] IStateBucketService stateBucketService, [NotNull] SearchOptionsViewModel searchOptionsViewModel, [NotNull] ITailViewStateRestorer restorer, [NotNull] SearchHints searchHints, [NotNull] ITailViewStateControllerFactory tailViewStateControllerFactory) { if (logger == null) { throw new ArgumentNullException(nameof(logger)); } if (schedulerProvider == null) { throw new ArgumentNullException(nameof(schedulerProvider)); } if (fileWatcher == null) { throw new ArgumentNullException(nameof(fileWatcher)); } if (selectionMonitor == null) { throw new ArgumentNullException(nameof(selectionMonitor)); } if (clipboardHandler == null) { throw new ArgumentNullException(nameof(clipboardHandler)); } if (searchInfoCollection == null) { throw new ArgumentNullException(nameof(searchInfoCollection)); } if (inlineViewerFactory == null) { throw new ArgumentNullException(nameof(inlineViewerFactory)); } if (generalOptions == null) { throw new ArgumentNullException(nameof(generalOptions)); } if (searchMetadataCollection == null) { throw new ArgumentNullException(nameof(searchMetadataCollection)); } if (stateBucketService == null) { throw new ArgumentNullException(nameof(stateBucketService)); } if (searchOptionsViewModel == null) { throw new ArgumentNullException(nameof(searchOptionsViewModel)); } if (searchHints == null) { throw new ArgumentNullException(nameof(searchHints)); } Name = fileWatcher.FullName; SelectionMonitor = selectionMonitor; SearchOptions = searchOptionsViewModel; SearchHints = searchHints; SearchCollection = new SearchCollection(searchInfoCollection, schedulerProvider); CopyToClipboardCommand = new Command(() => clipboardHandler.WriteToClipboard(selectionMonitor.GetSelectedText())); OpenFileCommand = new Command(() => Process.Start(fileWatcher.FullName)); OpenFolderCommand = new Command(() => Process.Start(fileWatcher.Folder)); SearchMetadataCollection = searchMetadataCollection; var horizonalScrollArgs = new ReplaySubject <TextScrollInfo>(1); HorizonalScrollChanged = args => { horizonalScrollArgs.OnNext(args); }; _tailViewStateControllerFactory = tailViewStateControllerFactory; //Move these 2 highlight fields to a service as all views require them UsingDarkTheme = generalOptions.Value .ObserveOn(schedulerProvider.MainThread) .Select(options => options.Theme == Theme.Dark) .ForBinding(); HighlightTail = generalOptions.Value .ObserveOn(schedulerProvider.MainThread) .Select(options => options.HighlightTail) .ForBinding(); //this deals with state when loading the system at start up and at shut-down _persister = new TailViewPersister(this, restorer); //An observable which acts as a scroll command var autoChanged = this.WhenValueChanged(vm => vm.AutoTail); var scroller = _userScrollRequested.CombineLatest(autoChanged, (user, auto) => { var mode = AutoTail ? ScrollReason.Tail : ScrollReason.User; return(new ScrollRequest(mode, user.PageSize, user.FirstIndex)); }) .Do(x => logger.Info("Scrolling to {0}/{1}", x.FirstIndex, x.PageSize)) .DistinctUntilChanged(); FileStatus = fileWatcher.Status.ForBinding(); //command to add the current search to the tail collection var searchInvoker = SearchHints.SearchRequested.Subscribe(request => { searchInfoCollection.Add(request.Text, request.UseRegEx); }); //User feedback to show file size FileSizeText = fileWatcher.Latest.Select(fn => fn.Size) .Select(size => size.FormatWithAbbreviation()) .DistinctUntilChanged() .ForBinding(); //tailer is the main object used to tail, scroll and filter in a file var lineScroller = new LineScroller(SearchCollection.Latest.ObserveOn(schedulerProvider.Background), scroller); MaximumChars = lineScroller.MaximumLines() .ObserveOn(schedulerProvider.MainThread) .ForBinding(); //load lines into observable collection var lineProxyFactory = new LineProxyFactory(new TextFormatter(searchMetadataCollection), new LineMatches(searchMetadataCollection), horizonalScrollArgs.DistinctUntilChanged()); var loader = lineScroller.Lines.Connect() .LogChanges(logger, "Received") .Transform(lineProxyFactory.Create, new ParallelisationOptions(ParallelType.Ordered, 3)) .LogChanges(logger, "Sorting") .Sort(SortExpressionComparer <LineProxy> .Ascending(proxy => proxy)) .ObserveOn(schedulerProvider.MainThread) .Bind(out _data, 100) .LogChanges(logger, "Bound") .DisposeMany() .LogErrors(logger) .Subscribe(); //monitor matching lines and start index, Count = searchInfoCollection.All.Select(latest => latest.Count).ForBinding(); CountText = searchInfoCollection.All.Select(latest => $"{latest.Count.ToString("##,###")} lines").ForBinding(); LatestCount = SearchCollection.Latest.Select(latest => latest.Count).ForBinding(); ////track first visible index var firstIndexMonitor = lineScroller.Lines.Connect() .Buffer(TimeSpan.FromMilliseconds(25)).FlattenBufferResult() .ToCollection() .Select(lines => lines.Count == 0 ? 0 : lines.Select(l => l.Index).Max() - lines.Count + 1) .ObserveOn(schedulerProvider.MainThread) .Subscribe(first => { FirstIndex = first; }); //Create objects required for inline viewing var isUserDefinedChanged = SearchCollection.WhenValueChanged(sc => sc.Selected) .Where(selected => selected != null) .Select(selected => selected.IsUserDefined) .DistinctUntilChanged() .Replay(1) .RefCount(); var inlineViewerVisible = isUserDefinedChanged.CombineLatest(this.WhenValueChanged(vm => vm.ShowInline), (userDefined, showInline) => userDefined && showInline); CanViewInline = isUserDefinedChanged.ForBinding(); InlineViewerVisible = inlineViewerVisible.ForBinding(); //return an empty line provider unless user is viewing inline - this saves needless trips to the file var inline = searchInfoCollection.All.CombineLatest(inlineViewerVisible, (index, ud) => ud ? index : new EmptyLineProvider()); InlineViewer = inlineViewerFactory.Create(inline, this.WhenValueChanged(vm => vm.SelectedItem), searchMetadataCollection); _cleanUp = new CompositeDisposable(lineScroller, loader, firstIndexMonitor, FileStatus, Count, LatestCount, FileSizeText, CanViewInline, InlineViewer, InlineViewerVisible, SearchCollection, searchInfoCollection, HighlightTail, UsingDarkTheme, searchHints, searchMetadataCollection, SelectionMonitor, SearchOptions, searchInvoker, MaximumChars, _stateMonitor, horizonalScrollArgs.SetAsComplete(), _userScrollRequested.SetAsComplete()); }
public SearchOptionsViewModel(ISearchMetadataCollection metadataCollection, ISchedulerProvider schedulerProvider, SearchHints searchHints) { SearchHints = searchHints; //TODO: options for colour var swatches = new SwatchesProvider().Swatches; bool binding = false; var orderChanged = Observable.FromEventPattern <OrderChangedEventArgs>( h => PositionMonitor.OrderChanged += h, h => PositionMonitor.OrderChanged -= h) .Throttle(TimeSpan.FromMilliseconds(125)) .Select(evt => evt.EventArgs) .Where(args => args.PreviousOrder != null && args.NewOrder.Length == args.PreviousOrder.Length) .Select(positionChangedArgs => { //reprioritise filters and highlights return(positionChangedArgs.NewOrder .OfType <SearchOptionsProxy>() .Select((item, index) => new { Meta = (SearchMetadata)item, index }) //.Where(x => x.index != x.Meta.Position) .Select(x => new SearchMetadata(x.Meta, x.index)) .ToArray()); }) .Subscribe(positionChangedArgs => { positionChangedArgs.ForEach(metadataCollection.AddorUpdate); }); ReadOnlyObservableCollection <SearchOptionsProxy> data; var userOptions = metadataCollection.Metadata.Connect() .WhereReasonsAre(ChangeReason.Add, ChangeReason.Remove) //ignore updates because we update from here .Transform(meta => new SearchOptionsProxy(meta, swatches, m => metadataCollection.Remove(m.SearchText))) .SubscribeMany(so => { //when a value changes, write the original value back to the cache return(so.WhenAnyPropertyChanged() .Select(_ => new SearchMetadata(so.Position, so.Text, so.Filter, so.Highlight, so.UseRegex, so.IgnoreCase)) .Subscribe(metadataCollection.AddorUpdate)); }) .Sort(SortExpressionComparer <SearchOptionsProxy> .Ascending(proxy => proxy.Position)) .ObserveOn(schedulerProvider.MainThread) .Bind(out data) .Subscribe(); Data = data; //command to add the current search to the tail collection var searchInvoker = SearchHints.SearchRequested.Subscribe(request => { schedulerProvider.Background.Schedule(() => { metadataCollection.AddorUpdate(new SearchMetadata(metadataCollection.NextIndex(), request.Text, false, true, request.UseRegEx, true)); }); }); _cleanUp = new CompositeDisposable(searchInvoker, userOptions, searchInvoker, orderChanged); }
public SearchOptionsViewModel(ISearchMetadataCollection metadataCollection, ISchedulerProvider schedulerProvider, SearchHints searchHints) { SearchHints = searchHints; //TODO: options for colour var swatches = new SwatchesProvider().Swatches; bool binding = false; var orderChanged = Observable.FromEventPattern<OrderChangedEventArgs>( h => PositionMonitor.OrderChanged += h, h => PositionMonitor.OrderChanged -= h) .Select(evt => evt.EventArgs) .Where(args=>args.PreviousOrder!=null && args.NewOrder.Length == args.PreviousOrder.Length) .Select(positionChangedArgs => { //reprioritise filters and highlights return positionChangedArgs.NewOrder .OfType<SearchOptionsProxy>() .Select((item, index) => new {Meta=(SearchMetadata)item, index}) //.Where(x => x.index != x.Meta.Position) .Select(x => new SearchMetadata(x.Meta, x.index)) .ToArray(); }) .Subscribe(positionChangedArgs => { positionChangedArgs.ForEach(metadataCollection.AddorUpdate); }); ReadOnlyObservableCollection<SearchOptionsProxy> data; var userOptions = metadataCollection.Metadata.Connect() .WhereReasonsAre(ChangeReason.Add, ChangeReason.Remove) //ignore updates because we update from here .Transform(meta => new SearchOptionsProxy(meta, swatches, m => metadataCollection.Remove(m.SearchText))) .SubscribeMany(so => { //when a value changes, write the original value back to the cache return so.WhenAnyPropertyChanged() .Subscribe(_ =>metadataCollection.AddorUpdate(new SearchMetadata(metadataCollection.Metadata.Count, so.Text, so.Filter, so.Highlight, so.UseRegex, so.IgnoreCase))); }) .Sort(SortExpressionComparer<SearchOptionsProxy>.Ascending(proxy => proxy.Position)) .ObserveOn(schedulerProvider.MainThread) .Bind(out data) .Subscribe(); Data = data; //command to add the current search to the tail collection var searchInvoker = SearchHints.SearchRequested.Subscribe(request => { schedulerProvider.Background.Schedule(() => { metadataCollection.AddorUpdate(new SearchMetadata(metadataCollection.NextIndex(), request.Text, false, true, request.UseRegEx, true)); }); }); _cleanUp = new CompositeDisposable(searchInvoker, userOptions, searchInvoker, orderChanged); }
public SearchOptionsViewModel(ISearchMetadataCollection metadataCollection, ISearchMetadataFactory searchMetadataFactory, ISchedulerProvider schedulerProvider, IColourProvider colourProvider, IIconProvider iconsProvider, ITextAssociationCollection textAssociationCollection, SearchHints searchHints) { SearchHints = searchHints; var proxyItems = metadataCollection.Metadata.Connect() .WhereReasonsAre(ChangeReason.Add, ChangeReason.Remove) //ignore updates because we update from here .Transform(meta => { return(new SearchOptionsProxy(meta, colourProvider, new IconSelector(iconsProvider, schedulerProvider), m => metadataCollection.Remove(m.SearchText), iconsProvider.DefaultIconSelector, Id)); }) .SubscribeMany(so => { //when a value changes, write the original value back to the metadata collection var anyPropertyHasChanged = so.WhenAnyPropertyChanged() .Select(_ => (SearchMetadata)so) .Subscribe(metadataCollection.AddorUpdate); //when an icon or colour has changed we need to record user choice so //the same choice can be used again var iconChanged = so.WhenValueChanged(proxy => proxy.IconKind, false).ToUnit(); var colourChanged = so.WhenValueChanged(proxy => proxy.HighlightHue, false).ToUnit(); var ignoreCaseChanged = so.WhenValueChanged(proxy => proxy.IgnoreCase, false).ToUnit(); var textAssociationChanged = iconChanged.Merge(colourChanged).Merge(ignoreCaseChanged) .Throttle(TimeSpan.FromMilliseconds(250)) .Select(_ => new TextAssociation(so.Text, so.IgnoreCase, so.UseRegex, so.HighlightHue.Swatch, so.IconKind.ToString(), so.HighlightHue.Name, DateTime.Now)) .Subscribe(textAssociationCollection.MarkAsChanged); return(new CompositeDisposable(anyPropertyHasChanged, textAssociationChanged)); }) .AsObservableCache(); var monitor = MonitorPositionalChanges() .Subscribe(metadataCollection.Add); //load data onto grid var collection = new ObservableCollectionExtended <SearchOptionsProxy>(); var userOptions = proxyItems.Connect() .Sort(SortExpressionComparer <SearchOptionsProxy> .Ascending(proxy => proxy.Position)) .ObserveOn(schedulerProvider.MainThread) //force reset for each new or removed item dues to a bug in the underlying dragablz control which inserts in an incorrect position .Bind(collection, new ObservableCollectionAdaptor <SearchOptionsProxy, string>(0)) .DisposeMany() .Subscribe(); Data = new ReadOnlyObservableCollection <SearchOptionsProxy>(collection); //command to add the current search to the tail collection var searchInvoker = SearchHints.SearchRequested .ObserveOn(schedulerProvider.Background) .Subscribe(request => { var meta = searchMetadataFactory.Create(request.Text, request.UseRegEx, metadataCollection.NextIndex(), false); metadataCollection.AddorUpdate(meta); }); _cleanUp = new CompositeDisposable(searchInvoker, userOptions, searchInvoker, monitor, SearchHints); }