antos-frontend/src/core/gui.coffee
2018-09-12 18:28:32 +02:00

491 lines
20 KiB
CoffeeScript

# Copyright 2017-2018 Xuan Sang LE <xsang.le AT gmail DOT com>
# AnTOS Web desktop is is licensed under the GNU General Public
# License v3.0, see the LICENCE file for more information
# This program is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of
# the License, or (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
# You should have received a copy of the GNU General Public License
#along with this program. If not, see https://www.gnu.org/licenses/.
self.OS.GUI =
subwindows: new Object()
dialog: undefined
fullscreen: false
shortcut:
ALT: {}
CTRL: {}
SHIFT: {}
META: {}
SYS_MENU: [
{
text: "",
iconclass: "fa fa-eercast",
dataid: "sys-menu-root",
child: [
{
text: "__(Applications)",
child: [],
dataid: "sys-apps"
iconclass: "fa fa-adn",
onmenuselect: (d) ->
_GUI.launch d.item.data.app
}
],
onmenuselect: (d) ->
return _OS.exit() if d.item.data.dataid is "sys-logout"
return _GUI.toggleFullscreen() if d.item.data.dataid is "os-fullsize"
_GUI.launch d.item.data.app unless d.item.data.dataid
}
]
htmlToScheme: (html, app, parent) ->
scheme = $.parseHTML html
$(app.scheme).remove() if app.scheme
($ parent).append scheme
app.scheme = scheme[0]
riot.mount ($ scheme), { observable: app.observable }
app.main()
app.show()
loadScheme: (path, app, parent) ->
path.asFileHandler().read (x) ->
return null unless x
_GUI.htmlToScheme x, app, parent
#, (e, s) ->
# _courrier.osfail "Cannot load scheme file: #{path} for #{app.name} (#{app.pid})", e, s
clearTheme: () ->
$ "head link#ostheme"
.attr "href", ""
loadTheme: (name, force) ->
_GUI.clearTheme() if force
path = "resources/themes/#{name}/#{name}.css"
$ "head link#ostheme"
.attr "href", path
pushServices: (srvs) ->
return unless srvs.length > 0
_courrier.observable.one "srvroutineready", () ->
srvs.splice 0, 1
_GUI.pushServices srvs
_GUI.pushService srvs[0]
openDialog: (d, f, title, data) ->
if _GUI.dialog
_GUI.dialog.show()
return
if not _GUI.subwindows[d]
ex = _API.throwe "Dialog"
return _courrier.oserror __("Dialog {0} not found", d), ex, null
_GUI.dialog = new _GUI.subwindows[d]()
_GUI.dialog.parent = _GUI
_GUI.dialog.handler = f
_GUI.dialog.pid = -1
_GUI.dialog.data = data
_GUI.dialog.title = title
_GUI.dialog.init()
pushService: (ph) ->
arr = ph.split "/"
srv = arr[1]
app = arr[0]
return _PM.createProcess srv, _OS.APP[srv] if _OS.APP[srv]
_GUI.loadApp app,
(a) ->
return _PM.createProcess srv, _OS.APP[srv] if _OS.APP[srv]
(e, s) ->
_courrier.trigger "srvroutineready", srv
_courrier.osfail __("Cannot read service script: {0}", srv), e, s
appsByMime: (mime) ->
metas = ( v for k, v of _OS.setting.system.packages when v and v.app )
mimes = ( m.mimes for m in metas when m)
apps = []
# search app by mimes
f = ( arr, idx ) ->
try
arr.filter (m, i) ->
if mime.match (new RegExp m, "g")
return false if (apps.indexOf metas[idx]) >= 0
apps.push metas[idx]
return false
return false
catch e
_courrier.osfail __("Error find app by mimes {0}", mime), e, mime
( f m, i if m ) for m, i in mimes
return apps
appsWithServices: () ->
o = {}
o[k] = v for k, v of _OS.setting.system.packages when v and v.services and v.services.length > 0
o
openWith: (it) ->
return unless it
return _GUI.launch it.app if it.type is "app" and it.app
return _courrier.osinfo __("Application {0} is not executable", it.text) if it.type is "app"
apps = _GUI.appsByMime ( if it.type is "dir" then "dir" else it.mime )
return _courrier.osinfo __("No application available to open {0}", it.filename) if apps.length is 0
return _GUI.launch apps[0].app, [it.path] if apps.length is 1
list = ( { text: e.app, icon: e.icon, iconclass: e.iconclass } for e in apps )
_GUI.openDialog "SelectionDialog", ( d ) ->
_GUI.launch d.text, [it.path]
, __("Open with"), list
forceLaunch: (app, args) ->
console.warn "This method is used for developing only, please use the launch method instead"
_GUI.unloadApp app
_GUI.launch app, args
unloadApp: (app) ->
_PM.killAll app, true
($ _OS.APP[app].style).remove() if _OS.APP[app] and _OS.APP[app].style
delete _OS.APP[app]
loadApp: (app, ok, err) ->
path = "os://packages/#{app}"
path = _OS.setting.system.packages[app].path if _OS.setting.system.packages[app].path
js = path + "/main.js"
js.asFileHandler().read (d) ->
# load app meta data
"#{path}/package.json".asFileHandler().read (data) ->
data.path = path
_OS.APP[app].meta = data if _OS.APP[app]
_OS.APP[v].meta = data for v in data.services if data.services
#load css file
css = "#{path}/main.css"
css.asFileHandler().onready (d) ->
stamp = (new Date).timestamp()
el = $ '<link>', { rel: 'stylesheet', type: 'text/css', 'href': "#{_API.handler.get}/#{css}?stamp=#{stamp}" }
.appendTo 'head'
_OS.APP[app].style = el[0] if _OS.APP[app]
ok app
, () ->
#launch
ok app
, "json"
#ok app
, "script"
launch: (app, args) ->
if not _OS.APP[app]
# first load it
_GUI.loadApp app,
(a)->
_PM.createProcess a, _OS.APP[a], args
, (e, s) ->
else
# now launch it
if _OS.APP[app]
_PM.createProcess app, _OS.APP[app], args
dock: (app, meta) ->
# dock an application to a dock
# create a data object
data =
icon: null
iconclass: meta.iconclass || ""
app: app
onbtclick: () -> app.toggle()
# TODO: this path is not good, need to create a blob of it
data.icon = "#{meta.path}/#{meta.icon}" if meta.icon
# TODO: add default app icon class in system setting
# so that it can be themed
data.iconclass = "fa fa-cogs" if (not meta.icon) and (not meta.iconclass)
dock = $ "#sysdock"
app.init()
app.one "rendered", () ->
dock.get(0).newapp data
app.sysdock = dock.get(0)
app.appmenu = ($ "[data-id = 'appmenu']", "#syspanel")[0]
app.subscribe "systemlocalechange", (name) -> app.update()
app.subscribe "appregistry", ( m ) ->
app.applySetting m.data.m if (m.name is app.name)
toggleFullscreen: () ->
el = ($ "body")[0]
if _GUI.fullscreen
return document.exitFullscreen() if document.exitFullscreen
return document.mozCancelFullScreen() if document.mozCancelFullScreen
return document.webkitExitFullscreen() if document.webkitExitFullscreen
return document.cancelFullScreen() if document.cancelFullScreen
else
return el.requestFullscreen() if el.requestFullscreen
return el.mozRequestFullScreen() if el.mozRequestFullScreen
return el.webkitRequestFullscreen() if el.webkitRequestFullscreen
return el.msRequestFullscreen() if el.msRequestFullscreen
undock: (app) ->
($ "#sysdock").get(0).removeapp app
attachservice: (srv) ->
($ "#syspanel")[0].attachservice srv
srv.init()
srv.subscribe "systemlocalechange", (name) -> srv.update()
detachservice: (srv) ->
($ "#syspanel")[0].detachservice srv
bindContextMenu: (event) ->
handler = (e) ->
if e.contextmenuHandler
e.contextmenuHandler event, ($ "#contextmenu")[0]
else
p = $(e).parent().get(0)
handler p if p isnt ($ "#workspace").get(0)
handler event.target
event.preventDefault()
bindKey: (k, f) ->
arr = k.split "-"
return unless arr.length is 2
fnk = arr[0].toUpperCase()
c = arr[1].toUpperCase()
return unless _GUI.shortcut[fnk]
_GUI.shortcut[fnk][c] = f
wallpaper: (obj) ->
if obj
_OS.setting.appearance.wp = obj
wp = _OS.setting.appearance.wp
$("body").css("background-image", "url(#{_API.handler.get}/#{wp.url})" )
.css("background-size", wp.size)
.css("background-repeat", wp.repeat)
showTooltip: (el, text, e) ->
el = el[0]
label = ($ "#systooltip")[0]
$("#workspace").on "mousemove", (ev) ->
if $(ev.target).closest(el).length is 0
$(label).hide()
$("#workspace").off "mousemove"
arr = text.split /:(.+)/
tip = text
tip = arr[1] if arr.length > 1
offset = $(el).offset()
w = $(el).width()
h = $(el).height()
label.set "text", tip
$(label).show()
switch arr[0]
when "cr" # center right of the element
left = offset.left + w + 5
top = offset.top + h / 2 - $(label).height() / 2
$(label).css "top", top + "px"
.css "left", left + "px"
else
return unless e
$(label).css "top", e.clientY + 5 + "px"
.css "left", e.clientX + 5 + "px"
initDM: ->
# check login first
_API.resource "schemes/dm.html", (x) ->
return null unless x
scheme = $.parseHTML x
($ "#wrapper").append scheme
_courrier.observable.one "sysdockloaded", () ->
($ window).bind 'keydown', (event) ->
dock = ($ "#sysdock")[0]
return unless dock
app = dock.get "selectedApp"
#return true unless app
c = String.fromCharCode(event.which).toUpperCase()
fnk = undefined
if event.ctrlKey
fnk = "CTRL"
else if event.metaKey
fnk = "META"
else if event.shiftKey
fnk = "SHIFT"
else if event.altKey
fnk = "ALT"
return unless fnk
r = if app then app.shortcut fnk, c, event else true
return event.preventDefault() if not r
return unless _GUI.shortcut[fnk]
return unless _GUI.shortcut[fnk][c]
_GUI.shortcut[fnk][c](event)
event.preventDefault()
# system menu and dock
riot.mount ($ "#syspanel", $ "#wrapper")
riot.mount ($ "#sysdock", $ "#workspace"), { items: [] }
riot.mount ($ "#systooltip", $ "#wrapper")
# context menu
riot.mount ($ "#contextmenu", $ "#wrapper")
($ "#workspace").contextmenu (e) -> _GUI.bindContextMenu e
# tooltip
($ "#workspace").mouseover (e) ->
el = $(e.target).closest "[tooltip]"
return unless el.length > 0
_GUI.showTooltip el, ($(el).attr "tooltip"), e
# desktop default file manager
desktop = $ "#desktop"
fp = _OS.setting.desktop.path.asFileHandler()
desktop[0].fetch = () ->
fn = () ->
fp = _OS.setting.desktop.path.asFileHandler()
fp.read (d) ->
return _courrier.osfail d.error, (_API.throwe "OS.VFS"), d.error if d.error
items = []
$.each d.result, (i, v) ->
return if v.filename[0] is '.' and not _OS.setting.desktop.showhidden
v.text = v.filename
#v.text = v.text.substring(0,9) + "..." ifv.text.length > 10
v.iconclass = v.type
items.push(v)
desktop[0].set "items", items
desktop[0].refresh()
fp.onready () ->
fn()
, ( e ) -> # try to create the path
console.log "#{fp.path} not found"
name = fp.basename
fp.parent().asFileHandler().mk name, (r) ->
ex = _API.throwe "OS.VFS"
if r.error then _courrier.osfail d.error, ex, d.error else fn()
desktop[0].ready = (e) ->
e.observable = _courrier
window.onresize = () ->
_courrier.trigger "desktopresize"
e.refresh()
desktop[0].set "onlistselect", (d) ->
($ "#sysdock").get(0).set "selectedApp", null
desktop[0].set "onlistdbclick", ( d ) ->
($ "#sysdock").get(0).set "selectedApp", null
it = desktop[0].get "selected"
_GUI.openWith it
#($ "#workingenv").on "click", (e) ->
# desktop[0].set "selected", -1
desktop.on "click", (e) ->
return unless e.target is desktop[0]
desktop[0].set "selected", -1
($ "#sysdock").get(0).set "selectedApp", null
#console.log "desktop clicked"
desktop[0].contextmenuHandler = (e, m) ->
desktop[0].set "selected", -1 if e.target is desktop[0]
($ "#sysdock").get(0).set "selectedApp", null
menu = [
{ text: __("Open"), dataid: "desktop-open" },
{ text: __("Refresh"), dataid: "desktop-refresh" }
]
menu = menu.concat ( v for k, v of _OS.setting.desktop.menu)
m.set "items", menu
m.set "onmenuselect", (evt) ->
switch evt.item.data.dataid
when "desktop-open"
it = desktop[0].get "selected"
return _GUI.openWith it if it
it = _OS.setting.desktop.path.asFileHandler()
it.mime = "dir"
_GUI.openWith it
when "desktop-refresh"
desktop[0].fetch()
else
_GUI.launch evt.item.data.app, evt.item.data.args if evt.item.data.app
m.show(e)
desktop[0].fetch()
_courrier.observable.on "VFS", (d) ->
desktop[0].fetch() if d.data.file.hash() is fp.hash() or d.data.file.parent().hash() is fp.hash()
_courrier.ostrigger "desktoploaded"
# mount it
riot.mount desktop
, (e, s) ->
alert __("System fail: Cannot init desktop manager")
console.log s, e
refreshDesktop: () ->
($ "#desktop")[0].fetch()
refreshSystemMenu: () ->
_GUI.SYS_MENU[0].child.length = 1
_GUI.SYS_MENU[0].child[0].child.length = 0
_GUI.SYS_MENU[0].child[0].child.push v for k, v of _OS.setting.system.packages when (v and v.app)
_GUI.SYS_MENU[0].child.push v for k, v of _OS.setting.system.menu
_GUI.SYS_MENU[0].child.push
text: "__(Toggle Full screen)",
dataid: "os-fullsize",
iconclass: "fa fa-tv"
_GUI.SYS_MENU[0].child.push
text: "__(Log out)",
dataid: "sys-logout",
iconclass: "fa fa-user-times"
($ "[data-id = 'os_menu']", "#syspanel")[0].update()
buildSystemMenu: () ->
($ "[data-id = 'os_menu']", "#syspanel")[0].set "items", _GUI.SYS_MENU
#console.log menu
mkdialog: (conf) ->
return new _GUI.BasicDialog conf.name, conf.layout
login: () ->
_API.resource "schemes/login.html", (x) ->
return null unless x
scheme = $.parseHTML x
($ "#wrapper").append scheme
($ "#btlogin").click () ->
data =
username: ($ "#txtuser").val(),
password: ($ "#txtpass").val()
_API.handler.login data, (d) ->
if d.error then ($ "#login_error").html d.error else _GUI.startAntOS d.result
($ "#txtpass").keyup (e) ->
($ "#btlogin").click() if e.which is 13
, (e, s) ->
alert __("System fail: Cannot init login screen")
startAntOS: (conf) ->
# clean up things
_OS.cleanup()
# get setting from conf
_OS.systemSetting conf
#console.log _OS.setting
# load theme
_GUI.loadTheme _OS.setting.appearance.theme
_GUI.wallpaper()
_courrier.observable.one "syspanelloaded", () ->
# TODO load packages list then build system menu
_courrier.observable.on "systemlocalechange", (name) ->
($ "#syspanel")[0].update()
_API.packages.cache (ret) ->
if ret.result
_API.packages.fetch (r) ->
if r.result
for k, v of r.result
v.text = v.name
v.filename = k
v.type = "app"
v.mime = "antos/app"
v.icon = "#{v.path}/#{v.icon}" if v.icon
v.iconclass = "fa fa-adn" unless v.iconclass or v.icon
_OS.setting.system.packages = if r.result then r.result else
_GUI.refreshSystemMenu()
_GUI.buildSystemMenu()
# push startup services
# TODO: get services list from user setting
_GUI.pushServices (v for v in _OS.setting.system.startup.services)
(_GUI.launch a) for a in _OS.setting.system.startup.apps
#_GUI.launch "DummyApp"
# initDM
_API.setLocale _OS.setting.system.locale, () ->
_GUI.initDM()