├── .gitignore ├── sc.sh ├── Praeclarum Resources.sketch ├── global.json ├── Praeclarum.Android ├── Resources │ ├── values │ │ └── Strings.xml │ └── AboutResources.txt ├── UI │ ├── DocumentListAppActivity.cs │ ├── DocumentEditor.cs │ └── Canvas.cs ├── Praeclarum.Android.csproj └── App │ └── TextDocument.cs ├── Praeclarum ├── UI │ ├── IThemeAware.cs │ ├── UserInterface.cs │ ├── Theme.cs │ ├── ITimer.cs │ ├── IView.cs │ ├── ITextEditor.cs │ ├── IDocumentEditor.cs │ ├── ICanvas.cs │ ├── IDocumentsView.cs │ ├── OpenUrlCommand.cs │ └── PForm.cs ├── App │ ├── ProPriceSpec.cs │ ├── IDocument.cs │ ├── Application.cs │ ├── IAppSettings.cs │ ├── AIChat.cs │ ├── DocumentApplication.cs │ ├── Document.cs │ ├── StoreManager.cs │ ├── DocumentAppSettings.cs │ └── DocumentReference.cs ├── IO │ ├── IConsole.cs │ ├── FileSystemManager.cs │ └── EmptyFileSystem.cs ├── StringHelper.cs ├── Command.cs ├── Graphics │ ├── Vector.cs │ ├── Rectangle.cs │ ├── NullGraphics.cs │ └── Point.cs ├── Praeclarum.Shared.shproj ├── Properties │ └── AssemblyInfo.cs ├── AsyncExtensions.cs ├── Localization.cs ├── Praeclarum.csproj ├── StringRange.cs ├── Praeclarum.Shared.projitems ├── Log.cs ├── ListDiff.cs └── NumberFormatting.cs ├── README.md ├── Praeclarum.iOS ├── GlobalSuppressions.cs ├── UI │ ├── ViewAnimation.cs │ ├── ProceduralImage.cs │ ├── DocumentThumbnailsAppDelegate.cs │ ├── StorageSection.cs │ ├── AnalyticsSection.cs │ ├── DarkModeSection.cs │ ├── SelectableButtonItem.cs │ ├── TitleView.cs │ ├── Editor.cs │ ├── ActivityIndicator.cs │ ├── GalleryViewController.cs │ ├── TranslateSection.cs │ ├── MoveDocumentsForm.cs │ ├── OldForm.cs │ ├── DocumentEditor.cs │ ├── Canvas.cs │ ├── ImageCache.cs │ ├── DocumentListAppDelegate.cs │ ├── ScrollableCanvas.cs │ ├── StorageForm.cs │ └── TextInputController.cs ├── App │ ├── ReviewNagging.cs │ └── TextDocument.cs ├── NSMutableAttributedStringWrapper.cs └── Praeclarum.iOS.csproj ├── Praeclarum.Mac ├── UI │ ├── UserInterfaceWindow.cs │ ├── Timer.cs │ ├── Canvas.cs │ └── DocumentAppDelegate.cs ├── Praeclarum.Mac.Shared.projitems ├── Praeclarum.Mac.Shared.shproj └── Praeclarum.Mac.csproj ├── Praeclarum.Net.sln ├── .github └── workflows │ └── build.yml ├── Directory.Packages.props ├── Praeclarum.Utilities └── Praeclarum.Utilities.csproj ├── Praeclarum.Utilities.sln └── Praeclarum.sln /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | bin 3 | obj 4 | 5 | *.userprefs 6 | .vs 7 | -------------------------------------------------------------------------------- /sc.sh: -------------------------------------------------------------------------------- 1 | fsharpi --exec ../StopCrashing/StopCrashing.fsx Praeclarum.sln 2 | -------------------------------------------------------------------------------- /Praeclarum Resources.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/praeclarum/Praeclarum/HEAD/Praeclarum Resources.sketch -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "10.0.100-rc.1.25451.107", 4 | "workloadVersion": "10.0.100-rc.1.25458.2" 5 | } 6 | } -------------------------------------------------------------------------------- /Praeclarum.Android/Resources/values/Strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Praeclarum.Android 4 | 5 | -------------------------------------------------------------------------------- /Praeclarum/UI/IThemeAware.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Praeclarum.UI 4 | { 5 | public interface IThemeAware 6 | { 7 | void ApplyTheme (Theme theme); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Praeclarum/UI/UserInterface.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Praeclarum.UI 4 | { 5 | public class UserInterface 6 | { 7 | public IView View { get; set; } 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /Praeclarum/App/ProPriceSpec.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | namespace Praeclarum.App 3 | { 4 | public struct ProPriceSpec 5 | { 6 | public int Months; 7 | public string Name; 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Frank's Utilties 2 | 3 | [![Build](https://github.com/praeclarum/Praeclarum/actions/workflows/build.yml/badge.svg)](https://github.com/praeclarum/Praeclarum/actions/workflows/build.yml) 4 | 5 | -------------------------------------------------------------------------------- /Praeclarum/IO/IConsole.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Praeclarum 4 | { 5 | public interface IConsole 6 | { 7 | TextReader In { get; } 8 | TextWriter Out { get; } 9 | } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /Praeclarum/UI/Theme.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Praeclarum.UI 4 | { 5 | public partial class Theme 6 | { 7 | public Graphics.Color DocumentBackgroundGraphicsColor = Graphics.Color.FromWhite (0.0f, 0.0f); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Praeclarum/UI/ITimer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Praeclarum 4 | { 5 | public interface ITimer 6 | { 7 | event EventHandler Tick; 8 | bool Enabled { get; set; } 9 | TimeSpan Interval { get; set; } 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /Praeclarum/UI/IView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Praeclarum.Graphics; 3 | 4 | namespace Praeclarum.UI 5 | { 6 | public interface IView 7 | { 8 | Color BackgroundColor { get; set; } 9 | 10 | RectangleF Bounds { get; } 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /Praeclarum.iOS/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage ("Obsoleted APIs", "CA1416", Justification = "False positives")] 2 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage ("Obsoleted APIs", "CA1422", Justification = "False positives")] 3 | -------------------------------------------------------------------------------- /Praeclarum/UI/ITextEditor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Praeclarum.UI 4 | { 5 | public interface ITextEditor : IView 6 | { 7 | void Modify (Action action); 8 | 9 | StringRange SelectedRange { get; set; } 10 | void ReplaceText (StringRange range, string text); 11 | 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /Praeclarum/StringHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Praeclarum 4 | { 5 | public static class StringHelper 6 | { 7 | public static bool IsBlank (this string s) 8 | { 9 | if (s == null) return true; 10 | var len = s.Length; 11 | if (len == 0) return true; 12 | for (var i = 0; i < len; i++) 13 | if (!char.IsWhiteSpace (s[i])) 14 | return false; 15 | return true; 16 | } 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /Praeclarum.iOS/UI/ViewAnimation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UIKit; 3 | 4 | namespace Praeclarum.UI 5 | { 6 | public static class ViewAnimation 7 | { 8 | public static void Run (Action action, double duration, bool animated) 9 | { 10 | if (animated) { 11 | UIView.Animate (duration, () => { 12 | try { 13 | action(); 14 | } catch (Exception ex) { 15 | Log.Error (ex); 16 | } 17 | }); 18 | } else { 19 | action (); 20 | } 21 | } 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /Praeclarum/UI/IDocumentEditor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Praeclarum.App; 3 | using System.Threading.Tasks; 4 | 5 | namespace Praeclarum.UI 6 | { 7 | public interface IDocumentEditor 8 | { 9 | DocumentReference DocumentReference { get; } 10 | IDocument Document { get; } 11 | 12 | // IView EditorView { get; } 13 | 14 | void DidEnterBackground (); 15 | void WillEnterForeground (); 16 | void OnCreated (); 17 | 18 | void BindDocument (); 19 | Task SaveDocument (); 20 | void UnbindDocument (); 21 | 22 | void UnbindUI (); 23 | 24 | bool IsPreviewing { get; set; } 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /Praeclarum/Command.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace Praeclarum 7 | { 8 | public delegate Task AsyncAction (); 9 | 10 | public class Command 11 | { 12 | public string Name { get; set; } 13 | 14 | public AsyncAction Action { get; set; } 15 | 16 | public Command (string name, AsyncAction action = null) 17 | { 18 | Name = name.Localize (); 19 | Action = action; 20 | } 21 | 22 | public virtual async Task ExecuteAsync () 23 | { 24 | if (Action != null) 25 | await Action (); 26 | } 27 | } 28 | 29 | 30 | } 31 | 32 | -------------------------------------------------------------------------------- /Praeclarum/App/IDocument.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Praeclarum.App 5 | { 6 | public enum DocumentSaveOperation 7 | { 8 | ForCreating, 9 | ForOverwriting, 10 | } 11 | 12 | public enum DocumentChangeKind 13 | { 14 | Done, 15 | } 16 | 17 | public interface IDocument : IDisposable 18 | { 19 | bool IsOpen { get; } 20 | Task OpenAsync (); 21 | Task SaveAsync (string path, DocumentSaveOperation operation); 22 | Task CloseAsync (); 23 | void UpdateChangeCount (DocumentChangeKind changeKind); 24 | } 25 | 26 | public interface ITextDocument : IDocument 27 | { 28 | string TextData { get; set; } 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /Praeclarum/Graphics/Vector.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Praeclarum.Graphics 4 | { 5 | public struct VectorF 6 | { 7 | public float X, Y; 8 | 9 | public VectorF (float x, float y) 10 | { 11 | X = x; 12 | Y = y; 13 | } 14 | 15 | public override string ToString() 16 | { 17 | return string.Format("<{0}, {1}>", X, Y); 18 | } 19 | 20 | public static VectorF operator * (VectorF v, float s) 21 | { 22 | return new VectorF (v.X * s, v.Y * s); 23 | } 24 | 25 | public VectorF Rotate (double angle) 26 | { 27 | var cf = (float)Math.Cos (angle); 28 | var sf = (float)Math.Sin (angle); 29 | 30 | return new VectorF (X * cf - Y * sf, Y * cf + X * sf); 31 | } 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /Praeclarum.Android/UI/DocumentListAppActivity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using Android.App; 6 | using Android.Content; 7 | using Android.OS; 8 | using Android.Runtime; 9 | using Android.Views; 10 | using Android.Widget; 11 | 12 | namespace Praeclarum.UI 13 | { 14 | [Activity (Label = "DocumentListAppActivity")] 15 | public class DocumentListAppActivity : Activity 16 | { 17 | public static DocumentListAppActivity Shared { get; private set; } 18 | 19 | protected override void OnCreate (Bundle bundle) 20 | { 21 | base.OnCreate (bundle); 22 | 23 | Shared = this; 24 | 25 | // Create your application here 26 | } 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /Praeclarum.Mac/UI/UserInterfaceWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using AppKit; 3 | using System.Drawing; 4 | 5 | namespace Praeclarum.UI 6 | { 7 | public class UserInterfaceWindow : NSWindow 8 | { 9 | // UserInterface ui; 10 | 11 | public UserInterfaceWindow (UserInterface ui, RectangleF frame, NSScreen screen) 12 | : base (frame, 13 | NSWindowStyle.Titled | NSWindowStyle.Resizable | NSWindowStyle.Closable | NSWindowStyle.Miniaturizable, 14 | NSBackingStore.Buffered, 15 | false, 16 | screen) 17 | { 18 | // this.ui = ui; 19 | 20 | var view = ui.View as NSView; 21 | if (view != null) { 22 | 23 | this.ContentView = view; 24 | 25 | } 26 | } 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /Praeclarum.Android/Praeclarum.Android.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0-android 4 | 21 5 | disable 6 | false 7 | 1.0.0 8 | false 9 | 10 | 11 | 12 | 13 | UI\IThemeAware.cs 14 | 15 | 16 | UI\Theme.cs 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Praeclarum.iOS/UI/ProceduralImage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using CoreGraphics; 3 | using UIKit; 4 | 5 | namespace Praeclarum.UI 6 | { 7 | public class ProceduralImage 8 | { 9 | public delegate void DrawFunc(CGContext c); 10 | 11 | public float Width { get; set; } 12 | public float Height { get; set; } 13 | public DrawFunc Draw { get; set; } 14 | 15 | public ProceduralImage (float width, float height, DrawFunc draw) 16 | { 17 | Width = width; 18 | Height = height; 19 | Draw = draw; 20 | } 21 | 22 | public UIImage Generate () 23 | { 24 | UIGraphics.BeginImageContext (new CGSize (Width, Height)); 25 | 26 | var c = UIGraphics.GetCurrentContext (); 27 | 28 | if (Draw != null) { 29 | Draw (c); 30 | } 31 | 32 | var image = UIGraphics.GetImageFromCurrentImageContext (); 33 | 34 | UIGraphics.EndImageContext (); 35 | 36 | return image; 37 | } 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /Praeclarum/App/Application.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | 3 | using System; 4 | using Praeclarum.UI; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using Praeclarum.Graphics; 8 | 9 | namespace Praeclarum.App 10 | { 11 | public class Application 12 | { 13 | public virtual string Name { get { return "App"; } } 14 | public virtual string UrlScheme { get { return "app"; } } 15 | public virtual Color TintColor { get { return Colors.Blue; } } 16 | public virtual Color VibrantTintColor { get { return Colors.Blue; } } 17 | public virtual string ProSymbol { get { return "🔷"; } } 18 | public virtual string ProMarketing { get { return "Upgrade to Pro"; } } 19 | public virtual IEnumerable GetProPrices () => Enumerable.Empty (); 20 | public virtual string? AppGroup { get { return null; } } 21 | public virtual string? CloudKitContainerId => null; 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /Praeclarum.Mac/Praeclarum.Mac.Shared.projitems: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 5 | true 6 | {4E4684C2-1942-4B53-B8D5-F8E715716A3D} 7 | 8 | 9 | Praeclarum.Mac.Shared 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Praeclarum/Praeclarum.Shared.shproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {B8A8BCD4-7815-4994-A9AF-3E603C844835} 5 | 8.0.30703 6 | 2.0 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Praeclarum.Mac/Praeclarum.Mac.Shared.shproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {4E4684C2-1942-4B53-B8D5-F8E715716A3D} 5 | 8.0.30703 6 | 2.0 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Praeclarum/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | 4 | // Information about this assembly is defined by the following attributes. 5 | // Change them to the values specific to your project. 6 | [assembly: AssemblyTitle ("Praeclarum")] 7 | [assembly: AssemblyDescription ("")] 8 | [assembly: AssemblyConfiguration ("")] 9 | [assembly: AssemblyCompany ("")] 10 | [assembly: AssemblyProduct ("")] 11 | [assembly: AssemblyCopyright ("2013 Frank A. Krueger")] 12 | [assembly: AssemblyTrademark ("")] 13 | [assembly: AssemblyCulture ("")] 14 | // The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". 15 | // The form "{Major}.{Minor}.*" will automatically update the build and revision, 16 | // and "{Major}.{Minor}.{Build}.*" will update just the revision. 17 | [assembly: AssemblyVersion ("1.0.*")] 18 | // The following attributes are used to specify the signing key for the assembly, 19 | // if desired. See the Mono documentation for more information about signing. 20 | //[assembly: AssemblyDelaySign(false)] 21 | //[assembly: AssemblyKeyFile("")] 22 | 23 | -------------------------------------------------------------------------------- /Praeclarum.iOS/UI/DocumentThumbnailsAppDelegate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Foundation; 3 | using UIKit; 4 | using System.Linq; 5 | using System.Collections.Generic; 6 | 7 | namespace Praeclarum.UI 8 | { 9 | [Register ("DocumentThumbnailsAppDelegate")] 10 | public class DocumentThumbnailsAppDelegate : DocumentAppDelegate 11 | { 12 | protected override void SetRootViewController () 13 | { 14 | window.RootViewController = (UIViewController)docListNav ?? docBrowser; 15 | } 16 | 17 | protected override void ShowEditor (int docIndex, bool advance, bool animated, UIViewController newEditorVC) 18 | { 19 | // Debug.WriteLine ("SHOWING EDITOR"); 20 | var nc = docListNav; 21 | var vcs = nc.ViewControllers; 22 | var oldEditor = CurrentDocumentEditor; 23 | var nvcs = new List (vcs.OfType ()); 24 | nvcs.Add (newEditorVC); 25 | vcs = nvcs.ToArray (); 26 | 27 | nc.SetViewControllers (vcs, false); 28 | } 29 | 30 | protected override DocumentsViewController CreateDirectoryViewController (string path) 31 | { 32 | return new DocumentsViewController (path, DocumentsViewMode.Thumbnails); 33 | } 34 | 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /Praeclarum.iOS/UI/StorageSection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Foundation; 3 | 4 | namespace Praeclarum.UI 5 | { 6 | public class StorageSection : PFormSection 7 | { 8 | public StorageSection () 9 | : base (new Command ("Storage")) 10 | { 11 | Title = "Storage".Localize (); 12 | // Hint = "Select where to save documents."; 13 | } 14 | 15 | public override bool GetItemNavigates (object item) 16 | { 17 | return true; 18 | } 19 | 20 | public override bool SelectItem (object item) 21 | { 22 | var f = new StorageForm (); 23 | f.NavigationItem.RightBarButtonItem = new UIKit.UIBarButtonItem (UIKit.UIBarButtonSystemItem.Done, (s, e) => { 24 | if (f != null && f.PresentingViewController != null) { 25 | f.DismissViewController (true, null); 26 | } 27 | }); 28 | if (this.Form.NavigationController != null) { 29 | this.Form.NavigationController.PushViewController (f, true); 30 | } 31 | return false; 32 | } 33 | 34 | public override string GetItemTitle (object item) 35 | { 36 | var isp = DocumentAppDelegate.Shared.FileSystem; 37 | return isp != null ? 38 | isp.Description : 39 | "Storage".Localize (); 40 | } 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /Praeclarum/UI/ICanvas.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Praeclarum.Graphics; 3 | 4 | namespace Praeclarum.UI 5 | { 6 | public interface ICanvas : IView 7 | { 8 | event EventHandler Drawing; 9 | 10 | event EventHandler TouchBegan; 11 | event EventHandler TouchMoved; 12 | event EventHandler TouchCancelled; 13 | event EventHandler TouchEnded; 14 | 15 | void Invalidate (); 16 | void Invalidate (RectangleF frame); 17 | } 18 | 19 | public class CanvasTouchEventArgs : EventArgs 20 | { 21 | public int TouchId { get; set; } 22 | public PointF Location { get; set; } 23 | } 24 | 25 | public class CanvasDrawingEventArgs : EventArgs 26 | { 27 | public CanvasDrawingEventArgs (IGraphics graphics, RectangleF visibleArea) 28 | { 29 | Graphics = graphics; 30 | VisibleArea = visibleArea; 31 | } 32 | public IGraphics Graphics { get; private set; } 33 | public RectangleF VisibleArea { get; private set; } 34 | } 35 | 36 | public static class CanvasEx 37 | { 38 | public static void Invalidate (this ICanvas canvas) 39 | { 40 | canvas.Invalidate (canvas.Bounds); 41 | } 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /Praeclarum.iOS/UI/AnalyticsSection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Praeclarum.UI 4 | { 5 | public class AnalyticsSection : PFormSection 6 | { 7 | readonly Action disable; 8 | 9 | public AnalyticsSection (Action disable) 10 | { 11 | this.disable = disable; 12 | Hint = ("In order to improve iCircuit, anonymous usage data can be collected and sent to the developer. " + 13 | "This data includes which elements you add, which properties you set (not including values), and what errors occur. " + 14 | "To opt out of this, tap the option to turn it off (unchecked).").Localize (); 15 | 16 | Items.Add ("Enable Anonymous Analytics".Localize ()); 17 | } 18 | 19 | public override bool GetItemChecked (object item) 20 | { 21 | return !DocumentAppDelegate.Shared.Settings.DisableAnalytics; 22 | } 23 | 24 | public override bool SelectItem (object item) 25 | { 26 | var disabled = !DocumentAppDelegate.Shared.Settings.DisableAnalytics; 27 | DocumentAppDelegate.Shared.Settings.DisableAnalytics = disabled; 28 | 29 | // Wait till next launch to enable to make sure everything is inited 30 | if (disabled) disable (); 31 | 32 | SetNeedsFormat (); 33 | return false; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Praeclarum.iOS/UI/DarkModeSection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Foundation; 3 | 4 | namespace Praeclarum.UI 5 | { 6 | public class DarkModeSection : PFormSection 7 | { 8 | public DarkModeSection () 9 | : this (() => DocumentAppDelegate.Shared.Settings.DarkMode, () => { 10 | var appdel = DocumentAppDelegate.Shared; 11 | appdel.Settings.DarkMode = !appdel.Settings.DarkMode; 12 | appdel.UpdateTheme (); 13 | }) 14 | { 15 | } 16 | 17 | readonly Func isDarkFunc; 18 | readonly Action toggleAction; 19 | 20 | public DarkModeSection (Func isDark, Action toggle) 21 | { 22 | Title = "Theme".Localize (); 23 | 24 | Items.Add ("Light Mode".Localize ()); 25 | Items.Add ("Dark Mode".Localize ()); 26 | 27 | this.isDarkFunc = isDark; 28 | this.toggleAction = toggle; 29 | } 30 | 31 | public override bool GetItemChecked (object item) 32 | { 33 | var isDark = isDarkFunc (); 34 | if ("Dark Mode" == item.ToString ()) { 35 | return isDark; 36 | } 37 | return !isDark; 38 | } 39 | 40 | public override bool SelectItem (object item) 41 | { 42 | NSTimer.CreateScheduledTimer (0.1, t => { 43 | toggleAction (); 44 | SetNeedsFormat (); 45 | }); 46 | return false; 47 | } 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /Praeclarum.Mac/UI/Timer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Foundation; 3 | 4 | namespace Praeclarum.UI 5 | { 6 | public class Timer : ITimer 7 | { 8 | public event EventHandler Tick; 9 | 10 | NSTimer timer; 11 | 12 | public bool Enabled { 13 | get { return timer != null; } 14 | set { 15 | if (value) { 16 | if (timer != null) 17 | return; 18 | try { 19 | timer = NSTimer.CreateRepeatingScheduledTimer ( 20 | Interval, NSTimerTick); 21 | } catch (Exception) { 22 | } 23 | } else { 24 | if (timer == null) 25 | return; 26 | try { 27 | timer.Invalidate (); 28 | } catch (Exception) { 29 | } 30 | finally { 31 | timer = null; 32 | } 33 | } 34 | } 35 | } 36 | 37 | TimeSpan interval; 38 | 39 | public TimeSpan Interval { 40 | get { 41 | return interval; 42 | } 43 | set { 44 | if (interval == value) 45 | return; 46 | interval = value; 47 | var en = Enabled; 48 | Enabled = false; 49 | Enabled = en; 50 | } 51 | } 52 | 53 | public Timer () 54 | { 55 | interval = TimeSpan.FromSeconds (1); 56 | } 57 | 58 | void NSTimerTick (NSTimer t) 59 | { 60 | var ev = Tick; 61 | if (ev != null) { 62 | ev (this, EventArgs.Empty); 63 | } 64 | } 65 | } 66 | } 67 | 68 | -------------------------------------------------------------------------------- /Praeclarum/AsyncExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Reflection; 4 | 5 | namespace Praeclarum 6 | { 7 | public static class AsyncExtensions 8 | { 9 | public static Task GetEventAsync (this object eventSource, string eventName) 10 | where T : EventArgs 11 | { 12 | var tcs = new TaskCompletionSource(); 13 | 14 | var type = eventSource.GetType (); 15 | var ev = type.GetTypeInfo ().GetDeclaredEvent (eventName); 16 | 17 | EventHandler handler = null; 18 | 19 | handler = delegate (object sender, T e) { 20 | ev.RemoveEventHandler (eventSource, handler); 21 | tcs.SetResult (e); 22 | }; 23 | 24 | ev.AddEventHandler (eventSource, handler); 25 | return tcs.Task; 26 | } 27 | 28 | public static Task GetEventAsync (this object eventSource, string eventName) 29 | { 30 | var tcs = new TaskCompletionSource(); 31 | 32 | var type = eventSource.GetType (); 33 | var ev = type.GetTypeInfo ().GetDeclaredEvent (eventName); 34 | 35 | EventHandler handler = null; 36 | 37 | handler = delegate (object sender, EventArgs e) { 38 | ev.RemoveEventHandler (eventSource, handler); 39 | tcs.SetResult (e); 40 | }; 41 | 42 | ev.AddEventHandler (eventSource, handler); 43 | return tcs.Task; 44 | } 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /Praeclarum.iOS/UI/SelectableButtonItem.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | 3 | using System; 4 | using UIKit; 5 | 6 | namespace Praeclarum.UI 7 | { 8 | public class SelectableButtonItem 9 | { 10 | public UIBarButtonItem Item { get; private set; } 11 | 12 | readonly UIButton? button = null; 13 | 14 | private bool selected; 15 | 16 | public bool Selected { 17 | get { 18 | return selected; 19 | } 20 | set 21 | { 22 | if (selected == value) return; 23 | selected = value; 24 | if (button is { } b) 25 | { 26 | b.Selected = selected; 27 | } 28 | else 29 | { 30 | if (ios15) 31 | { 32 | Item.Selected = selected; 33 | } 34 | } 35 | } 36 | } 37 | 38 | readonly static bool ios15 = UIDevice.CurrentDevice.CheckSystemVersion (15, 0); 39 | 40 | public SelectableButtonItem (UIImage image, EventHandler handler) 41 | { 42 | if (ios15) { 43 | Item = new UIBarButtonItem (image, UIBarButtonItemStyle.Plain, handler); 44 | Item.Selected = selected; 45 | } 46 | else { 47 | button = UIButton.FromType (UIButtonType.RoundedRect); 48 | button.SetImage (image, UIControlState.Normal); 49 | button.Frame = new CoreGraphics.CGRect (0, 0, 44, 44); 50 | button.TouchUpInside += handler; 51 | button.Selected = selected; 52 | Item = new UIBarButtonItem (button); 53 | } 54 | } 55 | } 56 | } 57 | 58 | -------------------------------------------------------------------------------- /Praeclarum.iOS/UI/TitleView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UIKit; 3 | using Foundation; 4 | using CoreGraphics; 5 | using System.Collections.Generic; 6 | using CoreAnimation; 7 | using System.Threading.Tasks; 8 | using System.IO; 9 | using System.Diagnostics; 10 | using System.Linq; 11 | using Praeclarum.UI; 12 | using Praeclarum; 13 | using Praeclarum.IO; 14 | 15 | namespace Praeclarum.UI 16 | { 17 | public class TitleView : UILabel 18 | { 19 | // UITapGestureRecognizer titleTap; 20 | 21 | public event EventHandler Tapped = delegate {}; 22 | 23 | public TitleView () 24 | { 25 | var isPhone = UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Phone; 26 | var titleX = isPhone ? (320 - 176) / 2 : (768 - 624) / 2; 27 | 28 | Frame = new CGRect (titleX + 1, 6, isPhone ? 176 : 624, 30); 29 | BackgroundColor = UIColor.Clear; 30 | Opaque = false; 31 | TextColor = DocumentAppDelegate.Shared.Theme.NavigationTextColor; 32 | // ShadowColor = WhiteTheme.BarTextShadowColor; 33 | // ShadowOffset = new SizeF (WhiteTheme.BarTextShadowOffset.Horizontal, WhiteTheme.BarTextShadowOffset.Vertical); 34 | Font = UIFont.FromName (WhiteTheme.TitleFontName, WhiteTheme.BarTitleFontSize); 35 | AdjustsFontSizeToFitWidth = true; 36 | TextAlignment = UITextAlignment.Center; 37 | UserInteractionEnabled = true; 38 | 39 | AddGestureRecognizer (new UITapGestureRecognizer (HandleTitleTap)); 40 | } 41 | 42 | void HandleTitleTap (UITapGestureRecognizer pan) 43 | { 44 | Tapped (this, EventArgs.Empty); 45 | } 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /Praeclarum.iOS/UI/Editor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UIKit; 3 | using Foundation; 4 | using Praeclarum.Graphics; 5 | 6 | namespace Praeclarum.UI 7 | { 8 | public class Editor : UITextView, ITextEditor 9 | { 10 | public Editor (CoreGraphics.CGRect frame) 11 | : base (frame) 12 | { 13 | } 14 | 15 | public Editor (CoreGraphics.CGRect frame, NSTextContainer container) 16 | : base (frame, container) 17 | { 18 | } 19 | 20 | #region IView implementation 21 | 22 | Color IView.BackgroundColor { 23 | get { return base.BackgroundColor.GetColor (); } 24 | set { base.BackgroundColor = value.GetUIColor (); } 25 | } 26 | 27 | RectangleF IView.Bounds { 28 | get { 29 | return Bounds.ToRectangleF (); 30 | } 31 | } 32 | 33 | #endregion 34 | 35 | #region ITextEditor implementation 36 | 37 | public void Modify (Action action) 38 | { 39 | BeginInvokeOnMainThread (() => action ()); 40 | } 41 | 42 | void ITextEditor.ReplaceText (StringRange range, string text) 43 | { 44 | var b = this.GetPosition (BeginningOfDocument, range.Location); 45 | var e = this.GetPosition (BeginningOfDocument, range.Location + range.Length); 46 | var r = this.GetTextRange (b, e); 47 | ReplaceText (r, text); 48 | } 49 | 50 | StringRange ITextEditor.SelectedRange { 51 | get { 52 | var r = SelectedRange; 53 | return new StringRange ((int)r.Location, (int)r.Length); 54 | } 55 | set { 56 | SelectedRange = new NSRange (value.Location, value.Length); 57 | } 58 | } 59 | 60 | #endregion 61 | } 62 | } 63 | 64 | -------------------------------------------------------------------------------- /Praeclarum.Net.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Praeclarum.Mac", "Praeclarum.Mac\Praeclarum.Mac.csproj", "{D02FAE64-9438-4D14-A06A-3C21DCADC101}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Praeclarum", "Praeclarum\Praeclarum.csproj", "{4A09F863-81C8-481B-8017-FDFC1F92D5B8}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(SolutionProperties) = preSolution 16 | HideSolutionNode = FALSE 17 | EndGlobalSection 18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 19 | {D02FAE64-9438-4D14-A06A-3C21DCADC101}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {D02FAE64-9438-4D14-A06A-3C21DCADC101}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {D02FAE64-9438-4D14-A06A-3C21DCADC101}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {D02FAE64-9438-4D14-A06A-3C21DCADC101}.Release|Any CPU.Build.0 = Release|Any CPU 23 | {4A09F863-81C8-481B-8017-FDFC1F92D5B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {4A09F863-81C8-481B-8017-FDFC1F92D5B8}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {4A09F863-81C8-481B-8017-FDFC1F92D5B8}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {4A09F863-81C8-481B-8017-FDFC1F92D5B8}.Release|Any CPU.Build.0 = Release|Any CPU 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /Praeclarum.Android/UI/DocumentEditor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | using Android.App; 8 | using Android.Content; 9 | using Android.OS; 10 | using Android.Runtime; 11 | using Android.Views; 12 | using Android.Widget; 13 | using Praeclarum.App; 14 | 15 | namespace Praeclarum.UI 16 | { 17 | public class DocumentEditor : Fragment, IDocumentEditor 18 | { 19 | public DocumentReference DocumentReference { get; private set; } 20 | 21 | public DocumentEditor (DocumentReference docRef) 22 | { 23 | DocumentReference = docRef; 24 | } 25 | 26 | #region IDocumentEditor implementation 27 | 28 | IView editorView = null; 29 | 30 | public virtual IView EditorView 31 | { 32 | get { return editorView; } 33 | set { 34 | View.AddTouchables (new List { 35 | (global::Android.Views.View)editorView, 36 | }); 37 | } 38 | } 39 | 40 | public IDocument Document => null; 41 | 42 | public virtual bool IsPreviewing { get; set; } 43 | 44 | public virtual void OnCreated () 45 | { 46 | } 47 | 48 | public virtual void DidEnterBackground () 49 | { 50 | } 51 | 52 | public virtual void WillEnterForeground () 53 | { 54 | } 55 | 56 | public virtual void BindDocument () 57 | { 58 | } 59 | 60 | public virtual void UnbindDocument () 61 | { 62 | } 63 | 64 | public virtual void UnbindUI () 65 | { 66 | } 67 | 68 | public virtual Task SaveDocument () 69 | { 70 | return Task.CompletedTask; 71 | } 72 | 73 | #endregion 74 | } 75 | } 76 | 77 | -------------------------------------------------------------------------------- /Praeclarum/App/IAppSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Praeclarum.IO; 3 | using Praeclarum.UI; 4 | 5 | namespace Praeclarum.App 6 | { 7 | public interface IDocumentAppSettings : IAppSettings 8 | { 9 | string LastDocumentPath { get; set; } 10 | 11 | DocumentsSort DocumentsSort { get; set; } 12 | 13 | string FileSystem { get; set; } 14 | 15 | bool UseCloud { get; set; } 16 | bool AskedToUseCloud { get; set; } 17 | 18 | string GetWorkingDirectory (IFileSystem fileSystem); 19 | void SetWorkingDirectory (IFileSystem fileSystem, string path); 20 | 21 | string DocumentationVersion { get; set; } 22 | 23 | bool DarkMode { get; set; } 24 | 25 | bool IsPatron { 26 | get; 27 | set; 28 | } 29 | 30 | DateTime PatronEndDate { 31 | get; 32 | set; 33 | } 34 | 35 | bool DisableAnalytics { get; set; } 36 | 37 | bool HasTipped { get; set; } 38 | DateTime TipDate { get; set; } 39 | 40 | DateTime SubscribedToProDate { get; set; } 41 | string SubscribedToProFromPlatform { get; set; } 42 | int SubscribedToProMonths { get; set; } 43 | } 44 | 45 | public static class DocumentAppSettingsExtensions 46 | { 47 | public static DateTime SubscribedToProEndDate (this IDocumentAppSettings settings) => settings.SubscribedToProDate.AddMonths(settings.SubscribedToProMonths); 48 | } 49 | 50 | public interface IAppSettings 51 | { 52 | int RunCount { get; set; } 53 | bool UseEnglish { get; set; } 54 | } 55 | 56 | public static class AppSettingsEx 57 | { 58 | public static bool IsFirstRun (this IAppSettings settings) 59 | { 60 | return settings.RunCount == 1; 61 | } 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /Praeclarum/Localization.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Praeclarum 5 | { 6 | public static class Localization 7 | { 8 | public static string Localize (object obj) 9 | { 10 | if (obj == null) 11 | return ""; 12 | var english = obj.ToString (); 13 | var t = Translate (english); 14 | return Note (english, t); 15 | } 16 | 17 | public static string Localize (this string english) 18 | { 19 | var t = Translate (english); 20 | return Note (english, t); 21 | } 22 | 23 | public static string Localize (this string format, params object[] args) 24 | { 25 | var t = Translate (format); 26 | return string.Format (Note (format, t), args); 27 | } 28 | 29 | static string Translate (string english) 30 | { 31 | #if IOS || MACCATALYST || __IOS__ || __MACOS__ 32 | if (Foundation.NSUserDefaults.StandardUserDefaults.BoolForKey ("UseEnglish")) 33 | return english; 34 | return Foundation.NSBundle.MainBundle.GetLocalizedString (key: english, value: english); 35 | #else 36 | return english; 37 | #endif 38 | } 39 | 40 | #if DEBUG 41 | static readonly HashSet notes = new HashSet (); 42 | #endif 43 | static string Note (string english, string translation) 44 | { 45 | #if DEBUG 46 | var lang = System.Globalization.CultureInfo.CurrentCulture.TwoLetterISOLanguageName; 47 | if (lang != "en" && english == translation && english.Length > 0 && !notes.Contains (english)) { 48 | notes.Add (english); 49 | System.Diagnostics.Debug.WriteLine ($"Needs Translation [{lang}]: \"{english}\" = \"\";"); 50 | } 51 | #endif 52 | return translation.Length > 0 ? translation : english; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Praeclarum.Mac/UI/Canvas.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using AppKit; 3 | using CoreGraphics; 4 | using Praeclarum.Graphics; 5 | 6 | namespace Praeclarum.UI 7 | { 8 | public class View : NSView, IView 9 | { 10 | RectangleF IView.Bounds { 11 | get { return base.Bounds.ToRectangleF (); } 12 | } 13 | 14 | public override bool IsFlipped { 15 | get { 16 | return true; 17 | } 18 | } 19 | 20 | Color bgColor = Colors.Black; 21 | public Color BackgroundColor { 22 | get { 23 | return bgColor; 24 | } 25 | set { 26 | bgColor = value; 27 | } 28 | } 29 | } 30 | 31 | public class Canvas : View, ICanvas 32 | { 33 | public event EventHandler TouchBegan = delegate {}; 34 | public event EventHandler TouchMoved = delegate {}; 35 | public event EventHandler TouchCancelled = delegate {}; 36 | public event EventHandler TouchEnded = delegate {}; 37 | 38 | public event EventHandler Drawing = delegate {}; 39 | 40 | public Canvas () 41 | { 42 | } 43 | 44 | public void Invalidate () 45 | { 46 | SetNeedsDisplayInRect (base.Bounds); 47 | } 48 | 49 | public void Invalidate (RectangleF frame) 50 | { 51 | SetNeedsDisplayInRect (frame.ToRectangleF ()); 52 | } 53 | 54 | public override void DrawRect (CGRect dirtyRect) 55 | { 56 | try { 57 | var c = NSGraphicsContext.CurrentContext.GraphicsPort; 58 | var g = new CoreGraphicsGraphics (c, true); 59 | var e = new CanvasDrawingEventArgs (g, Bounds.ToRectangleF ()); 60 | Drawing (this, e); 61 | } catch (Exception ex) { 62 | Log.Error (ex); 63 | } 64 | } 65 | } 66 | } 67 | 68 | -------------------------------------------------------------------------------- /Praeclarum.Android/App/TextDocument.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Praeclarum.App 5 | { 6 | public class TextDocument : ITextDocument 7 | { 8 | string path; 9 | 10 | public TextDocument (string path) 11 | { 12 | this.path = path; 13 | } 14 | 15 | #region IDocument implementation 16 | 17 | bool isOpen = false; 18 | 19 | public async Task OpenAsync () 20 | { 21 | TextData = System.IO.File.ReadAllText (path); 22 | isOpen = true; 23 | } 24 | 25 | public async Task SaveAsync (string path, DocumentSaveOperation operation) 26 | { 27 | this.path = path; 28 | System.IO.File.WriteAllText (path, TextData); 29 | } 30 | 31 | public async Task CloseAsync () 32 | { 33 | if (!isOpen) 34 | return; 35 | isOpen = false; 36 | } 37 | 38 | public bool IsOpen { 39 | get { 40 | return isOpen; 41 | } 42 | } 43 | 44 | #endregion 45 | 46 | #region ITextDocument implementation 47 | 48 | public void UpdateChangeCount (DocumentChangeKind changeKind) 49 | { 50 | } 51 | 52 | string textData = ""; 53 | private bool _disposedValue; 54 | 55 | public virtual string TextData { 56 | get { 57 | return textData; 58 | } 59 | set { 60 | textData = value ?? ""; 61 | } 62 | } 63 | 64 | protected virtual void Dispose (bool disposing) 65 | { 66 | if (!_disposedValue) 67 | { 68 | if (disposing) 69 | { 70 | } 71 | _disposedValue = true; 72 | } 73 | } 74 | 75 | public void Dispose () 76 | { 77 | // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method 78 | Dispose (disposing: true); 79 | GC.SuppressFinalize (this); 80 | } 81 | 82 | #endregion 83 | } 84 | } 85 | 86 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: macos-26 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Update Versions 17 | env: 18 | VERSION_PREFIX: 0.4 19 | VERSION_SUFFIX: ${{github.run_number}} 20 | run: | 21 | VERSION=$VERSION_PREFIX.$VERSION_SUFFIX 22 | sed -i.bak "s:1.0.0:$VERSION:g" Praeclarum/Praeclarum.csproj 23 | sed -i.bak "s:1.0.0:$VERSION:g" Praeclarum.Android/Praeclarum.Android.csproj 24 | sed -i.bak "s:1.0.0:$VERSION:g" Praeclarum.iOS/Praeclarum.iOS.csproj 25 | sed -i.bak "s:1.0.0:$VERSION:g" Praeclarum.Mac/Praeclarum.Mac.csproj 26 | sed -i.bak "s:1.0.0:$VERSION:g" Praeclarum.Utilities/Praeclarum.Utilities.csproj 27 | - name: Setup Xcode 28 | uses: maxim-lobanov/setup-xcode@v1 29 | with: 30 | xcode-version: 26.0 31 | - name: Setup .NET 32 | uses: actions/setup-dotnet@v4 33 | with: 34 | global-json-file: global.json 35 | - name: Install workloads 36 | run: dotnet workload restore Praeclarum.Utilities.sln 37 | - name: Restore dependencies 38 | run: dotnet restore Praeclarum.Utilities.sln 39 | - name: Build 40 | run: dotnet build --no-restore -c Release Praeclarum.Utilities.sln 41 | - name: Pack 42 | run: dotnet pack -c Release Praeclarum.Utilities.sln -o . 43 | - name: Store nuget 44 | uses: actions/upload-artifact@v4 45 | with: 46 | name: Praeclarum.UtilitiesNuget 47 | path: "*.nupkg" 48 | -------------------------------------------------------------------------------- /Praeclarum/IO/FileSystemManager.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using System.Collections.ObjectModel; 3 | using System.Linq; 4 | using System; 5 | 6 | namespace Praeclarum.IO 7 | { 8 | public class FileSystemManager 9 | { 10 | public ObservableCollection Providers { get; private set; } 11 | 12 | public ObservableCollection FileSystems { get; private set; } 13 | 14 | public IFileSystem ActiveFileSystem { 15 | get; 16 | set; 17 | } 18 | 19 | public static FileSystemManager Shared { get; } = new FileSystemManager { 20 | ActiveFileSystem = new LoadingFileSystem (), 21 | }; 22 | 23 | public FileSystemManager () 24 | { 25 | Providers = new ObservableCollection (); 26 | FileSystems = new ObservableCollection (); 27 | } 28 | 29 | public void Add (IFileSystem fs) 30 | { 31 | FileSystems.Add (fs); 32 | } 33 | 34 | public void Add (IFileSystemProvider fss) 35 | { 36 | Providers.Add (fss); 37 | foreach (var fs in fss.GetFileSystems ()) 38 | FileSystems.Add (fs); 39 | } 40 | 41 | public IFileSystem ChooseFileSystem (string lastFileSystemId) 42 | { 43 | var fs = FileSystems.FirstOrDefault (x => x.IsAvailable && x.Id == lastFileSystemId); 44 | #if __IOS__ 45 | if (fs == null) 46 | fs = FileSystems.OfType ().First (x => x.IsAvailable); 47 | #endif 48 | if (fs == null) 49 | fs = FileSystems.First (x => x.IsAvailable); 50 | 51 | return fs; 52 | } 53 | 54 | public static void EnsureDirectoryExists (string dir) 55 | { 56 | if (string.IsNullOrEmpty (dir) || dir == "/") 57 | return; 58 | 59 | if (!System.IO.Directory.Exists (dir)) { 60 | EnsureDirectoryExists (System.IO.Path.GetDirectoryName (dir)); 61 | System.IO.Directory.CreateDirectory (dir); 62 | } 63 | } 64 | } 65 | } 66 | 67 | -------------------------------------------------------------------------------- /Praeclarum.Android/Resources/AboutResources.txt: -------------------------------------------------------------------------------- 1 | Images, layout descriptions, binary blobs and string dictionaries can be included 2 | in your application as resource files. Various Android APIs are designed to 3 | operate on the resource IDs instead of dealing with images, strings or binary blobs 4 | directly. 5 | 6 | For example, a sample Android app that contains a user interface layout (main.axml), 7 | an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png) 8 | would keep its resources in the "Resources" directory of the application: 9 | 10 | Resources/ 11 | drawable/ 12 | icon.png 13 | 14 | layout/ 15 | main.axml 16 | 17 | values/ 18 | strings.xml 19 | 20 | In order to get the build system to recognize Android resources, set the build action to 21 | "AndroidResource". The native Android APIs do not operate directly with filenames, but 22 | instead operate on resource IDs. When you compile an Android application that uses resources, 23 | the build system will package the resources for distribution and generate a class called "R" 24 | (this is an Android convention) that contains the tokens for each one of the resources 25 | included. For example, for the above Resources layout, this is what the R class would expose: 26 | 27 | public class R { 28 | public class drawable { 29 | public const int icon = 0x123; 30 | } 31 | 32 | public class layout { 33 | public const int main = 0x456; 34 | } 35 | 36 | public class strings { 37 | public const int first_string = 0xabc; 38 | public const int second_string = 0xbcd; 39 | } 40 | } 41 | 42 | You would then use R.drawable.icon to reference the drawable/icon.png file, or R.layout.main 43 | to reference the layout/main.axml file, or R.strings.first_string to reference the first 44 | string in the dictionary file values/strings.xml. 45 | -------------------------------------------------------------------------------- /Praeclarum.iOS/UI/ActivityIndicator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UIKit; 3 | using CoreGraphics; 4 | 5 | namespace Praeclarum.UI 6 | { 7 | public class ActivityIndicator : UIView 8 | { 9 | readonly UILabel titleLabel; 10 | readonly UIActivityIndicatorView activity; 11 | 12 | public string Title { 13 | get { return titleLabel.Text; } 14 | set { titleLabel.Text = value; } 15 | } 16 | 17 | public ActivityIndicator () 18 | { 19 | Opaque = false; 20 | 21 | var bounds = new CGRect (0, 0, 150, 88); 22 | Frame = bounds; 23 | 24 | var isDark = DocumentAppDelegate.Shared.Theme.IsDark; 25 | BackgroundColor = isDark ? 26 | UIColor.FromWhiteAlpha (1-0.96f, 0.85f) : 27 | UIColor.FromWhiteAlpha (0.96f, 0.85f); 28 | Layer.CornerRadius = 12; 29 | 30 | const float margin = 12; 31 | 32 | activity = new UIActivityIndicatorView (isDark ? UIActivityIndicatorViewStyle.White : UIActivityIndicatorViewStyle.Gray) { 33 | Frame = new CGRect (margin, margin, 21, 21), 34 | HidesWhenStopped = false, 35 | }; 36 | 37 | titleLabel = new UILabel (new CGRect (activity.Frame.Right+margin, 0, bounds.Width - activity.Frame.Right - 2*margin, 44)) { 38 | TextAlignment = UITextAlignment.Center, 39 | TextColor = isDark ? UIColor.FromWhiteAlpha (1-0.33f, 1) : UIColor.FromWhiteAlpha (0.33f, 1), 40 | BackgroundColor = UIColor.Clear, 41 | }; 42 | 43 | AddSubviews (titleLabel, activity); 44 | } 45 | 46 | public void Show (bool animated = true) 47 | { 48 | Hidden = false; 49 | activity.StartAnimating (); 50 | if (animated) 51 | UIView.Animate (1, () => Alpha = 1); 52 | else 53 | Alpha = 1; 54 | } 55 | 56 | public void Hide (bool animated = true) 57 | { 58 | activity.StopAnimating (); 59 | if (animated) 60 | UIView.Animate (0.25, () => Alpha = 0, () => Hidden = true); 61 | else { 62 | Alpha = 0; 63 | Hidden = true; 64 | } 65 | } 66 | } 67 | } 68 | 69 | -------------------------------------------------------------------------------- /Praeclarum/UI/IDocumentsView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Praeclarum.UI; 4 | using Praeclarum.App; 5 | using System.Linq; 6 | using System.Collections.ObjectModel; 7 | using Praeclarum.IO; 8 | 9 | namespace Praeclarum.UI 10 | { 11 | public enum DocumentsSort 12 | { 13 | Date, 14 | Name 15 | } 16 | 17 | public class DocumentsViewItem 18 | { 19 | public DocumentReference Reference { get; set; } 20 | public List SubReferences { get; set; } 21 | 22 | public bool IsDirectory { get { return Reference.File.IsDirectory; } } 23 | 24 | public DateTime ModifiedTime { 25 | get { 26 | if (SubReferences == null || SubReferences.Count == 0) 27 | return Reference.File.ModifiedTime; 28 | return SubReferences.Max (x => x.File.ModifiedTime); 29 | } 30 | } 31 | 32 | public DocumentsViewItem (DocumentReference reference) 33 | { 34 | Reference = reference; 35 | } 36 | } 37 | 38 | public interface IDocumentsView 39 | { 40 | bool IsSyncing { get; set; } 41 | List Items { get; set; } 42 | DocumentsViewItem GetItemAtPoint (Praeclarum.Graphics.PointF p); 43 | void ReloadData (); 44 | 45 | DocumentsSort Sort { get; set; } 46 | event EventHandler SortChanged; 47 | 48 | void InsertItems (int[] docIndices); 49 | void DeleteItems (int[] docIndices, bool animated); 50 | void UpdateItem (int docIndex); 51 | void ShowItem (int docIndex, bool animated); 52 | void RefreshListTimes (); 53 | void SetOpenedDocument (int docIndex, bool animated); 54 | 55 | event Action RenameRequested; 56 | 57 | void SetEditing (bool editing, bool animated); 58 | 59 | void SetSelecting (bool selecting, bool animated); 60 | 61 | void UpdateLayout (); 62 | 63 | ObservableCollection SelectedDocuments { get; } 64 | 65 | bool ShowPatron { get; set; } 66 | 67 | void StopLoadingThumbnails (); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Praeclarum.iOS/App/ReviewNagging.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UIKit; 3 | using Foundation; 4 | using System.Linq; 5 | using StoreKit; 6 | 7 | namespace Praeclarum.App 8 | { 9 | public interface IHasReviewNagging 10 | { 11 | ReviewNagging ReviewNagging { get; } 12 | } 13 | 14 | public class ReviewNagging 15 | { 16 | // https://developer.apple.com/documentation/storekit/skstorereviewcontroller/requesting_app_store_reviews 17 | 18 | readonly string numPositiveKey; 19 | readonly string shownKey; 20 | readonly NSUserDefaults defs; 21 | 22 | int MinNumPositiveActions { get; } 23 | 24 | int NumPositiveActions => (int)defs.IntForKey (numPositiveKey); 25 | 26 | bool Shown => defs.BoolForKey (shownKey); 27 | 28 | public bool NeedsReview => !Shown; 29 | 30 | public ReviewNagging (int minNumPositiveActions = 5) 31 | { 32 | var appVersionFull = NSBundle.MainBundle.InfoDictionary["CFBundleShortVersionString"]?.ToString () ?? "1.0"; 33 | var appVersionMajorMinor = string.Join (".", appVersionFull.Split ('.').Take (2)); 34 | 35 | defs = NSUserDefaults.StandardUserDefaults; 36 | numPositiveKey = "ReviewCount" + appVersionMajorMinor; 37 | shownKey = "ReviewShown" + appVersionMajorMinor; 38 | MinNumPositiveActions = minNumPositiveActions; 39 | } 40 | 41 | public void Reset () 42 | { 43 | defs.SetInt (0, numPositiveKey); 44 | defs.SetBool (false, shownKey); 45 | } 46 | 47 | public void RegisterPositiveAction () 48 | { 49 | try { 50 | defs.SetInt (NumPositiveActions + 1, numPositiveKey); 51 | Log.Info ("Num Review Actions = " + NumPositiveActions); 52 | } 53 | catch (Exception ex) { 54 | Log.Error (ex); 55 | } 56 | } 57 | 58 | public bool ShouldPresent { 59 | get { 60 | var osok = UIDevice.CurrentDevice.CheckSystemVersion (10, 3); 61 | var shouldPresent = osok && !Shown && NumPositiveActions >= MinNumPositiveActions; 62 | Log.Info ($"Present Review (os={osok}, s={Shown}, n={NumPositiveActions}) = {shouldPresent}"); 63 | return shouldPresent; 64 | } 65 | } 66 | 67 | public void PresentIfAppropriate () 68 | { 69 | try { 70 | if (ShouldPresent) { 71 | 72 | defs.SetBool (true, shownKey); 73 | 74 | SKStoreReviewController.RequestReview (); 75 | } 76 | } 77 | catch (Exception ex) { 78 | Log.Error (ex); 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Praeclarum/Praeclarum.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0;net9.0-macos26.0;net9.0-ios26.0;net9.0-maccatalyst26.0 4 | disable 5 | false 6 | 1.0.0 7 | false 8 | true 9 | $(NoWarn);NU1900;XCODE_26_0_PREVIEW 10 | 11 | 12 | 12.2 13 | 14 | 15 | 10.0 16 | 17 | 18 | 15.0 19 | 20 | 21 | 12.0 22 | 23 | 24 | 21.0 25 | 26 | 27 | 10.0.17763.0 28 | 10.0.17763.0 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /Praeclarum.iOS/NSMutableAttributedStringWrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Praeclarum.Graphics; 3 | using System.Runtime.InteropServices; 4 | 5 | using Foundation; 6 | using ObjCRuntime; 7 | using NativeNSMutableAttributedString = Foundation.NSMutableAttributedString; 8 | using NativeCTStringAttributes = CoreText.CTStringAttributes; 9 | using CGColor = CoreGraphics.CGColor; 10 | #if __IOS__ 11 | using NativeColor = UIKit.UIColor; 12 | #elif MONOMAC || __MACOS__ 13 | using NativeColor = AppKit.NSColor; 14 | #endif 15 | 16 | namespace Praeclarum 17 | { 18 | 19 | 20 | public class NSMutableAttributedStringWrapper : IRichText 21 | { 22 | NativeNSMutableAttributedString s; 23 | 24 | public NSMutableAttributedStringWrapper (NativeNSMutableAttributedString ns) 25 | { 26 | s = ns; 27 | } 28 | 29 | public NSMutableAttributedStringWrapper (string data) 30 | { 31 | s = new NativeNSMutableAttributedString (data); 32 | } 33 | 34 | public NativeNSMutableAttributedString AttributedText { 35 | get { return s; } 36 | } 37 | 38 | #region NSMutableAttributedString implementation 39 | 40 | public void AddAttributes (IRichTextAttributes styleClass, StringRange range) 41 | { 42 | var attrs = ((NativeStringAttributesWrapper)styleClass).Attributes; 43 | s.AddAttributes (attrs, new NSRange (range.Location, range.Length)); 44 | } 45 | 46 | #endregion 47 | } 48 | 49 | public static class StringRangeEx 50 | { 51 | public static StringRange ToStringRange (this NSRange range) 52 | { 53 | return new StringRange ((int)range.Location, (int)range.Length); 54 | } 55 | 56 | public static NSRange ToNSRange (this StringRange r) 57 | { 58 | return new NSRange (r.Location, r.Length); 59 | } 60 | } 61 | 62 | public static class NSDictionaryEx 63 | { 64 | [DllImport ("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")] 65 | private static extern void CFDictionarySetValue (IntPtr theDict, IntPtr key, IntPtr value); 66 | 67 | public static void SetValue (this NSDictionary theDict, NSString key, INativeObject value) 68 | { 69 | SetValue (theDict.Handle, key.Handle, value.Handle); 70 | } 71 | 72 | static void SetValue (IntPtr theDict, IntPtr key, IntPtr value) 73 | { 74 | CFDictionarySetValue (theDict, key, value); 75 | } 76 | 77 | public static void AddAttributes (this NativeNSMutableAttributedString s, NativeCTStringAttributes a, StringRange r) 78 | { 79 | s.AddAttributes (a, new NSRange (r.Location, r.Length)); 80 | } 81 | } 82 | } 83 | 84 | -------------------------------------------------------------------------------- /Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Praeclarum.iOS/UI/GalleryViewController.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | 3 | using System; 4 | using System.Text.RegularExpressions; 5 | using System.Threading.Tasks; 6 | using Foundation; 7 | using UIKit; 8 | using WebKit; 9 | 10 | namespace Praeclarum.UI 11 | { 12 | public class GalleryViewController : UIViewController, IWKNavigationDelegate 13 | { 14 | private readonly string galleryUrl; 15 | private readonly Regex downloadUrlRe; 16 | WKWebView? webBrowser; 17 | UIBarButtonItem backItem; 18 | 19 | public Action<(NSUrl, Match)>? DownloadUrl; 20 | 21 | public GalleryViewController (string galleryUrl, Regex downloadUrlRe) 22 | { 23 | this.galleryUrl = galleryUrl; 24 | this.downloadUrlRe = downloadUrlRe; 25 | Title = "Gallery".Localize (); 26 | 27 | backItem = new UIBarButtonItem("Back", UIBarButtonItemStyle.Plain, (s, e) => { 28 | webBrowser?.GoBack(); 29 | }); 30 | backItem.Enabled = false; 31 | NavigationItem.LeftBarButtonItems = new UIBarButtonItem[] { 32 | backItem, 33 | }; 34 | 35 | NavigationItem.RightBarButtonItems = new UIBarButtonItem[] { 36 | new UIBarButtonItem (UIBarButtonSystemItem.Done, (s, e) => { 37 | DownloadUrl = null; 38 | DismissViewController (true, null); 39 | }), 40 | }; 41 | } 42 | 43 | public override void ViewDidLoad () 44 | { 45 | base.ViewDidLoad (); 46 | 47 | var config = new WKWebViewConfiguration (); 48 | var vc = this; 49 | var vcv = vc.View; 50 | if (vcv is null) { 51 | return; 52 | } 53 | webBrowser = new WKWebView (vcv.Bounds, config) { 54 | AutoresizingMask = UIViewAutoresizing.FlexibleDimensions 55 | }; 56 | webBrowser.NavigationDelegate = this; 57 | webBrowser.LoadRequest (new NSUrlRequest (new NSUrl (galleryUrl))); 58 | vcv.AddSubview (webBrowser); 59 | } 60 | 61 | [Export ("webView:decidePolicyForNavigationAction:decisionHandler:")] 62 | public void DecidePolicy (WKWebView webView, WKNavigationAction navigationAction, Action decisionHandler) 63 | { 64 | var url = navigationAction?.Request.Url; 65 | var urls = url?.AbsoluteString ?? ""; 66 | var m = downloadUrlRe.Match (urls); 67 | if (url != null && m.Success) { 68 | decisionHandler (WKNavigationActionPolicy.Cancel); 69 | BeginInvokeOnMainThread (() => { 70 | DownloadUrl?.Invoke ((url, m)); 71 | }); 72 | } 73 | else { 74 | decisionHandler (WKNavigationActionPolicy.Allow); 75 | } 76 | } 77 | 78 | [Export ("webView:didFinishNavigation:")] 79 | public void DidFinishNavigation (WKWebView webView, WKNavigation navigation) 80 | { 81 | backItem.Enabled = webView.CanGoBack; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Praeclarum.Utilities/Praeclarum.Utilities.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0;net9.0-android;net9.0-ios26.0;net9.0-maccatalyst26.0;net9.0-macos26.0 5 | 6 | Praeclarum.Utilities 7 | 1.0.0 8 | praeclarum 9 | https://github.com/praeclarum/Praeclarum 10 | MIT 11 | 12 | enable 13 | enable 14 | 15 | 16 | 17 | 12.2 18 | 19 | 20 | 10.0 21 | 22 | 23 | 15.0 24 | 25 | 26 | 12.0 27 | 28 | 29 | 21.0 30 | 31 | 32 | 10.0.17763.0 33 | 10.0.17763.0 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | runtime; build; native; contentfiles; analyzers; buildtransitive 46 | all 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /Praeclarum.Android/UI/Canvas.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using Praeclarum.Graphics; 7 | using Android.Content; 8 | 9 | namespace Praeclarum.UI 10 | { 11 | public class View : global::Android.Views.View, IView 12 | { 13 | public View (Context context) : 14 | base (context) 15 | { 16 | Initialize (); 17 | } 18 | 19 | public View (Context context, global::Android.Util.IAttributeSet attrs) : 20 | base (context, attrs) 21 | { 22 | Initialize (); 23 | } 24 | 25 | public View (Context context, global::Android.Util.IAttributeSet attrs, int defStyle) : 26 | base (context, attrs, defStyle) 27 | { 28 | Initialize (); 29 | } 30 | 31 | void Initialize () 32 | { 33 | } 34 | 35 | Color defaultBackgroundColor = Colors.Black; 36 | 37 | public Color BackgroundColor { 38 | get { 39 | try { 40 | return base.DrawingCacheBackgroundColor.ToColor (); 41 | } catch (Exception ex) { 42 | Debug.WriteLine (ex); 43 | return defaultBackgroundColor; 44 | } 45 | } 46 | set { 47 | try { 48 | base.SetBackgroundColor (value.ToColor ()); 49 | } catch (Exception ex) { 50 | Debug.WriteLine (ex); 51 | } 52 | } 53 | } 54 | 55 | RectangleF IView.Bounds { 56 | get { 57 | return new RectangleF (0, 0, Width, Height); 58 | } 59 | } 60 | 61 | } 62 | 63 | public class Canvas : View, ICanvas 64 | { 65 | public event EventHandler TouchBegan = delegate {}; 66 | public event EventHandler TouchMoved = delegate {}; 67 | public event EventHandler TouchCancelled = delegate {}; 68 | public event EventHandler TouchEnded = delegate {}; 69 | 70 | public event EventHandler Drawing = delegate {}; 71 | 72 | public Canvas () : 73 | base (DocumentListAppActivity.Shared) 74 | { 75 | Initialize (); 76 | } 77 | 78 | public Canvas (Context context) : 79 | base (context) 80 | { 81 | Initialize (); 82 | } 83 | 84 | public Canvas (Context context, global::Android.Util.IAttributeSet attrs) : 85 | base (context, attrs) 86 | { 87 | Initialize (); 88 | } 89 | 90 | public Canvas (Context context, global::Android.Util.IAttributeSet attrs, int defStyle) : 91 | base (context, attrs, defStyle) 92 | { 93 | Initialize (); 94 | } 95 | 96 | void Initialize () 97 | { 98 | } 99 | 100 | public override void Draw (global::Android.Graphics.Canvas canvas) 101 | { 102 | base.Draw (canvas); 103 | } 104 | 105 | public void Invalidate (RectangleF dirtyRect) 106 | { 107 | Invalidate (dirtyRect.ToRect ()); 108 | } 109 | } 110 | } 111 | 112 | -------------------------------------------------------------------------------- /Praeclarum/IO/EmptyFileSystem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Collections.Generic; 4 | using System.Collections.ObjectModel; 5 | 6 | namespace Praeclarum.IO 7 | { 8 | public class EmptyFileSystem : IFileSystem 9 | { 10 | public ICollection FileExtensions { get; private set; } 11 | 12 | public EmptyFileSystem () 13 | { 14 | FileExtensions = new Collection (); 15 | Description = "Empty"; 16 | } 17 | 18 | #pragma warning disable 67 19 | public event EventHandler FilesChanged; 20 | #pragma warning restore 67 21 | 22 | public Task Initialize () 23 | { 24 | return Task.FromResult (null); 25 | } 26 | 27 | public string Id { 28 | get { 29 | return "Empty"; 30 | } 31 | } 32 | public string IconUrl => null; 33 | public string ShortDescription { get { return Description; } } 34 | 35 | public bool CanRemoveFileSystem => false; 36 | public void RemoveFileSystem () { } 37 | 38 | public int MaxDirectoryDepth { get { return 0; } } 39 | 40 | public string Description { get; set; } 41 | 42 | public bool IsWritable { get { return false; } } 43 | 44 | public bool JustForApp { get { return true; } } 45 | 46 | public bool IsAvailable { get { return true; } } 47 | public string AvailabilityReason { get { return ""; } } 48 | 49 | public virtual bool IsSyncing => false; 50 | public string SyncStatus { get { return ""; } } 51 | 52 | public string GetLocalPath (string path) 53 | { 54 | return ""; 55 | } 56 | 57 | public Task GetFile (string path) 58 | { 59 | throw new Exception ("Empty File System contains no files"); 60 | } 61 | 62 | public Task CreateFile (string path, byte[] contents) 63 | { 64 | throw new Exception ("Empty File System cannot hold files"); 65 | } 66 | 67 | public Task CreateDirectory (string path) 68 | { 69 | return Task.FromResult (false); 70 | } 71 | 72 | public Task Move (string fromPath, string toPath) 73 | { 74 | return Task.FromResult (false); 75 | } 76 | 77 | public Task DeleteFile (string path) 78 | { 79 | var tcs = new TaskCompletionSource (); 80 | tcs.SetResult (false); 81 | return tcs.Task; 82 | } 83 | public bool ListFilesIsFast { get { return true; } } 84 | public Task> ListFiles (string directory) 85 | { 86 | return Task.FromResult (files); 87 | } 88 | List files = new List (); 89 | 90 | public Task FileExists (string path) 91 | { 92 | return Task.FromResult (false); 93 | } 94 | } 95 | 96 | public class LoadingFileSystem : EmptyFileSystem 97 | { 98 | public override bool IsSyncing => true; 99 | 100 | public LoadingFileSystem () 101 | { 102 | Description = "Loading Storage..."; 103 | } 104 | } 105 | } 106 | 107 | -------------------------------------------------------------------------------- /Praeclarum.iOS/UI/TranslateSection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Foundation; 4 | using System.IO; 5 | using System.Linq; 6 | 7 | namespace Praeclarum.UI 8 | { 9 | public class TranslateSection : PFormSection 10 | { 11 | class Contrib 12 | { 13 | public string Name; 14 | public string Language; 15 | } 16 | 17 | readonly List contribs = new List (); 18 | 19 | public TranslateSection () 20 | : base ("Use English", new Command ("Help Translate")) 21 | { 22 | try { 23 | var path = NSBundle.MainBundle.PathForResource ("LanguageCredits", "txt"); 24 | var q = from line in File.ReadAllLines (path) 25 | let tline = line.Trim () 26 | where tline.Length > 0 27 | let parts = tline.Split ('/') 28 | where parts.Length == 2 29 | select new Contrib { Name = parts[0].Trim (), Language = parts[1].Trim () }; 30 | contribs.AddRange (q); 31 | } 32 | catch (Exception ex) { 33 | Log.Error (ex); 34 | } 35 | Hint = "iCircuit is translated into several languages thanks to volunteers.".Localize (); 36 | } 37 | 38 | public override bool GetItemNavigates (object item) 39 | { 40 | return "Use English" != item.ToString (); 41 | } 42 | 43 | public override bool GetItemChecked (object item) 44 | { 45 | if ("Use English" == item.ToString ()) { 46 | return DocumentAppDelegate.Shared.Settings.UseEnglish; 47 | } 48 | return false; 49 | } 50 | 51 | public override bool SelectItem (object item) 52 | { 53 | if (item.ToString () == "Use English") { 54 | DocumentAppDelegate.Shared.Settings.UseEnglish = !DocumentAppDelegate.Shared.Settings.UseEnglish; 55 | SetNeedsReloadAll (); 56 | } 57 | else { 58 | var f = new TranslateForm (contribs); 59 | f.NavigationItem.RightBarButtonItem = new UIKit.UIBarButtonItem (UIKit.UIBarButtonSystemItem.Done, (s, e) => { 60 | if (f != null && f.PresentingViewController != null) { 61 | f.DismissViewController (true, null); 62 | } 63 | }); 64 | if (this.Form.NavigationController != null) { 65 | this.Form.NavigationController.PushViewController (f, true); 66 | } 67 | } 68 | return false; 69 | } 70 | 71 | class TranslateForm : PForm 72 | { 73 | public TranslateForm (List contribs) : base ("Help Translate".Localize ()) 74 | { 75 | var github = new PFormSection { 76 | Hint = "GitHub is used to coordinate the translation effort.".Localize (), 77 | Items = { new OpenUrlCommand ("Translations on Github", "https://github.com/praeclarum/CircuitTranslations") }, 78 | }; 79 | 80 | var people = new PFormSection { 81 | Title = "Contributors".Localize (), 82 | Hint = "Thank you for your help!".Localize () 83 | }; 84 | foreach (var c in contribs) { 85 | people.Items.Add (c.Name); 86 | } 87 | 88 | Sections.Add (github); 89 | Sections.Add (people); 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Praeclarum.iOS/Praeclarum.iOS.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0-ios26.0;net9.0-maccatalyst26.0 4 | 1.0.0 5 | disable 6 | false 7 | NO_DROPBOX 8 | false 9 | true 10 | $(NoWarn);NU1900;XCODE_26_0_PREVIEW 11 | 12 | 13 | 14 | 12.2 15 | 16 | 17 | 10.0 18 | 19 | 20 | 15.0 21 | 22 | 23 | 24 | false 25 | false 26 | 27 | 28 | false 29 | false 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | App\StoreManager.cs 45 | 46 | 47 | UI\ProForm.cs 48 | 49 | 50 | App\ProService.cs 51 | 52 | 53 | UI\PForm.cs 54 | 55 | 56 | App\DocumentAppSettings.cs 57 | 58 | 59 | UI\OpenUrlCommand.cs 60 | 61 | 62 | App\DocumentApplication.cs 63 | 64 | 65 | UI\IThemeAware.cs 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /Praeclarum.iOS/UI/MoveDocumentsForm.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Praeclarum.IO; 3 | using System.Threading.Tasks; 4 | using System.IO; 5 | using System.Linq; 6 | 7 | namespace Praeclarum.UI 8 | { 9 | public class MoveDocumentsForm : PForm 10 | { 11 | public IFileSystem FileSystem { get; private set; } 12 | public string Directory { get; private set; } 13 | 14 | public MoveDocumentsForm () 15 | { 16 | Title = "Move to"; 17 | 18 | var fileSystems = FileSystemManager.Shared.FileSystems; 19 | 20 | foreach (var fs in fileSystems) { 21 | Sections.Add (new FileSystemSection (fs)); 22 | } 23 | } 24 | 25 | void SelectItem (IFileSystem fs, string dir) 26 | { 27 | FileSystem = fs; 28 | Directory = dir; 29 | DismissAsync (); 30 | } 31 | 32 | class FileSystemSection : PFormSection 33 | { 34 | readonly IFileSystem fileSystem; 35 | 36 | public FileSystemSection (IFileSystem fileSystem) 37 | { 38 | this.fileSystem = fileSystem; 39 | 40 | AddFileSystemsAsync ().ContinueWith (task => { 41 | if (!task.IsFaulted) 42 | SetNeedsReload (); 43 | }, TaskScheduler.FromCurrentSynchronizationContext ()); 44 | } 45 | 46 | async Task AddFileSystemsAsync () 47 | { 48 | await fileSystem.Sync (TimeSpan.FromSeconds (5)); 49 | await AddDirAsync (""); 50 | } 51 | 52 | async Task AddDirAsync (string dir) 53 | { 54 | try { 55 | Items.Add (dir); 56 | 57 | var files = await fileSystem.ListFiles (dir); 58 | foreach (var f in files.OrderBy (x => x.Path)) { 59 | if (f.IsDirectory) { 60 | await AddDirAsync (f.Path); 61 | } 62 | } 63 | 64 | } catch (Exception ex) { 65 | Console.WriteLine (ex); 66 | } 67 | } 68 | 69 | public override string GetItemTitle (object item) 70 | { 71 | var path = (string)item; 72 | if (!path.StartsWith ("/", StringComparison.Ordinal)) 73 | path = "/" + path; 74 | var d = path.Count (x => x == '/'); 75 | string title; 76 | if (path == "/") { 77 | title = fileSystem.Description; 78 | } else { 79 | title = new string (' ', 6 * d) + Path.GetFileName (path); 80 | } 81 | return title; 82 | } 83 | 84 | public override bool GetItemEnabled (object item) 85 | { 86 | return fileSystem.IsWritable; 87 | } 88 | 89 | public override bool SelectItem (object item) 90 | { 91 | var f = Form as MoveDocumentsForm; 92 | if (f == null) 93 | return false; 94 | 95 | f.SelectItem (fileSystem, (string)item); 96 | 97 | return true; 98 | } 99 | 100 | public override bool GetItemChecked (object item) 101 | { 102 | var path = (string)item; 103 | return fileSystem == FileSystemManager.Shared.ActiveFileSystem && (path == DocumentAppDelegate.Shared.CurrentDirectory); 104 | } 105 | 106 | public override string GetItemImage (object item) 107 | { 108 | return fileSystem.GetType ().Name; 109 | } 110 | } 111 | } 112 | } 113 | 114 | -------------------------------------------------------------------------------- /Praeclarum/App/AIChat.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Collections.ObjectModel; 6 | using System.ComponentModel; 7 | using System.Runtime.CompilerServices; 8 | 9 | // ReSharper disable InconsistentNaming 10 | 11 | namespace Praeclarum.App; 12 | 13 | public class AIChat 14 | { 15 | public ObservableCollection Messages { get; } = []; 16 | 17 | public class Message : INotifyPropertyChanged 18 | { 19 | private string _text = ""; 20 | private MessageType _type = MessageType.Assistant; 21 | private bool _showProgress; 22 | 23 | public string Text 24 | { 25 | get => _text; 26 | set 27 | { 28 | if (value == _text) 29 | { 30 | return; 31 | } 32 | _text = value; 33 | OnPropertyChanged (); 34 | } 35 | } 36 | 37 | public MessageType Type 38 | { 39 | get => _type; 40 | set 41 | { 42 | if (value == _type) 43 | { 44 | return; 45 | } 46 | _type = value; 47 | OnPropertyChanged (); 48 | OnPropertyChanged (nameof(IsSystem)); 49 | OnPropertyChanged (nameof(IsUser)); 50 | OnPropertyChanged (nameof(IsAssistant)); 51 | OnPropertyChanged (nameof(IsError)); 52 | } 53 | } 54 | 55 | public bool IsSystem => Type == MessageType.System; 56 | public bool IsUser => Type == MessageType.User; 57 | public bool IsAssistant => Type == MessageType.Assistant; 58 | public bool IsError => Type == MessageType.Error; 59 | public event PropertyChangedEventHandler? PropertyChanged; 60 | 61 | public bool ShowProgress 62 | { 63 | get => _showProgress; 64 | set 65 | { 66 | if (value == _showProgress) 67 | { 68 | return; 69 | } 70 | _showProgress = value; 71 | OnPropertyChanged (); 72 | } 73 | } 74 | 75 | protected virtual void OnPropertyChanged ([CallerMemberName] string? propertyName = null) 76 | { 77 | PropertyChanged?.Invoke (this, new PropertyChangedEventArgs (propertyName)); 78 | } 79 | } 80 | 81 | public enum MessageType 82 | { 83 | System, 84 | User, 85 | Assistant, 86 | Error 87 | } 88 | } 89 | 90 | public class AIChatHistory : INotifyPropertyChanged 91 | { 92 | private int _activeChatIndex = 0; 93 | public ObservableCollection Chats { get; } = [new ()]; 94 | 95 | public int ActiveChatIndex 96 | { 97 | get => _activeChatIndex; 98 | set 99 | { 100 | var safeValue = Math.Clamp(value, 0, Chats.Count - 1); 101 | if (safeValue == _activeChatIndex) 102 | { 103 | return; 104 | } 105 | 106 | _activeChatIndex = safeValue; 107 | OnPropertyChanged (); 108 | OnPropertyChanged (nameof(ActiveChat)); 109 | } 110 | } 111 | 112 | public AIChat ActiveChat => Chats[ActiveChatIndex]; 113 | public event PropertyChangedEventHandler? PropertyChanged; 114 | 115 | protected virtual void OnPropertyChanged ([CallerMemberName] string? propertyName = null) 116 | { 117 | PropertyChanged?.Invoke (this, new PropertyChangedEventArgs (propertyName)); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Praeclarum.iOS/UI/OldForm.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.ObjectModel; 3 | using UIKit; 4 | using System.Linq; 5 | 6 | namespace Praeclarum.UI 7 | { 8 | public class Form : ObservableCollection 9 | { 10 | public string Title { get; set; } 11 | public Form () 12 | { 13 | Title = ""; 14 | } 15 | } 16 | 17 | public class FormElement 18 | { 19 | public string Hint { get; set; } 20 | 21 | public FormElement () 22 | { 23 | Hint = ""; 24 | } 25 | } 26 | 27 | public class FormAction : FormElement 28 | { 29 | public string Title { get; set; } 30 | 31 | public event EventHandler Executed; 32 | 33 | public FormAction () 34 | { 35 | Title = ""; 36 | } 37 | 38 | public FormAction (string title) 39 | { 40 | Title = title; 41 | } 42 | 43 | public FormAction (string title, Action action) 44 | { 45 | Title = title; 46 | Executed += (s, e) => action (); 47 | } 48 | 49 | public void Execute () 50 | { 51 | OnExecuted (); 52 | } 53 | 54 | protected virtual void OnExecuted () 55 | { 56 | var ev = Executed; 57 | if (ev != null) 58 | ev (this, EventArgs.Empty); 59 | } 60 | } 61 | 62 | public static partial class FormUI 63 | { 64 | public static UIActionSheet ToActionSheet (this Form form) 65 | { 66 | var actions = form.OfType ().ToList (); 67 | 68 | var ActionSheet = new UIActionSheet (form.Title, 69 | (IUIActionSheetDelegate)null, null, null, actions.Select (x => x.Title).ToArray ()); 70 | 71 | ActionSheet.CancelButtonIndex = ActionSheet.ButtonCount - 1; 72 | 73 | ActionSheet.Clicked += (ss, se) => { 74 | try { 75 | var index = (int)se.ButtonIndex; 76 | if (0 <= index && index < actions.Count) { 77 | Foundation.NSOperationQueue.MainQueue.AddOperation(() => 78 | { 79 | try { 80 | actions[index].Execute (); 81 | } catch (Exception ex) { 82 | Log.Error (ex); 83 | } 84 | }); 85 | } 86 | } catch (Exception ex) { 87 | Log.Error (ex); 88 | } 89 | }; 90 | 91 | return ActionSheet; 92 | } 93 | 94 | public static UIActionSheet ToActionSheet (this PForm form) 95 | { 96 | var q = from s in form.Sections 97 | from i in s.Items 98 | let c = i as Command 99 | where c != null 100 | select c; 101 | var actions = q.ToList (); 102 | 103 | var ActionSheet = new UIActionSheet (form.Title); 104 | foreach (var a in actions) { 105 | ActionSheet.Add (a.Name); 106 | } 107 | 108 | ActionSheet.CancelButtonIndex = ActionSheet.ButtonCount - 1; 109 | 110 | ActionSheet.Clicked += async (ss, se) => { 111 | var index = (int)se.ButtonIndex; 112 | if (0 <= index && index < actions.Count) { 113 | try { 114 | await actions[index].ExecuteAsync (); 115 | } catch (Exception ex) { 116 | Console.WriteLine ("Failed to execute action: " + ex); 117 | } 118 | } 119 | }; 120 | 121 | return ActionSheet; 122 | } 123 | } 124 | } 125 | 126 | -------------------------------------------------------------------------------- /Praeclarum.Utilities.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Praeclarum", "Praeclarum\Praeclarum.csproj", "{1BFC2CDF-74DA-41CF-8D3A-FD50287EE821}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Praeclarum.Mac", "Praeclarum.Mac\Praeclarum.Mac.csproj", "{6A6ED4BB-D71A-4CC6-937C-BA4B5A9323E7}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Praeclarum.Utilities", "Praeclarum.Utilities\Praeclarum.Utilities.csproj", "{1C9A9972-A715-4D0E-8D94-912F8F5E9748}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Praeclarum.iOS", "Praeclarum.iOS\Praeclarum.iOS.csproj", "{1715EDA6-58D5-48B5-B278-FD4323D88E8A}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Praeclarum.Android", "Praeclarum.Android\Praeclarum.Android.csproj", "{FB8E4302-7447-4422-BAB9-B774EAAC9DDB}" 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Release|Any CPU = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(SolutionProperties) = preSolution 22 | HideSolutionNode = FALSE 23 | EndGlobalSection 24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 25 | {1BFC2CDF-74DA-41CF-8D3A-FD50287EE821}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {1BFC2CDF-74DA-41CF-8D3A-FD50287EE821}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {1BFC2CDF-74DA-41CF-8D3A-FD50287EE821}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {1BFC2CDF-74DA-41CF-8D3A-FD50287EE821}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {6A6ED4BB-D71A-4CC6-937C-BA4B5A9323E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {6A6ED4BB-D71A-4CC6-937C-BA4B5A9323E7}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {6A6ED4BB-D71A-4CC6-937C-BA4B5A9323E7}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {6A6ED4BB-D71A-4CC6-937C-BA4B5A9323E7}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {1C9A9972-A715-4D0E-8D94-912F8F5E9748}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {1C9A9972-A715-4D0E-8D94-912F8F5E9748}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {1C9A9972-A715-4D0E-8D94-912F8F5E9748}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {1C9A9972-A715-4D0E-8D94-912F8F5E9748}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {1715EDA6-58D5-48B5-B278-FD4323D88E8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {1715EDA6-58D5-48B5-B278-FD4323D88E8A}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {1715EDA6-58D5-48B5-B278-FD4323D88E8A}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {1715EDA6-58D5-48B5-B278-FD4323D88E8A}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {FB8E4302-7447-4422-BAB9-B774EAAC9DDB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {FB8E4302-7447-4422-BAB9-B774EAAC9DDB}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {FB8E4302-7447-4422-BAB9-B774EAAC9DDB}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {FB8E4302-7447-4422-BAB9-B774EAAC9DDB}.Release|Any CPU.Build.0 = Release|Any CPU 45 | EndGlobalSection 46 | EndGlobal 47 | -------------------------------------------------------------------------------- /Praeclarum.Mac/Praeclarum.Mac.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0-macos26.0 4 | disable 5 | false 6 | 1.0.0 7 | 12.0 8 | false 9 | true 10 | $(NoWarn);NU1900;XCODE_26_0_PREVIEW 11 | 12 | 13 | 14 | false 15 | false 16 | 17 | 18 | false 19 | false 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | GlobalSuppressions.cs 28 | 29 | 30 | CoreGraphicsGraphics.cs 31 | 32 | 33 | NSMutableAttributedStringWrapper.cs 34 | 35 | 36 | CTStringAttributesWrapper.cs 37 | 38 | 39 | IO\NSUrlFileSystem.cs 40 | 41 | 42 | UI\AIChatView.cs 43 | 44 | 45 | App\StoreManager.cs 46 | 47 | 48 | UI\ProForm.cs 49 | 50 | 51 | App\ProService.cs 52 | 53 | 54 | UI\PForm.cs 55 | 56 | 57 | App\DocumentAppSettings.cs 58 | 59 | 60 | UI\OpenUrlCommand.cs 61 | 62 | 63 | App\DocumentApplication.cs 64 | 65 | 66 | UI\IThemeAware.cs 67 | 68 | 69 | UI\Theme.cs 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /Praeclarum/App/DocumentApplication.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | 3 | using System; 4 | using Praeclarum.UI; 5 | using System.Collections.Generic; 6 | using Praeclarum.Graphics; 7 | using System.Linq; 8 | using Foundation; 9 | using System.Threading.Tasks; 10 | 11 | namespace Praeclarum.App 12 | { 13 | public abstract class DocumentApplication : Application 14 | { 15 | public virtual string AutoOpenDocumentPath { get { return ""; } } 16 | 17 | public bool UseDocumentBrowser { get; set; } 18 | 19 | public virtual IDocument? CreateDocument (string localFilePath) 20 | { 21 | return null; 22 | } 23 | 24 | public virtual IDocument? CreateDocument (NSUrl url) 25 | { 26 | return null; 27 | } 28 | 29 | public virtual IDocumentEditor? CreateDocumentEditor (int docIndex, List docs) 30 | { 31 | return null; 32 | } 33 | 34 | public virtual IDocumentEditor? CreateDocumentEditor (NSUrl url) 35 | { 36 | return null; 37 | } 38 | 39 | public virtual IEnumerable FileExtensions 40 | { 41 | get { 42 | yield return "txt"; 43 | } 44 | } 45 | 46 | public virtual IEnumerable ContentTypes { 47 | get { 48 | yield return "public.plain-text"; 49 | } 50 | } 51 | 52 | public virtual string DefaultExtension { get { return FileExtensions.First (); } } 53 | 54 | public virtual string DocumentBaseName { get { return "Document"; } } 55 | public virtual string DocumentBaseNamePluralized { get { return Pluralize (DocumentBaseName); } } 56 | 57 | public virtual float ThumbnailAspectRatio { get { return 8.5f/11.0f; } } 58 | 59 | #if __IOS__ 60 | public virtual void DrawThumbnail (IDocument doc, IGraphics g, SizeF size, Theme theme, bool readOnlyDoc) 61 | { 62 | g.SetColor (GetThumbnailBackgroundColor (theme)); 63 | g.FillRect (new RectangleF (PointF.Empty, size)); 64 | } 65 | 66 | public virtual Color GetThumbnailBackgroundColor (Theme theme) { 67 | return theme.DocumentBackgroundColor.GetColor (); 68 | } 69 | #endif 70 | 71 | /// 72 | /// Called once everything is loaded 73 | /// 74 | public virtual void OnFileSystemInitialized () 75 | { 76 | } 77 | 78 | static string Pluralize (string word) 79 | { 80 | if (string.IsNullOrEmpty (word)) 81 | return "s"; 82 | 83 | var last = word [word.Length - 1]; 84 | 85 | if (last == 'y') { 86 | if (word.Length <= 1) 87 | return "ies"; 88 | var prev = word [word.Length - 2]; 89 | if (prev == 'a' || prev == 'e' || prev == 'i' || prev == 'o') 90 | return word + "s"; 91 | if (prev == 'u' && !word.EndsWith ("quy", StringComparison.Ordinal)) 92 | return word + "s"; 93 | return word.Substring (0, word.Length - 1) + "ies"; 94 | } 95 | 96 | if (last == 'o' || 97 | last == 's' || 98 | last == 'z' || 99 | word.EndsWith ("sh", StringComparison.Ordinal) || 100 | word.EndsWith ("ch", StringComparison.Ordinal)) 101 | return word + "es"; 102 | 103 | return word + "s"; 104 | } 105 | 106 | public bool HasGallery => !string.IsNullOrEmpty (GalleryUrl); 107 | public virtual string GalleryUrl => ""; 108 | public virtual string GalleryDownloadUrlPattern => ""; 109 | public virtual bool IsPatronSupported { 110 | get { return false; } 111 | } 112 | public virtual bool HasTips { 113 | get { return false; } 114 | } 115 | public virtual bool HasPro { 116 | get { return false; } 117 | } 118 | public virtual bool NagForReviews { 119 | get { return false; } 120 | } 121 | 122 | public virtual (string Section, string[] Features)[] GetProFeatures () { return Array.Empty<(string,string[])>(); } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Praeclarum/StringRange.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Praeclarum 4 | { 5 | public struct StringRange 6 | { 7 | public int Location; 8 | public int Length; 9 | 10 | public int End { get { return Location + Length; } } 11 | 12 | public StringRange (int location, int length) 13 | { 14 | Location = location; 15 | Length = length; 16 | } 17 | 18 | public StringRange FitIn (int length) 19 | { 20 | var loc = Location; 21 | if (loc > length) 22 | loc = length; 23 | var end = loc + Length; 24 | if (end > length) 25 | end = length; 26 | return new StringRange (loc, end - loc); 27 | } 28 | 29 | public StringRange Offset (int offset) 30 | { 31 | return new StringRange (offset + Location, Length); 32 | } 33 | 34 | public string Substring (string source) 35 | { 36 | return source.Substring (Location, Length); 37 | } 38 | 39 | public StringRange WithLength (int length) 40 | { 41 | return new StringRange (Location, length); 42 | } 43 | 44 | public bool Contains (StringRange other) 45 | { 46 | var end = Location + Length; 47 | var oend = other.Location + other.Length; 48 | 49 | return 50 | Location <= other.Location && other.Location < end && 51 | Location <= oend && oend < end; 52 | } 53 | 54 | public bool Intersects (StringRange other) 55 | { 56 | if (other.Location >= Location + Length) 57 | return false; 58 | if (Location >= other.Location + other.Length) 59 | return false; 60 | return true; 61 | } 62 | 63 | public bool EndsWith (string right, string source) 64 | { 65 | if (right.Length > Length) 66 | return false; 67 | 68 | var o = Location + Length - 1; 69 | 70 | for (var i = 0; i < right.Length; i++) { 71 | if (source [o - i] != right [i]) 72 | return false; 73 | } 74 | 75 | return true; 76 | } 77 | 78 | public override bool Equals (object obj) 79 | { 80 | if (!(obj is StringRange)) return false; 81 | var o = (StringRange)obj; 82 | return o.Location == Location && o.Length == Length; 83 | } 84 | 85 | public override int GetHashCode () 86 | { 87 | return Location.GetHashCode () + Length.GetHashCode (); 88 | } 89 | 90 | public static bool operator == (StringRange a, StringRange b) 91 | { 92 | return a.Location == b.Location && a.Length == b.Length; 93 | } 94 | 95 | public static bool operator != (StringRange a, StringRange b) 96 | { 97 | return a.Location != b.Location || a.Length != b.Length; 98 | } 99 | 100 | public override string ToString () 101 | { 102 | return string.Format ("[{0}, {1}]", Location, Length); 103 | } 104 | } 105 | 106 | public struct StringReplacement 107 | { 108 | public string Text; 109 | public StringRange Range; 110 | 111 | public StringReplacement (string text, StringRange range) 112 | { 113 | Text = text; 114 | Range = range; 115 | } 116 | 117 | public override bool Equals (object obj) 118 | { 119 | if (!(obj is StringReplacement)) return false; 120 | var o = (StringReplacement)obj; 121 | return o.Range == Range && o.Text == Text; 122 | } 123 | 124 | public override int GetHashCode () 125 | { 126 | return Text.GetHashCode () + Range.GetHashCode (); 127 | } 128 | 129 | public static bool operator == (StringReplacement a, StringReplacement b) 130 | { 131 | return a.Text == b.Text && a.Range == b.Range; 132 | } 133 | 134 | public static bool operator != (StringReplacement a, StringReplacement b) 135 | { 136 | return a.Text != b.Text || a.Range != b.Range; 137 | } 138 | 139 | public override string ToString () 140 | { 141 | return string.Format ("\"{0}\"@{1}", Text, Range); 142 | } 143 | } 144 | 145 | } 146 | 147 | -------------------------------------------------------------------------------- /Praeclarum.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 11.00 3 | # Visual Studio 2010 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Praeclarum.iOS", "Praeclarum.iOS\Praeclarum.iOS.csproj", "{BAEC0100-059C-4750-90E4-2D0392A76713}" 5 | EndProject 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Praeclarum.Mac", "Praeclarum.Mac\Praeclarum.Mac.csproj", "{05E9FE96-8FBA-403A-879C-16A59954CEB8}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Praeclarum", "Praeclarum\Praeclarum.csproj", "{8956A28C-EE46-4576-ADBC-CD26BD147CD0}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Praeclarum.Android", "Praeclarum.Android\Praeclarum.Android.csproj", "{1A598D1A-2E83-4387-8F57-15A42ED5549D}" 11 | EndProject 12 | Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Praeclarum.iOS.Shared", "Praeclarum.iOS\Praeclarum.iOS.Shared.shproj", "{97D7EEB3-E116-4790-824A-54A8BB53D102}" 13 | EndProject 14 | Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Praeclarum.Shared", "Praeclarum\Praeclarum.Shared.shproj", "{B8A8BCD4-7815-4994-A9AF-3E603C844835}" 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Praeclarum.Controls.Mac", "Praeclarum.Controls.Mac\Praeclarum.Controls.Mac.csproj", "{FA926E9F-A5B6-4F7B-A3A1-ED90BFF9A10B}" 17 | EndProject 18 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Praeclarum.Controls.iOS", "Praeclarum.Controls.iOS\Praeclarum.Controls.iOS.csproj", "{961DF89F-CDB2-48AB-BA17-0E9D4F341DD3}" 19 | EndProject 20 | Global 21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 22 | Debug|Any CPU = Debug|Any CPU 23 | Release|Any CPU = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 26 | {05E9FE96-8FBA-403A-879C-16A59954CEB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {05E9FE96-8FBA-403A-879C-16A59954CEB8}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {05E9FE96-8FBA-403A-879C-16A59954CEB8}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {05E9FE96-8FBA-403A-879C-16A59954CEB8}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {1A598D1A-2E83-4387-8F57-15A42ED5549D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {1A598D1A-2E83-4387-8F57-15A42ED5549D}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {1A598D1A-2E83-4387-8F57-15A42ED5549D}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {1A598D1A-2E83-4387-8F57-15A42ED5549D}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {8956A28C-EE46-4576-ADBC-CD26BD147CD0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {8956A28C-EE46-4576-ADBC-CD26BD147CD0}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {8956A28C-EE46-4576-ADBC-CD26BD147CD0}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {8956A28C-EE46-4576-ADBC-CD26BD147CD0}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {961DF89F-CDB2-48AB-BA17-0E9D4F341DD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {961DF89F-CDB2-48AB-BA17-0E9D4F341DD3}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {961DF89F-CDB2-48AB-BA17-0E9D4F341DD3}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {961DF89F-CDB2-48AB-BA17-0E9D4F341DD3}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {BAEC0100-059C-4750-90E4-2D0392A76713}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {BAEC0100-059C-4750-90E4-2D0392A76713}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {BAEC0100-059C-4750-90E4-2D0392A76713}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {BAEC0100-059C-4750-90E4-2D0392A76713}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {FA926E9F-A5B6-4F7B-A3A1-ED90BFF9A10B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {FA926E9F-A5B6-4F7B-A3A1-ED90BFF9A10B}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {FA926E9F-A5B6-4F7B-A3A1-ED90BFF9A10B}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {FA926E9F-A5B6-4F7B-A3A1-ED90BFF9A10B}.Release|Any CPU.Build.0 = Release|Any CPU 50 | EndGlobalSection 51 | EndGlobal 52 | -------------------------------------------------------------------------------- /Praeclarum.iOS/UI/DocumentEditor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UIKit; 4 | using Praeclarum.App; 5 | using System.Threading.Tasks; 6 | using Foundation; 7 | 8 | namespace Praeclarum.UI 9 | { 10 | public class DocumentEditor : UIViewController, IDocumentEditor 11 | { 12 | protected DocumentReference docRef; 13 | protected NSUrl docUrl; 14 | 15 | public DocumentReference DocumentReference { get { return docRef; } } 16 | public virtual IDocument Document => DocumentReference?.Document; 17 | 18 | public bool IsPreviewing { get; set; } 19 | 20 | public DocumentEditor (DocumentReference docRef) 21 | { 22 | this.docRef = docRef; 23 | } 24 | 25 | public DocumentEditor(NSUrl url) 26 | { 27 | this.docUrl = url; 28 | } 29 | 30 | public override void ViewDidLoad () 31 | { 32 | base.ViewDidLoad (); 33 | 34 | try { 35 | View.BackgroundColor = UIColor.White; 36 | 37 | OnCreated (); 38 | 39 | viewLoaded = true; 40 | foreach (var a in this.viewLoadedActions) { 41 | try { 42 | a (); 43 | } 44 | catch (Exception aex) { 45 | Log.Error (aex); 46 | } 47 | } 48 | viewLoadedActions.Clear (); 49 | 50 | } catch (Exception ex) { 51 | Log.Error (ex); 52 | } 53 | } 54 | 55 | public override async void ViewWillDisappear (bool animated) 56 | { 57 | base.ViewWillDisappear (animated); 58 | 59 | try { 60 | if (DocumentReference != null && DocumentReference.IsOpen && IsPreviewing) { 61 | Console.WriteLine ("CLOSING PREVIEW DOCUMENT"); 62 | UnbindDocument (); 63 | UnbindUI (); 64 | await DocumentReference.Close (); 65 | } 66 | } catch (Exception ex) { 67 | Log.Error (ex); 68 | } 69 | } 70 | 71 | IView editorView = null; 72 | 73 | public IView EditorView { 74 | get { 75 | return editorView; 76 | } 77 | set { 78 | if (editorView == value) 79 | return; 80 | 81 | editorView = value; 82 | 83 | var v = editorView as UIView; 84 | if (v != null) { 85 | v.Frame = View.Bounds; 86 | v.AutoresizingMask = UIViewAutoresizing.FlexibleDimensions; 87 | View.AddSubview (v); 88 | } 89 | } 90 | } 91 | 92 | public virtual void OnCreated () 93 | { 94 | } 95 | 96 | public override async void DidMoveToParentViewController (UIViewController parent) 97 | { 98 | try { 99 | if (parent == null) { 100 | DocumentAppDelegate.Shared.OpenedDocIndex = -1; 101 | await DocumentAppDelegate.Shared.CloseDocumentEditor (this, unbindUI: true, deleteThumbnail: true, reloadThumbnail: true); 102 | } 103 | } catch (Exception ex) { 104 | Log.Error (ex); 105 | } 106 | } 107 | 108 | bool viewLoaded = false; 109 | 110 | List viewLoadedActions = new List(); 111 | protected void WhenViewLoaded(Action action) { 112 | if (viewLoaded) { 113 | BeginInvokeOnMainThread (() => { 114 | try { 115 | action (); 116 | } 117 | catch (Exception ex) { 118 | Log.Error (ex); 119 | } 120 | }); 121 | } else { 122 | viewLoadedActions.Add (action); 123 | } 124 | } 125 | 126 | #region IDocumentEditor implementation 127 | 128 | public virtual void DidEnterBackground () 129 | { 130 | } 131 | 132 | public virtual void WillEnterForeground () 133 | { 134 | } 135 | 136 | public virtual void BindDocument () 137 | { 138 | 139 | } 140 | 141 | public virtual Task SaveDocument () 142 | { 143 | return Task.FromResult (null); 144 | } 145 | 146 | public virtual void UnbindDocument () 147 | { 148 | 149 | } 150 | 151 | public virtual void UnbindUI () 152 | { 153 | 154 | } 155 | 156 | #endregion 157 | } 158 | 159 | public class DocumentViewerAndEditor : DocumentEditor 160 | { 161 | public DocumentViewerAndEditor (DocumentReference docRef) 162 | : base (docRef) 163 | { 164 | NavigationItem.RightBarButtonItem = EditButtonItem; 165 | } 166 | } 167 | } 168 | 169 | -------------------------------------------------------------------------------- /Praeclarum/UI/OpenUrlCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Foundation; 3 | using System.IO; 4 | using System.Threading.Tasks; 5 | 6 | namespace Praeclarum 7 | { 8 | public class OpenUrlCommand : Command 9 | { 10 | public string Url { get; set; } 11 | 12 | public OpenUrlCommand (string name, string url, AsyncAction action = null) 13 | : base (name, action) 14 | { 15 | Url = url; 16 | } 17 | 18 | public override async Task ExecuteAsync () 19 | { 20 | await base.ExecuteAsync (); 21 | if (string.IsNullOrWhiteSpace(Url)) 22 | return; 23 | #if __IOS__ 24 | UIKit.UIApplication.SharedApplication.OpenUrl (NSUrl.FromString (Url)); 25 | #elif __MACOS__ 26 | AppKit.NSWorkspace.SharedWorkspace.OpenUrl (NSUrl.FromString (Url)); 27 | #else 28 | throw new NotSupportedException(); 29 | #endif 30 | } 31 | } 32 | 33 | public class EmailCommand : Command 34 | { 35 | public string Address { get; set; } 36 | public string Subject { get; set; } 37 | public string BodyHtml { get; set; } 38 | 39 | public EmailCommand (string name, string address, AsyncAction action = null) 40 | : base (name, action) 41 | { 42 | Address = address; 43 | Subject = ""; 44 | BodyHtml = ""; 45 | } 46 | 47 | public override async Task ExecuteAsync () 48 | { 49 | await base.ExecuteAsync (); 50 | 51 | if (string.IsNullOrWhiteSpace (Address)) 52 | return; 53 | 54 | #if __IOS__ 55 | var fromVC = UIKit.UIApplication.SharedApplication.Windows [0].RootViewController; 56 | var c = new MessageUI.MFMailComposeViewController (); 57 | c.Finished += (sender, e) => c.DismissViewController (true, null); 58 | c.SetToRecipients (new [] { Address }); 59 | c.SetSubject (Subject); 60 | c.SetMessageBody (BodyHtml, true); 61 | await fromVC.PresentViewControllerAsync (c, true); 62 | #else 63 | throw new NotSupportedException(); 64 | #endif 65 | } 66 | } 67 | 68 | public class EmailSupportCommand : EmailCommand 69 | { 70 | public EmailSupportCommand (string name, string address, AsyncAction action = null) 71 | : base (name, address, action) 72 | { 73 | var mainBundle = NSBundle.MainBundle; 74 | 75 | #if __IOS__ 76 | var dev = UIKit.UIDevice.CurrentDevice; 77 | var devSystemName = dev.SystemName; 78 | #elif __MACOS__ 79 | var devSystemName = "Mac"; 80 | #endif 81 | 82 | var appName = mainBundle.ObjectForInfoDictionary ("CFBundleDisplayName")?.ToString (); 83 | if (string.IsNullOrEmpty (appName)) { 84 | appName = mainBundle.ObjectForInfoDictionary ("CFBundleName")?.ToString (); 85 | } 86 | var version = mainBundle.ObjectForInfoDictionary ("CFBundleVersion"); 87 | 88 | Subject = appName + " Feedback (" + devSystemName + ")"; 89 | 90 | var sb = new System.Text.StringBuilder(); 91 | 92 | sb.AppendFormat ("

    "); 93 | sb.AppendFormat ("
  • Software: {0} {1}
  • ", appName, version); 94 | #if __IOS__ 95 | sb.AppendFormat ("
  • Model: {0}
  • ", dev.Model); 96 | var scr = UIKit.UIScreen.MainScreen; 97 | sb.AppendFormat ("
  • Screen: {0}x{1} @{2}x
  • ", scr.Bounds.Width, scr.Bounds.Height, scr.Scale); 98 | sb.AppendFormat ("
  • System: {0} {1}
  • ", devSystemName, dev.SystemVersion); 99 | #endif 100 | sb.AppendFormat ("
  • Culture: {0}
  • ", System.Globalization.CultureInfo.CurrentCulture.EnglishName); 101 | sb.AppendFormat ("
"); 102 | 103 | BodyHtml = sb.ToString (); 104 | } 105 | } 106 | 107 | public class CopyToClipboardCommand : Command 108 | { 109 | public string Text { get; set; } 110 | 111 | public CopyToClipboardCommand (string name, string text, AsyncAction action = null) 112 | : base (name, action) 113 | { 114 | Text = text; 115 | } 116 | 117 | public override async Task ExecuteAsync () 118 | { 119 | await base.ExecuteAsync (); 120 | #if __IOS__ 121 | UIKit.UIPasteboard.General.String = Text ?? ""; 122 | new UIKit.UIAlertView ("Copied", Text ?? "", (UIKit.IUIAlertViewDelegate)null, "OK").Show (); 123 | #else 124 | throw new NotSupportedException(); 125 | #endif 126 | } 127 | } 128 | } 129 | 130 | -------------------------------------------------------------------------------- /Praeclarum/Praeclarum.Shared.projitems: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 5 | true 6 | {B8A8BCD4-7815-4994-A9AF-3E603C844835} 7 | 8 | 9 | Praeclarum.Shared 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /Praeclarum.iOS/UI/Canvas.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UIKit; 3 | using Praeclarum.Graphics; 4 | 5 | namespace Praeclarum.UI 6 | { 7 | public class View : UIView, IView 8 | { 9 | Color bgColor = Colors.Black; 10 | public new virtual Color BackgroundColor { 11 | get { 12 | try { 13 | return base.BackgroundColor.GetColor (); 14 | } catch (Exception ex) { 15 | Log.Error (ex); 16 | return bgColor; 17 | } 18 | } 19 | set { 20 | try { 21 | bgColor = value; 22 | base.BackgroundColor = value.GetUIColor (); 23 | } catch (Exception ex) { 24 | Log.Error (ex); 25 | } 26 | } 27 | } 28 | 29 | RectangleF IView.Bounds { 30 | get { 31 | return Bounds.ToRectangleF (); 32 | } 33 | } 34 | 35 | public View () 36 | { 37 | } 38 | 39 | public View (RectangleF frame) 40 | : base (frame.ToRectangleF ()) 41 | { 42 | } 43 | } 44 | 45 | public class Canvas : View, ICanvas 46 | { 47 | public event EventHandler Drawing = delegate {}; 48 | public event EventHandler TouchBegan = delegate {}; 49 | public event EventHandler TouchMoved = delegate {}; 50 | public event EventHandler TouchCancelled = delegate {}; 51 | public event EventHandler TouchEnded = delegate {}; 52 | 53 | public Canvas () 54 | { 55 | Initialize (); 56 | } 57 | 58 | public Canvas (RectangleF frame) 59 | : base (frame) 60 | { 61 | Initialize (); 62 | } 63 | 64 | void Initialize () 65 | { 66 | ContentMode = UIViewContentMode.Redraw; 67 | UserInteractionEnabled = true; 68 | MultipleTouchEnabled = true; 69 | } 70 | 71 | public void Invalidate () 72 | { 73 | SetNeedsDisplay (); 74 | } 75 | 76 | public void Invalidate (RectangleF frame) 77 | { 78 | SetNeedsDisplayInRect (frame.ToRectangleF ()); 79 | } 80 | 81 | public override void Draw (CoreGraphics.CGRect rect) 82 | { 83 | try { 84 | base.Draw (rect); 85 | 86 | var c = UIGraphics.GetCurrentContext (); 87 | 88 | var e = new CanvasDrawingEventArgs ( 89 | new CoreGraphicsGraphics (c, true), 90 | Bounds.ToRectangleF () 91 | ); 92 | 93 | Drawing (this, e); 94 | } catch (Exception ex) { 95 | Log.Error (ex); 96 | } 97 | } 98 | 99 | public override void TouchesBegan (Foundation.NSSet touches, UIEvent evt) 100 | { 101 | try { 102 | foreach (UITouch t in touches) { 103 | TouchBegan (this, new CanvasTouchEventArgs { 104 | #if NET6_0_OR_GREATER 105 | TouchId = t.Handle.Handle.ToInt32(), 106 | #else 107 | TouchId = t.Handle.ToInt32 (), 108 | #endif 109 | Location = t.LocationInView (this).ToPointF (), 110 | }); 111 | } 112 | } catch (Exception ex) { 113 | Log.Error (ex); 114 | } 115 | } 116 | 117 | public override void TouchesMoved (Foundation.NSSet touches, UIEvent evt) 118 | { 119 | try { 120 | foreach (UITouch t in touches) { 121 | TouchMoved (this, new CanvasTouchEventArgs { 122 | #if NET6_0_OR_GREATER 123 | TouchId = t.Handle.Handle.ToInt32(), 124 | #else 125 | TouchId = t.Handle.ToInt32 (), 126 | #endif 127 | Location = t.LocationInView (this).ToPointF (), 128 | }); 129 | } 130 | } catch (Exception ex) { 131 | Log.Error (ex); 132 | } 133 | } 134 | 135 | public override void TouchesEnded (Foundation.NSSet touches, UIEvent evt) 136 | { 137 | try { 138 | foreach (UITouch t in touches) { 139 | TouchEnded (this, new CanvasTouchEventArgs { 140 | #if NET6_0_OR_GREATER 141 | TouchId = t.Handle.Handle.ToInt32(), 142 | #else 143 | TouchId = t.Handle.ToInt32 (), 144 | #endif 145 | Location = t.LocationInView (this).ToPointF (), 146 | }); 147 | } 148 | } catch (Exception ex) { 149 | Log.Error (ex); 150 | } 151 | } 152 | 153 | public override void TouchesCancelled (Foundation.NSSet touches, UIEvent evt) 154 | { 155 | try { 156 | foreach (UITouch t in touches) { 157 | TouchCancelled (this, new CanvasTouchEventArgs { 158 | #if NET6_0_OR_GREATER 159 | TouchId = t.Handle.Handle.ToInt32(), 160 | #else 161 | TouchId = t.Handle.ToInt32 (), 162 | #endif 163 | Location = t.LocationInView (this).ToPointF (), 164 | }); 165 | } 166 | } catch (Exception ex) { 167 | Log.Error (ex); 168 | } 169 | } 170 | } 171 | } 172 | 173 | -------------------------------------------------------------------------------- /Praeclarum/Graphics/Rectangle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | #if __MACOS__ || __IOS__ || __MACCATALYST__ 4 | using NativeSize = CoreGraphics.CGSize; 5 | using NativePoint = CoreGraphics.CGPoint; 6 | using NativeRect = CoreGraphics.CGRect; 7 | #endif 8 | 9 | namespace Praeclarum.Graphics 10 | { 11 | public struct RectangleF 12 | { 13 | public float X, Y, Width, Height; 14 | 15 | public float Left { get { return X; } } 16 | public float Top { get { return Y; } } 17 | 18 | public float Right { get { return X + Width; } } 19 | public float Bottom { get { return Y + Height; } } 20 | 21 | public PointF BottomLeft { get { return new PointF (Left, Bottom); } } 22 | public PointF BottomRight { get { return new PointF (Right, Bottom); } } 23 | public PointF TopLeft { 24 | get { return new PointF (Left, Top); } 25 | set { X = value.X; Y = value.Y; } 26 | } 27 | public PointF TopRight { get { return new PointF (Right, Top); } } 28 | 29 | public RectangleF (float left, float top, float width, float height) 30 | { 31 | X = left; 32 | Y = top; 33 | Width = width; 34 | Height = height; 35 | } 36 | 37 | public RectangleF (PointF origin, SizeF size) 38 | { 39 | X = origin.X; 40 | Y = origin.Y; 41 | Width = size.Width; 42 | Height = size.Height; 43 | } 44 | 45 | public void Inflate (float width, float height) 46 | { 47 | Inflate (new SizeF (width, height)); 48 | } 49 | 50 | public void Inflate (SizeF size) 51 | { 52 | X -= size.Width; 53 | Y -= size.Height; 54 | Width += size.Width * 2; 55 | Height += size.Height * 2; 56 | } 57 | 58 | public bool IntersectsWith(RectangleF rect) 59 | { 60 | return !((Left >= rect.Right) || (Right <= rect.Left) || 61 | (Top >= rect.Bottom) || (Bottom <= rect.Top)); 62 | } 63 | 64 | public bool Contains(PointF loc) 65 | { 66 | return (X <= loc.X && loc.X < (X + Width) && Y <= loc.Y && loc.Y < (Y + Height)); 67 | } 68 | 69 | public static RectangleF Union (RectangleF a, RectangleF b) 70 | { 71 | var left = Math.Min (a.Left, b.Left); 72 | var top = Math.Min (a.Top, b.Top); 73 | return new RectangleF (left, 74 | top, 75 | Math.Max (a.Right, b.Right) - left, 76 | Math.Max (a.Bottom, b.Bottom) - top); 77 | } 78 | 79 | public override string ToString() 80 | { 81 | return string.Format("[RectangleF: Left={0} Top={1} Width={2} Height={3}]", Left, Top, Width, Height); 82 | } 83 | } 84 | 85 | public struct Rectangle 86 | { 87 | public int X, Y, Width, Height; 88 | 89 | public int Left { get { return X; } } 90 | 91 | public int Top { get { return Y; } } 92 | 93 | public int Bottom { get { return Top + Height; } } 94 | 95 | public int Right { get { return Left + Width; } } 96 | 97 | public Rectangle (int left, int top, int width, int height) 98 | { 99 | X = left; 100 | Y = top; 101 | Width = width; 102 | Height = height; 103 | } 104 | 105 | public void Offset (int dx, int dy) 106 | { 107 | X += dx; 108 | Y += dy; 109 | } 110 | 111 | public bool Contains (int x, int y) 112 | { 113 | return (x >= X && x < X + Width) && (y >= Y && y < Y + Height); 114 | } 115 | 116 | public static Rectangle Union (Rectangle a, Rectangle b) 117 | { 118 | var left = Math.Min (a.Left, b.Left); 119 | var top = Math.Min (a.Top, b.Top); 120 | return new Rectangle (left, 121 | top, 122 | Math.Max (a.Right, b.Right) - left, 123 | Math.Max (a.Bottom, b.Bottom) - top); 124 | } 125 | 126 | public bool IntersectsWith (Rectangle rect) 127 | { 128 | return !((Left >= rect.Right) || (Right <= rect.Left) || 129 | (Top >= rect.Bottom) || (Bottom <= rect.Top)); 130 | } 131 | 132 | public void Inflate (int width, int height) 133 | { 134 | Inflate (new Size (width, height)); 135 | } 136 | 137 | public void Inflate (Size size) 138 | { 139 | X -= size.Width; 140 | Y -= size.Height; 141 | Width += size.Width * 2; 142 | Height += size.Height * 2; 143 | } 144 | 145 | public override string ToString () 146 | { 147 | return string.Format ("[Rectangle: Left={0} Top={1} Width={2} Height={3}]", Left, Top, Width, Height); 148 | } 149 | } 150 | 151 | public static class RectangleEx 152 | { 153 | public static RectangleF ToRectangleF (this System.Drawing.Rectangle rect) 154 | { 155 | return new RectangleF (rect.X, rect.Y, rect.Width, rect.Height); 156 | } 157 | } 158 | } 159 | 160 | -------------------------------------------------------------------------------- /Praeclarum/UI/PForm.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | 3 | using System; 4 | using System.Collections.ObjectModel; 5 | using System.Collections.Generic; 6 | using System.Collections.Specialized; 7 | using System.Threading.Tasks; 8 | 9 | namespace Praeclarum.UI 10 | { 11 | public partial class PForm 12 | { 13 | readonly ObservableCollection _sections; 14 | public IList Sections { get { return _sections; } } 15 | 16 | void HandleSectionsChanged (object? sender, NotifyCollectionChangedEventArgs e) 17 | { 18 | if (e.OldItems != null) 19 | foreach (PFormSection o in e.OldItems) 20 | o.Form = null; 21 | if (e.NewItems != null) 22 | foreach (PFormSection o in e.NewItems) 23 | o.Form = this; 24 | } 25 | 26 | public void ShowFormError (string title, Exception ex) 27 | { 28 | try 29 | { 30 | var iex = ex; 31 | while (iex.InnerException != null) 32 | { 33 | iex = iex.InnerException; 34 | } 35 | var m = iex.Message; 36 | ShowAlert (title, m); 37 | } 38 | catch (Exception ex2) 39 | { 40 | Log.Error (ex2); 41 | } 42 | } 43 | 44 | public void ShowError (Exception ex) 45 | { 46 | ShowFormError("Error", ex); 47 | } 48 | } 49 | 50 | public enum PFormItemDisplay 51 | { 52 | Title, 53 | TitleAndSubtitle, 54 | TitleAndValue, 55 | } 56 | 57 | public class PFormSection 58 | { 59 | public PForm? Form { get; internal set; } 60 | 61 | public string Title { get; set; } = ""; 62 | public string Hint { get; set; } = ""; 63 | 64 | readonly ObservableCollection items; 65 | public IList Items { get { return items; } } 66 | 67 | public PFormSection (params object[] items) 68 | { 69 | this.items = new ObservableCollection (); 70 | foreach (var i in items) 71 | this.items.Add (i); 72 | } 73 | 74 | public void SetItems(IEnumerable newItems) 75 | { 76 | items.Clear(); 77 | foreach (var i in newItems) 78 | this.items.Add (i); 79 | } 80 | 81 | public virtual void Dismiss () 82 | { 83 | Form = null; 84 | } 85 | 86 | public virtual void SetNeedsReload () 87 | { 88 | Form?.ReloadSection (this); 89 | } 90 | 91 | public virtual void SetNeedsReloadAll () 92 | { 93 | Form?.ReloadAll (); 94 | } 95 | 96 | public virtual void SetNeedsFormat () 97 | { 98 | if (Form != null) 99 | Form.FormatSection (this); 100 | } 101 | 102 | public virtual PFormItemDisplay GetItemDisplay (object item) 103 | { 104 | return PFormItemDisplay.Title; 105 | } 106 | 107 | public virtual string GetItemDetails (object item) 108 | { 109 | return ""; 110 | } 111 | 112 | public virtual bool GetItemEnabled (object item) 113 | { 114 | return true; 115 | } 116 | 117 | public virtual bool GetItemChecked (object item) 118 | { 119 | return false; 120 | } 121 | 122 | public virtual string GetItemImage (object item) 123 | { 124 | return ""; 125 | } 126 | 127 | public virtual string GetItemTitle (object item) 128 | { 129 | if (item is Command c) 130 | return c.Name; 131 | 132 | return item.ToString () ?? ""; 133 | } 134 | 135 | public virtual bool GetItemNavigates (object item) 136 | { 137 | return false; 138 | } 139 | 140 | [Flags] 141 | public enum EditAction 142 | { 143 | None = 0x00, 144 | Delete = 0x01, 145 | } 146 | 147 | public virtual EditAction GetItemEditActions (object item) 148 | { 149 | return EditAction.None; 150 | } 151 | 152 | public virtual void DeleteItem (object item) 153 | { 154 | Items.Remove (item); 155 | } 156 | 157 | /// 158 | /// Execute when an item is selected. Return true to keep the item selected, false to deselect it. 159 | /// If the item is a Command, it will be executed automatically. 160 | /// 161 | /// The item that was selected 162 | /// Whether to keep the item selected 163 | public virtual bool SelectItem (object item) 164 | { 165 | if (item is not Command c) 166 | { 167 | return true; 168 | } 169 | 170 | c.ExecuteAsync ().ContinueWith (t => { 171 | if (t.IsFaulted) { 172 | Log.Error ("Execute command async failed", t.Exception); 173 | } 174 | }, TaskScheduler.FromCurrentSynchronizationContext ()); 175 | return false; 176 | 177 | } 178 | 179 | public virtual bool GetItemDisplayActivity (object item) 180 | { 181 | return false; 182 | } 183 | } 184 | } 185 | 186 | -------------------------------------------------------------------------------- /Praeclarum/Log.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace Praeclarum 7 | { 8 | public static class Log 9 | { 10 | public static string Domain = "Praeclarum"; 11 | 12 | public class LogExtras 13 | { 14 | public Action> TrackEvent; 15 | public Action> TrackError; 16 | } 17 | 18 | public static LogExtras Logger; 19 | 20 | public static void TaskError (Task task) 21 | { 22 | if (task == null) 23 | return; 24 | if (task.IsFaulted) 25 | Error (task.Exception); 26 | } 27 | 28 | public static void Error (string context, Exception ex) 29 | { 30 | try { 31 | if (ex == null) 32 | return; 33 | var props = new Dictionary (); 34 | if (!string.IsNullOrWhiteSpace (context)) { 35 | props["Context"] = context; 36 | } 37 | Logger?.TrackError (ex, props); 38 | WriteLine("E", ex.ToString()); 39 | } 40 | catch 41 | { 42 | } 43 | } 44 | 45 | public static void QuietError (string context, Exception ex) 46 | { 47 | try { 48 | if (ex == null) 49 | return; 50 | WriteLine("E", String.Format ("{0}: {1}", context, ex)); 51 | } 52 | catch 53 | { 54 | } 55 | } 56 | 57 | public static void Error (string message) 58 | { 59 | try { 60 | Logger?.TrackEvent ("Error", new Dictionary{{ 61 | "Message", message}}); 62 | WriteLine ("E", message); 63 | } 64 | catch { 65 | } 66 | } 67 | 68 | public static void Analytics (string message) 69 | { 70 | try { 71 | Logger?.TrackEvent (message, null); 72 | WriteLine ("A", message); 73 | } 74 | catch { 75 | } 76 | } 77 | 78 | public static void Analytics (string message, params ValueTuple[] values) 79 | { 80 | try { 81 | Logger?.TrackEvent (message, null); 82 | WriteLine ("A", message + " " + string.Join (" ", values.Select (v => v.Item1 + "=" + v.Item2))); 83 | } 84 | catch { 85 | } 86 | } 87 | 88 | public static void Info (string message) 89 | { 90 | try { 91 | WriteLine ("I", message); 92 | } 93 | catch { 94 | } 95 | } 96 | 97 | public static void Error (Exception ex) 98 | { 99 | Error("", ex); 100 | } 101 | 102 | static void WriteLine (string type, string line) 103 | { 104 | #if MONODROID 105 | if (type == "E") { 106 | Android.Util.Log.Error (Domain, line); 107 | } 108 | else { 109 | Android.Util.Log.Info (Domain, line); 110 | } 111 | #elif __MACOS__ || __IOS__ 112 | if (type == "E") { 113 | Console.WriteLine ("ERROR: " + line); 114 | } 115 | else { 116 | Console.WriteLine (line); 117 | } 118 | #else 119 | if (type == "E") { 120 | System.Diagnostics.Debug.WriteLine ("ERROR: " + line); 121 | } 122 | else { 123 | System.Diagnostics.Debug.WriteLine (line); 124 | } 125 | //Console.WriteLine (line); 126 | #endif 127 | } 128 | 129 | public static string GetUserErrorMessage (Exception ex) 130 | { 131 | if (ex == null) 132 | return ""; 133 | 134 | var i = ex; 135 | while (i.InnerException != null) { 136 | i = i.InnerException; 137 | } 138 | return i.Message; 139 | } 140 | 141 | #if __IOS__ 142 | public static void ShowError (this Foundation.NSObject obj, Exception ex, string format, params string[] args) 143 | { 144 | var title = format; 145 | try { 146 | title = string.Format (format, args); 147 | } catch (Exception ex2) { 148 | Log.Error (ex2); 149 | } 150 | ShowError (obj, ex, title); 151 | } 152 | public static void ShowError (this Foundation.NSObject obj, Exception ex, string title = "") 153 | { 154 | if (ex == null) 155 | return; 156 | 157 | Error (ex); 158 | 159 | try { 160 | if (string.IsNullOrEmpty (title)) { 161 | title = "Error"; 162 | } 163 | var message = GetUserErrorMessage (ex); 164 | #if DEBUG 165 | message += "\n\n" + ((ex.GetType () == typeof (Exception)) ? "" : ex.GetType ().Name); 166 | message += " " + ex.StackTrace; 167 | #endif 168 | obj?.BeginInvokeOnMainThread (() => { 169 | try { 170 | #pragma warning disable CA1422 171 | var alert = new UIKit.UIAlertView ( 172 | title, 173 | message, 174 | null!, 175 | "OK"); 176 | alert.Show (); 177 | #pragma warning restore CA1422 178 | } catch (Exception ex3) { 179 | Error (ex3); 180 | } 181 | }); 182 | } catch (Exception ex2) { 183 | Error (ex2); 184 | } 185 | } 186 | #endif 187 | } 188 | } 189 | 190 | -------------------------------------------------------------------------------- /Praeclarum/Graphics/NullGraphics.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2010-2012 Frank A. Krueger 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | using System; 23 | 24 | namespace Praeclarum.Graphics 25 | { 26 | public class NullGraphics : IGraphics 27 | { 28 | const int MaxFontSize = 120; 29 | NullGraphicsFontMetrics[] _fontMetrics; 30 | 31 | int _fontSize = 10; 32 | 33 | public NullGraphics () 34 | { 35 | } 36 | 37 | public void SetFont (Font f) 38 | { 39 | _fontSize = f.Size; 40 | } 41 | 42 | public void SetColor (Color c) 43 | { 44 | } 45 | 46 | public void SetGradient (Gradient g) 47 | { 48 | } 49 | 50 | public void Clear (Color c) 51 | { 52 | } 53 | 54 | public void FillPolygon (Polygon poly) 55 | { 56 | } 57 | 58 | public void DrawPolygon (Polygon poly, float w) 59 | { 60 | } 61 | 62 | public void FillRoundedRect (float x, float y, float width, float height, float radius) 63 | { 64 | } 65 | 66 | public void DrawRoundedRect (float x, float y, float width, float height, float radius, float w) 67 | { 68 | } 69 | 70 | public void FillRect (float x, float y, float width, float height) 71 | { 72 | } 73 | 74 | public void FillOval (float x, float y, float width, float height) 75 | { 76 | } 77 | 78 | public void DrawOval (float x, float y, float width, float height, float w) 79 | { 80 | } 81 | 82 | public void DrawRect (float x, float y, float width, float height, float w) 83 | { 84 | } 85 | 86 | public void BeginLines (bool rounded) 87 | { 88 | } 89 | 90 | public void DrawLine (float sx, float sy, float ex, float ey, float w) 91 | { 92 | } 93 | 94 | public void EndLines () 95 | { 96 | } 97 | 98 | public void FillArc (float cx, float cy, float radius, float startAngle, float endAngle) 99 | { 100 | } 101 | 102 | public void DrawArc (float cx, float cy, float radius, float startAngle, float endAngle, float w) 103 | { 104 | } 105 | 106 | public void DrawString (string s, float x, float y) 107 | { 108 | } 109 | 110 | public void DrawString (string s, float x, float y, float width, float height, LineBreakMode lineBreak, TextAlignment align) 111 | { 112 | } 113 | 114 | public IFontMetrics GetFontMetrics () 115 | { 116 | if (_fontMetrics == null) { 117 | _fontMetrics = new NullGraphicsFontMetrics[MaxFontSize + 1]; 118 | } 119 | var i = Math.Min (_fontMetrics.Length, _fontSize); 120 | if (_fontMetrics[i] == null) { 121 | _fontMetrics[i] = new NullGraphicsFontMetrics (i); 122 | } 123 | return _fontMetrics[i]; 124 | } 125 | 126 | public void DrawImage (IImage img, float x, float y, float width, float height) 127 | { 128 | } 129 | 130 | public void SaveState () 131 | { 132 | } 133 | 134 | public void SetClippingRect (float x, float y, float width, float height) 135 | { 136 | } 137 | 138 | public void Translate (float dx, float dy) 139 | { 140 | } 141 | 142 | public void Scale (float sx, float sy) 143 | { 144 | } 145 | 146 | public void RestoreState () 147 | { 148 | } 149 | 150 | public IImage ImageFromFile (string filename) 151 | { 152 | return null; 153 | } 154 | 155 | public void BeginEntity (object entity) 156 | { 157 | } 158 | } 159 | 160 | public class NullGraphicsFontMetrics : IFontMetrics 161 | { 162 | int _height; 163 | int _charWidth; 164 | 165 | public NullGraphicsFontMetrics (int size) 166 | { 167 | _height = size; 168 | _charWidth = (855 * size) / 1600; 169 | } 170 | 171 | public int StringWidth (string str, int startIndex, int length) 172 | { 173 | return length * _charWidth; 174 | } 175 | 176 | public int Height 177 | { 178 | get { 179 | return _height; 180 | } 181 | } 182 | 183 | public int Ascent 184 | { 185 | get { 186 | return Height; 187 | } 188 | } 189 | 190 | public int Descent 191 | { 192 | get { 193 | return 0; 194 | } 195 | } 196 | } 197 | } 198 | 199 | -------------------------------------------------------------------------------- /Praeclarum.Mac/UI/DocumentAppDelegate.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | 3 | using System; 4 | using System.Threading.Tasks; 5 | using AppKit; 6 | using Foundation; 7 | using Praeclarum.IO; 8 | using System.IO; 9 | using System.Globalization; 10 | using System.Linq; 11 | using Praeclarum.App; 12 | using System.Collections.Generic; 13 | using System.Diagnostics; 14 | using System.Threading; 15 | 16 | namespace Praeclarum.UI 17 | { 18 | [Register("DocumentAppDelegate")] 19 | public abstract class DocumentAppDelegate : NSApplicationDelegate 20 | { 21 | private DocumentApplication? _app; 22 | 23 | public DocumentApplication App 24 | { 25 | get 26 | { 27 | if (_app is null) 28 | { 29 | _app = CreateApplication(); 30 | } 31 | return _app; 32 | } 33 | } 34 | 35 | public static DocumentAppDelegate? Shared { get; private set; } 36 | public static string AppName => Shared?.App.Name ?? "App"; 37 | 38 | public IDocumentAppSettings? Settings 39 | { 40 | get; 41 | private set; 42 | } 43 | 44 | NSWindow? _proWindow; 45 | 46 | public override void WillFinishLaunching(NSNotification notification) 47 | { 48 | Shared = this; 49 | Settings = CreateSettings(); 50 | } 51 | 52 | public override void DidFinishLaunching(NSNotification notification) 53 | { 54 | // 55 | // In-app Purchases 56 | // 57 | try 58 | { 59 | if (App is {} app && (app.IsPatronSupported || app.HasTips || app.HasPro)) 60 | { 61 | if (Settings is {} settings && settings.IsPatron) 62 | { 63 | settings.IsPatron = DateTime.UtcNow <= Settings.PatronEndDate; 64 | } 65 | StoreManager.Shared.RestoredActions.Add (HandlePurchaseRestoredAsync); 66 | StoreManager.Shared.PurchasingActions.Add (HandlePurchasingAsync); 67 | StoreManager.Shared.CompletionActions.Add (HandlePurchaseCompletionAsync); 68 | StoreManager.Shared.FailActions.Add (HandlePurchaseFailAsync); 69 | } 70 | } 71 | catch (Exception ex) 72 | { 73 | Log.Error (ex); 74 | } 75 | 76 | // 77 | // Start in app purchase data 78 | // 79 | try 80 | { 81 | StoreKit.SKPaymentQueue.DefaultQueue.AddTransactionObserver (StoreManager.Shared); 82 | } 83 | catch (Exception ex) 84 | { 85 | Log.Error (ex); 86 | } 87 | } 88 | 89 | protected virtual IDocumentAppSettings CreateSettings() 90 | { 91 | return new DocumentAppSettings(NSUserDefaults.StandardUserDefaults); 92 | } 93 | 94 | protected abstract DocumentApplication CreateApplication(); 95 | 96 | static async Task HandlePurchaseRestoredAsync (NSError? error) 97 | { 98 | // await TipJarForm.HandlePurchaseRestoredAsync(error); 99 | await ProService.Shared.HandlePurchaseRestoredAsync (error); 100 | await ProForm.HandlePurchaseRestoredAsync (error); 101 | // await PatronForm.HandlePurchaseRestoredAsync (error); 102 | } 103 | 104 | static async Task HandlePurchasingAsync (StoreKit.SKPaymentTransaction t) 105 | { 106 | var pid = t.Payment.ProductIdentifier; 107 | if (pid.Contains (".tip.")) 108 | { 109 | } 110 | else if (pid.Contains (".pro.")) 111 | { 112 | await ProForm.HandlePurchasingAsync (t); 113 | } 114 | else 115 | { 116 | } 117 | } 118 | 119 | static async Task HandlePurchaseCompletionAsync (StoreKit.SKPaymentTransaction t) 120 | { 121 | var pid = t.Payment.ProductIdentifier; 122 | if (pid.Contains (".tip.")) 123 | { 124 | //await TipJarForm.HandlePurchaseCompletionAsync (t); 125 | } 126 | else if (pid.Contains (".pro.")) 127 | { 128 | await ProService.Shared.HandlePurchaseCompletionAsync (t); 129 | await ProForm.HandlePurchaseCompletionAsync (t); 130 | } 131 | else 132 | { 133 | //await PatronForm.HandlePurchaseCompletionAsync (t); 134 | } 135 | } 136 | 137 | static async Task HandlePurchaseFailAsync (StoreKit.SKPaymentTransaction t) 138 | { 139 | if (t.Payment.ProductIdentifier.Contains (".tip.")) 140 | { 141 | //return TipJarForm.HandlePurchaseFailAsync (t); 142 | } 143 | if (t.Payment.ProductIdentifier.Contains(".pro.")) 144 | { 145 | await ProService.Shared.HandlePurchaseFailAsync(t); 146 | await ProForm.HandlePurchaseFailAsync(t); 147 | } 148 | else 149 | { 150 | //return PatronForm.HandlePurchaseFailAsync(t); 151 | } 152 | } 153 | 154 | [Export("showProPanel:")] 155 | public void ShowProPanel(NSObject? sender) 156 | { 157 | PForm.ShowWindow(sender, ref _proWindow, () => new ProForm()); 158 | } 159 | 160 | public void PromotePro (string error, NSObject? sender) 161 | { 162 | BeginInvokeOnMainThread(() => 163 | { 164 | if (App is not {} app ) 165 | return; 166 | var message = $"{App.ProSymbol} This feature is only available in {App.Name} Pro.\n\nYou can unlock this feature and others by clicking \"Learn More\" below."; 167 | 168 | var alert = NSAlert.WithMessage (error, "Learn More About Pro", "Cancel", otherButton: null, full: message); 169 | var result = alert.RunSheetModal (null, NSApplication.SharedApplication); 170 | if (result.ToInt64 () == 1) 171 | { 172 | ShowProPanel (sender); 173 | } 174 | }); 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /Praeclarum.iOS/UI/ImageCache.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using Foundation; 8 | using UIKit; 9 | using Praeclarum.App; 10 | using Praeclarum.IO; 11 | using CoreGraphics; 12 | 13 | namespace Praeclarum.UI 14 | { 15 | public class ImageCache 16 | { 17 | readonly string cacheDirectory; 18 | 19 | public ImageCache (string cacheDirectory) 20 | { 21 | Debug.WriteLine ("Image Cache: {0} {1}", DateTime.Now, cacheDirectory); 22 | this.cacheDirectory = cacheDirectory; 23 | } 24 | 25 | string GetCachePath (string key) 26 | { 27 | return Path.Combine (cacheDirectory, key) + "@2x.png"; 28 | } 29 | 30 | class MemoryImage 31 | { 32 | public UIImage Image; 33 | public DateTime GeneratedTime; 34 | public string Key; 35 | public DateTime LastAccessTime; 36 | } 37 | 38 | const int MaxMemorySize = 50; 39 | 40 | readonly Dictionary memory = new Dictionary (); 41 | 42 | public UIImage GetMemoryImage (string key) 43 | { 44 | lock (memory) { 45 | MemoryImage memImage; 46 | if (memory.TryGetValue (key, out memImage)) { 47 | return memImage.Image; 48 | } 49 | } 50 | return null; 51 | } 52 | 53 | public async Task GetImageAsync (string key, DateTime oldestTime, bool getFromDisk = true) 54 | { 55 | // Console.WriteLine ("GetImage: {0} {1}", key, oldestTime); 56 | 57 | var scale = UIScreen.MainScreen.Scale; 58 | 59 | key = key ?? ""; 60 | 61 | // 62 | // Is it in memory? 63 | // 64 | lock (memory) { 65 | MemoryImage memImage; 66 | if (memory.TryGetValue (key, out memImage)) { 67 | if (memImage.GeneratedTime > oldestTime) { 68 | memImage.LastAccessTime = DateTime.UtcNow; 69 | return memImage.Image; 70 | } else { 71 | memory.Remove (key); 72 | } 73 | } 74 | } 75 | 76 | // 77 | // Is it on disk? If so, try to load it and add it to memory. 78 | // 79 | if (getFromDisk) { 80 | return await Task.Run (() => { 81 | var cachePath = GetCachePath (key); 82 | var info = new FileInfo (cachePath); 83 | if (info.Exists && info.LastWriteTimeUtc > oldestTime) { 84 | 85 | var uiImage = UIImage.LoadFromData (NSData.FromFile (cachePath), scale); 86 | 87 | if (uiImage != null) { 88 | SetMemoryImage (key, uiImage, info.LastWriteTimeUtc); 89 | return uiImage; 90 | } 91 | } 92 | 93 | // 94 | // Nowhere to be found 95 | // 96 | return null; 97 | }); 98 | } 99 | 100 | // 101 | // Nowhere to be found 102 | // 103 | return null; 104 | } 105 | 106 | public void RemoveImage (string key, bool removeFromDisk = false) 107 | { 108 | key = key ?? ""; 109 | 110 | lock (memory) { 111 | if (memory.ContainsKey (key)) 112 | memory.Remove (key); 113 | } 114 | 115 | if (removeFromDisk) { 116 | try { 117 | var cachePath = GetCachePath (key); 118 | if (File.Exists (cachePath)) { 119 | File.Delete (cachePath); 120 | } 121 | } catch (IOException ex) { 122 | // Swallow IO exceptions 123 | Debug.WriteLine (ex); 124 | } 125 | } 126 | } 127 | 128 | MemoryImage SetMemoryImage (string key, UIImage uiImage, DateTime genTime) 129 | { 130 | lock (memory) { 131 | MemoryImage memImage; 132 | if (memory.TryGetValue (key, out memImage)) { 133 | memImage.LastAccessTime = DateTime.UtcNow; 134 | memImage.Image = uiImage; 135 | return memImage; 136 | } 137 | 138 | // 139 | // If not in memory, then let's make room to add it 140 | // 141 | var numToRemove = (memory.Count + 1) - MaxMemorySize; 142 | if (numToRemove > 0) { 143 | var toRemove = memory.Values.OrderBy (x => x.LastAccessTime).Take (numToRemove).ToList (); 144 | foreach (var r in toRemove) { 145 | memory.Remove (r.Key); 146 | } 147 | } 148 | 149 | // 150 | // Now add it 151 | // 152 | memImage = new MemoryImage { 153 | Key = key, 154 | Image = uiImage, 155 | LastAccessTime = DateTime.UtcNow, 156 | GeneratedTime = genTime, 157 | }; 158 | memory.Add (key, memImage); 159 | return memImage; 160 | } 161 | } 162 | 163 | public async Task SetGeneratedImageAsync (string key, UIImage uiImage, bool saveToDisk = true) 164 | { 165 | if (uiImage == null) { 166 | RemoveImage (key); 167 | return; 168 | } 169 | 170 | // 171 | // Does it already exist in memory? 172 | // 173 | SetMemoryImage (key, uiImage, DateTime.UtcNow); 174 | 175 | // 176 | // If we added it to memory, then save it to disk 177 | // 178 | if (saveToDisk) { 179 | await Task.Run (() => { 180 | try { 181 | FileSystemManager.EnsureDirectoryExists (cacheDirectory); 182 | var cachePath = GetCachePath (key); 183 | uiImage.AsPNG ().Save (cachePath, false, out var err); 184 | if (err != null) { 185 | Log.Error ("Failed to save thumbnail: " + err); 186 | } 187 | } 188 | catch (Exception ex) { 189 | Log.Error (ex); 190 | } 191 | }); 192 | } 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /Praeclarum/App/Document.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Text; 5 | using System.IO; 6 | 7 | namespace Praeclarum.App 8 | { 9 | public abstract class Document : INotifyPropertyChanged 10 | { 11 | public event EventHandler Committed; 12 | 13 | public string Title { get; private set; } 14 | 15 | public Document () 16 | { 17 | Title = "Untitled"; 18 | } 19 | 20 | #region File Operations 21 | 22 | public void Open (string path, TextReader reader) 23 | { 24 | var state = reader.ReadToEnd (); 25 | RestoreState (state); 26 | InitializeUndo (); 27 | } 28 | 29 | public void Save (TextWriter writer) 30 | { 31 | var state = GetState (); 32 | writer.Write (state); 33 | HasUnsavedChanges = false; 34 | } 35 | 36 | #if !PORTABLE 37 | 38 | public string Path { get; set; } 39 | 40 | public void Open (string path) 41 | { 42 | Path = path; 43 | Title = System.IO.Path.GetFileNameWithoutExtension (Path); 44 | var state = File.ReadAllText (path); 45 | RestoreState (state); 46 | InitializeUndo (); 47 | } 48 | 49 | public void Save () 50 | { 51 | Save (Path); 52 | } 53 | 54 | public void Save (string path) 55 | { 56 | Path = path; 57 | Title = System.IO.Path.GetFileNameWithoutExtension (Path); 58 | var state = GetState (); 59 | File.WriteAllText (path, state, Encoding.UTF8); 60 | HasUnsavedChanges = false; 61 | } 62 | #endif 63 | 64 | #endregion 65 | 66 | #region Undo 67 | 68 | bool hasUnsavedChanges = false; 69 | 70 | public bool HasUnsavedChanges 71 | { 72 | get { return hasUnsavedChanges; } 73 | set 74 | { 75 | if (hasUnsavedChanges == value) 76 | return; 77 | hasUnsavedChanges = value; 78 | OnPropertyChanged ("HasUnsavedChanges"); 79 | } 80 | } 81 | 82 | public void InitializeUndo () 83 | { 84 | commitStates.Clear (); 85 | activeCommitState = -1; 86 | Commit ("Initial"); 87 | HasUnsavedChanges = false; 88 | } 89 | 90 | class CommitState 91 | { 92 | public string Message; 93 | public string State; 94 | } 95 | 96 | readonly List commitStates = new List (); 97 | int activeCommitState = -1; 98 | 99 | protected abstract string GetState (); 100 | 101 | protected abstract void RestoreState (string state); 102 | 103 | public void Commit (string message) 104 | { 105 | // 106 | // Serialize the document 107 | // 108 | var state = GetState (); 109 | 110 | // 111 | // Don't commit non-changes 112 | // 113 | if (commitStates.Count != 0 && 114 | state == commitStates[activeCommitState].State) return; 115 | 116 | // 117 | // Remove undone states 118 | // 119 | if (commitStates.Count - 1 > activeCommitState) { 120 | commitStates.RemoveRange ( 121 | activeCommitState + 1, 122 | commitStates.Count - activeCommitState - 1); 123 | } 124 | 125 | // 126 | // Set this as the active state 127 | // 128 | commitStates.Add (new CommitState { 129 | State = state, 130 | Message = message, 131 | }); 132 | activeCommitState = commitStates.Count - 1; 133 | 134 | // 135 | // Notify 136 | // 137 | HasUnsavedChanges = true; 138 | 139 | var ev = Committed; 140 | if (ev != null) { 141 | var prevState = activeCommitState > 0 ? 142 | commitStates[activeCommitState-1].State : 143 | ""; 144 | ev (this, new CommittedEventArgs (message, prevState, state)); 145 | } 146 | 147 | OnPropertyChanged ("CanUndo"); 148 | OnPropertyChanged ("CanRedo"); 149 | } 150 | 151 | public bool CanUndo { get { return activeCommitState > 0; } } 152 | 153 | public string UndoMessage { get { return CanUndo ? commitStates[activeCommitState].Message : ""; } } 154 | 155 | public void Undo () 156 | { 157 | if (CanUndo) { 158 | activeCommitState--; 159 | 160 | RestoreState (commitStates[activeCommitState].State); 161 | 162 | HasUnsavedChanges = true; 163 | 164 | OnPropertyChanged ("CanUndo"); 165 | OnPropertyChanged ("CanRedo"); 166 | } 167 | } 168 | 169 | public bool CanRedo { get { return activeCommitState + 1 < commitStates.Count; } } 170 | 171 | public string RedoMessage { get { return CanRedo ? commitStates[activeCommitState + 1].Message : ""; } } 172 | 173 | public void Redo () 174 | { 175 | if (CanRedo) { 176 | activeCommitState++; 177 | 178 | RestoreState (commitStates[activeCommitState].State); 179 | 180 | HasUnsavedChanges = true; 181 | 182 | OnPropertyChanged ("CanUndo"); 183 | OnPropertyChanged ("CanRedo"); 184 | } 185 | } 186 | 187 | #endregion 188 | 189 | public event PropertyChangedEventHandler PropertyChanged; 190 | 191 | void OnPropertyChanged (string name) 192 | { 193 | var ev = PropertyChanged; 194 | if (ev != null) { 195 | ev (this, new PropertyChangedEventArgs (name)); 196 | } 197 | } 198 | } 199 | 200 | public class CommittedEventArgs : EventArgs 201 | { 202 | public string Message { get; private set; } 203 | public string PreviousState { get; private set; } 204 | public string State { get; private set; } 205 | public CommittedEventArgs (string message, string previousState, string state) 206 | { 207 | Message = message; 208 | PreviousState = previousState; 209 | State = state; 210 | } 211 | } 212 | } 213 | 214 | -------------------------------------------------------------------------------- /Praeclarum/App/StoreManager.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | 3 | using System; 4 | using StoreKit; 5 | using Foundation; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | using CloudKit; 10 | 11 | namespace Praeclarum.App 12 | { 13 | public class StoreManager : SKPaymentTransactionObserver 14 | { 15 | public static readonly StoreManager Shared = new StoreManager (); 16 | 17 | readonly List productsPurchased = new List (); 18 | readonly List productsRestored = new List (); 19 | 20 | public readonly List> RestoredActions = new List> (); 21 | public readonly List> PurchasingActions = new List> (); 22 | public readonly List> CompletionActions = new List> (); 23 | public readonly List> FailActions = new List> (); 24 | 25 | public Task FetchProductInformationAsync (string[] ids) 26 | { 27 | var request = new SKProductsRequest ( 28 | NSSet.MakeNSObjectSet (ids.Select (x => new NSString(x)).ToArray ())); 29 | var del = new TaskRequestDelegate (); 30 | request.Delegate = del; 31 | request.Start (); 32 | return del.Task; 33 | } 34 | 35 | public void Buy (SKProduct product) 36 | { 37 | Console.WriteLine ("STORE Buy({0})", product.ProductIdentifier); 38 | var payment = SKMutablePayment.PaymentWithProduct (product); 39 | SKPaymentQueue.DefaultQueue.AddPayment (payment); 40 | } 41 | 42 | public bool IsRestoring { get; private set; } 43 | 44 | public void Restore () 45 | { 46 | if (IsRestoring) return; 47 | IsRestoring = true; 48 | Console.WriteLine ("STORE Restore()"); 49 | productsRestored.Clear (); 50 | SKPaymentQueue.DefaultQueue.RestoreCompletedTransactions (); 51 | } 52 | 53 | public override async void RestoreCompletedTransactionsFinished (SKPaymentQueue queue) 54 | { 55 | IsRestoring = false; 56 | Console.WriteLine ("STORE RestoreCompleted()"); 57 | await RestoredAsync(error: null); 58 | } 59 | 60 | public override async void RestoreCompletedTransactionsFailedWithError (SKPaymentQueue queue, NSError error) 61 | { 62 | IsRestoring = false; 63 | Console.WriteLine ("STORE ERROR RestoreError ({0})", error); 64 | await RestoredAsync(error: error); 65 | } 66 | 67 | public override async void UpdatedTransactions (SKPaymentQueue queue, SKPaymentTransaction[] transactions) 68 | { 69 | if (transactions == null) 70 | return; 71 | try { 72 | foreach (var t in transactions) { 73 | try { 74 | Console.WriteLine ("STORE Transaction: {0} {1} {2} {3} {4}", t.Payment.ProductIdentifier, t.TransactionState, t.TransactionIdentifier, t.TransactionDate, t.Error); 75 | switch (t.TransactionState) { 76 | case SKPaymentTransactionState.Purchasing: 77 | foreach (var a in PurchasingActions) 78 | { 79 | await a (t); 80 | } 81 | break; 82 | case SKPaymentTransactionState.Purchased: 83 | productsPurchased.Add (t); 84 | await CompleteTransactionAsync (t); 85 | break; 86 | case SKPaymentTransactionState.Restored: 87 | productsRestored.Add (t); 88 | await CompleteTransactionAsync (t); 89 | break; 90 | case SKPaymentTransactionState.Failed: 91 | await CompleteTransactionAsync (t); 92 | break; 93 | } 94 | } catch (Exception ex) { 95 | Log.Error (ex); 96 | } 97 | } 98 | } catch (Exception ex) { 99 | Log.Error (ex); 100 | } 101 | } 102 | 103 | async Task CompleteTransactionAsync (SKPaymentTransaction t) 104 | { 105 | if (t == null) 106 | return; 107 | 108 | if (t.TransactionState == SKPaymentTransactionState.Failed) { 109 | Console.WriteLine ("STORE ERROR CompleteTransaction: {0} {1} {2}", t.TransactionState, t.TransactionIdentifier, t.TransactionDate); 110 | foreach (var a in FailActions) { 111 | await a (t); 112 | } 113 | } else { 114 | Console.WriteLine ("STORE CompleteTransaction: {0} {1} {2} {3}", t.Payment.ProductIdentifier, t.TransactionState, t.TransactionIdentifier, t.TransactionDate); 115 | foreach (var a in CompletionActions) { 116 | await a (t); 117 | } 118 | } 119 | Console.WriteLine ("STORE FinishTransaction()"); 120 | SKPaymentQueue.DefaultQueue.FinishTransaction (t); 121 | } 122 | 123 | async Task RestoredAsync (NSError? error) 124 | { 125 | foreach (var a in RestoredActions) 126 | { 127 | try 128 | { 129 | await a(error); 130 | } 131 | catch (Exception ex) 132 | { 133 | Log.Error (ex); 134 | } 135 | } 136 | } 137 | 138 | class TaskRequestDelegate : SKProductsRequestDelegate 139 | { 140 | readonly TaskCompletionSource tcs = new TaskCompletionSource (); 141 | 142 | public Task Task { get { return tcs.Task; } } 143 | 144 | public override void ReceivedResponse (SKProductsRequest request, SKProductsResponse response) 145 | { 146 | tcs.SetResult (response); 147 | } 148 | public override void RequestFailed (SKRequest request, NSError error) 149 | { 150 | tcs.SetException (new Exception (error != null ? error.ToString () : "")); 151 | } 152 | } 153 | } 154 | } 155 | 156 | -------------------------------------------------------------------------------- /Praeclarum.iOS/UI/DocumentListAppDelegate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UIKit; 3 | using Foundation; 4 | using System.Linq; 5 | using System.Collections.Generic; 6 | using System.Diagnostics; 7 | 8 | namespace Praeclarum.UI 9 | { 10 | [Register ("DocumentListAppDelegate")] 11 | public class DocumentListAppDelegate : DocumentAppDelegate 12 | { 13 | UISplitViewController split; 14 | 15 | protected override void SetRootViewController () 16 | { 17 | if (IsPhone) { 18 | 19 | window.RootViewController = docListNav; 20 | 21 | } else { 22 | 23 | var blankVC = new BlankVC (); 24 | blankVC.View.BackgroundColor = UIColor.White; 25 | 26 | detailNav = new UINavigationController (blankVC); 27 | detailNav.NavigationBar.BarStyle = Theme.NavigationBarStyle; 28 | detailNav.ToolbarHidden = false; 29 | Theme.Apply (detailNav.Toolbar); 30 | 31 | split = new UISplitViewController { 32 | PresentsWithGesture = false, 33 | ViewControllers = new UIViewController[] { 34 | docListNav, 35 | detailNav, 36 | }, 37 | Delegate = new SplitDelegate (), 38 | }; 39 | 40 | window.RootViewController = split; 41 | } 42 | } 43 | 44 | protected override void ShowEditor (int docIndex, bool advance, bool animated, UIViewController newEditorVC) 45 | { 46 | // 47 | // Control Animation 48 | // 49 | var transition = advance ? 50 | UIViewAnimationTransition.CurlUp : 51 | UIViewAnimationTransition.CurlDown; 52 | var useTransition = true; 53 | 54 | 55 | // 56 | // Change the UI 57 | // 58 | UINavigationController nc; 59 | UIViewController[] vcs; 60 | if (IsPhone) { 61 | // Debug.WriteLine ("SHOWING EDITOR"); 62 | nc = docListNav; 63 | vcs = nc.ViewControllers; 64 | var oldEditor = CurrentDocumentEditor; 65 | useTransition = useTransition && (oldEditor != null); 66 | var nvcs = new List (vcs.OfType ()); 67 | nvcs.Add (newEditorVC); 68 | vcs = nvcs.ToArray (); 69 | } 70 | else { 71 | // 72 | // Close the master list 73 | // 74 | var p = ((SplitDelegate)split.Delegate).Popover; 75 | if (p != null) { 76 | p.Dismiss (animated); 77 | } 78 | // 79 | // Set the button 80 | // 81 | var oldC = detailNav.TopViewController; 82 | var left = oldC.NavigationItem.LeftBarButtonItem; 83 | oldC.NavigationItem.LeftBarButtonItem = null; 84 | newEditorVC.NavigationItem.LeftBarButtonItem = left; 85 | nc = detailNav; 86 | vcs = new UIViewController[] { 87 | newEditorVC, 88 | }; 89 | } 90 | 91 | // 92 | // Set View Controllers 93 | // Throttle the animations 94 | // 95 | var now = DateTime.UtcNow; 96 | if (animated && (now - lastOpenTime).TotalSeconds > 1) { 97 | if (useTransition) { 98 | UIView.Animate (0.5, () => { 99 | try { 100 | UIView.SetAnimationTransition (transition, nc.View, true); 101 | nc.SetViewControllers (vcs, false); 102 | } catch (Exception ex) { 103 | Log.Error (ex); 104 | } 105 | }); 106 | } 107 | else { 108 | nc.SetViewControllers (vcs, true); 109 | } 110 | } 111 | else { 112 | nc.SetViewControllers (vcs, false); 113 | } 114 | } 115 | 116 | class BlankVC : UIViewController 117 | { 118 | bool showed = false; 119 | public BlankVC () 120 | { 121 | Title = DocumentListAppDelegate.Shared.App.Name; 122 | } 123 | public override void ViewDidAppear (bool animated) 124 | { 125 | base.ViewDidAppear (animated); 126 | 127 | try { 128 | if (!showed) { 129 | showed = true; 130 | if (InterfaceOrientation == UIInterfaceOrientation.Portrait) { 131 | var left = NavigationItem.LeftBarButtonItem; 132 | if (left != null) { 133 | left.Target.PerformSelector (left.Action, NavigationItem, 0.1); 134 | } 135 | } 136 | } 137 | 138 | } catch (Exception ex) { 139 | Debug.WriteLine (ex); 140 | 141 | } 142 | } 143 | } 144 | 145 | class SplitDelegate : UISplitViewControllerDelegate 146 | { 147 | public UIPopoverController Popover { get; private set; } 148 | public UIBarButtonItem Button { get; private set; } 149 | 150 | public override bool ShouldHideViewController (UISplitViewController svc, UIViewController viewController, UIInterfaceOrientation inOrientation) 151 | { 152 | return true; 153 | } 154 | 155 | public override void WillHideViewController (UISplitViewController svc, UIViewController aViewController, UIBarButtonItem barButtonItem, UIPopoverController pc) 156 | { 157 | try { 158 | Popover = pc; 159 | var detailNav = (UINavigationController)svc.ViewControllers [1]; 160 | 161 | var newItem = new UIBarButtonItem (UIImage.FromBundle ("Hamburger.png"), UIBarButtonItemStyle.Plain, barButtonItem.Target, barButtonItem.Action); 162 | 163 | detailNav.TopViewController.NavigationItem.LeftBarButtonItem = newItem; 164 | 165 | Button = newItem; 166 | 167 | } catch (Exception ex) { 168 | Log.Error (ex); 169 | } 170 | } 171 | 172 | public override void WillShowViewController (UISplitViewController svc, UIViewController aViewController, UIBarButtonItem button) 173 | { 174 | try { 175 | Popover = null; 176 | var detailNav = (UINavigationController)svc.ViewControllers [1]; 177 | detailNav.TopViewController.NavigationItem.LeftBarButtonItem = null; 178 | } catch (Exception ex) { 179 | Log.Error (ex); 180 | } 181 | } 182 | } 183 | } 184 | } 185 | 186 | -------------------------------------------------------------------------------- /Praeclarum/App/DocumentAppSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | using Foundation; 5 | using Praeclarum.IO; 6 | using Praeclarum.UI; 7 | 8 | namespace Praeclarum.App 9 | { 10 | public class DocumentAppSettings : IDocumentAppSettings 11 | { 12 | #if __IOS__ || __MACOS__ 13 | protected readonly NSUserDefaults defs; 14 | 15 | public DocumentAppSettings (NSUserDefaults defaults) 16 | { 17 | if (defaults == null) 18 | throw new ArgumentNullException ("defaults"); 19 | defs = defaults; 20 | } 21 | #else 22 | protected readonly Dictionary defs; 23 | 24 | public DocumentAppSettings (Dictionary defaults) 25 | { 26 | if (defaults == null) 27 | throw new ArgumentNullException ("defaults"); 28 | defs = defaults; 29 | } 30 | #endif 31 | 32 | public DocumentsSort DocumentsSort { 33 | get { 34 | var str = defs.StringForKey ("DocumentsSort"); 35 | return str == "Name" ? DocumentsSort.Name : DocumentsSort.Date; 36 | } 37 | set { 38 | var str = value == DocumentsSort.Name ? "Name" : "Date"; 39 | defs.SetString (str, "DocumentsSort"); 40 | defs.Synchronize (); 41 | } 42 | } 43 | 44 | public string DocumentationVersion { 45 | get { 46 | return defs.StringForKey ("DocumentationVersion"); 47 | } 48 | set { 49 | defs.SetString (value, "DocumentationVersion"); 50 | defs.Synchronize (); 51 | } 52 | } 53 | 54 | public string GetWorkingDirectory (IFileSystem fileSystem) 55 | { 56 | var path = defs.StringForKey (fileSystem.Id + " CWD") ?? ""; 57 | if (path.StartsWith ("file:", StringComparison.Ordinal)) 58 | return ""; 59 | return path; 60 | } 61 | 62 | public void SetWorkingDirectory (IFileSystem fileSystem, string directory) 63 | { 64 | defs.SetString (directory, fileSystem.Id + " CWD"); 65 | defs.Synchronize (); 66 | } 67 | 68 | static readonly DateTime Epoch = new DateTime (1970, 1, 1); 69 | 70 | protected DateTime GetDateTime (string key) 71 | { 72 | var s = defs.IntForKey (key); 73 | return Epoch + TimeSpan.FromSeconds (s); 74 | } 75 | 76 | protected void SetDateTime (string key, DateTime value) 77 | { 78 | var s = (int)(value - Epoch).TotalSeconds; 79 | defs.SetInt (s, key); 80 | } 81 | 82 | protected double GetDouble (string key, double defaultValue) 83 | { 84 | var s = defs [key] as NSNumber; 85 | if (s != null) { 86 | return s.DoubleValue; 87 | } 88 | return defaultValue; 89 | } 90 | 91 | protected void SetDouble (string key, double value) 92 | { 93 | defs.SetDouble (value, key); 94 | } 95 | 96 | public string FileSystem { 97 | get { 98 | return defs.StringForKey ("FileSystem") ?? ""; 99 | } 100 | set { 101 | defs.SetString (value ?? "", "FileSystem"); 102 | } 103 | } 104 | 105 | public string LastDocumentPath { 106 | get { 107 | return defs.StringForKey ("LastDocumentPath") ?? ""; 108 | } 109 | set { 110 | defs.SetString (value ?? "", "LastDocumentPath"); 111 | defs.Synchronize (); 112 | } 113 | } 114 | 115 | public int RunCount { 116 | get { 117 | return (int)defs.IntForKey ("RunCount"); 118 | } 119 | set { 120 | defs.SetInt (value, "RunCount"); 121 | } 122 | } 123 | 124 | public bool DisableAnalytics { 125 | get { 126 | return defs.BoolForKey ("DisableAnalytics"); 127 | } 128 | set { 129 | defs.SetBool (value, "DisableAnalytics"); 130 | } 131 | } 132 | 133 | public bool AskedToUseCloud { 134 | get { 135 | return defs.BoolForKey ("AskedToUseCloud"); 136 | } 137 | set { 138 | defs.SetBool (value, "AskedToUseCloud"); 139 | } 140 | } 141 | 142 | public bool UseEnglish { 143 | get { 144 | return defs.BoolForKey ("UseEnglish"); 145 | } 146 | set { 147 | defs.SetBool (value, "UseEnglish"); 148 | } 149 | } 150 | 151 | public bool UseCloud { 152 | get { 153 | return defs.BoolForKey ("UseCloud"); 154 | } 155 | set { 156 | defs.SetBool (value, "UseCloud"); 157 | } 158 | } 159 | 160 | public bool FirstRun { 161 | get { 162 | return RunCount == 1; 163 | } 164 | } 165 | 166 | public bool DarkMode { 167 | get { 168 | return defs.BoolForKey ("DarkMode"); 169 | } 170 | set { 171 | defs.SetBool (value, "DarkMode"); 172 | } 173 | } 174 | 175 | public bool IsPatron { 176 | get { 177 | return defs.BoolForKey ("IsPatron"); 178 | } 179 | set { 180 | defs.SetBool (value, "IsPatron"); 181 | } 182 | } 183 | 184 | public DateTime PatronEndDate { 185 | get { 186 | return GetDateTime ("PatronEndDate"); 187 | } 188 | set { 189 | SetDateTime ("PatronEndDate", value); 190 | } 191 | } 192 | 193 | public bool HasTipped { 194 | get { 195 | return defs.BoolForKey ("HasTipped"); 196 | } 197 | set { 198 | defs.SetBool (value, "HasTipped"); 199 | } 200 | } 201 | 202 | public DateTime TipDate { 203 | get { 204 | return GetDateTime ("TipDate"); 205 | } 206 | set { 207 | SetDateTime ("TipDate", value); 208 | } 209 | } 210 | 211 | public DateTime SubscribedToProDate 212 | { 213 | get 214 | { 215 | return GetDateTime("SubscribedToProDate"); 216 | } 217 | set 218 | { 219 | SetDateTime("SubscribedToProDate", value); 220 | } 221 | } 222 | 223 | public int SubscribedToProMonths 224 | { 225 | get 226 | { 227 | return (int)defs.IntForKey("SubscribedToProMonths"); 228 | } 229 | set 230 | { 231 | defs.SetInt(value, "SubscribedToProMonths"); 232 | } 233 | } 234 | 235 | public string SubscribedToProFromPlatform 236 | { 237 | get 238 | { 239 | return defs.StringForKey ("SubscribedToProFromPlatform") ?? ""; 240 | } 241 | set 242 | { 243 | defs.SetString (value ?? "", "SubscribedToProFromPlatform"); 244 | defs.Synchronize (); 245 | } 246 | } 247 | } 248 | } 249 | 250 | -------------------------------------------------------------------------------- /Praeclarum/Graphics/Point.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2010 Frank A. Krueger 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | using System; 23 | 24 | namespace Praeclarum.Graphics 25 | { 26 | public struct PointF 27 | { 28 | public float X, Y; 29 | 30 | public static PointF Empty = new PointF (0, 0); 31 | 32 | public PointF (float x, float y) 33 | { 34 | X = x; 35 | Y = y; 36 | } 37 | 38 | public static PointF operator + (PointF p, VectorF v) 39 | { 40 | return new PointF (p.X + v.X, p.Y + v.Y); 41 | } 42 | 43 | public override string ToString() 44 | { 45 | return string.Format("({0}, {1})", X, Y); 46 | } 47 | } 48 | 49 | public struct Point 50 | { 51 | public int X, Y; 52 | 53 | public Point (int x, int y) 54 | { 55 | X = x; 56 | Y = y; 57 | } 58 | } 59 | 60 | public struct Size 61 | { 62 | public int Width, Height; 63 | 64 | public Size (int width, int height) 65 | { 66 | Width = width; 67 | Height = height; 68 | } 69 | } 70 | 71 | public struct SizeF 72 | { 73 | public float Width, Height; 74 | 75 | public SizeF (float width, float height) 76 | { 77 | Width = width; 78 | Height = height; 79 | } 80 | public override string ToString () 81 | { 82 | return string.Format ("{{ Width = {0}; Height = {1}; }}", Width, Height); 83 | } 84 | } 85 | 86 | public static class PointFEx 87 | { 88 | public static PointF Add (this PointF a, PointF b) 89 | { 90 | return new PointF (a.X + b.X, a.Y + b.Y); 91 | } 92 | 93 | public static PointF Add (this PointF a, float dx, float dy) 94 | { 95 | return new PointF (a.X + dx, a.Y + dy); 96 | } 97 | 98 | public static PointF Subtract (this PointF a, PointF b) 99 | { 100 | return new PointF (a.X - b.X, a.Y - b.Y); 101 | } 102 | 103 | public static PointF Multiply (this PointF a, float s) 104 | { 105 | return new PointF (a.X * s, a.Y * s); 106 | } 107 | 108 | public static float Length (this PointF a) 109 | { 110 | return (float)Math.Sqrt (a.X*a.X + a.Y*a.Y); 111 | } 112 | 113 | public static float DistanceTo (this PointF a, PointF b) 114 | { 115 | var dx = a.X - b.X; 116 | var dy = a.Y - b.Y; 117 | return (float)Math.Sqrt (dx*dx + dy*dy); 118 | } 119 | 120 | public static float DistanceSquaredTo (this PointF a, float bx, float by) 121 | { 122 | var dx = a.X - bx; 123 | var dy = a.Y - by; 124 | return dx * dx + dy * dy; 125 | } 126 | 127 | public static double DistanceSquaredToD (this PointF a, double bx, double by) 128 | { 129 | var dx = a.X - bx; 130 | var dy = a.Y - by; 131 | return dx * dx + dy * dy; 132 | } 133 | 134 | public static PointF Normalized (this PointF a) 135 | { 136 | var d = a.X * a.X + a.Y * a.Y; 137 | if (d <= 0) return a; 138 | var r = (float)(1.0 / Math.Sqrt (d)); 139 | return new PointF (a.X * r, a.Y * r); 140 | } 141 | 142 | public static float Dot (this PointF a, PointF b) 143 | { 144 | return a.X * b.X + a.Y * b.Y; 145 | } 146 | 147 | public static PointF Lerp (this PointF s, PointF d, float t) 148 | { 149 | var dx = d.X - s.X; 150 | var dy = d.Y - s.Y; 151 | return new PointF (s.X + t * dx, s.Y + t * dy); 152 | } 153 | 154 | public static float DistanceToLine (this PointF p3, PointF p1, PointF p2) 155 | { 156 | return new LineSegmentF (p1, p2).DistanceTo (p3); 157 | } 158 | } 159 | 160 | public struct LineSegmentF 161 | { 162 | public float X, Y, EndX, EndY; 163 | 164 | public LineSegmentF (PointF begin, PointF end) 165 | { 166 | X = begin.X; 167 | Y = begin.Y; 168 | EndX = end.X; 169 | EndY = end.Y; 170 | } 171 | 172 | public float DistanceTo (PointF p3) 173 | { 174 | var x21 = EndX - X; 175 | var y21 = EndY - Y; 176 | var x31 = p3.X - X; 177 | var y31 = p3.Y - Y; 178 | 179 | var d = x21*x21 + y21*y21; 180 | if (d <= 0) return (float)Math.Sqrt (x31*x31 + y31*y31); 181 | 182 | var n = x31*x21 + y31*y21; 183 | var u = n / d; 184 | 185 | if (u <= 0) { 186 | return (float)Math.Sqrt (x31*x31 + y31*y31); 187 | } 188 | else if (u >= 1) { 189 | var x32 = p3.X - EndX; 190 | var y32 = p3.Y - EndY; 191 | return (float)Math.Sqrt (x32*x32 + y32*y32); 192 | } 193 | else { 194 | var dx = X + u*x21 - p3.X; 195 | var dy = Y + u*y21 - p3.Y; 196 | return (float)Math.Sqrt (dx*dx + dy*dy); 197 | } 198 | } 199 | 200 | public PointF Start { 201 | get { return new PointF (X, Y); } 202 | } 203 | 204 | public PointF End { 205 | get { return new PointF (EndX, EndY); } 206 | } 207 | 208 | public PointF MidPoint { 209 | get { return new PointF ((X + EndX)/2, (Y + EndY)/2); } 210 | } 211 | } 212 | } 213 | 214 | -------------------------------------------------------------------------------- /Praeclarum.iOS/UI/ScrollableCanvas.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UIKit; 3 | using Praeclarum.Graphics; 4 | 5 | namespace Praeclarum.UI 6 | { 7 | public class ScrollableCanvas : View, ICanvas 8 | { 9 | public event EventHandler Drawing = delegate {}; 10 | public event EventHandler TouchBegan = delegate {}; 11 | public event EventHandler TouchMoved = delegate {}; 12 | public event EventHandler TouchCancelled = delegate {}; 13 | public event EventHandler TouchEnded = delegate {}; 14 | 15 | Scroller scroll; 16 | Canvas scrollContent; 17 | 18 | Canvas canvas; 19 | 20 | RectangleF visibleArea; 21 | 22 | SizeF contentSize = new SizeF (768, 1024); 23 | 24 | public bool TouchEnabled { get; set; } 25 | 26 | public bool TouchDelayed { 27 | get { return scroll.DelaysContentTouches; } 28 | set { scroll.DelaysContentTouches = value; } 29 | } 30 | 31 | public bool ScrollingEnabled { 32 | get { return scroll.ScrollEnabled; } 33 | set { scroll.ScrollEnabled = true; scroll.UserInteractionEnabled = true; } 34 | } 35 | 36 | public float Zoom { 37 | get { return (float)scroll.ZoomScale; } 38 | } 39 | 40 | public ScrollableCanvas () 41 | { 42 | Initialize (); 43 | } 44 | 45 | public ScrollableCanvas (RectangleF frame) 46 | : base (frame) 47 | { 48 | Initialize (); 49 | } 50 | 51 | public override Color BackgroundColor { 52 | get { 53 | return base.BackgroundColor; 54 | } 55 | set { 56 | base.BackgroundColor = value; 57 | canvas.BackgroundColor = value; 58 | } 59 | } 60 | 61 | void Initialize () 62 | { 63 | var bounds = Bounds; 64 | 65 | visibleArea = bounds.ToRectangleF (); 66 | 67 | canvas = new Canvas (bounds.ToRectangleF ()) { 68 | AutoresizingMask = UIViewAutoresizing.FlexibleDimensions, 69 | }; 70 | 71 | BackgroundColor = Colors.White; 72 | 73 | scrollContent = new Canvas (new RectangleF (PointF.Empty, contentSize)) { 74 | Opaque = false, 75 | BackgroundColor = UIColor.Clear.GetColor (), 76 | }; 77 | scrollContent.TouchBegan += HandleTouchBegan; 78 | scrollContent.TouchMoved += HandleTouchMoved; 79 | scrollContent.TouchEnded += HandleTouchEnded; 80 | scrollContent.TouchCancelled += HandleTouchCancelled; 81 | 82 | scroll = new Scroller (bounds) { 83 | AutoresizingMask = UIViewAutoresizing.FlexibleDimensions, 84 | MinimumZoomScale = 1/4.0f, 85 | MaximumZoomScale = 4.0f, 86 | AlwaysBounceVertical = true, 87 | AlwaysBounceHorizontal = true, 88 | BackgroundColor = UIColor.Clear, 89 | }; 90 | 91 | scroll.AddSubview (scrollContent); 92 | scroll.ContentSize = contentSize.ToSizeF (); 93 | 94 | TouchEnabled = true; 95 | TouchDelayed = true; 96 | 97 | scroll.ViewForZoomingInScrollView = delegate { 98 | return scrollContent; 99 | }; 100 | scroll.ZoomingEnded += delegate { 101 | }; 102 | scroll.Scrolled += HandleScrolled; 103 | 104 | AddSubviews (canvas, scroll); 105 | 106 | // 107 | // Prime the visible area 108 | // 109 | HandleScrolled (scroll, EventArgs.Empty); 110 | 111 | // 112 | // Ready to Draw 113 | // 114 | SetVisibleArea (); 115 | canvas.Drawing += HandleDrawing; 116 | } 117 | 118 | void HandleTouchBegan (object sender, CanvasTouchEventArgs e) 119 | { 120 | var ne = e; 121 | TouchBegan (this, ne); 122 | } 123 | 124 | void HandleTouchMoved (object sender, CanvasTouchEventArgs e) 125 | { 126 | var ne = e; 127 | TouchMoved (this, ne); 128 | } 129 | 130 | void HandleTouchEnded (object sender, CanvasTouchEventArgs e) 131 | { 132 | var ne = e; 133 | TouchEnded (this, ne); 134 | } 135 | 136 | void HandleTouchCancelled (object sender, CanvasTouchEventArgs e) 137 | { 138 | var ne = e; 139 | TouchCancelled (this, ne); 140 | } 141 | 142 | void HandleDrawing (object sender, CanvasDrawingEventArgs e) 143 | { 144 | var g = e.Graphics; 145 | 146 | if (visibleArea.Width <= 0 || visibleArea.Height <= 0) 147 | return; 148 | 149 | var scale = (float)canvas.Frame.Width / (float)visibleArea.Width; 150 | 151 | g.Scale (scale, scale); 152 | 153 | g.Translate (-visibleArea.X, -visibleArea.Y); 154 | 155 | var ne = new CanvasDrawingEventArgs ( 156 | e.Graphics, 157 | visibleArea 158 | ); 159 | Drawing (this, ne); 160 | } 161 | 162 | void HandleScrolled (object sender, EventArgs e) 163 | { 164 | try { 165 | SetVisibleArea (); 166 | } catch (Exception ex) { 167 | Log.Error (ex); 168 | } 169 | } 170 | 171 | public override void LayoutSubviews () 172 | { 173 | try { 174 | base.LayoutSubviews (); 175 | SetVisibleArea (); 176 | } catch (Exception ex) { 177 | Log.Error (ex); 178 | } 179 | } 180 | 181 | public void SetVisibleArea () 182 | { 183 | var s = (float)scroll.ZoomScale; 184 | 185 | var va = scroll.Bounds.ToRectangleF (); 186 | va.X /= s; 187 | va.Y /= s; 188 | va.Width /= s; 189 | va.Height /= s; 190 | 191 | visibleArea = va; 192 | 193 | canvas.Invalidate (); 194 | } 195 | 196 | public void Invalidate () 197 | { 198 | canvas.Invalidate (); 199 | } 200 | 201 | public void Invalidate (RectangleF frame) 202 | { 203 | canvas.Invalidate (); 204 | } 205 | 206 | class Scroller : UIScrollView 207 | { 208 | public Scroller (CoreGraphics.CGRect frame) 209 | : base (frame) 210 | { 211 | 212 | } 213 | 214 | public override bool TouchesShouldCancelInContentView (UIView view) 215 | { 216 | return false; 217 | } 218 | 219 | public override bool TouchesShouldBegin (Foundation.NSSet touches, UIEvent withEvent, UIView inContentView) 220 | { 221 | try { 222 | var s = Superview as ScrollableCanvas; 223 | if (s == null) 224 | return base.TouchesShouldBegin (touches, withEvent, inContentView); 225 | 226 | return s.TouchEnabled; 227 | } catch (Exception ex) { 228 | Log.Error (ex); 229 | return false; 230 | } 231 | } 232 | } 233 | } 234 | } 235 | 236 | -------------------------------------------------------------------------------- /Praeclarum/App/DocumentReference.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using System.Linq; 5 | using System.Diagnostics; 6 | using Praeclarum.IO; 7 | 8 | namespace Praeclarum.App 9 | { 10 | public delegate IDocument DocumentConstructor (string localFilePath); 11 | 12 | public class DocumentReference 13 | { 14 | public IFile File { get; private set; } 15 | 16 | public bool IsNew { get; set; } 17 | 18 | readonly DocumentConstructor dctor; 19 | 20 | public DocumentReference (IFile file, DocumentConstructor dctor, bool isNew) 21 | { 22 | if (file == null) 23 | throw new ArgumentNullException ("file"); 24 | if (dctor == null) 25 | throw new ArgumentNullException ("dctor"); 26 | File = file; 27 | IsNew = isNew; 28 | this.dctor = dctor; 29 | } 30 | 31 | LocalFileAccess local = null; 32 | string LocalFilePath { get { return local != null ? local.LocalPath : null; } } 33 | 34 | public string Name { 35 | get { 36 | return File.IsDirectory ? 37 | Path.GetFileName (File.Path) : 38 | Path.GetFileNameWithoutExtension (File.Path); 39 | } 40 | } 41 | 42 | public async Task Rename (string newName) 43 | { 44 | try { 45 | 46 | var dir = Path.GetDirectoryName (File.Path); 47 | var ext = Path.GetExtension (File.Path); 48 | 49 | var newPath = Path.Combine (dir, newName + ext); 50 | 51 | var r = await File.Move (newPath); 52 | return r; 53 | 54 | } catch (Exception ex) { 55 | Debug.WriteLine (ex); 56 | return false; 57 | } 58 | } 59 | 60 | public static Task New (string path, string defaultExt, IFileSystem fs, DocumentConstructor dctor, string contents = null) 61 | { 62 | var dir = Path.GetDirectoryName (path); 63 | var ext = Path.GetExtension (path); 64 | if (string.IsNullOrEmpty (ext)) 65 | ext = defaultExt; 66 | var baseName = Path.GetFileNameWithoutExtension (path); 67 | 68 | return New (dir, baseName, ext, fs, dctor, contents); 69 | } 70 | 71 | public static async Task New (string directory, string baseName, string ext, IFileSystem fs, DocumentConstructor dctor, string contents = null) 72 | { 73 | if (ext [0] != '.') { 74 | ext = '.' + ext; 75 | } 76 | 77 | // 78 | // Get a name 79 | // 80 | var n = baseName + ext; 81 | var i = 1; 82 | var p = Path.Combine (directory, n); 83 | var files = await fs.ListFiles (directory); 84 | while (files.Exists (x => x.Path == p)) { 85 | i++; 86 | n = baseName + " " + i + ext; 87 | p = Path.Combine (directory, n); 88 | } 89 | 90 | return new DocumentReference (await fs.CreateFile (p, contents), dctor, isNew: true); 91 | } 92 | 93 | public async Task Duplicate (IFileSystem fs) 94 | { 95 | var baseName = Name; 96 | if (!baseName.EndsWith ("Copy", StringComparison.Ordinal)) { 97 | baseName = baseName + " Copy"; 98 | } 99 | 100 | var directory = Path.GetDirectoryName (File.Path); 101 | LocalFileAccess local = null; 102 | var contents = ""; 103 | try { 104 | local = await File.BeginLocalAccess (); 105 | var localPath = local.LocalPath; 106 | contents = System.IO.File.ReadAllText (localPath); 107 | } catch (Exception ex) { 108 | Debug.WriteLine (ex); 109 | } 110 | if (local != null) 111 | await local.End (); 112 | 113 | var ext = Path.GetExtension (File.Path); 114 | 115 | var dr = await New (directory, baseName, ext, fs, dctor, contents); 116 | 117 | dr.IsNew = false; 118 | 119 | return dr; 120 | } 121 | 122 | public IDocument Document { get; private set; } 123 | 124 | public bool IsOpen { 125 | get { return Document != null || local != null; } 126 | } 127 | 128 | public async Task Open () 129 | { 130 | if (Document != null) 131 | throw new InvalidOperationException ("Cannot Open already opened document"); 132 | 133 | if (local != null) 134 | throw new InvalidOperationException ("Cannot Open already locally accessed document"); 135 | 136 | local = await File.BeginLocalAccess (); 137 | 138 | try { 139 | var doc = dctor (LocalFilePath); 140 | if (doc == null) 141 | throw new ApplicationException ("CreateDocument must return a document"); 142 | 143 | if (!System.IO.File.Exists (LocalFilePath)) { 144 | Debug.WriteLine ("CREATE " + LocalFilePath); 145 | await doc.SaveAsync (LocalFilePath, DocumentSaveOperation.ForCreating); 146 | } 147 | else { 148 | Debug.WriteLine ("OPEN " + LocalFilePath); 149 | await doc.OpenAsync (); 150 | } 151 | Document = doc; 152 | 153 | } catch (Exception ex) { 154 | Document = null; 155 | Debug.WriteLine (ex); 156 | } 157 | 158 | return Document; 159 | } 160 | 161 | public async Task Close () 162 | { 163 | if (Document == null) 164 | throw new InvalidOperationException ("Trying to Close an unopened document"); 165 | 166 | await Document.CloseAsync (); 167 | Document = null; 168 | 169 | if (local != null) { 170 | await local.End (); 171 | local = null; 172 | } 173 | } 174 | 175 | public string ModifiedAgo { 176 | get { 177 | var t = ModifiedTime; 178 | var dt = DateTime.UtcNow - t; 179 | 180 | if (dt.TotalDays > 10000) 181 | dt = TimeSpan.Zero; 182 | 183 | if (dt.TotalDays >= 2.0) { 184 | return t.ToShortDateString (); 185 | } else if (dt.TotalDays >= 1.0) { 186 | return "yesterday"; 187 | } else if (dt.TotalHours >= 1.0) { 188 | var n = (int)(dt.TotalHours + 0.5); 189 | if (n == 1) { 190 | return string.Format ("an hour ago"); 191 | } else if (n == 24) { 192 | return string.Format ("yesterday"); 193 | } else { 194 | return string.Format ("{0:0} hours ago", n); 195 | } 196 | } else if (dt.TotalMinutes >= 2.0) { 197 | return string.Format ("{0:0} mins ago", dt.TotalMinutes); 198 | } else if (dt.TotalMinutes >= 0.5) { 199 | return "a minute ago"; 200 | } else { 201 | return "just now"; 202 | } 203 | } 204 | } 205 | 206 | public DateTime ModifiedTime { 207 | get { 208 | try { 209 | return File.ModifiedTime; 210 | } catch (Exception) { 211 | return DateTime.UtcNow; 212 | } 213 | } 214 | } 215 | 216 | public override string ToString () 217 | { 218 | return Name; 219 | } 220 | } 221 | } 222 | 223 | -------------------------------------------------------------------------------- /Praeclarum.iOS/UI/StorageForm.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | 3 | using System; 4 | using System.Linq; 5 | using Praeclarum.IO; 6 | using Foundation; 7 | using System.Collections.Specialized; 8 | using System.Threading.Tasks; 9 | 10 | // ReSharper disable once CheckNamespace 11 | namespace Praeclarum.UI 12 | { 13 | public class StorageForm : PForm 14 | { 15 | public StorageForm () 16 | { 17 | base.Title = "Choose Storage"; 18 | 19 | Sections.Add (new FileSystemsSection ()); 20 | } 21 | 22 | class FileSystemProvidersSection : PFormSection 23 | { 24 | public FileSystemProvidersSection () 25 | { 26 | Title = "Choose a Provider"; 27 | 28 | Refresh (); 29 | } 30 | 31 | void Refresh () 32 | { 33 | Items.Clear (); 34 | 35 | foreach (var f in FileSystemManager.Shared.Providers.Where (x => x.CanAddFileSystem)) { 36 | Items.Add (f); 37 | } 38 | } 39 | 40 | public override string GetItemImage (object item) 41 | { 42 | if (item is IFileSystemProvider f) { 43 | var iconImage = f.IconUrl; 44 | if (iconImage is not null) 45 | return iconImage; 46 | var typeName = f.GetType ().Name; 47 | return typeName.Replace ("Provider", ""); 48 | } 49 | 50 | return base.GetItemImage (item); 51 | } 52 | 53 | public override string GetItemTitle (object item) 54 | { 55 | return item is IFileSystemProvider f ? f.Name : base.GetItemTitle (item); 56 | } 57 | 58 | public override bool SelectItem (object item) 59 | { 60 | if (item is not IFileSystemProvider f) 61 | { 62 | return false; 63 | } 64 | 65 | AddFileSystemAsync (f).ContinueWith (t => { 66 | if (t.IsFaulted) 67 | Log.Error (t.Exception); 68 | }); 69 | 70 | return false; 71 | } 72 | 73 | async Task AddFileSystemAsync (IFileSystemProvider fileSystemProvider) 74 | { 75 | if (Form is not {} form) 76 | return; 77 | if (await fileSystemProvider.ShowAddUI (form) is {} newFileSystem) 78 | { 79 | FileSystemManager.Shared.Add (newFileSystem); 80 | } 81 | await form.DismissAsync (true); 82 | } 83 | } 84 | 85 | class FileSystemsSection : PFormSection 86 | { 87 | readonly object _addStorage = "Add Storage"; 88 | 89 | NotifyCollectionChangedEventHandler? _h; 90 | NSTimer? _timer; 91 | 92 | private bool ignoreChanges; 93 | 94 | public FileSystemsSection () 95 | { 96 | Refresh (); 97 | 98 | _h = HandleFileSystemsChanged; 99 | 100 | FileSystemManager.Shared.FileSystems.CollectionChanged += _h; 101 | 102 | _timer = NSTimer.CreateRepeatingScheduledTimer (1, FormatTick); 103 | } 104 | 105 | public override void Dismiss () 106 | { 107 | base.Dismiss (); 108 | 109 | if (_timer != null) { 110 | _timer.Invalidate (); 111 | _timer = null; 112 | } 113 | 114 | if (_h != null) { 115 | FileSystemManager.Shared.FileSystems.CollectionChanged -= _h; 116 | _h = null; 117 | } 118 | } 119 | 120 | void FormatTick (NSTimer obj) 121 | { 122 | SetNeedsFormat (); 123 | } 124 | 125 | void HandleFileSystemsChanged (object? sender, NotifyCollectionChangedEventArgs e) 126 | { 127 | if (ignoreChanges) 128 | return; 129 | Refresh (); 130 | SetNeedsReload (); 131 | } 132 | 133 | void Refresh () 134 | { 135 | Items.Clear (); 136 | 137 | var fileSystemManager = FileSystemManager.Shared; 138 | 139 | foreach (var f in fileSystemManager.FileSystems) { 140 | Items.Add (f); 141 | } 142 | 143 | if (fileSystemManager.Providers.Any (x => x.CanAddFileSystem)) 144 | Items.Add (_addStorage); 145 | } 146 | 147 | public override bool GetItemEnabled (object item) 148 | { 149 | if (item is IFileSystem f) 150 | return f.IsAvailable; 151 | 152 | return base.GetItemEnabled (item); 153 | } 154 | 155 | public override bool GetItemChecked (object item) 156 | { 157 | return item == FileSystemManager.Shared.ActiveFileSystem; 158 | } 159 | 160 | public override string GetItemTitle (object item) 161 | { 162 | if (item is IFileSystem f) { 163 | var desc = f.Description; 164 | if (!f.IsAvailable) { 165 | desc += " (" + f.AvailabilityReason + ")"; 166 | } else if (f.IsSyncing) { 167 | desc += " (" + f.SyncStatus + ")"; 168 | } 169 | return desc; 170 | } 171 | 172 | return base.GetItemTitle (item); 173 | } 174 | 175 | public override string GetItemImage (object item) 176 | { 177 | if (item is IFileSystem f) 178 | { 179 | if (f.IconUrl is { } url) 180 | return url; 181 | return f.GetType ().Name; 182 | } 183 | return base.GetItemImage (item); 184 | } 185 | 186 | public override bool GetItemNavigates (object item) 187 | { 188 | return item == _addStorage; 189 | } 190 | 191 | public override bool SelectItem (object item) 192 | { 193 | if (item == _addStorage) { 194 | 195 | var chooseProviderForm = new PForm ("Add Storage"); 196 | chooseProviderForm.Sections.Add (new FileSystemProvidersSection ()); 197 | 198 | Form?.NavigationController?.PushViewController (chooseProviderForm, true); 199 | 200 | return true; 201 | } 202 | 203 | var fs = item as IFileSystem; 204 | SetNeedsFormat (); 205 | 206 | DocumentAppDelegate.Shared.SetFileSystemAsync (fs, true).ContinueWith (Log.TaskError); 207 | 208 | return true; 209 | } 210 | 211 | public override EditAction GetItemEditActions (object item) 212 | { 213 | return item is IFileSystem { CanRemoveFileSystem: true } ? EditAction.Delete : EditAction.None; 214 | } 215 | 216 | public override void DeleteItem (object item) 217 | { 218 | base.DeleteItem (item); 219 | if (item is not IFileSystem fs) 220 | return; 221 | var fileSystemManager = FileSystemManager.Shared; 222 | try 223 | { 224 | ignoreChanges = true; 225 | fileSystemManager.FileSystems.Remove (fs); 226 | } 227 | finally 228 | { 229 | ignoreChanges = false; 230 | } 231 | fs.RemoveFileSystem (); 232 | // If we deleted the active file system, switch to another available one 233 | if (ReferenceEquals(fs, fileSystemManager.ActiveFileSystem) 234 | && fileSystemManager.FileSystems.FirstOrDefault (x => x.IsAvailable) is {} newFileSystem) 235 | { 236 | DocumentAppDelegate.Shared.SetFileSystemAsync (newFileSystem, false).ContinueWith (t => 237 | { 238 | if (t.IsFaulted) 239 | Log.Error (t.Exception); 240 | }); 241 | } 242 | } 243 | } 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /Praeclarum.iOS/UI/TextInputController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UIKit; 3 | using Foundation; 4 | using System.Threading.Tasks; 5 | 6 | namespace Praeclarum.UI 7 | { 8 | public class TextInputController : UITableViewController 9 | { 10 | public string LabelText { get; set; } 11 | public string InputText { get; set; } 12 | public string Hint { get; set; } 13 | 14 | public event EventHandler Cancelled = delegate {}; 15 | public event EventHandler Done = delegate {}; 16 | 17 | public Func> ValidateFunc { get; set; } 18 | 19 | public TextInputController () 20 | : base (UITableViewStyle.Grouped) 21 | { 22 | Title = "Input"; 23 | 24 | LabelText = ""; 25 | InputText = ""; 26 | Hint = ""; 27 | 28 | NavigationItem.LeftBarButtonItem = new UIBarButtonItem ( 29 | UIBarButtonSystemItem.Cancel, 30 | HandleCancel); 31 | 32 | // NavigationItem.RightBarButtonItem = new UIBarButtonItem ( 33 | // UIBarButtonSystemItem.Done, 34 | // HandleDone); 35 | DocumentAppDelegate.Shared.Theme.Apply (TableView); 36 | // TableView.Delegate = new TextInputDelegate (this); 37 | TableView.DataSource = new TextInputDataSource (this); 38 | } 39 | 40 | public override UIInterfaceOrientationMask GetSupportedInterfaceOrientations () 41 | { 42 | return UIInterfaceOrientationMask.All; 43 | } 44 | 45 | public override UIInterfaceOrientation PreferredInterfaceOrientationForPresentation () 46 | { 47 | return UIInterfaceOrientation.Portrait; 48 | } 49 | 50 | public override bool ShouldAutorotateToInterfaceOrientation (UIInterfaceOrientation toInterfaceOrientation) 51 | { 52 | return true; 53 | } 54 | 55 | void HandleCancel (object sender, EventArgs e) 56 | { 57 | Cancelled (this, EventArgs.Empty); 58 | } 59 | 60 | UIAlertView noAlert; 61 | 62 | async void HandleDone (object sender, EventArgs e) 63 | { 64 | await ValidateAndNotify (); 65 | } 66 | 67 | public async Task ValidateAndNotify () 68 | { 69 | var inputField = ((TextInputCell)TableView.CellAt (NSIndexPath.FromRowSection (0, 0))).InputField; 70 | inputField.ResignFirstResponder (); 71 | 72 | InputText = inputField.Text.Trim (); 73 | 74 | if (string.IsNullOrWhiteSpace (InputText)) { 75 | 76 | Cancelled (this, EventArgs.Empty); 77 | return true; 78 | 79 | } else { 80 | 81 | if (ValidateFunc != null) { 82 | var error = await ValidateFunc (InputText); 83 | if (error != null) { 84 | noAlert = new UIAlertView ("", error, (IUIAlertViewDelegate)null, "OK"); 85 | noAlert.Show (); 86 | return false; 87 | } 88 | } 89 | 90 | Done (this, EventArgs.Empty); 91 | return true; 92 | } 93 | } 94 | 95 | // class TextInputDelegate : UITableViewDelegate 96 | // { 97 | // TextInputController controller; 98 | // 99 | // public TextInputDelegate (TextInputController controller) 100 | // { 101 | // this.controller = controller; 102 | // } 103 | // 104 | // public override void RowSelected (UITableView tableView, MonoTouch.Foundation.NSIndexPath indexPath) 105 | // { 106 | // } 107 | // } 108 | 109 | class TextInputDataSource : UITableViewDataSource 110 | { 111 | TextInputController controller; 112 | TextInputCell cell; 113 | 114 | public TextInputDataSource (TextInputController controller) 115 | { 116 | this.controller = controller; 117 | } 118 | 119 | public override nint NumberOfSections (UITableView tableView) 120 | { 121 | return 1; 122 | } 123 | 124 | public override nint RowsInSection (UITableView tableView, nint section) 125 | { 126 | return 1; 127 | } 128 | 129 | public override string TitleForFooter (UITableView tableView, nint section) 130 | { 131 | try { 132 | return controller.Hint; 133 | } catch (Exception ex) { 134 | Log.Error (ex); 135 | return ""; 136 | } 137 | } 138 | 139 | public override UITableViewCell GetCell (UITableView tableView, NSIndexPath indexPath) 140 | { 141 | try { 142 | if (cell == null) { 143 | cell = new TextInputCell ( 144 | "C", 145 | controller.LabelText, 146 | controller.ValidateAndNotify); 147 | var i = cell.InputField; 148 | i.Placeholder = controller.InputText; 149 | i.Text = controller.InputText; 150 | i.AccessibilityLabel = controller.Title; 151 | i.AutocorrectionType = UITextAutocorrectionType.No; 152 | var theme = DocumentAppDelegate.Shared.Theme; 153 | i.TextColor = theme.TableCellTextColor; 154 | i.KeyboardAppearance = theme.IsDark ? UIKeyboardAppearance.Dark : UIKeyboardAppearance.Default; 155 | theme.Apply (cell); 156 | i.BecomeFirstResponder (); 157 | } 158 | 159 | return cell; 160 | } catch (Exception ex) { 161 | Log.Error (ex); 162 | return new UITableViewCell (); 163 | } 164 | } 165 | } 166 | } 167 | 168 | public class TextInputCell : UITableViewCell 169 | { 170 | public readonly UITextField InputField; 171 | 172 | public TextInputCell (string reuseId, string labelText, Func> done) 173 | : base (UITableViewCellStyle.Default, reuseId) 174 | { 175 | SelectionStyle = UITableViewCellSelectionStyle.None; 176 | 177 | TextLabel.Text = labelText; 178 | 179 | InputField = new UITextField () { 180 | AutoresizingMask = UIViewAutoresizing.FlexibleWidth, 181 | BackgroundColor = UIColor.Clear, 182 | VerticalAlignment = UIControlContentVerticalAlignment.Center, 183 | TextColor = UIColor.Black, 184 | TextAlignment = UITextAlignment.Center, 185 | ClearButtonMode = UITextFieldViewMode.WhileEditing, 186 | AutocorrectionType = UITextAutocorrectionType.Default, 187 | AutocapitalizationType = UITextAutocapitalizationType.Sentences, 188 | KeyboardAppearance = Theme.Current.KeyboardAppearance, 189 | ReturnKeyType = UIReturnKeyType.Done, 190 | ShouldReturn = tf => { 191 | done ().ContinueWith (ta => { 192 | if (ta.IsCompleted && !ta.Result) 193 | tf.BecomeFirstResponder (); 194 | }, TaskScheduler.FromCurrentSynchronizationContext ()); 195 | return false; 196 | }, 197 | }; 198 | 199 | // InputField.BecomeFirstResponder (); 200 | 201 | ContentView.AddSubview (InputField); 202 | } 203 | 204 | public override void LayoutSubviews () 205 | { 206 | base.LayoutSubviews (); 207 | 208 | try { 209 | var b = ContentView.Bounds; 210 | 211 | var text = TextLabel.Text; 212 | if (!string.IsNullOrEmpty (text)) { 213 | var lw = TextLabel.Text.StringSize (TextLabel.Font, nfloat.MaxValue, UILineBreakMode.TailTruncation).Width; 214 | 215 | b.Width -= lw + 33; 216 | b.X += lw + 33; 217 | } else { 218 | b.Width -= 44; 219 | b.X += 22; 220 | } 221 | 222 | InputField.Frame = b; 223 | 224 | } catch (Exception ex) { 225 | Log.Error (ex); 226 | } 227 | } 228 | } 229 | } 230 | 231 | -------------------------------------------------------------------------------- /Praeclarum.iOS/App/TextDocument.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UIKit; 3 | using Foundation; 4 | using System.Threading.Tasks; 5 | using System.Diagnostics; 6 | using System.Collections.Generic; 7 | using Praeclarum.UI; 8 | using Praeclarum.Graphics; 9 | 10 | namespace Praeclarum.App 11 | { 12 | public class TextDocument : UIDocument, ITextDocument 13 | { 14 | public string LocalFilePath { get; private set; } 15 | 16 | public TextDocument (string localFilePath) 17 | : base (NSUrl.FromFilename (localFilePath)) 18 | { 19 | LocalFilePath = localFilePath; 20 | } 21 | 22 | public TextDocument (NSUrl url) 23 | : base (url) 24 | { 25 | LocalFilePath = url.AbsoluteString; 26 | } 27 | 28 | public TextDocument (IntPtr handle) 29 | : base (handle) 30 | { 31 | LocalFilePath = ""; 32 | try { 33 | if (this.FileUrl is NSUrl url) { 34 | LocalFilePath = url.AbsoluteString; 35 | } 36 | } 37 | catch (Exception ex) { 38 | Log.Error (ex); 39 | } 40 | } 41 | 42 | public bool IsOpen { get { return !DocumentState.HasFlag (UIDocumentState.Closed); } } 43 | 44 | public event EventHandler Saving = delegate {}; 45 | public event EventHandler Loading = delegate {}; 46 | 47 | string textData = ""; 48 | public virtual string TextData { 49 | get { return textData; } 50 | set { textData = value ?? ""; } 51 | } 52 | 53 | public void UpdateChangeCount (DocumentChangeKind changeKind) 54 | { 55 | try { 56 | base.UpdateChangeCount (UIDocumentChangeKind.Done); 57 | } catch { 58 | throw; 59 | } 60 | } 61 | 62 | public override NSObject ContentsForType (string typeName, out NSError outError) 63 | { 64 | try { 65 | outError = null; 66 | 67 | var text = TextData; 68 | 69 | var data = NSData.FromString (text, NSStringEncoding.UTF8); 70 | 71 | Debug.WriteLine ("SAVE " + LocalFilePath); 72 | 73 | Saving (this, new SavingEventArgs { 74 | TextData = text 75 | }); 76 | 77 | return data; 78 | 79 | } catch (Exception ex) { 80 | Log.Error (ex); 81 | outError = new NSError (new NSString ("Praeclarum"), 334); 82 | return new NSData (); 83 | } 84 | } 85 | 86 | public override bool LoadFromContents (NSObject contents, string typeName, out NSError outError) 87 | { 88 | try { 89 | outError = null; 90 | var data = contents as NSData; 91 | if (data != null) { 92 | TextData = data.ToString (NSStringEncoding.UTF8); 93 | } else { 94 | TextData = ""; 95 | } 96 | 97 | Loading (this, EventArgs.Empty); 98 | 99 | return true; 100 | } catch (Exception ex) { 101 | Log.Error (ex); 102 | outError = new NSError (new NSString ("Praeclarum"), 335); 103 | return false; 104 | } 105 | } 106 | 107 | public override NSDictionary GetFileAttributesToWrite (NSUrl forUrl, UIDocumentSaveOperation saveOperation, out NSError outError) 108 | { 109 | try { 110 | var imageScale = (nfloat)1.0; 111 | var size = new SizeF (1024, 1024); 112 | 113 | UIGraphics.BeginImageContextWithOptions (new CoreGraphics.CGSize (size.Width, size.Height), true, imageScale); 114 | var c = UIGraphics.GetCurrentContext (); 115 | var g = new Graphics.CoreGraphicsGraphics (c, true); 116 | 117 | DocumentAppDelegate.Shared.App.DrawThumbnail (this, g, size, DocumentAppDelegate.Shared.Theme, readOnlyDoc: true); 118 | 119 | var image = UIGraphics.GetImageFromCurrentImageContext (); 120 | UIGraphics.EndImageContext (); 121 | 122 | outError = null; 123 | var images = NSDictionary.FromObjectsAndKeys ( 124 | new NSObject[] { image }, 125 | new NSObject[] { new NSString ("NSThumbnail1024x1024SizeKey") }); 126 | return NSDictionary.FromObjectsAndKeys ( 127 | new NSObject[] { NSNumber.FromBoolean (true), images }, 128 | new NSObject[] { NSUrl.HasHiddenExtensionKey, NSUrl.ThumbnailDictionaryKey }); 129 | } 130 | catch (Exception ex) { 131 | Log.Error (ex); 132 | return base.GetFileAttributesToWrite (forUrl, saveOperation, out outError); 133 | } 134 | } 135 | 136 | #region IDocument implementation 137 | 138 | async Task IDocument.OpenAsync () 139 | { 140 | var ok = await OpenAsync (); 141 | // Console.WriteLine ("OpenAsync? {0}", ok); 142 | if (!ok) 143 | throw new Exception ("UIDocument.OpenAsync failed"); 144 | } 145 | 146 | async Task IDocument.SaveAsync (string path, DocumentSaveOperation operation) 147 | { 148 | var ok = await SaveAsync ( 149 | NSUrl.FromFilename (path), 150 | operation == DocumentSaveOperation.ForCreating ? 151 | UIDocumentSaveOperation.ForCreating : 152 | UIDocumentSaveOperation.ForOverwriting); 153 | if (!ok) 154 | throw new Exception ("UIDocument.SaveAsync failed"); 155 | } 156 | 157 | async Task IDocument.CloseAsync () 158 | { 159 | if (DocumentState != UIDocumentState.Closed) { 160 | var ok = await CloseAsync (); 161 | if (!ok) 162 | throw new Exception ("UIDocument.CloseAsync failed"); 163 | } 164 | } 165 | 166 | #endregion 167 | 168 | public virtual Task GetActivityItemsAsync (UIViewController fromController) 169 | { 170 | var str = new NSAttributedString (TextData); 171 | return Task.FromResult (new NSObject[] { 172 | str, 173 | new UISimpleTextPrintFormatter (str), 174 | }); 175 | } 176 | 177 | public virtual Task GetActivitiesAsync (UIViewController fromController) 178 | { 179 | return Task.FromResult (new UIActivity[0]); 180 | } 181 | } 182 | 183 | public class TextDocumentHistory 184 | { 185 | List revisions = new List (); 186 | int revisionIndex = 0; 187 | 188 | public TextDocumentHistory () 189 | { 190 | SaveInitialRevision ("Initial", ""); 191 | } 192 | 193 | public void SaveInitialRevision (string title, string textData) 194 | { 195 | revisions.Clear (); 196 | revisionIndex = -1; 197 | SaveRevision (title, textData); 198 | } 199 | 200 | public void SaveRevision (string title, string textData) 201 | { 202 | Console.WriteLine ("SAVE REV " + title); 203 | var r = new TextDocumentRevision { 204 | ModifiedTimeUtc = DateTime.UtcNow, 205 | Title = title, 206 | TextData = textData, 207 | }; 208 | 209 | if (revisionIndex >= 0 && revisionIndex + 1 < revisions.Count) { 210 | revisions.RemoveRange (revisionIndex + 1, (revisions.Count - (revisionIndex + 1))); 211 | } 212 | 213 | revisions.Add (r); 214 | revisionIndex = revisions.Count - 1; 215 | } 216 | 217 | public bool CanUndo { get { return revisionIndex > 0; } } 218 | public bool CanRedo { get { return revisionIndex < revisions.Count - 1; } } 219 | 220 | public void Undo () 221 | { 222 | if (!CanUndo) 223 | return; 224 | revisionIndex--; 225 | } 226 | 227 | public void Redo () 228 | { 229 | if (!CanRedo) 230 | return; 231 | revisionIndex++; 232 | } 233 | 234 | public string TextData 235 | { 236 | get { return revisions [revisionIndex].TextData; } 237 | } 238 | } 239 | 240 | public class TextDocumentRevision 241 | { 242 | public string Title; 243 | public DateTime ModifiedTimeUtc; 244 | public string TextData; 245 | } 246 | 247 | public class SavingEventArgs : EventArgs 248 | { 249 | public string TextData; 250 | } 251 | } 252 | 253 | -------------------------------------------------------------------------------- /Praeclarum/ListDiff.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Praeclarum 5 | { 6 | /// 7 | /// The type of . 8 | /// 9 | public enum ListDiffActionType 10 | { 11 | /// 12 | /// Update the SourceItem to make it like the DestinationItem 13 | /// 14 | Update, 15 | /// 16 | /// Add the DestinationItem 17 | /// 18 | Add, 19 | /// 20 | /// Remove the SourceItem 21 | /// 22 | Remove, 23 | } 24 | 25 | /// 26 | /// A action that can be one of: Update, Add, or Remove. 27 | /// 28 | /// The type of the source list elements 29 | /// The type of the destination list elements 30 | public class ListDiffAction 31 | { 32 | public ListDiffActionType ActionType; 33 | public S SourceItem; 34 | public D DestinationItem; 35 | 36 | public ListDiffAction(ListDiffActionType type, S source, D dest) 37 | { 38 | ActionType = type; 39 | SourceItem = source; 40 | DestinationItem = dest; 41 | } 42 | 43 | public override string ToString() 44 | { 45 | return string.Format("{0} {1} {2}", ActionType, SourceItem, DestinationItem); 46 | } 47 | } 48 | 49 | /// 50 | /// Finds a diff between two lists (that contain possibly different types). 51 | /// are generated such that the order of items in the 52 | /// destination list is preserved. 53 | /// The algorithm is from: http://en.wikipedia.org/wiki/Longest_common_subsequence_problem 54 | /// 55 | /// The type of the source list elements 56 | /// The type of the destination list elements 57 | public class ListDiff 58 | { 59 | /// 60 | /// The actions needed to transform a source list to a destination list. 61 | /// 62 | public List> Actions { get; private set; } 63 | 64 | /// 65 | /// Whether the only contain Update actions 66 | /// (no Adds or Removes). 67 | /// 68 | public bool ContainsOnlyUpdates { get; private set; } 69 | 70 | public ListDiff(IEnumerable sources, IEnumerable destinations) 71 | : this (sources, destinations, (a,b) => a.Equals (b)) 72 | { 73 | } 74 | 75 | public ListDiff(IEnumerable sources, 76 | IEnumerable destinations, 77 | Func match) 78 | { 79 | if (sources == null) throw new ArgumentNullException("sources"); 80 | if (destinations == null) throw new ArgumentNullException("destinations"); 81 | if (match == null) throw new ArgumentNullException("match"); 82 | 83 | var x = new List(sources); 84 | var y = new List(destinations); 85 | 86 | Actions = new List>(); 87 | 88 | var m = x.Count; 89 | var n = y.Count; 90 | 91 | // 92 | // Construct the C matrix 93 | // 94 | var c = new int[m + 1, n + 1]; 95 | for (var i = 1; i <= m; i++) 96 | { 97 | for (var j = 1; j <= n; j++) 98 | { 99 | if (match(x[i - 1], y[j - 1])) 100 | { 101 | c[i, j] = c[i - 1, j - 1] + 1; 102 | } 103 | else 104 | { 105 | c[i, j] = Math.Max(c[i, j - 1], c[i - 1, j]); 106 | } 107 | } 108 | } 109 | 110 | // 111 | // Generate the actions 112 | // 113 | ContainsOnlyUpdates = true; 114 | GenDiff(c, x, y, m, n, match); 115 | } 116 | 117 | void GenDiff(int[,] c, List x, List y, int i, int j, Func match) 118 | { 119 | if (i > 0 && j > 0 && match(x[i - 1], y[j - 1])) 120 | { 121 | GenDiff(c, x, y, i - 1, j - 1, match); 122 | Actions.Add(new ListDiffAction(ListDiffActionType.Update, x[i - 1], y[j - 1])); 123 | } 124 | else 125 | { 126 | if (j > 0 && (i == 0 || c[i, j - 1] >= c[i - 1, j])) 127 | { 128 | GenDiff(c, x, y, i, j - 1, match); 129 | ContainsOnlyUpdates = false; 130 | Actions.Add(new ListDiffAction(ListDiffActionType.Add, default(S), y[j - 1])); 131 | } 132 | else if (i > 0 && (j == 0 || c[i, j - 1] < c[i - 1, j])) 133 | { 134 | GenDiff(c, x, y, i - 1, j, match); 135 | ContainsOnlyUpdates = false; 136 | Actions.Add(new ListDiffAction(ListDiffActionType.Remove, x[i - 1], default(D))); 137 | } 138 | } 139 | } 140 | } 141 | 142 | public static class ListDiffEx 143 | { 144 | public static ListDiff MergeInto (this IList source, IEnumerable destination, Func match) 145 | { 146 | var diff = new ListDiff (source, destination, match); 147 | 148 | var p = 0; 149 | 150 | foreach (var a in diff.Actions) { 151 | if (a.ActionType == ListDiffActionType.Add) { 152 | source.Insert (p, a.DestinationItem); 153 | p++; 154 | } else if (a.ActionType == ListDiffActionType.Remove) { 155 | source.RemoveAt (p); 156 | } else { 157 | p++; 158 | } 159 | } 160 | 161 | return diff; 162 | } 163 | 164 | public static ListDiff MergeInto (this IList source, IEnumerable destination, Func match, Func create, Action update, Action delete) 165 | { 166 | var diff = new ListDiff (source, destination, match); 167 | 168 | var p = 0; 169 | 170 | foreach (var a in diff.Actions) { 171 | if (a.ActionType == ListDiffActionType.Add) { 172 | source.Insert (p, create (a.DestinationItem)); 173 | p++; 174 | } else if (a.ActionType == ListDiffActionType.Remove) { 175 | delete (a.SourceItem); 176 | source.RemoveAt (p); 177 | } else { 178 | update (a.SourceItem, a.DestinationItem); 179 | p++; 180 | } 181 | } 182 | 183 | return diff; 184 | } 185 | 186 | public static ListDiff Diff (this IEnumerable sources, IEnumerable destinations) 187 | { 188 | return new ListDiff (sources, destinations); 189 | } 190 | 191 | public static ListDiff Diff (this IEnumerable sources, IEnumerable destinations, Func match) 192 | { 193 | return new ListDiff (sources, destinations, match); 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /Praeclarum/NumberFormatting.cs: -------------------------------------------------------------------------------- 1 | #nullable disable 2 | 3 | using System; 4 | 5 | namespace Praeclarum 6 | { 7 | public static class NumberFormatting 8 | { 9 | static string[] _zeroPrecisionFormats; 10 | static string[] _precisionFormats; 11 | 12 | static NumberFormatting () 13 | { 14 | var maxP = 9; 15 | _zeroPrecisionFormats = new string[maxP]; 16 | _precisionFormats = new string[maxP]; 17 | for (var p = 0; p < maxP; p++) { 18 | _zeroPrecisionFormats[p] = "0." + new string ('0', p); 19 | _precisionFormats[p] = "0." + new string ('#', p); 20 | } 21 | } 22 | 23 | public class ValScale 24 | { 25 | public readonly double Scale; 26 | public readonly string Prefix; 27 | public readonly string Suffix; 28 | public ValScale (double s, string p) 29 | { 30 | Scale = s; 31 | Prefix = p; 32 | Suffix = " " + p; 33 | } 34 | } 35 | 36 | public static readonly ValScale[] Scales = new ValScale[] { 37 | new ValScale (1e18, "E"), 38 | new ValScale (1e15, "P"), 39 | new ValScale (1e12, "T"), 40 | new ValScale (1e9, "G"), 41 | new ValScale (1e6, "M"), 42 | new ValScale (1e3, "k"), 43 | new ValScale (1e0, ""), 44 | new ValScale (1e-3, "m"), 45 | new ValScale (1e-6, "µ"), 46 | new ValScale (1e-6, "u"), 47 | new ValScale (1e-9, "n"), 48 | new ValScale (1e-12, "p"), 49 | new ValScale (1e-15, "f") 50 | }; 51 | 52 | public static string ToUnitsString (this double value, string units = "", int precision = 3, bool showZeroes = false) 53 | { 54 | if (double.IsNaN (value)) 55 | return "NaN"; 56 | if (double.IsPositiveInfinity (value)) 57 | return "∞" + (units.Length > 0 ? " " + units : ""); 58 | if (double.IsNegativeInfinity (value)) 59 | return "-∞" + (units.Length > 0 ? " " + units : ""); 60 | 61 | if (units == "mm") { 62 | units = "m"; 63 | value *= 1.0e-3; 64 | } 65 | 66 | if (units == "s") { 67 | return ToTimeString (value, precision, showZeroes); 68 | } 69 | 70 | var suffix = ""; 71 | 72 | var neg = value < 0; 73 | var v = Math.Abs (value); 74 | if (v < 1e-18) { 75 | v = 0; 76 | neg = false; 77 | } 78 | 79 | for (var i = 0; i < Scales.Length; i++) { 80 | if (v >= Scales[i].Scale) { 81 | v /= Scales[i].Scale; 82 | suffix = Scales[i].Suffix; 83 | break; 84 | } 85 | } 86 | 87 | if (!string.IsNullOrEmpty (units)) { 88 | if (suffix.Length == 0) { 89 | suffix = " " + units; 90 | } 91 | else { 92 | suffix += units; 93 | } 94 | } 95 | 96 | var f = ""; 97 | if (showZeroes) { 98 | f = _zeroPrecisionFormats[precision]; 99 | } 100 | else { 101 | f = _precisionFormats[precision]; 102 | } 103 | 104 | return (neg ? -v : v).ToString (f) + suffix.TrimEnd (); 105 | } 106 | 107 | public static string ToTimeString (this double value, int precision = 3, bool showZeroes = false) 108 | { 109 | var seconds = Math.Abs (value); 110 | double yearSeconds = 60 * 60 * 60 * 24 * 365; 111 | if (seconds > yearSeconds) { 112 | var years = Math.Ceiling (seconds / yearSeconds); 113 | if (years > 100) { 114 | return "∞ s"; 115 | } 116 | return $"{years} years"; 117 | } 118 | var ts = TimeSpan.FromSeconds (value); 119 | if (ts.TotalDays >= 1) { 120 | return ts.ToString (@"d\.hh\:mm\:ss"); 121 | } 122 | else if (ts.TotalHours >= 1) { 123 | return ts.ToString (@"h\:mm\:ss"); 124 | } 125 | else if (ts.TotalMinutes >= 1) { 126 | return ts.ToString (@"m\:ss"); 127 | } 128 | else { 129 | return ToUnitsString (value, "X", precision, showZeroes).Replace ("X", "s"); 130 | } 131 | } 132 | 133 | public static string ToShortScaledUnitsString (this double value, double scale, string units) 134 | { 135 | if (double.IsNaN (value)) 136 | return "NaN"; 137 | 138 | var v = Math.Abs (value); 139 | var prefix = value < 0 ? "-" : ""; 140 | var r = (v / scale); 141 | if (r < 1) 142 | return r.ToString ("0.##") + " " + units; 143 | if (r < 10) 144 | return r.ToString ("0.#") + " " + units; 145 | return prefix + r.ToString ("0") + " " + units; 146 | } 147 | 148 | public static string ToShortScaledString (this double value, double scale) 149 | { 150 | if (double.IsNaN (value)) 151 | return "NaN"; 152 | 153 | var v = Math.Abs (value); 154 | var prefix = value < 0 ? "-" : ""; 155 | var r = (v / scale); 156 | if (r < 1) 157 | return r.ToString ("0.##"); 158 | if (r < 10) 159 | return r.ToString ("0.#"); 160 | return prefix + r.ToString ("0"); 161 | } 162 | 163 | public static string ToShortUnitsString (this double value, string units) 164 | { 165 | if (double.IsNaN (value)) 166 | return "NaN"; 167 | 168 | var v = Math.Abs (value); 169 | var prefix = value < 0 ? "-" : ""; 170 | if (v < 1e-12) 171 | return "0 " + units; 172 | if (v < 1e-9) 173 | return prefix + Math.Round (v / 1e-12) + " f" + units; 174 | if (v < 1e-6) 175 | return prefix + Math.Round (v / 1e-9) + " n" + units; 176 | if (v < 1e-3) 177 | return prefix + Math.Round (v / 1e-6) + " µ" + units; 178 | if (v < 1) 179 | return prefix + Math.Round (v / 1e-3) + " m" + units; 180 | if (v < 1e1) 181 | return prefix + Math.Round (v / 1, 1) + " " + units; 182 | if (v < 1e3) 183 | return prefix + Math.Round (v / 1) + " " + units; 184 | if (v < 1e4) 185 | return prefix + Math.Round (v / 1e3, 1) + " k" + units; 186 | if (v < 1e6) 187 | return prefix + Math.Round (v / 1e3) + " k" + units; 188 | if (v < 1e7) 189 | return prefix + Math.Round (v / 1e6, 1) + " M" + units; 190 | if (v < 1e9) 191 | return prefix + Math.Round (v / 1e6) + " M" + units; 192 | if (v < 1e12) 193 | return prefix + Math.Round (v / 1e9) + " G" + units; 194 | return prefix + Math.Round (v / 1e12) + " T" + units; 195 | } 196 | } 197 | } 198 | 199 | --------------------------------------------------------------------------------