/* * Term.cs * * This is free software. See COPYING for details. */ using System; using System.Collections; using System.Text; using Mono.Unix; using Gtk; using Gdk; namespace FSpot.Query { public abstract class Term { private ArrayList sub_terms = new ArrayList (); private Term parent = null; private string separator; protected bool is_negated = false; protected Tag tag = null; public Term (Term parent, Literal after) { this.parent = parent; if (parent != null) { if (after == null) parent.Add (this); else parent.SubTerms.Insert (parent.SubTerms.IndexOf (after) + 1, this); } } /** Properties **/ public bool HasMultiple { get { return (SubTerms.Count > 1); } } public ArrayList SubTerms { get { return sub_terms; } } public Term Last { get { // Return the last Literal in this term if (SubTerms.Count > 0) return SubTerms[SubTerms.Count - 1] as Term; else return null; } } public int Count { get { return SubTerms.Count; } } public Term Parent { get { return parent; } set { if (parent == value) return; // If our parent was already set, remove ourself from it if (parent != null) parent.Remove(this); // Add ourself to our new parent parent = value; parent.Add(this); } } public virtual bool IsNegated { get { return is_negated; } set { if (is_negated != value) Invert(false); is_negated = value; } } /** Methods **/ public void Add (Term term) { SubTerms.Add (term); } public void Remove (Term term) { SubTerms.Remove (term); // Remove ourselves if we're now empty if (SubTerms.Count == 0) if (Parent != null) Parent.Remove (this); } public void CopyAndInvertSubTermsFrom (Term term, bool recurse) { is_negated = true; ArrayList termsToMove = new ArrayList(term.SubTerms); foreach (Term subterm in termsToMove) { if (recurse) subterm.Invert(true).Parent = this; else subterm.Parent = this; } } public ArrayList FindByTag (Tag t) { return FindByTag (t, true); } public ArrayList FindByTag (Tag t, bool recursive) { ArrayList results = new ArrayList (); if (tag != null && tag == t) results.Add (this); if (recursive) foreach (Term term in SubTerms) results.AddRange (term.FindByTag (t, true)); else foreach (Term term in SubTerms) { foreach (Term literal in SubTerms) { if (literal.tag != null && literal.tag == t) { results.Add (literal); } } if (term.tag != null && term.tag == t) { results.Add (term); } } return results; } public ArrayList LiteralParents () { ArrayList results = new ArrayList (); bool meme = false; foreach (Term term in SubTerms) { if (term is Literal) meme = true; results.AddRange (term.LiteralParents ()); } if (meme) results.Add (this); return results; } public bool TagIncluded(Tag t) { ArrayList parents = LiteralParents (); if (parents.Count == 0) return false; foreach (Term term in parents) { bool termHasTag = false; bool onlyTerm = true; foreach (Term literal in term.SubTerms) { if (literal.tag != null) { if (literal.tag == t) { termHasTag = true; } else { onlyTerm = false; } } } if (termHasTag && onlyTerm) return true; } return false; } public bool TagRequired(Tag t) { int count, grouped_with; return TagRequired(t, out count, out grouped_with); } public bool TagRequired(Tag t, out int num_terms, out int grouped_with) { ArrayList parents = LiteralParents (); num_terms = 0; grouped_with = 100; int min_grouped_with = 100; if (parents.Count == 0) return false; foreach (Term term in parents) { bool termHasTag = false; // Don't count it as required if it's the only subterm..though it is.. // it is more clearly identified as Included at that point. if (term.Count > 1) { foreach (Term literal in term.SubTerms) { if (literal.tag != null) { if (literal.tag == t) { num_terms++; termHasTag = true; grouped_with = term.SubTerms.Count; break; } } } } if (grouped_with < min_grouped_with) min_grouped_with = grouped_with; if (!termHasTag) return false; } grouped_with = min_grouped_with; return true; } public abstract Term Invert(bool recurse); // Recursively generate the SQL condition clause that this // term represents. public virtual string SqlCondition () { StringBuilder condition = new StringBuilder ("("); for (int i = 0; i < SubTerms.Count; i++) { Term term = SubTerms[i] as Term; condition.Append (term.SqlCondition ()); if (i != SubTerms.Count - 1) condition.Append (SQLOperator ()); } condition.Append(")"); return condition.ToString (); } public virtual Gtk.Widget SeparatorWidget () { return null; } public virtual string SQLOperator () { return String.Empty; } protected static Hashtable op_term_lookup = new Hashtable(); public static Term TermFromOperator (string op, Term parent, Literal after) { //Console.WriteLine ("finding type for operator {0}", op); //op = op.Trim (); op = op.ToLower (); if (AndTerm.Operators.Contains (op)) { //Console.WriteLine ("AND!"); return new AndTerm (parent, after); } else if (OrTerm.Operators.Contains (op)) { //Console.WriteLine ("OR!"); return new OrTerm (parent, after); } Console.WriteLine ("Do not have Term for operator {0}", op); return null; } } public class AndTerm : Term { static ArrayList operators = new ArrayList (); static AndTerm () { operators.Add (Catalog.GetString (" and ")); //operators.Add (Catalog.GetString (" && ")); operators.Add (Catalog.GetString (", ")); } public static ArrayList Operators { get { return operators; } } public AndTerm (Term parent, Literal after) : base (parent, after) {} public override Term Invert (bool recurse) { OrTerm newme = new OrTerm(Parent, null); newme.CopyAndInvertSubTermsFrom(this, recurse); if (Parent != null) Parent.Remove(this); return newme; } public override Widget SeparatorWidget () { Widget sep = new Label (String.Empty); sep.SetSizeRequest (3, 1); sep.Show (); return sep; //return null; } public override string SqlCondition () { StringBuilder condition = new StringBuilder ("("); condition.Append (base.SqlCondition()); Tag hidden = Core.Database.Tags.Hidden; if (hidden != null) { if (FindByTag (hidden, true).Count == 0) { condition.Append (String.Format ( " AND id NOT IN (SELECT photo_id FROM photo_tags WHERE tag_id = {0})", hidden.Id )); } } condition.Append (")"); return condition.ToString (); } public override string SQLOperator () { return " AND "; } } public class OrTerm : Term { static ArrayList operators = new ArrayList (); static OrTerm () { operators.Add (Catalog.GetString (" or ")); //operators.Add (Catalog.GetString (" || ")); } public static OrTerm FromTags(Tag [] from_tags) { if (from_tags == null || from_tags.Length == 0) return null; OrTerm or = new OrTerm(null, null); foreach (Tag t in from_tags) { Literal l = new Literal(t); l.Parent = or; } return or; } public static ArrayList Operators { get { return operators; } } public OrTerm (Term parent, Literal after) : base (parent, after) {} private static string OR = Catalog.GetString ("or"); public override Term Invert (bool recurse) { AndTerm newme = new AndTerm(Parent, null); newme.CopyAndInvertSubTermsFrom(this, recurse); if (Parent != null) Parent.Remove(this); return newme; } public override Gtk.Widget SeparatorWidget () { Widget label = new Label (" " + OR + " "); label.Show (); return label; } public override string SQLOperator () { return " OR "; } } public abstract class AbstractLiteral : Term { public AbstractLiteral(Term parent, Literal after) : base (parent, after) {} public override Term Invert (bool recurse) { is_negated = !is_negated; return this; } } // TODO rename to TagLiteral? public class Literal : AbstractLiteral { public Literal (Tag tag) : this (null, tag, null) { } public Literal (Term parent, Tag tag, Literal after) : base (parent, after) { this.tag = tag; } /** Properties **/ public static ArrayList FocusedLiterals { get { return focusedLiterals; } set { focusedLiterals = value; } } public static Tooltips Tips { set { tips = value; } } public Tag Tag { get { return tag; } } public override bool IsNegated { get { return is_negated; } set { if (is_negated == value) return; is_negated = value; NormalIcon = null; NegatedIcon = null; Update (); if (NegatedToggled != null) NegatedToggled (this); } } private Pixbuf NegatedIcon { get { if (negated_icon != null) return negated_icon; if (NormalIcon == null) return null; negated_icon = NormalIcon.Copy (); int offset = ICON_SIZE - overlay_size; NegatedOverlay.Composite (negated_icon, offset, 0, overlay_size, overlay_size, offset, 0, 1.0, 1.0, InterpType.Bilinear, 200); return negated_icon; } set { negated_icon = null; } } public Widget Widget { get { if (widget != null) return widget; container = new EventBox (); box = new HBox (); handle_box = new LiteralBox (); handle_box.BorderWidth = 1; label = new Label (tag.Name); label.UseMarkup = true; image = new Gtk.Image (NormalIcon); container.CanFocus = true; container.KeyPressEvent += KeyHandler; container.ButtonPressEvent += HandleButtonPress; container.ButtonReleaseEvent += HandleButtonRelease; container.EnterNotifyEvent += HandleMouseIn; container.LeaveNotifyEvent += HandleMouseOut; //new PopupManager (new LiteralPopup (container, this)); // Setup this widget as a drag source (so tags can be moved after being placed) container.DragDataGet += HandleDragDataGet; container.DragBegin += HandleDragBegin; container.DragEnd += HandleDragEnd; Gtk.Drag.SourceSet (container, Gdk.ModifierType.Button1Mask | Gdk.ModifierType.Button3Mask, tag_target_table, DragAction.Copy | DragAction.Move); // Setup this widget as a drag destination (so tags can be added to our parent's Term) container.DragDataReceived += HandleDragDataReceived; container.DragMotion += HandleDragMotion; container.DragLeave += HandleDragLeave; Gtk.Drag.DestSet (container, DestDefaults.All, tag_dest_target_table, DragAction.Copy | DragAction.Move ); tips.SetTip (container, tag.Name, null); label.Show (); image.Show (); if (tag.Icon == null) { handle_box.Add (label); } else { handle_box.Add (image); } handle_box.Show (); box.Add (handle_box); box.Show (); container.Add (box); widget = container; return widget; } } private Pixbuf NormalIcon { get { if (normal_icon != null) return normal_icon; Pixbuf scaled = null; scaled = tag.Icon; for (Category category = tag.Category; category != null && scaled == null; category = category.Category) scaled = category.Icon; if (scaled == null) return null; if (scaled.Width != ICON_SIZE) { scaled = scaled.ScaleSimple (ICON_SIZE, ICON_SIZE, InterpType.Bilinear); } normal_icon = scaled; return normal_icon; } set { normal_icon = null; } } /** Methods **/ public void Update () { // Clear out the old icons normal_icon = null; negated_icon = null; if (IsNegated) { tips.SetTip (widget, String.Format (Catalog.GetString ("Not {0}"), tag.Name), null); label.Text = "" + tag.Name + ""; image.Pixbuf = NegatedIcon; } else { tips.SetTip (widget, tag.Name, null); label.Text = tag.Name; image.Pixbuf = NormalIcon; } label.UseMarkup = true; // Show the icon unless it's null if (tag.Icon == null && container.Children [0] == image) { container.Remove (image); container.Add (label); } else if (tag.Icon != null && container.Children [0] == label) { container.Remove (label); container.Add (image); } if (isHoveredOver && image.Pixbuf != null ) { // Brighten the image slightly Pixbuf brightened = image.Pixbuf.Copy (); image.Pixbuf.SaturateAndPixelate (brightened, 1.85f, false); //Pixbuf brightened = PixbufUtils.Glow (image.Pixbuf, .6f); image.Pixbuf = brightened; } } public void RemoveSelf () { if (Removing != null) Removing (this); if (Parent != null) Parent.Remove (this); if (Removed != null) Removed (this); } public override string SqlCondition () { StringBuilder ids = new StringBuilder (tag.Id.ToString ()); if (tag is Category) { ArrayList tags = new ArrayList (); (tag as Category).AddDescendentsTo (tags); for (int i = 0; i < tags.Count; i++) ids.Append (", " + (tags [i] as Tag).Id.ToString ()); } return String.Format ( "id {0}IN (SELECT photo_id FROM photo_tags WHERE tag_id IN ({1}))", (IsNegated ? "NOT " : String.Empty), ids.ToString ()); } public override Gtk.Widget SeparatorWidget () { return new Label ("ERR"); } private static Pixbuf NegatedOverlay { get { if (negated_overlay == null) { System.Reflection.Assembly assembly = System.Reflection.Assembly.GetCallingAssembly (); negated_overlay = new Pixbuf (assembly.GetManifestResourceStream ("f-spot-not.png")); negated_overlay = negated_overlay.ScaleSimple (overlay_size, overlay_size, InterpType.Bilinear); } return negated_overlay; } } public static void RemoveFocusedLiterals () { if (focusedLiterals != null) foreach (Literal literal in focusedLiterals) literal.RemoveSelf (); } /** Handlers **/ private void KeyHandler (object o, KeyPressEventArgs args) { args.RetVal = false; switch (args.Event.Key) { case Gdk.Key.Delete: RemoveFocusedLiterals (); args.RetVal = true; return; } } private void HandleButtonPress (object o, ButtonPressEventArgs args) { args.RetVal = true; switch (args.Event.Type) { case EventType.TwoButtonPress: if (args.Event.Button == 1) IsNegated = !IsNegated; else args.RetVal = false; return; case EventType.ButtonPress: Widget.GrabFocus (); if (args.Event.Button == 1) { // TODO allow multiple selection of literals so they can be deleted, modified all at once //if ((args.Event.State & ModifierType.ControlMask) != 0) { //} } else if (args.Event.Button == 3) { LiteralPopup popup = new LiteralPopup (); popup.Activate (args.Event, this); } return; default: args.RetVal = false; return; } } private void HandleButtonRelease (object o, ButtonReleaseEventArgs args) { args.RetVal = true; switch (args.Event.Type) { case EventType.TwoButtonPress: args.RetVal = false; return; case EventType.ButtonPress: if (args.Event.Button == 1) { } return; default: args.RetVal = false; return; } } private void HandleMouseIn (object o, EnterNotifyEventArgs args) { isHoveredOver = true; Update (); } private void HandleMouseOut (object o, LeaveNotifyEventArgs args) { isHoveredOver = false; Update (); } void HandleDragDataGet (object sender, DragDataGetArgs args) { args.RetVal = true; switch (args.Info) { case (uint) MainWindow.TargetType.TagList: case (uint) MainWindow.TargetType.TagQueryItem: Byte [] data = Encoding.UTF8.GetBytes (String.Empty); Atom [] targets = args.Context.Targets; args.SelectionData.Set (targets[0], 8, data, data.Length); return; default: // Drop cancelled args.RetVal = false; foreach (Widget w in hiddenWidgets) w.Visible = true; focusedLiterals = null; break; } } void HandleDragBegin (object sender, DragBeginArgs args) { Gtk.Drag.SetIconPixbuf (args.Context, image.Pixbuf, 0, 0); focusedLiterals.Add (this); // Hide the tag and any separators that only exist because of it container.Visible = false; hiddenWidgets.Add (container); foreach (Widget w in LogicWidget.Box.HangersOn (this)) { hiddenWidgets.Add (w); w.Visible = false; } } void HandleDragEnd (object sender, DragEndArgs args) { // Remove any literals still marked as focused, because // the user is throwing them away. RemoveFocusedLiterals (); focusedLiterals = new ArrayList(); args.RetVal = true; } private void HandleDragDataReceived (object o, EventArgs args) { // If focusedLiterals is not null, this is a drag of a tag that's already been placed if (focusedLiterals.Count == 0) { if (TermAdded != null) TermAdded (Parent, this); } else { if (! focusedLiterals.Contains(this)) if (LiteralsMoved != null) LiteralsMoved (focusedLiterals, Parent, this); // Unmark the literals as focused so they don't get nixed focusedLiterals = null; } } private bool preview = false; private Gtk.Widget preview_widget; private void HandleDragMotion (object o, DragMotionArgs args) { if (!preview) { if (preview_widget == null) { preview_widget = new Gtk.Label (" | "); box.Add (preview_widget); } preview_widget.Show (); } } private void HandleDragLeave (object o, EventArgs args) { preview = false; preview_widget.Hide (); } public void HandleToggleNegatedCommand (object o, EventArgs args) { IsNegated = !IsNegated; } public void HandleRemoveCommand (object o, EventArgs args) { RemoveSelf (); } public void HandleAttachTagCommand (Tag t) { if (AttachTag != null) AttachTag (t, Parent, this); } public void HandleRequireTag (object sender, EventArgs args) { if (RequireTag != null) RequireTag (new Tag [] {this.Tag}); } public void HandleUnRequireTag (object sender, EventArgs args) { if (UnRequireTag != null) UnRequireTag (new Tag [] {this.Tag}); } private const int ICON_SIZE = 24; private const int overlay_size = (int) (.40 * ICON_SIZE); private static TargetEntry [] tag_target_table = new TargetEntry [] { new TargetEntry ("application/x-fspot-tag-query-item", 0, (uint) MainWindow.TargetType.TagQueryItem), }; private static TargetEntry [] tag_dest_target_table = new TargetEntry [] { new TargetEntry ("application/x-fspot-tags", 0, (uint) MainWindow.TargetType.TagList), new TargetEntry ("application/x-fspot-tag-query-item", 0, (uint) MainWindow.TargetType.TagQueryItem), }; private static ArrayList focusedLiterals = new ArrayList(); private static ArrayList hiddenWidgets = new ArrayList(); private Gtk.Container container; private LiteralBox handle_box; private Gtk.Box box; private Gtk.Image image; private Gtk.Label label; private Pixbuf normal_icon; //private EventBox widget; private Widget widget; private Pixbuf negated_icon; private static Pixbuf negated_overlay; private static Tooltips tips; private bool isHoveredOver = false; public delegate void NegatedToggleHandler (Literal group); public event NegatedToggleHandler NegatedToggled; public delegate void RemovingHandler (Literal group); public event RemovingHandler Removing; public delegate void RemovedHandler (Literal group); public event RemovedHandler Removed; public delegate void TermAddedHandler (Term parent, Literal after); public event TermAddedHandler TermAdded; public delegate void AttachTagHandler (Tag tag, Term parent, Literal after); public event AttachTagHandler AttachTag; public delegate void TagRequiredHandler (Tag [] tags); public event TagRequiredHandler RequireTag; public delegate void TagUnRequiredHandler (Tag [] tags); public event TagUnRequiredHandler UnRequireTag; public delegate void LiteralsMovedHandler (ArrayList literals, Term parent, Literal after); public event LiteralsMovedHandler LiteralsMoved; } public class TextLiteral : AbstractLiteral { private string text; public TextLiteral (Term parent, string text) : base (parent, null) { this.text = text; } public override string SqlCondition () { return String.Format ( "id {0}IN (SELECT id FROM photos WHERE name LIKE '%{1}%' OR directory_path LIKE '%{1}%' OR description LIKE '%{1}%')", (IsNegated ? "NOT " : ""), EscapeQuotes(text) ); } protected static string EscapeQuotes (string v) { return v == null ? String.Empty : v.Replace("'", "''"); } } }