*xF;l&7ZjH82gC#JrK{2r*
z^@)KVVebZ^w^JAyaJ#2`PKIX`fHJ_$BNJeFKWzPtKIU?Ys}wV@GAKWCGmQzBXwsr;
z#k9pR^)l$~6n4F+F}%-zYiAptQ2<6Fzbt^ap3ujXJ5Ds1e38kd3(QpLsa=9D!R!=YU6L?Ai
z4EKWR(Qu$cZ>4Yk2TK*>nsVdq{~j7lzCOv;djc3kz7nt-4J4<=$Ek?-r{0oBgIhfF
zC>Yq0{$!|FXo8*v@~8O(N)`3j7%q&U{3C#l6I&^Tz`XE{S5b{|MoAcH_=O#>#~pK
zsk+oaE67m|mK&%5+d2*aH_)=I0z=hreHBbvwINhS*-Qqbn;7A53qKYPaBSmWY0)zh7bPQ+GWakSC;lByR
z|2q9$fM=gg0`DAo)}8WiXI))D;karHa|R-Rf#PF!w=8i$NE`ya)CHN9_M1%>g04O9)NQ{XwIbDvWf}fGHT+tr42)t%X3a##`+<4u!6sak^6HEr5*;
zuY-EWiK+&>06pW*({#D~=ot+B_*NZd+AEhaC=74bNgfbK`avsJNYL>C>k#InZq({dJ^)>N5YxQF
z?rKyLhBxXI&dY7raiHEH-LJ|#EDUebD41)HBUBAT|NRA+M@}gQ>rIN!PY|4JqnR$C
zl_?BQ-Y_J2qyP%bo?`I8eqg-aRSj*>3AN<50?;Z=q%bw<(qZ7mAi*;Xu)%RY+#Pst
z@&hFHXdL+O0ty!ao{%{FB%aKzZ+W;U;Pte@SoGKBp^oUI8?=wSsyGo6iFrkJ+!G&oGeU
z++9Oz6HStlAY`&yG$FZJPi-sK7#S`866=GEyg2%OI0-5A>DDf76YGK~?_~=vLCI}Y
zw(KL;xz@>wol^j_K3+~u(3v=5SNgOKLw$Q+>n;X10Co2WbUN%n4VBnOf+3LxBe
z9=iEk;8I8GI|*vPhBnziayVO%R%2pp)*(KINiZT9Zv`+q1DFT2)H=OLy?->AVD@Mj
z>w}CuS%BJ`fytmipKsx!-Gt;Tr}01JK24bFW-1%sAlCJ^-Idv^N-w=Mz~0(#q13l<
z%inN(gR4z~H(BqTauRc=PV$ig>nB1XNP!7SP(#`gw|nMDxgxlH$TIQNK_PLUE5@T6
zm?-sc=<4f4I!5fO#J)*6w{-@CWsd;M$)?mC0|I3xQklE25h+)Y0QIiLUtf}xWswj|
zl#5g;v<$Z|PlS3`$K342eZUhUh@6X(tEp`M9kEEC9hKOl0K#pbK;&1z8Fuzirl=kb
zX-uGU@YbC70%9hauv>>L_K0hZNvN~X=dxW#c{?A#iKs{TG*!)qiA7$qx^QcIbN&tE
zsW|f<;C}%jhdL;I5|Vn;S?r;96akrTptc8KI1814rjX0xmW~1y4MHq?j7bLx0w1QT
zc|H^7Jw;#Vpj8E1rvMt4E+(lz0bJlh2c=I;N7WmsvK+oWCqPPxZPimsEt{B^M4e&n
zjteh@#^(IYeHzfztyDF25sPfLqQJ2$fLT`;P_yvwf&T=?x!ghN@vaWx%=W~yh5`jM
zP1vbtk0nrP!WD6A`}L3puVt@M=^&*b%cyRyXX?zSd1q4wm&X5*u_=J2)<(3#$3Si(
z7yasSfngwngEm=3NWm-vdNerfvNo$Q;kse#7l3c+usv@afTu%%MI?eZP}BG#ysN$ze9zK=aRJYN9)
z4o;Z4Nw_GUQbbmh1nw*GY6PHFJc%i{vT^mEbUu{ctD<4aRhZ^)K^D5*T1!a^yc2*q
ziQM)F9HmS_IFa%l_vxt#WEUkSERMtc5&QaXTL89rHW!#x%tW}A>gG-KMmCQ2L!(y!
z4Q*dQWF-!s;B%BgfgypZIqnZQiU73+G>$+4uwSg*DJWF1Faehh!vvd`Pp=ffK`&@#
z+bIT?QrWDCMSeG$4~%|9gxmUn32wL0c0M!=VGkZUSEQB(n}ewxgG^4;X+WI;vlB4W
zu*cj#o*{wzOS~TcZ#4#*Ie9c$hDYgEZPiI>7w_|YA(RZk6b~wa`ef=+z5rV*V
z1CYZRggn8Bz^YQP?I?^_k>3$red%caa`X!;UA_Svbh~MekN|#DoLYCf9`2XNPiN4d
z{OMz&sjrXrk4NtW+}k-oWs@MX*zFcNLPB6i0A?I#_WC$O0(hp_-W?2Y@?Uhd|L17`
zI6c>@u6_Vm5`&x*9Ma&?qSOw7+i$~sdtNU{MQQq^!yyQMJDm@t7YESWWl+_!3utq@
zrH&DoR0!(SWGe37$A0>Cc&?CCCRCqMqtsv1z3TCFK9YXSXsolF>gMx-S#G!2F-8Qo
z2U9x(z7nt(1w2~>hw|>+|5({V^3ZMU+mcowOJ>m(Vt$p>DlLB)5O}oMk4LwgO~F)D
zrcauvFR|sZc)Fj;OwIOo^-|qj4V>e4vmGTNsgTqP6UzN9Ld)5&!&AlnzJJ=NCy1^|
zYwdrO%wK1^{%s)UcGDdvDXBr)?VnAzl%&8jMVZ}skl!RQ)fY4UaOMlGy&Hz9Y8eEs
zak~dxVOJovET~U{_^+%n^HL$~@lF4<5q?DXnw6P;d~6HasPUh`dbj()H8zLf*Cna#
zLZ8gpP>||NpY%{_FQNE%$5O-?U9DY;nt7X0c@iRgB8D{B8cH3D&Gn=rZm=x~ujEax
zf2;Wloxd2<&B9~fYw7KLi>l^%K-lfxa1BG??I0Z1Q%eHhAQ&CHg77=v>6amI(5*fX
zU1RD!Yc^XaHok_)9eDHh_R-U?r{V?1n^5Xo1E=KZnJ<$FE+^Mg
zgWJUcJvGon3M%})t@r3Ggx8AvFyG7>0{uo}8xB}em~{%^olOa1w|T;a0M7Rk=@x$4lXfQi$d7g@l%qv@c|A*pn
zQ;4=VpqUFmR^i;C`$^Gm@34u2YVRKH^;*yyP!I?#%Ghagcja>YwOt-;{CN!fihWK4n`Qtwsl?jlhf%
zsLHWRO5z%9EPzb~Uc15PCq%<{jC%4POH*qILtloduLF&liy%KHy83oI8fV`()zlio
z)HVQf()iRNl)8sx@cV3eV3#HB3orRskUzJropw=Z!qgF%nt;kIjxHQLGVwPTq;@^L
zgyLW64u3hL$^U+Yh9w_C~+EzOWDX2-nqy*F^?YVq29TLFK0@xmcy#bGg@~S(DuKA|r#WuBG
zgsFW4xR%k)zpoH&|Dj#2v|nX6EO`u(51*v=Z7A;Ika;(|I)-eNLTFg>Lqz_<_VzkK
zi2;>HYMC&>fbmHyBRWpZK^@)=rY8KJkeAQxE8wekPC5X>ZOy=60!vBVMDam{Zi>du
zW9{9xW#6)20W`OTF?bm$Kc?EXh`N)m@GmSduSVb2aNEBFpL3`_=g>$gg>eRy8>y3t
zB_N>}>>j@1}mHVu{hDk7f(N{;!B
zLaD3iUh`W!t9NG6*3{aFq5TCS8bUi=tDkbD-s4$xdyv5cyMZcK>URqP3XUj(qq}m!
zBYHO}M;;)drM&o_H0ei02hsK=uGBo|=1KzU3n;Y|NTAgJrhCmQyQ}q>*Jc!i+cp8S
z-4??q8IXyGzskzB*A)F#V
z6u`XY)xe**Erw5yp!gk
zF=ZwjSMPCK5IgX&1t4E{TLK?XP=Kbs57);r${8;3WGspIEP#gA7DRi~lWp^HlO7J4Gi**>=Qtj;0JLx5PktZk
zsi9O~?zRwCEpx$v?pwpt5Nu%cM?bkNr9SdO2&6Vip+t_(Iqct2rGtVN@
zvE`3HTA>k+o&$0%sB3@)c^Kw}r3*FQv%JcE`-y44a?N=#!o8
zTY;^x;wOZo4*?%>yD=w3ssBfJxNS_Ay$qY5r$D<9nC5nKPL6mcU!ZrKfTI3^?7h;$
zKx8@FIu4Ajm=z|P_AhQX=A>xaU1PrKXJV!UlhIbExtL#a%!mba6~F|>1ubac5bh*fw;0RVKb
z`7K4`=Aq;cvSw>d)}pJwv8=fG;U2Qn(osibvxeHe-vM56yHQ8jg0O^W`&DdOm3=Rm
zG2k61;PEuIPQZ}wAoAzfe|PT57{D|av9V*LrKN`3_5l?xwdrK+Bie3l>b2&!Gcb4q
zD08Vz1Bm<+gN5H?`_G15lEGHIWdR&Zq+=i5YrceLo(25gB`Lma`Q%UWu1oE4R;JGO
zttjP2QWpi>hi1;AYxN4RPJZyR0ys*fV;#}Z;2OYyz}I7;`LM>u3Qf_xJ=Y;$cr1VeY&Sbp{mOeOZo
zWN4pvAXIn{O70*UK9}x}mu#*pYseo30I;iLi0JA&G0XaXn=+(vUdQEn#%L=0EC5}%U#2aO#nH`WK&E}%4}6*Go)#jje&b~KWGlV`I7+1B
zFkP!xfZhW9D(%mU{3RP#t#?M)EVKYfIwMmm3h#fWD;&bzB+R))+keEf&w3`7f2?>n
z5NND~ms|nzL!fCCfBiVo_7CG!gkJcTI!I(!o_fu!b8y>C2rqdgl|SLMfB6iC_GRak
zb8g{V04F25=1GdjEdahuDtpbzh(Yi+oD9#6#`gl*UA>oS`jSxB&YV<>;!J5
ztNn6+@}C^v0vHVqH%^4$w@(;WEZjg>`$Nvlw4?`-YPY-Q2t7pGYaOq(vF&OUzvlhp
zco32AdpUg9N_-1oG<5&6kM1>JLYe0tbK!^q@@-cpKY(PFAHW@_WLNuRAYTOO1BO6;
zjw!Rf9=>ZOz6Fp8bC=d*7zws?^tvWPc*)}+SGwI5Dby20*ZhgwQuw5gmofr5M^;=E
znN1deWPe6qpA+IQ0dkitQUF;ci%%N(3LsZeSriPEm7&Q#qrq1Ixr&xmOMonr#U~wn
z1(363(WKL}NEV;8@D)I=;x7SwEWlR)xr%0H!A=AKCc0#cEIw)CD}Y=j(yrrYQ$>4Y0?gpRC`TrP#aY%e|EQ$aC002ov
JPDHLkV1o4mne+ev
diff --git a/Morphic.Client/Backups.cs b/Morphic.Client/Backups.cs
deleted file mode 100644
index 22a73dcd..00000000
--- a/Morphic.Client/Backups.cs
+++ /dev/null
@@ -1,94 +0,0 @@
-namespace Morphic.Client
-{
- using System;
- using System.Collections.Generic;
- using System.IO;
- using System.Linq;
- using System.Text.Json;
- using System.Threading.Tasks;
- using Config;
- using Core;
- using Microsoft.Extensions.Logging;
- using Service;
- using Path = System.IO.Path;
-
- public class Backups
- {
- private readonly MorphicSession morphicSession;
- private readonly ILogger logger;
- private readonly IServiceProvider serviceProvider;
-
- public static string BackupDirectory => AppPaths.GetUserLocalConfigDir("backups");
- private static readonly string BackupExtension = ".preferences";
-
- public Backups(MorphicSession morphicSession, ILogger logger, IServiceProvider serviceProvider)
- {
- this.morphicSession = morphicSession;
- this.logger = logger;
- this.serviceProvider = serviceProvider;
- }
-
-
- ///
- /// Stores some preferences to a file, for a backup.
- ///
- /// Short description for display (one or two words, file-safe characters)
- /// The preferences to store - null to capture them.
- public async Task Store(Preferences? preferences = null)
- {
- this.logger.LogInformation("Making backup");
- if (preferences == null)
- {
- preferences = new Preferences();
- await this.morphicSession.Solutions.CapturePreferences(preferences);
- }
-
- string json = JsonSerializer.Serialize(preferences);
- string filename = DateTime.Now.ToString("yyyy-MM-dd_HH.mm.ss") + BackupExtension;
- string path = Path.Combine(BackupDirectory, filename);
-
- Directory.CreateDirectory(BackupDirectory);
- await File.WriteAllTextAsync(path, json);
-
- this.logger.LogInformation($"Stored backup to {path}");
- }
-
- ///
- /// Gets the list of backup files.
- ///
- /// filename:date
- public IDictionary GetBackups()
- {
- Dictionary backups = new Dictionary();
-
- if (Directory.Exists(BackupDirectory))
- {
- foreach (string path in Directory.EnumerateFiles(BackupDirectory, "*" + BackupExtension)
- .OrderBy(f => f))
- {
- // Get the date from the filename.
- string dateString = Path.ChangeExtension(Path.GetFileName(path), null);
- if (DateTime.TryParse(dateString.Replace('_', ' ').Replace('.', ':'), out DateTime date))
- {
- backups.Add(path, date.ToString("g"));
- }
- }
- }
-
- return backups;
- }
-
- ///
- /// Applies a back-up.
- ///
- /// The backup file.
- public async Task Apply(string path)
- {
- string json = await File.ReadAllTextAsync(path);
- JsonSerializerOptions options = new JsonSerializerOptions();
- options.Converters.Add(new JsonElementInferredTypeConverter());
- this.morphicSession.Preferences = JsonSerializer.Deserialize(json, options);
- await this.morphicSession.ApplyAllPreferences();
- }
- }
-}
diff --git a/Morphic.Client/Bar/AppFocus.cs b/Morphic.Client/Bar/AppFocus.cs
deleted file mode 100644
index 55847987..00000000
--- a/Morphic.Client/Bar/AppFocus.cs
+++ /dev/null
@@ -1,121 +0,0 @@
-namespace Morphic.Client.Bar
-{
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Windows;
- using System.Windows.Threading;
- using UI.AppBarWindow;
-
- public class AppFocus
- {
- public static AppFocus Current { get; } = new AppFocus();
-
- ///
- /// true if the current application is active.
- ///
- public bool IsActive { get; private set; }
-
- /// The mouse has entered any window belonging to the application.
- public event EventHandler? MouseEnter;
- /// The mouse has left any window belonging to the application.
- public event EventHandler? MouseLeave;
-
- public event EventHandler? Activated;
- public event EventHandler? Deactivated;
-
- protected AppFocus()
- {
- App.Current.Activated += (o, args) => this.Activated?.Invoke(o, args);
- App.Current.Activated += (o, args) => this.Deactivated?.Invoke(o, args);
- this.Activated += (sender, args) => this.IsActive = true;
- this.Deactivated += (sender, args) => this.IsActive = false;
- }
-
- // The mouse is over any window in mouseOverWindows
- private bool mouseOver;
-
- // The windows where the mouse-over status is needed.
- private readonly List mouseOverWindows = new List();
- private DispatcherTimer? mouseTimer;
-
- ///
- /// Register interest in observing the mouse-over state of a window.
- ///
- ///
- public void AddMouseOverWindow(Window window)
- {
- this.mouseOverWindows.Add(window);
- window.MouseEnter += this.CheckMouseOver;
- window.MouseLeave += this.CheckMouseOver;
- }
-
- private void CheckMouseOver(object? sender, EventArgs e)
- {
- if (this.mouseOverWindows.Count == 0)
- {
- return;
- }
-
- bool isOver = false;
- IEnumerable windows = this.mouseOverWindows.Where(w => w.IsVisible && w.Opacity > 0);
-
- Point? cursor = null;
-
- // Window.IsMouseOver is false if the mouse is over the window border, check if that's the case.
- foreach (Window window in windows)
- {
- if (window.IsMouseOver)
- {
- isOver = true;
- break;
- }
-
- cursor ??= PresentationSource.FromVisual(window)?.CompositionTarget.TransformFromDevice
- .Transform(WindowMovement.GetCursorPos());
-
- if (cursor != null)
- {
- System.Windows.Rect rc = window.GetRect();
- rc.Inflate(10, 10);
- if (rc.Contains(cursor.Value))
- {
- isOver = true;
- if (this.mouseTimer == null)
- {
- // Keep an eye on the current position.
- this.mouseTimer = new DispatcherTimer(DispatcherPriority.Input)
- {
- Interval = TimeSpan.FromMilliseconds(100),
- };
- this.mouseTimer.Tick += this.CheckMouseOver;
- this.mouseTimer.Start();
- }
-
- break;
- }
- }
- }
-
- if (!isOver)
- {
- this.mouseTimer?.Stop();
- this.mouseTimer = null;
- }
-
- if (this.mouseOver != isOver)
- {
- this.mouseOver = isOver;
- if (isOver)
- {
- this.MouseEnter?.Invoke(sender, new EventArgs());
- }
- else
- {
- this.MouseLeave?.Invoke(sender, new EventArgs());
- }
- }
- }
-
- }
-}
diff --git a/Morphic.Client/Bar/BarData.md b/Morphic.Client/Bar/BarData.md
deleted file mode 100644
index 89f62832..00000000
--- a/Morphic.Client/Bar/BarData.md
+++ /dev/null
@@ -1,519 +0,0 @@
-# Data structure for Morphic Bar
-
-## `Bar`
-
-This is what the client can handle for the bar, which is a super-set of what is provided by the web app.
-
-There is initial data, in `default-bar.json5`. This is loaded first, then the data from the web app is merged over it.
-
-Not all fields are required, as the client will already have its own predefined defaults. Assume fields to be optional, unless stated.
-
-```js
-Bar = {
- // Bar identifier
- id: "bar1",
- // Bar name
- name: "Example bar",
-
- // Initial position
- position: {
- // Dock it to an edge, reserving desktop space
- docked: "left", // "left", "right", "top", "bottom", "none" (default), or "disable".
-
- horizontal: false, // true for horizontal orientation.
- restricted: false, // true to restrict the position to just the corners
-
- // Position of the bar. Can be "Left"/"Top", "Middle", "Right"/"Bottom", a number, or a percentage.
- // Numbers or percentages can be negative (including -0), meaning distance from the right.
- // Percentages specify the position of the middle of the bar.
- // Ignored if `docked` is used.
- x: "50%",
- y: "Bottom",
-
- // Position of the secondary bar, relative to the primary bar. Same syntax as `x`/`y` above.
- // (can be split with `secondaryX` and `secondaryY`)
- secondary: "Middle",
-
- // Position of the expander button (the thing that opens the secondary bar)
- // (can be split with `expanderX` and `expanderY`)
- expander: "Middle",
- // What the position in `expander` is relative to.
- // "primary", "secondary", or "both" (secondary if the secondary bar is open, otherwise primary)
- expanderRelative: "Both",
- },
-
- // Settings for the secondary bar
- secondaryBar: {
- // Close the secondary bar when another application takes focus.
- autohide: true,
- // Hide the expander button when another application takes focus (shown on mouse-over).
- autohideExpander: false
- },
-
- // Size of everything
- scale: 1,
-
- // What happens when all buttons do not fit.
- // "resize": shrinks some items until it fits
- // "wrap": Adds another column
- // "secondary": Move over-flow items to the secondary bar.
- // "hide": Do nothing.
- overflow: "resize",
-
- // Bar theme
- theme: {Theme},
-
- // Theme for bar items
- itemTheme: {ItemTheme},
-
- // The bar items
- items: [
- {BarItem}
- ],
-
- sizes: {
- // Padding between edge of bar and items.
- windowPadding: "10 15",
- // Spacing between items.
- itemSpacing: 10,
- // Item width.
- itemWidth: 100,
- // Maximum Button Item title lines.
- buttonTextLines: 2,
- // Button Item padding between edge and title. And for the top, between circle and title.
- buttonPadding: "10",
- // Button Item circle image diameter (a fraction relative to the itemWidth).
- buttonCircleDiameter: 0.66,
- // Button Item circle overlap with rectangle (a fraction relative to buttonImageSize).
- buttonImageOverlap: 0.33,
- buttonFontSize: 14,
- buttonFontWeight: "normal",
- circleBorderWidth: 2,
- buttonCornerRadius: 10
- }
-}
-```
-
-## `BarItem`
-
-Describes an individual bar item.
-
-```js
-BarItem = {
- // email|calendar|videocall|photos|...
- // Currently ignored by the client
- category: "calendar",
-
- // unique identifier (currently ignored by client)
- id: "calendar-button",
-
- // `true` if the item is shown on the primary bar. `false` to show on the secondary bar.
- is_primary: true,
-
- // `true` to never move this item to the secondary bar (for Bar.overflow == "secondary")
- no_overflow: false,
-
- // Per-button theme, overrides the `Bar.itemTheme` field from above.
- // If unset, this is generated using `configuration.color`
- theme: {Theme},
-
- // `true` to not show this button. While it's expected that the client will only receive the items which should be
- // shown, this field provides the ability to show or hide items depending on the platform, using the platform
- // identifier, described later. For example, `hidden$win: true` will make the item only available for macOS.
- hidden: false,
-
- // Items are sorted by this (higher values at the top).
- priority: 0,
-
- // The kind of item (see Item kinds below) [REQUIRED]
- // "link", "application", "action"
- kind: "link",
-
- // "button" (default), "image", "multi" (see Widgets below)
- widget: "button",
-
- // Specific to the item kind.
- configuration: {
- // ...
- }
-}
-```
-
-## Button items
-
-```js
-/** @mixes BarItem */
-ButtonItem = {
- kind: "",
- configuration: {
- // Displayed on the button [REQUIRED]
- label: "Calendar",
-
- // Tooltip.
- tooltipHeader: "Open the calendar",
- // More details.
- tooltip: "Displays your google calendar",
-
- // Automation UI name - this is used by narrator. default is the label.
- uiName: "Calendar",
-
- // local/remote url of the icon. For values without a directory, a matching file in ./Assets/bar-icons/`) is
- // discovered. If this value is omitted (or not working), an image is detected depending on the kind of item:
- // - link: favicon of the site.
- // - application: The application icon.
- image_url: "calendar.png",
-
- // Item color (overrides BarItem.theme, generates the different shades for the states)
- color: '#002957',
-
- // Size of the item. "textonly", "small", "medium", or "large" (default)
- size: "large",
-
- // Context menu
- menu: {ContextMenu}
-
- }
-}
-```
-
-### `kind = "link"`
-
-Opens a web page.
-
-```js
-/** @extends ButtonItem */
-LinkButton = {
- kind: "link",
- /** @mixes LinkAction */
- configuration: {
- url: "https://example.com"
- }
-}
-```
-
-### `kind = "application"`
-
-Opens an application.
-
-```js
-/** @extends ButtonItem */
-ApplicationButton = {
- kind: "application",
- /** @mixes ApplicationAction */
- configuration: {
- // Executable name (or full path). Full path is discovered via `App Paths` registry or the PATH environment variable.
- // To pass arguments, surround the executable with quotes and append the arguments after (or use the args field)
- exe: "notepad.exe",
- // Arguments to pass to the process
- args: [ "arg1", "arg2" ],
- // Extra environment variables
- env: {
- name: "value"
- },
- // Always start a new instance (otherwise, activate the existing instance if one is found)
- newInstance: true,
- // Initial state of the window (not all apps honour this)
- windowStyle: "normal" // "normal" (default), "maximized", "minimized" or "hidden"
- }
-}
-```
-
-Or, run a default application. Use the `default` field to identify an entry in [`default-apps.json5`](#default-appsjson5).
-
-```js
-/** @extends ButtonItem */
-ApplicationButton = {
- kind: "application",
- /** @mixes ApplicationAction */
- configuration: {
- // The key to lookup in default-apps.json5.
- default: "email",
- }
-}
-```
-
-### `kind = "internal"`
-
-Invokes a built-in routine.
-
-```js
-/** @extends ButtonItem */
-InternalButton = {
- kind: "internal",
- /** @mixes InternalAction */
- configuration: {
- // Name of the internal function.
- function: "fname",
- // Arguments to pass.
- args: ["a1", "a2"]
- }
-}
-```
-
-### `kind = "shellExec"`
-
-Executes a command via the windows shell (similar to the `start` command or the run dialog box).
-
-```js
-/** @extends ButtonItem */
-ShellExecButton = {
- kind: "shellExec",
- /** @mixes ApplicationAction */
- configuration: {
- // The command
- default: "ms-settings:"
- }
-}
-```
-
-### `kind = "setting"`
-
-Changes a setting. Currently, only boolean or integer settings are supported.
-
-```js
-/** @extends ButtonItem */
-SettingButton = {
- kind: "setting",
- /** @mixes SettingAction */
- configuration: {
- // The setting path
- settingId: "com.microsoft.windows.magnifier/enabled"
- }
-}
-```
-
-
-### `kind = "action"`
-
-This performs a lookup of an `action` object in [`presets.json5`](#presetsjson5), using `configuration.identifier`.
-The object in `presets.json5` will be merged onto this one.
-
-This allows for a richer set of data than what the web app provides.
-
-```js
-/** @extends ButtonItem */
-ActionButton = {
- kind: "action",
- /** @mixes PresetAction */
- configuration: {
- identifier: "example-action"
- }
-}
-```
-
-## Widgets
-
-### `widget = "button"`
-
-Standard button.
-
-### `widget = "image"`
-
-Behaves like a button, but only displays an image. Used for the logo button.
-
-```js
-/** @extends ButtonItem */
-ImageItem = {
- widget: "image"
-}
-```
-
-### `widget = "multi"`
-
-Displays multiple buttons in a single item. Used by the settings items.
-
-```js
-/** @extends BarItem */
-MultiButtonItem = {
- widget: "multi",
- configuration: {
- // How the buttons are interracted with via the keyboard: "buttons", "additive", "toggle", "auto" (default)
- // "additive" and "toggle" cause the bar item to behave as a single control (for keyboard navigation), and
- // the button pair is accessed via -/+ keys.
- // For "buttons", each button is a tab stop. "auto" (default) will detect, based on the button names.
- type: "auto",
- buttons: {
- // First button
- button1: {
- // Display text
- label: "day",
- // A value that replaces "{button}" in any action payload (eg, `exe: "app.exe {button}"`).
- value: "b1",
-
- tooltip: "Tooltip header|Tooltip text",
- uiName: "Button one",
- menu: {ContextMenu}
- },
- // next button
- button2: {
- label: "night"
- },
- // ...
- }
- }
-}
-```
-
-Example:
-
-```json5
- {
- // Pass either ^c or ^v to the `sendKeys` internal function.
- kind: "internal",
- widget: "multi",
- configuration: {
- defaultLabel: "Clipboard",
- function: "sendKeys",
- args: {
- keys: "{button}"
- },
- buttons: {
- copy: {
- label: "Copy",
- value: "^c"
- },
- paste: {
- label: "Paste",
- value: "^v"
- }
- }
- }
- }
-```
-
-
-## `Theme`
-
-Used to specify the theme of the bar or an item.
-
-```js
-Theme = {
- color: "white",
- background: "#002957",
- // Only used by bar items
- borderColor: "#ff0",
- focusDotColor: "#000",
- borderSize: 2
-}
-```
-
-## `ItemTheme : Theme`
-
-```js
-/** @extends Theme */
-ItemTheme = {
- // from Theme
- color: "white",
- background: "#002957",
- borderColor: "#ff0",
- focusDotColor: "#000",
- borderSize: 2,
-
- // Optional, will use the above style.
- hover: {Theme}, // Mouse is over the item.
- focus: {Theme}, // Item has keyboard focus.
- active: {Theme} // Item is being clicked (mouse is down).
-}
-```
-
-## `ContextMenu`
-
-```js
-ContextMenu = {
- "setting": "easeofaccess-colorfilter",
- "learn": "color",
- "demo": "color"
-}
-
-```
-
-## presets.json5
-
-This file contains additional data for certain bar items. This allows for additional bar information provided by the client.
-A bar item, from the web app, which points to an object in this file will have this object merged onto it.
-
-For bar items with `kind = "action"`, the value of `configuration.identifier` identifies a key in `actions`.
-For bar items with `kind = "application"`, the value of `configuration.default` identifies a key in `defaults`.
-
-```js
-presets.json5 = {
- actions: {
- "identifier": {BarItem},
-
- // start task manager
- "taskManager": {
- kind: "application",
- configuration: {
- exe: "taskmgr.exe"
- }
- },
-
- // Example: invoke an internal function
- "example": {
- kind: "internal",
- configuration: {
- function: "hello"
- }
- },
-
- // Real example
- "high-contrast": {
- kind: "application",
- widget: "multi",
- configuration: {
- defaultLabel: "High-Contrast",
- exe: "sethc.exe",
- args: [ "{button}" ],
- buttons: {
- on: {
- label: "On",
- value: "100"
- },
- off: {
- label: "Off",
- value: "1"
- }
- }
- }
- }
- },
-
- defaults: {
- // Same as actions.
- "identifier": {BarItem},
-
- "email": {
- configuration: {
- exe: "mailto:"
- }
- }
- }
-}
-```
-
-## Cross-platform
-
-All fields in the bar json and `presets.json5` can be suffixed with an OS identifier (`$mac` or `$win`), which will take precedence over the non-suffixed field. This pre-processing would be done on the client.
-
-examples:
-
-```js
-[
- {
- command: "default command",
- command$mac: "macOS command",
-
- label$win: "on windows",
- labelText: "not windows"
- },
- {
- command: "default command",
- command$win: "windows command"
- },
- {
- command: "default command (ignored)",
- command$win: "windows command",
- command$mac: "macOS command"
- },
-]
-```
diff --git a/Morphic.Client/Bar/BarImages.cs b/Morphic.Client/Bar/BarImages.cs
deleted file mode 100644
index aaa30767..00000000
--- a/Morphic.Client/Bar/BarImages.cs
+++ /dev/null
@@ -1,169 +0,0 @@
-namespace Morphic.Client.Bar
-{
- using System;
- using System.Collections.Generic;
- using System.IO;
- using System.Linq;
- using System.Text.RegularExpressions;
- using System.Windows.Media;
- using System.Windows.Media.Imaging;
- using System.Xml;
- using Config;
- using SharpVectors.Converters;
- using SharpVectors.Dom.Svg;
- using SharpVectors.Renderers.Wpf;
-
- public class BarImages
- {
- ///
- /// Gets the full path to a bar icon in the assets directory, based on its name (with or without the extension).
- ///
- /// Name of the icon.
- ///
- public static string? GetBarIconFile(string name)
- {
- string safe = new Regex(@"\.\.|[^-a-zA-Z0-9./]+", RegexOptions.Compiled)
- .Replace(name, "_")
- .Trim('/')
- .Replace('/', Path.DirectorySeparatorChar);
- string assetFile = AppPaths.GetAssetFile("bar-icons\\" + safe);
- string[] extensions = { "", ".svg", ".png", ".ico", ".jpg", ".jpeg", ".gif" };
-
- string? foundFile = extensions.Select(extension => assetFile + extension)
- .FirstOrDefault(File.Exists);
-
- return foundFile;
- }
-
- ///
- /// Creates an image source from a local image.
- ///
- /// The path to the image, or the name of the icon in the assets directory.
- /// The color, for monochrome vectors.
- /// null if the image is not supported.
- public static ImageSource? CreateImageSource(string imagePath, Color? color = null)
- {
- ImageSource? result;
-
- // Attempt to load an SVG image.
- ImageSource? TrySvg()
- {
- try
- {
- using FileSvgReader svg = new FileSvgReader(new WpfDrawingSettings());
- DrawingGroup drawingGroup = svg.Read(imagePath);
- if (color.HasValue)
- {
- ChangeDrawingColor(drawingGroup, color.Value);
- }
-
- return new DrawingImage(drawingGroup);
- }
- catch (Exception e) when (e is NotSupportedException || e is XmlException || e is SvgException)
- {
- return null;
- }
- }
-
- // Attempt to load a bitmap image.
- ImageSource? TryBitmap()
- {
- try
- {
- BitmapImage image = new BitmapImage();
- image.BeginInit();
- image.CacheOption = BitmapCacheOption.OnLoad;
- image.UriSource = new Uri(imagePath);
- image.EndInit();
- return image;
- }
- catch (Exception e) when (e is NotSupportedException || e is XmlException || e is SvgException)
- {
- return null;
- }
- }
-
- if (!imagePath.Contains('/'))
- {
- imagePath = GetBarIconFile(imagePath) ?? imagePath;
- }
-
- if (Path.GetExtension(imagePath) == ".svg")
- {
- result = TrySvg() ?? TryBitmap();
- }
- else
- {
- result = TryBitmap() ?? TrySvg();
- }
-
- return result;
- }
-
- ///
- /// Replaces the brushes used in a monochrome drawing with a new one, which can be set to a specific colour.
- ///
- /// The drawing to change.
- /// The new colour to set (if brush is null).
- /// The brush to use.
- /// The brush used (null if the drawing isn't monochrome).
- public static SolidColorBrush? ChangeDrawingColor(Drawing drawing, Color color, SolidColorBrush? brush = null)
- {
- List? geometryDrawings;
-
- // Get all the geometries in the drawing.
- if (drawing is DrawingGroup drawingGroup)
- {
- geometryDrawings = GetDrawings(drawingGroup).OfType().ToList();
- }
- else
- {
- geometryDrawings = new List();
- if (drawing is GeometryDrawing gd)
- {
- geometryDrawings.Add(gd);
- }
- }
-
- // If there's only 1 colour, it's mono.
- bool mono = geometryDrawings.Count > 0
- && geometryDrawings
- .Select(gd => gd.Brush)
- .OfType()
- .Where(b => b.Opacity > 0)
- .Select(b => b.Color)
- .Where(c => c.A != 0)
- .Distinct()
- .Count() == 1;
-
- if (!mono)
- {
- return null;
- }
- else
- {
- brush ??= new SolidColorBrush(color);
- geometryDrawings.ForEach(gd =>
- {
- if (gd.Brush is SolidColorBrush && gd.Brush.Opacity > 0)
- {
- gd.Brush = brush;
- }
- });
- return brush;
- }
- }
-
- ///
- /// Gets all drawings within a drawing group.
- ///
- ///
- ///
- private static IEnumerable GetDrawings(DrawingGroup drawingGroup)
- {
- return drawingGroup.Children.OfType()
- .SelectMany(GetDrawings)
- .Concat(drawingGroup.Children.OfType());
- }
- }
-}
diff --git a/Morphic.Client/Bar/BarManager.cs b/Morphic.Client/Bar/BarManager.cs
deleted file mode 100644
index 89ad11ee..00000000
--- a/Morphic.Client/Bar/BarManager.cs
+++ /dev/null
@@ -1,362 +0,0 @@
-// BarManager.cs: Loads and shows bar.
-//
-// Copyright 2020 Raising the Floor - International
-//
-// Licensed under the New BSD license. You may not use this file except in
-// compliance with this License.
-//
-// You may obtain a copy of the License at
-// https://github.com/GPII/universal/blob/master/LICENSE.txt
-
-
-namespace Morphic.Client.Bar
-{
- using System;
- using System.Collections.Generic;
- using System.ComponentModel;
- using System.IO;
- using System.Linq;
- using System.Runtime.CompilerServices;
- using System.Threading.Tasks;
- using System.Windows;
- using Config;
- using Core;
- using Core.Community;
- using Data;
- using Dialogs;
- using Microsoft.Extensions.Logging;
- using Service;
- using UI;
- using MessageBox = System.Windows.Forms.MessageBox;
- using SystemJson = System.Text.Json;
-
- ///
- /// Looks after the bar.
- ///
- public class BarManager : INotifyPropertyChanged
- {
- private PrimaryBarWindow? barWindow;
- private ILogger Logger => App.Current.Logger;
-
- public event EventHandler? BarLoaded;
- public event EventHandler? BarUnloaded;
-
- private bool firstBar = true;
-
- public bool BarVisible => this.barWindow?.Visibility == Visibility.Visible;
-
- public BarManager()
- {
- }
-
- public bool BarIsLoaded { get; private set; } = false;
-
- ///
- /// Show a bar that's already loaded.
- ///
- public void ShowBar()
- {
- if (this.barWindow != null)
- {
- AppOptions.Current.MorphicBarIsVisible = true;
- this.barWindow.Visibility = Visibility.Visible;
- this.barWindow.Focus();
- }
- }
-
- public void HideBar()
- {
- AppOptions.Current.MorphicBarIsVisible = false;
- this.barWindow?.Hide();
- this.barWindow?.OtherWindow?.Hide();
- }
-
- ///
- /// Closes the bar.
- ///
- public void CloseBar()
- {
- this.BarIsLoaded = false;
-
- if (this.barWindow != null)
- {
- this.OnBarUnloaded(this.barWindow);
- BarData bar = this.barWindow.Bar;
- this.barWindow.IsClosing = true;
- this.barWindow.Close();
- this.barWindow = null;
- bar.Dispose();
- }
- }
-
- public BarWindow CreateBarWindow(BarData bar)
- {
- this.barWindow = new PrimaryBarWindow(bar);
- this.barWindow.BarLoaded += this.OnBarLoaded;
- this.barWindow.IsVisibleChanged += this.BarWindowOnIsVisibleChanged;
-
- bool showMorphicBar = false;
- if (AppOptions.Current.AutoShow == true)
- {
- showMorphicBar = true;
- }
- if (this.firstBar == false)
- {
- showMorphicBar = true;
- }
- if (AppOptions.Current.MorphicBarIsVisible == true)
- {
- showMorphicBar = true;
- }
- if (ConfigurableFeatures.MorphicBarVisibilityAfterLogin != null)
- {
- switch (ConfigurableFeatures.MorphicBarVisibilityAfterLogin.Value)
- {
- case ConfigurableFeatures.MorphicBarVisibilityAfterLoginOption.Show:
- showMorphicBar = true;
- break;
- case ConfigurableFeatures.MorphicBarVisibilityAfterLoginOption.Restore:
- // if the bar has not been shown before, show it now; if it has been shown/hidden before, use the last known visibility state
- showMorphicBar = AppOptions.Current.MorphicBarIsVisible ?? true;
- break;
- case ConfigurableFeatures.MorphicBarVisibilityAfterLoginOption.Hide:
- showMorphicBar = false;
- break;
- }
- }
-
- // if we were started up manually, always show the MorphicBar
- if (Environment.GetCommandLineArgs().Contains("--run-after-login") == false)
- {
- showMorphicBar = true;
- }
-
- if (showMorphicBar == true)
- {
- AppOptions.Current.MorphicBarIsVisible = true;
- this.barWindow.Show();
- }
-
- this.firstBar = false;
- return this.barWindow;
- }
-
- private void BarWindowOnIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
- {
- this.OnPropertyChanged(nameof(this.BarVisible));
- }
-
- ///
- /// Called when a bar has loaded.
- ///
- protected virtual void OnBarLoaded(object sender, EventArgs? args = null)
- {
- if (sender is PrimaryBarWindow window)
- {
- this.BarLoaded?.Invoke(this, new BarEventArgs(window));
- }
- }
-
- ///
- /// Called when a bar has closed.
- ///
- protected virtual void OnBarUnloaded(object sender, EventArgs? args = null)
- {
- if (sender is PrimaryBarWindow window)
- {
- this.BarUnloaded?.Invoke(this, new BarEventArgs(window));
- }
- }
-
- #region DataLoading
- private void OnBarOnReloadRequired(object? sender, EventArgs args)
- {
- if (sender is BarData bar)
- {
- string source = bar.Source;
-
- this.CloseBar();
- this.LoadFromBarJson(source);
- }
- }
-
- public BarData? LoadBasicMorphicBar()
- {
- var result = LoadFromBarJson(AppPaths.GetConfigFile("basic-bar.json5", true));
- AppOptions.Current.LastCommunity = null;
- return result;
- }
-
- ///
- /// Loads and shows a bar.
- ///
- /// JSON file containing the bar data.
- /// The file content (if it's already loaded).
- ///
- public BarData? LoadFromBarJson(string path, string? content = null, IServiceProvider? serviceProvider = null)
- {
- if (this.firstBar && AppOptions.Current.Launch.BarFile != null)
- {
- path = AppOptions.Current.Launch.BarFile;
- }
-
- BarData? bar = null;
- try
- {
- bar = BarData.Load(serviceProvider ?? App.Current.ServiceProvider, path, content);
- }
- catch (Exception e) when (!(e is OutOfMemoryException))
- {
- this.Logger.LogError(e, "Problem loading the bar.");
- }
-
- if (this.barWindow != null)
- {
- this.CloseBar();
- }
-
- this.BarIsLoaded = true;
-
- if (bar != null)
- {
- this.CreateBarWindow(bar);
- bar.ReloadRequired += this.OnBarOnReloadRequired;
- }
-
- return bar;
- }
-
- ///
- /// Loads the bar for the given session. If the user is a member of several, either the last one is used,
- /// or a selection dialog is presented.
- ///
- /// The current session.
- /// Force this community to show.
- public async Task LoadSessionBarAsync(MorphicSession session, string communityId)
- {
- if (this.firstBar && AppOptions.Current.Launch.BarFile != null)
- {
- this.LoadFromBarJson(AppOptions.Current.Launch.BarFile);
- return;
- }
-
- this.Logger.LogInformation($"Loading a bar ({session.Communities.Length} communities)");
-
- UserBar? bar;
-
- UserCommunity? community = null;
- UserBar? userBar = null;
-
- //if (session.Communities.Length == 0)
- //{
- // MessageBox.Show("You are not part of a Morphic community yet.", "Morphic");
- //}
- //else if (session.Communities.Length == 1)
- //{
- // community = session.Communities.First();
- //}
- //else
- //{
- // The user is a member of multiple communities.
-
- //// See if any membership has changed
- //bool changed = session.Communities.Length != lastCommunities.Length
- // || !session.Communities.Select(c => c.Id).OrderBy(id => id)
- // .SequenceEqual(lastCommunities.OrderBy(id => id));
-
- if (/*!changed &&*/ communityId != null)
- {
- community = session.Communities.FirstOrDefault(c => c.Id == communityId);
- }
-
- //if (community == null)
- //{
- // this.Logger.LogInformation("Showing community picker");
-
- // // Load the bars while the picker is shown
- // Dictionary> bars =
- // session.Communities.ToDictionary(c => c.Id, c => session.GetBar(c.Id));
-
- // // Show the picker
- // CommunityPickerWindow picker = new CommunityPickerWindow(session.Communities);
- // bool gotCommunity = picker.ShowDialog() == true;
- // community = gotCommunity ? picker.SelectedCommunity : null;
-
- // if (community != null)
- // {
- // userBar = await bars[community.Id];
- // }
- //}
- //}
-
- if (community != null)
- {
- userBar ??= await session.GetBar(community.Id);
-
- this.Logger.LogInformation($"Showing bar for community {community.Id} {community.Name}");
- string barJson = this.GetUserBarJson(userBar);
- BarData? barData = this.LoadFromBarJson(userBar.Id, barJson);
- if (barData != null)
- {
- barData.CommunityId = community.Id;
- }
-
- AppOptions.Current.LastCommunity = community?.Id;
- }
- else
- {
- // if the community could not be found, show the Basic MorphicBar instead
- this.LoadBasicMorphicBar();
-
- AppOptions.Current.LastCommunity = null;
- }
-
- AppOptions.Current.Communities = session.Communities.Select(c => c.Id).ToArray();
- }
-
- ///
- /// Gets the json for a , so it can be loaded with a better deserialiser.
- ///
- /// Bar data object from Morphic.Core
- private string GetUserBarJson(UserBar userBar)
- {
- // Serialise the bar data so it can be loaded with a better deserialiser.
- SystemJson.JsonSerializerOptions serializerOptions = new SystemJson.JsonSerializerOptions();
- serializerOptions.Converters.Add(new JsonElementInferredTypeConverter());
- serializerOptions.Converters.Add(
- new SystemJson.Serialization.JsonStringEnumConverter(SystemJson.JsonNamingPolicy.CamelCase));
- string barJson = SystemJson.JsonSerializer.Serialize(userBar, serializerOptions);
-
- // Dump to a file, for debugging.
- string barFile = AppPaths.GetConfigFile("last-bar.json5");
- File.WriteAllText(barFile, barJson);
-
- return barJson;
- }
-
- #endregion
-
- #region INotifyPropertyChanged
- public event PropertyChangedEventHandler? PropertyChanged;
-
- protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
- {
- this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
- }
- #endregion
-
- }
-
- public class BarEventArgs : EventArgs
- {
- public BarEventArgs(PrimaryBarWindow window)
- {
- this.Window = window;
- this.Bar = this.Window.Bar;
- }
-
- public BarData Bar { get; private set; }
- public PrimaryBarWindow Window { get; private set; }
-
- }
-}
diff --git a/Morphic.Client/Bar/Data/Actions/ApplicationAction.cs b/Morphic.Client/Bar/Data/Actions/ApplicationAction.cs
deleted file mode 100644
index 5f31b030..00000000
--- a/Morphic.Client/Bar/Data/Actions/ApplicationAction.cs
+++ /dev/null
@@ -1,356 +0,0 @@
-// ApplicationAction.cs: A bar action that starts an application.
-//
-// Copyright 2020 Raising the Floor - International
-//
-// Licensed under the New BSD license. You may not use this file except in
-// compliance with this License.
-//
-// You may obtain a copy of the License at
-// https://github.com/GPII/universal/blob/master/LICENSE.txt
-
-using Morphic.Windows.Native.WindowsCom;
-
-namespace Morphic.Client.Bar.Data.Actions
-{
- using Microsoft.Extensions.Logging;
- using Microsoft.Win32;
- using Morphic.Core;
- using Newtonsoft.Json;
- using System;
- using System.Collections.Generic;
- using System.Diagnostics;
- using System.IO;
- using System.Linq;
- using System.Threading.Tasks;
- using System.Windows;
- using System.Windows.Input;
- using System.Windows.Interop;
- using System.Windows.Media;
- using System.Windows.Media.Imaging;
-
- ///
- /// Action to start an application.
- ///
- [JsonTypeName("application")]
- public class ApplicationAction : BarAction
- {
- private string? exeNameValue;
-
- ///
- /// The actual path to the executable.
- ///
- public string? AppPath { get; set; }
-
- public override ImageSource? DefaultImageSource
- {
- get
- {
- if (this.AppPath != null)
- {
- return Imaging.CreateBitmapSourceFromHIcon(
- System.Drawing.Icon.ExtractAssociatedIcon(this.AppPath).Handle,
- Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
- }
- else
- {
- return null;
- }
- }
- }
-
- ///
- /// Start a default application. This value points to an entry in default-apps.json5.
- ///
- [JsonProperty("default")]
- public string? DefaultAppName { get; set; }
-
- public BarAction? DefaultApp { get; private set; }
-
- ///
- /// Invoke the value in `exe` as-is, via the shell (explorer). Don't resolve the path.
- ///
- [JsonProperty("shell")]
- public bool Shell { get; set; }
-
- ///
- /// This is a windows store app. The value of `exe` is the Application User Model ID of the app.
- /// For example, `Microsoft.WindowsCalculator_8wekyb3d8bbwe!App`
- ///
- [JsonProperty("appx")]
- public bool AppX { get; set; }
-
- ///
- /// true to always start a new instance. false to activate an existing instance.
- ///
- [JsonProperty("newInstance")]
- public bool NewInstance { get; set; }
-
- ///
- /// The initial state of the window.
- ///
- [JsonProperty("windowStyle")]
- public ProcessWindowStyle WindowStyle { get; set; } = ProcessWindowStyle.Normal;
-
-
- ///
- /// Executable name, or the full path to it. If also providing arguments, surround the executable path with quotes.
- ///
- [JsonProperty("exe", Required = Required.Always)]
- public string ExeName
- {
- get => this.exeNameValue ?? string.Empty;
- set
- {
- this.exeNameValue = value;
-
- // A url like "mailto:"
- bool isUrl = this.exeNameValue.Length > 3 && this.exeNameValue.EndsWith(':');
- if (isUrl)
- {
- this.Shell = true;
- }
-
- if (this.exeNameValue.StartsWith("appx:", StringComparison.InvariantCultureIgnoreCase))
- {
- this.AppX = true;
- this.exeNameValue = this.exeNameValue.Substring(5);
- }
-
- if (this.Shell || this.AppX || this.exeNameValue.Length == 0)
- {
- this.AppPath = null;
- }
- else
- {
- if (this.ExeName.StartsWith('"'))
- {
- int nextQuote = this.exeNameValue.IndexOf('"', 1);
- if (nextQuote < 0)
- {
- App.Current.Logger.LogWarning($"Executable path [{this.ExeName}] has mismatching quote");
- this.AppPath = this.ExeName.Substring(1);
- }
- else
- {
- this.AppPath = this.ExeName.Substring(1, nextQuote - 1);
- this.ArgumentsString = this.ExeName.Substring(nextQuote + 1).Trim();
- }
- }
-
- this.AppPath = this.ResolveAppPath(this.exeNameValue);
- App.Current.Logger.LogDebug($"Resolved exe file '{this.exeNameValue}' to '{this.AppPath ?? "(null)"}'");
- }
-
- this.IsAvailable = this.AppPath != null;
- }
- }
-
- ///
- /// Array of arguments.
- ///
- [JsonProperty("args")]
- public List Arguments { get; set; } = new List();
-
- ///
- /// The arguments, if they're passed after the exe name.
- ///
- public string? ArgumentsString { get; set; }
-
- ///
- /// Environment variables to set
- ///
- [JsonProperty("env")]
- public Dictionary EnvironmentVariables { get; set; } = new Dictionary();
-
- ///
- /// Resolves the path of an executable, by looking in the "App Paths" registry key or the PATH environment.
- /// If a full path is provided, and it doesn't exist, then the path for the file name alone is resolved.
- ///
- /// Environment variables in the file path are also resolved.
- ///
- /// The `exeName` input value.
- /// Full path to the executable if found, or null.
- private string? ResolveAppPath(string exeName)
- {
- string file = Environment.ExpandEnvironmentVariables(exeName);
- string ext = Path.GetExtension(file).ToLower();
- string withExe, withoutExe;
-
- // Try with the .exe extension first, then without, but if the file ends with a '.', then try that first.
- // (similar to CreateProcess)
- if (file.EndsWith("."))
- {
- string? result1 = this.ResolveAppPathAsIs(file);
- if (result1 != null)
- {
- return result1;
- }
-
- withExe = Path.ChangeExtension(file, ".exe");
- withoutExe = Path.ChangeExtension(file, null);
- }
- else if (ext == ".exe")
- {
- withExe = file;
- withoutExe = Path.ChangeExtension(file, null);
- }
- else
- {
- withExe = file + ".exe";
- withoutExe = file;
- }
-
- string? result = this.ResolveAppPathAsIs(withExe);
- if (result != null)
- {
- return result;
- }
- else
- {
- return this.ResolveAppPathAsIs(withoutExe);
- }
- }
-
- ///
- /// Called by ResolveAppPath to perform the actual resolving work.
- ///
- ///
- /// Full path to the executable if found, or null.
- private string? ResolveAppPathAsIs(string file)
- {
- string? fullPath = null;
-
- if (Path.IsPathRooted(file))
- {
- if (File.Exists(file))
- {
- fullPath = file;
- }
-
- file = Path.GetFileName(file);
- }
-
- return fullPath ?? this.SearchAppPaths(file) ?? this.SearchPathEnv(file);
- }
-
- ///
- /// Searches the directories in the PATH environment variable.
- ///
- ///
- /// null if not found.
- private string? SearchPathEnv(string file)
- {
- // Alternative: https://docs.microsoft.com/en-us/windows/win32/api/shlwapi/nf-shlwapi-pathfindonpathw
- return Environment.GetEnvironmentVariable("PATH")?
- .Split(Path.PathSeparator)
- .Select(p => Path.Combine(p, file))
- .FirstOrDefault(File.Exists);
- }
-
- ///
- /// Searches SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths (in both HKCU and HKLM) for an executable.
- ///
- ///
- /// null if not found.
- private string? SearchAppPaths(string file)
- {
- string? fullPath = null;
-
- // Look in *\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths
- foreach (RegistryKey rootKey in new[] {Registry.CurrentUser, Registry.LocalMachine})
- {
- RegistryKey? key =
- rootKey.OpenSubKey($@"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\{file}");
- if (key != null)
- {
- fullPath = key.GetValue(string.Empty) as string;
- if (fullPath != null)
- {
- break;
- }
- }
- }
-
- return fullPath;
- }
-
- protected override Task InvokeAsyncImpl(string? source = null, bool? toggleState = null)
- {
- if (this.DefaultApp != null && string.IsNullOrEmpty(this.ExeName))
- {
- return this.DefaultApp.InvokeAsync(source);
- }
-
- if (this.AppX)
- {
- var pid = Appx.Start(this.ExeName);
- return Task.FromResult(pid > 0 ? IMorphicResult.SuccessResult : IMorphicResult.ErrorResult);
- }
-
- if (!this.NewInstance && (Keyboard.Modifiers & ModifierKeys.Shift) != ModifierKeys.Shift)
- {
- bool activated = this.ActivateInstance().IsSuccess;
- if (activated)
- {
- return Task.FromResult(IMorphicResult.SuccessResult);
- }
- }
-
- ProcessStartInfo startInfo = new ProcessStartInfo()
- {
- FileName = this.AppPath ?? this.ExeName,
- ErrorDialog = true,
- // This is required to start taskmgr (the UAC prompt)
- UseShellExecute = true,
- WindowStyle = this.WindowStyle
-
- };
-
- if (this.Shell)
- {
- startInfo.UseShellExecute = true;
- }
-
- if (this.Arguments.Count > 0)
- {
- foreach (string argument in this.Arguments)
- {
- startInfo.ArgumentList.Add(this.ResolveString(argument, source));
- }
- }
- else
- {
- startInfo.Arguments = this.ResolveString(this.ArgumentsString, source);
- }
-
- foreach (var (key, value) in this.EnvironmentVariables)
- {
- startInfo.EnvironmentVariables.Add(key, this.ResolveString(value, source));
- }
-
- Process? process = Process.Start(startInfo);
-
- return Task.FromResult(process != null ? IMorphicResult.SuccessResult : IMorphicResult.ErrorResult);
- }
-
- ///
- /// Activates a running instance of the application.
- ///
- /// false if it could not be done.
- ///
- private IMorphicResult ActivateInstance()
- {
- bool success = false;
- string? friendlyName = Path.GetFileNameWithoutExtension(this.AppPath);
- if (!string.IsNullOrEmpty(friendlyName))
- {
- success = Process.GetProcessesByName(friendlyName)
- .Where(p => p.MainWindowHandle != IntPtr.Zero)
- .OrderByDescending(p => p.StartTime)
- .Any(process => WinApi.ActivateWindow(process.MainWindowHandle));
- }
-
- return success ? IMorphicResult.SuccessResult : IMorphicResult.ErrorResult;
- }
- }
-}
diff --git a/Morphic.Client/Bar/Data/Actions/BarAction.cs b/Morphic.Client/Bar/Data/Actions/BarAction.cs
deleted file mode 100644
index f5b725d4..00000000
--- a/Morphic.Client/Bar/Data/Actions/BarAction.cs
+++ /dev/null
@@ -1,342 +0,0 @@
-// BarAction.cs: Actions performed by bar items.
-//
-// Copyright 2020 Raising the Floor - International
-//
-// Licensed under the New BSD license. You may not use this file except in
-// compliance with this License.
-//
-// You may obtain a copy of the License at
-// https://github.com/GPII/universal/blob/master/LICENSE.txt
-
-namespace Morphic.Client.Bar.Data.Actions
-{
- using CountlySDK;
- using Microsoft.Extensions.Logging;
- using Morphic.Core;
- using Newtonsoft.Json;
- using Newtonsoft.Json.Linq;
- using System;
- using System.Collections.Generic;
- using System.Diagnostics;
- using System.Linq;
- using System.Net.WebSockets;
- using System.Text;
- using System.Threading;
- using System.Threading.Tasks;
- using System.Windows.Forms;
- using System.Windows.Media;
-
- ///
- /// An action for a bar item.
- ///
- [JsonObject(MemberSerialization.OptIn)]
- [JsonConverter(typeof(TypedJsonConverter), "kind", "shellExec")]
- public abstract class BarAction
- {
- [JsonProperty("identifier")]
- public string Id { get; set; } = string.Empty;
-
- ///
- /// Called by Invoke to perform the implementation-specific action invocation.
- ///
- /// Button ID, for multi-button bar items.
- /// New state, if the button is a toggle.
- ///
- protected abstract Task InvokeAsyncImpl(string? source = null, bool? toggleState = null);
-
- ///
- /// Invokes the action.
- ///
- /// Button ID, for multi-button bar items.
- /// New state, if the button is a toggle.
- ///
- public async Task InvokeAsync(string? source = null, bool? toggleState = null)
- {
- IMorphicResult result;
- try
- {
- try
- {
- result = await this.InvokeAsyncImpl(source, toggleState);
- }
- catch (Exception e) when (!(e is ActionException || e is OutOfMemoryException))
- {
- throw new ActionException(e.Message, e);
- }
- }
- catch (ActionException e)
- {
- App.Current.Logger.LogError(e, $"Error while invoking action for bar {this.Id} {this}");
-
- if (e.UserMessage != null)
- {
- MessageBox.Show($"There was a problem performing the action:\n\n{e.UserMessage}",
- "Custom MorphicBar", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
- }
-
- result = IMorphicResult.ErrorResult;
- }
- finally
- {
- // record telemetry data for this action
- await this.SendTelemetryForBarAction(source, toggleState);
- }
-
- return result;
- }
-
- // NOTE: we should refactor this functionality to functions attached to each button (similar to how action callbacks are invoked)
- private async Task SendTelemetryForBarAction(string? source = null, bool? toggleState = null)
- {
- // handle actions which must be filted by id
- switch (this.Id)
- {
- case "magnify":
- {
- if (source == "on")
- {
- await Countly.RecordEvent("magnifierShow");
- }
- else if (source == "off")
- {
- await Countly.RecordEvent("magnifierHide");
- }
- }
- break;
- case "read-aloud":
- {
- if (source == "play")
- {
- await Countly.RecordEvent("readSelectedPlay");
- }
- else if (source == "stop")
- {
- await Countly.RecordEvent("readSelectedStop");
- break;
- }
- }
- break;
- case "":
- switch (source)
- {
- case "com.microsoft.windows.colorFilters/enabled":
- {
- if (toggleState == true)
- {
- await Countly.RecordEvent("colorFiltersOn");
- return;
- }
- else
- {
- await Countly.RecordEvent("colorFiltersOff");
- return;
- }
- }
- break;
- case "com.microsoft.windows.highContrast/enabled":
- {
- if (toggleState == true)
- {
- await Countly.RecordEvent("highContrastOn");
- return;
- }
- else
- {
- await Countly.RecordEvent("highContrastOff");
- return;
- }
- }
- break;
- case "com.microsoft.windows.nightMode/enabled":
- {
- if (toggleState == true)
- {
- await Countly.RecordEvent("nightModeOn");
- return;
- }
- else
- {
- await Countly.RecordEvent("nightModeOff");
- return;
- }
- }
- break;
- case "copy":
- {
- await Countly.RecordEvent("screenSnip");
- }
- break;
- case "dark-mode":
- {
- if (toggleState == true)
- {
- await Countly.RecordEvent("darkModeOn");
- }
- else
- {
- await Countly.RecordEvent("darkModeOff");
- }
- }
- break;
- case null:
- // no tags; this is the Morphie button or another custom element with no known tags
- break;
- default:
- // we do not understand this action type (for telemetry logging purposes)
- Debug.Assert(false, "Unknown Action ID (missing telemetry hooks)");
- break;
- }
- break;
- case "screen-zoom":
- // this action type's telemetry is logged elsewhere
- break;
- default:
- // we do not understand this action type (for telemetry logging purposes)
- Debug.Assert(false, "Unknown Action ID (missing telemetry hooks)");
- break;
- }
- }
-
- ///
- /// Resolves "{identifiers}" in a string with its value.
- ///
- ///
- ///
- /// null if arg is null
- protected string? ResolveString(string? arg, string? source)
- {
- // Today, there is only "{button}".
- return arg?.Replace("{button}", source ?? string.Empty);
- }
-
- public virtual Uri? DefaultImageUri { get; }
- public virtual ImageSource? DefaultImageSource { get; }
- public virtual bool IsAvailable { get; protected set; } = true;
-
- public virtual void Deserialized(BarData barData)
- {
- }
- }
-
- [JsonTypeName("null")]
- public class NoOpAction : BarAction
- {
- protected override Task InvokeAsyncImpl(string? source = null, bool? toggleState = null)
- {
- return Task.FromResult(IMorphicResult.SuccessResult);
- }
- }
-
- [JsonTypeName("internal")]
- public class InternalAction : BarAction
- {
- [JsonProperty("function", Required = Required.Always)]
- public string? FunctionName { get; set; }
-
- [JsonProperty("args")]
- public Dictionary Arguments { get; set; } = new Dictionary();
-
- public string? TelemetryEventName { get; set; }
-
- protected override Task InvokeAsyncImpl(string? source = null, bool? toggleState = null)
- {
- try
- {
- if (this.FunctionName == null)
- {
- return Task.FromResult(IMorphicResult.SuccessResult);
- }
-
- Dictionary resolvedArgs = this.Arguments
- .ToDictionary(kv => kv.Key, kv => this.ResolveString(kv.Value, source) ?? string.Empty);
-
- resolvedArgs.Add("state", toggleState == true ? "on" : "off");
-
- return InternalFunctions.Default.InvokeFunction(this.FunctionName, resolvedArgs);
- }
- finally
- {
- if (this.TelemetryEventName != null)
- {
- Countly.RecordEvent(this.TelemetryEventName!);
- }
- }
- }
- }
-
- [JsonTypeName("gpii")]
- public class GpiiAction : BarAction
- {
- [JsonProperty("data", Required = Required.Always)]
- public JObject RequestObject { get; set; } = null!;
-
- protected override async Task InvokeAsyncImpl(string? source = null, bool? toggleState = null)
- {
- ClientWebSocket socket = new ClientWebSocket();
- CancellationTokenSource cancel = new CancellationTokenSource();
- await socket.ConnectAsync(new Uri("ws://localhost:8081/pspChannel"), cancel.Token);
-
- string requestString = this.RequestObject.ToString();
- byte[] bytes = Encoding.UTF8.GetBytes(requestString);
-
- ArraySegment sendBuffer = new ArraySegment(bytes);
- await socket.SendAsync(sendBuffer, WebSocketMessageType.Text, true, cancel.Token);
-
- return IMorphicResult.SuccessResult;
- }
- }
-
- [JsonTypeName("shellExec")]
- public class ShellExecuteAction : BarAction
- {
- [JsonProperty("run")]
- public string? ShellCommand { get; set; }
-
- protected override Task