diff --git a/Core/FormBrowser.cs b/Core/FormBrowser.cs index fa94e2cc..abc88fee 100644 --- a/Core/FormBrowser.cs +++ b/Core/FormBrowser.cs @@ -18,6 +18,7 @@ private static UserConfig Config{ private readonly ChromiumWebBrowser browser; private readonly TweetDeckBridge bridge; + private readonly FormNotification notification; public FormBrowser(){ InitializeComponent(); @@ -29,6 +30,9 @@ public FormBrowser(){ browser.RegisterJsObject("$TD",bridge); Controls.Add(browser); + + notification = new FormNotification(this); + notification.Show(this); } protected override void WndProc(ref Message m){ @@ -97,5 +101,9 @@ public void OpenSettings(){ public void OpenAbout(){ // TODO } + + public void OnTweetPopup(string tweetHtml, string tweetColumn){ + notification.ShowNotification(new TweetNotification(tweetHtml)); + } } -} +} \ No newline at end of file diff --git a/Core/FormNotification.Designer.cs b/Core/FormNotification.Designer.cs new file mode 100644 index 00000000..c68cb224 --- /dev/null +++ b/Core/FormNotification.Designer.cs @@ -0,0 +1,54 @@ +namespace TweetDick.Core { + partial class FormNotification { + /// <summary> + /// Required designer variable. + /// </summary> + private System.ComponentModel.IContainer components = null; + + /// <summary> + /// Clean up any resources being used. + /// </summary> + /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param> + protected override void Dispose(bool disposing) { + if (disposing && (components != null)) { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// <summary> + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// </summary> + private void InitializeComponent() { + this.components = new System.ComponentModel.Container(); + this.timer = new System.Windows.Forms.Timer(this.components); + this.SuspendLayout(); + // + // timer + // + this.timer.Tick += new System.EventHandler(this.timer_Tick); + // + // FormNotification + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(284, 118); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.SizableToolWindow; + this.Location = new System.Drawing.Point(32000, 32000); + this.Name = "FormNotification"; + this.ShowInTaskbar = false; + this.StartPosition = System.Windows.Forms.FormStartPosition.Manual; + this.Text = "TweetDick"; + this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.FormNotification_FormClosing); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.Timer timer; + } +} \ No newline at end of file diff --git a/Core/FormNotification.cs b/Core/FormNotification.cs new file mode 100644 index 00000000..504c5361 --- /dev/null +++ b/Core/FormNotification.cs @@ -0,0 +1,86 @@ +using System.Windows.Forms; +using CefSharp; +using CefSharp.WinForms; +using System.Drawing; +using System; +using System.Text; +using System.Collections.Generic; +using TweetDick.Core.Handling; + +namespace TweetDick.Core{ + partial class FormNotification : Form{ + private readonly FormBrowser owner; + private readonly ChromiumWebBrowser browser; + + private readonly Queue<TweetNotification> tweetQueue = new Queue<TweetNotification>(4); + + public FormNotification(FormBrowser owner){ + InitializeComponent(); + + this.owner = owner; + + browser = new ChromiumWebBrowser(""){ MenuHandler = new MenuHandlerEmpty() }; + Controls.Add(browser); + } + + public void ShowNotification(TweetNotification notification){ + Screen screen = Screen.FromControl(owner); + Location = new Point(screen.WorkingArea.X+screen.WorkingArea.Width-16-Width,screen.WorkingArea.Y+16); + + tweetQueue.Enqueue(notification); + + if (!timer.Enabled){ + LoadNextNotification(); + } + } + + public void HideNotification(){ + browser.Load("about:blank"); + Location = new Point(32000,32000); + } + + private void LoadNextNotification(){ + TweetNotification tweet = tweetQueue.Dequeue(); + + browser.Load("about:blank"); + browser.LoadHtml(tweet.GenerateHtml(),"http://tweetdeck.twitter.com/"); + + timer.Stop(); + timer.Interval = 5000; + timer.Start(); + } + + private void timer_Tick(object sender, EventArgs e){ + if (tweetQueue.Count > 0){ + LoadNextNotification(); + } + else{ + HideNotification(); + } + } + + private void FormNotification_FormClosing(object sender, FormClosingEventArgs e){ + if (e.CloseReason == CloseReason.UserClosing){ + HideNotification(); + tweetQueue.Clear(); + e.Cancel = true; + } + } + + private class MenuHandlerEmpty : IContextMenuHandler{ + public void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model){ + model.Clear(); + } + + public bool OnContextMenuCommand(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags){ + return false; + } + + public void OnContextMenuDismissed(IWebBrowser browserControl, IBrowser browser, IFrame frame){} + + public bool RunContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model, IRunContextMenuCallback callback){ + return false; + } + } + } +} diff --git a/Core/Handling/TweetDeckBridge.cs b/Core/Handling/TweetDeckBridge.cs index f68e0c5d..0dc6ffc6 100644 --- a/Core/Handling/TweetDeckBridge.cs +++ b/Core/Handling/TweetDeckBridge.cs @@ -6,12 +6,30 @@ public TweetDeckBridge(FormBrowser form){ this.form = form; } + public void LoadFontSizeClass(string fsClass){ + form.InvokeSafe(() => { + TweetNotification.SetFontSizeClass(fsClass); + }); + } + + public void LoadNotificationHeadContents(string headContents){ + form.InvokeSafe(() => { + TweetNotification.SetHeadTag(headContents); + }); + } + public void OpenSettingsMenu(){ form.InvokeSafe(() => { form.OpenSettings(); }); } + public void OnTweetPopup(string tweetHtml, string tweetColumn){ + form.InvokeSafe(() => { + form.OnTweetPopup(tweetHtml,tweetColumn); + }); + } + public void Log(string data){ System.Diagnostics.Debug.WriteLine(data); } diff --git a/Core/Handling/TweetNotification.cs b/Core/Handling/TweetNotification.cs new file mode 100644 index 00000000..f9f30acd --- /dev/null +++ b/Core/Handling/TweetNotification.cs @@ -0,0 +1,34 @@ +using System.Text; + +namespace TweetDick.Core.Handling{ + sealed class TweetNotification{ + private static string FontSizeClass { get; set; } + private static string HeadTag { get; set; } + + public static void SetFontSizeClass(string newFSClass){ + FontSizeClass = newFSClass; + } + + public static void SetHeadTag(string headContents){ + HeadTag = headContents; + } + + private readonly string html; + + public TweetNotification(string html){ + this.html = html; + } + + public string GenerateHtml(){ + StringBuilder build = new StringBuilder(); + build.Append("<!DOCTYPE html>"); + build.Append("<html class='os-windows ").Append(FontSizeClass).Append("'>"); + build.Append("<head>").Append(HeadTag).Append("</head>"); + build.Append("<body class='hearty'><div class='app-columns-container'><div class='column' style='width:100%'>"); + build.Append(html); + build.Append("</div></div></body>"); + build.Append("</html>"); + return build.ToString(); + } + } +} diff --git a/Resources/code.js b/Resources/code.js index 3e9c0cd3..6c0a9777 100644 --- a/Resources/code.js +++ b/Resources/code.js @@ -1,9 +1,19 @@ (function($,$TD){ + // + // Constant: List of valid font size classes. + // + var fontSizeClasses = [ "txt-base-smallest", "txt-base-small", "txt-base-medium", "txt-base-large", "txt-base-largest" ]; + // // Variable: Says whether TweetDick events was initialized. // var isInitialized = false; + // + // Variable: Previous font size class in the <html> tag. + // + var prevFontSizeClass; + // // Function: Initializes TweetDick events. Called after the website app is loaded. // @@ -29,9 +39,94 @@ },0); }); + // Tweet notifications + (function(){ + var columns = $(".js-app-columns").first(); + + var refreshColumnObservers = function(){ + columns.children().each(function(){ + registerTweetObserverForColumn($(this)); + }); + }; + + new MutationObserver(refreshColumnObservers).observe(columns[0],{ + childList: true + }); + + refreshColumnObservers(); + })(); + isInitialized = true; }; + // + // Function: Registers an observer to a TweetDeck column, which reports new tweets. + // + var registerTweetObserverForColumn = function(column){ + if (column[0].hasAttribute("data-tweetdick-observed"))return; + + var mid = column; + mid = mid.children().first(); + mid = mid.children().first(); + mid = mid.children(".js-column-content").first(); + mid = mid.children(".js-column-scroller").first(); + + var container = mid.children(".js-chirp-container").first(); + if (container.length == 0)return; + + new MutationObserver(function(mutations){ + // TODO check if popups are enabled first + + Array.prototype.forEach.call(mutations,function(mutation){ + Array.prototype.forEach.call(mutation.addedNodes,function(node){ + if (node.tagName != "ARTICLE")return; + + onNewTweet(column,node); + }); + }); + }).observe(container[0],{ + childList: true + }); + + column[0].setAttribute("data-tweetdick-observed",""); + }; + + // + // Function: Event callback for a new tweet. + // + var onNewTweet = function(column, tweet){ + var html = $(tweet.outerHTML); + html.find("footer:first").remove(); + + $TD.onTweetPopup(html.html(),""); // TODO + }; + + // + // Function: Retrieves the font size using <html> class attribute. + // + var getFontSizeClass = function(){ + for(var index = 0; index < fontSizeClasses.length; index++){ + if (document.documentElement.classList.contains(fontSizeClasses[index])){ + return fontSizeClasses[index]; + } + } + + return fontSizeClasses[0]; + }; + + // + // Function: Retrieves the tags to be put into <head> for notification HTML code. + // + var getNotificationHeadContents = function(){ + var tags = []; + + $(document.head).children("link[rel='stylesheet'],meta[charset],meta[http-equiv]").each(function(){ + tags.push($(this)[0].outerHTML); + }); + + return tags.join(""); + }; + // // Block: Observe the app <div> element and initialize TweetDick whenever possible. // @@ -48,4 +143,31 @@ attributes: true, attributeFilter: [ "class" ] }); + + // + // Block: Observe changes in <html> class to update font size. + // + new MutationObserver(function(mutations){ + var fsClass = getFontSizeClass(); + + if (fsClass != prevFontSizeClass){ + prevFontSizeClass = fsClass; + $TD.loadFontSizeClass(fsClass); + } + }).observe(document.documentElement,{ + attributes: true, + attributeFilter: [ "class" ] + }); + + // + // Block: Observe stylesheet swapping. + // + new MutationObserver(function(mutations){ + $TD.loadNotificationHeadContents(getNotificationHeadContents()); + }).observe(document.head.querySelector("[http-equiv='Default-Style']"),{ + attributes: true, + attributeFilter: [ "content" ] + }); + + $TD.loadNotificationHeadContents(getNotificationHeadContents()); })($,$TD); diff --git a/TweetDick.csproj b/TweetDick.csproj index 8197786b..13c9dbb9 100644 --- a/TweetDick.csproj +++ b/TweetDick.csproj @@ -86,6 +86,13 @@ <Compile Include="Core\FormBrowser.Designer.cs"> <DependentUpon>FormBrowser.cs</DependentUpon> </Compile> + <Compile Include="Core\FormNotification.cs"> + <SubType>Form</SubType> + </Compile> + <Compile Include="Core\FormNotification.Designer.cs"> + <DependentUpon>FormNotification.cs</DependentUpon> + </Compile> + <Compile Include="Core\Handling\TweetNotification.cs" /> <Compile Include="Core\RichTextLabel.cs"> <SubType>Component</SubType> </Compile>