Skip to content

Commit 5ee9311

Browse files
committed
Object oriented clipboard content management
1 parent bb22911 commit 5ee9311

File tree

8 files changed

+376
-326
lines changed

8 files changed

+376
-326
lines changed

PasteIntoFile/ClipboardContents.cs

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Drawing;
4+
using System.Drawing.Imaging;
5+
using System.IO;
6+
using System.Linq;
7+
using System.Text;
8+
using System.Text.RegularExpressions;
9+
using System.Windows.Forms;
10+
using PasteIntoFile.Properties;
11+
12+
namespace PasteIntoFile {
13+
14+
/// <summary>
15+
/// This is the base class to hold clipboard contents, metadata, and perform actions with it
16+
/// </summary>
17+
public abstract class BaseContent {
18+
19+
/// <summary>
20+
/// List of known file extensions for this content type
21+
/// The first extension is considered the default
22+
/// </summary>
23+
public abstract string[] Extensions { get; }
24+
25+
/// <summary>
26+
/// A friendly description of the contents
27+
/// </summary>
28+
public abstract string Description { get; }
29+
30+
/// <summary>
31+
/// The actual data content
32+
/// </summary>
33+
protected object Data;
34+
35+
/// <summary>
36+
/// The default extension for this content type
37+
/// </summary>
38+
public string DefaultExtension {
39+
get {
40+
var ext = Extensions;
41+
return ext.Length > 0 ? ext.First() : "";
42+
}
43+
}
44+
45+
/// <summary>
46+
/// Saves the content to a file
47+
/// </summary>
48+
/// <param name="path">Full path where to save (incl. filename and extension)</param>
49+
/// <param name="extension">format to use for saving</param>
50+
public abstract void SaveAs(string path, string extension);
51+
52+
}
53+
54+
55+
public class ImageContent : BaseContent {
56+
public ImageContent(Image image) {
57+
Data = image;
58+
}
59+
public Image Image => Data as Image;
60+
public override string[] Extensions => new[] { "png", "bpm", "emf", "gif", "ico", "jpg", "tif", "wmf" };
61+
public override string Description => string.Format(Resources.str_preview_image, Image.Width, Image.Height);
62+
public override void SaveAs(string path, string extension) {
63+
ImageFormat imageFormat;
64+
switch (extension) {
65+
case "bpm": imageFormat = ImageFormat.Bmp; break;
66+
case "emf": imageFormat = ImageFormat.Emf; break;
67+
case "gif": imageFormat = ImageFormat.Gif; break;
68+
case "ico": imageFormat = ImageFormat.Icon; break;
69+
case "jpg": imageFormat = ImageFormat.Jpeg; break;
70+
case "tif": imageFormat = ImageFormat.Tiff; break;
71+
case "wmf": imageFormat = ImageFormat.Wmf; break;
72+
default: imageFormat = ImageFormat.Png; break;
73+
}
74+
Image.Save(path, imageFormat);
75+
}
76+
}
77+
78+
79+
public abstract class TextLikeContent : BaseContent {
80+
public TextLikeContent(string text) {
81+
Data = text;
82+
}
83+
public string Text => Data as string;
84+
}
85+
86+
87+
public class TextContent : TextLikeContent {
88+
public TextContent(string text) : base(text) { }
89+
public override string[] Extensions => new[] { "txt", "md", "log", "bat", "ps1", "java", "js", "cpp", "cs", "py", "css", "html", "php", "json", "csv"};
90+
public override string Description => string.Format(Resources.str_preview_text, Text.Length, Text.Split('\n').Length);
91+
public override void SaveAs(string path, string extension) {
92+
File.WriteAllText(path, Text, Encoding.UTF8);
93+
}
94+
}
95+
96+
97+
public class HtmlContent : TextLikeContent {
98+
public HtmlContent(string text) : base(text) { }
99+
public override string[] Extensions => new[] { "html", "htm", "xhtml" };
100+
public override string Description => Resources.str_preview_html;
101+
public override void SaveAs(string path, string extension) {
102+
var html = Text;
103+
if (!html.StartsWith("<!DOCTYPE html>"))
104+
html = "<!DOCTYPE html>\n" + html;
105+
File.WriteAllText(path, html, Encoding.UTF8);
106+
}
107+
}
108+
109+
110+
public class CsvContent : TextLikeContent {
111+
public CsvContent(string text) : base(text) { }
112+
public override string[] Extensions => new[] { "csv", "tsv", "tab" };
113+
public override string Description => Resources.str_preview_csv;
114+
public override void SaveAs(string path, string extension) {
115+
File.WriteAllText(path, Text, Encoding.UTF8);
116+
}
117+
}
118+
119+
120+
public class SylkContent : TextLikeContent {
121+
public SylkContent(string text) : base(text) { }
122+
public override string[] Extensions => new[] { "slk" };
123+
public override string Description => Resources.str_preview_sylk;
124+
public override void SaveAs(string path, string extension) {
125+
File.WriteAllText(path, Text, Encoding.ASCII);
126+
}
127+
}
128+
129+
130+
public class DifContent : TextLikeContent {
131+
public DifContent(string text) : base(text) { }
132+
public override string[] Extensions => new[] { "dif" };
133+
public override string Description => Resources.str_preview_dif;
134+
public override void SaveAs(string path, string extension) {
135+
File.WriteAllText(path, Text, Encoding.ASCII);
136+
}
137+
}
138+
139+
140+
public class RtfContent : TextLikeContent {
141+
public RtfContent(string text) : base(text) { }
142+
public override string[] Extensions => new[] { "rtf" };
143+
public override string Description => Resources.str_preview_rtf;
144+
public override void SaveAs(string path, string extension) {
145+
File.WriteAllText(path, Text, Encoding.ASCII);
146+
}
147+
}
148+
149+
150+
public class UrlContent : TextLikeContent {
151+
public UrlContent(string text) : base(text) { }
152+
public override string[] Extensions => new[] { "url" };
153+
public override string Description => Resources.str_preview_url;
154+
public override void SaveAs(string path, string extension) {
155+
File.WriteAllLines(path, new[] {
156+
@"[InternetShortcut]",
157+
@"URL=" + Text
158+
}, Encoding.UTF8);
159+
}
160+
}
161+
162+
163+
164+
165+
166+
/// <summary>
167+
/// Class to hold all supported clipboard content
168+
/// and provide concise methods to access it
169+
/// </summary>
170+
public class ClipboardContents {
171+
172+
public DateTime Timestamp;
173+
public readonly IList<BaseContent> Contents = new List<BaseContent>();
174+
175+
/// <summary>
176+
/// Return contents matching the given extension, fall back to text content if available
177+
/// </summary>
178+
/// <param name="ext"></param>
179+
/// <returns></returns>
180+
public BaseContent ForExtension(string ext) {
181+
foreach (var content in Contents) {
182+
if (content.Extensions.Contains(ext))
183+
return content;
184+
}
185+
// if ext is not compatible with text, return null ...
186+
foreach (var reserved in new BaseContent[]
187+
{new ImageContent(null), new UrlContent(null)}) {
188+
if (reserved.Extensions.Contains(ext))
189+
return null;
190+
}
191+
// ... otherwise default to text
192+
return Contents.OfType<TextContent>().FirstOrDefault();
193+
}
194+
195+
/// <summary>
196+
/// Return contents of specific type
197+
/// </summary>
198+
/// <param name="type">Any type of BaseContent or its subclasses</param>
199+
/// <returns></returns>
200+
public BaseContent ForContentType(Type type) {
201+
foreach (var content in Contents) {
202+
if (content.GetType() == type)
203+
return content;
204+
}
205+
return null;
206+
}
207+
208+
/// <summary>
209+
/// Determines the primary type of data in this container according to a custom prioritisation order
210+
/// </summary>
211+
public BaseContent PrimaryContent => ForContentType(typeof(ImageContent)) ??
212+
ForContentType(typeof(TextContent)) ??
213+
ForContentType(typeof(BaseContent));
214+
215+
/// <summary>
216+
/// Static constructor to create an instance from the current clipboard data
217+
/// </summary>
218+
/// <returns></returns>
219+
public static ClipboardContents FromClipboard() {
220+
var container = new ClipboardContents {
221+
Timestamp = DateTime.Now
222+
};
223+
224+
// Read all supported clipboard data
225+
// https://docs.microsoft.com/en-us/windows/win32/dataxchg/standard-clipboard-formats
226+
227+
if (Clipboard.ContainsImage())
228+
container.Contents.Add(new ImageContent(Clipboard.GetImage()));
229+
if (Clipboard.ContainsData(DataFormats.Html))
230+
container.Contents.Add(new HtmlContent(ReadClipboardHtml()));
231+
if (Clipboard.ContainsData(DataFormats.CommaSeparatedValue))
232+
container.Contents.Add(new CsvContent(ReadClipboardString(DataFormats.CommaSeparatedValue)));
233+
if (Clipboard.ContainsData(DataFormats.SymbolicLink))
234+
container.Contents.Add(new SylkContent(ReadClipboardString(DataFormats.SymbolicLink)));
235+
if (Clipboard.ContainsData(DataFormats.Rtf))
236+
container.Contents.Add(new RtfContent(ReadClipboardString(DataFormats.Rtf)));
237+
if (Clipboard.ContainsData(DataFormats.Dif))
238+
container.Contents.Add(new DifContent(ReadClipboardString(DataFormats.Dif)));
239+
240+
if (Clipboard.ContainsFileDropList() && !Clipboard.ContainsText())
241+
// save list of file paths instead
242+
container.Contents.Add(new TextContent(string.Join("\n", Clipboard.GetFileDropList().Cast<string>().ToList())));
243+
244+
if (Clipboard.ContainsText() && Uri.IsWellFormedUriString(Clipboard.GetText().Trim(), UriKind.RelativeOrAbsolute))
245+
container.Contents.Add(new UrlContent(Clipboard.GetText().Trim()));
246+
247+
// make sure text content comes last, as it may includes extensions from previous formats
248+
if (Clipboard.ContainsText())
249+
container.Contents.Add(new TextContent(Clipboard.GetText()));
250+
251+
252+
return container;
253+
}
254+
255+
private static string ReadClipboardHtml() {
256+
var content = Clipboard.GetText(TextDataFormat.Html);
257+
var match = Regex.Match(content, @"StartHTML:(?<startHTML>\d*).*EndHTML:(?<endHTML>\d*)", RegexOptions.Singleline);
258+
if (match.Success) {
259+
var startHtml = Math.Max(int.Parse(match.Groups["startHTML"].Value), 0);
260+
var endHtml = Math.Min(int.Parse(match.Groups["endHTML"].Value), content.Length);
261+
return content.Substring(startHtml, endHtml-startHtml);
262+
}
263+
return null;
264+
}
265+
266+
private static string ReadClipboardString(string format) {
267+
var data = Clipboard.GetData(format);
268+
switch (data) {
269+
case string str:
270+
return str;
271+
case MemoryStream stream:
272+
return new StreamReader(stream).ReadToEnd().TrimEnd('\0');
273+
default:
274+
return null;
275+
}
276+
}
277+
278+
}
279+
}

0 commit comments

Comments
 (0)