| 1 | // Aseprite |
| 2 | // Copyright (C) 2018-2022 Igara Studio S.A. |
| 3 | // Copyright (C) 2001-2018 David Capello |
| 4 | // |
| 5 | // This program is distributed under the terms of |
| 6 | // the End-User License Agreement for Aseprite. |
| 7 | |
| 8 | #ifdef HAVE_CONFIG_H |
| 9 | #include "config.h" |
| 10 | #endif |
| 11 | |
| 12 | #include "app/app.h" |
| 13 | #include "app/app_menus.h" |
| 14 | #include "app/commands/cmd_open_file.h" |
| 15 | #include "app/commands/command.h" |
| 16 | #include "app/commands/commands.h" |
| 17 | #include "app/commands/params.h" |
| 18 | #include "app/console.h" |
| 19 | #include "app/crash/data_recovery.h" |
| 20 | #include "app/doc.h" |
| 21 | #include "app/ini_file.h" |
| 22 | #include "app/modules/editors.h" |
| 23 | #include "app/modules/gfx.h" |
| 24 | #include "app/modules/gui.h" |
| 25 | #include "app/modules/palettes.h" |
| 26 | #include "app/pref/preferences.h" |
| 27 | #include "app/tools/ink.h" |
| 28 | #include "app/tools/tool_box.h" |
| 29 | #include "app/ui/editor/editor.h" |
| 30 | #include "app/ui/keyboard_shortcuts.h" |
| 31 | #include "app/ui/main_menu_bar.h" |
| 32 | #include "app/ui/main_menu_bar.h" |
| 33 | #include "app/ui/main_window.h" |
| 34 | #include "app/ui/skin/skin_property.h" |
| 35 | #include "app/ui/skin/skin_theme.h" |
| 36 | #include "app/ui/status_bar.h" |
| 37 | #include "app/ui/toolbar.h" |
| 38 | #include "app/ui_context.h" |
| 39 | #include "app/util/open_batch.h" |
| 40 | #include "base/fs.h" |
| 41 | #include "base/memory.h" |
| 42 | #include "base/string.h" |
| 43 | #include "doc/sprite.h" |
| 44 | #include "os/error.h" |
| 45 | #include "os/screen.h" |
| 46 | #include "os/surface.h" |
| 47 | #include "os/system.h" |
| 48 | #include "os/window.h" |
| 49 | #include "ui/intern.h" |
| 50 | #include "ui/ui.h" |
| 51 | |
| 52 | #ifdef ENABLE_STEAM |
| 53 | #include "steam/steam.h" |
| 54 | #endif |
| 55 | |
| 56 | #include <algorithm> |
| 57 | #include <cstdio> |
| 58 | #include <list> |
| 59 | #include <vector> |
| 60 | |
| 61 | #if defined(_DEBUG) && defined(ENABLE_DATA_RECOVERY) |
| 62 | #include "app/crash/data_recovery.h" |
| 63 | #include "app/modules/editors.h" |
| 64 | #endif |
| 65 | |
| 66 | namespace app { |
| 67 | |
| 68 | using namespace gfx; |
| 69 | using namespace ui; |
| 70 | using namespace app::skin; |
| 71 | |
| 72 | static struct { |
| 73 | int width; |
| 74 | int height; |
| 75 | int scale; |
| 76 | } try_resolutions[] = { { 1024, 768, 2 }, |
| 77 | { 800, 600, 2 }, |
| 78 | { 640, 480, 2 }, |
| 79 | { 320, 240, 1 }, |
| 80 | { 320, 200, 1 }, |
| 81 | { 0, 0, 0 } }; |
| 82 | |
| 83 | ////////////////////////////////////////////////////////////////////// |
| 84 | |
| 85 | class CustomizedGuiManager : public ui::Manager |
| 86 | , public ui::LayoutIO { |
| 87 | public: |
| 88 | CustomizedGuiManager(const os::WindowRef& nativeWindow) |
| 89 | : ui::Manager(nativeWindow) { |
| 90 | } |
| 91 | |
| 92 | protected: |
| 93 | bool onProcessMessage(Message* msg) override; |
| 94 | #if ENABLE_DEVMODE |
| 95 | bool onProcessDevModeKeyDown(KeyMessage* msg); |
| 96 | #endif |
| 97 | void onInitTheme(InitThemeEvent& ev) override; |
| 98 | LayoutIO* onGetLayoutIO() override { return this; } |
| 99 | void onNewDisplayConfiguration(Display* display) override; |
| 100 | |
| 101 | // LayoutIO implementation |
| 102 | std::string loadLayout(Widget* widget) override; |
| 103 | void saveLayout(Widget* widget, const std::string& str) override; |
| 104 | }; |
| 105 | |
| 106 | static os::WindowRef main_window = nullptr; |
| 107 | static CustomizedGuiManager* manager = nullptr; |
| 108 | static Theme* gui_theme = nullptr; |
| 109 | |
| 110 | static ui::Timer* defered_invalid_timer = nullptr; |
| 111 | static gfx::Region defered_invalid_region; |
| 112 | |
| 113 | // Load & save graphics configuration |
| 114 | static bool load_gui_config(os::WindowSpec& spec, bool& maximized); |
| 115 | static void save_gui_config(); |
| 116 | |
| 117 | static bool create_main_window(bool gpuAccel, |
| 118 | bool& maximized, |
| 119 | std::string& lastError) |
| 120 | { |
| 121 | os::WindowSpec spec; |
| 122 | if (!load_gui_config(spec, maximized)) |
| 123 | return false; |
| 124 | |
| 125 | // Scale is equal to 0 when it's the first time the program is |
| 126 | // executed. |
| 127 | int scale = Preferences::instance().general.screenScale(); |
| 128 | |
| 129 | os::instance()->setGpuAcceleration(gpuAccel); |
| 130 | |
| 131 | try { |
| 132 | if (!spec.frame().isEmpty() || |
| 133 | !spec.contentRect().isEmpty()) { |
| 134 | spec.scale(scale == 0 ? 2: std::clamp(scale, 1, 4)); |
| 135 | main_window = os::instance()->makeWindow(spec); |
| 136 | } |
| 137 | } |
| 138 | catch (const os::WindowCreationException& e) { |
| 139 | lastError = e.what(); |
| 140 | } |
| 141 | |
| 142 | if (!main_window) { |
| 143 | for (int c=0; try_resolutions[c].width; ++c) { |
| 144 | try { |
| 145 | spec.frame(); |
| 146 | spec.position(os::WindowSpec::Position::Default); |
| 147 | spec.scale(scale == 0 ? try_resolutions[c].scale: scale); |
| 148 | spec.contentRect(gfx::Rect(0, 0, |
| 149 | try_resolutions[c].width * spec.scale(), |
| 150 | try_resolutions[c].height * spec.scale())); |
| 151 | main_window = os::instance()->makeWindow(spec); |
| 152 | break; |
| 153 | } |
| 154 | catch (const os::WindowCreationException& e) { |
| 155 | lastError = e.what(); |
| 156 | } |
| 157 | } |
| 158 | } |
| 159 | |
| 160 | if (main_window) { |
| 161 | // Change the scale value only in the first run (this will be |
| 162 | // saved when the program is closed). |
| 163 | if (scale == 0) |
| 164 | Preferences::instance().general.screenScale(main_window->scale()); |
| 165 | |
| 166 | if (main_window->isMinimized()) |
| 167 | main_window->maximize(); |
| 168 | } |
| 169 | |
| 170 | return (main_window != nullptr); |
| 171 | } |
| 172 | |
| 173 | // Initializes GUI. |
| 174 | int init_module_gui() |
| 175 | { |
| 176 | auto& pref = Preferences::instance(); |
| 177 | bool maximized = false; |
| 178 | std::string lastError = "Unknown error" ; |
| 179 | bool gpuAccel = pref.general.gpuAcceleration(); |
| 180 | |
| 181 | if (!create_main_window(gpuAccel, maximized, lastError)) { |
| 182 | // If we've created the native window with hardware acceleration, |
| 183 | // now we try to do it without hardware acceleration. |
| 184 | if (gpuAccel && |
| 185 | os::instance()->hasCapability(os::Capabilities::GpuAccelerationSwitch)) { |
| 186 | if (create_main_window(false, maximized, lastError)) { |
| 187 | // Disable hardware acceleration |
| 188 | pref.general.gpuAcceleration(false); |
| 189 | } |
| 190 | } |
| 191 | } |
| 192 | |
| 193 | if (!main_window) { |
| 194 | os::error_message( |
| 195 | ("Unable to create a user-interface window.\nDetails: " +lastError+"\n" ).c_str()); |
| 196 | return -1; |
| 197 | } |
| 198 | |
| 199 | // Create the default-manager |
| 200 | manager = new CustomizedGuiManager(main_window); |
| 201 | |
| 202 | // Setup the GUI theme for all widgets |
| 203 | gui_theme = new SkinTheme; |
| 204 | ui::set_theme(gui_theme, pref.general.uiScale()); |
| 205 | |
| 206 | if (maximized) |
| 207 | main_window->maximize(); |
| 208 | |
| 209 | // Handle live resize too redraw the entire manager, dispatch the UI |
| 210 | // messages, and flip the window. |
| 211 | os::instance()->handleWindowResize = |
| 212 | [](os::Window* window) { |
| 213 | Display* display = Manager::getDisplayFromNativeWindow(window); |
| 214 | if (!display) |
| 215 | display = manager->display(); |
| 216 | ASSERT(display); |
| 217 | |
| 218 | Message* msg = new Message(kResizeDisplayMessage); |
| 219 | msg->setDisplay(display); |
| 220 | msg->setRecipient(manager); |
| 221 | msg->setPropagateToChildren(false); |
| 222 | |
| 223 | manager->enqueueMessage(msg); |
| 224 | manager->dispatchMessages(); |
| 225 | }; |
| 226 | |
| 227 | // Set graphics options for next time |
| 228 | save_gui_config(); |
| 229 | |
| 230 | update_windows_color_profile_from_preferences(); |
| 231 | |
| 232 | return 0; |
| 233 | } |
| 234 | |
| 235 | void exit_module_gui() |
| 236 | { |
| 237 | save_gui_config(); |
| 238 | |
| 239 | delete defered_invalid_timer; |
| 240 | delete manager; |
| 241 | |
| 242 | // Now we can destroy theme |
| 243 | ui::set_theme(nullptr, ui::guiscale()); |
| 244 | delete gui_theme; |
| 245 | |
| 246 | // This should be the last unref() of the display to delete it. |
| 247 | main_window.reset(); |
| 248 | } |
| 249 | |
| 250 | void update_windows_color_profile_from_preferences() |
| 251 | { |
| 252 | auto system = os::instance(); |
| 253 | |
| 254 | gen::WindowColorProfile windowProfile; |
| 255 | if (Preferences::instance().color.manage()) |
| 256 | windowProfile = Preferences::instance().color.windowProfile(); |
| 257 | else |
| 258 | windowProfile = gen::WindowColorProfile::SRGB; |
| 259 | |
| 260 | os::ColorSpaceRef osCS = nullptr; |
| 261 | |
| 262 | switch (windowProfile) { |
| 263 | case gen::WindowColorProfile::MONITOR: |
| 264 | osCS = nullptr; |
| 265 | break; |
| 266 | case gen::WindowColorProfile::SRGB: |
| 267 | osCS = system->makeColorSpace(gfx::ColorSpace::MakeSRGB()); |
| 268 | break; |
| 269 | case gen::WindowColorProfile::SPECIFIC: { |
| 270 | std::string name = |
| 271 | Preferences::instance().color.windowProfileName(); |
| 272 | |
| 273 | std::vector<os::ColorSpaceRef> colorSpaces; |
| 274 | system->listColorSpaces(colorSpaces); |
| 275 | |
| 276 | for (auto& cs : colorSpaces) { |
| 277 | auto gfxCs = cs->gfxColorSpace(); |
| 278 | if (gfxCs->type() == gfx::ColorSpace::ICC && |
| 279 | gfxCs->name() == name) { |
| 280 | osCS = cs; |
| 281 | break; |
| 282 | } |
| 283 | } |
| 284 | break; |
| 285 | } |
| 286 | } |
| 287 | |
| 288 | // Set the default color space for all windows (osCS can be nullptr |
| 289 | // which means that each window should use its monitor color space) |
| 290 | system->setWindowsColorSpace(osCS); |
| 291 | |
| 292 | // Set the color space of all windows |
| 293 | for (ui::Widget* widget : manager->children()) { |
| 294 | ASSERT(widget->type() == ui::kWindowWidget); |
| 295 | auto window = static_cast<ui::Window*>(widget); |
| 296 | if (window->ownDisplay()) { |
| 297 | if (auto display = window->display()) |
| 298 | display->nativeWindow()->setColorSpace(osCS); |
| 299 | } |
| 300 | } |
| 301 | } |
| 302 | |
| 303 | static bool load_gui_config(os::WindowSpec& spec, bool& maximized) |
| 304 | { |
| 305 | os::ScreenRef screen = os::instance()->mainScreen(); |
| 306 | #ifdef LAF_SKIA |
| 307 | ASSERT(screen); |
| 308 | #else |
| 309 | // Compiled without Skia (none backend), without screen. |
| 310 | if (!screen) { |
| 311 | std::printf( |
| 312 | "\n" |
| 313 | " Aseprite cannot initialize GUI because it was compiled with LAF_BACKEND=none\n" |
| 314 | "\n" |
| 315 | " Check the documentation in:\n" |
| 316 | " https://github.com/aseprite/laf/blob/main/README.md\n" |
| 317 | " https://github.com/aseprite/aseprite/blob/main/INSTALL.md\n" |
| 318 | "\n" ); |
| 319 | return false; |
| 320 | } |
| 321 | #endif |
| 322 | |
| 323 | spec.screen(screen); |
| 324 | |
| 325 | gfx::Rect frame; |
| 326 | frame = get_config_rect("GfxMode" , "Frame" , frame); |
| 327 | if (!frame.isEmpty()) { |
| 328 | spec.position(os::WindowSpec::Position::Frame); |
| 329 | |
| 330 | // Limit the content rect position into the available workarea, |
| 331 | // e.g. this is needed in case that the user closed Aseprite in a |
| 332 | // 2nd monitor that then unplugged and start Aseprite again. |
| 333 | bool ok = false; |
| 334 | os::ScreenList screens; |
| 335 | os::instance()->listScreens(screens); |
| 336 | for (const auto& screen : screens) { |
| 337 | gfx::Rect wa = screen->workarea(); |
| 338 | gfx::Rect intersection = (frame & wa); |
| 339 | if (intersection.w >= 32 && |
| 340 | intersection.h >= 32) { |
| 341 | ok = true; |
| 342 | break; |
| 343 | } |
| 344 | } |
| 345 | |
| 346 | // Reset content rect |
| 347 | if (!ok) { |
| 348 | spec.position(os::WindowSpec::Position::Default); |
| 349 | frame = gfx::Rect(); |
| 350 | } |
| 351 | } |
| 352 | |
| 353 | if (frame.isEmpty()) { |
| 354 | frame = screen->workarea().shrink(64); |
| 355 | |
| 356 | // Try to get Width/Height from previous Aseprite versions |
| 357 | frame.w = get_config_int("GfxMode" , "Width" , frame.w); |
| 358 | frame.h = get_config_int("GfxMode" , "Height" , frame.h); |
| 359 | } |
| 360 | spec.frame(frame); |
| 361 | |
| 362 | maximized = get_config_bool("GfxMode" , "Maximized" , true); |
| 363 | |
| 364 | ui::set_multiple_displays(Preferences::instance().experimental.multipleWindows()); |
| 365 | return true; |
| 366 | } |
| 367 | |
| 368 | static void save_gui_config() |
| 369 | { |
| 370 | os::Window* window = manager->display()->nativeWindow(); |
| 371 | if (window) { |
| 372 | const bool maximized = (window->isMaximized() || |
| 373 | window->isFullscreen()); |
| 374 | const gfx::Rect frame = (maximized ? window->restoredFrame(): |
| 375 | window->frame()); |
| 376 | |
| 377 | set_config_bool("GfxMode" , "Maximized" , maximized); |
| 378 | set_config_rect("GfxMode" , "Frame" , frame); |
| 379 | } |
| 380 | } |
| 381 | |
| 382 | void update_screen_for_document(const Doc* document) |
| 383 | { |
| 384 | // Without document. |
| 385 | if (!document) { |
| 386 | // Well, change to the default palette. |
| 387 | if (set_current_palette(NULL, false)) { |
| 388 | // If the palette changes, refresh the whole screen. |
| 389 | if (manager) |
| 390 | manager->invalidate(); |
| 391 | } |
| 392 | } |
| 393 | // With a document. |
| 394 | else { |
| 395 | const_cast<Doc*>(document)->notifyGeneralUpdate(); |
| 396 | |
| 397 | // Update the tabs (maybe the modified status has been changed). |
| 398 | app_rebuild_documents_tabs(); |
| 399 | } |
| 400 | } |
| 401 | |
| 402 | void load_window_pos(Window* window, const char* section, |
| 403 | const bool limitMinSize) |
| 404 | { |
| 405 | Display* parentDisplay = |
| 406 | (window->display() ? window->display(): |
| 407 | window->manager()->display()); |
| 408 | Rect workarea = |
| 409 | (get_multiple_displays() ? |
| 410 | parentDisplay->nativeWindow()->screen()->workarea(): |
| 411 | parentDisplay->bounds()); |
| 412 | |
| 413 | // Default position |
| 414 | Rect origPos = window->bounds(); |
| 415 | |
| 416 | // Load configurated position |
| 417 | Rect pos = get_config_rect(section, "WindowPos" , origPos); |
| 418 | |
| 419 | if (limitMinSize) { |
| 420 | pos.w = std::clamp(pos.w, origPos.w, workarea.w); |
| 421 | pos.h = std::clamp(pos.h, origPos.h, workarea.h); |
| 422 | } |
| 423 | else { |
| 424 | pos.w = std::min(pos.w, workarea.w); |
| 425 | pos.h = std::min(pos.h, workarea.h); |
| 426 | } |
| 427 | |
| 428 | pos.setOrigin(Point(std::clamp(pos.x, workarea.x, workarea.x2()-pos.w), |
| 429 | std::clamp(pos.y, workarea.y, workarea.y2()-pos.h))); |
| 430 | |
| 431 | window->setBounds(pos); |
| 432 | |
| 433 | if (get_multiple_displays()) { |
| 434 | Rect frame = get_config_rect(section, "WindowFrame" , gfx::Rect()); |
| 435 | if (!frame.isEmpty()) { |
| 436 | limit_with_workarea(parentDisplay, frame); |
| 437 | window->loadNativeFrame(frame); |
| 438 | } |
| 439 | } |
| 440 | else { |
| 441 | del_config_value(section, "WindowFrame" ); |
| 442 | } |
| 443 | } |
| 444 | |
| 445 | void save_window_pos(Window* window, const char* section) |
| 446 | { |
| 447 | gfx::Rect rc; |
| 448 | |
| 449 | if (!window->lastNativeFrame().isEmpty()) { |
| 450 | const os::Window* mainNativeWindow = manager->display()->nativeWindow(); |
| 451 | rc = window->lastNativeFrame(); |
| 452 | set_config_rect(section, "WindowFrame" , rc); |
| 453 | rc.offset(-mainNativeWindow->frame().origin()); |
| 454 | rc /= mainNativeWindow->scale(); |
| 455 | } |
| 456 | else { |
| 457 | del_config_value(section, "WindowFrame" ); |
| 458 | rc = window->bounds(); |
| 459 | } |
| 460 | |
| 461 | set_config_rect(section, "WindowPos" , rc); |
| 462 | } |
| 463 | |
| 464 | // TODO Replace this with new theme styles |
| 465 | Widget* setup_mini_font(Widget* widget) |
| 466 | { |
| 467 | auto skinProp = get_skin_property(widget); |
| 468 | skinProp->setMiniFont(); |
| 469 | return widget; |
| 470 | } |
| 471 | |
| 472 | // TODO Replace this with new theme styles |
| 473 | Widget* setup_mini_look(Widget* widget) |
| 474 | { |
| 475 | auto skinProp = get_skin_property(widget); |
| 476 | skinProp->setLook(MiniLook); |
| 477 | return widget; |
| 478 | } |
| 479 | |
| 480 | ////////////////////////////////////////////////////////////////////// |
| 481 | // Button style (convert radio or check buttons and draw it like |
| 482 | // normal buttons) |
| 483 | |
| 484 | void defer_invalid_rect(const gfx::Rect& rc) |
| 485 | { |
| 486 | if (!defered_invalid_timer) |
| 487 | defered_invalid_timer = new ui::Timer(250, manager); |
| 488 | |
| 489 | defered_invalid_timer->stop(); |
| 490 | defered_invalid_timer->start(); |
| 491 | defered_invalid_region.createUnion(defered_invalid_region, gfx::Region(rc)); |
| 492 | } |
| 493 | |
| 494 | ////////////////////////////////////////////////////////////////////// |
| 495 | // Manager event handler. |
| 496 | |
| 497 | bool CustomizedGuiManager::onProcessMessage(Message* msg) |
| 498 | { |
| 499 | #ifdef ENABLE_STEAM |
| 500 | if (auto steamAPI = steam::SteamAPI::instance()) |
| 501 | steamAPI->runCallbacks(); |
| 502 | #endif |
| 503 | |
| 504 | switch (msg->type()) { |
| 505 | |
| 506 | case kCloseDisplayMessage: |
| 507 | // Only call the exit command/close the app when the the main |
| 508 | // display is the closed window in this kCloseDisplayMessage |
| 509 | // message and it's the current running foreground window. |
| 510 | if (msg->display() == this->display() && |
| 511 | getForegroundWindow() == App::instance()->mainWindow()) { |
| 512 | // Execute the "Exit" command. |
| 513 | Command* command = Commands::instance()->byId(CommandId::Exit()); |
| 514 | UIContext::instance()->executeCommandFromMenuOrShortcut(command); |
| 515 | return true; |
| 516 | } |
| 517 | break; |
| 518 | |
| 519 | case kDropFilesMessage: |
| 520 | // Files are processed only when the main window is the current |
| 521 | // window running. |
| 522 | // |
| 523 | // TODO could we send the files to each dialog? |
| 524 | if (getForegroundWindow() == App::instance()->mainWindow()) { |
| 525 | base::paths files = static_cast<DropFilesMessage*>(msg)->files(); |
| 526 | UIContext* ctx = UIContext::instance(); |
| 527 | OpenBatchOfFiles batch; |
| 528 | |
| 529 | while (!files.empty()) { |
| 530 | auto fn = files.front(); |
| 531 | files.erase(files.begin()); |
| 532 | |
| 533 | // If the document is already open, select it. |
| 534 | Doc* doc = ctx->documents().getByFileName(fn); |
| 535 | if (doc) { |
| 536 | DocView* docView = ctx->getFirstDocView(doc); |
| 537 | if (docView) |
| 538 | ctx->setActiveView(docView); |
| 539 | else { |
| 540 | ASSERT(false); // Must be some DocView available |
| 541 | } |
| 542 | } |
| 543 | // Load the file |
| 544 | else { |
| 545 | // Depending on the file type we will want to do different things: |
| 546 | std::string extension = base::string_to_lower( |
| 547 | base::get_file_extension(fn)); |
| 548 | |
| 549 | // Install the extension |
| 550 | if (extension == "aseprite-extension" ) { |
| 551 | Command* cmd = Commands::instance()->byId(CommandId::Options()); |
| 552 | Params params; |
| 553 | params.set("installExtension" , fn.c_str()); |
| 554 | ctx->executeCommandFromMenuOrShortcut(cmd, params); |
| 555 | } |
| 556 | // Other extensions will be handled as an image/sprite |
| 557 | else { |
| 558 | batch.open(ctx, fn, |
| 559 | false); // Open all frames |
| 560 | |
| 561 | // Remove all used file names from the "dropped files" |
| 562 | for (const auto& usedFn : batch.usedFiles()) { |
| 563 | auto it = std::find(files.begin(), files.end(), usedFn); |
| 564 | if (it != files.end()) |
| 565 | files.erase(it); |
| 566 | } |
| 567 | } |
| 568 | } |
| 569 | } |
| 570 | } |
| 571 | break; |
| 572 | |
| 573 | case kKeyDownMessage: { |
| 574 | #if ENABLE_DEVMODE |
| 575 | if (onProcessDevModeKeyDown(static_cast<KeyMessage*>(msg))) |
| 576 | return true; |
| 577 | #endif // ENABLE_DEVMODE |
| 578 | |
| 579 | // Call base impl to check if there is a foreground window as |
| 580 | // top level that needs keys. (In this way we just do not |
| 581 | // process keyboard shortcuts for menus and tools). |
| 582 | if (Manager::onProcessMessage(msg)) |
| 583 | return true; |
| 584 | |
| 585 | KeyboardShortcuts* keys = KeyboardShortcuts::instance(); |
| 586 | for (const KeyPtr& key : *keys) { |
| 587 | if (key->isPressed(msg, *keys)) { |
| 588 | // Cancel menu-bar loops (to close any popup menu) |
| 589 | App::instance()->mainWindow()->getMenuBar()->cancelMenuLoop(); |
| 590 | |
| 591 | switch (key->type()) { |
| 592 | |
| 593 | case KeyType::Tool: { |
| 594 | tools::Tool* current_tool = App::instance()->activeTool(); |
| 595 | tools::Tool* select_this_tool = key->tool(); |
| 596 | tools::ToolBox* toolbox = App::instance()->toolBox(); |
| 597 | std::vector<tools::Tool*> possibles; |
| 598 | |
| 599 | // Collect all tools with the pressed keyboard-shortcut |
| 600 | for (tools::Tool* tool : *toolbox) { |
| 601 | const KeyPtr key = KeyboardShortcuts::instance()->tool(tool); |
| 602 | if (key && key->isPressed(msg, *keys)) |
| 603 | possibles.push_back(tool); |
| 604 | } |
| 605 | |
| 606 | if (possibles.size() >= 2) { |
| 607 | bool done = false; |
| 608 | |
| 609 | for (size_t i=0; i<possibles.size(); ++i) { |
| 610 | if (possibles[i] != current_tool && |
| 611 | ToolBar::instance()->isToolVisible(possibles[i])) { |
| 612 | select_this_tool = possibles[i]; |
| 613 | done = true; |
| 614 | break; |
| 615 | } |
| 616 | } |
| 617 | |
| 618 | if (!done) { |
| 619 | for (size_t i=0; i<possibles.size(); ++i) { |
| 620 | // If one of the possibilities is the current tool |
| 621 | if (possibles[i] == current_tool) { |
| 622 | // We select the next tool in the possibilities |
| 623 | select_this_tool = possibles[(i+1) % possibles.size()]; |
| 624 | break; |
| 625 | } |
| 626 | } |
| 627 | } |
| 628 | } |
| 629 | |
| 630 | ToolBar::instance()->selectTool(select_this_tool); |
| 631 | return true; |
| 632 | } |
| 633 | |
| 634 | case KeyType::Command: { |
| 635 | Command* command = key->command(); |
| 636 | |
| 637 | // Commands are executed only when the main window is |
| 638 | // the current window running. |
| 639 | if (getForegroundWindow() == App::instance()->mainWindow()) { |
| 640 | // OK, so we can execute the command represented |
| 641 | // by the pressed-key in the message... |
| 642 | UIContext::instance()->executeCommandFromMenuOrShortcut( |
| 643 | command, key->params()); |
| 644 | return true; |
| 645 | } |
| 646 | break; |
| 647 | } |
| 648 | |
| 649 | case KeyType::Quicktool: { |
| 650 | // Do nothing, it is used in the editor through the |
| 651 | // KeyboardShortcuts::getCurrentQuicktool() function. |
| 652 | break; |
| 653 | } |
| 654 | |
| 655 | } |
| 656 | break; |
| 657 | } |
| 658 | } |
| 659 | break; |
| 660 | } |
| 661 | |
| 662 | case kTimerMessage: |
| 663 | if (static_cast<TimerMessage*>(msg)->timer() == defered_invalid_timer) { |
| 664 | invalidateRegion(defered_invalid_region); |
| 665 | defered_invalid_region.clear(); |
| 666 | defered_invalid_timer->stop(); |
| 667 | } |
| 668 | break; |
| 669 | |
| 670 | } |
| 671 | |
| 672 | return Manager::onProcessMessage(msg); |
| 673 | } |
| 674 | |
| 675 | #if ENABLE_DEVMODE |
| 676 | bool CustomizedGuiManager::onProcessDevModeKeyDown(KeyMessage* msg) |
| 677 | { |
| 678 | // Ctrl+Shift+Q generates a crash (useful to test the anticrash feature) |
| 679 | if (msg->ctrlPressed() && |
| 680 | msg->shiftPressed() && |
| 681 | msg->scancode() == kKeyQ) { |
| 682 | int* p = nullptr; |
| 683 | *p = 0; // *Crash* |
| 684 | return true; // This line should not be executed anyway |
| 685 | } |
| 686 | |
| 687 | // Ctrl+F1 switches screen/UI scaling |
| 688 | if (msg->ctrlPressed() && |
| 689 | msg->scancode() == kKeyF1) { |
| 690 | try { |
| 691 | os::Window* window = display()->nativeWindow(); |
| 692 | int screenScale = window->scale(); |
| 693 | int uiScale = ui::guiscale(); |
| 694 | |
| 695 | if (msg->shiftPressed()) { |
| 696 | if (screenScale == 2 && uiScale == 1) { |
| 697 | screenScale = 1; |
| 698 | uiScale = 1; |
| 699 | } |
| 700 | else if (screenScale == 1 && uiScale == 1) { |
| 701 | screenScale = 1; |
| 702 | uiScale = 2; |
| 703 | } |
| 704 | else if (screenScale == 1 && uiScale == 2) { |
| 705 | screenScale = 2; |
| 706 | uiScale = 1; |
| 707 | } |
| 708 | } |
| 709 | else { |
| 710 | if (screenScale == 2 && uiScale == 1) { |
| 711 | screenScale = 1; |
| 712 | uiScale = 2; |
| 713 | } |
| 714 | else if (screenScale == 1 && uiScale == 2) { |
| 715 | screenScale = 1; |
| 716 | uiScale = 1; |
| 717 | } |
| 718 | else if (screenScale == 1 && uiScale == 1) { |
| 719 | screenScale = 2; |
| 720 | uiScale = 1; |
| 721 | } |
| 722 | } |
| 723 | |
| 724 | if (uiScale != ui::guiscale()) { |
| 725 | ui::set_theme(ui::get_theme(), uiScale); |
| 726 | } |
| 727 | if (screenScale != window->scale()) { |
| 728 | updateAllDisplaysWithNewScale(screenScale); |
| 729 | } |
| 730 | } |
| 731 | catch (const std::exception& ex) { |
| 732 | Console::showException(ex); |
| 733 | } |
| 734 | return true; |
| 735 | } |
| 736 | |
| 737 | #ifdef ENABLE_DATA_RECOVERY |
| 738 | // Ctrl+Shift+R recover active sprite from the backup store |
| 739 | if (msg->ctrlPressed() && |
| 740 | msg->shiftPressed() && |
| 741 | msg->scancode() == kKeyR && |
| 742 | App::instance()->dataRecovery() && |
| 743 | App::instance()->dataRecovery()->activeSession() && |
| 744 | current_editor && |
| 745 | current_editor->document()) { |
| 746 | Doc* doc = App::instance() |
| 747 | ->dataRecovery() |
| 748 | ->activeSession() |
| 749 | ->restoreBackupById(current_editor->document()->id(), nullptr); |
| 750 | if (doc) |
| 751 | UIContext::instance()->documents().add(doc); |
| 752 | return true; |
| 753 | } |
| 754 | #endif // ENABLE_DATA_RECOVERY |
| 755 | |
| 756 | return false; |
| 757 | } |
| 758 | #endif // ENABLE_DEVMODE |
| 759 | |
| 760 | void CustomizedGuiManager::onInitTheme(InitThemeEvent& ev) |
| 761 | { |
| 762 | Manager::onInitTheme(ev); |
| 763 | |
| 764 | // Update the theme on all menus |
| 765 | AppMenus::instance()->initTheme(); |
| 766 | } |
| 767 | |
| 768 | void CustomizedGuiManager::onNewDisplayConfiguration(Display* display) |
| 769 | { |
| 770 | Manager::onNewDisplayConfiguration(display); |
| 771 | |
| 772 | // Only whne the main display/window is modified |
| 773 | if (display == this->display()) { |
| 774 | save_gui_config(); |
| 775 | |
| 776 | // TODO Should we provide a more generic way for all ui::Window to |
| 777 | // detect the os::Window (or UI Screen Scaling) change? |
| 778 | Console::notifyNewDisplayConfiguration(); |
| 779 | } |
| 780 | } |
| 781 | |
| 782 | std::string CustomizedGuiManager::loadLayout(Widget* widget) |
| 783 | { |
| 784 | if (widget->window() == nullptr) |
| 785 | return "" ; |
| 786 | |
| 787 | std::string windowId = widget->window()->id(); |
| 788 | std::string widgetId = widget->id(); |
| 789 | |
| 790 | return get_config_string(("layout:" +windowId).c_str(), widgetId.c_str(), "" ); |
| 791 | } |
| 792 | |
| 793 | void CustomizedGuiManager::saveLayout(Widget* widget, const std::string& str) |
| 794 | { |
| 795 | if (widget->window() == NULL) |
| 796 | return; |
| 797 | |
| 798 | std::string windowId = widget->window()->id(); |
| 799 | std::string widgetId = widget->id(); |
| 800 | |
| 801 | set_config_string(("layout:" +windowId).c_str(), |
| 802 | widgetId.c_str(), |
| 803 | str.c_str()); |
| 804 | } |
| 805 | |
| 806 | } // namespace app |
| 807 | |