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