@@ -301,59 +301,44 @@ public override void AddTo(IDataObject data) {
301301 }
302302
303303
304- /// <summary>
305- /// Class to hold SVG data
306- /// </summary>
307- public class SvgContent : TextLikeContent {
308- public static readonly string [ ] EXTENSIONS = { "svg" } ;
309- public SvgContent ( string xml ) : base ( xml ) { }
310304
311- public string Xml {
312- get {
313- var xml = Text ;
314- if ( ! xml . StartsWith ( "<?xml" ) )
315- xml = "<?xml version=\" 1.0\" encoding=\" " + Encoding . BodyName + "\" ?>\n " + xml ;
316- return xml ;
317- }
305+ public class TextLikeContent : BaseContent {
306+ private readonly string [ ] Formats ;
307+ public override string [ ] Extensions { get ; }
308+
309+ public TextLikeContent ( string [ ] formats , string [ ] extensions , string text ) {
310+ Formats = formats ;
311+ Extensions = extensions ;
312+ Data = text ;
318313 }
314+ public string Text => Data as string ;
319315
320- public override string [ ] Extensions => EXTENSIONS ;
321- public override string Description => Resources . str_preview_svg ;
316+ public static readonly Encoding DefaultEncoding = new UTF8Encoding ( false ) ; // omit unnecessary BOM bytes
322317
323- public override void SaveAs ( string path , string extension , bool append = false ) {
324- if ( append )
325- throw new AppendNotSupportedException ( ) ;
326- switch ( NormalizeExtension ( extension ) ) {
327- case "svg" :
328- default :
329- Save ( path , Xml ) ;
330- break ;
331- }
332- }
318+ public override string Description => Resources . str_preview ;
333319
334320 public override void AddTo ( IDataObject data ) {
335- data . SetData ( "image/svg+xml" , Stream ) ;
336- }
337- public override string TextPreview ( string extension ) {
338- return Xml ;
321+ AddTo ( data , Text ) ;
339322 }
340- }
341-
342323
343-
344- public abstract class TextLikeContent : BaseContent {
345- public TextLikeContent ( string text ) {
346- Data = text ;
324+ protected void AddTo ( IDataObject data , string text , Encoding encoding = null ) {
325+ foreach ( var f in Formats ) {
326+ if ( DataFormats . GetFormat ( f ) . Id < 32 ) { // Native formats, handled well by default
327+ data . SetData ( f , text ) ;
328+ } else { // Non-native formats
329+ // Manually encode to avoid default object serialization header,
330+ // see https://devblogs.microsoft.com/oldnewthing/20181130-00/?p=100365
331+ data . SetData ( f , new MemoryStream ( ( encoding ?? DefaultEncoding ) . GetBytes ( text ) ) ) ;
332+ }
333+ }
347334 }
348- public string Text => Data as string ;
349- public Stream Stream => new MemoryStream ( Encoding . GetBytes ( Text ) ) ;
350- public static readonly Encoding Encoding = new UTF8Encoding ( false ) ; // omit unnecessary BOM bytes
335+
351336 public override void SaveAs ( string path , string extension , bool append = false ) {
352- Save ( path , Text , append ) ;
337+ Save ( path , TextPreview ( extension ) , append ) ;
353338 }
354339
355- protected static void Save ( string path , string text , bool append = false ) {
356- using ( var streamWriter = new StreamWriter ( path , append , Encoding ) )
340+ protected static void Save ( string path , string text , bool append = false , Encoding encoding = null ) {
341+ using ( var streamWriter = new StreamWriter ( path , append , ( encoding ?? DefaultEncoding ) ) )
357342 streamWriter . Write ( EnsureNewline ( text ) ) ;
358343 }
359344
@@ -365,27 +350,88 @@ public static string EnsureNewline(string text) {
365350 /// Return a string used for preview
366351 /// </summary>
367352 /// <returns></returns>
368- public abstract string TextPreview ( string extension ) ;
353+ public virtual string TextPreview ( string extension ) {
354+ return Text ;
355+ }
369356 }
370357
371358
372359 public class TextContent : TextLikeContent {
373- public TextContent ( string text ) : base ( text ) { }
374- public override string [ ] Extensions => new [ ] { "txt" , "md" , "log" , "bat" , "ps1" , "java" , "js" , "cpp" , "cs" , "py" , "css" , "html" , "php" , "json" , "csv" } ;
360+ public static readonly string [ ] FORMATS = { DataFormats . Text , DataFormats . UnicodeText } ;
361+ public static readonly string [ ] EXTENSIONS = { "txt" , "md" , "log" , "bat" , "ps1" , "java" , "js" , "cpp" , "cs" , "py" , "css" , "html" , "php" , "json" , "csv" } ;
362+
363+ public TextContent ( string text ) : base ( FORMATS , EXTENSIONS , text ) { }
364+
375365 public override string Description => string . Format ( Resources . str_preview_text , Text . Length , Text . Split ( '\n ' ) . Length ) ;
376- public override void AddTo ( IDataObject data ) {
377- data . SetData ( DataFormats . Text , Text ) ;
378- data . SetData ( DataFormats . UnicodeText , Text ) ;
366+
367+ }
368+
369+ public class RtfContent : TextLikeContent {
370+ public static readonly string [ ] FORMATS = { DataFormats . Rtf , "text/rtf" } ;
371+ public static readonly string [ ] EXTENSIONS = { "rtf" } ;
372+
373+ public RtfContent ( string text ) : base ( FORMATS , EXTENSIONS , text ) { }
374+
375+ public override string Description => Resources . str_preview_rtf ;
376+ }
377+
378+ public class DifContent : TextLikeContent {
379+ public static readonly string [ ] FORMATS = { DataFormats . Dif } ;
380+ public static readonly string [ ] EXTENSIONS = { "dif" } ;
381+
382+ public DifContent ( string text ) : base ( FORMATS , EXTENSIONS , text ) { }
383+
384+ public override string Description => Resources . str_preview_dif ;
385+ }
386+
387+ public class SlkContent : TextLikeContent {
388+ public static readonly string [ ] FORMATS = { DataFormats . SymbolicLink } ;
389+ public static readonly string [ ] EXTENSIONS = { "sylk" } ;
390+
391+ public SlkContent ( string text ) : base ( FORMATS , EXTENSIONS , text ) { }
392+
393+ public override string Description => Resources . str_preview_sylk ;
394+ }
395+
396+
397+
398+ /// <summary>
399+ /// Class to hold SVG data
400+ /// </summary>
401+ public class SvgContent : TextLikeContent {
402+ public static readonly string [ ] FORMATS = { "image/svg+xml" , "svg" } ;
403+ public static readonly string [ ] EXTENSIONS = { "svg" } ;
404+
405+ public SvgContent ( string xml ) : base ( FORMATS , EXTENSIONS , xml ) { }
406+
407+ public string Xml {
408+ get {
409+ var xml = Text ;
410+ if ( ! xml . StartsWith ( "<?xml" ) )
411+ xml = "<?xml version=\" 1.0\" encoding=\" " + DefaultEncoding . BodyName + "\" ?>\n " + xml ;
412+ return xml ;
413+ }
379414 }
415+
416+ public override string Description => Resources . str_preview_svg ;
417+
418+ public override void SaveAs ( string path , string extension , bool append = false ) {
419+ if ( append ) throw new AppendNotSupportedException ( ) ;
420+ base . SaveAs ( path , extension , append ) ;
421+ }
422+
380423 public override string TextPreview ( string extension ) {
381- return Text ;
424+ return Xml ;
382425 }
383426 }
384427
385428
386429 public class HtmlContent : TextLikeContent {
387- public HtmlContent ( string text ) : base ( text ) { }
388- public override string [ ] Extensions => new [ ] { "html" , "htm" , "xhtml" } ;
430+ public static readonly string [ ] FORMATS = { DataFormats . Html } ;
431+ public static readonly string [ ] EXTENSIONS = { "html" , "htm" , "xhtml" } ;
432+
433+ public HtmlContent ( string text ) : base ( FORMATS , EXTENSIONS , text ) { }
434+
389435 public override string Description => Resources . str_preview_html ;
390436 public override void SaveAs ( string path , string extension , bool append = false ) {
391437 var html = Text ;
@@ -403,25 +449,22 @@ public override void AddTo(IDataObject data) {
403449 "EndHTML:" + STOP + "\r \n " +
404450 "StartFragment:" + START + "\r \n " +
405451 "EndFragment:" + STOP + "\r \n " ;
406- var bytecount = Encoding . UTF8 . GetByteCount ( Text ) ;
452+ var bytecount = DefaultEncoding . GetByteCount ( Text ) ;
407453 header = header . Replace ( START , ( header . Length ) . ToString ( ) . PadLeft ( START . Length , '0' ) ) ;
408454 header = header . Replace ( STOP , ( header . Length + bytecount ) . ToString ( ) . PadLeft ( STOP . Length , '0' ) ) ;
409455
410- data . SetData ( DataFormats . Html , header + Text ) ;
411- }
412- public override string TextPreview ( string extension ) {
413- return Text ;
456+ AddTo ( data , header + Text ) ;
414457 }
458+
415459 }
416460
417461
418462 public class CsvContent : TextLikeContent {
419- public CsvContent ( string text ) : base ( text ) { }
420- public override string [ ] Extensions => new [ ] { "csv" , "tsv" , "tab" , "md" } ;
463+ public static readonly string [ ] FORMATS = { DataFormats . CommaSeparatedValue , "text/csv" , "text/tab-separated-values" } ;
464+ public static readonly string [ ] EXTENSIONS = { "csv" , "tsv" , "tab" , "md" } ;
465+
466+ public CsvContent ( string text ) : base ( FORMATS , EXTENSIONS , text ) { }
421467 public override string Description => Resources . str_preview_csv ;
422- public override void AddTo ( IDataObject data ) {
423- data . SetData ( DataFormats . CommaSeparatedValue , Text ) ;
424- }
425468
426469 /// <summary>
427470 /// Heuristically determine the (most likely) delimiter
@@ -477,49 +520,48 @@ public override string TextPreview(string extension) {
477520 case "md" :
478521 return AsMarkdown ( ) ;
479522 default :
480- return Text ;
523+ return base . TextPreview ( extension ) ;
481524 }
482525 }
483526
484- public override void SaveAs ( string path , string extension , bool append = false ) {
485- Save ( path , TextPreview ( extension ) , append ) ;
486- }
487- }
488-
489-
490- public class GenericTextContent : TextLikeContent {
491- private readonly string _format ;
492- public GenericTextContent ( string format , string extension , string text ) : base ( text ) {
493- _format = format ;
494- Extensions = new [ ] { extension } ;
495- }
496- public override string [ ] Extensions { get ; }
497- public override string Description => Resources . str_preview ;
498- public override void AddTo ( IDataObject data ) {
499- data . SetData ( _format , Text ) ;
500- }
501- public override string TextPreview ( string extension ) {
502- return Text ;
503- }
504527 }
505528
506529
507530 public class UrlContent : TextLikeContent {
531+ public static readonly string [ ] FORMATS = { DataFormats . Text } ;
508532 public static readonly string [ ] EXTENSIONS = { "url" } ;
509- public UrlContent ( string text ) : base ( text ) { }
510- public override string [ ] Extensions => EXTENSIONS ;
533+
534+ public UrlContent ( string text ) : base ( FORMATS , EXTENSIONS , text ) { }
535+
511536 public override string Description => Resources . str_preview_url ;
537+
512538 public override void SaveAs ( string path , string extension , bool append = false ) {
513- if ( append )
514- throw new AppendNotSupportedException ( ) ;
539+ if ( append ) throw new AppendNotSupportedException ( ) ;
515540 Save ( path , "[InternetShortcut]\n URL=" + Text ) ;
516541 }
542+
543+ }
544+
545+
546+ public class CalendarContent : TextLikeContent {
547+ public static readonly string [ ] FORMATS = { "text/calendar" , "ics" , DataFormats . Text } ;
548+ public static readonly string [ ] EXTENSIONS = { "ics" } ;
549+
550+ public CalendarContent ( string text ) : base ( FORMATS , EXTENSIONS , text ) { }
551+
552+ public override string Description => string . Format ( Resources . str_preview_calendar ,
553+ Text . ToUpperInvariant ( ) . Split ( '\n ' ) . Count ( l => l . Trim ( ) . StartsWith ( "BEGIN:VEVENT" ) ) ) ;
554+
517555 public override void AddTo ( IDataObject data ) {
518- data . SetData ( DataFormats . Text , Text ) ;
556+ // Note: Spec says UTF8 is the default, but thunderbird only accepts UTF16 (simply called "Unicode" in .NET)
557+ AddTo ( data , Text , Encoding . Unicode ) ;
519558 }
520- public override string TextPreview ( string extension ) {
521- return Text ;
559+
560+ public override void SaveAs ( string path , string extension , bool append = false ) {
561+ if ( append ) throw new AppendNotSupportedException ( ) ;
562+ base . SaveAs ( path , extension , append ) ;
522563 }
564+
523565 }
524566
525567
@@ -767,20 +809,13 @@ public static ClipboardContents FromClipboard() {
767809 if ( ReadClipboardHtml ( ) is string html )
768810 container . Contents . Add ( new HtmlContent ( html ) ) ;
769811
770- if ( ReadClipboardString ( DataFormats . CommaSeparatedValue , "text/csv" , "text/tab-separated-values" ) is string csv )
771- container . Contents . Add ( new CsvContent ( csv ) ) ;
772-
773- if ( ReadClipboardString ( DataFormats . SymbolicLink ) is string lnk )
774- container . Contents . Add ( new GenericTextContent ( DataFormats . SymbolicLink , "slk" , lnk ) ) ;
775-
776- if ( ReadClipboardString ( DataFormats . Rtf , "text/rtf" ) is string rtf )
777- container . Contents . Add ( new GenericTextContent ( DataFormats . Rtf , "rtf" , rtf ) ) ;
778-
779- if ( ReadClipboardString ( DataFormats . Dif ) is string dif )
780- container . Contents . Add ( new GenericTextContent ( DataFormats . Dif , "dif" , dif ) ) ;
781-
782- if ( ReadClipboardString ( "image/svg+xml" , "svg" ) is string svg )
783- container . Contents . Add ( new SvgContent ( svg ) ) ;
812+ if ( ReadClipboardString ( CsvContent . FORMATS ) is string csv ) container . Contents . Add ( new CsvContent ( csv ) ) ;
813+ if ( ReadClipboardString ( SlkContent . FORMATS ) is string lnk ) container . Contents . Add ( new SlkContent ( lnk ) ) ;
814+ if ( ReadClipboardString ( RtfContent . FORMATS ) is string rtf ) container . Contents . Add ( new RtfContent ( rtf ) ) ;
815+ if ( ReadClipboardString ( DifContent . FORMATS ) is string dif ) container . Contents . Add ( new DifContent ( dif ) ) ;
816+ if ( ReadClipboardString ( SvgContent . FORMATS ) is string svg ) container . Contents . Add ( new SvgContent ( svg ) ) ;
817+ if ( ReadClipboardString ( CalendarContent . FORMATS ) is string ics && ics . ToUpperInvariant ( ) . StartsWith ( "BEGIN:VCALENDAR" ) )
818+ container . Contents . Add ( new CalendarContent ( ics ) ) ;
784819
785820 if ( Clipboard . ContainsText ( ) && Uri . IsWellFormedUriString ( Clipboard . GetText ( ) . Trim ( ) , UriKind . Absolute ) )
786821 container . Contents . Add ( new UrlContent ( Clipboard . GetText ( ) . Trim ( ) ) ) ;
@@ -824,14 +859,37 @@ private static string ReadClipboardHtml() {
824859
825860 private static string ReadClipboardString ( params string [ ] formats ) {
826861 foreach ( var format in formats ) {
827- if ( ! Clipboard . ContainsData ( format ) )
828- continue ;
829- var data = Clipboard . GetData ( format ) ;
830- switch ( data ) {
831- case string str :
832- return str ;
833- case MemoryStream stream :
834- return new StreamReader ( stream ) . ReadToEnd ( ) . TrimEnd ( '\0 ' ) ;
862+ // Standard formats with native support
863+ foreach ( var simpleFormat in new Dict < string , TextDataFormat > {
864+ { DataFormats . Text , TextDataFormat . Text } ,
865+ { DataFormats . UnicodeText , TextDataFormat . UnicodeText } ,
866+ { DataFormats . Html , TextDataFormat . Html } ,
867+ { DataFormats . Rtf , TextDataFormat . Rtf } ,
868+ { DataFormats . CommaSeparatedValue , TextDataFormat . CommaSeparatedValue } ,
869+ } ) {
870+ if ( string . Equals ( format , simpleFormat . Key ) && Clipboard . ContainsText ( simpleFormat . Value ) ) {
871+ return Clipboard . GetText ( simpleFormat . Value ) ;
872+ }
873+ }
874+
875+ // Other non-standard formats
876+ if ( Clipboard . ContainsData ( format ) ) {
877+ switch ( Clipboard . GetData ( format ) ) {
878+ // Serialized string
879+ case string str :
880+ return str ;
881+ // Raw string
882+ case MemoryStream stream :
883+ var encoding = Encoding . UTF8 ;
884+ if ( stream . Length > 2 ) {
885+ // Heuristic to tell UTF8 and UTF16 apart
886+ int b0 = stream . ReadByte ( ) , b1 = stream . ReadByte ( ) ;
887+ if ( b0 == 0xFE && b1 == 0xFF ) encoding = Encoding . BigEndianUnicode ;
888+ if ( b0 == 0xFF && b1 == 0xFE || b1 == 0x00 ) encoding = Encoding . Unicode ;
889+ stream . Position = 0 ;
890+ }
891+ return new StreamReader ( stream , encoding ) . ReadToEnd ( ) . TrimEnd ( '\0 ' ) ;
892+ }
835893 }
836894 }
837895 return null ;
@@ -897,12 +955,14 @@ public static ClipboardContents FromFile(string path) {
897955 container . Contents . Add ( new SvgContent ( contents ) ) ;
898956 if ( ext == "csv" )
899957 container . Contents . Add ( new CsvContent ( contents ) ) ;
900- if ( ext == "dif" )
901- container . Contents . Add ( new GenericTextContent ( DataFormats . Dif , ext , contents ) ) ;
902- if ( ext == "rtf" )
903- container . Contents . Add ( new GenericTextContent ( DataFormats . Rtf , ext , contents ) ) ;
904- if ( ext == "syk" )
905- container . Contents . Add ( new GenericTextContent ( DataFormats . SymbolicLink , ext , contents ) ) ;
958+ if ( ext == "ics" )
959+ container . Contents . Add ( new CalendarContent ( contents ) ) ;
960+ if ( DifContent . EXTENSIONS . Contains ( ext ) )
961+ container . Contents . Add ( new DifContent ( contents ) ) ;
962+ if ( RtfContent . EXTENSIONS . Contains ( ext ) )
963+ container . Contents . Add ( new RtfContent ( contents ) ) ;
964+ if ( SlkContent . EXTENSIONS . Contains ( ext ) )
965+ container . Contents . Add ( new SlkContent ( contents ) ) ;
906966
907967 } else {
908968 container . Contents . Add ( new TextContent ( path ) ) ;
0 commit comments