1 // Aseprite
2 // Copyright (C) 2001-2018  David Capello
3 //
4 // This program is distributed under the terms of
5 // the End-User License Agreement for Aseprite.
6 
7 #ifdef HAVE_CONFIG_H
8 #include "config.h"
9 #endif
10 
11 #include "app/app.h"
12 #include "app/app_menus.h"
13 #include "app/commands/cmd_open_file.h"
14 #include "app/commands/command.h"
15 #include "app/commands/commands.h"
16 #include "app/commands/params.h"
17 #include "app/console.h"
18 #include "app/crash/data_recovery.h"
19 #include "app/doc.h"
20 #include "app/ini_file.h"
21 #include "app/modules/editors.h"
22 #include "app/modules/gfx.h"
23 #include "app/modules/gui.h"
24 #include "app/modules/palettes.h"
25 #include "app/pref/preferences.h"
26 #include "app/tools/ink.h"
27 #include "app/tools/tool_box.h"
28 #include "app/ui/editor/editor.h"
29 #include "app/ui/keyboard_shortcuts.h"
30 #include "app/ui/main_menu_bar.h"
31 #include "app/ui/main_menu_bar.h"
32 #include "app/ui/main_window.h"
33 #include "app/ui/skin/skin_property.h"
34 #include "app/ui/skin/skin_theme.h"
35 #include "app/ui/status_bar.h"
36 #include "app/ui/toolbar.h"
37 #include "app/ui_context.h"
38 #include "base/memory.h"
39 #include "base/shared_ptr.h"
40 #include "base/unique_ptr.h"
41 #include "doc/sprite.h"
42 #include "she/display.h"
43 #include "she/error.h"
44 #include "she/surface.h"
45 #include "she/system.h"
46 #include "ui/intern.h"
47 #include "ui/ui.h"
48 
49 #include <algorithm>
50 #include <list>
51 #include <vector>
52 
53 #if defined(_DEBUG) && defined(ENABLE_DATA_RECOVERY)
54 #include "app/crash/data_recovery.h"
55 #include "app/modules/editors.h"
56 #endif
57 
58 namespace app {
59 
60 using namespace gfx;
61 using namespace ui;
62 using namespace app::skin;
63 
64 static struct {
65   int width;
66   int height;
67   int scale;
68 } try_resolutions[] = { { 1024, 768, 2 },
69                         {  800, 600, 2 },
70                         {  640, 480, 2 },
71                         {  320, 240, 1 },
72                         {  320, 200, 1 },
73                         {    0,   0, 0 } };
74 
75 //////////////////////////////////////////////////////////////////////
76 
77 class CustomizedGuiManager : public Manager
78                            , public LayoutIO {
79 protected:
80   bool onProcessMessage(Message* msg) override;
81 #if ENABLE_DEVMODE
82   bool onProcessDevModeKeyDown(KeyMessage* msg);
83 #endif
84   void onInitTheme(InitThemeEvent& ev) override;
onGetLayoutIO()85   LayoutIO* onGetLayoutIO() override { return this; }
86   void onNewDisplayConfiguration() override;
87 
88   // LayoutIO implementation
89   std::string loadLayout(Widget* widget) override;
90   void saveLayout(Widget* widget, const std::string& str) override;
91 };
92 
93 static she::Display* main_display = NULL;
94 static CustomizedGuiManager* manager = NULL;
95 static Theme* gui_theme = NULL;
96 
97 static ui::Timer* defered_invalid_timer = nullptr;
98 static gfx::Region defered_invalid_region;
99 
100 // Load & save graphics configuration
101 static void load_gui_config(int& w, int& h, bool& maximized,
102                             std::string& windowLayout);
103 static void save_gui_config();
104 
create_main_display(bool gpuAccel,bool & maximized,std::string & lastError)105 static bool create_main_display(bool gpuAccel,
106                                 bool& maximized,
107                                 std::string& lastError)
108 {
109   int w, h;
110   std::string windowLayout;
111   load_gui_config(w, h, maximized, windowLayout);
112 
113   // Scale is equal to 0 when it's the first time the program is
114   // executed.
115   int scale = Preferences::instance().general.screenScale();
116 
117   she::instance()->setGpuAcceleration(gpuAccel);
118 
119   try {
120     if (w > 0 && h > 0) {
121       main_display = she::instance()->createDisplay(
122         w, h, (scale == 0 ? 2: MID(1, scale, 4)));
123     }
124   }
125   catch (const she::DisplayCreationException& e) {
126     lastError = e.what();
127   }
128 
129   if (!main_display) {
130     for (int c=0; try_resolutions[c].width; ++c) {
131       try {
132         main_display =
133           she::instance()->createDisplay(
134             try_resolutions[c].width,
135             try_resolutions[c].height,
136             (scale == 0 ? try_resolutions[c].scale: scale));
137         break;
138       }
139       catch (const she::DisplayCreationException& e) {
140         lastError = e.what();
141       }
142     }
143   }
144 
145   if (main_display) {
146     // Change the scale value only in the first run (this will be
147     // saved when the program is closed).
148     if (scale == 0)
149       Preferences::instance().general.screenScale(main_display->scale());
150 
151     if (!windowLayout.empty()) {
152       main_display->setLayout(windowLayout);
153       if (main_display->isMinimized())
154         main_display->maximize();
155     }
156   }
157 
158   return (main_display != nullptr);
159 }
160 
161 // Initializes GUI.
init_module_gui()162 int init_module_gui()
163 {
164   auto& pref = Preferences::instance();
165   bool maximized = false;
166   std::string lastError = "Unknown error";
167   bool gpuAccel = pref.general.gpuAcceleration();
168 
169   if (!create_main_display(gpuAccel, maximized, lastError)) {
170     // If we've created the display with hardware acceleration,
171     // now we try to do it without hardware acceleration.
172     if (gpuAccel &&
173         (int(she::instance()->capabilities()) &
174          int(she::Capabilities::GpuAccelerationSwitch)) == int(she::Capabilities::GpuAccelerationSwitch)) {
175       if (create_main_display(false, maximized, lastError)) {
176         // Disable hardware acceleration
177         pref.general.gpuAcceleration(false);
178       }
179     }
180   }
181 
182   if (!main_display) {
183     she::error_message(
184       ("Unable to create a user-interface display.\nDetails: "+lastError+"\n").c_str());
185     return -1;
186   }
187 
188   // Create the default-manager
189   manager = new CustomizedGuiManager();
190   manager->setDisplay(main_display);
191 
192   // Setup the GUI theme for all widgets
193   gui_theme = new SkinTheme;
194   ui::set_theme(gui_theme, pref.general.uiScale());
195 
196   if (maximized)
197     main_display->maximize();
198 
199   // Set graphics options for next time
200   save_gui_config();
201 
202   return 0;
203 }
204 
exit_module_gui()205 void exit_module_gui()
206 {
207   save_gui_config();
208 
209   delete defered_invalid_timer;
210   delete manager;
211 
212   // Now we can destroy theme
213   ui::set_theme(nullptr, ui::guiscale());
214   delete gui_theme;
215 
216   main_display->dispose();
217 }
218 
load_gui_config(int & w,int & h,bool & maximized,std::string & windowLayout)219 static void load_gui_config(int& w, int& h, bool& maximized,
220                             std::string& windowLayout)
221 {
222   gfx::Size defSize = she::instance()->defaultNewDisplaySize();
223 
224   w = get_config_int("GfxMode", "Width", defSize.w);
225   h = get_config_int("GfxMode", "Height", defSize.h);
226   maximized = get_config_bool("GfxMode", "Maximized", false);
227   windowLayout = get_config_string("GfxMode", "WindowLayout", "");
228 }
229 
save_gui_config()230 static void save_gui_config()
231 {
232   she::Display* display = manager->getDisplay();
233   if (display) {
234     set_config_bool("GfxMode", "Maximized", display->isMaximized());
235     set_config_int("GfxMode", "Width", display->originalWidth());
236     set_config_int("GfxMode", "Height", display->originalHeight());
237 
238     std::string windowLayout = display->getLayout();
239     if (!windowLayout.empty())
240       set_config_string("GfxMode", "WindowLayout", windowLayout.c_str());
241   }
242 }
243 
update_screen_for_document(const Doc * document)244 void update_screen_for_document(const Doc* document)
245 {
246   // Without document.
247   if (!document) {
248     // Well, change to the default palette.
249     if (set_current_palette(NULL, false)) {
250       // If the palette changes, refresh the whole screen.
251       if (manager)
252         manager->invalidate();
253     }
254   }
255   // With a document.
256   else {
257     const_cast<Doc*>(document)->notifyGeneralUpdate();
258 
259     // Update the tabs (maybe the modified status has been changed).
260     app_rebuild_documents_tabs();
261   }
262 }
263 
load_window_pos(Widget * window,const char * section)264 void load_window_pos(Widget* window, const char *section)
265 {
266   // Default position
267   Rect orig_pos = window->bounds();
268   Rect pos = orig_pos;
269 
270   // Load configurated position
271   pos = get_config_rect(section, "WindowPos", pos);
272 
273   pos.w = MID(orig_pos.w, pos.w, ui::display_w());
274   pos.h = MID(orig_pos.h, pos.h, ui::display_h());
275 
276   pos.setOrigin(Point(MID(0, pos.x, ui::display_w()-pos.w),
277       MID(0, pos.y, ui::display_h()-pos.h)));
278 
279   window->setBounds(pos);
280 }
281 
save_window_pos(Widget * window,const char * section)282 void save_window_pos(Widget* window, const char *section)
283 {
284   set_config_rect(section, "WindowPos", window->bounds());
285 }
286 
287 // TODO Replace this with new theme styles
setup_mini_font(Widget * widget)288 Widget* setup_mini_font(Widget* widget)
289 {
290   SkinPropertyPtr skinProp = get_skin_property(widget);
291   skinProp->setMiniFont();
292   return widget;
293 }
294 
295 // TODO Replace this with new theme styles
setup_mini_look(Widget * widget)296 Widget* setup_mini_look(Widget* widget)
297 {
298   SkinPropertyPtr skinProp = get_skin_property(widget);
299   skinProp->setLook(MiniLook);
300   return widget;
301 }
302 
303 //////////////////////////////////////////////////////////////////////
304 // Button style (convert radio or check buttons and draw it like
305 // normal buttons)
306 
defer_invalid_rect(const gfx::Rect & rc)307 void defer_invalid_rect(const gfx::Rect& rc)
308 {
309   if (!defered_invalid_timer)
310     defered_invalid_timer = new ui::Timer(250, manager);
311 
312   defered_invalid_timer->stop();
313   defered_invalid_timer->start();
314   defered_invalid_region.createUnion(defered_invalid_region, gfx::Region(rc));
315 }
316 
317 // Manager event handler.
onProcessMessage(Message * msg)318 bool CustomizedGuiManager::onProcessMessage(Message* msg)
319 {
320   switch (msg->type()) {
321 
322     case kCloseDisplayMessage:
323       {
324         // Execute the "Exit" command.
325         Command* command = Commands::instance()->byId(CommandId::Exit());
326         UIContext::instance()->executeCommand(command);
327       }
328       break;
329 
330     case kDropFilesMessage:
331       {
332         base::paths files = static_cast<DropFilesMessage*>(msg)->files();
333         UIContext* ctx = UIContext::instance();
334         OpenFileCommand cmd;
335 
336         while (!files.empty()) {
337           auto fn = files.front();
338           files.erase(files.begin());
339 
340           // If the document is already open, select it.
341           Doc* doc = ctx->documents().getByFileName(fn);
342           if (doc) {
343             DocView* docView = ctx->getFirstDocView(doc);
344             if (docView)
345               ctx->setActiveView(docView);
346             else {
347               ASSERT(false);    // Must be some DocView available
348             }
349           }
350           // Load the file
351           else {
352             Params params;
353             params.set("filename", fn.c_str());
354             params.set("repeat_checkbox", "true");
355             ctx->executeCommand(&cmd, params);
356 
357             // Remove all used file names from the "dropped files"
358             for (const auto& usedFn : cmd.usedFiles()) {
359               auto it = std::find(files.begin(), files.end(), usedFn);
360               if (it != files.end())
361                 files.erase(it);
362             }
363           }
364         }
365       }
366       break;
367 
368     case kKeyDownMessage: {
369 #if ENABLE_DEVMODE
370       if (onProcessDevModeKeyDown(static_cast<KeyMessage*>(msg)))
371         return true;
372 #endif  // ENABLE_DEVMODE
373 
374       // Call base impl to check if there is a foreground window as
375       // top level that needs keys. (In this way we just do not
376       // process keyboard shortcuts for menus and tools).
377       if (Manager::onProcessMessage(msg))
378         return true;
379 
380       KeyboardShortcuts* keys = KeyboardShortcuts::instance();
381       for (const KeyPtr& key : *keys) {
382         if (key->isPressed(msg, *keys)) {
383           // Cancel menu-bar loops (to close any popup menu)
384           App::instance()->mainWindow()->getMenuBar()->cancelMenuLoop();
385 
386           switch (key->type()) {
387 
388             case KeyType::Tool: {
389               tools::Tool* current_tool = App::instance()->activeTool();
390               tools::Tool* select_this_tool = key->tool();
391               tools::ToolBox* toolbox = App::instance()->toolBox();
392               std::vector<tools::Tool*> possibles;
393 
394               // Collect all tools with the pressed keyboard-shortcut
395               for (tools::Tool* tool : *toolbox) {
396                 const KeyPtr key = KeyboardShortcuts::instance()->tool(tool);
397                 if (key && key->isPressed(msg, *keys))
398                   possibles.push_back(tool);
399               }
400 
401               if (possibles.size() >= 2) {
402                 bool done = false;
403 
404                 for (size_t i=0; i<possibles.size(); ++i) {
405                   if (possibles[i] != current_tool &&
406                       ToolBar::instance()->isToolVisible(possibles[i])) {
407                     select_this_tool = possibles[i];
408                     done = true;
409                     break;
410                   }
411                 }
412 
413                 if (!done) {
414                   for (size_t i=0; i<possibles.size(); ++i) {
415                     // If one of the possibilities is the current tool
416                     if (possibles[i] == current_tool) {
417                       // We select the next tool in the possibilities
418                       select_this_tool = possibles[(i+1) % possibles.size()];
419                       break;
420                     }
421                   }
422                 }
423               }
424 
425               ToolBar::instance()->selectTool(select_this_tool);
426               return true;
427             }
428 
429             case KeyType::Command: {
430               Command* command = key->command();
431 
432               // Commands are executed only when the main window is
433               // the current window running.
434               if (getForegroundWindow() == App::instance()->mainWindow()) {
435                 // OK, so we can execute the command represented
436                 // by the pressed-key in the message...
437                 UIContext::instance()->executeCommand(
438                   command, key->params());
439                 return true;
440               }
441               break;
442             }
443 
444             case KeyType::Quicktool: {
445               // Do nothing, it is used in the editor through the
446               // KeyboardShortcuts::getCurrentQuicktool() function.
447               break;
448             }
449 
450           }
451           break;
452         }
453       }
454       break;
455     }
456 
457     case kTimerMessage:
458       if (static_cast<TimerMessage*>(msg)->timer() == defered_invalid_timer) {
459         invalidateDisplayRegion(defered_invalid_region);
460         defered_invalid_region.clear();
461         defered_invalid_timer->stop();
462       }
463       break;
464 
465   }
466 
467   return Manager::onProcessMessage(msg);
468 }
469 
470 #if ENABLE_DEVMODE
onProcessDevModeKeyDown(KeyMessage * msg)471 bool CustomizedGuiManager::onProcessDevModeKeyDown(KeyMessage* msg)
472 {
473   // Ctrl+Shift+Q generates a crash (useful to test the anticrash feature)
474   if (msg->ctrlPressed() &&
475       msg->shiftPressed() &&
476       msg->scancode() == kKeyQ) {
477     int* p = nullptr;
478     *p = 0;      // *Crash*
479     return true; // This line should not be executed anyway
480   }
481 
482   // F1 switches screen/UI scaling
483   if (msg->ctrlPressed() &&
484       msg->scancode() == kKeyF1) {
485     try {
486       she::Display* display = getDisplay();
487       int screenScale = display->scale();
488       int uiScale = ui::guiscale();
489 
490       if (msg->shiftPressed()) {
491         if (screenScale == 2 && uiScale == 1) {
492           screenScale = 1;
493           uiScale = 1;
494         }
495         else if (screenScale == 1 && uiScale == 1) {
496           screenScale = 1;
497           uiScale = 2;
498         }
499         else if (screenScale == 1 && uiScale == 2) {
500           screenScale = 2;
501           uiScale = 1;
502         }
503       }
504       else {
505         if (screenScale == 2 && uiScale == 1) {
506           screenScale = 1;
507           uiScale = 2;
508         }
509         else if (screenScale == 1 && uiScale == 2) {
510           screenScale = 1;
511           uiScale = 1;
512         }
513         else if (screenScale == 1 && uiScale == 1) {
514           screenScale = 2;
515           uiScale = 1;
516         }
517       }
518 
519       if (uiScale != ui::guiscale()) {
520         ui::set_theme(ui::get_theme(), uiScale);
521       }
522       if (screenScale != display->scale()) {
523         display->setScale(screenScale);
524         setDisplay(display);
525       }
526     }
527     catch (const std::exception& ex) {
528       Console::showException(ex);
529     }
530     return true;
531   }
532 
533 #ifdef ENABLE_DATA_RECOVERY
534   // Ctrl+Shift+R recover active sprite from the backup store
535   if (msg->ctrlPressed() &&
536       msg->shiftPressed() &&
537       msg->scancode() == kKeyR &&
538       App::instance()->dataRecovery() &&
539       App::instance()->dataRecovery()->activeSession() &&
540       current_editor &&
541       current_editor->document()) {
542     App::instance()
543       ->dataRecovery()
544       ->activeSession()
545       ->restoreBackupById(current_editor->document()->id());
546     return true;
547   }
548 #endif  // ENABLE_DATA_RECOVERY
549 
550   return false;
551 }
552 #endif  // ENABLE_DEVMODE
553 
onInitTheme(InitThemeEvent & ev)554 void CustomizedGuiManager::onInitTheme(InitThemeEvent& ev)
555 {
556   Manager::onInitTheme(ev);
557 
558   // Update the theme on all menus
559   AppMenus::instance()->initTheme();
560 }
561 
onNewDisplayConfiguration()562 void CustomizedGuiManager::onNewDisplayConfiguration()
563 {
564   Manager::onNewDisplayConfiguration();
565   save_gui_config();
566 }
567 
loadLayout(Widget * widget)568 std::string CustomizedGuiManager::loadLayout(Widget* widget)
569 {
570   if (widget->window() == nullptr)
571     return "";
572 
573   std::string windowId = widget->window()->id();
574   std::string widgetId = widget->id();
575 
576   return get_config_string(("layout:"+windowId).c_str(), widgetId.c_str(), "");
577 }
578 
saveLayout(Widget * widget,const std::string & str)579 void CustomizedGuiManager::saveLayout(Widget* widget, const std::string& str)
580 {
581   if (widget->window() == NULL)
582     return;
583 
584   std::string windowId = widget->window()->id();
585   std::string widgetId = widget->id();
586 
587   set_config_string(("layout:"+windowId).c_str(),
588                     widgetId.c_str(),
589                     str.c_str());
590 }
591 
592 } // namespace app
593