Created
March 20, 2018 12:54
-
-
Save DamianSuess/6d5deb1095caf493a2dfcda9be148957 to your computer and use it in GitHub Desktop.
Tomboy - Trac Plugin
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.IO; | |
using System.Runtime.InteropServices; | |
using System.Text.RegularExpressions; | |
using System.Text; | |
using System.Xml; | |
using Mono.Unix; | |
using Tomboy; | |
public class InsertBugAction : SplitterAction | |
{ | |
TracLink Tag; | |
int Offset; | |
string Id; | |
public InsertBugAction (Gtk.TextIter start, | |
string id, | |
Gtk.TextBuffer buffer, | |
TracLink tag) | |
{ | |
Tag = tag; | |
Id = id; | |
Offset = start.Offset; | |
} | |
public override void Undo (Gtk.TextBuffer buffer) | |
{ | |
// Tag images change the offset by one, but only when deleting. | |
Gtk.TextIter start_iter = buffer.GetIterAtOffset (Offset); | |
Gtk.TextIter end_iter = buffer.GetIterAtOffset (Offset + chop.Length + 1); | |
buffer.Delete (ref start_iter, ref end_iter); | |
buffer.MoveMark (buffer.InsertMark, buffer.GetIterAtOffset (Offset)); | |
buffer.MoveMark (buffer.SelectionBound, buffer.GetIterAtOffset (Offset)); | |
Tag.ImageLocation = null; | |
ApplySplitTags (buffer); | |
} | |
public override void Redo (Gtk.TextBuffer buffer) | |
{ | |
RemoveSplitTags (buffer); | |
Gtk.TextIter cursor = buffer.GetIterAtOffset (Offset); | |
Gtk.TextTag[] tags = {Tag}; | |
buffer.InsertWithTags (ref cursor, Id, tags); | |
buffer.MoveMark (buffer.SelectionBound, buffer.GetIterAtOffset (Offset)); | |
buffer.MoveMark (buffer.InsertMark, | |
buffer.GetIterAtOffset (Offset + chop.Length)); | |
} | |
public override void Merge (EditAction action) | |
{ | |
SplitterAction splitter = action as SplitterAction; | |
this.splitTags = splitter.SplitTags; | |
this.chop = splitter.Chop; | |
} | |
/* | |
* The internal listeners will create an InsertAction when the text | |
* is inserted. Since it's ugly to have the bug insertion appear | |
* to the user as two items in the undo stack, have this item eat | |
* the other one. | |
*/ | |
public override bool CanMerge (EditAction action) | |
{ | |
InsertAction insert = action as InsertAction; | |
if (insert == null) { | |
return false; | |
} | |
if (String.Compare(Id, insert.Chop.Text) == 0) { | |
return true; | |
} | |
return false; | |
} | |
public override void Destroy () | |
{ | |
} | |
} | |
public class TracLink : DynamicNoteTag | |
{ | |
Gdk.Pixbuf Icon; | |
public TracLink () | |
: base () | |
{ | |
} | |
public override void Initialize (string element_name) | |
{ | |
base.Initialize (element_name); | |
Underline = Pango.Underline.Single; | |
Foreground = "blue"; | |
CanActivate = true; | |
CanGrow = true; | |
CanSpellCheck = false; | |
CanSplit = false; | |
} | |
public string BugUrl | |
{ | |
get { return (string) Attributes ["uri"]; } | |
set { Attributes ["uri"] = value; } | |
} | |
protected override bool OnActivate (NoteEditor editor, Gtk.TextIter start, Gtk.TextIter end) | |
{ | |
if (BugUrl != string.Empty) { | |
Logger.Log ("Opening url '{0}'...", BugUrl); | |
Gnome.Url.Show (BugUrl); | |
} | |
return true; | |
} | |
public override Gdk.Pixbuf Image | |
{ | |
get | |
{ | |
if (Icon != null) | |
return Icon; | |
System.Uri uri = new System.Uri(BugUrl); | |
if (uri == null) | |
return null; | |
string host = uri.Host; | |
string imageDir = "~/.tomboy/TracIcons/"; | |
string imagePath = imageDir.Replace ("~", Environment.GetEnvironmentVariable ("HOME")) + host + ".png"; | |
try { | |
Icon = new Gdk.Pixbuf (imagePath); | |
} catch (GLib.GException) { | |
Icon = new Gdk.Pixbuf(null, "stock_bug.png"); | |
} | |
return Icon; | |
} | |
} | |
} | |
class TracPreferences : Gtk.VBox | |
{ | |
Gtk.TreeView icon_tree; | |
Gtk.ListStore icon_store; | |
Gtk.Button add_button; | |
Gtk.Button remove_button; | |
string last_opened_dir; | |
static string IMAGE_DIR = "~/.tomboy/TracIcons"; | |
static string image_dir = null; | |
static TracPreferences () | |
{ | |
image_dir = IMAGE_DIR.Replace ("~", Environment.GetEnvironmentVariable ("HOME")); | |
} | |
public TracPreferences () | |
: base (false, 12) | |
{ | |
last_opened_dir = Environment.GetEnvironmentVariable ("HOME"); | |
Gtk.Label l = new Gtk.Label (Catalog.GetString ( | |
"You can use any trac just by dragging links " + | |
"into notes. If you want a special icon for " + | |
"certain hosts, add them here.")); | |
l.Wrap = true; | |
l.Xalign = 0; | |
PackStart (l, false, false, 0); | |
icon_store = CreateIconStore (); | |
icon_tree = new Gtk.TreeView (icon_store); | |
icon_tree.HeadersVisible = true; | |
icon_tree.Selection.Mode = Gtk.SelectionMode.Single; | |
icon_tree.Selection.Changed += SelectionChanged; | |
Gtk.CellRenderer renderer; | |
Gtk.TreeViewColumn host_col = new Gtk.TreeViewColumn (); | |
host_col.Title = Catalog.GetString ("Host Name"); | |
host_col.Sizing = Gtk.TreeViewColumnSizing.Autosize; | |
host_col.Resizable = true; | |
host_col.Expand = true; | |
host_col.MinWidth = 200; | |
renderer = new Gtk.CellRendererText (); | |
host_col.PackStart (renderer, true); | |
host_col.AddAttribute (renderer, "text", 1 /* host name */); | |
host_col.SortColumnId = 1; /* host name */ | |
host_col.SortIndicator = false; | |
host_col.Reorderable = false; | |
host_col.SortOrder = Gtk.SortType.Ascending; | |
icon_tree.AppendColumn (host_col); | |
Gtk.TreeViewColumn icon_col = new Gtk.TreeViewColumn (); | |
icon_col.Title = Catalog.GetString ("Icon"); | |
icon_col.Sizing = Gtk.TreeViewColumnSizing.Fixed; | |
icon_col.MaxWidth = 50; | |
icon_col.MinWidth = 50; | |
icon_col.Resizable = false; | |
renderer = new Gtk.CellRendererPixbuf (); | |
icon_col.PackStart (renderer, false); | |
icon_col.AddAttribute (renderer, "pixbuf", 0 /* icon */); | |
icon_tree.AppendColumn (icon_col); | |
Gtk.ScrolledWindow sw = new Gtk.ScrolledWindow (); | |
sw.ShadowType = Gtk.ShadowType.In; | |
sw.HeightRequest = 200; | |
sw.WidthRequest = 300; | |
sw.SetPolicy (Gtk.PolicyType.Automatic, Gtk.PolicyType.Automatic); | |
sw.Add (icon_tree); | |
PackStart (sw, true, true, 0); | |
add_button = new Gtk.Button (Gtk.Stock.Add); | |
add_button.Clicked += AddClicked; | |
remove_button = new Gtk.Button (Gtk.Stock.Remove); | |
remove_button.Sensitive = false; | |
remove_button.Clicked += RemoveClicked; | |
Gtk.HButtonBox hbutton_box = new Gtk.HButtonBox (); | |
hbutton_box.Layout = Gtk.ButtonBoxStyle.Start; | |
hbutton_box.Spacing = 6; | |
hbutton_box.PackStart (add_button); | |
hbutton_box.PackStart (remove_button); | |
PackStart (hbutton_box, false, false, 0); | |
ShowAll (); | |
} | |
Gtk.ListStore CreateIconStore () | |
{ | |
Gtk.ListStore store = new Gtk.ListStore ( | |
typeof (Gdk.Pixbuf), // icon | |
typeof (string), // host | |
typeof (string)); // file path | |
store.SetSortColumnId (1, Gtk.SortType.Ascending); | |
return store; | |
} | |
void UpdateIconStore () | |
{ | |
// Read ~/.tomboy/TracIcons/" | |
if (!Directory.Exists (image_dir)) | |
Directory.CreateDirectory(image_dir); | |
icon_store.Clear (); // clear out the old entries | |
string [] icon_files = Directory.GetFiles (image_dir); | |
foreach (string icon_file in icon_files) { | |
FileInfo file_info = new FileInfo (icon_file); | |
Gdk.Pixbuf pixbuf = null; | |
try { | |
pixbuf = new Gdk.Pixbuf (icon_file); | |
} catch (Exception e) { | |
Logger.Warn ("Error loading Trac Icon {0}: {1}", icon_file, e.Message); | |
} | |
if (pixbuf == null) | |
continue; | |
string host = ParseHost (file_info); | |
if (host != null) { | |
Gtk.TreeIter iter = icon_store.Append (); | |
icon_store.SetValue (iter, 0, pixbuf); | |
icon_store.SetValue (iter, 1, host); | |
icon_store.SetValue (iter, 2, icon_file); | |
} | |
} | |
} | |
string ParseHost (FileInfo file_info) | |
{ | |
string name = file_info.Name; | |
string ext = file_info.Extension; | |
if (ext == null || ext == String.Empty) | |
return null; | |
int ext_pos = name.IndexOf (ext); | |
if (ext_pos <= 0) | |
return null; | |
string host = name.Substring (0, ext_pos); | |
if (host == null || host == String.Empty) | |
return null; | |
return host; | |
} | |
protected override void OnRealized () | |
{ | |
base.OnRealized (); | |
UpdateIconStore (); | |
} | |
void SelectionChanged (object sender, EventArgs args) | |
{ | |
remove_button.Sensitive = | |
icon_tree.Selection.CountSelectedRows() > 0; | |
} | |
void AddClicked (object sender, EventArgs args) | |
{ | |
Gtk.FileChooserDialog dialog = new Gtk.FileChooserDialog ( | |
Catalog.GetString ("Select an icon..."), | |
null, Gtk.FileChooserAction.Open, new object[] {}); | |
dialog.AddButton (Gtk.Stock.Cancel, Gtk.ResponseType.Cancel); | |
dialog.AddButton (Gtk.Stock.Open, Gtk.ResponseType.Ok); | |
dialog.DefaultResponse = Gtk.ResponseType.Ok; | |
dialog.LocalOnly = true; | |
dialog.SetCurrentFolder (last_opened_dir); | |
Gtk.FileFilter filter = new Gtk.FileFilter (); | |
filter.AddPixbufFormats (); | |
dialog.Filter = filter; | |
// Extra Widget | |
Gtk.Label l = new Gtk.Label (Catalog.GetString ("_Host name:")); | |
Gtk.Entry host_entry = new Gtk.Entry (); | |
l.MnemonicWidget = host_entry; | |
Gtk.HBox hbox = new Gtk.HBox (false, 6); | |
hbox.PackStart (l, false, false, 0); | |
hbox.PackStart (host_entry, true, true, 0); | |
hbox.ShowAll (); | |
dialog.ExtraWidget = hbox; | |
int response; | |
string icon_file; | |
string host; | |
run_add_dialog: | |
response = dialog.Run (); | |
icon_file = dialog.Filename; | |
host = host_entry.Text.Trim (); | |
if (response == (int) Gtk.ResponseType.Ok | |
&& host == String.Empty) { | |
// Let the user know that they | |
// have to specify a host name. | |
HIGMessageDialog warn = | |
new HIGMessageDialog ( | |
null, | |
Gtk.DialogFlags.DestroyWithParent, | |
Gtk.MessageType.Warning, | |
Gtk.ButtonsType.Ok, | |
Catalog.GetString ("No host name specified"), | |
Catalog.GetString ("You must specify the Trac " + | |
"host name to use with this icon.")); | |
warn.Run (); | |
warn.Destroy (); | |
host_entry.GrabFocus (); | |
goto run_add_dialog; | |
} else if (response != (int) Gtk.ResponseType.Ok) { | |
dialog.Destroy (); | |
return; | |
} | |
// Keep track of the last directory the user had open | |
last_opened_dir = dialog.CurrentFolder; | |
dialog.Destroy (); | |
// Copy the file to the TracIcons directory | |
string err_msg; | |
if (!CopyToTracIconsDir (icon_file, host, out err_msg)) { | |
HIGMessageDialog err = | |
new HIGMessageDialog ( | |
null, | |
Gtk.DialogFlags.DestroyWithParent, | |
Gtk.MessageType.Error, | |
Gtk.ButtonsType.Ok, | |
Catalog.GetString ("Error saving icon"), | |
Catalog.GetString ("Could not save the icon file. " + | |
err_msg)); | |
err.Run (); | |
err.Destroy (); | |
} | |
UpdateIconStore (); | |
} | |
bool CopyToTracIconsDir (string file_path, | |
string host, | |
out string err_msg) | |
{ | |
err_msg = null; | |
FileInfo file_info = new FileInfo (file_path); | |
string ext = file_info.Extension; | |
string saved_path = System.IO.Path.Combine (image_dir, host + ext); | |
try { | |
File.Copy (file_path, saved_path); | |
} catch (Exception e) { | |
err_msg = e.Message; | |
return false; | |
} | |
ResizeIfNeeded (saved_path); | |
return true; | |
} | |
void ResizeIfNeeded (string file_path) | |
{ | |
// FIXME: Resize the icon to not be larger than 16x16 | |
} | |
void RemoveClicked (object sender, EventArgs args) | |
{ | |
// Remove the icon file and call UpdateIconStore (). | |
Gtk.TreeIter iter; | |
if (!icon_tree.Selection.GetSelected (out iter)) | |
return; | |
string icon_path = icon_store.GetValue (iter, 2) as string; | |
HIGMessageDialog dialog = | |
new HIGMessageDialog ( | |
null, | |
Gtk.DialogFlags.DestroyWithParent, | |
Gtk.MessageType.Question, | |
Gtk.ButtonsType.None, | |
Catalog.GetString ("Really remove this icon?"), | |
Catalog.GetString ("If you remove an icon it is " + | |
"permanently lost.")); | |
Gtk.Button button; | |
button = new Gtk.Button (Gtk.Stock.Cancel); | |
button.CanDefault = true; | |
button.Show (); | |
dialog.AddActionWidget (button, Gtk.ResponseType.Cancel); | |
dialog.DefaultResponse = Gtk.ResponseType.Cancel; | |
button = new Gtk.Button (Gtk.Stock.Delete); | |
button.CanDefault = true; | |
button.Show (); | |
dialog.AddActionWidget (button, 666); | |
int result = dialog.Run (); | |
if (result == 666) { | |
try { | |
File.Delete (icon_path); | |
UpdateIconStore (); | |
} catch (Exception e) { | |
Logger.Error ("Error removing icon {0}: {1}", | |
icon_path, | |
e.Message); | |
} | |
} | |
dialog.Destroy(); | |
} | |
} | |
[PluginInfo("Trac Plugin", "@VERSION@", | |
"Rob Sharp", | |
"Allows you to drag a Trac URL from your browser directly " + | |
"into a tomboy note. The issue number is inserted as a link with " + | |
"a little bug icon next to it.", | |
WebSite = "http://sharp.id.au/", | |
PreferencesWidget = typeof (TracPreferences) | |
)] | |
public class TracPlugin : NotePlugin | |
{ | |
static string last_bug; | |
static TracPlugin () | |
{ | |
last_bug = "-1"; | |
} | |
protected override void Initialize () | |
{ | |
if (!Note.TagTable.IsDynamicTagRegistered ("link:trac")) { | |
Note.TagTable.RegisterDynamicTag ("link:trac", typeof (TracLink)); | |
} | |
} | |
protected override void Shutdown () | |
{ | |
} | |
protected override void OnNoteOpened () | |
{ | |
Window.Editor.DragDataReceived += OnDragDataReceived; | |
} | |
[DllImport("libgobject-2.0.so.0")] | |
static extern void g_signal_stop_emission_by_name (IntPtr raw, string name); | |
[GLib.ConnectBefore] | |
void OnDragDataReceived (object sender, Gtk.DragDataReceivedArgs args) | |
{ | |
foreach (Gdk.Atom atom in args.Context.Targets) { | |
if (atom.Name == "text/uri-list" || | |
atom.Name == "_NETSCAPE_URL") { | |
DropUriList (args); | |
return; | |
} | |
} | |
} | |
void DropUriList (Gtk.DragDataReceivedArgs args) | |
{ | |
if (args.SelectionData.Length > 0) { | |
string uriString = Encoding.UTF8.GetString (args.SelectionData.Data); | |
if (uriString.IndexOf ("ticket") != -1) { | |
if (InsertBug (args.X, args.Y, uriString)) { | |
Gtk.Drag.Finish (args.Context, true, false, args.Time); | |
g_signal_stop_emission_by_name(Window.Editor.Handle, | |
"drag_data_received"); | |
} | |
} | |
} | |
} | |
bool InsertBug (int x, int y, string uri) | |
{ | |
try { | |
string bug = uri.Substring (uri.IndexOf ("ticket") + 7); | |
// Debounce. I'm not sure why this is necessary :( | |
if (bug == last_bug) { | |
last_bug = "-1"; | |
return true; | |
} | |
Match matchBugInteger = Regex.Match(bug, @"\d+"); | |
bug = matchBugInteger.Value; | |
last_bug = bug; | |
TracLink link_tag = (TracLink) | |
Note.TagTable.CreateDynamicTag ("link:trac"); | |
link_tag.BugUrl = uri; | |
// Place the cursor in the position where the uri was | |
// dropped, adjusting x,y by the TextView's VisibleRect. | |
Gdk.Rectangle rect = Window.Editor.VisibleRect; | |
x = x + rect.X; | |
y = y + rect.Y; | |
Gtk.TextIter cursor = Window.Editor.GetIterAtLocation (x, y); | |
Buffer.PlaceCursor (cursor); | |
Buffer.Undoer.AddUndoAction (new InsertBugAction (cursor, bug, Buffer, link_tag)); | |
Gtk.TextTag[] tags = {link_tag}; | |
Buffer.InsertWithTags (ref cursor, bug, tags); | |
return true; | |
} catch { | |
return false; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment