cowbell r32 - in trunk: . base gui
- From: btaylor svn gnome org
- To: svn-commits-list gnome org
- Subject: cowbell r32 - in trunk: . base gui
- Date: Mon, 28 Apr 2008 05:59:45 +0100 (BST)
Author: btaylor
Date: Mon Apr 28 04:59:45 2008
New Revision: 32
URL: http://svn.gnome.org/viewvc/cowbell?rev=32&view=rev
Log:
Add initial MusicBrainz support.
WARNING: This support is very incomplete and half-baked, and will likely
microwave your cat and pee on the couch if you don't watch it carefully. YMMV.
Modified:
trunk/base/Batch.cs
trunk/base/IMetadataProxy.cs
trunk/base/Makefile.am
trunk/base/MetadataProxyService.cs
trunk/base/MusicBrainzMetadataProxy.cs
trunk/configure.ac
trunk/gui/AlbumCoverImage.cs
Modified: trunk/base/Batch.cs
==============================================================================
--- trunk/base/Batch.cs (original)
+++ trunk/base/Batch.cs Mon Apr 28 04:59:45 2008
@@ -31,6 +31,7 @@
public Batch (string[] paths, bool quiet)
{
Ask = !quiet;
+Ask.ToString ();
ArrayList search_dirs = new ArrayList ();
foreach (string path in paths)
@@ -96,6 +97,7 @@
// Guess song info
Console.WriteLine (Catalog.GetString ("Guessing Song Information..."));
+/*
AmazonMetadataProxy p = new AmazonMetadataProxy ();
p.Block = true;
p.Import ();
@@ -165,6 +167,7 @@
Console.WriteLine (Catalog.GetString ("Creating album-order playlist..."));
fs_svc.ExportPlaylist (fs_svc.GetBasedir ());
}
+*/
}
}
}
Modified: trunk/base/IMetadataProxy.cs
==============================================================================
--- trunk/base/IMetadataProxy.cs (original)
+++ trunk/base/IMetadataProxy.cs Mon Apr 28 04:59:45 2008
@@ -23,13 +23,19 @@
namespace Cowbell.Base
{
+ public delegate void ProgressHandler (bool completed, ProgressStatus s);
+
public interface IMetadataProxy
{
- event StringHandler Heartbeat;
- event StringHandler ConnectionFailed;
- event StringHandler ConnectionRejected;
- event VoidHandler Completed;
+ void Import (ProgressHandler progress_cb);
+ }
- void Import ();
+ public enum ProgressStatus
+ {
+ NetworkError,
+ ArtistNotFound,
+ ReleaseNotFound,
+ PartialTrackMatch,
+ CompleteMatch
}
}
Modified: trunk/base/Makefile.am
==============================================================================
--- trunk/base/Makefile.am (original)
+++ trunk/base/Makefile.am Mon Apr 28 04:59:45 2008
@@ -2,11 +2,11 @@
REFERENCES = \
-r:System.Web.Services \
+-r:../musicbrainz-sharp/MusicBrainz.dll \
$(TAGLIBSHARP_LIBS)
FILES = \
-AmazonMetadataProxy.cs \
-AmazonSearchService.cs \
+MusicBrainzMetadataProxy.cs \
Batch.cs \
Catalog.cs \
CliParser.cs \
Modified: trunk/base/MetadataProxyService.cs
==============================================================================
--- trunk/base/MetadataProxyService.cs (original)
+++ trunk/base/MetadataProxyService.cs Mon Apr 28 04:59:45 2008
@@ -45,10 +45,12 @@
IMetadataProxy p = (IMetadataProxy)Activator.CreateInstance (t);
+/*
p.Heartbeat += new StringHandler (OnProxyPulse);
p.ConnectionFailed += new StringHandler (OnConnectionFailed);
p.ConnectionRejected += new StringHandler (OnConnectionRejected);
p.Completed += new VoidHandler (OnProxyCompleted);
+*/
proxies.Add (new DictionaryEntry (priority, p));
}
}
@@ -64,7 +66,7 @@
foreach (DictionaryEntry d in proxies)
{
IMetadataProxy p = (IMetadataProxy)d.Value;
- p.Import ();
+ p.Import (new ProgressHandler (ImportProgress));
}
}
@@ -72,6 +74,11 @@
List<DictionaryEntry> proxies = new List<DictionaryEntry> ();
/* private methods */
+ private void ImportProgress (bool completed, ProgressStatus s)
+ {
+ Console.WriteLine ("Completed: {0}, Status: {1}", completed, s);
+ }
+/*
private void OnProxyPulse (string text)
{
if (ImportHeartbeat != null) {
@@ -99,5 +106,6 @@
ImportCompleted (true, String.Empty);
}
}
+*/
}
}
Modified: trunk/base/MusicBrainzMetadataProxy.cs
==============================================================================
--- trunk/base/MusicBrainzMetadataProxy.cs (original)
+++ trunk/base/MusicBrainzMetadataProxy.cs Mon Apr 28 04:59:45 2008
@@ -21,305 +21,292 @@
using System;
using System.Text;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
using System.Runtime.InteropServices;
+using System.Text.RegularExpressions;
using System.Threading;
-using musicbrainz;
+using MusicBrainz;
namespace Cowbell.Base
{
- // XXX: A few bugs cause this proxy to be temporarily disabled
- //[MetadataProxyPriority (30)]
+ [MetadataProxyPriority (20)]
public class MusicBrainzMetadataProxy : IMetadataProxy
{
- private delegate void MusicBrainzThread (int pos);
-
- class TrackData
+#region public methods
+ public void Import (ProgressHandler progress_cb)
{
- public bool Exists;
-
- public string Title;
- public string TrackId;
- public string Artist;
- public string ArtistId;
- public string Album;
- public int TrackNumber;
+ db_svc = (IDatabaseService)ServiceManager.GetService (
+ typeof (IDatabaseService));
- public override string ToString ()
- {
- return String.Format ("Exists: {0}\n" +
- "Title: {1}\n" +
- "Artist: {2}\n" +
- "Album: {3}\n" +
- "Track Number: {4}\n" +
- "TrackId: {5}\n" +
- "ArtistId: {6}\n",
- Exists, Title,
- Artist, Album,
- TrackNumber, TrackId,
- ArtistId);
+ dispatch_svc = (IDispatchService)ServiceManager.GetService (
+ typeof (IDispatchService));
+
+ Artist a = FindClosestArtist (db_svc.GlobalData.Artist);
+ if (a == null) {
+ progress_cb (true, ProgressStatus.ArtistNotFound);
+ return;
}
- }
- enum TPCallbackEnum
- {
- tpFileAdded,
- tpFileChanged,
- tpFileRemoved,
- tpWriteTagsComplete,
- tpCallbackLast
- };
-
- enum TPFileStatus
- {
- eUnrecognized, // unrecognized
- eRecognized, // Recognized and previously saved
- ePending, // pending trm calculation
- eTRMLookup, // trm done, pending trm lookup
- eTRMCollision, // trm done, pending trm lookup
- eFileLookup, // trm done, no matches, pending file lookup
- eUserSelection, // file lookup done, needs user selection
- eVerified, // User verified, about to write changes to disk
- eSaved, // File was saved
- eDeleted, // to be deleted, waiting for refcount == 0
- eError, // Error
-
- eLastStatus // Just a placeholder -- don't delete
- };
-
- public event MetadataProxyHeartbeatHandler Heartbeat;
- public int TotalBeats {
- get {
- return 2 * Runtime.Database.Count;
+ Release r = FindClosestRelease (a, db_svc.GlobalData.Album);
+ if (r == null) {
+ progress_cb (true, ProgressStatus.ReleaseNotFound);
+ return;
}
- }
-
- /* We use this to encode byte arrays into UTF for output */
- static UTF8Encoding e = new UTF8Encoding ();
- MusicBrainz mb;
-
- public MusicBrainzMetadataProxy ()
- {
- mb = new MusicBrainz ();
- mb.SetDebug (Debug.WriteLineEnabled);
- mb.SetDepth (2);
- }
+ int num_matches;
+ MatchTracks (a, r, db_svc, out num_matches);
- public void Import ()
- {
- for (int i = 0; i < Runtime.Database.Count; i++)
- {
- Dispatcher.FireAndForget (new MusicBrainzThread (ThreadedImport), i);
+ if (num_matches == db_svc.Count) {
+ progress_cb (true, ProgressStatus.CompleteMatch);
+ return;
}
- }
- private void Pulse (string text)
- {
- if (Heartbeat != null)
- Heartbeat (text);
+ progress_cb (true, ProgressStatus.PartialTrackMatch);
}
+#endregion
- private void ThreadedImport (int pos)
- {
- TrackData d = new TrackData ();
-
- Song s = (Song)Runtime.Database[pos];
+#region private delegates
+ private delegate void DatabaseUpdateHandler (int i, Song s);
+#endregion
- Pulse (String.Format (Catalog.GetString ("Retrieving track id for {0}"), s.Title));
- d = GetTrackInfoFromTRM (s, GetTRM (s.Filename));
+#region private fields
+ private IDatabaseService db_svc;
+ private IDispatchService dispatch_svc;
+ private const string LUCENE_ARTIST_FORMAT = "artist:{0}";
+ private const int MAX_TRIES = 5;
+ private const int ACCEPT_DISTANCE_THRESHOLD = 2;
- Pulse (String.Format (Catalog.GetString ("Populating {0}"), s.Title));
- if (d.Exists) {
- s.Title = d.Title;
- s.Artist = d.Artist;
- s.Album = d.Album;
- s.TrackNumber = Convert.ToUInt32 (d.TrackNumber);
-
- Runtime.Database[pos] = s;
- }
+ // good lower bound for a "match"
+ // TODO: test this heuristic
+ private const double MATCH_LOWER_BOUND = 0.6;
+#endregion
- Debug.WriteLine (d);
- }
-
- /**
- * Returns the TRM generated for the given mp3, ogg or flac
- * file.
- * @note This is a _very_ expensive operation (on the order of
- * 2-3 sec) so please cache results.
- */
- private string GetTRM (string filename)
+#region private methods
+ private void MatchTracks (Artist artist, Release release,
+ IDatabaseService db_svc, out int num_matches)
{
- IntPtr track = IntPtr.Zero, pimp;
- int id = 0, fileId = 0;
- TPCallbackEnum type = 0;
- byte[] trm = new byte[64];
- String err;
- bool finished = false;
+ num_matches = 0;
- pimp = tp_NewWithArgs ("tunepimp-sharp", "0.01", 0);
- id = tp_AddFile (pimp, filename);
+ ReadOnlyCollection<Track> tracks = release.GetTracks ();
- while (tp_GetNotification (pimp, ref type, ref fileId) || !finished)
+ for (int i = 0; i < db_svc.Items.Count; i++)
{
- if (type != TPCallbackEnum.tpFileChanged)
- continue;
+ Song s = (Song)db_svc.Items[i];
+ string[] db_tokens = SanitizeString (s.Title).Split (' ');
- track = tp_GetTrack (pimp, id);
+ int best_score = 0;
+ int score = 0;
+ int pos = -1;
- Debug.WriteLine ("Status: " + tr_GetStatus (track));
- switch (tr_GetStatus (track))
+ // Match these up with the tracks in the database
+ for (int j = 0; j < tracks.Count; j++)
{
- case TPFileStatus.eTRMLookup:
- tr_GetTRM (track, trm, 64);
- if (trm[0] != 0)
- finished = true;
- break;
+ if (tracks[j] == null) {
+ continue;
+ }
+
+ string[] mb_tokens = SanitizeString (tracks[j].GetTitle ()).Split (' ');
+ score = CalculateStringMatchScore ((string[])(db_tokens.Clone ()), mb_tokens);
+
+ if (score >= 0) {
+ Debug.WriteLine ("Matching \"{0}\" <=> \"{1}\" ; score {2}", SanitizeString (s.Title),
+ SanitizeString (tracks[j].GetTitle ()),
+ score);
+ }
+
+ if (score > best_score) {
+ pos = j;
+ best_score = score;
+ }
+ }
- case TPFileStatus.eRecognized:
- tr_GetTRM (track, trm, 64);
+ if (best_score < 0) {
+ // None of the words matched, so lets move on
+ Debug.WriteLine ("Found no match for {0}", ((Song)db_svc.Items[i]).Title);
+ continue;
+ }
+
+ if (pos > -1) {
+ // we must have hit the best match now
+ Song temp = (Song)db_svc.Items[i];
+
+ // dump the amazon supplied information into the song
+ temp.Artist = artist.GetName ();
+ temp.TrackNumber = Convert.ToUInt32 (pos + 1);
+ temp.Title = tracks[pos].GetTitle ();
+ temp.Album = release.GetTitle ();
+ temp.Year = GetMusicBrainzYearFromDate (release.GetEvents ()[0].Date);
- if (trm[0] != 0)
- Debug.WriteLine ("TRM read from file...");
+ dispatch_svc.GuiDispatch (new DatabaseUpdateHandler (UpdateDatabase),
+ i, temp);
- tp_IdentifyAgain (pimp, id);
- finished = true;
- break;
+ num_matches++;
+ }
+ }
+ }
- case TPFileStatus.ePending:
- break;
+ private Release FindClosestRelease (Artist artist, string release_search)
+ {
+ int min_distance = Int32.MaxValue;
+ Release closest_release = null;
+ foreach (Release r in artist.GetReleases ())
+ {
+ Debug.WriteLine ("Examining {0}...", r);
- case TPFileStatus.eError:
- tr_GetError (track, out err, 255);
- finished = true;
- throw new Exception (err);
+ // If we have more tracks than MusicBrainz
+ // gave us within a fuzz (+/- 2), we didn't
+ // find the right album.
+
+ // This also saves users who try to import
+ // their entire library from shooting
+ // themselves in the foot.
+ if (db_svc.Items.Count > (r.GetTracks ().Count + 2)) {
+ Debug.WriteLine (" We have 3 more tracks than MusicBrainz's results; we didn't find the right album.");
+ continue;
}
- tp_ReleaseTrack (pimp, track);
- if (!finished)
- Thread.Sleep (5000);
- }
-
- tp_ReleaseTrack (pimp, track);
- tp_Delete (pimp);
+ int distance = Utils.LevenshteinDistance (release_search,
+ r.GetTitle ());
+ if (distance <= ACCEPT_DISTANCE_THRESHOLD) {
+ Debug.WriteLine (" MATCH below distance threshold, picking");
+ min_distance = distance;
+ closest_release = r;
+ break;
+ }
- return e.GetString (trm);
- }
+ min_distance = Math.Min (min_distance, distance);
+ if (min_distance == distance) {
+ Debug.WriteLine (" MATCH with distance {0}", distance);
+ closest_release = r;
+ continue;
+ }
- /**
- * Returns a TrackData object containing the data
- * returned by a MusicBrainz TrackInfoFromTRMId call on the TRM
- * provided.
- * @param trm the TRM generated by calling GetTRM () on a music file
- * @return the track information relating to that TRM
- */
- private TrackData GetTrackInfoFromTRM (Song s, string trm)
- {
- TrackData track;
- string track_uri;
- string[] args = new string[] {
- trm,
- s.Artist,
- s.Album,
- s.Title,
- Convert.ToString (s.TrackNumber)
- };
-
- track = new TrackData ();
- track.Exists = false;
-
- try {
- // Query for Track Info
- Query (MusicBrainz.MBQ_TrackInfoFromTRMId, args);
-
- // Grab the first result (for now)
- mb.Select (MusicBrainz.MBS_SelectTrack, 1);
+ Debug.WriteLine (" NOT MATCH with distance {0}", distance);
+ }
- // Get and parse the result
- track_uri = GetResult (MusicBrainz.MBE_TrackGetTrackId);
- track.TrackId = GetIdResult (MusicBrainz.MBE_TrackGetTrackId);
- track.ArtistId = GetIdResult (MusicBrainz.MBE_TrackGetArtistId);
- track.Artist = GetResult (MusicBrainz.MBE_TrackGetArtistName);
- track.Title = GetResult (MusicBrainz.MBE_TrackGetTrackName);
-
- // Descend into the album part of the RDF
- mb.Select (MusicBrainz.MBS_SelectTrackAlbum);
-
- track.TrackNumber = mb.GetOrdinalFromList (
- MusicBrainz.MBE_AlbumGetTrackList,
- track_uri
- );
-
- if (track.TrackNumber < 0 && track.TrackNumber > 255)
- track.TrackNumber = 0;
-
- track.Album = GetResult (MusicBrainz.MBE_TrackGetTrackName);
-
- track.Exists = true;
- } catch (Exception e) {
- // FIXME: Show a dialog at some point
+ if ((min_distance / release_search.Length) > MATCH_LOWER_BOUND) {
+ return null;
}
- return track;
+ return closest_release;
}
- private void Query (string query, string[] args)
+ private Artist FindClosestArtist (string artist_search)
{
- if (!mb.Query (query, args)) {
- string error;
+ Query<Artist> artists
+ = Artist.QueryLucene (String.Format (LUCENE_ARTIST_FORMAT,
+ db_svc.GlobalData.Artist));
- mb.GetQueryError (out error);
- throw new Exception (error);
- }
- }
+ int tries = 0;
+ int min_distance = Int32.MaxValue;
+ Artist closest_artist = null;
+ foreach (Artist a in artists)
+ {
+ if (tries > MAX_TRIES)
+ break;
- private string GetResult (string query)
- {
- string data;
+ tries++;
- if (!mb.GetResultData (query, out data))
- throw new Exception ("GetResultData failed.");
+ Debug.WriteLine ("Examining {0}...", a);
- return data;
- }
+ int distance = Utils.LevenshteinDistance (artist_search,
+ a.GetName ());
+ if (distance <= ACCEPT_DISTANCE_THRESHOLD) {
+ Debug.WriteLine (" MATCH below distance threshold, picking");
+ min_distance = distance;
+ closest_artist = a;
+ break;
+ }
- private string GetIdResult (string query)
- {
- string data, ret;
+ min_distance = Math.Min (min_distance, distance);
+ if (min_distance == distance) {
+ Debug.WriteLine (" MATCH with distance {0}", distance);
+ closest_artist = a;
+ continue;
+ }
- data = GetResult (query);
- mb.GetIDFromURL (data, out ret);
-
- return ret;
+ Debug.WriteLine (" NOT MATCH with distance {0}", distance);
+ }
+
+ if ((min_distance / artist_search.Length) > MATCH_LOWER_BOUND) {
+ return null;
+ }
+
+ return closest_artist;
}
- [DllImport ("tunepimp")]
- static extern IntPtr tp_NewWithArgs (string appName, string appVersion, int startThreads);
+ private int CalculateStringMatchScore (string[] a, string[] b)
+ {
+ int score = 0;
+ for (int i = 0; i < a.Length; i++)
+ {
+ int best_distance = 3;
+ int best_pos = -1;
+ for (int j = 0; j < b.Length; j++)
+ {
+ if (a[i] != String.Empty && b[j] != String.Empty) {
+ int distance = Utils.LevenshteinDistance (a[i], b[j]);
+ if (distance < best_distance) {
+ best_distance = distance;
+ best_pos = j;
+ }
+ }
+ }
- [DllImport ("tunepimp")]
- static extern int tp_AddFile (IntPtr o, string fileName);
+ if (best_pos > -1) {
+ b[best_pos] = String.Empty;
+ a[i] = String.Empty;
- [DllImport ("tunepimp")]
- static extern IntPtr tp_GetTrack (IntPtr o, int fileId);
+ score += 30 - (best_distance * 10);
+ }
+ }
- [DllImport ("tunepimp")]
- static extern void tp_ReleaseTrack (IntPtr o, IntPtr track);
+ foreach (string token in a)
+ {
+ if (token != String.Empty) {
+ score -= 2;
+ }
+ }
- [DllImport ("tunepimp")]
- static extern bool tp_GetNotification (IntPtr o, ref TPCallbackEnum type, ref int fileId);
+ foreach (string token in b)
+ {
+ if (token != String.Empty) {
+ score -= 2;
+ }
+ }
- [DllImport ("tunepimp")]
- static extern void tp_IdentifyAgain (IntPtr o, int fileId);
+ return score;
+ }
- [DllImport ("tunepimp")]
- static extern void tr_GetTRM (IntPtr t, byte[] trm, int maxLen);
+ private string SanitizeString (string s)
+ {
+ s = s.ToLower ();
+ s = s.Replace ("-", " ");
+ s = s.Replace ("_", " ");
+ s = Regex.Replace (s, " +", " ");
- [DllImport ("tunepimp")]
- static extern void tr_GetError (IntPtr o, [MarshalAs (UnmanagedType.LPWStr)] out string error, int maxLen);
+ return s;
+ }
- [DllImport ("tunepimp")]
- static extern TPFileStatus tr_GetStatus (IntPtr t);
+ private void UpdateDatabase (int i, Song s)
+ {
+ db_svc.Items[i] = s;
+ }
- [DllImport ("tunepimp")]
- static extern void tp_Delete (IntPtr o);
+ /**
+ * Gets the year part of a MusicBrainz event date. This is
+ * supposed to be in the format YYYY-MM-DD.
+ */
+ private uint GetMusicBrainzYearFromDate (string date)
+ {
+ try {
+ return (uint)DateTime.Parse (date).Year;
+ } catch {
+ Debug.WriteLine ("Unable to parse MusicBrainz event date {0}", date);
+ return 0;
+ }
+ }
+#endregion
}
}
Modified: trunk/configure.ac
==============================================================================
--- trunk/configure.ac (original)
+++ trunk/configure.ac Mon Apr 28 04:59:45 2008
@@ -55,6 +55,7 @@
AC_SUBST(TAGLIBSHARP_LIBS)
AC_OUTPUT([
+musicbrainz-sharp/Makefile
resources/Makefile
base/Globals.cs
base/Makefile
Modified: trunk/gui/AlbumCoverImage.cs
==============================================================================
--- trunk/gui/AlbumCoverImage.cs (original)
+++ trunk/gui/AlbumCoverImage.cs Mon Apr 28 04:59:45 2008
@@ -152,7 +152,7 @@
bool UserPixbuf;
- static string DevTag = "1G1CFFX6R1ZWFXDMVJG2";
+// static string DevTag = "1G1CFFX6R1ZWFXDMVJG2";
static TargetEntry[] CoverDragEntries = new TargetEntry[] {
new TargetEntry ("text/uri-list", 0, (uint)TargetType.UriList),
@@ -261,6 +261,7 @@
*/
private Pixbuf GetCoverFromAmazon ()
{
+/*
Song song = db_svc.GlobalData;
AmazonSearchService search_service = new AmazonSearchService ();
@@ -270,15 +271,15 @@
string [] album_title_array = sane_album_title.Split (' ');
Array.Sort (album_title_array);
- /* This assumes the right artist is always in Artists [0] */
+ * This assumes the right artist is always in Artists [0] *
string sane_artist = SanitizeString (song.Artist);
- /* Prepare for handling multi-page results */
+ * Prepare for handling multi-page results *
int total_pages = 1;
int current_page = 1;
- int max_pages = 2; /* check no more than 2 pages */
+ int max_pages = 2; * check no more than 2 pages *
- /* Create Encapsulated Request */
+ * Create Encapsulated Request *
ArtistRequest asearch = new ArtistRequest ();
asearch.devtag = DevTag;
@@ -292,7 +293,7 @@
asearch.mode = "music";
asearch.tag = "webservices-20";
- /* Use selected Amazon service */
+ * Use selected Amazon service *
search_service.Url = "http://soap.amazon.com/onca/soap3";
double best_match_percent = 0.0;
@@ -304,28 +305,28 @@
ProductInfo pi;
- /* Amazon API requires this .. */
+ * Amazon API requires this .. *
System.Threading.Thread.Sleep (1000);
- /* Web service calls timeout after 30 seconds */
+ * Web service calls timeout after 30 seconds *
search_service.Timeout = 30000;
- /* This may throw an exception, we catch it in Song.cs in the calling function */
+ * This may throw an exception, we catch it in Song.cs in the calling function *
pi = search_service.ArtistSearchRequest (asearch);
int num_results = pi.Details.Length;
total_pages = Convert.ToInt32 (pi.TotalPages);
- /* Work out how many matches are on this page */
+ * Work out how many matches are on this page *
if (num_results < 1)
return null;
for (int i = 0; i < num_results; i++)
{
- /* Ignore bracketed text on the result from Amazon */
+ * Ignore bracketed text on the result from Amazon *
string sane_product_name = SanitizeString (pi.Details[i].ProductName);
- /* Compare the two strings statistically */
+ * Compare the two strings statistically *
string [] product_name_array = sane_product_name.Split (' ');
Array.Sort (product_name_array);
@@ -384,6 +385,8 @@
}
return best_match;
+*/
+ return null;
}
private Pixbuf GetCoverFromUrl (string url)
@@ -430,6 +433,7 @@
return cover;
}
+/*
private string SanitizeString (string s)
{
s = s.ToLower ();
@@ -440,6 +444,7 @@
return s;
}
+*/
/* DnD Callbacks mostly stolen from Jorn Baayen */
private void DragDataReceivedCb (object o, DragDataReceivedArgs args)
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]