Oops, fix a broken part of the patch
[beagle.git] / Util / Conf.cs
blob49525043b574a5babd1351ce549790e03aef7cc7
1 //
2 // Conf.cs
3 //
4 // Copyright (C) 2005 Novell, Inc.
5 //
7 //
8 // Permission is hereby granted, free of charge, to any person obtaining a copy
9 // of this software and associated documentation files (the "Software"), to deal
10 // in the Software without restriction, including without limitation the rights
11 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 // copies of the Software, and to permit persons to whom the Software is
13 // furnished to do so, subject to the following conditions:
15 // The above copyright notice and this permission notice shall be included in all
16 // copies or substantial portions of the Software.
18 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24 // SOFTWARE.
27 using System;
28 using System.Collections;
29 using System.IO;
30 using System.Reflection;
31 using System.Xml.Serialization;
32 using System.Text.RegularExpressions;
34 using Beagle.Util;
36 namespace Beagle.Util {
38 public class Conf {
40 // No instantiation
41 private Conf () { }
43 public static Hashtable Sections;
45 public static IndexingConfig Indexing = null;
46 public static DaemonConfig Daemon = null;
47 public static SearchingConfig Searching = null;
49 //#if ENABLE_WEBSERVICES
50 public static NetworkingConfig Networking = null;
51 public static WebServicesConfig WebServices = null;
52 //#endif
53 private static string configs_dir;
54 private static Hashtable mtimes;
55 private static Hashtable subscriptions;
57 private static bool watching_for_updates;
58 private static bool update_watch_present;
60 private static BindingFlags method_search_flags = BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod;
62 public delegate void ConfigUpdateHandler (Section section);
64 static Conf ()
66 Sections = new Hashtable (3);
67 mtimes = new Hashtable (3);
68 subscriptions = new Hashtable (3);
70 configs_dir = Path.Combine (PathFinder.StorageDir, "config");
71 if (!Directory.Exists (configs_dir))
72 Directory.CreateDirectory (configs_dir);
74 Conf.Load ();
77 public static void WatchForUpdates ()
79 // Make sure we don't try and watch for updates more than once
80 if (update_watch_present)
81 return;
83 if (Inotify.Enabled) {
84 Inotify.Subscribe (configs_dir, OnInotifyEvent, Inotify.EventType.Create | Inotify.EventType.CloseWrite);
85 } else {
86 // Poll for updates every 60 secs
87 GLib.Timeout.Add (60000, new GLib.TimeoutHandler (CheckForUpdates));
90 update_watch_present = true;
93 private static void OnInotifyEvent (Inotify.Watch watch, string path, string subitem, string srcpath, Inotify.EventType type)
95 if (subitem == "" || watching_for_updates == false)
96 return;
98 Load ();
101 private static bool CheckForUpdates ()
103 if (watching_for_updates)
104 Load ();
105 return true;
108 public static void Subscribe (Type type, ConfigUpdateHandler callback)
110 if (!subscriptions.ContainsKey (type))
111 subscriptions.Add (type, new ArrayList (1));
113 ArrayList callbacks = (ArrayList) subscriptions [type];
114 callbacks.Add (callback);
117 private static void NotifySubscribers (Section section)
119 Type type = section.GetType ();
120 ArrayList callbacks = (ArrayList) subscriptions [type];
122 if (callbacks == null)
123 return;
125 foreach (ConfigUpdateHandler callback in callbacks)
126 callback (section);
129 public static void Load ()
131 Load (false);
134 public static void Load (bool force)
136 Section temp;
138 // FIXME: Yeah
139 LoadFile (typeof (IndexingConfig), Indexing, out temp, force);
140 Indexing = (IndexingConfig) temp;
141 NotifySubscribers (Indexing);
143 LoadFile (typeof (DaemonConfig), Daemon, out temp, force);
144 Daemon = (DaemonConfig) temp;
145 NotifySubscribers (Daemon);
147 LoadFile (typeof (SearchingConfig), Searching, out temp, force);
148 Searching = (SearchingConfig) temp;
149 NotifySubscribers (Searching);
151 //#if ENABLE_WEBSERVICES
152 LoadFile (typeof (NetworkingConfig), Networking, out temp, force);
153 Networking = (NetworkingConfig) temp;
154 NotifySubscribers (Networking);
156 LoadFile (typeof (WebServicesConfig), WebServices, out temp, force);
157 WebServices = (WebServicesConfig) temp;
158 NotifySubscribers (WebServices);
159 //#endif
161 watching_for_updates = true;
164 public static void Save ()
166 Save (false);
169 public static void Save (bool force)
171 foreach (Section section in Sections.Values)
172 if (force || section.SaveNeeded)
173 SaveFile (section);
176 private static bool LoadFile (Type type, Section current, out Section section, bool force)
178 section = current;
179 object [] attrs = Attribute.GetCustomAttributes (type, typeof (ConfigSection));
180 if (attrs.Length == 0)
181 throw new ConfigException ("Could not find ConfigSection attribute on " + type);
183 string sectionname = ((ConfigSection) attrs [0]).Name;
184 string filename = sectionname + ".xml";
185 string filepath = Path.Combine (configs_dir, filename);
186 if (!File.Exists (filepath)) {
187 if (current == null)
188 ConstructDefaultSection (type, sectionname, out section);
189 return false;
192 if (!force && current != null && mtimes.ContainsKey (sectionname) &&
193 File.GetLastWriteTimeUtc (filepath).CompareTo ((DateTime) mtimes [sectionname]) <= 0)
194 return false;
196 Logger.Log.Debug ("Loading {0} from {1}", type, filename);
197 FileStream fs = null;
199 try {
200 fs = File.Open (filepath, FileMode.Open, FileAccess.Read, FileShare.Read);
201 XmlSerializer serializer = new XmlSerializer (type);
202 section = (Section) serializer.Deserialize (fs);
203 } catch (Exception e) {
204 Logger.Log.Error ("Could not load configuration from {0}: {1}", filename, e.Message);
205 if (fs != null)
206 fs.Close ();
207 if (current == null)
208 ConstructDefaultSection (type, sectionname, out section);
209 return false;
212 fs.Close ();
213 Sections.Remove (sectionname);
214 Sections.Add (sectionname, section);
215 mtimes.Remove (sectionname);
216 mtimes.Add (sectionname, File.GetLastWriteTimeUtc (filepath));
217 return true;
220 private static bool SaveFile (Section section)
222 Type type = section.GetType ();
223 object [] attrs = Attribute.GetCustomAttributes (type, typeof (ConfigSection));
224 if (attrs.Length == 0)
225 throw new ConfigException ("Could not find ConfigSection attribute on " + type);
227 string sectionname = ((ConfigSection) attrs [0]).Name;
228 string filename = sectionname + ".xml";
229 string filepath = Path.Combine (configs_dir, filename);
231 Logger.Log.Debug ("Saving {0} to {1}", type, filename);
232 FileStream fs = null;
234 try {
235 watching_for_updates = false;
236 fs = new FileStream (filepath, FileMode.Create);
237 XmlSerializer serializer = new XmlSerializer (type);
238 XmlFu.SerializeUtf8 (serializer, fs, section);
239 } catch (Exception e) {
240 if (fs != null)
241 fs.Close ();
242 Logger.Log.Error ("Could not save configuration to {0}: {1}", filename, e);
243 watching_for_updates = true;
244 return false;
247 fs.Close ();
248 mtimes.Remove (sectionname);
249 mtimes.Add (sectionname, File.GetLastWriteTimeUtc (filepath));
250 watching_for_updates = true;
251 return true;
254 private static void ConstructDefaultSection (Type type, string sectionname, out Section section)
256 ConstructorInfo ctor = type.GetConstructor (Type.EmptyTypes);
257 section = (Section) ctor.Invoke (null);
258 Sections.Remove (sectionname);
259 Sections.Add (sectionname, section);
262 // Lists all config file options in a hash table where key is option name,
263 // and value is description.
264 public static Hashtable GetOptions (Section section)
266 Hashtable options = new Hashtable ();
267 MemberInfo [] members = section.GetType ().GetMembers (method_search_flags);
269 // Find all of the methods ("options") inside the specified section
270 // object which have the ConfigOption attribute.
271 foreach (MemberInfo member in members) {
272 object [] attrs = member.GetCustomAttributes (typeof (ConfigOption), false);
273 if (attrs.Length > 0)
274 options.Add (member.Name, ((ConfigOption) attrs [0]).Description);
277 return options;
280 public static bool InvokeOption (Section section, string option, string [] args, out string output)
282 MethodInfo method = section.GetType ().GetMethod (option, method_search_flags);
283 if (method == null) {
284 string msg = String.Format ("No such method '{0}' for section '{1}'", option, section);
285 throw new ConfigException(msg);
287 object [] attrs = method.GetCustomAttributes (typeof (ConfigOption), false);
288 if (attrs.Length == 0) {
289 string msg = String.Format ("Method '{0}' is not a configurable option", option);
290 throw new ConfigException (msg);
293 // Check the required number of parameters have been provided
294 ConfigOption attr = (ConfigOption) attrs [0];
295 if (attr.Params > 0 && args.Length < attr.Params) {
296 string msg = String.Format ("Option '{0}' requires {1} parameter(s): {2}", option, attr.Params, attr.ParamsDescription);
297 throw new ConfigException (msg);
300 object [] methodparams = { null, args };
301 bool result = (bool) method.Invoke (section, methodparams);
302 output = (string) methodparams [0];
304 // Mark the section as save-needed if we just changed something
305 if (result && attr.IsMutator)
306 section.SaveNeeded = true;
308 return result;
311 [ConfigSection (Name="searching")]
312 public class SearchingConfig : Section {
314 private bool autostart = true;
315 public bool Autostart {
316 get { return autostart; }
317 set { autostart = value; }
320 private KeyBinding show_search_window_binding = new KeyBinding ("F12");
321 public KeyBinding ShowSearchWindowBinding {
322 get { return show_search_window_binding; }
323 set { show_search_window_binding = value; }
326 private int max_displayed = 5;
327 public int MaxDisplayed {
328 get { return max_displayed; }
329 set {
330 if (value <= 0)
331 max_displayed = 1;
332 else
333 max_displayed = value;
337 // Best window position and dimension
338 // stored as percentage of screen co-ordinates
339 // to deal with change of resolution problem - hints from tberman
341 private float best_pos_x = 0;
342 public float BestPosX {
343 get { return best_pos_x; }
344 set { best_pos_x = value; }
347 private float best_pos_y = 0;
348 public float BestPosY {
349 get { return best_pos_y; }
350 set { best_pos_y = value; }
353 // dont explicitly set height on first run
354 private float best_width = 0;
355 public float BestWidth {
356 get { return best_width; }
357 set { best_width = value; }
360 private float best_height = 0; // -ditto-
361 public float BestHeight {
362 get { return best_height; }
363 set { best_height = value; }
366 // FIXME Change names when Holmes is released under new name
367 // Holmes window position and dimension
369 private float holmes_pos_x = 0;
370 public float HolmesPosX {
371 get { return holmes_pos_x; }
372 set { holmes_pos_x = value; }
375 private float holmes_pos_y = 0;
376 public float HolmesPosY {
377 get { return holmes_pos_y; }
378 set { holmes_pos_y = value; }
381 private float holmes_width = 0;
382 public float HolmesWidth {
383 get { return holmes_width; }
384 set { holmes_width = value; }
387 private float holmes_height = 0;
388 public float HolmesHeight {
389 get { return holmes_height; }
390 set { holmes_height = value; }
393 // ah!We want a Queue but Queue doesnt serialize *easily*
394 private ArrayList search_history = new ArrayList ();
395 public ArrayList SearchHistory {
396 get { return search_history; }
397 set { search_history = value; }
402 [ConfigSection (Name="daemon")]
403 public class DaemonConfig : Section {
404 private ArrayList static_queryables = new ArrayList ();
405 public ArrayList StaticQueryables {
406 get { return static_queryables; }
407 set { static_queryables = value; }
410 private ArrayList allowed_backends = new ArrayList ();
411 public ArrayList AllowedBackends {
412 get { return allowed_backends; }
413 set { allowed_backends = value; }
416 private ArrayList denied_backends = new ArrayList ();
417 public ArrayList DeniedBackends {
418 get { return denied_backends; }
419 set { denied_backends = value; }
422 private bool index_synchronization = true;
423 public bool IndexSynchronization {
424 get { return index_synchronization; }
425 // Don't really want to expose this, but serialization requires it
426 set { index_synchronization = value; }
429 private bool allow_root = false;
430 public bool AllowRoot {
431 get { return allow_root; }
432 set { allow_root = value; }
435 [ConfigOption (Description="Add a static queryable", Params=1, ParamsDescription="Index path")]
436 internal bool AddStaticQueryable (out string output, string [] args)
438 static_queryables.Add (args [0]);
439 output = "Static queryable added.";
440 return true;
443 [ConfigOption (Description="Remove a static queryable", Params=1, ParamsDescription="Index path")]
444 internal bool DelStaticQueryable (out string output, string [] args)
446 static_queryables.Remove (args [0]);
447 output = "Static queryable removed.";
448 return true;
451 [ConfigOption (Description="List user-specified static queryables", IsMutator=false)]
452 internal bool ListStaticQueryables (out string output, string [] args)
454 output = "User-specified static queryables:\n";
455 foreach (string index_path in static_queryables)
456 output += String.Format (" - {0}\n", index_path);
457 return true;
460 [ConfigOption (Description="Toggles whether your indexes will be synchronized locally if your home directory is on a network device (eg. NFS/Samba)")]
461 internal bool ToggleIndexSynchronization (out string output, string [] args)
463 index_synchronization = !index_synchronization;
464 output = "Index Synchronization is " + ((index_synchronization) ? "enabled" : "disabled") + ".";
465 return true;
468 [ConfigOption (Description="Toggles whether Beagle can be run as root")]
469 internal bool ToggleAllowRoot (out string output, string [] args)
471 allow_root = ! allow_root;
472 if (allow_root)
473 output = "Beagle is now permitted to run as root";
474 else
475 output = "Beagle is no longer permitted to run as root";
476 return true;
480 [ConfigSection (Name="indexing")]
481 public class IndexingConfig : Section
483 private ArrayList roots = new ArrayList ();
484 [XmlArray]
485 [XmlArrayItem(ElementName="Root", Type=typeof(string))]
486 public ArrayList Roots {
487 get { return roots; }
488 set { roots = value; }
491 private bool index_home_dir = true;
492 public bool IndexHomeDir {
493 get { return index_home_dir; }
494 set { index_home_dir = value; }
497 private ArrayList excludes = new ArrayList ();
498 [XmlArray]
499 [XmlArrayItem (ElementName="ExcludeItem", Type=typeof(ExcludeItem))]
500 public ArrayList Excludes {
501 get { return excludes; }
502 set { excludes = value; }
505 [ConfigOption (Description="List the indexing roots", IsMutator=false)]
506 internal bool ListRoots (out string output, string [] args)
508 output = "Current roots:\n";
509 if (this.index_home_dir == true)
510 output += " - Your home directory\n";
511 foreach (string root in roots)
512 output += " - " + root + "\n";
514 return true;
517 [ConfigOption (Description="Toggles whether your home directory is to be indexed as a root")]
518 internal bool IndexHome (out string output, string [] args)
520 if (index_home_dir)
521 output = "Your home directory will not be indexed.";
522 else
523 output = "Your home directory will be indexed.";
524 index_home_dir = !index_home_dir;
525 return true;
528 [ConfigOption (Description="Add a root path to be indexed", Params=1, ParamsDescription="A path")]
529 internal bool AddRoot (out string output, string [] args)
531 roots.Add (args [0]);
532 output = "Root added.";
533 return true;
536 [ConfigOption (Description="Remove an indexing root", Params=1, ParamsDescription="A path")]
537 internal bool DelRoot (out string output, string [] args)
539 roots.Remove (args [0]);
540 output = "Root removed.";
541 return true;
544 [ConfigOption (Description="List user-specified resources to be excluded from indexing", IsMutator=false)]
545 internal bool ListExcludes (out string output, string [] args)
547 output = "User-specified resources to be excluded from indexing:\n";
548 foreach (ExcludeItem exclude_item in excludes)
549 output += String.Format (" - [{0}] {1}\n", exclude_item.Type.ToString (), exclude_item.Value);
550 return true;
553 [ConfigOption (Description="Add a resource to exclude from indexing", Params=2, ParamsDescription="A type [path/pattern/mailfolder], a path/pattern/name")]
554 internal bool AddExclude (out string output, string [] args)
556 ExcludeType type;
557 try {
558 type = (ExcludeType) Enum.Parse (typeof (ExcludeType), args [0], true);
559 } catch (Exception e) {
560 output = String.Format("Invalid type '{0}'. Valid types: Path, Pattern, MailFolder", args [0]);
561 return false;
564 excludes.Add (new ExcludeItem (type, args [1]));
565 output = "Exclude added.";
566 return true;
569 [ConfigOption (Description="Remove an excluded resource", Params=2, ParamsDescription="A type [path/pattern/mailfolder], a path/pattern/name")]
570 internal bool DelExclude (out string output, string [] args)
572 ExcludeType type;
573 try {
574 type = (ExcludeType) Enum.Parse (typeof (ExcludeType), args [0], true);
575 } catch (Exception e) {
576 output = String.Format("Invalid type '{0}'. Valid types: Path, Pattern, MailFolder", args [0]);
577 return false;
580 foreach (ExcludeItem item in excludes) {
581 if (item.Type != type || item.Value != args [1])
582 continue;
583 excludes.Remove (item);
584 output = "Exclude removed.";
585 return true;
588 output = "Could not find requested exclude to remove.";
589 return false;
594 //#if ENABLE_WEBSERVICES
595 [ConfigSection (Name="webservices")]
596 public class WebServicesConfig: Section
598 private ArrayList publicFolders = new ArrayList ();
599 [XmlArray]
600 [XmlArrayItem(ElementName="PublicFolders", Type=typeof(string))]
601 public ArrayList PublicFolders {
602 get { return publicFolders; }
603 set { publicFolders = value; }
606 private bool allowGlobalAccess = true;
607 public bool AllowGlobalAccess {
608 get { return allowGlobalAccess; }
609 set { allowGlobalAccess = value; }
612 [ConfigOption (Description="List the public folders", IsMutator=false)]
613 internal bool ListPublicFolders(out string output, string [] args)
615 output = "Current list of public folders:\n";
617 foreach (string pf in publicFolders)
618 output += " - " + pf + "\n";
620 return true;
623 [ConfigOption (Description="Check current configuration of global access to Beagle web-services", IsMutator=false)]
624 internal bool CheckGlobalAccess(out string output, string [] args)
626 if (allowGlobalAccess)
627 output = "Global Access to Beagle WebServices is currently ENABLED.";
628 else
629 output = "Global Access to Beagle WebServices is currently DISABLED.";
631 return true;
634 [ConfigOption (Description="Enable/Disable global access to Beagle web-services")]
635 internal bool SwitchGlobalAccess (out string output, string [] args)
637 allowGlobalAccess = !allowGlobalAccess;
639 if (allowGlobalAccess)
640 output = "Global Access to Beagle WebServices now ENABLED.";
641 else
642 output = "Global Access to Beagle WebServices now DISABLED.";
644 return true;
647 [ConfigOption (Description="Add public web-service access to a folder", Params=1, ParamsDescription="A path")]
648 internal bool AddPublicFolder (out string output, string [] args)
650 publicFolders.Add (args [0]);
651 output = "PublicFolder " + args[0] + " added.";
652 return true;
655 [ConfigOption (Description="Remove public web-service access to a folder", Params=1, ParamsDescription="A path")]
656 internal bool DelPublicFolder (out string output, string [] args)
658 publicFolders.Remove (args [0]);
659 output = "PublicFolder " + args[0] + " removed.";
660 return true;
664 [ConfigSection (Name="networking")]
665 public class NetworkingConfig: Section
667 private ArrayList netBeagleNodes = new ArrayList ();
669 [XmlArray]
670 [XmlArrayItem(ElementName="NetBeagleNodes", Type=typeof(string))]
671 public ArrayList NetBeagleNodes {
672 get { return netBeagleNodes; }
673 set { netBeagleNodes = value; }
676 [ConfigOption (Description="List Networked Beagle Daemons to query", IsMutator=false)]
677 internal bool ListBeagleNodes (out string output, string [] args)
679 output = "Current list of Networked Beagle Daemons to query:\n";
681 foreach (string nb in netBeagleNodes)
682 output += " - " + nb + "\n";
684 return true;
687 [ConfigOption (Description="Add a Networked Beagle Daemon to query", Params=1, ParamsDescription="HostName:PortNo")]
688 internal bool AddBeagleNode (out string output, string [] args)
690 string node = args[0];
692 if (((string[])node.Split(':')).Length < 2)
693 node = args [0].Trim() + ":8888";
695 netBeagleNodes.Add(node);
696 output = "Networked Beagle Daemon \"" + node +"\" added.";
697 return true;
700 [ConfigOption (Description="Remove a configured Networked Beagle Daemon", Params=1, ParamsDescription="HostName:PortNo")]
701 internal bool DelBeagleNode (out string output, string [] args)
703 string node = args[0];
705 if (((string[])node.Split(':')).Length < 2)
706 node = args [0].Trim() + ":8888";
708 netBeagleNodes.Remove(node);
709 output = "Networked Beagle Daemon \"" + node +"\" removed.";
710 return true;
713 //#endif
715 public class Section {
716 [XmlIgnore]
717 public bool SaveNeeded = false;
720 private class ConfigOption : Attribute {
721 public string Description;
722 public int Params;
723 public string ParamsDescription;
724 public bool IsMutator = true;
727 private class ConfigSection : Attribute {
728 public string Name;
731 public class ConfigException : Exception {
732 public ConfigException (string msg) : base (msg) { }
737 //////////////////////////////////////////////////////////////////////
739 public enum ExcludeType {
740 Path,
741 Pattern,
742 MailFolder
745 public class ExcludeItem {
747 private ExcludeType type;
748 private string val;
750 [XmlAttribute]
751 public ExcludeType Type {
752 get { return type; }
753 set { type = value; }
756 private string exactMatch;
757 private string prefix;
758 private string suffix;
759 private Regex regex;
761 [XmlAttribute]
762 public string Value {
763 get { return val; }
764 set {
765 switch (type) {
766 case ExcludeType.Path:
767 case ExcludeType.MailFolder:
768 prefix = value;
769 break;
771 case ExcludeType.Pattern:
772 if (value.StartsWith ("/") && value.EndsWith ("/")) {
773 regex = new Regex (value.Substring (1, value.Length - 2));
774 break;
777 int i = value.IndexOf ('*');
778 if (i == -1) {
779 exactMatch = value;
780 } else {
781 if (i > 0)
782 prefix = value.Substring (0, i);
783 if (i < value.Length-1)
784 suffix = value.Substring (i+1);
786 break;
789 val = value;
793 public ExcludeItem () {}
795 public ExcludeItem (ExcludeType type, string value) {
796 this.Type = type;
797 this.Value = value;
800 public bool IsMatch (string param)
802 switch (Type) {
803 case ExcludeType.Path:
804 case ExcludeType.MailFolder:
805 if (prefix != null && ! param.StartsWith (prefix))
806 return false;
808 return true;
810 case ExcludeType.Pattern:
811 if (exactMatch != null)
812 return param == exactMatch;
813 if (prefix != null && ! param.StartsWith (prefix))
814 return false;
815 if (suffix != null && ! param.EndsWith (suffix))
816 return false;
817 if (regex != null && ! regex.IsMatch (param))
818 return false;
820 return true;
823 return false;
826 public override bool Equals (object obj)
828 ExcludeItem exclude = obj as ExcludeItem;
829 return (exclude != null && exclude.Type == type && exclude.Value == val);
832 public override int GetHashCode ()
834 return (this.Value.GetHashCode () ^ (int) this.Type);
839 //////////////////////////////////////////////////////////////////////
841 public class KeyBinding {
842 public string Key;
844 [XmlAttribute]
845 public bool Ctrl = false;
846 [XmlAttribute]
847 public bool Alt = false;
849 public KeyBinding () {}
850 public KeyBinding (string key) : this (key, false, false) {}
852 public KeyBinding (string key, bool ctrl, bool alt)
854 Key = key;
855 Ctrl = ctrl;
856 Alt = alt;
859 public override string ToString ()
861 string result = "";
863 if (Ctrl)
864 result += "<Ctrl>";
865 if (Alt)
866 result += "<Alt>";
868 result += Key;
870 return result;
873 public string ToReadableString ()
875 return ToString ().Replace (">", "-").Replace ("<", "");