1 // --------------------------------------------------------------------
2 // AppUi for QT
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 #include "appui_qt.h"
32 #include "ipecanvas_qt.h"
33 #include "controls_qt.h"
34 
35 #include "ipelua.h"
36 
37 #include "ipethumbs.h"
38 
39 #include <cstdio>
40 #include <cstdlib>
41 
42 #include <QMenuBar>
43 #include <QKeySequence>
44 #include <QCloseEvent>
45 #include <QStatusBar>
46 #include <QGridLayout>
47 #include <QButtonGroup>
48 #include <QMessageBox>
49 #include <QSignalMapper>
50 #include <QToolTip>
51 #include <QDialogButtonBox>
52 #include <QClipboard>
53 #include <QMimeData>
54 #include <QWindow>
55 #include <QScreen>
56 
57 #include <sys/types.h>
58 #include <sys/stat.h>
59 
60 using namespace ipe;
61 using namespace ipelua;
62 
63 // --------------------------------------------------------------------
64 
adapt_size(const QSize & size,int factor)65 inline QSize adapt_size(const QSize &size, int factor)
66 {
67   return QSize(factor * size.width() / 100, factor * size.height() / 100);
68 }
69 
prefsIcon(String name)70 QIcon AppUi::prefsIcon(String name)
71 {
72   if (name == "ipe")
73     return QIcon(QIpe(ipeIconDirectory() + "icon_128x128.png"));
74 
75   String svgdir = Platform::latexDirectory() + "/icons/";
76   if (!Platform::fileExists(svgdir) && mkdir(svgdir.z(), 0700) != 0)
77     return QIcon();
78 
79   String svgname = svgdir + name + ".svg";
80   int pno = ipeIcon(name);
81   if (pno >= 0) {
82     bool dark = QGuiApplication::palette().text().color().value() > 128;
83     Document *doc = dark ? ipeIconsDark.get() : ipeIcons.get();
84     Thumbnail thumbs(doc, 22);
85     thumbs.setNoCrop(true);
86     thumbs.saveRender(Thumbnail::ESVG, svgname.z(), doc->page(pno), 0, 1.0);
87     return QIcon(QIpe(svgname));
88   } else
89     return QIcon();
90 }
91 
prefsPixmap(String name)92 QPixmap AppUi::prefsPixmap(String name)
93 {
94   QIcon icon = prefsIcon(name);
95   int size = uiscale(24);
96   return icon.pixmap(QSize(size, size));
97 }
98 
prefsColorIcon(Color color)99 QIcon AppUi::prefsColorIcon(Color color)
100 {
101   int size = uiscale(16);
102   QPixmap pixmap(size, size);
103   pixmap.fill(QIpe(color));
104   return QIcon(pixmap);
105 }
106 
107 // --------------------------------------------------------------------
108 
findAction(const char * name) const109 QAction *AppUi::findAction(const char *name) const
110 {
111   std::map<String, QAction*>::const_iterator it = iActions.find(name);
112   if (it != iActions.end())
113     return it->second;
114   else
115     return nullptr;
116 }
117 
addItem(QMenu * m,const QString & title,const char * name)118 void AppUi::addItem(QMenu *m, const QString &title, const char *name)
119 {
120   if (name[0] == '@') {
121     // canUseWhileDrawing = true;
122     name = name + 1;
123   }
124   bool checkable = (m == iMenu[EModeMenu]) ||
125     (String(name).find('|') >= 0);
126   if (name[0] == '*') {
127     checkable = true;
128     name = name + 1;
129   }
130   QAction *a = new QAction(title, this);
131   a->setIconVisibleInMenu(false);
132   if (checkable)
133     a->setCheckable(true);
134   lua_getglobal(L, "shortcuts");
135   lua_getfield(L, -1, name);
136   if (lua_isstring(L, -1)) {
137     QString s = lua_tostring(L, -1);
138     a->setShortcut(QKeySequence(s));
139     QString tt = title + " [" + s + "]";
140     a->setToolTip(tt);
141   }
142   a->setIcon(prefsIcon(name));
143   lua_pop(L, 2);
144   m->addAction(a);
145   if (m == iMenu[EModeMenu]) {
146     a->setActionGroup(iModeActionGroup);
147     iObjectTools->addAction(a);
148   }
149   connect(a, SIGNAL(triggered()), iActionMap, SLOT(map()));
150   iActionMap->setMapping(a, name);
151   iActions[name] = a;
152 }
153 
addSnap(const char * name)154 void AppUi::addSnap(const char *name)
155 {
156   QAction *a = findAction(name);
157   assert(a);
158   a->setCheckable(true);
159   iSnapTools->addAction(a);
160 }
161 
addEdit(const char * name)162 void AppUi::addEdit(const char *name)
163 {
164   QAction *a = findAction(name);
165   assert(a);
166   iEditTools->addAction(a);
167 }
168 
addRootMenu(int id,const char * name)169 void AppUi::addRootMenu(int id, const char *name)
170 {
171   iMenu[id] = menuBar()->addMenu(name);
172 }
173 
174 // if title and name == 0, add separator (default)
addItem(int id,const char * title,const char * name)175 void AppUi::addItem(int id, const char *title, const char *name)
176 {
177   if (title == nullptr)
178     iMenu[id]->addSeparator();
179   else
180     addItem(iMenu[id], QString::fromUtf8(title), name);
181 }
182 
183 static QMenu *submenu = nullptr;
184 static int submenuId = 0;
185 
startSubMenu(int id,const char * name,int tag)186 void AppUi::startSubMenu(int id, const char *name, int tag)
187 {
188   submenuId = id;
189   submenu = new QMenu(name);
190 }
191 
addSubItem(const char * title,const char * name)192 void AppUi::addSubItem(const char *title, const char *name)
193 {
194   addItem(submenu, title, name);
195 }
196 
endSubMenu()197 MENUHANDLE AppUi::endSubMenu()
198 {
199   iMenu[submenuId]->addMenu(submenu);
200   return submenu;
201 }
202 
getDockSide(lua_State * L,const char * name,Qt::DockWidgetArea deflt)203 Qt::DockWidgetArea getDockSide(lua_State *L, const char *name,
204 			       Qt::DockWidgetArea deflt)
205 {
206   Qt::DockWidgetArea r = deflt;
207   lua_getglobal(L, "prefs");
208   lua_getfield(L, -1, "tools_placement");
209   if (lua_istable(L, -1)) {
210     lua_getfield(L, -1, name);
211     const char *s = lua_tolstring(L, -1, nullptr);
212     if (!strcmp(s, "left"))
213       r = Qt::LeftDockWidgetArea;
214     else if (!strcmp(s, "right"))
215       r = Qt::RightDockWidgetArea;
216     lua_pop(L, 1); // left or right
217   }
218   lua_pop(L, 2); // prefs, tools_placement
219   return r;
220 }
221 
set_toolbar_size(QToolBar * bar,int factor)222 static void set_toolbar_size(QToolBar *bar, int factor)
223 {
224   QSize size = bar->iconSize();
225   QSize size1 = adapt_size(size, factor);
226   bar->setIconSize(size1);
227   /*
228   ipeDebug("toolbar %s has size %d x %d, changed to %d x %d",
229 	   bar->windowTitle().toUtf8().constData(), size.width(), size.height(),
230 	   size1.width(), size1.height());
231   */
232 }
233 
234 // --------------------------------------------------------------------
235 
AppUi(lua_State * L0,int model,Qt::WindowFlags f)236 AppUi::AppUi(lua_State *L0, int model, Qt::WindowFlags f)
237   : QMainWindow(nullptr, f), AppUiBase(L0, model)
238 {
239   qApp->setWindowIcon(prefsIcon("ipe"));
240   QMainWindow::setAttribute(Qt::WA_DeleteOnClose);
241   qApp->setAttribute(Qt::AA_UseHighDpiPixmaps);
242   setDockOptions(AnimatedDocks); // do not allow stacking properties and layers
243 
244   Canvas *canvas = new Canvas(this);
245   iCanvas = canvas;
246   setCentralWidget(canvas);
247 
248   iSnapTools = addToolBar("Snap");
249   iEditTools = addToolBar("Edit");
250   addToolBarBreak();
251   iObjectTools = addToolBar("Objects");
252 
253   set_toolbar_size(iEditTools, iToolbarScale);
254   set_toolbar_size(iSnapTools, iToolbarScale);
255   set_toolbar_size(iObjectTools, iToolbarScale);
256 
257   iActionMap = new QSignalMapper(this);
258   connect(iActionMap, SIGNAL(mapped(const QString &)),
259 	  SLOT(qAction(const QString &)));
260 
261   iModeActionGroup = new QActionGroup(this);
262   iModeActionGroup->setExclusive(true);
263   QActionGroup *cg = new QActionGroup(this);
264   cg->setExclusive(true);
265   QActionGroup *cs = new QActionGroup(this);
266   cs->setExclusive(true);
267 
268   buildMenus();
269 
270   findAction("coordinates|points")->setActionGroup(cg);
271   findAction("coordinates|mm")->setActionGroup(cg);
272   findAction("coordinates|m")->setActionGroup(cg);
273   findAction("coordinates|inch")->setActionGroup(cg);
274 
275   for (uint i = 0; i < iScalings.size(); ++i) {
276     char action[32];
277     sprintf(action, "scaling|%d", iScalings[i]);
278     findAction(action)->setActionGroup(cs);
279   }
280 
281   connect(iRecentFileMenu, SIGNAL(triggered(QAction *)),
282 	  SLOT(recentFileAction(QAction *)));
283   connect(iSelectLayerMenu, SIGNAL(triggered(QAction *)),
284 	  SLOT(selectLayerAction(QAction *)));
285   connect(iMoveToLayerMenu, SIGNAL(triggered(QAction *)),
286 	  SLOT(moveToLayerAction(QAction *)));
287   connect(iTextStyleMenu, SIGNAL(triggered(QAction *)),
288 	  SLOT(textStyleAction(QAction *)));
289   connect(iLabelStyleMenu, SIGNAL(triggered(QAction *)),
290 	  SLOT(labelStyleAction(QAction *)));
291   connect(iGridSizeMenu, SIGNAL(triggered(QAction *)),
292 	  SLOT(gridSizeAction(QAction *)));
293   connect(iAngleSizeMenu, SIGNAL(triggered(QAction *)),
294 	  SLOT(angleSizeAction(QAction *)));
295 
296   QSignalMapper *comboMap = new QSignalMapper(this);
297 
298   iSelector[EUiGridSize] = new QComboBox();
299   iSelector[EUiAngleSize] = new QComboBox();
300   connect(iSelector[EUiGridSize], SIGNAL(activated(int)),
301 	  comboMap, SLOT(map()));
302   connect(iSelector[EUiAngleSize], SIGNAL(activated(int)),
303 	  comboMap, SLOT(map()));
304   comboMap->setMapping(iSelector[EUiGridSize], EUiGridSize);
305   comboMap->setMapping(iSelector[EUiAngleSize], EUiAngleSize);
306 
307   addSnap("snapvtx");
308   addSnap("snapctl");
309   addSnap("snapbd");
310   addSnap("snapint");
311   addSnap("snapgrid");
312   iSnapTools->addWidget(iSelector[EUiGridSize]);
313   addSnap("snapangle");
314   iSnapTools->addWidget(iSelector[EUiAngleSize]);
315   addSnap("snapcustom");
316   addSnap("snapauto");
317 
318   addEdit("copy");
319   addEdit("cut");
320   addEdit("paste");
321   addEdit("delete");
322   addEdit("undo");
323   addEdit("redo");
324   addEdit("zoom_in");
325   addEdit("zoom_out");
326   addEdit("fit_objects");
327   addEdit("fit_page");
328   addEdit("fit_width");
329   addEdit("keyboard");
330   iShiftKey = new QAction("shift_key", this);
331   iShiftKey->setCheckable(true);
332   iShiftKey->setIcon(prefsIcon("shift_key"));
333   iEditTools->addAction(iShiftKey);
334   iEditTools->addAction(findAction("grid_visible"));
335   iAbortButton = new QAction("stop", this);
336   iAbortButton->setIcon(prefsIcon("stop"));
337   iEditTools->addAction(iAbortButton);
338   connect(iShiftKey, SIGNAL(triggered()), SLOT(toolbarModifiersChanged()));
339   connect(iAbortButton, SIGNAL(triggered()), SLOT(abortDrawing()));
340 
341   iPropertiesTools = new QDockWidget("Properties", this);
342   addDockWidget(getDockSide(L, "properties", Qt::LeftDockWidgetArea),
343 		iPropertiesTools);
344   iPropertiesTools->setAllowedAreas(Qt::LeftDockWidgetArea|
345 				    Qt::RightDockWidgetArea);
346 
347   iBookmarkTools = new QDockWidget("Bookmarks", this);
348   addDockWidget(getDockSide(L, "bookmarks", Qt::RightDockWidgetArea),
349 		iBookmarkTools);
350   iBookmarkTools->setAllowedAreas(Qt::LeftDockWidgetArea|
351 				  Qt::RightDockWidgetArea);
352   iMenu[EPageMenu]->addAction(iBookmarkTools->toggleViewAction());
353 
354   iNotesTools = new QDockWidget("Notes", this);
355   addDockWidget(getDockSide(L, "notes", Qt::RightDockWidgetArea),
356 		iNotesTools);
357   iNotesTools->setAllowedAreas(Qt::LeftDockWidgetArea|
358 			       Qt::RightDockWidgetArea);
359   iMenu[EPageMenu]->addAction(iNotesTools->toggleViewAction());
360 
361   iLayerTools = new QDockWidget("Layers", this);
362   addDockWidget(getDockSide(L, "layers", Qt::LeftDockWidgetArea),
363 		iLayerTools);
364   iLayerTools->setAllowedAreas(Qt::LeftDockWidgetArea|Qt::RightDockWidgetArea);
365 
366   // object names are used for saving toolbar state
367   iSnapTools->setObjectName(QLatin1String("SnapTools"));
368   iObjectTools->setObjectName(QLatin1String("ObjectTools"));
369   iPropertiesTools->setObjectName(QLatin1String("PropertiesTools"));
370   iLayerTools->setObjectName(QLatin1String("LayerTools"));
371   iNotesTools->setObjectName(QLatin1String("NotesTools"));
372   iBookmarkTools->setObjectName(QLatin1String("BookmarkTools"));
373 
374   QWidget *wg = new QFrame();
375   QGridLayout *lo = new QGridLayout();
376   wg->setLayout(lo);
377   int m = uiscale(2);
378   lo->setSpacing(1);
379   lo->setContentsMargins(m, m, m, m); // l t r b
380   // wg->setMaximumWidth(150);
381   lo->setSizeConstraint(QLayout::SetFixedSize);
382   QButtonGroup *bg = new QButtonGroup(wg);
383   bg->setExclusive(false);
384   connect(bg, SIGNAL(buttonClicked(int)), this, SLOT(absoluteButton(int)));
385   iButton[EUiMarkShape] = nullptr;   // no such buttons
386   iButton[EUiDashStyle] = nullptr;
387   /* enum { EUiStroke, EUiFill, EUiPen, EUiDashStyle,
388      EUiTextSize, EUiMarkShape, EUiSymbolSize, EUiOpacity ... }
389      layout:
390      0: stroke
391      1: fill
392      2: pen       \   pen button
393      3: dashstyle /
394      4: pathview
395      5: textsize
396      6: markshape   \ symbolsize button
397      7: symbolsize  /
398      8: opacity
399    */
400   for (int i = 0; i <= EUiOpacity; ++i) {
401     if (i != EUiDashStyle && i != EUiMarkShape && i != EUiOpacity) {
402       iButton[i] = new QToolButton();
403       iButton[i]->setFocusPolicy(Qt::NoFocus);
404       iButton[i]->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding,
405 					    QSizePolicy::ToolButton));
406       bg->addButton(iButton[i], i);
407       if (i == EUiPen)
408 	lo->addWidget(iButton[i], i, 0, 2, 1);
409       else if (i == EUiSymbolSize)
410 	lo->addWidget(iButton[i], i, 0, 2, 1);
411       else
412 	lo->addWidget(iButton[i], i >= EUiTextSize ? i+1 : i, 0);
413       iButton[i]->setIconSize(adapt_size(iButton[i]->iconSize(), iUiScale));
414     }
415     iSelector[i] = new QComboBox();
416     if (i == EUiOpacity)
417       lo->addWidget(iSelector[i], i+1, 0, 1, 2);
418     else
419       lo->addWidget(iSelector[i], i >= EUiTextSize ? i+1 : i, 1);
420     connect(iSelector[i], SIGNAL(activated(int)), comboMap, SLOT(map()));
421     comboMap->setMapping(iSelector[i], i);
422   }
423   iButton[EUiStroke]->setIcon(prefsColorIcon(Color(1000, 0, 0)));
424   iButton[EUiFill]->setIcon(prefsColorIcon(Color(1000, 1000, 0)));
425   iButton[EUiPen]->setIcon(prefsIcon("pen"));
426   iButton[EUiTextSize]->setIcon(prefsIcon("mode_label"));
427   iButton[EUiSymbolSize]->setIcon(prefsIcon("mode_marks"));
428 
429   iButton[EUiStroke]->setToolTip("Absolute stroke color");
430   iButton[EUiFill]->setToolTip("Absolute fill color");
431   iButton[EUiPen]->setToolTip("Absolute pen width");
432   iButton[EUiTextSize]->setToolTip("Absolute text size");
433   iButton[EUiSymbolSize]->setToolTip("Absolute symbol size");
434 
435   iSelector[EUiStroke]->setToolTip("Symbolic stroke color");
436   iSelector[EUiFill]->setToolTip("Symbolic fill color");
437   iSelector[EUiPen]->setToolTip("Symbolic pen width");
438   iSelector[EUiTextSize]->setToolTip("Symbolic text size");
439   iSelector[EUiMarkShape]->setToolTip("Mark shape");
440   iSelector[EUiSymbolSize]->setToolTip("Symbolic symbol size");
441   iSelector[EUiDashStyle]->setToolTip("Dash style");
442   iSelector[EUiOpacity]->setToolTip("Opacity");
443 
444   iSelector[EUiGridSize]->setToolTip("Grid size");
445   iSelector[EUiAngleSize]->setToolTip("Angle for angular snap");
446 
447   connect(comboMap, SIGNAL(mapped(int)), this, SLOT(comboSelector(int)));
448 
449   iPathView = new PathView(uiscale(100));
450   connect(iPathView, SIGNAL(activated(String)), SLOT(action(String)));
451   connect(iPathView, SIGNAL(showPathStylePopup(Vector)),
452 	  SLOT(showPathStylePopup(Vector)));
453   lo->addWidget(iPathView, 4, 1);
454   iPropertiesTools->setWidget(wg);
455 
456   iModeIndicator = new QLabel();
457   iModeIndicator->setPixmap(prefsPixmap("mode_select"));
458   lo->addWidget(iModeIndicator, 4, 0);
459 
460   QHBoxLayout *hol = new QHBoxLayout();
461   iViewNumber = new QToolButton();
462   iPageNumber = new QToolButton();
463   iViewNumber->setFocusPolicy(Qt::NoFocus);
464   iPageNumber->setFocusPolicy(Qt::NoFocus);
465   iViewNumber->setText("View 1/1");
466   iViewNumber->setToolTip("Current view number");
467   iPageNumber->setText("Page 1/1");
468   iPageNumber->setToolTip("Current page number");
469   iViewMarked = new QCheckBox();
470   iPageMarked = new QCheckBox();
471   iViewMarked->setFocusPolicy(Qt::NoFocus);
472   iPageMarked->setFocusPolicy(Qt::NoFocus);
473   bg->addButton(iViewNumber, EUiView);
474   bg->addButton(iPageNumber, EUiPage);
475   bg->addButton(iViewMarked, EUiViewMarked);
476   bg->addButton(iPageMarked, EUiPageMarked);
477   hol->setSpacing(0);
478   hol->addWidget(iViewMarked);
479   hol->addWidget(iViewNumber);
480   hol->addStretch(1);
481   hol->addWidget(iPageMarked);
482   hol->addWidget(iPageNumber);
483   lo->addLayout(hol, EUiOpacity + 2, 0, 1, -1);
484 
485   iPageNotes = new QTextEdit();
486   iPageNotes->setAcceptRichText(false);
487   iPageNotes->setReadOnly(true);
488   iPageNotes->setFocusPolicy(Qt::NoFocus);
489   iNotesTools->setWidget(iPageNotes);
490 
491   iBookmarks = new QListWidget();
492   iBookmarks->setToolTip("Bookmarks of this document");
493   iBookmarks->setFocusPolicy(Qt::NoFocus);
494   connect(iBookmarks, SIGNAL(itemActivated(QListWidgetItem *)),
495 	  this, SLOT(bookmarkSelected(QListWidgetItem *)));
496   iBookmarkTools->setWidget(iBookmarks);
497 
498   iLayerList = new LayerBox();
499   iLayerList->setToolTip("Layers of this page");
500   iLayerTools->setWidget(iLayerList);
501   connect(iLayerList, SIGNAL(activated(String, String)),
502 	  this, SLOT(layerAction(String, String)));
503   connect(iLayerList, SIGNAL(showLayerBoxPopup(Vector, String)),
504 	  SLOT(showLayerBoxPopup(Vector, String)));
505 
506   connect(iSelectLayerMenu, SIGNAL(aboutToShow()),
507 	  this, SLOT(aboutToShowSelectLayerMenu()));
508   connect(iMoveToLayerMenu, SIGNAL(aboutToShow()),
509 	  this, SLOT(aboutToShowMoveToLayerMenu()));
510   connect(iTextStyleMenu, SIGNAL(aboutToShow()),
511 	  this, SLOT(aboutToShowTextStyleMenu()));
512   connect(iLabelStyleMenu, SIGNAL(aboutToShow()),
513 	  this, SLOT(aboutToShowLabelStyleMenu()));
514   connect(iGridSizeMenu, SIGNAL(aboutToShow()),
515 	  this, SLOT(aboutToShowGridSizeMenu()));
516   connect(iAngleSizeMenu, SIGNAL(aboutToShow()),
517 	  this, SLOT(aboutToShowAngleSizeMenu()));
518 
519   iSnapIndicator = new QLabel(statusBar());
520   statusBar()->addPermanentWidget(iSnapIndicator, 0);
521   QFont font = iSnapIndicator->font();
522   font.setFamily("Monospace");
523   iSnapIndicator->setFont(font);
524 
525   iMouse = new QLabel(statusBar());
526   findAction("coordinates|points")->setChecked(true);
527   findAction("scaling|1")->setChecked(true);
528   statusBar()->addPermanentWidget(iMouse, 0);
529   iMouse->setFont(font);
530 
531   iResolution = new QLabel(statusBar());
532   statusBar()->addPermanentWidget(iResolution, 0);
533 
534   iCanvas->setObserver(this);
535 }
536 
~AppUi()537 AppUi::~AppUi()
538 {
539   ipeDebug("AppUi C++ destructor");
540 }
541 
542 // --------------------------------------------------------------------
543 
setRecentFileMenu(const std::vector<String> & names)544 void AppUi::setRecentFileMenu(const std::vector<String> & names)
545 {
546   iRecentFileMenu->clear();
547   for (const auto & name : names) {
548     QAction *a = new QAction(QIpe(name), iRecentFileMenu);
549     iRecentFileMenu->addAction(a);
550   }
551 }
552 
recentFileAction(QAction * a)553 void AppUi::recentFileAction(QAction *a)
554 {
555   luaRecentFileSelected(IpeQ(a->text()));
556 }
557 
populateLayerMenu(LayerBox * layerList,QMenu * menu)558 static void populateLayerMenu(LayerBox *layerList, QMenu *menu)
559 {
560   menu->clear();
561   for (int i = 0; i < layerList->count(); ++i) {
562     LayerItem *item = dynamic_cast<LayerItem *>(layerList->item(i));
563     if (item) {
564       QAction *a = new QAction(QIpe(item->ipeLayerName), menu);
565       menu->addAction(a);
566     }
567   }
568 
569 }
570 
aboutToShowSelectLayerMenu()571 void AppUi::aboutToShowSelectLayerMenu()
572 {
573   populateLayerMenu(iLayerList, iSelectLayerMenu);
574 }
575 
selectLayerAction(QAction * a)576 void AppUi::selectLayerAction(QAction *a)
577 {
578   action(String("selectinlayer-") + IpeQ(a->text()));
579 }
580 
aboutToShowMoveToLayerMenu()581 void AppUi::aboutToShowMoveToLayerMenu()
582 {
583   populateLayerMenu(iLayerList, iMoveToLayerMenu);
584 }
585 
moveToLayerAction(QAction * a)586 void AppUi::moveToLayerAction(QAction *a)
587 {
588   action(String("movetolayer-") + IpeQ(a->text()));
589 }
590 
aboutToShowStyleMenu(Kind kind,MENUHANDLE menu,String current)591 void AppUi::aboutToShowStyleMenu(Kind kind, MENUHANDLE menu, String current)
592 {
593   AttributeSeq seq;
594   iCascade->allNames(kind, seq);
595   menu->clear();
596   for (uint i = 0; i < seq.size(); ++i) {
597     String s = seq[i].string();
598     QAction *a = new QAction(QIpe(s), menu);
599     a->setCheckable(true);
600     if (s == current)
601       a->setChecked(true);
602     menu->addAction(a);
603   }
604 }
605 
aboutToShowTextStyleMenu()606 void AppUi::aboutToShowTextStyleMenu()
607 {
608   aboutToShowStyleMenu(ETextStyle, iTextStyleMenu, iAll.iTextStyle.string());
609 }
610 
aboutToShowLabelStyleMenu()611 void AppUi::aboutToShowLabelStyleMenu()
612 {
613   aboutToShowStyleMenu(ELabelStyle, iLabelStyleMenu, iAll.iLabelStyle.string());
614 }
615 
aboutToShowGridSizeMenu()616 void AppUi::aboutToShowGridSizeMenu()
617 {
618   iGridSizeMenu->clear();
619   for (auto &name : iComboContents[EUiGridSize]) {
620     QString s = QIpe(name);
621     QAction *a = new QAction(s, iGridSizeMenu);
622     a->setCheckable(true);
623     if (s == iSelector[EUiGridSize]->currentText())
624       a->setChecked(true);
625     iGridSizeMenu->addAction(a);
626   }
627 }
628 
aboutToShowAngleSizeMenu()629 void AppUi::aboutToShowAngleSizeMenu()
630 {
631   iAngleSizeMenu->clear();
632   for (auto &name : iComboContents[EUiAngleSize]) {
633     QString s = QIpe(name);
634     QAction *a = new QAction(s, iAngleSizeMenu);
635     a->setCheckable(true);
636     if (s == iSelector[EUiAngleSize]->currentText())
637       a->setChecked(true);
638     iAngleSizeMenu->addAction(a);
639   }
640 }
641 
textStyleAction(QAction * a)642 void AppUi::textStyleAction(QAction *a)
643 {
644   action(String("textstyle|") + IpeQ(a->text()));
645 }
646 
labelStyleAction(QAction * a)647 void AppUi::labelStyleAction(QAction *a)
648 {
649   action(String("labelstyle|") + IpeQ(a->text()));
650 }
651 
gridSizeAction(QAction * a)652 void AppUi::gridSizeAction(QAction *a)
653 {
654   action(String("gridsize|") + IpeQ(a->text()));
655 }
656 
angleSizeAction(QAction * a)657 void AppUi::angleSizeAction(QAction *a)
658 {
659   action(String("anglesize|") + IpeQ(a->text()));
660 }
661 
toolbarModifiersChanged()662 void AppUi::toolbarModifiersChanged()
663 {
664   if (iCanvas) {
665     int mod = 0;
666     if (iShiftKey->isChecked())
667       mod |= CanvasBase::EShift;
668     iCanvas->setAdditionalModifiers(mod);
669   }
670 }
671 
abortDrawing()672 void AppUi::abortDrawing()
673 {
674   action("stop");
675 }
676 
677 // --------------------------------------------------------------------
678 
resetCombos()679 void AppUi::resetCombos()
680 {
681   for (int i = 0; i < EUiView; ++i)
682     iSelector[i]->clear();
683 }
684 
addComboColors(AttributeSeq & sym,AttributeSeq & abs)685 void AppUi::addComboColors(AttributeSeq &sym, AttributeSeq &abs)
686 {
687   iSelector[EUiStroke]->addItem(IPEABSOLUTE);
688   iSelector[EUiFill]->addItem(IPEABSOLUTE);
689   iComboContents[EUiStroke].push_back(IPEABSOLUTE);
690   iComboContents[EUiFill].push_back(IPEABSOLUTE);
691   for (uint i = 0; i < sym.size(); ++i) {
692     Color color = abs[i].color();
693     String s = sym[i].string();
694     QIcon icon = prefsColorIcon(color);
695     iSelector[EUiStroke]->addItem(icon, QIpe(s));
696     iSelector[EUiFill]->addItem(icon, QIpe(s));
697     iComboContents[EUiStroke].push_back(s);
698     iComboContents[EUiFill].push_back(s);
699   }
700 }
701 
addCombo(int sel,String s)702 void AppUi::addCombo(int sel, String s)
703 {
704   iSelector[sel]->addItem(QIpe(s));
705 }
706 
setComboCurrent(int sel,int idx)707 void AppUi::setComboCurrent(int sel, int idx)
708 {
709   iSelector[sel]->setCurrentIndex(idx);
710 }
711 
setButtonColor(int sel,Color color)712 void AppUi::setButtonColor(int sel, Color color)
713 {
714   iButton[sel]->setIcon(prefsColorIcon(color));
715 }
716 
setPathView(const AllAttributes & all,Cascade * sheet)717 void AppUi::setPathView(const AllAttributes &all, Cascade *sheet)
718 {
719   iPathView->set(all, sheet);
720 }
721 
setCheckMark(String name,Attribute a)722 void AppUi::setCheckMark(String name, Attribute a)
723 {
724   String sa = name + "|";
725   int na = sa.size();
726   String sb = sa + a.string();
727   for (std::map<String, QAction *>::iterator it = iActions.begin();
728        it != iActions.end(); ++it) {
729     if (it->first.left(na) == sa)
730       it->second->setChecked(it->first == sb);
731   }
732 }
733 
setNumbers(String vno,bool vm,String pno,bool pm)734 void AppUi::setNumbers(String vno, bool vm, String pno, bool pm)
735 {
736   if (vno.empty()) {
737     iViewNumber->hide();
738     iViewMarked->hide();
739   } else {
740     iViewNumber->setText(QIpe(vno));
741     iViewNumber->show();
742     iViewMarked->setCheckState(vm ? Qt::Checked : Qt::Unchecked);
743     iViewMarked->show();
744   }
745   if (pno.empty()) {
746     iPageNumber->hide();
747     iPageMarked->hide();
748   } else {
749     iPageNumber->show();
750     iPageMarked->show();
751     iPageNumber->setText(QIpe(pno));
752     iPageMarked->setCheckState(pm ? Qt::Checked : Qt::Unchecked);
753   }
754 }
755 
setNotes(String notes)756 void AppUi::setNotes(String notes)
757 {
758   iPageNotes->setPlainText(QIpe(notes));
759 }
760 
setLayers(const Page * page,int view)761 void AppUi::setLayers(const Page *page, int view)
762 {
763   iLayerList->set(page, view);
764 }
765 
setBookmarks(int no,const String * s)766 void AppUi::setBookmarks(int no, const String *s)
767 {
768   iBookmarks->clear();
769   for (int i = 0; i < no; ++i) {
770     QListWidgetItem *item = new QListWidgetItem(QIpe(s[i]));
771     if (s[i][0] == ' ')
772       item->setTextColor(Qt::blue);
773     iBookmarks->addItem(item);
774   }
775 }
776 
bookmarkSelected(QListWidgetItem * item)777 void AppUi::bookmarkSelected(QListWidgetItem *item)
778 {
779   int index = iBookmarks->row(item);
780   luaBookmarkSelected(index);
781 }
782 
setToolVisible(int m,bool vis)783 void AppUi::setToolVisible(int m, bool vis)
784 {
785   QWidget *tool = nullptr;
786   switch (m) {
787   case 0: tool = iPropertiesTools; break;
788   case 1: tool = iBookmarkTools; break;
789   case 2: tool = iNotesTools; break;
790   case 3: tool = iLayerTools; break;
791   default: break;
792   }
793   if (vis)
794     tool->show();
795   else
796     tool->hide();
797 }
798 
setZoom(double zoom)799 void AppUi::setZoom(double zoom)
800 {
801   QString s;
802   s.sprintf("(%dppi)", int(72.0 * zoom));
803   iResolution->setText(s);
804   iCanvas->setZoom(zoom);
805 }
806 
807 // --------------------------------------------------------------------
808 
enableActions(QMenu * menu,bool mode)809 static void enableActions(QMenu *menu, bool mode)
810 {
811   menu->setEnabled(mode);
812   QListIterator<QAction *> it(menu->actions());
813   while (it.hasNext())
814     it.next()->setEnabled(mode);
815 }
816 
setActionsEnabled(bool mode)817 void AppUi::setActionsEnabled(bool mode)
818 {
819   enableActions(iMenu[EFileMenu], mode);
820   enableActions(iMenu[EEditMenu], mode);
821   enableActions(iMenu[EModeMenu], mode);
822   enableActions(iMenu[EPropertiesMenu], mode);
823   enableActions(iMenu[ELayerMenu], mode);
824   enableActions(iMenu[EViewMenu], mode);
825   enableActions(iMenu[EPageMenu], mode);
826   enableActions(iMenu[EIpeletMenu], mode);
827 
828   iModeActionGroup->setEnabled(mode);
829   iPropertiesTools->setEnabled(mode);
830   iLayerTools->setEnabled(mode);
831   iBookmarkTools->setEnabled(mode);
832 }
833 
834 // --------------------------------------------------------------------
835 
836 //! Window has been closed
closeEvent(QCloseEvent * ce)837 void AppUi::closeEvent(QCloseEvent* ce)
838 {
839   // calls model
840   lua_rawgeti(L, LUA_REGISTRYINDEX, iModel);
841   lua_getfield(L, -1, "closeEvent");
842   lua_pushvalue(L, -2); // model
843   lua_remove(L, -3);
844   lua_call(L, 1, 1);
845   bool result = lua_toboolean(L, -1);
846   if (result)
847     ce->accept();
848   else
849     ce->ignore();
850 }
851 
852 // --------------------------------------------------------------------
853 
854 // Determine if action is checked
855 // Used for snapXXX, grid_visible, viewmarked, and pagemarked
actionState(const char * name)856 bool AppUi::actionState(const char *name)
857 {
858   if (!strcmp(name, "viewmarked"))
859     return (iViewMarked->checkState() == Qt::Checked);
860   if (!strcmp(name, "pagemarked"))
861     return (iPageMarked->checkState() == Qt::Checked);
862 
863   QAction *a = findAction(name);
864   if (a)
865     return a->isChecked();
866   else
867     return 0;
868 }
869 
870 // Check/uncheck an action
871 // Used for snapXXX, grid_visible, to initialize mode_select
setActionState(const char * name,bool value)872 void AppUi::setActionState(const char *name, bool value)
873 {
874   QAction *a = findAction(name);
875   if (a) a->setChecked(value);
876 }
877 
absoluteButton(int id)878 void AppUi::absoluteButton(int id)
879 {
880   luaAbsoluteButton(selectorNames[id]);
881 }
882 
selector(int id,String value)883 void AppUi::selector(int id, String value)
884 {
885   luaSelector(String(selectorNames[id]), value);
886 }
887 
comboSelector(int id)888 void AppUi::comboSelector(int id)
889 {
890   luaSelector(String(selectorNames[id]), IpeQ(iSelector[id]->currentText()));
891 }
892 
893 // --------------------------------------------------------------------
894 
895 static const char * const aboutText =
896   "<qt><h1>Ipe %d.%d.%d</h1>"
897   "<p>Copyright (c) 1993-%d Otfried Cheong</p>"
898   "<p>The extensible drawing editor Ipe creates figures in PDF format, "
899   "using LaTeX to format the text in the figures.</p>"
900   "<p>Ipe is released under the GNU Public License.</p>"
901   "<p>See the <a href=\"http://ipe.otfried.org\">Ipe homepage</a>"
902   " for further information.</p>"
903   "<p>You can \"like\" Ipe and follow Ipe announcements on "
904   "<a href=\"http://www.facebook.com/drawing.editor.Ipe7\">Facebook</a>.</p>"
905   "<p>If you are an Ipe fan and want to show others, have a look at the "
906   "<a href=\"https://www.shirtee.com/en/store/ipe\">Ipe T-shirts</a>.</p>"
907   "<h3>Platinum and gold sponsors</h3>"
908   "<ul><li>Hee-Kap Ahn</li>"
909   "<li>Günter Rote</li>"
910   "<li>SCALGO</li>"
911   "<li>Martin Ziegler</li></ul>"
912   "<p>If you enjoy Ipe, feel free to treat the author on a cup of coffee at "
913   "<a href=\"https://ko-fi.com/ipe7author\">Ko-fi</a>.</p>"
914   "<p>You can also become a member of the exclusive community of "
915   "<a href=\"http://patreon.com/otfried\">Ipe patrons</a>. "
916   "For the price of a cup of coffee per month you can make a meaningful contribution "
917   "to the continuing development of Ipe.</p>"
918   "</qt>";
919 
aboutIpe()920 void AppUi::aboutIpe()
921 {
922   std::vector<char> buf(strlen(aboutText) + 100);
923   sprintf(&buf[0], aboutText,
924 	  IPELIB_VERSION / 10000,
925 	  (IPELIB_VERSION / 100) % 100,
926 	  IPELIB_VERSION % 100,
927 	  COPYRIGHT_YEAR);
928 
929   QMessageBox msgBox(this);
930   msgBox.setWindowTitle("About Ipe");
931   msgBox.setWindowIcon(prefsIcon("ipe"));
932   msgBox.setInformativeText(&buf[0]);
933   msgBox.setIconPixmap(prefsPixmap("ipe"));
934   msgBox.setStandardButtons(QMessageBox::Ok);
935   msgBox.exec();
936 }
937 
qAction(const QString & name)938 void AppUi::qAction(const QString &name)
939 {
940   action(IpeQ(name));
941 }
942 
action(String name)943 void AppUi::action(String name)
944 {
945   if (name == "fullscreen") {
946     setWindowState(windowState() ^ Qt::WindowFullScreen);
947   } else if (name == "about") {
948     aboutIpe();
949   } else {
950     if (name.left(5) == "mode_")
951       iModeIndicator->setPixmap(prefsPixmap(name));
952     luaAction(name);
953   }
954 }
955 
956 // --------------------------------------------------------------------
957 
pageSorter(lua_State * L,Document * doc,int pno,int width,int height,int thumbWidth)958 int AppUi::pageSorter(lua_State *L, Document *doc, int pno,
959 		      int width, int height, int thumbWidth)
960 {
961   QDialog *d = new QDialog();
962   if (pno >= 0)
963     d->setWindowTitle("Ipe View Sorter");
964   else
965     d->setWindowTitle("Ipe Page Sorter");
966 
967   QLayout *lo = new QVBoxLayout;
968   PageSorter *p = new PageSorter(doc, pno, thumbWidth);
969   QDialogButtonBox *buttonBox =
970     new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel);
971   lo->addWidget(p);
972   lo->addWidget(buttonBox);
973   d->setLayout(lo);
974 
975   d->connect(buttonBox, SIGNAL(accepted()), SLOT(accept()));
976   d->connect(buttonBox, SIGNAL(rejected()), SLOT(reject()));
977 
978   d->resize(width, height);
979 
980   if (d->exec() == QDialog::Rejected) {
981     delete d;
982     return 0;
983   }
984 
985   lua_createtable(L, p->count(), 0);
986   for (int i = 1; i <= p->count(); ++i) {
987     lua_pushinteger(L, p->pageAt(i-1) + 1);
988     lua_rawseti(L, -2, i);
989   }
990   int m = p->iMarks.size();
991   lua_createtable(L, m, 0);
992   for (int i = 1; i <= m; ++i) {
993     lua_pushboolean(L, p->iMarks[i-1]);
994     lua_rawseti(L, -2, i);
995   }
996   delete d;
997   return 2;
998 }
999 
1000 // --------------------------------------------------------------------
1001 
showPathStylePopup(Vector v)1002 void AppUi::showPathStylePopup(Vector v)
1003 {
1004   luaShowPathStylePopup(v);
1005 }
1006 
showLayerBoxPopup(Vector v,String layer)1007 void AppUi::showLayerBoxPopup(Vector v, String layer)
1008 {
1009   luaShowLayerBoxPopup(v, layer);
1010 }
1011 
layerAction(String name,String layer)1012 void AppUi::layerAction(String name, String layer)
1013 {
1014   luaLayerAction(name, layer);
1015 }
1016 
1017 // --------------------------------------------------------------------
1018 
windowId()1019 WINID AppUi::windowId()
1020 {
1021   return this;
1022 }
1023 
closeWindow()1024 void AppUi::closeWindow()
1025 {
1026   close();
1027 }
1028 
setWindowCaption(bool mod,const char * s)1029 void AppUi::setWindowCaption(bool mod, const char *s)
1030 {
1031   setWindowModified(mod);
1032   setWindowTitle(QString::fromUtf8(s));
1033 }
1034 
setMouseIndicator(const char * s)1035 void AppUi::setMouseIndicator(const char *s)
1036 {
1037   iMouse->setText(s);
1038 }
1039 
setSnapIndicator(const char * s)1040 void AppUi::setSnapIndicator(const char *s)
1041 {
1042   iSnapIndicator->setText(s);
1043 }
1044 
explain(const char * s,int t)1045 void AppUi::explain(const char *s, int t)
1046 {
1047   statusBar()->showMessage(QString::fromUtf8(s), t);
1048 }
1049 
showWindow(int width,int height,int x,int y)1050 void AppUi::showWindow(int width, int height, int x, int y)
1051 {
1052   if (width > 0 && height > 0)
1053     resize(width, height);
1054   if (x >= 0 && y >= 0)
1055     move(x, y);
1056   show();
1057 }
1058 
setFullScreen(int mode)1059 void AppUi::setFullScreen(int mode)
1060 {
1061   Qt::WindowStates state = windowState() & ~(Qt::WindowFullScreen | Qt::WindowMaximized);
1062   switch (mode) {
1063   case 1:
1064     state |= Qt::WindowMaximized;
1065     break;
1066   case 2:
1067     state |= Qt::WindowFullScreen;
1068     break;
1069   default:
1070     break;
1071   }
1072   setWindowState(state);
1073 }
1074 
setClipboard(lua_State * L)1075 int AppUi::setClipboard(lua_State *L)
1076 {
1077   QString data = QString::fromUtf8(luaL_checkstring(L, 2));
1078   QClipboard *cb = QApplication::clipboard();
1079   cb->setText(data);
1080   return 0;
1081 }
1082 
getImage(lua_State * L,QImage & img)1083 static int getImage(lua_State *L, QImage &img)
1084 {
1085   QImage im1 = img.convertToFormat(QImage::Format_ARGB32);
1086   int w = im1.width();
1087   int h = im1.height();
1088   Buffer data(w * h * sizeof(uint32_t));
1089   uint32_t *d = (uint32_t *) data.data();
1090   for (int y = 0; y < h; ++y) {
1091     uint32_t *p = (uint32_t *) im1.scanLine(y);
1092     for (int x = 0; x < w; ++x) {
1093       *d++ = *p++;
1094     }
1095   }
1096 
1097   Bitmap bitmap(w, h, Bitmap::ENative, data);
1098   Rect r(Vector::ZERO, Vector(w, h));
1099   Image *im = new Image(r, bitmap);
1100   push_object(L, im);
1101   return 1;
1102 }
1103 
clipboard(lua_State * L)1104 int AppUi::clipboard(lua_State *L)
1105 {
1106   bool allow_bitmap = lua_toboolean(L, 2);
1107   const QClipboard *cb = QApplication::clipboard();
1108   if (allow_bitmap) {
1109     const QMimeData *md = cb->mimeData();
1110     if (md->hasUrls()) {
1111       auto urls = md->urls();
1112       if (urls.size() == 1 && urls[0].isLocalFile()) {
1113 	if (readImage(L, IpeQ(urls[0].toLocalFile())))
1114 	  return 1;
1115       }
1116     }
1117     QImage img = cb->image();
1118     if (!img.isNull()) {
1119       return getImage(L, img);
1120     }
1121   }
1122   QString data = cb->text();
1123   lua_pushstring(L, data.toUtf8());
1124   return 1;
1125 }
1126 
createAppUi(lua_State * L0,int model)1127 AppUiBase *createAppUi(lua_State *L0, int model)
1128 {
1129   return new AppUi(L0, model);
1130 }
1131 
1132 // --------------------------------------------------------------------
1133