├── Account.cs ├── App.xaml ├── App.xaml.cs ├── Broker.cs ├── Call.cs ├── Conference.cs ├── Controls ├── OurAutoCompleteBox.cs ├── PhonePadButton.xaml └── PhonePadButton.xaml.cs ├── Converters.cs ├── DelayedFunction.cs ├── DpiUtil.cs ├── EventSocket.cs ├── External Items ├── JabraSDK.dll ├── Plantronics.Device.Common.dll ├── conf │ └── freeswitch.xml ├── fs_cli.exe ├── libjabra.dll ├── link.pl └── portaudio_notes.txt ├── FSClient.csproj ├── FSClient.sln ├── FSEvent.cs ├── Field.cs ├── GenericEditor.xaml ├── GenericEditor.xaml.cs ├── HotKey.cs ├── IContactPlugin.cs ├── IHeadsetPlugin.cs ├── IPlugin.cs ├── IncomingCallNotification.xaml ├── IncomingCallNotification.xaml.cs ├── InputBox.xaml ├── InputBox.xaml.cs ├── JabraHeadset ├── JabraHeadsetPlugin.csproj ├── JabraProvider.cs ├── Properties │ └── AssemblyInfo.cs └── app.config ├── MainWindow.xaml ├── MainWindow.xaml.cs ├── ObservableClass.cs ├── Options.xaml ├── Options.xaml.cs ├── PlantronicsHeadset ├── PlantronicsHeadsetPlugin.csproj ├── PlantronicsProvider.cs ├── Properties │ └── AssemblyInfo.cs └── app.config ├── PluginManagerBase.cs ├── PluginOptionsWindow.xaml ├── PluginOptionsWindow.xaml.cs ├── PortAudio.cs ├── Properties ├── AssemblyInfo.cs ├── Resources.Designer.cs ├── Resources.resx ├── Settings.Designer.cs └── Settings.settings ├── README.md ├── Setup ├── Banner.bmp ├── Dialog.bmp ├── FSClient.wxs ├── FSCore.wxs ├── Product.wxs └── Setup.wixproj ├── SimpleContactPluginBase.cs ├── SimpleXmlContactPlugin ├── Properties │ └── AssemblyInfo.cs ├── SimpleXmlContactPlugin.cs ├── SimpleXmlContactPlugin.csproj └── app.config ├── Sofia.cs ├── Themes ├── Base.xaml ├── Black.xaml ├── RoyalBlue.xaml ├── Steel.xaml └── White.xaml ├── Utils.cs ├── Windows.cs ├── XmlUtils.cs ├── app.config ├── phone.ico ├── phone_dnd.ico ├── screenshots ├── FSClient_themes.png └── FSclient_screen.png └── warning.png /App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | namespace FSClient { 4 | /// 5 | /// Interaction logic for App.xaml 6 | /// 7 | public partial class App : Application { 8 | public App() { 9 | DispatcherUnhandledException += App_DispatcherUnhandledException; 10 | System.Diagnostics.Process.GetCurrentProcess().PriorityClass = System.Diagnostics.ProcessPriorityClass.High; 11 | 12 | } 13 | 14 | void App_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e) { 15 | MessageBox.Show("Dispatcher exception of: " + e.Exception.Message, "Dispatcher Exception", MessageBoxButton.OK, MessageBoxImage.Error); 16 | e.Handled = true; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Controls/OurAutoCompleteBox.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Windows; 6 | using System.Windows.Controls; 7 | 8 | namespace FSClient.Controls { 9 | public class OurAutoCompleteBox : AutoCompleteBox { 10 | public enum OurAutoCompleteFilterMode { 11 | None, 12 | StartsWith, 13 | StartsWithCaseSensitive, 14 | StartsWithOrdinal, 15 | StartsWithOrdinalCaseSensitive, 16 | Contains, 17 | ContainsCaseSensitive, 18 | ContainsOrdinal, 19 | ContainsOrdinalCaseSensitive, 20 | Equals, 21 | EqualsCaseSensitive, 22 | EqualsOrdinal, 23 | EqualsOrdinalCaseSensitive, 24 | Custom, 25 | ContainsSplit 26 | } 27 | private string last_search_term; 28 | private string[] last_words; 29 | private bool MultiTextFilter(string search, string item) { 30 | if(search != last_search_term) { 31 | last_search_term = search; 32 | last_words = search.Split(' '); 33 | } 34 | return last_words.All(word => item.IndexOf(word, StringComparison.CurrentCultureIgnoreCase) != -1); 35 | } 36 | public new OurAutoCompleteFilterMode FilterMode { 37 | get { return (OurAutoCompleteFilterMode)GetValue(FilterModeProperty); } 38 | set { 39 | SetValue(FilterModeProperty, value); 40 | } 41 | } 42 | public static new readonly DependencyProperty FilterModeProperty = DependencyProperty.Register("FilterMode", typeof(OurAutoCompleteFilterMode), typeof(OurAutoCompleteBox), new PropertyMetadata((object)OurAutoCompleteFilterMode.StartsWith, new PropertyChangedCallback(OurAutoCompleteBox.OnFilterModePropertyChanged))); 43 | 44 | private static void OnFilterModePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { 45 | OurAutoCompleteBox autoCompleteBox = d as OurAutoCompleteBox; 46 | AutoCompleteBox base_box = autoCompleteBox; 47 | var mode = (OurAutoCompleteFilterMode)e.NewValue; 48 | switch(mode) { 49 | case OurAutoCompleteFilterMode.ContainsSplit: 50 | base_box.FilterMode = AutoCompleteFilterMode.Custom; 51 | autoCompleteBox.TextFilter = autoCompleteBox.MultiTextFilter; 52 | break; 53 | default: 54 | base_box.FilterMode = (AutoCompleteFilterMode)mode; 55 | break; 56 | } 57 | } 58 | private TextBox the_textbox; 59 | private void find_textbox() { 60 | if(the_textbox != null) 61 | return; 62 | the_textbox = Template.FindName("Text", this) as TextBox; 63 | if(the_textbox != null) 64 | the_textbox.TabIndex = TabIndex; 65 | } 66 | public TextBox GetActualTextbox() { 67 | find_textbox(); 68 | return the_textbox; 69 | } 70 | public bool TextBoxHasFocus() { 71 | find_textbox(); 72 | if(the_textbox == null) 73 | return false; 74 | return the_textbox.IsFocused; 75 | } 76 | public void TextBoxFocus() { 77 | find_textbox(); 78 | if(the_textbox != null) 79 | the_textbox.Focus(); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Controls/PhonePadButton.xaml: -------------------------------------------------------------------------------- 1 |  10 | 11 | 12 | 26 | 27 | -------------------------------------------------------------------------------- /Controls/PhonePadButton.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Windows; 6 | using System.Windows.Automation; 7 | using System.Windows.Automation.Peers; 8 | using System.Windows.Controls; 9 | using System.Windows.Data; 10 | using System.Windows.Documents; 11 | using System.Windows.Input; 12 | using System.Windows.Media; 13 | using System.Windows.Media.Imaging; 14 | using System.Windows.Navigation; 15 | using System.Windows.Shapes; 16 | using System.ComponentModel; 17 | 18 | namespace FSClient.Controls 19 | { 20 | /// 21 | /// Interaction logic for PhonePadButton.xaml 22 | /// 23 | public partial class PhonePadButton : UserControl 24 | { 25 | public PhonePadButton() 26 | { 27 | this.InitializeComponent(); 28 | btn.Click += new RoutedEventHandler(btn_Click); 29 | } 30 | 31 | void btn_Click(object sender, RoutedEventArgs e) 32 | { 33 | if (Click != null) 34 | Click(this, e); 35 | } 36 | [Category("Behavior")] 37 | public event RoutedEventHandler Click; 38 | 39 | public string Number 40 | { 41 | get { return lblNumber.Text; } 42 | set { lblNumber.Text = value; AutomationProperties.SetName(btn, value); AutomationProperties.SetName(this, value); AutomationProperties.SetItemType(this,"Button"); } 43 | } 44 | public string Letters 45 | { 46 | get { return lblLetters.Text; } 47 | set { lblLetters.Text = value; } 48 | } 49 | protected AutomationControlType GetAutomationControlTypeCore() { 50 | return AutomationControlType.Button; 51 | } 52 | 53 | } 54 | } -------------------------------------------------------------------------------- /Converters.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Windows; 7 | using System.Windows.Data; 8 | using System.Windows.Media; 9 | 10 | namespace FSClient { 11 | public class ConfStateConverter : IValueConverter{ 12 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture){ 13 | if (value == null) 14 | return ""; 15 | ConferenceUser.USER_STATE state = (ConferenceUser.USER_STATE)value; 16 | return StateConvert(state); 17 | } 18 | public static String StateConvert(ConferenceUser.USER_STATE state){ 19 | String ret = ""; 20 | 21 | if (ConferenceUser.StateTest(state,ConferenceUser.USER_STATE.DEAF)) 22 | ret += "D "; 23 | if (ConferenceUser.StateTest(state, ConferenceUser.USER_STATE.FLOOR)) 24 | ret += "F "; 25 | if (ConferenceUser.StateTest(state, ConferenceUser.USER_STATE.MUTE)) 26 | ret += "M "; 27 | if (ConferenceUser.StateTest(state, ConferenceUser.USER_STATE.TALK)) 28 | ret += "T "; 29 | return ret; 30 | } 31 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture){ 32 | throw new NotImplementedException(); 33 | } 34 | } 35 | public class StateConverter : IValueConverter { 36 | public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { 37 | 38 | if (value == null) 39 | return Colors.White.ToString(); 40 | String ret = Colors.White.ToString(); 41 | Call.CALL_STATE state = (Call.CALL_STATE)value; 42 | switch (state) { 43 | case Call.CALL_STATE.Answered: 44 | ret = "#FF4EFF00"; 45 | break; 46 | case Call.CALL_STATE.Busy: 47 | ret = "#FFFFAF00"; 48 | break; 49 | case Call.CALL_STATE.Ended: 50 | ret = "#FFAFAFAF"; 51 | break; 52 | case Call.CALL_STATE.Failed: 53 | ret = "#FF000000"; 54 | break; 55 | case Call.CALL_STATE.Hold: 56 | ret = "#FFF1FF00"; 57 | break; 58 | case Call.CALL_STATE.Missed: 59 | ret = "#FFFF0000"; 60 | break; 61 | case Call.CALL_STATE.Hold_Ringing: 62 | case Call.CALL_STATE.Ringing: 63 | ret = "#FF00B3FF"; 64 | break; 65 | } 66 | return ret; 67 | } 68 | public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { 69 | throw new NotImplementedException(); 70 | } 71 | } 72 | public class AccountDefaultConverter : IValueConverter { 73 | public static SolidColorBrush default_account_color; 74 | public static SolidColorBrush normal_account_color; 75 | 76 | public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { 77 | if (value == null) 78 | return normal_account_color; 79 | bool is_default = (bool)value; 80 | return is_default ? default_account_color : normal_account_color; 81 | } 82 | public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { 83 | throw new NotImplementedException(); 84 | } 85 | } 86 | 87 | public class EnglishDirectionConverter : IValueConverter { 88 | public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { 89 | if (value == null) 90 | return ""; 91 | return EnglishDirectionConverter.Convert((bool)value); 92 | 93 | } 94 | public static String Convert(bool is_outgoing) { 95 | return is_outgoing ? "Outgoing" : "Incoming"; 96 | } 97 | public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { 98 | throw new NotImplementedException(); 99 | } 100 | } 101 | public class DirectionConverter : IValueConverter { 102 | public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { 103 | if (value == null) 104 | return ""; 105 | bool is_outgoing = (bool)value; 106 | return is_outgoing ? "<-" : "->"; 107 | } 108 | public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { 109 | throw new NotImplementedException(); 110 | } 111 | } 112 | public class DurationTimeConverter : IValueConverter { 113 | public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { 114 | if (value == null) 115 | return ""; 116 | return Convert((TimeSpan)value); 117 | } 118 | public static string Convert(TimeSpan duration) { 119 | return duration.Minutes + ":" + duration.Seconds.ToString("00"); 120 | } 121 | public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { 122 | throw new NotImplementedException(); 123 | } 124 | } 125 | public class ShortDateTimeConverter : IValueConverter { 126 | public static string Convert(DateTime time) { 127 | if (time == DateTime.MinValue) 128 | return ""; 129 | return time.ToString("ddd, dd MMM yyyy HH:mm:ss tt"); 130 | } 131 | public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { 132 | if (value == null) 133 | return ""; 134 | return Convert((DateTime)value); 135 | 136 | } 137 | 138 | public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { 139 | throw new NotImplementedException(); 140 | } 141 | } 142 | public class BoolToVisibilityConverter : IValueConverter { 143 | public static Visibility Convert(bool value) { 144 | return value ? Visibility.Visible : Visibility.Hidden; 145 | } 146 | public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { 147 | if (value == null) 148 | return ""; 149 | return Convert((bool)value); 150 | 151 | } 152 | 153 | public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { 154 | throw new NotImplementedException(); 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /DelayedFunction.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Timers; 4 | 5 | namespace FSClient { 6 | public class DelayedFunction { 7 | private class DelayedItem { 8 | public String key; 9 | public Action action; 10 | public DateTime end_time; 11 | } 12 | private static List functions = new List(); 13 | private static Timer timer; 14 | private static bool timer_enabled; 15 | public static void DelayedCall(String key, Action action, int ms) { 16 | if (timer == null) { 17 | timer = new Timer(); 18 | timer.Interval = 200; 19 | timer.Elapsed += timer_Elapsed; 20 | } 21 | lock (functions) { 22 | if (!String.IsNullOrWhiteSpace(key)){ 23 | for (int i = 0; i < functions.Count; i++){ 24 | if (functions[i].key == key){ 25 | functions.RemoveAt(i); 26 | break; 27 | } 28 | } 29 | } 30 | functions.Add(new DelayedItem { key = key, action = action, end_time = DateTime.Now.AddMilliseconds(ms) }); 31 | if (!timer_enabled) 32 | timer_enabled = timer.Enabled = true; 33 | } 34 | } 35 | 36 | static void timer_Elapsed(object sender, ElapsedEventArgs e) { 37 | timer.Enabled = false; 38 | DateTime current = DateTime.Now; 39 | List to_perform = new List(); 40 | 41 | lock (functions) { 42 | for (int i = 0; i < functions.Count; i++) { 43 | if (functions[i].end_time > current) 44 | continue; 45 | to_perform.Add(functions[i].action); 46 | functions.RemoveAt(i); 47 | i--; 48 | } 49 | } 50 | foreach (Action action in to_perform) 51 | action(); 52 | 53 | lock (functions) { 54 | timer_enabled = timer.Enabled = (functions.Count != 0); 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /DpiUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | /* 4 | This class taken directly from keepass slimmed downed and slightly modified: 5 | https://github.com/dlech/KeePass2.x/blob/9b57541e6fcb49cb5f12029fb8e553295cf153c4/KeePass/UI/DpiUtil.cs 6 | KeePass Password Safe - The Open-Source Password Manager 7 | Copyright (C) 2003-2019 Dominik Reichl 8 | */ 9 | 10 | namespace FSClient { 11 | 12 | public static class DpiUtil { 13 | internal const int LOGPIXELSX = 88; 14 | internal const int LOGPIXELSY = 90; 15 | [DllImport("User32.dll")] 16 | internal static extern IntPtr GetDC(IntPtr hWnd); 17 | [DllImport("Gdi32.dll")] 18 | internal static extern int GetDeviceCaps(IntPtr hdc, int nIndex); 19 | [DllImport("User32.dll")] 20 | internal static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC); 21 | 22 | public static int ScaleIntX(double i) { 23 | EnsureInitialized(); 24 | return (int)Math.Round((double)i * m_dScaleX); 25 | } 26 | 27 | public static int ScaleIntY(double i) { 28 | EnsureInitialized(); 29 | return (int)Math.Round((double)i * m_dScaleY); 30 | } 31 | public static int DeScaleIntX(double i) { 32 | EnsureInitialized(); 33 | return (int)Math.Round((double)i / m_dScaleX); 34 | } 35 | 36 | public static int DeScaleIntY(double i) { 37 | EnsureInitialized(); 38 | return (int)Math.Round((double)i / m_dScaleY); 39 | } 40 | private static void EnsureInitialized() { 41 | if (m_bInitialized) return; 42 | 43 | try { 44 | IntPtr hDC = GetDC(IntPtr.Zero); 45 | if (hDC != IntPtr.Zero) { 46 | m_nDpiX = GetDeviceCaps(hDC, 47 | LOGPIXELSX); 48 | m_nDpiY = GetDeviceCaps(hDC, 49 | LOGPIXELSY); 50 | if ((m_nDpiX <= 0) || (m_nDpiY <= 0)) { 51 | System.Diagnostics.Debug.Assert(false); 52 | m_nDpiX = StdDpi; 53 | m_nDpiY = StdDpi; 54 | } 55 | 56 | if (ReleaseDC(IntPtr.Zero, hDC) != 1) { 57 | System.Diagnostics.Debug.Assert(false); 58 | } 59 | } else { System.Diagnostics.Debug.Assert(false); } 60 | } catch (Exception) { System.Diagnostics.Debug.Assert(false); } 61 | 62 | m_dScaleX = (double)m_nDpiX / (double)StdDpi; 63 | m_dScaleY = (double)m_nDpiY / (double)StdDpi; 64 | 65 | m_bInitialized = true; 66 | } 67 | private const int StdDpi = 96; 68 | 69 | private static bool m_bInitialized = false; 70 | 71 | private static int m_nDpiX = StdDpi; 72 | private static int m_nDpiY = StdDpi; 73 | 74 | private static double m_dScaleX = 1.0; 75 | public static double FactorX { 76 | get { 77 | EnsureInitialized(); 78 | return m_dScaleX; 79 | } 80 | } 81 | 82 | private static double m_dScaleY = 1.0; 83 | public static double FactorY { 84 | get { 85 | EnsureInitialized(); 86 | return m_dScaleY; 87 | } 88 | } 89 | 90 | } 91 | 92 | } -------------------------------------------------------------------------------- /EventSocket.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Windows; 4 | using System.Xml; 5 | using System.Xml.Serialization; 6 | 7 | namespace FSClient { 8 | 9 | [XmlRoot("settingsEventSocket")] 10 | public class SettingsEventSocket { 11 | public SettingsField[] fields { get; set; } 12 | public EventSocket GetEventSocket() { 13 | EventSocket socket = new EventSocket(); 14 | foreach (SettingsField field in fields) { 15 | FieldValue val = FieldValue.GetByName(socket.values, field.name); 16 | if (val != null) 17 | val.value = field.value; 18 | } 19 | return socket; 20 | } 21 | public SettingsEventSocket() { 22 | } 23 | public SettingsEventSocket(EventSocket socket) { 24 | fields = (from fv in socket.values select new SettingsField(fv)).ToArray(); 25 | } 26 | } 27 | public class EventSocket{ 28 | public static Field[] fields = { 29 | new Field(Field.FIELD_TYPE.String, "Listen IP","listen-ip","listen-ip","127.0.0.1",""), 30 | new Field(Field.FIELD_TYPE.Int,"Listen Port","listen-port","listen-port","8022",""), 31 | new Field(Field.FIELD_TYPE.String,"Password","password","password","ClueCon",""), 32 | new Field(Field.FIELD_TYPE.String,"Inbound ACL","apply-inbound-acl","apply-inbound-acl","",""), 33 | new Field(Field.FIELD_TYPE.Bool,"Nat Map","nat-map","nat-map","false",""), 34 | new Field(Field.FIELD_TYPE.Combo,"Debug Mode","debug","debug","0","",new Field.FieldOption{display_value="true", value="1"},new Field.FieldOption{display_value="false", value="0"}), 35 | }; 36 | private static string[] AllowedEmptyFields = new[] { "password", "listen-ip" }; 37 | public FieldValue[] values = FieldValue.FieldValues(fields); 38 | public void gen_config(XmlNode config_node){ 39 | XmlNode settings = XmlUtils.AddNodeNode(config_node, "settings"); 40 | foreach (FieldValue value in values) { 41 | if (String.IsNullOrEmpty(value.field.xml_name)) 42 | continue; 43 | if (String.IsNullOrWhiteSpace(value.value) && !AllowedEmptyFields.Contains(value.field.name)) 44 | continue; 45 | Utils.add_xml_param(settings, value.field.xml_name, value.value); 46 | } 47 | } 48 | public void reload_config(){ 49 | Utils.bgapi_exec("reload", "mod_event_socket"); 50 | 51 | } 52 | public void edit() { 53 | GenericEditor editor = new GenericEditor(); 54 | editor.Init("Editing Event Socket Settings", values); 55 | editor.ShowDialog(); 56 | if (editor.DialogResult == true){ 57 | MessageBoxResult mres = MessageBox.Show("Do you want to reload the event socket settings now, doing so will drop anyone connected to the event socket?", "Reload Module Warning", MessageBoxButton.YesNo); 58 | if (mres != MessageBoxResult.Yes) 59 | return; 60 | reload_config(); 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /External Items/JabraSDK.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchcapper/FSClient/e1cc332a216a2331e49d46c67a5b7fe526aba5cd/External Items/JabraSDK.dll -------------------------------------------------------------------------------- /External Items/Plantronics.Device.Common.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchcapper/FSClient/e1cc332a216a2331e49d46c67a5b7fe526aba5cd/External Items/Plantronics.Device.Common.dll -------------------------------------------------------------------------------- /External Items/conf/freeswitch.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 |
117 | 118 |
119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 |
145 |
146 | -------------------------------------------------------------------------------- /External Items/fs_cli.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchcapper/FSClient/e1cc332a216a2331e49d46c67a5b7fe526aba5cd/External Items/fs_cli.exe -------------------------------------------------------------------------------- /External Items/libjabra.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchcapper/FSClient/e1cc332a216a2331e49d46c67a5b7fe526aba5cd/External Items/libjabra.dll -------------------------------------------------------------------------------- /External Items/link.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | use strict; 3 | my $FS_BUILD_PATH = "c:\\software\\freeswitch\\Win32\\Debug"; 4 | my $WORKING_DIR = "..\\bin\\Debug"; 5 | 6 | my $USE_LN =0; #THIS SHOULD BE SET TO 0 (unless you have an ln equiv for windows) 7 | my $COPY_CONF = 1; 8 | my $VERBOSE = 0; 9 | my $try_pdb = 1; 10 | my $VERBOSE_SYS_RES = 0; 11 | die "Build directory does not exist: $FS_BUILD_PATH" if (! -e $FS_BUILD_PATH); 12 | die "Working directory does not exist: $WORKING_DIR" if (! -e $WORKING_DIR); 13 | my @MAIN_FILES = qw/FreeSwitch.dll libapr.dll libaprutil.dll libbroadvoice.dll libspandsp.dll libteletone.dll mod_managed.dll pthread.dll libeay32.dll ssleay32.dll FreeSWITCH.Managed.dll libpng16.dll/; 14 | my @modules = qw/mod_celt mod_commands mod_console mod_dialplan_xml mod_dptools mod_event_socket mod_ilbc mod_local_stream mod_logfile mod_loopback mod_PortAudio mod_siren mod_sndfile mod_sofia mod_opus mod_tone_stream mod_h26x mod_silk mod_spandsp mod_bv mod_iSAC mod_conference/; 15 | 16 | my $link_cmd = $USE_LN ? "ln -s -a" : "copy"; 17 | 18 | sub exec_cmd($){ 19 | my ($cmd) = @_; 20 | print $cmd . "\n" if ($VERBOSE); 21 | my $res = `$cmd 2>&1`; 22 | chomp($res); 23 | chomp($res); 24 | $res =~ s/\n/\n\t/g; 25 | print $res . "\n" if ($VERBOSE_SYS_RES); 26 | } 27 | sub link_file($$){ 28 | my ($src,$dst) = @_; 29 | exec_cmd(qq~$link_cmd "$src" "$dst"~); 30 | print STDERR "Warning after copying/linking file: \"$dst\" did not exist src: $src\n" if (! -e $dst); 31 | } 32 | exec_cmd("mkdir " . $WORKING_DIR . "\\mod") if (-e $WORKING_DIR . "\\mod" == 0); 33 | foreach my $module (@modules){ 34 | my $rel_path = "mod\\" . $module . ".dll"; 35 | my $src_path = $FS_BUILD_PATH . "\\" . $rel_path; 36 | if (-e $src_path == 0){ 37 | print STDERR "Warning: $src_path does not exist\n"; 38 | next; 39 | } 40 | if ($try_pdb){ 41 | my $pdb_file = $src_path; 42 | $pdb_file =~ s/\.dll$/.pdb/si; 43 | my $pdb_dest = "$WORKING_DIR\\$rel_path"; 44 | $pdb_dest =~ s/\.dll$/.pdb/si; 45 | link_file($pdb_file,$pdb_dest) if (-e $pdb_file && (! $USE_LN || ! -e $pdb_dest)); 46 | } 47 | next if (-e "$WORKING_DIR\\" . $rel_path && $USE_LN); 48 | link_file($src_path, "$WORKING_DIR\\$rel_path"); 49 | 50 | } 51 | 52 | foreach my $file (@MAIN_FILES){ 53 | my $src_path = $FS_BUILD_PATH . "\\" . $file; 54 | $src_path = $FS_BUILD_PATH . "\\mod\\" . $file if (-e $FS_BUILD_PATH . "\\mod\\" . $file); #mainly for putting mod_managed.dll into the root 55 | if (-e $src_path == 0){ 56 | print STDERR "Warning: $src_path does not exist\n"; 57 | next; 58 | } 59 | if ($try_pdb){ 60 | my $pdb_file = $src_path; 61 | $pdb_file =~ s/\.dll$/.pdb/si; 62 | my $pdb_dest = "$WORKING_DIR\\$file"; 63 | $pdb_dest =~ s/\.dll$/.pdb/si; 64 | link_file($pdb_file,$pdb_dest) if (-e $pdb_file && (! $USE_LN || ! -e $pdb_dest)); 65 | } 66 | next if (-e "$WORKING_DIR\\" . $file && $USE_LN); 67 | link_file($src_path, "$WORKING_DIR\\$file"); 68 | } 69 | 70 | if ($COPY_CONF){ 71 | exec_cmd("mkdir " . $WORKING_DIR . "\\conf") if (-e $WORKING_DIR . "\\conf" == 0); 72 | link_file("conf\\freeswitch.xml","$WORKING_DIR\\conf\\freeswitch.xml"); 73 | } 74 | -------------------------------------------------------------------------------- /External Items/portaudio_notes.txt: -------------------------------------------------------------------------------- 1 | Portaudio changes: 2 | The major change in portaudio is the way streams are handled. Streams are only torn down at the end of all of the calls (rather than the end of any call). This increases the speed when switching between calls. Timers were also tweaked to make sure there are no more shared timers (caused some audio issues otherwise with multiple calls). 3 | 4 | These changes have not been tested in linux, however 99% of the changes are off by default (and must be enabled using vairables in the portaudio config) so it should be fairly stable. I also found the best results in windows using libportaudio compiled with DX support, however these changes are not DX specific. 5 | 6 | New Additions: 7 | added call held and call resumed events 8 | base support for an infinite number of streams 9 | Ability to keep streams around until all calls are completed rather than re-initing them between any call swtiches. 10 | Call ID variable standarized on "pa_call_id" for events (but left behind old for backwards compatability). 11 | Added ability to prevent ringing if there is currently an active call (variable "no-ring-during-call") 12 | Add option to not auto-unhold the next call once a call is ended (variable "no-auto-resume-call") 13 | Added ability to play audio directly to a specific stream ("pa playdev # [ringtest|] [seconds] [no_close]") 14 | Add ability to switch between streams during a call ("pa indev or pa outdev" only if variable "live-stream-switch" is enabled) 15 | Add ability to switch both streams at once for speed ("pa switchstream # #") 16 | Add ability to open a stream prior to using it for faster swtiching ("pa preparestream # #") 17 | Added the option to specify the number of seconds to play the audio for "pa play" and "pa playdev" 18 | Added option of no_close to not close streams after play or playdev, useful if playing multiple things in rapid succession 19 | Add closestreams command to allow for closing any currently open/prepared streams. 20 | 21 | Fixes: 22 | independent timers for everything eneded (should fix some audio glitches) 23 | No longer will ring the ring device during a call if the ring device is the same as the call device(will just screw up audio otherwise) 24 | Muting is global and no longer reset at the start of each call (this is a change in behavior) 25 | As you could have multiple play or playdevs running try to avoid destroying streams 26 | -------------------------------------------------------------------------------- /FSClient.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.2.32414.248 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FSClient", "FSClient.csproj", "{A2D2A785-3B79-4042-9C3D-97B1CD9A804E}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlantronicsHeadsetPlugin", "PlantronicsHeadset\PlantronicsHeadsetPlugin.csproj", "{5E156019-7D68-402D-9109-7F7F1E55EA8A}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JabraHeadsetPlugin", "JabraHeadset\JabraHeadsetPlugin.csproj", "{084B8F8C-1E32-4545-B865-FA7274BD8054}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "External Items", "External Items", "{E5A3FCB6-A888-4290-B076-864674F90508}" 13 | ProjectSection(SolutionItems) = preProject 14 | External Items\JabraTelephonyAPI.dll = External Items\JabraTelephonyAPI.dll 15 | External Items\Plantronics.Device.Common.dll = External Items\Plantronics.Device.Common.dll 16 | EndProjectSection 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "conf", "conf", "{BDB28E84-D1D5-4102-BD29-C638197EFD42}" 19 | ProjectSection(SolutionItems) = preProject 20 | External Items\conf\freeswitch.xml = External Items\conf\freeswitch.xml 21 | EndProjectSection 22 | EndProject 23 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleXmlContactPlugin", "SimpleXmlContactPlugin\SimpleXmlContactPlugin.csproj", "{8DA31553-28E9-4A08-897E-7EE1E01356A3}" 24 | EndProject 25 | Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "Setup", "Setup\Setup.wixproj", "{80D26E64-EECF-47F2-8132-804F087C27BA}" 26 | EndProject 27 | Global 28 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 29 | Debug|Any CPU = Debug|Any CPU 30 | Debug|Mixed Platforms = Debug|Mixed Platforms 31 | Debug|x64 = Debug|x64 32 | Debug|x86 = Debug|x86 33 | Release|Any CPU = Release|Any CPU 34 | Release|Mixed Platforms = Release|Mixed Platforms 35 | Release|x64 = Release|x64 36 | Release|x86 = Release|x86 37 | EndGlobalSection 38 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 39 | {A2D2A785-3B79-4042-9C3D-97B1CD9A804E}.Debug|Any CPU.ActiveCfg = Debug|x86 40 | {A2D2A785-3B79-4042-9C3D-97B1CD9A804E}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 41 | {A2D2A785-3B79-4042-9C3D-97B1CD9A804E}.Debug|Mixed Platforms.Build.0 = Debug|x86 42 | {A2D2A785-3B79-4042-9C3D-97B1CD9A804E}.Debug|x64.ActiveCfg = Debug|x64 43 | {A2D2A785-3B79-4042-9C3D-97B1CD9A804E}.Debug|x64.Build.0 = Debug|x64 44 | {A2D2A785-3B79-4042-9C3D-97B1CD9A804E}.Debug|x86.ActiveCfg = Debug|x86 45 | {A2D2A785-3B79-4042-9C3D-97B1CD9A804E}.Debug|x86.Build.0 = Debug|x86 46 | {A2D2A785-3B79-4042-9C3D-97B1CD9A804E}.Release|Any CPU.ActiveCfg = Release|x86 47 | {A2D2A785-3B79-4042-9C3D-97B1CD9A804E}.Release|Mixed Platforms.ActiveCfg = Release|x86 48 | {A2D2A785-3B79-4042-9C3D-97B1CD9A804E}.Release|Mixed Platforms.Build.0 = Release|x86 49 | {A2D2A785-3B79-4042-9C3D-97B1CD9A804E}.Release|x64.ActiveCfg = Release|x64 50 | {A2D2A785-3B79-4042-9C3D-97B1CD9A804E}.Release|x86.ActiveCfg = Release|x86 51 | {A2D2A785-3B79-4042-9C3D-97B1CD9A804E}.Release|x86.Build.0 = Release|x86 52 | {5E156019-7D68-402D-9109-7F7F1E55EA8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 53 | {5E156019-7D68-402D-9109-7F7F1E55EA8A}.Debug|Any CPU.Build.0 = Debug|Any CPU 54 | {5E156019-7D68-402D-9109-7F7F1E55EA8A}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU 55 | {5E156019-7D68-402D-9109-7F7F1E55EA8A}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU 56 | {5E156019-7D68-402D-9109-7F7F1E55EA8A}.Debug|x64.ActiveCfg = Debug|Any CPU 57 | {5E156019-7D68-402D-9109-7F7F1E55EA8A}.Debug|x64.Build.0 = Debug|Any CPU 58 | {5E156019-7D68-402D-9109-7F7F1E55EA8A}.Debug|x86.ActiveCfg = Debug|Any CPU 59 | {5E156019-7D68-402D-9109-7F7F1E55EA8A}.Release|Any CPU.ActiveCfg = Release|Any CPU 60 | {5E156019-7D68-402D-9109-7F7F1E55EA8A}.Release|Any CPU.Build.0 = Release|Any CPU 61 | {5E156019-7D68-402D-9109-7F7F1E55EA8A}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU 62 | {5E156019-7D68-402D-9109-7F7F1E55EA8A}.Release|Mixed Platforms.Build.0 = Release|Any CPU 63 | {5E156019-7D68-402D-9109-7F7F1E55EA8A}.Release|x64.ActiveCfg = Release|Any CPU 64 | {5E156019-7D68-402D-9109-7F7F1E55EA8A}.Release|x86.ActiveCfg = Release|Any CPU 65 | {084B8F8C-1E32-4545-B865-FA7274BD8054}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 66 | {084B8F8C-1E32-4545-B865-FA7274BD8054}.Debug|Any CPU.Build.0 = Debug|Any CPU 67 | {084B8F8C-1E32-4545-B865-FA7274BD8054}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU 68 | {084B8F8C-1E32-4545-B865-FA7274BD8054}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU 69 | {084B8F8C-1E32-4545-B865-FA7274BD8054}.Debug|x64.ActiveCfg = Debug|Any CPU 70 | {084B8F8C-1E32-4545-B865-FA7274BD8054}.Debug|x64.Build.0 = Debug|Any CPU 71 | {084B8F8C-1E32-4545-B865-FA7274BD8054}.Debug|x86.ActiveCfg = Debug|Any CPU 72 | {084B8F8C-1E32-4545-B865-FA7274BD8054}.Debug|x86.Build.0 = Debug|Any CPU 73 | {084B8F8C-1E32-4545-B865-FA7274BD8054}.Release|Any CPU.ActiveCfg = Release|Any CPU 74 | {084B8F8C-1E32-4545-B865-FA7274BD8054}.Release|Any CPU.Build.0 = Release|Any CPU 75 | {084B8F8C-1E32-4545-B865-FA7274BD8054}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU 76 | {084B8F8C-1E32-4545-B865-FA7274BD8054}.Release|Mixed Platforms.Build.0 = Release|Any CPU 77 | {084B8F8C-1E32-4545-B865-FA7274BD8054}.Release|x64.ActiveCfg = Release|Any CPU 78 | {084B8F8C-1E32-4545-B865-FA7274BD8054}.Release|x86.ActiveCfg = Release|Any CPU 79 | {084B8F8C-1E32-4545-B865-FA7274BD8054}.Release|x86.Build.0 = Release|Any CPU 80 | {8DA31553-28E9-4A08-897E-7EE1E01356A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 81 | {8DA31553-28E9-4A08-897E-7EE1E01356A3}.Debug|Any CPU.Build.0 = Debug|Any CPU 82 | {8DA31553-28E9-4A08-897E-7EE1E01356A3}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU 83 | {8DA31553-28E9-4A08-897E-7EE1E01356A3}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU 84 | {8DA31553-28E9-4A08-897E-7EE1E01356A3}.Debug|x64.ActiveCfg = Debug|Any CPU 85 | {8DA31553-28E9-4A08-897E-7EE1E01356A3}.Debug|x64.Build.0 = Debug|Any CPU 86 | {8DA31553-28E9-4A08-897E-7EE1E01356A3}.Debug|x86.ActiveCfg = Debug|Any CPU 87 | {8DA31553-28E9-4A08-897E-7EE1E01356A3}.Release|Any CPU.ActiveCfg = Release|Any CPU 88 | {8DA31553-28E9-4A08-897E-7EE1E01356A3}.Release|Any CPU.Build.0 = Release|Any CPU 89 | {8DA31553-28E9-4A08-897E-7EE1E01356A3}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU 90 | {8DA31553-28E9-4A08-897E-7EE1E01356A3}.Release|Mixed Platforms.Build.0 = Release|Any CPU 91 | {8DA31553-28E9-4A08-897E-7EE1E01356A3}.Release|x64.ActiveCfg = Release|Any CPU 92 | {8DA31553-28E9-4A08-897E-7EE1E01356A3}.Release|x86.ActiveCfg = Release|Any CPU 93 | {80D26E64-EECF-47F2-8132-804F087C27BA}.Debug|Any CPU.ActiveCfg = Debug|x86 94 | {80D26E64-EECF-47F2-8132-804F087C27BA}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 95 | {80D26E64-EECF-47F2-8132-804F087C27BA}.Debug|x64.ActiveCfg = Debug|x86 96 | {80D26E64-EECF-47F2-8132-804F087C27BA}.Debug|x86.ActiveCfg = Debug|x86 97 | {80D26E64-EECF-47F2-8132-804F087C27BA}.Debug|x86.Build.0 = Debug|x86 98 | {80D26E64-EECF-47F2-8132-804F087C27BA}.Release|Any CPU.ActiveCfg = Release|x86 99 | {80D26E64-EECF-47F2-8132-804F087C27BA}.Release|Mixed Platforms.ActiveCfg = Release|x86 100 | {80D26E64-EECF-47F2-8132-804F087C27BA}.Release|x64.ActiveCfg = Release|x86 101 | {80D26E64-EECF-47F2-8132-804F087C27BA}.Release|x86.ActiveCfg = Release|x86 102 | {80D26E64-EECF-47F2-8132-804F087C27BA}.Release|x86.Build.0 = Release|x86 103 | EndGlobalSection 104 | GlobalSection(SolutionProperties) = preSolution 105 | HideSolutionNode = FALSE 106 | EndGlobalSection 107 | GlobalSection(NestedProjects) = preSolution 108 | {BDB28E84-D1D5-4102-BD29-C638197EFD42} = {E5A3FCB6-A888-4290-B076-864674F90508} 109 | EndGlobalSection 110 | GlobalSection(ExtensibilityGlobals) = postSolution 111 | SolutionGuid = {CA73B252-DD83-404C-A0A4-B6FF0862370F} 112 | EndGlobalSection 113 | EndGlobal 114 | -------------------------------------------------------------------------------- /FSEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FreeSWITCH.Native; 3 | 4 | 5 | 6 | namespace FSClient { 7 | public class FSEvent : EventArgs { 8 | private switch_event evt; 9 | public FSEvent(switch_event evt) { 10 | this.evt = evt; 11 | } 12 | public string get_header(string name) { 13 | return freeswitch.switch_event_get_header_idx(evt, name, -1); 14 | //for (var x = evt.headers; x != null; x = x.next){ 15 | // if (name.ToLower() == x.name.ToLower()) 16 | // return x.value; 17 | //} 18 | //return null; 19 | } 20 | public string body { get { return evt.body; } } 21 | public int flags { get { return evt.flags; } } 22 | public uint key { get { return evt.key; } } 23 | public string owner { get { return evt.owner; } } 24 | public string subclass_name { get { return evt.subclass_name; } } 25 | public switch_event_types_t event_id { get { return evt.event_id; } } 26 | public switch_event_header first_header { get { return evt.headers; } } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Field.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | namespace FSClient { 5 | public class SettingsField { 6 | public String name { get; set; } 7 | public String value { get; set; } 8 | public SettingsField() { 9 | } 10 | public SettingsField(FieldValue fv) { 11 | name = fv.field.name; 12 | value = fv.value; 13 | } 14 | } 15 | public class Field { 16 | public class FieldOption { 17 | public string display_value; 18 | public string value; 19 | public override string ToString() { 20 | return display_value; 21 | } 22 | public static FieldOption GetByValue(FieldOption[] options, String value) { 23 | return (from o in options where o.value == value select o).SingleOrDefault(); 24 | } 25 | } 26 | public enum FIELD_TYPE { String, Int, Bool, Combo, MultiItem, Password, MultiItemSort }; 27 | public FIELD_TYPE type; 28 | public delegate string validate_field_del(String value); 29 | public validate_field_del Validator; 30 | public static Field GetByName(Field[] fields, string name) { 31 | return (from f in fields where f.name == name select f).SingleOrDefault(); 32 | } 33 | public Field(FIELD_TYPE type, string display_name, string name, string xml_name, string default_value, string category) { 34 | this.type = type; 35 | this.display_name = display_name; 36 | this.name = name; 37 | this.default_value = default_value; 38 | this.category = category; 39 | this.xml_name = xml_name; 40 | } 41 | public Field(FIELD_TYPE type, string display_name, string name, string xml_name, string default_value, string category, params string[] option_values) { 42 | this.type = type; 43 | this.display_name = display_name; 44 | this.name = name; 45 | this.default_value = default_value; 46 | this.category = category; 47 | this.xml_name = xml_name; 48 | foreach (string value in option_values) 49 | AddOption(value); 50 | } 51 | public Field(FIELD_TYPE type, string display_name, string name, string xml_name, string default_value, string category, params FieldOption[] option_values) { 52 | this.type = type; 53 | this.display_name = display_name; 54 | this.name = name; 55 | this.default_value = default_value; 56 | this.category = category; 57 | this.xml_name = xml_name; 58 | foreach (FieldOption value in option_values) 59 | _options.Add(value); 60 | } 61 | public FieldValue GetDefaultValue() { 62 | return new FieldValue { field = this, value = default_value }; 63 | } 64 | public string display_name; 65 | public string name; 66 | public string xml_name; 67 | public string category; 68 | public string default_value; 69 | private List _options = new List(); 70 | public FieldOption[] options { 71 | get { 72 | return _options.ToArray(); 73 | } 74 | } 75 | public void AddOption(string value) { 76 | AddOption(value, value); 77 | } 78 | public void AddOption(string value, string display_value) { 79 | _options.Add(new FieldOption { display_value = display_value, value = value }); 80 | } 81 | 82 | } 83 | public class FieldValue : ObservableClass { 84 | public Field field; 85 | 86 | public string value { 87 | get { return _value; } 88 | set { 89 | if (value == _value) 90 | return; 91 | _value = value; 92 | RaisePropertyChanged("value"); 93 | } 94 | } 95 | private string _value; 96 | 97 | public override string ToString() { 98 | return value; 99 | } 100 | public static FieldValue[] FieldValues(Field[] fields) { 101 | FieldValue[] ret = (from f in fields select f.GetDefaultValue()).ToArray(); 102 | return ret; 103 | } 104 | public static void SetValues(FieldValue[] values, params string[] name_value_pairs) { 105 | for (int i = 0; i < name_value_pairs.Length; i += 2) 106 | GetByName(values, name_value_pairs[i]).value = name_value_pairs[i + 1]; 107 | } 108 | public static FieldValue GetByName(FieldValue[] values, string name) { 109 | return (from v in values where v.field.name == name select v).SingleOrDefault(); 110 | } 111 | public static FieldValue GetByDisplayName(FieldValue[] values, string name) { 112 | return (from v in values where v.field.display_name == name select v).SingleOrDefault(); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /GenericEditor.xaml: -------------------------------------------------------------------------------- 1 |  8 | 9 | 10 | 11 | 12 | 13 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |