/* * Widgets/FindBar.cs * * Author(s) * Gabriel Burt * Stephane Delcroix * * This is free software. See COPYING for details. */ using System; using System.Collections; using System.Text.RegularExpressions; using Gtk; using Gdk; using Mono.Unix; using FSpot.Query; namespace FSpot.Widgets { public class HighlightedBox : EventBox { private bool changing_style = false; public HighlightedBox(Widget child) : base() { Child = child; AppPaintable = true; } protected override void OnStyleSet(Style style) { if (!changing_style) { changing_style = true; ModifyBg(StateType.Normal, Style.Background(StateType.Selected)); changing_style = false; } } protected override bool OnExposeEvent(Gdk.EventExpose evnt) { GdkWindow.DrawRectangle(Style.ForegroundGC(StateType.Normal), false, 0, 0, Allocation.Width - 1, Allocation.Height - 1); return base.OnExposeEvent(evnt); } } public class FindBar : HighlightedBox { private Entry entry; private string last_entry_text = String.Empty; private int open_parens = 0, close_parens = 0; private PhotoQuery query; private Term root_term = null; private HBox box; /* * Properties */ public bool Completing { get { return (entry.Completion as LogicEntryCompletion).Completing; } } public Gtk.Entry Entry { get { return entry; } } public Term RootTerm { get { return root_term; } } /* * Constructor */ public FindBar (PhotoQuery query, TreeModel model) : base(new HBox()) { this.query = query; box = Child as HBox; box.Spacing = 6; box.BorderWidth = 2; box.PackStart (new Label (Catalog.GetString ("Find:")), false, false, 0); entry = new Entry (); entry.Completion = new LogicEntryCompletion (entry, model); entry.TextInserted += HandleEntryTextInserted; entry.TextDeleted += HandleEntryTextDeleted; entry.KeyPressEvent += HandleEntryKeyPress; box.PackStart (entry, true, true, 0); Button clear_button = new Gtk.Button (); clear_button.Add (new Gtk.Image ("gtk-close", Gtk.IconSize.Button)); clear_button.Clicked += HandleCloseButtonClicked; clear_button.Relief = Gtk.ReliefStyle.None; box.PackStart (clear_button, false, false, 0); } /* * Event Handlers */ private void HandleCloseButtonClicked (object sender, EventArgs args) { Clear (); } private void HandleEntryTextInserted (object sender, TextInsertedArgs args) { //Console.WriteLine ("inserting {0}, ( = {1} ) = {2}", args.Text, open_parens, close_parens); //int start = args.Position - args.Length; for (int i = 0; i < args.Text.Length; i++) { char c = args.Text [i]; if (c == '(') open_parens++; else if (c == ')') close_parens++; } int pos = entry.Position + 1; int close_parens_needed = open_parens - close_parens; for (int i = 0; i < close_parens_needed; i++) { entry.TextInserted -= HandleEntryTextInserted; entry.InsertText (")", ref pos); close_parens++; entry.TextInserted += HandleEntryTextInserted; pos++; } //Console.WriteLine ("done w/ insert, {0}, ( = {1} ) = {2}", args.Text, open_parens, close_parens); last_entry_text = entry.Text; QueueUpdate (); } private void HandleEntryTextDeleted (object sender, TextDeletedArgs args) { int length = args.EndPos - args.StartPos; //Console.WriteLine ("start {0} end {1} len {2} last {3}", args.StartPos, args.EndPos, length, last_entry_text); string txt = last_entry_text.Substring (args.StartPos, length); for (int i = 0; i < txt.Length; i++) { if (txt [i] == '(') open_parens--; else if (txt [i] == ')') close_parens--; } last_entry_text = entry.Text; QueueUpdate (); } private void HandleEntryKeyPress (object sender, KeyPressEventArgs args) { //bool shift = ModifierType.ShiftMask == (args.Event.State & ModifierType.ShiftMask); switch (args.Event.Key) { case (Gdk.Key.Escape): Clear (); args.RetVal = true; break; case (Gdk.Key.Tab): // If we are at the end of the entry box, let the normal Tab handler do its job if (entry.Position == entry.Text.Length) { args.RetVal = false; return; } // Go until the current character is an open paren while (entry.Position < entry.Text.Length && entry.Text [entry.Position] != '(') entry.Position++; // Put the cursor right after the open paren entry.Position++; args.RetVal = true; break; default: args.RetVal = false; break; } } /* * Helper methods. */ private void Clear () { entry.Text = String.Empty; Hide (); } // OPS The operators we support, case insensitive //private static string op_str = "(?'Ops' or | and |, | \\s+ )"; private static string op_str = "(?'Ops' or | and |, )"; // Match literals, eg tags or other text to search on private static string literal_str = "[^{0}{1}]+?"; //private static string not_literal_str = "not\\s*\\((?'NotTag'[^{0}{1}]+)\\)"; // Match a group surrounded by parenthesis and one or more terms separated by operators private static string term_str = "(((?'Open'{0})(?'Pre'[^{0}{1}]*?))+((?'Close-Open'{1})(?'Post'[^{0}{1}]*?))+)*?(?(Open)(?!))"; // Match a group surrounded by parenthesis and one or more terms separated by operators, surrounded by not() //private static string not_term_str = String.Format("not\\s*(?'NotTerm'{0})", term_str); // Match a simple term or a group term or a not(group term) //private static string comb_term_str = String.Format ("(?'Term'{0}|{2}|{1})", simple_term_str, term_str, not_term_str); private static string comb_term_str = String.Format ("(?'Term'{0}|{1})|not\\s*\\((?'NotTerm'{0})\\)|not\\s*(?'NotTerm'{1})", literal_str, term_str); // Match a single term or a set of terms separated by operators private static string regex_str = String.Format ("^((?'Terms'{0}){1})*(?'Terms'{0})$", comb_term_str, op_str); private static Regex term_regex = new Regex ( String.Format (regex_str, "\\(", "\\)"), RegexOptions.IgnoreCase | RegexOptions.Compiled ); // Debug method used to print results to verify the regex private static void PrintGroup (GroupCollection groups, string name) { Group group = groups [name]; Console.WriteLine ("Name: {2} (success = {1}) group: {0}", group, group.Success, name); foreach (Capture capture in group.Captures) { Console.WriteLine (" Have capture: {0}", capture); } } // Breaking the query the user typed into something useful involves running // it through the above regular expression recursively until it is broken down // into literals and operators that we can use to generate SQL queries. private bool ConstructQuery (Term parent, int depth, string txt) { return ConstructQuery(parent, depth, txt, false); } private bool ConstructQuery (Term parent, int depth, string txt, bool negated) { if (txt == null || txt.Length == 0) return true; string indent = String.Format ("{0," + depth*2 + "}", " "); //Console.WriteLine (indent + "Have text: {0}", txt); // Match the query the user typed against our regular expression Match match = term_regex.Match (txt); if (!match.Success) { //Console.WriteLine (indent + "Failed to match."); return false; } bool op_valid = true; string op = String.Empty; // For the moment at least we don't support operator precedence, so we require // that only a single operator is used for any given term unless it is made unambiguous // by using parenthesis. foreach (Capture capture in match.Groups ["Ops"].Captures) { if (op == String.Empty) op = capture.Value; else if (op != capture.Value) { op_valid = false; break; } } if (!op_valid) { Console.WriteLine (indent + "Ambiguous operator sequence. Use parenthesis to explicitly define evaluation order."); return false; } if (match.Groups ["Terms"].Captures.Count == 1 && match.Groups["NotTerm"].Captures.Count != 1) { //Console.WriteLine (indent + "Unbreakable term: {0}", match.Groups ["Terms"].Captures [0]); string literal; bool is_negated = false; Tag tag = null; if (match.Groups ["NotTag"].Captures.Count == 1) { literal = match.Groups ["NotTag"].Captures [0].Value; is_negated = true; } else { literal = match.Groups ["Terms"].Captures [0].Value; } is_negated = is_negated || negated; tag = MainWindow.Toplevel.Database.Tags.GetTagByName (literal); // New OR term so we can match against both tag and text search parent = new OrTerm(parent, null); // If the literal is the name of a tag, include it in the OR AbstractLiteral term = null; if (tag != null) { new Literal (parent, tag, null); } // Always include the literal text in the search (path, comment, etc) new TextLiteral (parent, literal); // If the term was negated, negate the OR parent term if (is_negated) { parent = parent.Invert(true); } if (RootTerm == null) root_term = parent; return true; } else { Term us = null; if (op != null && op != String.Empty) { us = Term.TermFromOperator (op, parent, null); if (RootTerm == null) root_term = us; } foreach (Capture capture in match.Groups ["Term"].Captures) { string subterm = capture.Value.Trim (); if (subterm == null || subterm.Length == 0) continue; // Strip leading/trailing parens if (subterm [0] == '(' && subterm [subterm.Length - 1] == ')') { subterm = subterm.Remove (subterm.Length - 1, 1); subterm = subterm.Remove (0, 1); } //Console.WriteLine (indent + "Breaking subterm apart: {0}", subterm); if (!ConstructQuery (us, depth + 1, subterm, negated)) return false; } foreach (Capture capture in match.Groups ["NotTerm"].Captures) { string subterm = capture.Value.Trim (); if (subterm == null || subterm.Length == 0) continue; // Strip leading/trailing parens if (subterm [0] == '(' && subterm [subterm.Length - 1] == ')') { subterm = subterm.Remove (subterm.Length - 1, 1); subterm = subterm.Remove (0, 1); } //Console.WriteLine (indent + "Breaking not subterm apart: {0}", subterm); if (!ConstructQuery (us, depth + 1, subterm, true)) return false; } if (negated && us != null) { if (us == RootTerm) root_term = us.Invert(false); else us.Invert(false); } return true; } } private bool updating = false; private uint update_timeout_id = 0; private void QueueUpdate () { if (updating || update_timeout_id != 0) { lock(this) { // If there is a timer set and we are not yet handling its timeout, then remove the timer // so we delay its trigger for another 500ms. if (!updating && update_timeout_id != 0) GLib.Source.Remove (update_timeout_id); // Assuming we're not currently handling a timeout, add a new timer if (!updating) update_timeout_id = GLib.Timeout.Add(500, OnUpdateTimer); } } else { // If we are not updating and there isn't a timer already set, then there is // no risk of race condition with the timeout handler. update_timeout_id = GLib.Timeout.Add(500, OnUpdateTimer); } } private bool OnUpdateTimer () { lock(this) { updating = true; } Update(); lock(this) { updating = false; update_timeout_id = 0; } return false; } private void Update () { // Clear the last root term root_term = null; if (ParensValid () && ConstructQuery (null, 0, entry.Text)) { if (RootTerm != null) { //Console.WriteLine("rootTerm = {0}", RootTerm); if (!(RootTerm is AndTerm)) { // A little hacky, here to make sure the root term is a AndTerm which will // ensure we handle the Hidden tag properly AndTerm root_parent = new AndTerm(null, null); RootTerm.Parent = root_parent; root_term = root_parent; } //Console.WriteLine("rootTerm = {0}", RootTerm); if (!(RootTerm is AndTerm)) { // A little hacky, here to make sure the root term is a AndTerm which will // ensure we handle the Hidden tag properly AndTerm root_parent = new AndTerm(null, null); RootTerm.Parent = root_parent; root_term = root_parent; } //Console.WriteLine ("condition = {0}", RootTerm.SqlCondition ()); query.ExtraCondition = RootTerm.SqlCondition (); } else { query.ExtraCondition = null; //Console.WriteLine ("root term is null"); } } } private bool ParensValid () { for (int i = 0; i < entry.Text.Length; i++) { if (entry.Text [i] == '(' || entry.Text [i] == ')') { int pair_pos = ParenPairPosition (entry.Text, i); if (pair_pos == -1) return false; } } return true; } /* * Static Utility Methods */ private static int ParenPairPosition (string txt, int pos) { char one = txt [pos]; bool open = (one == '('); char two = (open) ? ')' : '('; //int level = 0; int num = (open) ? txt.Length - pos - 1: pos; int sames = 0; for (int i = 0; i < num; i++) { if (open) pos++; else pos--; if (pos < 0 || pos > txt.Length - 1) return -1; if (txt [pos] == one) sames++; else if (txt [pos] == two) { if (sames == 0) return pos; else sames--; } } return -1; } /*private static string ReverseString (string txt) { char [] txt_a = txt.ToCharArray (); System.Reverse (txt_a); return new String (txt_a); }*/ } public class LogicEntryCompletion : EntryCompletion { private Entry entry; private bool completing = false; public bool Completing { get { return completing; } } public LogicEntryCompletion (Entry entry, TreeModel tree_model) { this.entry = entry; Model = new DependentListStore(tree_model); InlineCompletion = false; MinimumKeyLength = 1; TextColumn = 1; PopupSetWidth = false; MatchFunc = LogicEntryCompletionMatchFunc; MatchSelected += HandleMatchSelected; // Insert these when appropriate.. //InsertActionText (0, "or"); //InsertActionText (1, "and"); // HandleAction... } [GLib.ConnectBefore] private void HandleMatchSelected (object sender, MatchSelectedArgs args) { // Delete what the user had entered in case their casing wasn't the same entry.DeleteText (entry.Position - transformed_key.Length, entry.Position); string name = args.Model.GetValue (args.Iter, TextColumn) as string; //Console.WriteLine ("match selected..{0}", name); int pos = entry.Position; completing = true; entry.InsertText (name, ref pos); completing = false; entry.Position += name.Length; args.RetVal = true; //Console.WriteLine ("done w/ match selected"); } string last_key = String.Empty; string transformed_key = String.Empty; public bool LogicEntryCompletionMatchFunc (EntryCompletion completion, string key, TreeIter iter) { if (Completing) return false; // If this is the fist comparison for this key, convert the key (which is the entire search string) // into just the part that is relevant to completing this tag name. if (key != last_key) { last_key = key; int pos = entry.Position - 1; if (key == null || key.Length == 0 || pos < 0 || pos > key.Length - 1) transformed_key = String.Empty; else if (key [pos] == '(' || key [pos] == ')' || key [pos] == ',') transformed_key = String.Empty; else { int start = 0; for (int i = entry.Position - 1; i >= 0; i--) { if (key [i] == ' ' || key [i] == ')' || key [i] == '(') { //Console.WriteLine ("have start break char at {0}", i); start = i + 1; break; } } int end = key.Length - 1; for (int j = entry.Position - 1; j < key.Length; j++) { if (key [j] == ' ' || key [j] == ')' || key [j] == '(') { end = j - 1; break; } } //Console.WriteLine ("start = {0} end = {1}", start, end); int len = end - start + 1; if (len > 0 && start < last_key.Length) transformed_key = last_key.Substring (start, end - start + 1); else transformed_key = String.Empty; } //Console.WriteLine ("transformed key {0} into {1}", key, transformed_key); } if (transformed_key == String.Empty) return false; string name = completion.Model.GetValue (iter, completion.TextColumn) as string; // Ignore null or names that are too short if (name == null || name.Length <= transformed_key.Length) return false; //Console.WriteLine ("entered = {0} compared to {1}", transformed_key, name); return (String.Compare (transformed_key, name.Substring(0, transformed_key.Length), true) == 0); } } }