using Gtk; using System; using System.Runtime.InteropServices; using Cairo; using Mono.Unix; using FSpot.Widgets; namespace FSpot { public class Sharpener : Loupe { Gtk.SpinButton amount_spin = new Gtk.SpinButton (0.5, 100.0, .01); Gtk.SpinButton radius_spin = new Gtk.SpinButton (5.0, 50.0, .01); Gtk.SpinButton threshold_spin = new Gtk.SpinButton (0.0, 50.0, .01); Gtk.Dialog dialog; public Sharpener (PhotoImageView view) : base (view) { } protected override void UpdateSample () { base.UpdateSample (); if (overlay != null) overlay.Dispose (); overlay = null; if (source != null) overlay = PixbufUtils.UnsharpMask (source, radius_spin.Value, amount_spin.Value, threshold_spin.Value); } private void HandleSettingsChanged (object sender, EventArgs args) { UpdateSample (); } private void HandleOkClicked (object sender, EventArgs args) { Photo photo = view.Item.Current as Photo; if (photo == null) return; try { Gdk.Pixbuf orig = view.Pixbuf; Gdk.Pixbuf final = PixbufUtils.UnsharpMask (orig, radius_spin.Value, amount_spin.Value, threshold_spin.Value); bool create_version = photo.DefaultVersionId == Photo.OriginalVersionId; photo.SaveVersion (final, create_version); } catch (System.Exception e) { string msg = Catalog.GetString ("Error saving sharpened photo"); string desc = String.Format (Catalog.GetString ("Received exception \"{0}\". Unable to save photo {1}"), e.Message, photo.Name); HigMessageDialog md = new HigMessageDialog (this, DialogFlags.DestroyWithParent, Gtk.MessageType.Error, ButtonsType.Ok, msg, desc); md.Run (); md.Destroy (); } Destroy (); } public void HandleCancelClicked (object sender, EventArgs args) { Destroy (); } public void HandleLoupeDestroyed (object sender, EventArgs args) { dialog.Destroy (); } protected override void BuildUI () { base.BuildUI (); string title = Catalog.GetString ("Sharpen"); dialog = new Gtk.Dialog (title, (Gtk.Window) this, DialogFlags.DestroyWithParent, new object [0]); dialog.BorderWidth = 12; dialog.VBox.Spacing = 6; Gtk.Table table = new Gtk.Table (3, 2, false); table.ColumnSpacing = 6; table.RowSpacing = 6; table.Attach (SetFancyStyle (new Gtk.Label (Catalog.GetString ("Amount:"))), 0, 1, 0, 1); table.Attach (SetFancyStyle (new Gtk.Label (Catalog.GetString ("Radius:"))), 0, 1, 1, 2); table.Attach (SetFancyStyle (new Gtk.Label (Catalog.GetString ("Threshold:"))), 0, 1, 2, 3); SetFancyStyle (amount_spin = new Gtk.SpinButton (0.00, 100.0, .01)); SetFancyStyle (radius_spin = new Gtk.SpinButton (1.0, 50.0, .01)); SetFancyStyle (threshold_spin = new Gtk.SpinButton (0.0, 50.0, .01)); amount_spin.Value = .5; radius_spin.Value = 5; threshold_spin.Value = 0.0; amount_spin.ValueChanged += HandleSettingsChanged; radius_spin.ValueChanged += HandleSettingsChanged; threshold_spin.ValueChanged += HandleSettingsChanged; table.Attach (amount_spin, 1, 2, 0, 1); table.Attach (radius_spin, 1, 2, 1, 2); table.Attach (threshold_spin, 1, 2, 2, 3); Gtk.Button cancel_button = new Gtk.Button (Gtk.Stock.Cancel); cancel_button.Clicked += HandleCancelClicked; dialog.AddActionWidget (cancel_button, Gtk.ResponseType.Cancel); Gtk.Button ok_button = new Gtk.Button (Gtk.Stock.Ok); ok_button.Clicked += HandleOkClicked; dialog.AddActionWidget (ok_button, Gtk.ResponseType.Cancel); Destroyed += HandleLoupeDestroyed; table.ShowAll (); dialog.VBox.PackStart (table); dialog.ShowAll (); } } public class Loupe : Gtk.Window { protected PhotoImageView view; protected Gdk.Rectangle region; bool use_shape_ext = false; protected Gdk.Pixbuf source; protected Gdk.Pixbuf overlay; private int radius = 128; private int inner = 128; private int border = 6; private double angle = Math.PI / 4; Gdk.Point start; Gdk.Point start_hot; Gdk.Point pos_hot; Gdk.Point hotspot; public Loupe (PhotoImageView view) : base ("Loupe") { this.view = view; Decorated = false; Gtk.Window win = (Gtk.Window) view.Toplevel; win.GetPosition (out old_win_pos.X, out old_win_pos.Y); win.ConfigureEvent += HandleToplevelConfigure; TransientFor = win; DestroyWithParent = true; BuildUI (); } Gdk.Point old_win_pos; [GLib.ConnectBefore] public void HandleToplevelConfigure (object o, ConfigureEventArgs args) { int x, y; int loupe_x, loupe_y; x = args.Event.X - old_win_pos.X; y = args.Event.Y - old_win_pos.Y; GetPosition (out loupe_x, out loupe_y); Move (loupe_x + x, loupe_y + y); old_win_pos.X = args.Event.X; old_win_pos.Y = args.Event.Y; } // FIXME //screen "composited-changed" public int Radius { get { return radius; } set { if (radius != value) { radius = value; UpdateSample (); } } } public int Border { get { return border; } set { if (border != value) { border = value; UpdateSample (); } } } public double Angle { get { return angle; } set { Gdk.Point then = hotspot; angle = value; Layout (); Gdk.Point now = hotspot; //System.Console.WriteLine ("{0} now {1}", then, now); int x, y; GdkWindow.GetOrigin (out x, out y); //GdkWindow.MoveResize (x + then.X - now.X, y + then.Y - now.Y, Bounds.Width, Bounds.Height); ShapeWindow (); Move (x + then.X - now.X, y + then.Y - now.Y); //QueueResize (); } } protected override void OnSizeAllocated (Gdk.Rectangle allocation) { if (IsRealized) ShapeWindow (); base.OnSizeAllocated (allocation); } protected override void OnRealized () { use_shape_ext = ! (CompositeUtils.IsComposited (Screen) && CompositeUtils.SetRgbaColormap (this)); base.OnRealized (); ShapeWindow (); } public void SetSamplePoint (Gdk.Point p) { region.X = p.X; region.Y = p.Y; region.Width = 2 * radius; region.Height = 2 * radius; if (view.Pixbuf != null) { Gdk.Pixbuf pixbuf = view.Pixbuf; region.Offset (- Math.Min (region.X, Math.Max (region.Right - pixbuf.Width, radius)), - Math.Min (region.Y, Math.Max (region.Bottom - pixbuf.Height, radius))); region.Intersect (new Gdk.Rectangle (0, 0, pixbuf.Width, pixbuf.Height)); } UpdateSample (); } protected virtual void UpdateSample () { if (source != null) source.Dispose (); source = null; if (view.Pixbuf == null) return; int small = (int) (radius * view.Zoom); if (small != inner) { inner = small; QueueResize (); } source = new Gdk.Pixbuf (view.Pixbuf, region.X, region.Y, region.Width, region.Height); //FIXME sometimes that ctor returns results with a null //handle this case ourselves if (source.Handle == IntPtr.Zero) source = null; this.QueueDraw (); } [GLib.ConnectBefore] private void HandleImageViewMotion (object sender, MotionNotifyEventArgs args) { Gdk.Point coords; coords = new Gdk.Point ((int) args.Event.X, (int) args.Event.Y); SetSamplePoint (view.WindowCoordsToImage (coords)); } private void ShapeWindow () { Layout (); Gdk.Pixmap bitmap = new Gdk.Pixmap (GdkWindow, Allocation.Width, Allocation.Height, 1); Context g = CairoUtils.CreateContext (bitmap); DrawShape (g, Allocation.Width, Allocation.Height); ((IDisposable)g).Dispose (); if (use_shape_ext) ShapeCombineMask (bitmap, 0, 0); else { Context rgba = CairoUtils.CreateContext (GdkWindow); DrawShape (rgba, Allocation.Width, Allocation.Height); ((IDisposable)rgba).Dispose (); try { CompositeUtils.InputShapeCombineMask (this, bitmap, 0,0); } catch (EntryPointNotFoundException) { System.Console.WriteLine ("Warning: gtk+ version doesn't support input shapping"); } } bitmap.Dispose (); } Gdk.Point Center; Requisition Bounds; public void Layout () { double a = radius + border; double b = inner + border; double x_proj = (a + b - border) * Math.Cos (angle); double y_proj = (a + b - border) * Math.Sin (angle); Center.X = (int) Math.Ceiling (Math.Max (-x_proj + b, a)); Center.Y = (int) Math.Ceiling (Math.Max (-y_proj + b, a)); Bounds.Width = (int) Math.Ceiling (Math.Max (Math.Abs (x_proj) + b, a) + b + a); Bounds.Height = (int) Math.Ceiling (Math.Max (Math.Abs (y_proj) + b, a) + b + a); hotspot.X = (int) Math.Ceiling (Center.X + x_proj); hotspot.Y = (int) Math.Ceiling (Center.Y + y_proj); } private void DrawShape (Cairo.Context g, int width, int height) { int inner_x = radius + border + inner; int cx = Center.X; int cy = Center.Y; g.Operator = Operator.Source; g.Source = new SolidPattern (new Cairo.Color (0,0,0,0)); g.Rectangle (0, 0, width, height); g.Paint (); g.NewPath (); g.Translate (cx, cy); g.Rotate (angle); g.Source = new SolidPattern (new Cairo.Color (0.2, 0.2, 0.2, .6)); g.Operator = Operator.Over; g.Rectangle (0, - (border + inner), inner_x, 2 * (border + inner)); g.Arc (inner_x, 0, inner + border, 0, 2 * Math.PI); g.Arc (0, 0, radius + border, 0, 2 * Math.PI); g.Fill (); g.Source = new SolidPattern (new Cairo.Color (0, 0, 0, 1.0)); g.Operator = Operator.DestOut; g.Arc (inner_x, 0, inner, 0, 2 * Math.PI); #if true g.Fill (); #else g.FillPreserve (); g.Operator = Operator.Over; RadialGradient rg = new RadialGradient (inner_x - (inner * 0.3), inner * 0.3 , inner * 0.1, inner_x, 0, inner); rg.AddColorStop (0, new Cairo.Color (0.0, 0.2, .8, 0.5)); rg.AddColorStop (0.7, new Cairo.Color (0.0, 0.2, .8, 0.1)); rg.AddColorStop (1.0, new Cairo.Color (0.0, 0.0, 0.0, 0.0)); g.Source = rg; g.Fill (); rg.Destroy (); #endif g.Operator = Operator.Over; g.Matrix = new Matrix (); g.Translate (cx, cy); if (source != null) SetSourcePixbuf (g, source, -source.Width / 2, -source.Height / 2); g.Arc (0, 0, radius, 0, 2 * Math.PI); g.Fill (); if (overlay != null) { SetSourcePixbuf (g, overlay, -overlay.Width / 2, -overlay.Height / 2); g.Arc (0, 0, radius, angle, angle + Math.PI); g.ClosePath (); g.FillPreserve (); g.Source = new SolidPattern (new Cairo.Color (1.0, 1.0, 1.0, 1.0)); g.Stroke (); } } protected override bool OnExposeEvent (Gdk.EventExpose args) { Context g = CairoUtils.CreateContext (GdkWindow); DrawShape (g, Allocation.Width, Allocation.Height); //base.OnExposeEvent (args); ((IDisposable)g).Dispose (); return false; } bool dragging = false; bool rotate = false; Delay drag; Gdk.Point pos; double start_angle = 0; Gdk.Point root_pos; Gdk.Point start_root; private void HandleMotionNotifyEvent (object sender, MotionNotifyEventArgs args) { pos.X = (int) args.Event.XRoot - start.X; pos.Y = (int) args.Event.YRoot - start.Y; root_pos.X = (int) args.Event.XRoot; root_pos.Y = (int) args.Event.YRoot; if (dragging) drag.Start (); } private bool DragUpdate () { if (!dragging) return false; if (!rotate) { return MoveWindow (); } else { Gdk.Point initial = start_root; Gdk.Point hot = start_hot; Gdk.Point win = Gdk.Point.Zero; hot.X += win.X; hot.Y += win.Y; initial.X -= hot.X; initial.Y -= hot.Y; Gdk.Point now = root_pos; now.X -= hot.X; now.Y -= hot.Y; Vector v1 = new Vector (initial); Vector v2 = new Vector (now); double angle = Vector.AngleBetween (v1, v2); Angle = start_angle + angle; return false; } } private bool MoveWindow () { Gdk.Point view_coords; Gdk.Point top; Gdk.Point current; GdkWindow.GetOrigin (out current.X, out current.Y); if (current == pos) return false; Move (pos.X, pos.Y); pos.Offset (hotspot.X, hotspot.Y); Gtk.Window toplevel = (Gtk.Window) view.Toplevel; toplevel.GdkWindow.GetOrigin (out top.X, out top.Y); toplevel.TranslateCoordinates (view, pos.X - top.X, pos.Y - top.Y, out view_coords.X, out view_coords.Y); SetSamplePoint (view.WindowCoordsToImage (view_coords)); return false; } private void HandleItemChanged (BrowsablePointer pointer, BrowsablePointerChangedArgs args) { UpdateSample (); } private void HandleButtonPressEvent (object sender, ButtonPressEventArgs args) { switch (args.Event.Type) { case Gdk.EventType.ButtonPress: if (args.Event.Button == 1) { start = new Gdk.Point ((int)args.Event.X, (int)args.Event.Y); start_root = new Gdk.Point ((int)args.Event.XRoot, (int)args.Event.YRoot); start_hot = hotspot; Gdk.Point win; GdkWindow.GetOrigin (out win.X, out win.Y); start_hot.X += win.X; start_hot.Y += win.Y; dragging = true; rotate = (args.Event.State & Gdk.ModifierType.ShiftMask) > 0; start_angle = Angle; } else { Angle += Math.PI /8; } break; case Gdk.EventType.TwoButtonPress: dragging = false; this.Destroy (); break; } } private void HandleViewZoomChanged (object sender, System.EventArgs args) { UpdateSample (); } private void HandleButtonReleaseEvent (object sender, ButtonReleaseEventArgs args) { dragging = false; } private void HandleKeyPressEvent (object sender, Gtk.KeyPressEventArgs args) { switch (args.Event.Key) { case Gdk.Key.v: Destroy (); args.RetVal = true; break; default: break; } return; } protected override void OnDestroyed () { view.MotionNotifyEvent -= HandleImageViewMotion; view.Item.Changed -= HandleItemChanged; view.ZoomChanged -= HandleViewZoomChanged; base.OnDestroyed (); } protected Widget SetFancyStyle (Widget widget) { //widget.ModifyFg (Gtk.StateType.Normal, new Gdk.Color (127, 127, 127)); //widget.ModifyBg (Gtk.StateType.Normal, new Gdk.Color (0, 0, 0)); return widget; } [DllImport ("libcairo-2.dll")] static extern void cairo_user_to_device (IntPtr cr, ref double x, ref double y); static void UserToDevice (Context ctx, ref double x, ref double y) { cairo_user_to_device (ctx.Handle, ref x, ref y); } [DllImport("libgdk-2.0-0.dll")] extern static void gdk_cairo_set_source_pixbuf (IntPtr handle, IntPtr pixbuf, double pixbuf_x, double pixbuf_y); static void SetSourcePixbuf (Context ctx, Gdk.Pixbuf pixbuf, double x, double y) { gdk_cairo_set_source_pixbuf (ctx.Handle, pixbuf.Handle, x, y); } protected override void OnSizeRequested (ref Requisition requisition) { Layout (); requisition = Bounds; base.OnSizeRequested (ref requisition); } protected virtual void BuildUI () { SetFancyStyle (this); TransientFor = (Gtk.Window) view.Toplevel; SkipPagerHint = true; SkipTaskbarHint = true; //view.MotionNotifyEvent += HandleImageViewMotion; view.Item.Changed += HandleItemChanged; view.ZoomChanged += HandleViewZoomChanged; SetSamplePoint (Gdk.Point.Zero); AddEvents ((int) (Gdk.EventMask.PointerMotionMask | Gdk.EventMask.ButtonPressMask | Gdk.EventMask.ButtonReleaseMask)); ButtonPressEvent += HandleButtonPressEvent; ButtonReleaseEvent += HandleButtonReleaseEvent; MotionNotifyEvent += HandleMotionNotifyEvent; KeyPressEvent += HandleKeyPressEvent; drag = new Delay (20, new GLib.IdleHandler (DragUpdate)); } } }