mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-09-12 07:54:49 +08:00
Add taskbar icon to desktop app (#1119)
This commit is contained in:
parent
88f3f8fc97
commit
e5092e4c99
8 changed files with 310 additions and 135 deletions
|
@ -111,7 +111,7 @@ defmodule AppBuilder.MacOS do
|
||||||
|
|
||||||
File.mkdir_p!("tmp")
|
File.mkdir_p!("tmp")
|
||||||
launcher_src_path = "tmp/Launcher.swift"
|
launcher_src_path = "tmp/Launcher.swift"
|
||||||
File.write!(launcher_src_path, launcher(additional_paths))
|
File.write!(launcher_src_path, launcher(release, additional_paths))
|
||||||
launcher_path = Path.join([app_bundle_path, "Contents", "MacOS", app_name <> "Launcher"])
|
launcher_path = Path.join([app_bundle_path, "Contents", "MacOS", app_name <> "Launcher"])
|
||||||
File.mkdir_p!(Path.dirname(launcher_path))
|
File.mkdir_p!(Path.dirname(launcher_path))
|
||||||
|
|
||||||
|
@ -133,7 +133,7 @@ defmodule AppBuilder.MacOS do
|
||||||
release
|
release
|
||||||
end
|
end
|
||||||
|
|
||||||
defp launcher(additional_paths) do
|
defp launcher(release, additional_paths) do
|
||||||
additional_paths = Enum.map_join(additional_paths, ":", &"\\(resourcePath)#{&1}")
|
additional_paths = Enum.map_join(additional_paths, ":", &"\\(resourcePath)#{&1}")
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
@ -149,7 +149,7 @@ defmodule AppBuilder.MacOS do
|
||||||
let logFile = FileHandle(forUpdatingAtPath: logPath)
|
let logFile = FileHandle(forUpdatingAtPath: logPath)
|
||||||
logFile?.seekToEndOfFile()
|
logFile?.seekToEndOfFile()
|
||||||
|
|
||||||
let releaseScriptPath = Bundle.main.path(forResource: "rel/bin/mac_app", ofType: "")!
|
let releaseScriptPath = Bundle.main.path(forResource: "rel/bin/#{release.name}", ofType: "")!
|
||||||
|
|
||||||
let resourcePath = Bundle.main.resourcePath ?? ""
|
let resourcePath = Bundle.main.resourcePath ?? ""
|
||||||
let additionalPaths = "#{additional_paths}"
|
let additionalPaths = "#{additional_paths}"
|
||||||
|
|
|
@ -4,6 +4,29 @@ defmodule AppBuilder.Windows do
|
||||||
import AppBuilder.Utils
|
import AppBuilder.Utils
|
||||||
require EEx
|
require EEx
|
||||||
|
|
||||||
|
def __send_events__(server, input)
|
||||||
|
|
||||||
|
def __send_events__(server, "new_file") do
|
||||||
|
send(server, {:new_file, ''})
|
||||||
|
end
|
||||||
|
|
||||||
|
def __send_events__(server, "reopen_app") do
|
||||||
|
send(server, {:reopen_app, ''})
|
||||||
|
end
|
||||||
|
|
||||||
|
def __send_events__(server, "open_url:" <> url) do
|
||||||
|
send(server, {:open_url, String.to_charlist(url)})
|
||||||
|
end
|
||||||
|
|
||||||
|
def __send_events__(server, "open_file:" <> path) do
|
||||||
|
path =
|
||||||
|
path
|
||||||
|
|> String.replace("\\", "/")
|
||||||
|
|> String.to_charlist()
|
||||||
|
|
||||||
|
send(server, {:open_file, path})
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Creates a Windows installer.
|
Creates a Windows installer.
|
||||||
"""
|
"""
|
||||||
|
@ -36,6 +59,9 @@ defmodule AppBuilder.Windows do
|
||||||
erl_exe = Path.join([tmp_dir, "rel", "erts-#{release.erts_version}", "bin", "erl.exe"])
|
erl_exe = Path.join([tmp_dir, "rel", "erts-#{release.erts_version}", "bin", "erl.exe"])
|
||||||
rcedit_path = ensure_rcedit()
|
rcedit_path = ensure_rcedit()
|
||||||
cmd!(rcedit_path, ["--set-icon", app_icon_path, erl_exe])
|
cmd!(rcedit_path, ["--set-icon", app_icon_path, erl_exe])
|
||||||
|
manifest_path = Path.join(tmp_dir, "manifest.xml")
|
||||||
|
File.write!(manifest_path, manifest())
|
||||||
|
cmd!(rcedit_path, ["--application-manifest", manifest_path, erl_exe])
|
||||||
|
|
||||||
File.write!(Path.join(tmp_dir, "#{app_name}.vbs"), launcher_vbs(release, options))
|
File.write!(Path.join(tmp_dir, "#{app_name}.vbs"), launcher_vbs(release, options))
|
||||||
nsi_path = Path.join(tmp_dir, "#{app_name}.nsi")
|
nsi_path = Path.join(tmp_dir, "#{app_name}.nsi")
|
||||||
|
@ -51,6 +77,21 @@ defmodule AppBuilder.Windows do
|
||||||
release
|
release
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# https://docs.microsoft.com/en-us/windows/win32/hidpi/setting-the-default-dpi-awareness-for-a-process
|
||||||
|
defp manifest do
|
||||||
|
"""
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<asmv3:application>
|
||||||
|
<asmv3:windowsSettings>
|
||||||
|
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
|
||||||
|
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||||
|
</asmv3:windowsSettings>
|
||||||
|
</asmv3:application>
|
||||||
|
</assembly>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
code = """
|
code = """
|
||||||
<%
|
<%
|
||||||
app_name = Keyword.fetch!(options, :name)
|
app_name = Keyword.fetch!(options, :name)
|
||||||
|
@ -112,7 +153,7 @@ defmodule AppBuilder.Windows do
|
||||||
<% end %>
|
<% end %>
|
||||||
WriteRegStr HKCR "<%= app_name %>.<%= type.name %>" "" "<%= type.name %>"
|
WriteRegStr HKCR "<%= app_name %>.<%= type.name %>" "" "<%= type.name %>"
|
||||||
WriteRegStr HKCR "<%= app_name %>.<%= type.name %>\\DefaultIcon" "" "$INSTDIR\\app_icon.ico"
|
WriteRegStr HKCR "<%= app_name %>.<%= type.name %>\\DefaultIcon" "" "$INSTDIR\\app_icon.ico"
|
||||||
WriteRegStr HKCR "<%= app_name %>.<%= type.name %>\\shell\\open\\command" "" '$WINDIR\\system32\\wscript.exe "$INSTDIR\\<%= app_name %>.vbs" "%1"'
|
WriteRegStr HKCR "<%= app_name %>.<%= type.name %>\\shell\\open\\command" "" '$WINDIR\\system32\\wscript.exe "$INSTDIR\\<%= app_name %>.vbs" "open_file:%1"'
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= for url_scheme <- url_schemes do %>
|
<%= for url_scheme <- url_schemes do %>
|
||||||
|
@ -122,7 +163,7 @@ defmodule AppBuilder.Windows do
|
||||||
WriteRegStr HKCR "<%= url_scheme %>" "URL Protocol" ""
|
WriteRegStr HKCR "<%= url_scheme %>" "URL Protocol" ""
|
||||||
WriteRegStr HKCR "<%= url_scheme %>\\shell" "" ""
|
WriteRegStr HKCR "<%= url_scheme %>\\shell" "" ""
|
||||||
WriteRegStr HKCR "<%= url_scheme %>\\shell\\open" "" ""
|
WriteRegStr HKCR "<%= url_scheme %>\\shell\\open" "" ""
|
||||||
WriteRegStr HKCR "<%= url_scheme %>\\shell\\open\\command" "" '$WINDIR\\system32\\wscript.exe "$INSTDIR\\<%= app_name %>.vbs" "%1"'
|
WriteRegStr HKCR "<%= url_scheme %>\\shell\\open\\command" "" '$WINDIR\\system32\\wscript.exe "$INSTDIR\\<%= app_name %>.vbs" "open_url:%1"'
|
||||||
<% end %>
|
<% end %>
|
||||||
SectionEnd
|
SectionEnd
|
||||||
|
|
||||||
|
@ -143,16 +184,10 @@ defmodule AppBuilder.Windows do
|
||||||
<%
|
<%
|
||||||
app_name = Keyword.fetch!(options, :name)
|
app_name = Keyword.fetch!(options, :name)
|
||||||
module = Keyword.fetch!(options, :module)
|
module = Keyword.fetch!(options, :module)
|
||||||
%>
|
%>' This vbs script avoids a flashing cmd window when launching the release bat file
|
||||||
' This vbs script avoids a flashing cmd window when launching the release bat file
|
|
||||||
|
|
||||||
path = Left(Wscript.ScriptFullName, Len(Wscript.ScriptFullName) - Len(Wscript.ScriptName)) & "rel\bin\<%= release.name %>.bat"
|
root = Left(Wscript.ScriptFullName, Len(Wscript.ScriptFullName) - Len(Wscript.ScriptName))
|
||||||
|
script = root & "rel\bin\<%= release.name %>.bat"
|
||||||
If WScript.Arguments.Count > 0 Then
|
|
||||||
url = WScript.Arguments(0)
|
|
||||||
Else
|
|
||||||
url = ""
|
|
||||||
End If
|
|
||||||
|
|
||||||
Set shell = CreateObject("WScript.Shell")
|
Set shell = CreateObject("WScript.Shell")
|
||||||
|
|
||||||
|
@ -167,16 +202,35 @@ defmodule AppBuilder.Windows do
|
||||||
Set env = shell.Environment("Process")
|
Set env = shell.Environment("Process")
|
||||||
env("PATH") = ".\rel\vendor\elixir\bin;.\rel\erts-<%= release.erts_version %>\bin;" & env("PATH")
|
env("PATH") = ".\rel\vendor\elixir\bin;.\rel\erts-<%= release.erts_version %>\bin;" & env("PATH")
|
||||||
|
|
||||||
' > bin/release rpc "mod.windows_connected(url)"
|
If WScript.Arguments.Count > 0 Then
|
||||||
|
input = WScript.Arguments(0)
|
||||||
|
Else
|
||||||
|
input = "reopen_app"
|
||||||
|
End If
|
||||||
|
|
||||||
|
' Below, we're basically doing:
|
||||||
'
|
'
|
||||||
' We send the URL through IO, as opposed through the rpc expression, to avoid RCE.
|
' $ bin/release rpc 'AppBuilder.Windows.__send_events__(MyApp, input)'
|
||||||
cmd = "echo """ & url & """ | """ & path & """ rpc <%= inspect(module) %>.windows_connected(IO.read(:line))"
|
'
|
||||||
|
' We send the input through IO, as opposed using the rpc expression, to avoid RCE.
|
||||||
|
cmd = "echo " & input & " | \""" & script & \""" rpc ""AppBuilder.Windows.__send_events__(<%= inspect(module) %>, String.trim(IO.read(:line)))\"""
|
||||||
code = shell.Run("cmd /c " & cmd, 0)
|
code = shell.Run("cmd /c " & cmd, 0)
|
||||||
|
|
||||||
' > bin/release start
|
' Below, we're basically doing:
|
||||||
cmd = """" & path & """ start"
|
'
|
||||||
env("<%= String.upcase(app_name) <> "_URL" %>") = url
|
' $ bin/release start
|
||||||
code = shell.Run("cmd /c " & cmd & " >> .\Logs\<%= app_name %>.log 2>&1", 0)
|
'
|
||||||
|
' We send the input through the environment variable as we can't easily access argv
|
||||||
|
' when booting through the release script.
|
||||||
|
|
||||||
|
If WScript.Arguments.Count > 0 Then
|
||||||
|
env("APP_BUILDER_INPUT") = WScript.Arguments(0)
|
||||||
|
Else
|
||||||
|
env("APP_BUILDER_INPUT") = "new_file"
|
||||||
|
End If
|
||||||
|
|
||||||
|
cmd = \"""" & script & \""" start"
|
||||||
|
code = shell.Run("cmd /c " & cmd & " >> " & root & "\Logs\<%= app_name %>.log 2>&1", 0)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
EEx.function_from_string(:defp, :launcher_vbs, code, [:release, :options], trim: true)
|
EEx.function_from_string(:defp, :launcher_vbs, code, [:release, :options], trim: true)
|
||||||
|
@ -203,7 +257,7 @@ defmodule AppBuilder.Windows do
|
||||||
url =
|
url =
|
||||||
"https://download.imagemagick.org/ImageMagick/download/binaries/ImageMagick-7.1.0-portable-Q16-x64.zip"
|
"https://download.imagemagick.org/ImageMagick/download/binaries/ImageMagick-7.1.0-portable-Q16-x64.zip"
|
||||||
|
|
||||||
sha256 = "d7b82e95d2860042c241d9913e14832cf1491f39c4da91286bace39582916dc8"
|
sha256 = "b61a726cea1e3bf395b9aeb323fca062f574fbf8f11f4067f88a0e6b984a1391"
|
||||||
AppBuilder.Utils.ensure_executable(url, sha256, "magick.exe")
|
AppBuilder.Utils.ensure_executable(url, sha256, "magick.exe")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
39
app_builder/lib/app_builder/wx.ex
Normal file
39
app_builder/lib/app_builder/wx.ex
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
defmodule AppBuilder.Wx do
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Subscribe the given `server` to application events.
|
||||||
|
|
||||||
|
The possible events are:
|
||||||
|
|
||||||
|
* `{:new_file, []}`
|
||||||
|
* `{:reopen_app, []}`
|
||||||
|
* `{:open_file, path}`
|
||||||
|
* `{:open_url, url}`
|
||||||
|
|
||||||
|
On macOS, we simply call `:wx.subscribe_events()`. On Windows,
|
||||||
|
we emulate it.
|
||||||
|
"""
|
||||||
|
def subscribe_to_app_events(server) do
|
||||||
|
unless Code.ensure_loaded?(:wx) do
|
||||||
|
Logger.error("""
|
||||||
|
wx is not available.
|
||||||
|
|
||||||
|
Please add it to your extra applications:
|
||||||
|
|
||||||
|
extra_applications: [:wx]
|
||||||
|
""")
|
||||||
|
|
||||||
|
raise "wx is not available"
|
||||||
|
end
|
||||||
|
|
||||||
|
case :os.type() do
|
||||||
|
{:unix, :darwin} ->
|
||||||
|
:wx.subscribe_events()
|
||||||
|
|
||||||
|
{:win32, _} ->
|
||||||
|
input = System.get_env("APP_BUILDER_INPUT", "new_file")
|
||||||
|
AppBuilder.Windows.__send_events__(server, input)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -7,7 +7,14 @@ defmodule AppBuilder.MixProject do
|
||||||
version: "0.1.0",
|
version: "0.1.0",
|
||||||
elixir: "~> 1.13",
|
elixir: "~> 1.13",
|
||||||
start_permanent: Mix.env() == :prod,
|
start_permanent: Mix.env() == :prod,
|
||||||
deps: deps()
|
deps: deps(),
|
||||||
|
|
||||||
|
# Suppress warnings
|
||||||
|
xref: [
|
||||||
|
exclude: [
|
||||||
|
:wx
|
||||||
|
]
|
||||||
|
]
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,96 +1,204 @@
|
||||||
if Mix.target() == :app do
|
if Mix.target() == :app do
|
||||||
defmodule LivebookApp do
|
defmodule WxUtils do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
|
|
||||||
@behaviour :wx_object
|
defmacro wxID_ANY, do: -1
|
||||||
|
defmacro wxID_OPEN, do: 5000
|
||||||
|
defmacro wxID_EXIT, do: 5006
|
||||||
|
defmacro wxID_OSX_HIDE, do: 5250
|
||||||
|
defmacro wxBITMAP_TYPE_PNG, do: 15
|
||||||
|
|
||||||
# https://github.com/erlang/otp/blob/OTP-24.1.2/lib/wx/include/wx.hrl#L1314
|
def os do
|
||||||
@wx_id_exit 5006
|
case :os.type() do
|
||||||
@wx_id_osx_hide 5250
|
{:unix, :darwin} -> :macos
|
||||||
|
{:win32, _} -> :windows
|
||||||
def start_link(_) do
|
end
|
||||||
{:wx_ref, _, _, pid} = :wx_object.start_link(__MODULE__, [], [])
|
|
||||||
{:ok, pid}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def child_spec(init_arg) do
|
def taskbar(title, icon, menu_items) do
|
||||||
%{
|
pid = self()
|
||||||
id: __MODULE__,
|
options = if os() == :macos, do: [iconType: 1], else: []
|
||||||
start: {__MODULE__, :start_link, [init_arg]},
|
|
||||||
restart: :transient
|
# skip keyboard shortcuts
|
||||||
}
|
menu_items =
|
||||||
|
for item <- menu_items do
|
||||||
|
{title, options} = item
|
||||||
|
options = Keyword.delete(options, :key)
|
||||||
|
{title, options}
|
||||||
|
end
|
||||||
|
|
||||||
|
taskbar =
|
||||||
|
:wxTaskBarIcon.new(
|
||||||
|
[
|
||||||
|
createPopupMenu: fn ->
|
||||||
|
menu = menu(menu_items)
|
||||||
|
|
||||||
|
# For some reason, on macOS the menu event must be handled in another process
|
||||||
|
# but on Windows it must be either the same process OR we use the callback.
|
||||||
|
case os() do
|
||||||
|
:macos ->
|
||||||
|
env = :wx.get_env()
|
||||||
|
|
||||||
|
Task.start_link(fn ->
|
||||||
|
:wx.set_env(env)
|
||||||
|
:wxMenu.connect(menu, :command_menu_selected)
|
||||||
|
|
||||||
|
receive do
|
||||||
|
message ->
|
||||||
|
send(pid, message)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
:windows ->
|
||||||
|
:ok =
|
||||||
|
:wxMenu.connect(menu, :command_menu_selected,
|
||||||
|
callback: fn wx, _ ->
|
||||||
|
send(pid, wx)
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
menu
|
||||||
|
end
|
||||||
|
] ++ options
|
||||||
|
)
|
||||||
|
|
||||||
|
:wxTaskBarIcon.setIcon(taskbar, icon, tooltip: title)
|
||||||
|
taskbar
|
||||||
end
|
end
|
||||||
|
|
||||||
def windows_connected(url) do
|
def menu(items) do
|
||||||
url
|
menu = :wxMenu.new()
|
||||||
|> String.trim()
|
|
||||||
|> String.trim_leading("\"")
|
Enum.each(items, fn
|
||||||
|> String.trim_trailing("\"")
|
{title, options} ->
|
||||||
|> windows_to_wx()
|
id = Keyword.get(options, :id, wxID_ANY())
|
||||||
|
|
||||||
|
title =
|
||||||
|
case Keyword.fetch(options, :key) do
|
||||||
|
{:ok, key} ->
|
||||||
|
title <> "\t" <> key
|
||||||
|
|
||||||
|
:error ->
|
||||||
|
title
|
||||||
|
end
|
||||||
|
|
||||||
|
:wxMenu.append(menu, id, title)
|
||||||
|
end)
|
||||||
|
|
||||||
|
menu
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def menubar(app_name, menus) do
|
||||||
|
menubar = :wxMenuBar.new()
|
||||||
|
|
||||||
|
if os() == :macos, do: fixup_macos_menubar(menubar, app_name)
|
||||||
|
|
||||||
|
for {title, menu_items} <- menus do
|
||||||
|
true = :wxMenuBar.append(menubar, menu(menu_items), title)
|
||||||
|
end
|
||||||
|
|
||||||
|
menubar
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fixup_macos_menubar(menubar, app_name) do
|
||||||
|
menu = :wxMenuBar.oSXGetAppleMenu(menubar)
|
||||||
|
|
||||||
|
menu
|
||||||
|
|> :wxMenu.findItem(wxID_OSX_HIDE())
|
||||||
|
|> :wxMenuItem.setItemLabel("Hide #{app_name}\tCtrl+H")
|
||||||
|
|
||||||
|
menu
|
||||||
|
|> :wxMenu.findItem(wxID_EXIT())
|
||||||
|
|> :wxMenuItem.setItemLabel("Quit #{app_name}\tCtrl+Q")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defmodule LivebookApp do
|
||||||
|
@moduledoc false
|
||||||
|
@name __MODULE__
|
||||||
|
|
||||||
|
use GenServer
|
||||||
|
import WxUtils
|
||||||
|
|
||||||
|
def start_link(arg) do
|
||||||
|
GenServer.start_link(__MODULE__, arg, name: @name)
|
||||||
|
end
|
||||||
|
|
||||||
|
taskbar_icon_path = "rel/app/taskbar_icon.png"
|
||||||
|
@external_resource taskbar_icon_path
|
||||||
|
@taskbar_icon File.read!(taskbar_icon_path)
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def init(_) do
|
def init(_) do
|
||||||
app_name = "Livebook"
|
|
||||||
|
|
||||||
true = Process.register(self(), __MODULE__)
|
|
||||||
os = os()
|
os = os()
|
||||||
|
|
||||||
# TODO: on all platforms either add a basic window with some buttons OR a wxwebview
|
|
||||||
size = if os == :macos, do: {0, 0}, else: {100, 100}
|
|
||||||
|
|
||||||
wx = :wx.new()
|
wx = :wx.new()
|
||||||
frame = :wxFrame.new(wx, -1, app_name, size: size)
|
AppBuilder.Wx.subscribe_to_app_events(@name)
|
||||||
:wxFrame.show(frame)
|
|
||||||
|
menu_items = [
|
||||||
|
{"Open Browser", key: "ctrl+o", id: wxID_OPEN()},
|
||||||
|
{"Quit", key: "ctrl+q", id: wxID_EXIT()}
|
||||||
|
]
|
||||||
|
|
||||||
if os == :macos do
|
if os == :macos do
|
||||||
fixup_macos_menubar(frame, app_name)
|
:wxMenuBar.setAutoWindowMenu(false)
|
||||||
|
|
||||||
|
menubar =
|
||||||
|
menubar("Livebook", [
|
||||||
|
{"File", menu_items}
|
||||||
|
])
|
||||||
|
|
||||||
|
:ok = :wxMenuBar.connect(menubar, :command_menu_selected, skip: true)
|
||||||
|
|
||||||
|
# TODO: use :wxMenuBar.macSetCommonMenuBar/1 when OTP 25 is out
|
||||||
|
frame = :wxFrame.new(wx, -1, "", size: {0, 0})
|
||||||
|
:wxFrame.show(frame)
|
||||||
|
:wxFrame.setMenuBar(frame, menubar)
|
||||||
end
|
end
|
||||||
|
|
||||||
:wxFrame.connect(frame, :command_menu_selected, skip: true)
|
# TODO: instead of embedding the icon and copying to tmp, copy the file known location.
|
||||||
:wxFrame.connect(frame, :close_window, skip: true)
|
# It's a bit tricky because it needs to support all the ways of running the app:
|
||||||
|
# 1. MIX_TARGET=app mix phx.server
|
||||||
|
# 2. mix app
|
||||||
|
# 3. mix release app
|
||||||
|
taskbar_icon_path = Path.join(System.tmp_dir!(), "taskbar_icon.png")
|
||||||
|
File.write!(taskbar_icon_path, @taskbar_icon)
|
||||||
|
icon = :wxIcon.new(taskbar_icon_path, type: wxBITMAP_TYPE_PNG())
|
||||||
|
|
||||||
case os do
|
taskbar = taskbar("Livebook", icon, menu_items)
|
||||||
:macos ->
|
|
||||||
:wx.subscribe_events()
|
|
||||||
|
|
||||||
:windows ->
|
if os == :windows do
|
||||||
windows_to_wx(System.get_env("LIVEBOOK_URL") || "")
|
:wxTaskBarIcon.connect(taskbar, :taskbar_left_down,
|
||||||
|
callback: fn _, _ ->
|
||||||
|
open_browser()
|
||||||
|
end
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
state = %{frame: frame}
|
{:ok, nil}
|
||||||
{frame, state}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event({:wx, @wx_id_exit, _, _, _}, state) do
|
def handle_info({:wx, wxID_EXIT(), _, _, _}, _state) do
|
||||||
:init.stop()
|
System.stop(0)
|
||||||
{:stop, :shutdown, state}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event({:wx, _, _, _, {:wxClose, :close_window}}, state) do
|
def handle_info({:wx, wxID_OPEN(), _, _, _}, state) do
|
||||||
:init.stop()
|
open_browser()
|
||||||
{:stop, :shutdown, state}
|
{:noreply, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
# This event is triggered when the application is opened for the first time
|
# This event is triggered when the application is opened for the first time
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info({:new_file, ''}, state) do
|
def handle_info({:new_file, ''}, state) do
|
||||||
Livebook.Utils.browser_open(LivebookWeb.Endpoint.access_url())
|
open_browser()
|
||||||
{:noreply, state}
|
{:noreply, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: investigate "Universal Links" [1], that is, instead of livebook://foo, we have
|
|
||||||
# https://livebook.dev/foo, which means the link works with and without Livebook.app.
|
|
||||||
#
|
|
||||||
# [1] https://developer.apple.com/documentation/xcode/allowing-apps-and-websites-to-link-to-your-content
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info({:open_url, 'livebook://' ++ rest}, state) do
|
def handle_info({:reopen_app, _}, state) do
|
||||||
"https://#{rest}"
|
open_browser()
|
||||||
|> Livebook.Utils.notebook_import_url()
|
|
||||||
|> Livebook.Utils.browser_open()
|
|
||||||
|
|
||||||
{:noreply, state}
|
{:noreply, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -99,69 +207,30 @@ if Mix.target() == :app do
|
||||||
path
|
path
|
||||||
|> List.to_string()
|
|> List.to_string()
|
||||||
|> Livebook.Utils.notebook_open_url()
|
|> Livebook.Utils.notebook_open_url()
|
||||||
|> Livebook.Utils.browser_open()
|
|> open_browser()
|
||||||
|
|
||||||
{:noreply, state}
|
{:noreply, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info({:reopen_app, _}, state) do
|
def handle_info({:open_url, 'livebook://' ++ rest}, state) do
|
||||||
Livebook.Utils.browser_open(LivebookWeb.Endpoint.access_url())
|
"https://#{rest}"
|
||||||
|
|> Livebook.Utils.notebook_import_url()
|
||||||
|
|> open_browser()
|
||||||
|
|
||||||
{:noreply, state}
|
{:noreply, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
# ignore other events
|
if Mix.env() == :dev do
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info(_event, state) do
|
def handle_info(event, state) do
|
||||||
{:noreply, state}
|
IO.inspect(event)
|
||||||
end
|
{:noreply, state}
|
||||||
|
|
||||||
# 1. WxeApp attaches event handler to "Quit" menu item that does nothing (to not accidentally bring
|
|
||||||
# down the VM). Let's create a fresh menu bar without that caveat.
|
|
||||||
# 2. Fix app name
|
|
||||||
defp fixup_macos_menubar(frame, app_name) do
|
|
||||||
menubar = :wxMenuBar.new()
|
|
||||||
:wxFrame.setMenuBar(frame, menubar)
|
|
||||||
|
|
||||||
menu = :wxMenuBar.oSXGetAppleMenu(menubar)
|
|
||||||
|
|
||||||
# without this, for some reason setting the title later will make it non-bold
|
|
||||||
:wxMenu.getTitle(menu)
|
|
||||||
|
|
||||||
# this is useful in dev, not needed when bundled in .app
|
|
||||||
:wxMenu.setTitle(menu, app_name)
|
|
||||||
|
|
||||||
menu
|
|
||||||
|> :wxMenu.findItem(@wx_id_osx_hide)
|
|
||||||
|> :wxMenuItem.setItemLabel("Hide #{app_name}\tCtrl+H")
|
|
||||||
|
|
||||||
menu
|
|
||||||
|> :wxMenu.findItem(@wx_id_exit)
|
|
||||||
|> :wxMenuItem.setItemLabel("Quit #{app_name}\tCtrl+Q")
|
|
||||||
end
|
|
||||||
|
|
||||||
defp os() do
|
|
||||||
case :os.type() do
|
|
||||||
{:unix, :darwin} -> :macos
|
|
||||||
{:win32, _} -> :windows
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp windows_to_wx("") do
|
defp open_browser(url \\ LivebookWeb.Endpoint.access_url()) do
|
||||||
send(__MODULE__, {:new_file, ''})
|
Livebook.Utils.browser_open(url)
|
||||||
end
|
|
||||||
|
|
||||||
defp windows_to_wx("livebook://" <> _ = url) do
|
|
||||||
send(__MODULE__, {:open_url, String.to_charlist(url)})
|
|
||||||
end
|
|
||||||
|
|
||||||
defp windows_to_wx(path) do
|
|
||||||
path =
|
|
||||||
path
|
|
||||||
|> String.replace("\\", "/")
|
|
||||||
|> String.to_charlist()
|
|
||||||
|
|
||||||
send(__MODULE__, {:open_file, path})
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
9
mix.exs
9
mix.exs
|
@ -5,8 +5,8 @@ defmodule Livebook.MixProject do
|
||||||
@version "0.5.2"
|
@version "0.5.2"
|
||||||
@description "Interactive and collaborative code notebooks - made with Phoenix LiveView"
|
@description "Interactive and collaborative code notebooks - made with Phoenix LiveView"
|
||||||
|
|
||||||
@app_elixir_version "1.13.2"
|
@app_elixir_version "1.13.4"
|
||||||
@app_otp_version "24.2"
|
@app_otp_version "24.3"
|
||||||
|
|
||||||
def project do
|
def project do
|
||||||
[
|
[
|
||||||
|
@ -176,7 +176,10 @@ defmodule Livebook.MixProject do
|
||||||
name: "Livebook",
|
name: "Livebook",
|
||||||
version: @version,
|
version: @version,
|
||||||
logo_path: "rel/app/mac-icon.png",
|
logo_path: "rel/app/mac-icon.png",
|
||||||
additional_paths: ["/rel/vendor/bin", "/rel/vendor/elixir/bin"],
|
additional_paths: [
|
||||||
|
"/rel/erts-#{:erlang.system_info(:version)}/bin",
|
||||||
|
"/rel/vendor/elixir/bin"
|
||||||
|
],
|
||||||
url_schemes: ["livebook"],
|
url_schemes: ["livebook"],
|
||||||
document_types: [
|
document_types: [
|
||||||
%{
|
%{
|
||||||
|
|
|
@ -7,7 +7,10 @@ defmodule Standalone do
|
||||||
"""
|
"""
|
||||||
@spec copy_otp(Mix.Release.t(), otp_version :: String.t()) :: Mix.Release.t()
|
@spec copy_otp(Mix.Release.t(), otp_version :: String.t()) :: Mix.Release.t()
|
||||||
def copy_otp(release, otp_version) do
|
def copy_otp(release, otp_version) do
|
||||||
ensure_otp_version(otp_version)
|
if Mix.env() != :dev do
|
||||||
|
ensure_otp_version(otp_version)
|
||||||
|
end
|
||||||
|
|
||||||
{erts_source, otp_bin_dir, otp_lib_dir} = otp_dirs()
|
{erts_source, otp_bin_dir, otp_lib_dir} = otp_dirs()
|
||||||
|
|
||||||
# 1. copy erts/bin
|
# 1. copy erts/bin
|
||||||
|
|
BIN
rel/app/taskbar_icon.png
Normal file
BIN
rel/app/taskbar_icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
Loading…
Add table
Reference in a new issue