1----------------------------------------------------------------------
2-- Ipe
3----------------------------------------------------------------------
4--[[
5
6    This file is part of the extensible drawing editor Ipe.
7    Copyright (c) 1993-2020 Otfried Cheong
8
9    Ipe is free software; you can redistribute it and/or modify it
10    under the terms of the GNU General Public License as published by
11    the Free Software Foundation; either version 3 of the License, or
12    (at your option) any later version.
13
14    As a special exception, you have permission to link Ipe with the
15    CGAL library and distribute executables, as long as you follow the
16    requirements of the Gnu General Public License in regard to all of
17    the software in the executable aside from CGAL.
18
19    Ipe is distributed in the hope that it will be useful, but WITHOUT
20    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
21    or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
22    License for more details.
23
24    You should have received a copy of the GNU General Public License
25    along with Ipe; if not, you can find it at
26    "http://www.gnu.org/copyleft/gpl.html", or write to the Free
27    Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
28
29--]]
30
31-- order is important
32require "prefs"
33require "model"
34require "actions"
35require "tools"
36require "editpath"
37require "properties"
38require "shortcuts"
39require "mouse"
40
41----------------------------------------------------------------------
42
43-- short names
44V = ipe.Vector
45
46-- store the model for the first window
47-- Used only by OSX file_open_event
48-- must be global, as it's changed from model.lua
49first_model = nil
50
51----------------------------------------------------------------------
52
53function printTable(t)
54  for k in pairs(t) do
55    print(k, t[k])
56  end
57end
58
59-- only used for saving
60function formatFromFileName(fname)
61  local s = string.lower(fname:sub(-4))
62  if s == ".xml" or s == ".ipe" then return "xml" end
63  if s == ".pdf" then return "pdf" end
64  return nil
65end
66
67function revertOriginal(t, doc)
68  doc:set(t.pno, t.original)
69end
70
71function revertFinal(t, doc)
72  doc:set(t.pno, t.final)
73end
74
75function indexOf(el, list)
76  for i,n in ipairs(list) do
77    if n == el then return i end
78  end
79  return nil
80end
81
82function symbolNames(sheet, prefix, postfix)
83  local list = sheet:allNames("symbol")
84  local result = {}
85  for _, n in ipairs(list) do
86    if n:sub(1, #prefix) == prefix and n:sub(-#postfix) == postfix then
87      result[#result + 1] = n
88    end
89  end
90  return result
91end
92
93function stripPrefixPostfix(list, m, mm)
94  local result = {}
95  for _, n in ipairs(list) do
96    result[#result + 1] = n:sub(m, -mm-1)
97  end
98  return result
99end
100
101function arrowshapeToName(i, s)
102  return s:match("^arrow/(.+)%(s?f?p?x%)$")
103end
104
105function colorString(color)
106  if type(color) == "string" then return color end
107  -- else must be table
108  return string.format("(%g,%g,%g)", color.r, color.g, color.b)
109end
110
111function extractElements(p, selection)
112  local r = {}
113  local l = {}
114  for i,j in ipairs(selection) do
115    r[#r + 1] = p[j-i+1]:clone()
116    l[#l + 1] = p:layerOf(j-i+1)
117    p:remove(j-i+1)
118  end
119  return r, l
120end
121
122-- make a list of all values from stylesheets
123function allValues(sheets, kind)
124  local syms = sheets:allNames(kind)
125  local values = {}
126  for _,sym in ipairs(syms) do
127    values[#values + 1] = sheets:find(kind, sym)
128  end
129  return values
130end
131
132-- apply transformation to a shape
133function transformShape(matrix, shape)
134  local result = {}
135  for _,path in ipairs(shape) do
136    if path.type == "ellipse" or path.type == "closedspline" then
137      for i = 1,#path do
138	path[i] = matrix * path[i]
139      end
140    else -- must be "curve"
141      for _,seg in ipairs(path) do
142	for i = 1,#seg do
143	  seg[i] = matrix * seg[i]
144	end
145	if seg.type == "arc" then
146	  seg.arc = matrix * seg.arc
147	end
148      end
149    end
150  end
151end
152
153function findStyle(w, dir)
154  if dir and ipe.fileExists(dir .. prefs.fsep .. w) then
155    return dir .. prefs.fsep .. w
156  end
157  for _, d in ipairs(config.styleDirs) do
158    local s = d .. prefs.fsep .. w
159    if ipe.fileExists(s) then return s end
160  end
161end
162
163-- show a message box
164-- type is one of "none" "warning" "information" "question" "critical"
165-- details may be nil
166-- buttons may be nil (for "ok") or one of
167-- "ok" "okcancel" "yesnocancel", "discardcancel", "savediscardcancel",
168-- or a number from 0 to 4 corresponding to these
169-- return 1 for ok/yes/save, 0 for no/discard, -1 for cancel
170function messageBox(parent, type, text, details, buttons)
171  if config.toolkit == "win" and buttons and
172    (buttons == 3 or buttons == 4 or buttons == "discardcancel"
173     or buttons == "savediscardcancel") then
174    -- native Windows messagebox does not support these
175    -- so we build our own dialog - this one has no icon, though
176    local result = 0
177    local d = ipeui.Dialog(parent, "Ipe")
178    d:add("text", "label", {label=text .. "\n\n" .. details}, 1, 1)
179    if buttons == 4 or buttons == "savediscardcancel" then
180      d:addButton("ok", "&Save", function (d) result=1; d:accept(true) end)
181    end
182    d:addButton("discard", "&Discard", "accept")
183    d:addButton("cancel", "&Cancel", "reject")
184    local r = d:execute()
185    if r then
186      return result
187    else
188      return -1
189    end
190  else
191    return ipeui.messageBox(parent, type, text, details, buttons)
192  end
193end
194
195filter_ipe = { "Ipe files (*.ipe *.pdf *.eps *.xml)",
196	       "*.ipe;*.pdf;*.eps;*.xml",
197	       "All files (*.*)", "*.*" }
198filter_save = { "XML (*.ipe *.xml)", "*.ipe;*.xml",
199		"PDF (*.pdf)", "*.pdf" }
200filter_stylesheets = { "Ipe stylesheets (*.isy)", "*.isy" }
201if config.platform == "win" then
202  filter_images = { "Images (*.png *.jpg *.jpeg *.bmp *.gif *.tiff)",
203		    "*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.tiff" }
204else
205  filter_images = { "Images (*.png *.jpg *.jpeg)", "*.png;*.jpg;*.jpeg" }
206end
207filter_png = { "Images (*.png)", "*.png" }
208filter_eps = { "Postscript files (*.eps)", "*.eps" }
209filter_svg = { "SVG files (*.svg)", "*.svg" }
210
211----------------------------------------------------------------------
212-- This function is called to launch a file on MacOS X
213
214function file_open_event(fname)
215  if first_model and first_model.pristine then
216    first_model:loadDocument(fname)
217    first_model:action_fit_top()
218  else
219    local m = MODEL.new(nil, fname)
220    m:action_fit_top()
221  end
222end
223
224----------------------------------------------------------------------
225
226-- msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
227
228local win32_conversions = {
229  PgDown=0x22, PgUp=0x21, Home=0x24, End=0x23,
230  Left=0x25, Up=0x26, Right=0x27, Down=0x28,
231  insert=0x2d, delete=0x2e
232}
233
234function win32_shortcut_convert(s)
235  local k = 0
236  local done = false
237  while not done do
238    if s:sub(1,5) == "Ctrl+" then
239      s = s:sub(6)
240      k = k + 0x20000
241    elseif s:sub(1,6) == "Shift+" then
242      s = s:sub(7)
243      k = k + 0x40000
244    elseif s:sub(1,4) == "Alt+" then
245      s = s:sub(5)
246      k = k + 0x10000
247    else
248      done = true
249    end
250  end
251  if s ~= "F" and s:sub(1,1) == "F" and tonumber(s:sub(2)) then
252    return k + 0x6f + tonumber(s:sub(2))
253  elseif win32_conversions[s] then
254    return k + win32_conversions[s]
255  elseif ("0" <= s and s <= "9") or ("A" <= s and s <= "Z") then
256    return k + string.byte(s:sub(1,1))
257  else
258    return k + string.byte(s:sub(1,1)) + 0x80000
259  end
260end
261
262function win32_shortcut_append(t, ts, s, id, ao)
263  local sc = win32_shortcut_convert(s)
264  t[#t+1] = sc
265  t[#t+1] = id
266  if ao then
267    ts[#ts+1] = sc
268    ts[#ts+1] = id
269  end
270end
271
272function win32_shortcuts(ui)
273  if config.toolkit ~= "win" then return end
274  local accel = {}
275  local accelsub = {}
276  for i in pairs(shortcuts) do
277    local id, alwaysOn = ui:actionInfo(i)
278    local s = shortcuts[i]
279    if type(s) == "table" then
280      for j,s1 in ipairs(s) do
281	win32_shortcut_append(accel, accelsub, s1, id, alwaysOn)
282      end
283    elseif s then
284      win32_shortcut_append(accel, accelsub, s, id, alwaysOn)
285    end
286  end
287  return accel, accelsub
288end
289
290----------------------------------------------------------------------
291
292local function show_configuration()
293  local s = config.version
294  s = s .. "\nLua code: " .. package.path
295  s = s .. "\nStyle directories: " .. table.concat(config.styleDirs, ", ")
296  s = s .. "\nStyles for new documents: " .. table.concat(prefs.styles, ", ")
297  s = s .. "\nAutosave file: " .. prefs.autosave_filename
298  s = s .. "\nSave-as directory: " .. prefs.save_as_directory
299  s = s .. "\nDocumentation: " .. config.docdir
300  s = s .. "\nIpelets: " .. table.concat(config.ipeletDirs, ", ")
301  s = s .. "\nLatex program path: " .. config.latexpath
302  s = s .. "\nLatex directory: " .. config.latexdir
303  s = s .. "\nIcons: " .. config.icons
304  s = s .. "\nExternal editor: " .. (prefs.external_editor or "none")
305  s = s .. "\n"
306  io.stdout:write(s)
307end
308
309local function usage()
310  io.stderr:write("Usage: ipe { -sheet <filename.isy> } [ <filename> ]\n")
311  io.stderr:write("or:    ipe -show-configuration\n")
312  io.stderr:write("or:    ipe --help\n")
313end
314
315--------------------------------------------------------------------
316
317-- set locale so that "tonumber" will work right with decimal points
318os.setlocale("C", "numeric")
319
320local test1 = string.format("%g", 1.5)
321local test2 = string.format("%g", tonumber("1.5"))
322if test1 ~= "1.5" or test2 ~= "1.5" then
323  m = "Formatting the number '1.5' results in '"
324    .. test1 .. "'. "
325    .. "Reading '1.5' results in '" .. test2 .. "'\n"
326    .. "Therefore Ipe will not work correctly when loading or saving files. "
327    .. "PLEASE REPORT THIS PROBLEM!\n"
328    .. "As a workaround, you can start Ipe from the commandline like this: "
329    .. "export LANG=C\nexport LC_NUMERIC=C\nipe"
330  ipeui.messageBox(nil, "critical",
331		   "Ipe is running with an incorrect locale", m)
332  return
333end
334
335--------------------------------------------------------------------
336
337local home = os.getenv("HOME")
338local ipeletpath = os.getenv("IPELETPATH")
339if ipeletpath then
340  config.ipeletDirs = {}
341  for w in string.gmatch(ipeletpath, prefs.fname_pattern) do
342    if w == "_" then w = config.system_ipelets end
343    if w:sub(1,4) == "ipe:" then
344      w = config.ipedrive .. w:sub(5)
345    end
346    config.ipeletDirs[#config.ipeletDirs + 1] = w
347  end
348else
349  config.ipeletDirs = { config.system_ipelets }
350  if config.platform == "win" then
351    local userdir = os.getenv("USERPROFILE")
352    if userdir then
353      config.ipeletDirs[#config.ipeletDirs + 1] = userdir .. "\\Ipelets"
354    end
355  else
356    config.ipeletDirs[#config.ipeletDirs + 1] = home .. "/.ipe/ipelets"
357    if config.platform == "apple" then
358      config.ipeletDirs[#config.ipeletDirs + 1] = home.."/Library/Ipe/Ipelets"
359    end
360  end
361end
362
363local ipestyles = os.getenv("IPESTYLES")
364if ipestyles then
365  config.styleDirs = {}
366  for w in string.gmatch(ipestyles, prefs.fname_pattern) do
367    if w == "_" then w = config.system_styles end
368    if w:sub(1,4) == "ipe:" then
369      w = config.ipedrive .. w:sub(5)
370    end
371    config.styleDirs[#config.styleDirs + 1] = w
372  end
373else
374  config.styleDirs = { config.system_styles }
375  if config.platform ~= "win" then
376    table.insert(config.styleDirs, 1, home .. "/.ipe/styles")
377    if config.platform == "apple" then
378      table.insert(config.styleDirs, 2, home .. "/Library/Ipe/Styles")
379    end
380  end
381end
382
383--------------------------------------------------------------------
384
385function load_ipelets()
386  for _,ft in ipairs(ipelets) do
387    local fd = ipe.openFile(ft.path, "rb")
388    local ff = assert(load(function () return fd:read("*L") end,
389			   ft.path, "bt", ft))
390    ff()
391  end
392end
393
394-- look for ipelets
395ipelets = {}
396for _,w in ipairs(config.ipeletDirs) do
397  if ipe.fileExists(w) then
398    local files = ipe.directory(w)
399    for i, f in ipairs(files) do
400      if f:sub(-4) == ".lua" then
401	ft = {}
402	ft.name = f:sub(1,-5)
403	ft.path = w .. prefs.fsep .. f
404	ft.dllname = w .. prefs.fsep .. ft.name
405	ft._G = _G
406	ft.ipe = ipe
407	ft.ipeui = ipeui
408	ft.math = math
409	ft.string = string
410	ft.table = table
411	ft.assert = assert
412	ft.shortcuts = shortcuts
413	ft.prefs = prefs
414	ft.config = config
415	ft.mouse = mouse
416	ft.ipairs = ipairs
417	ft.pairs = pairs
418	ft.print = print
419	ft.tonumber = tonumber
420	ft.tostring = tostring
421	ipelets[#ipelets + 1] = ft
422      end
423    end
424  end
425end
426
427load_ipelets()
428
429--------------------------------------------------------------------
430
431recent_files = {}
432
433function load_recent_files()
434  local w = config.latexdir .. "recent_files.lua"
435  if ipe.fileExists(w) then
436    local r = {}
437    local fd = ipe.openFile(w, "r")
438    local ff = load(function () return fd:read("*L") end, w, "bt", r)
439    if ff then
440      ff()
441      recent_files = r.recent_files
442    end
443    fd:close()
444  end
445end
446
447load_recent_files()
448
449--------------------------------------------------------------------
450
451if #argv == 1 and argv[1] == "-show-configuration" then
452  show_configuration()
453  return
454end
455
456if #argv == 1 and (argv[1] == "--help" or argv[1] == "-h") then
457  usage()
458  return
459end
460
461--------------------------------------------------------------------
462
463local first_file = nil
464local i = 1
465local style_sheets = {}
466
467while i <= #argv do
468  if argv[i] == "-sheet" then
469    if i == #argv then usage() return end
470    style_sheets[#style_sheets + 1] = argv[i+1]
471    i = i + 2
472  else
473    if i ~= #argv then usage() return end
474    first_file = ipe.realPath(argv[i])
475    i = i + 1
476  end
477end
478
479-- Cocoa handles opening files itself, using open_file_event
480if config.toolkit == "cocoa" then first_file = nil end
481
482if #style_sheets > 0 then prefs.styles = style_sheets end
483
484config.styleList = {}
485for _,w in ipairs(prefs.styles) do
486  if w:sub(-4) ~= ".isy" then w = w .. ".isy" end
487  if not w:find(prefs.fsep) then w = findStyle(w) end
488  config.styleList[#config.styleList + 1] = w
489end
490
491first_model = MODEL:new(first_file)
492first_model:action_fit_top()
493first_model.ui:setScreen(prefs.start_screen)
494
495local acc, accsub = win32_shortcuts(first_model.ui)
496mainloop(acc, accsub)
497
498----------------------------------------------------------------------
499