From 9d1660270272180cd71147d6ddd53d1382643d2e Mon Sep 17 00:00:00 2001
From: wenxiangchao <wenxiangchao@ihengxin.com.cn>
Date: 星期一, 02 十一月 2026 18:00:14 +0800
Subject: [PATCH] merge: 并入远程 Initial commit 作为历史根节点

---
 Plugin.SeqEditor/ViewModel/SeqEditorViewModel.cs |  715 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 715 insertions(+), 0 deletions(-)

diff --git a/Plugin.SeqEditor/ViewModel/SeqEditorViewModel.cs b/Plugin.SeqEditor/ViewModel/SeqEditorViewModel.cs
new file mode 100644
index 0000000..fe22156
--- /dev/null
+++ b/Plugin.SeqEditor/ViewModel/SeqEditorViewModel.cs
@@ -0,0 +1,715 @@
+锘縰sing CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using GongSolutions.Wpf.DragDrop;
+using HMI.Core.Util;
+using HMI.UI;
+using HMI.UI.Util;
+using log4net;
+using Microsoft.Win32;
+using NationalInstruments.TestStand.Interop.API;
+using Plugin.SeqEditor.Constant;
+using Plugin.SeqEditor.Entity;
+using Plugin.SeqEditor.Util;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Input;
+using System.Windows.Media;
+
+namespace Plugin.SeqEditor.ViewModel
+{
+
+    public partial class SeqEditorViewModel : ObservableObject
+    {
+        private ObservableCollection<StepUI> setupSteps = new ObservableCollection<StepUI>();
+
+        private ObservableCollection<StepUI> mainSteps = new ObservableCollection<StepUI>();
+
+        private ObservableCollection<StepUI> cleanupSteps = new ObservableCollection<StepUI>();
+
+        [ObservableProperty]
+        private ObservableCollection<StepUI> allSteps = new ObservableCollection<StepUI>();
+
+        [ObservableProperty]
+        private ObservableCollection<SeqUI> sequences = new ObservableCollection<SeqUI>();
+
+        [ObservableProperty]
+        private SeqUI currentSeqUI;
+
+        [ObservableProperty]
+        private StepUI currentStepUI;
+
+        [ObservableProperty]
+        private string currentFileName;
+
+        [ObservableProperty]
+        private string currentSeqTitle = LangUtil.Get("se.steps");
+
+        [ObservableProperty]
+        private string targetType = null;
+
+        [ObservableProperty]
+        private string currentFilePath;
+
+        [ObservableProperty]
+        private bool canDrop = true;
+
+        public IDropTarget StepDropHandler { get; set; }
+
+        private Engine engine;
+        private SequenceFile sequenceFile;
+
+        public Dictionary<string, SequenceFile> cacheFiles = new Dictionary<string, SequenceFile>();
+
+        private bool isLoaded;
+        private ILog log;
+        public SeqEditor control { get; private set; }
+
+        public List<ItemData> Types = FilterTypeData.ItemSource;
+
+        private HashSet<string> edits = new HashSet<string>();
+        public Dictionary<Sequence, ObservableCollection<StepUI>> sequenceCache { get; private set; } = new();
+
+        private HashSet<string> indexType = new HashSet<string> {
+            StepTypeStr.PassFailTest,
+            StepTypeStr.NumericLimitTest,
+            StepTypeStr.NI_MultipleNumericLimitTest,
+            StepTypeStr.StringValueTest,
+            StepTypeStr.NI_Wait,
+            StepTypeStr.Action
+    };
+
+        public SeqEditorViewModel(SeqEditor control, ILog log)
+        {
+            this.log = log;
+            this.control = control;
+            engine = new Engine();
+            engine.LoadTypePaletteFilesEx();
+            TargetType = string.Empty;
+            StepDropHandler = new TreeListViewHandler(this);
+        }
+
+        ~SeqEditorViewModel()
+        {
+            if (engine != null)
+            {
+                if (sequenceFile != null)
+                {
+                    engine.ReleaseSequenceFileEx(sequenceFile);
+                }
+                foreach (var file in cacheFiles.Values)
+                {
+                    engine.ReleaseSequenceFileEx(file);
+                }
+                engine.ShutDown(true);
+                // 涓嶅姞寤舵椂浼氬脊绐梞d
+                //System.Threading.Thread.Sleep(2500);
+            }
+        }
+
+        private ObservableCollection<StepUI> LoadSteps(Sequence seq, string group = null)
+        {
+            //if (sequenceCache.ContainsKey(seq))
+            //{
+            //    return sequenceCache[seq];
+            //}
+
+            //setupSteps.Clear();
+            //var steps = new ObservableCollection<StepUI>();
+            //int num = seq.GetNumSteps(StepGroups.StepGroup_Setup);
+            //for (int i = 0; i < num; i++)
+            //{
+            //    Step step = seq.GetStep(i, StepGroups.StepGroup_Setup);
+            //    var ui = new StepUI(step);
+            //    if (edits.Contains(ui.Type))
+            //    {
+            //        setupSteps.Add(ui);
+            //        steps.Add(ui);
+            //    }
+            //}
+            ////SetupSteps = steps;
+            ////steps = new ObservableCollection<StepUI>();
+            //mainSteps.Clear();
+            //num = seq.GetNumSteps(StepGroups.StepGroup_Main);
+            //for (int i = 0; i < num; i++)
+            //{
+            //    Step step = seq.GetStep(i, StepGroups.StepGroup_Main);
+            //    var ui = new StepUI(step);
+            //    if (edits.Contains(ui.Type))
+            //    {
+            //        mainSteps.Add(ui);
+            //        steps.Add(ui);
+            //    }
+            //}
+            ////MainSteps = steps;
+            ////steps = new ObservableCollection<StepUI>();
+            //cleanupSteps.Clear();
+            //num = seq.GetNumSteps(StepGroups.StepGroup_Cleanup);
+            //for (int i = 0; i < num; i++)
+            //{
+            //    Step step = seq.GetStep(i, StepGroups.StepGroup_Cleanup);
+            //    var ui = new StepUI(step);
+            //    if (edits.Contains(ui.Type))
+            //    {
+            //        cleanupSteps.Add(ui);
+            //        steps.Add(ui);
+            //    }
+            //}
+            //AllSteps = steps;
+            //CurrentStepUI = null;
+
+            var steps = new ObservableCollection<StepUI>();
+            StepGroups[] groups = new StepGroups[] { StepGroups.StepGroup_Setup, StepGroups.StepGroup_Main, StepGroups.StepGroup_Cleanup };
+
+            foreach (var stepGroup in groups)
+            {
+                int num = seq.GetNumSteps(stepGroup);
+                for (int i = 0; i < num; i++)
+                {
+                    Step step = seq.GetStep(i, stepGroup);
+                    var ui = Convert2StepUI(i, step, seq, group);
+                    if (ui != null)
+                    {
+                        steps.Add(ui);
+                    }
+                }
+            }
+
+            //int num = seq.GetNumSteps(StepGroups.StepGroup_Main);
+            //for (int i = 0; i < num; i++)
+            //{
+            //    Step step = seq.GetStep(i, StepGroups.StepGroup_Main);
+            //    var ui = Convert2StepUI(step, seq, group);
+            //    if (ui != null)
+            //    {
+            //        steps.Add(ui);
+            //    }
+            //}
+
+            //num = seq.GetNumSteps(StepGroups.StepGroup_Cleanup);
+            //for (int i = 0; i < num; i++)
+            //{
+            //    Step step = seq.GetStep(i, StepGroups.StepGroup_Cleanup);
+            //    var ui = Convert2StepUI(step, seq, group);
+            //    if (ui != null)
+            //    {
+            //        steps.Add(ui);
+            //    }
+            //}
+            sequenceCache[seq] = steps;
+            return steps;
+        }
+
+        private int indexCache = 0;
+
+        private StepUI Convert2StepUI(int index, Step step, Sequence sequence, string group)
+        {
+            var ui = new StepUI(step, sequence);
+            if (edits != null && edits.Count > 0 && !edits.Contains(ui.Type))
+            {
+                return null;
+            }
+
+            if (string.IsNullOrEmpty(group))
+            {
+                if (ui.StepGroup == StepGroups.StepGroup_Setup)
+                {
+                    group = "Setup";
+                }
+                else if (ui.StepGroup == StepGroups.StepGroup_Main)
+                {
+                    group = "Main";
+                }
+                else if (ui.StepGroup == StepGroups.StepGroup_Cleanup)
+                {
+                    group = "Cleanup";
+                }
+                else
+                {
+                    group = "Unkown";
+                }
+            }
+            ui.Group = group;
+
+            if (ui.Type == StepTypeStr.SequenceCall)
+            {
+                var po = step.Module.AsPropertyObject();
+                bool useCurFile = po.GetValBoolean("UseCurFile", 0);
+                string sfPath = po.GetValString("SFPath", 0);
+                if (useCurFile || Path.GetFileName(currentFilePath).Equals(sfPath, StringComparison.CurrentCultureIgnoreCase))
+                {
+                    string seqName = po.GetValString("SeqName", 0);
+                    Sequence sunSequence = sequenceFile.GetSequenceByName(seqName);
+                    if (sunSequence == null)
+                    {
+                        return ui;
+                    }
+                    ui.Children = LoadSteps(sunSequence, group);
+                }
+                else
+                {
+                    string seqName = po.GetValString("SeqName", 0);
+                    if (!Path.IsPathRooted(sfPath))
+                    {
+                        sfPath = Path.Combine(Path.GetDirectoryName(CurrentFilePath), sfPath);
+                    }
+                    SequenceFile diffSequence = null;
+                    try
+                    {
+                        if (cacheFiles.ContainsKey(sfPath))
+                        {
+                            diffSequence = cacheFiles[sfPath];
+                        }
+                        else if (File.Exists(sfPath))
+                        {
+                            diffSequence = engine.GetSequenceFileEx(sfPath);
+                            cacheFiles[sfPath] = diffSequence;
+                        }
+                    }
+                    catch (Exception ex)
+                    {
+                        log.Warn("Read Diff SubSequence Failed.", ex);
+                    }
+                    if (diffSequence != null)
+                    {
+                        Sequence sunSequence = diffSequence.GetSequenceByName(seqName);
+                        ui.Children = LoadSteps(sunSequence, group);
+                    }
+                }
+            }
+            else
+            {
+            }
+            if (CurrentSeqUI != null)
+            {
+                ui.SettingChanged += CurrentSeqUI.OnStepSettingChanged;
+            }
+            return ui;
+        }
+
+
+        [RelayCommand]
+        private void Loaded()
+        {
+            if (!isLoaded)
+            {
+            }
+        }
+
+        [RelayCommand]
+        private void OpenSeqFile()
+        {
+            OpenFileDialog ofd = new OpenFileDialog()
+            {
+                Filter = $"{LangUtil.Get("se.seq")}|*.seq;"
+            };
+            if (ofd.ShowDialog() == true && DoClose())
+            {
+                control.BackgroundAction(() =>
+                {
+                    try
+                    {
+                        var start = DateTime.Now;
+                        CurrentFilePath = ofd.FileName;
+                        if (sequenceFile != null)
+                        {
+                            engine.ReleaseSequenceFileEx(sequenceFile);
+                        }
+                        sequenceFile = engine.GetSequenceFileEx(CurrentFilePath);
+                        Sequence main = sequenceFile.GetSequenceByName("MainSequence");
+                        CurrentSeqUI = new SeqUI(main);
+
+                        //var seqs = new ObservableCollection<SeqUI>();
+                        //for (int i = 0; i < sequenceFile.NumSequences; i++)
+                        //{
+                        //    var tsSequence = sequenceFile.GetSequence(i);
+                        //    seqs.Add(new SeqUI(tsSequence));
+                        //}
+                        //Sequences = seqs;
+
+                        //if (Sequences != null && Sequences.Count > 0)
+                        //{
+                        //    CurrentSeqUI = Sequences[0];
+                        //}
+                        log.Info($"Open File [{CurrentFilePath}] In {(DateTime.Now - start).TotalMilliseconds}ms");
+                    }
+                    catch (Exception ex)
+                    {
+                        log.Error($"Open Sequence Failed.", ex);
+                        control.Dispatcher.Invoke(() =>
+                        {
+                            MsgBoxHelper2.Error(ex.Message);
+                        });
+                    }
+                }, LangUtil.Get("se.seqIsLoading"));
+            }
+        }
+
+        [RelayCommand]
+        private void SaveSeqFile()
+        {
+            if (sequenceFile == null) return;
+            try
+            {
+                sequenceFile?.Save(CurrentFilePath);
+                CurrentSeqUI.IsChanged = false;
+                foreach (var key in cacheFiles.Keys)
+                {
+                    cacheFiles[key]?.Save(key);
+                }
+                ToastHelper.Success(LangUtil.Get("App.SaveSuccess"));
+            }
+            catch (Exception ex)
+            {
+                log.Error("Save Sequence File Error.", ex);
+                ToastHelper.Error($"{LangUtil.Get("App.SaveFailed")}: {ex.Message}");
+            }
+        }
+
+        [RelayCommand]
+        private void SaveToFile()
+        {
+            if (sequenceFile == null) return;
+            try
+            {
+                SaveFileDialog sfd = new SaveFileDialog()
+                {
+                    Filter = $"{LangUtil.Get("se.seq")}|*.seq;"
+                };
+                if (sfd.ShowDialog() == true)
+                {
+                    sequenceFile.Save(sfd.FileName);
+                    CurrentFilePath = sfd.FileName;
+                    foreach (var key in cacheFiles.Keys)
+                    {
+                        cacheFiles[key]?.Save(key);
+                    }
+                    ToastHelper.Success(LangUtil.Get("App.SaveSuccess"));
+                    CurrentSeqUI.IsChanged = false;
+                }
+            }
+            catch (Exception ex)
+            {
+                log.Error("Save Sequence File Error.", ex);
+                ToastHelper.Error($"{LangUtil.Get("App.SaveFailed")}: {ex.Message}");
+            }
+        }
+
+        [RelayCommand]
+        private void Refresh()
+        {
+            if (CurrentSeqUI == null) return;
+            RefreshData();
+        }
+
+        private bool DoClose()
+        {
+            if (CurrentSeqUI?.IsChanged == true)
+            {
+                if (MsgBoxHelper2.WarnChoose(LangUtil.Get("se.makeSureClose"))
+                    == System.Windows.MessageBoxResult.OK)
+                {
+                    goto DoClose;
+                }
+                else
+                {
+                    return false;
+                }
+            }
+
+        DoClose:
+            if (engine != null && sequenceFile != null)
+            {
+                engine.ReleaseSequenceFileEx(sequenceFile);
+                foreach (var file in cacheFiles.Values)
+                {
+                    engine.ReleaseSequenceFileEx(file);
+                }
+                sequenceFile = null;
+            }
+
+            cacheFiles.Clear();
+            setupSteps.Clear();
+            mainSteps.Clear();
+            cleanupSteps.Clear();
+            AllSteps = new();
+            Sequences.Clear();
+            CurrentFilePath = string.Empty;
+            CurrentFileName = string.Empty;
+            CurrentSeqUI = null;
+            CurrentStepUI = null;
+            CurrentSeqTitle = LangUtil.Get("se.steps");
+            sequenceCache.Clear();
+            return true;
+        }
+
+        [RelayCommand]
+        private void CloseSeqFile()
+        {
+            DoClose();
+        }
+
+        [RelayCommand]
+        private void AddStep(string type)
+        {
+            try
+            {
+                var group = CurrentStepUI.step.StepGroup;
+                var targetSequence = CurrentStepUI.sequence;
+
+                ObservableCollection<StepUI> source = sequenceCache[targetSequence];
+                //if (group == StepGroups.StepGroup_Setup)
+                //{
+                //    source = setupSteps;
+                //}
+                //else if (group == StepGroups.StepGroup_Main)
+                //{
+                //    source = mainSteps;
+                //}
+                //else
+                //{
+                //    source = cleanupSteps;
+                //}
+                int index = CurrentStepUI.IndexInSequence;
+                if (index < 0)
+                {
+                    return;
+                }
+                Step newStep;
+                switch (type)
+                {
+                    case "Tests.PassFail":
+                        newStep = engine.NewStep(AdapterKeyNames.FlexLVAdapterKeyName, StepTypeStr.PassFailTest);
+                        newStep.Name = DefaultStepName.PassFailTest;
+                        break;
+                    case "Tests.NumericLimitTest":
+                        newStep = engine.NewStep(AdapterKeyNames.FlexLVAdapterKeyName, StepTypeStr.NumericLimitTest);
+                        newStep.Name = DefaultStepName.NumericLimitTest;
+                        break;
+                    case "Tests.NI_MultipleNumericLimitTest":
+                        newStep = engine.NewStep(AdapterKeyNames.FlexLVAdapterKeyName, StepTypeStr.NI_MultipleNumericLimitTest);
+                        newStep.Name = DefaultStepName.NI_MultipleNumericLimitTest;
+                        break;
+                    case "Tests.StringValueTest":
+                        newStep = engine.NewStep(AdapterKeyNames.FlexLVAdapterKeyName, StepTypeStr.StringValueTest);
+                        newStep.Name = DefaultStepName.StringValueTest;
+                        break;
+                    case "Actions.LabVIEW":
+                        newStep = engine.NewStep(AdapterKeyNames.FlexLVAdapterKeyName, StepTypeStr.Action);
+                        newStep.Name = DefaultStepName.Action;
+                        break;
+                    case "Actions.LabWindows/CVI":
+                        newStep = engine.NewStep(AdapterKeyNames.FlexCVIAdapterKeyName, StepTypeStr.Action);
+                        newStep.Name = DefaultStepName.Action;
+                        break;
+                    case "Actions.C/C++ DLL":
+                        newStep = engine.NewStep(AdapterKeyNames.FlexCAdapterKeyName, StepTypeStr.Action);
+                        newStep.Name = DefaultStepName.Action;
+                        break;
+                    case "Actions..NET":
+                        newStep = engine.NewStep(AdapterKeyNames.DotNetAdapterKeyname, StepTypeStr.Action);
+                        newStep.Name = DefaultStepName.Action;
+                        break;
+                    case "Actions.ActiveX/COM":
+                        newStep = engine.NewStep(AdapterKeyNames.AutomationAdapterKeyName, StepTypeStr.Action);
+                        newStep.Name = DefaultStepName.Action;
+                        break;
+                    case "Actions.None":
+                        newStep = engine.NewStep(AdapterKeyNames.NoneAdapterKeyName, StepTypeStr.Action);
+                        newStep.Name = DefaultStepName.Action;
+                        break;
+                    case "Actions.LabVIEW NXG":
+                        newStep = engine.NewStep(AdapterKeyNames.LabVIEWNXGAdapterKeyName, StepTypeStr.Action);
+                        newStep.Name = DefaultStepName.Action;
+                        break;
+                    case "Actions.Python":
+                        newStep = engine.NewStep(AdapterKeyNames.PythonAdapterKeyName, StepTypeStr.Action);
+                        newStep.Name = DefaultStepName.Action;
+                        break;
+                    default:
+                        return;
+                }
+                var newUI = new StepUI(newStep, targetSequence);
+
+
+                targetSequence.InsertStep(newStep, index + 1, group);
+                ObservableCollection<StepUI> datasource = sequenceCache[targetSequence];
+                int uiIndex = datasource.IndexOf(CurrentStepUI);
+                datasource.Insert(uiIndex + 1, newUI);
+
+
+                //var newItem = new TreeItemData
+                //{
+                //    Level = TreeItemData.Level,
+                //    ParentItem = TreeItemData.ParentItem,
+                //    ParentItems = TreeItemData.ParentItems,
+                //    Data = newUI
+                //};
+                //var parentItem = TreeItemData.ParentItem;
+                //if (parentItem != null)
+                //{
+                //    var items = parentItem.Items;
+                //    int innerIndex = items.IndexOf(TreeItemData);
+
+
+                //    items.Insert(innerIndex + 1, newItem);
+                //    TreeItemDataCollection.ResetSubItems(items);
+                //}
+                //int itemIndex = TreeCollection.IndexOf(TreeItemData);
+                //TreeCollection.Insert(itemIndex + TreeItemData.AllSubCount + 1, newItem);
+            }
+            catch (Exception ex)
+            {
+                log.Error("Insert Step Error.", ex);
+                MsgBoxHelper2.Error(ex.Message);
+            }
+        }
+
+        [RelayCommand]
+        private void Sb(RoutedPropertyChangedEventArgs<object> e)
+        {
+            CurrentStepUI = e.NewValue as StepUI;
+        }
+
+        static TreeListViewItem VisualUpwardSearch(DependencyObject source)
+        {
+            while (source != null && !(source is TreeViewItem))
+                source = VisualTreeHelper.GetParent(source);
+
+            return source as TreeListViewItem;
+        }
+
+        [RelayCommand]
+        private void Sb2(MouseButtonEventArgs e)
+        {
+            var item = VisualUpwardSearch(e.OriginalSource as DependencyObject);
+            if (item != null)
+            {
+                item.Focus();
+                e.Handled = true;
+            }
+        }
+
+        [ObservableProperty]
+        private TreeItemData treeItemData;
+
+        public void RefreshView()
+        {
+            CollectionViewSource cvs = control.Resources["TreeGridFilter"] as CollectionViewSource;
+            control.Dispatcher.Invoke(() =>
+            {
+                cvs.View.Refresh();
+            });
+        }
+
+        [ObservableProperty]
+        private TreeItemDataCollection treeCollection = new TreeItemDataCollection();
+
+        public void RefreshData()
+        {
+            indexCache = 0;
+            sequenceCache.Clear();
+            foreach (var file in cacheFiles.Values)
+            {
+                try
+                {
+                    engine.ReleaseSequenceFileEx(file);
+                    Marshal.ReleaseComObject(file);
+                }
+                catch (Exception ex)
+                {
+                    log.Error("RefreshData Release Last CacheFile Failed.", ex);
+                }
+            }
+            var converter = control.Resources["treeNumber"] as RowNumberConverter;
+            converter.Reset();
+            var temp = LoadSteps(CurrentSeqUI.sequence);
+            SetIndex(temp);
+            AllSteps = temp;
+        }
+
+        void SetIndex(IEnumerable<StepUI> datas)
+        {
+            foreach (var data in datas)
+            {
+                if (indexType.Contains(data.Type))
+                {
+                    data.CustomIndex = ++indexCache;
+                }
+                if (data.Children != null && data.Children.Count > 0)
+                {
+                    SetIndex(data.Children);
+                }
+            }
+        }
+
+        partial void OnAllStepsChanged(ObservableCollection<StepUI> data)
+        {
+            TreeCollection = new TreeItemDataCollection(AllSteps, "Children", true);
+        }
+
+        partial void OnTreeItemDataChanged(TreeItemData data)
+        {
+            CurrentStepUI = data?.Data as StepUI;
+        }
+
+        partial void OnTargetTypeChanged(string value)
+        {
+            if (string.IsNullOrEmpty(value))
+            {
+                edits = new HashSet<string>
+                {
+                    //StepTypeStr.PassFailTest,
+                    //StepTypeStr.NumericLimitTest,
+                    //StepTypeStr.NI_MultipleNumericLimitTest,
+                    //StepTypeStr.StringValueTest,
+                    //StepTypeStr.Action,
+                    //StepTypeStr.NI_Wait,
+                    //StepTypeStr.SequenceCall,
+                };
+            }
+            else
+            {
+                edits = new HashSet<string>(value.Split(MultiComboBox.splits, StringSplitOptions.RemoveEmptyEntries))
+                {
+                    StepTypeStr.SequenceCall
+                };
+            }
+        }
+
+        partial void OnCurrentFilePathChanged(string value)
+        {
+            if (string.IsNullOrEmpty(value))
+            {
+                CurrentFileName = string.Empty;
+            }
+            else
+            {
+                CurrentFileName = $"{LangUtil.Get("se.currentFile")}{Path.GetFileName(value)}";
+            }
+        }
+
+        partial void OnCurrentStepUIChanged(StepUI stepUI)
+        {
+            var panel = StepUI2EPO.Convert2EditPanel(stepUI);
+            control.editPanelContainer.Child = panel;
+            if (stepUI != null)
+            {
+                //window.commonP.RefreshStep(stepUI);
+            }
+        }
+
+        partial void OnCurrentSeqUIChanged(SeqUI seqUI)
+        {
+            if (seqUI != null)
+            {
+                CurrentStepUI = null;
+                RefreshData();
+            }
+            CurrentSeqTitle = $"{seqUI?.Name} {LangUtil.Get("se.steps")}";
+        }
+    }
+}

--
Gitblit v1.9.1