From d7968978ad974d526981b2fbd2f546318b9b6145 Mon Sep 17 00:00:00 2001 From: whitequark Date: Tue, 17 Jul 2018 15:00:46 +0000 Subject: [PATCH] Add a platform abstraction for message dialogs. This commit changes the awfully specific code for dialogs with messages duplicated three times to go through a generic interface. It also fixes some issues with the way translated messages were parameterized. This commit removes the custom message dialog box used on Windows, for several reasons. First, it was the last element not respecting HiDPI displays. Second, other OSes do not easily provide this much control over rendering default message boxes, and both Gnome and macOS frown upon non-standard renderings such as those; so the custom rendering was already not used on the other OSes. --- src/file.cpp | 70 ++++++++---- src/lib.cpp | 5 - src/platform/cocoamain.mm | 100 +---------------- src/platform/gtkmain.cpp | 85 --------------- src/platform/gui.h | 32 ++++++ src/platform/guigtk.cpp | 114 +++++++++++++++++-- src/platform/guimac.mm | 87 +++++++++++++-- src/platform/guinone.cpp | 22 ++-- src/platform/guiwin.cpp | 117 +++++++++++++++++++- src/platform/w32main.cpp | 224 -------------------------------------- src/solvespace.cpp | 40 ++++++- src/solvespace.h | 14 +-- src/util.cpp | 104 +++++++++--------- 13 files changed, 473 insertions(+), 541 deletions(-) diff --git a/src/file.cpp b/src/file.cpp index 5b8447a..24eb6c3 100644 --- a/src/file.cpp +++ b/src/file.cpp @@ -825,6 +825,28 @@ bool SolveSpaceUI::LoadEntitiesFromFile(const Platform::Path &filename, EntityLi return true; } +static Platform::MessageDialog::Response LocateImportedFile(const Platform::Path &filename, + bool canCancel) { + Platform::MessageDialogRef dialog = CreateMessageDialog(SS.GW.window); + + using Platform::MessageDialog; + dialog->SetType(MessageDialog::Type::QUESTION); + dialog->SetTitle(C_("title", "Missing File")); + dialog->SetMessage(ssprintf(C_("dialog", "The linked file “%s” is not present."), + filename.raw.c_str())); + dialog->SetDescription(C_("dialog", "Do you want to locate it manually?\n\n" + "If you decline, any geometry that depends on " + "the missing file will be permanently removed.")); + dialog->AddButton(C_("button", "&Yes"), MessageDialog::Response::YES, + /*isDefault=*/true); + dialog->AddButton(C_("button", "&No"), MessageDialog::Response::NO); + if(canCancel) { + dialog->AddButton(C_("button", "&Cancel"), MessageDialog::Response::CANCEL); + } + + return dialog->RunModal(); +} + bool SolveSpaceUI::ReloadAllLinked(const Platform::Path &saveFile, bool canCancel) { std::map linkMap; @@ -853,26 +875,29 @@ try_again: } } else if(linkMap.count(g.linkFile) == 0) { // The file was moved; prompt the user for its new location. - switch(LocateImportedFileYesNoCancel(g.linkFile.RelativeTo(saveFile), canCancel)) { - case DIALOG_YES: { - Platform::Path newLinkFile; - if(GetOpenFile(&newLinkFile, "", SlvsFileFilter)) { - linkMap[g.linkFile] = newLinkFile; - g.linkFile = newLinkFile; - goto try_again; - } else { - if(canCancel) return false; - break; + switch(LocateImportedFile(g.linkFile.RelativeTo(saveFile), canCancel)) { + case Platform::MessageDialog::Response::YES: { + Platform::Path newLinkFile; + if(GetOpenFile(&newLinkFile, "", SlvsFileFilter)) { + linkMap[g.linkFile] = newLinkFile; + g.linkFile = newLinkFile; + goto try_again; + } else { + if(canCancel) return false; + break; + } } - } - case DIALOG_NO: - linkMap[g.linkFile].Clear(); - // Geometry will be pruned by GenerateAll(). - break; + case Platform::MessageDialog::Response::NO: + linkMap[g.linkFile].Clear(); + // Geometry will be pruned by GenerateAll(). + break; - case DIALOG_CANCEL: - return false; + case Platform::MessageDialog::Response::CANCEL: + return false; + + default: + ssassert(false, "Unexpected dialog response"); } } else { // User was already asked to and refused to locate a missing linked file. @@ -904,17 +929,20 @@ bool SolveSpaceUI::ReloadLinkedImage(const Platform::Path &saveFile, pixmap = Pixmap::ReadPng(*filename); if(pixmap == NULL) { // The file was moved; prompt the user for its new location. - switch(LocateImportedFileYesNoCancel(filename->RelativeTo(saveFile), canCancel)) { - case DIALOG_YES: + switch(LocateImportedFile(filename->RelativeTo(saveFile), canCancel)) { + case Platform::MessageDialog::Response::YES: promptOpenFile = true; break; - case DIALOG_NO: + case Platform::MessageDialog::Response::NO: // We don't know where the file is, record it as absent. break; - case DIALOG_CANCEL: + case Platform::MessageDialog::Response::CANCEL: return false; + + default: + ssassert(false, "Unexpected dialog response"); } } } diff --git a/src/lib.cpp b/src/lib.cpp index 6a73ed6..e4a02f9 100644 --- a/src/lib.cpp +++ b/src/lib.cpp @@ -22,11 +22,6 @@ void Group::GenerateEquations(IdList *) { // Nothing to do for now. } -void SolveSpace::DoMessageBox(const char *, int, int, bool) -{ - abort(); -} - extern "C" { void Slvs_QuaternionU(double qw, double qx, double qy, double qz, diff --git a/src/platform/cocoamain.mm b/src/platform/cocoamain.mm index cd9f7cd..8640d43 100644 --- a/src/platform/cocoamain.mm +++ b/src/platform/cocoamain.mm @@ -5,11 +5,8 @@ // // Copyright 2015 //----------------------------------------------------------------------------- -#include -#include -#import - #include "solvespace.h" +#import using SolveSpace::dbp; @@ -125,103 +122,8 @@ bool SolveSpace::GetSaveFile(Platform::Path *filename, const std::string &defExt } } -SolveSpace::DialogChoice SolveSpace::SaveFileYesNoCancel() { - NSAlert *alert = [[NSAlert alloc] init]; - if(!SolveSpace::SS.saveFile.IsEmpty()) { - [alert setMessageText: - [[@"Do you want to save the changes you made to the sketch “" - stringByAppendingString: - [Wrap(SolveSpace::SS.saveFile.raw) - stringByAbbreviatingWithTildeInPath]] - stringByAppendingString:@"”?"]]; - } else { - [alert setMessageText: - Wrap(_("Do you want to save the changes you made to the new sketch?"))]; - } - [alert setInformativeText:Wrap(_("Your changes will be lost if you don't save them."))]; - [alert addButtonWithTitle:Wrap(C_("button", "Save"))]; - [alert addButtonWithTitle:Wrap(C_("button", "Cancel"))]; - [alert addButtonWithTitle:Wrap(C_("button", "Don't Save"))]; - switch([alert runModal]) { - case NSAlertFirstButtonReturn: - return DIALOG_YES; - case NSAlertSecondButtonReturn: - default: - return DIALOG_CANCEL; - case NSAlertThirdButtonReturn: - return DIALOG_NO; - } -} - -SolveSpace::DialogChoice SolveSpace::LoadAutosaveYesNo() { - NSAlert *alert = [[NSAlert alloc] init]; - [alert setMessageText: - Wrap(_("An autosave file is available for this project."))]; - [alert setInformativeText: - Wrap(_("Do you want to load the autosave file instead?"))]; - [alert addButtonWithTitle:Wrap(C_("button", "Load"))]; - [alert addButtonWithTitle:Wrap(C_("button", "Don't Load"))]; - switch([alert runModal]) { - case NSAlertFirstButtonReturn: - return DIALOG_YES; - case NSAlertSecondButtonReturn: - default: - return DIALOG_NO; - } -} - -SolveSpace::DialogChoice SolveSpace::LocateImportedFileYesNoCancel( - const Platform::Path &filename, bool canCancel) { - NSAlert *alert = [[NSAlert alloc] init]; - [alert setMessageText: - Wrap("The linked file “" + filename.raw + "” is not present.")]; - [alert setInformativeText: - Wrap(_("Do you want to locate it manually?\n" - "If you select “No”, any geometry that depends on " - "the missing file will be removed."))]; - [alert addButtonWithTitle:Wrap(C_("button", "Yes"))]; - if(canCancel) - [alert addButtonWithTitle:Wrap(C_("button", "Cancel"))]; - [alert addButtonWithTitle:Wrap(C_("button", "No"))]; - switch([alert runModal]) { - case NSAlertFirstButtonReturn: - return DIALOG_YES; - case NSAlertSecondButtonReturn: - default: - if(canCancel) - return DIALOG_CANCEL; - /* fallthrough */ - case NSAlertThirdButtonReturn: - return DIALOG_NO; - } -} - /* Miscellanea */ -void SolveSpace::DoMessageBox(const char *str, int rows, int cols, bool error) { - NSAlert *alert = [[NSAlert alloc] init]; - [alert setAlertStyle:(error ? NSWarningAlertStyle : NSInformationalAlertStyle)]; - [alert addButtonWithTitle:Wrap(C_("button", "OK"))]; - - /* do some additional formatting of the message; - these are heuristics, but they are made failsafe and lead to nice results. */ - NSString *input = [NSString stringWithUTF8String:str]; - NSRange dot = [input rangeOfCharacterFromSet: - [NSCharacterSet characterSetWithCharactersInString:@".:"]]; - if(dot.location != NSNotFound) { - [alert setMessageText:[[input substringToIndex:dot.location + 1] - stringByReplacingOccurrencesOfString:@"\n" withString:@" "]]; - [alert setInformativeText: - [[input substringFromIndex:dot.location + 1] - stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]]]; - } else { - [alert setMessageText:[input - stringByReplacingOccurrencesOfString:@"\n" withString:@" "]]; - } - - [alert runModal]; -} - void SolveSpace::OpenWebsite(const char *url) { [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString:[NSString stringWithUTF8String:url]]]; diff --git a/src/platform/gtkmain.cpp b/src/platform/gtkmain.cpp index fb05ccd..05e35bd 100644 --- a/src/platform/gtkmain.cpp +++ b/src/platform/gtkmain.cpp @@ -166,93 +166,8 @@ bool GetSaveFile(Platform::Path *filename, const std::string &defExtension, } } -DialogChoice SaveFileYesNoCancel(void) { - Glib::ustring message = - _("The file has changed since it was last saved.\n\n" - "Do you want to save the changes?"); - Gtk::MessageDialog dialog(*(Gtk::Window *)SS.GW.window->NativePtr(), - message, /*use_markup*/ true, Gtk::MESSAGE_QUESTION, - Gtk::BUTTONS_NONE, /*is_modal*/ true); - dialog.set_title(Title(C_("title", "Modified File"))); - dialog.add_button(C_("button", "_Save"), Gtk::RESPONSE_YES); - dialog.add_button(C_("button", "Do_n't Save"), Gtk::RESPONSE_NO); - dialog.add_button(C_("button", "_Cancel"), Gtk::RESPONSE_CANCEL); - - switch(dialog.run()) { - case Gtk::RESPONSE_YES: - return DIALOG_YES; - - case Gtk::RESPONSE_NO: - return DIALOG_NO; - - case Gtk::RESPONSE_CANCEL: - default: - return DIALOG_CANCEL; - } -} - -DialogChoice LoadAutosaveYesNo(void) { - Glib::ustring message = - _("An autosave file is available for this project.\n\n" - "Do you want to load the autosave file instead?"); - Gtk::MessageDialog dialog(*(Gtk::Window *)SS.GW.window->NativePtr(), - message, /*use_markup*/ true, Gtk::MESSAGE_QUESTION, - Gtk::BUTTONS_NONE, /*is_modal*/ true); - dialog.set_title(Title(C_("title", "Autosave Available"))); - dialog.add_button(C_("button", "_Load autosave"), Gtk::RESPONSE_YES); - dialog.add_button(C_("button", "Do_n't Load"), Gtk::RESPONSE_NO); - - switch(dialog.run()) { - case Gtk::RESPONSE_YES: - return DIALOG_YES; - - case Gtk::RESPONSE_NO: - default: - return DIALOG_NO; - } -} - -DialogChoice LocateImportedFileYesNoCancel(const Platform::Path &filename, - bool canCancel) { - Glib::ustring message = - "The linked file " + filename.raw + " is not present.\n\n" - "Do you want to locate it manually?\n\n" - "If you select \"No\", any geometry that depends on " - "the missing file will be removed."; - Gtk::MessageDialog dialog(*(Gtk::Window *)SS.GW.window->NativePtr(), - message, /*use_markup*/ true, Gtk::MESSAGE_QUESTION, - Gtk::BUTTONS_NONE, /*is_modal*/ true); - dialog.set_title(Title(C_("title", "Missing File"))); - dialog.add_button(C_("button", "_Yes"), Gtk::RESPONSE_YES); - dialog.add_button(C_("button", "_No"), Gtk::RESPONSE_NO); - if(canCancel) - dialog.add_button(C_("button", "_Cancel"), Gtk::RESPONSE_CANCEL); - - switch(dialog.run()) { - case Gtk::RESPONSE_YES: - return DIALOG_YES; - - case Gtk::RESPONSE_NO: - return DIALOG_NO; - - case Gtk::RESPONSE_CANCEL: - default: - return DIALOG_CANCEL; - } -} - /* Miscellanea */ -void DoMessageBox(const char *message, int rows, int cols, bool error) { - Gtk::MessageDialog dialog(*(Gtk::Window *)SS.GW.window->NativePtr(), - message, /*use_markup*/ true, - error ? Gtk::MESSAGE_ERROR : Gtk::MESSAGE_INFO, Gtk::BUTTONS_OK, - /*is_modal*/ true); - dialog.set_title(error ? - Title(C_("title", "Error")) : Title(C_("title", "Message"))); - dialog.run(); -} - void OpenWebsite(const char *url) { gtk_show_uri(Gdk::Screen::get_default()->gobj(), url, GDK_CURRENT_TIME, NULL); } diff --git a/src/platform/gui.h b/src/platform/gui.h index a283675..63ff9b4 100644 --- a/src/platform/gui.h +++ b/src/platform/gui.h @@ -250,6 +250,38 @@ typedef std::shared_ptr WindowRef; WindowRef CreateWindow(Window::Kind kind = Window::Kind::TOPLEVEL, WindowRef parentWindow = NULL); +// A native dialog that asks for one choice out of several. +class MessageDialog { +public: + enum class Type { + INFORMATION, + QUESTION, + WARNING, + ERROR + }; + + enum class Response { + NONE, + OK, + YES, + NO, + CANCEL + }; + + virtual void SetType(Type type) = 0; + virtual void SetTitle(std::string title) = 0; + virtual void SetMessage(std::string caption) = 0; + virtual void SetDescription(std::string text) = 0; + + virtual void AddButton(std::string name, Response response, bool isDefault = false) = 0; + + virtual Response RunModal() = 0; +}; + +typedef std::shared_ptr MessageDialogRef; + +MessageDialogRef CreateMessageDialog(WindowRef parentWindow); + //----------------------------------------------------------------------------- // Application-wide APIs //----------------------------------------------------------------------------- diff --git a/src/platform/guigtk.cpp b/src/platform/guigtk.cpp index daf2ff6..0258702 100644 --- a/src/platform/guigtk.cpp +++ b/src/platform/guigtk.cpp @@ -21,10 +21,24 @@ #include #include #include +#include namespace SolveSpace { namespace Platform { +//----------------------------------------------------------------------------- +// Utility functions +//----------------------------------------------------------------------------- + +static std::string PrepareMnemonics(std::string label) { + std::replace(label.begin(), label.end(), '&', '_'); + return label; +} + +static std::string PrepareTitle(std::string title) { + return title + " — SolveSpace"; +} + //----------------------------------------------------------------------------- // Fatal errors //----------------------------------------------------------------------------- @@ -251,11 +265,6 @@ protected: // Menus //----------------------------------------------------------------------------- -static std::string PrepareMenuLabel(std::string label) { - std::replace(label.begin(), label.end(), '&', '_'); - return label; -} - class MenuItemImplGtk : public MenuItem { public: GtkMenuItem gtkMenuItem; @@ -329,7 +338,7 @@ public: auto menuItem = std::make_shared(); menuItems.push_back(menuItem); - menuItem->gtkMenuItem.set_label(PrepareMenuLabel(label)); + menuItem->gtkMenuItem.set_label(PrepareMnemonics(label)); menuItem->gtkMenuItem.set_use_underline(true); menuItem->gtkMenuItem.show(); menuItem->onTrigger = onTrigger; @@ -345,7 +354,7 @@ public: auto subMenu = std::make_shared(); subMenus.push_back(subMenu); - menuItem->gtkMenuItem.set_label(PrepareMenuLabel(label)); + menuItem->gtkMenuItem.set_label(PrepareMnemonics(label)); menuItem->gtkMenuItem.set_use_underline(true); menuItem->gtkMenuItem.set_submenu(subMenu->gtkMenu); menuItem->gtkMenuItem.show_all(); @@ -391,7 +400,7 @@ public: subMenus.push_back(subMenu); Gtk::MenuItem *gtkMenuItem = Gtk::manage(new Gtk::MenuItem); - gtkMenuItem->set_label(PrepareMenuLabel(label)); + gtkMenuItem->set_label(PrepareMnemonics(label)); gtkMenuItem->set_use_underline(true); gtkMenuItem->set_submenu(subMenu->gtkMenu); gtkMenuItem->show_all(); @@ -848,7 +857,7 @@ public: } void SetTitle(const std::string &title) override { - gtkWindow.set_title(title + " — SolveSpace"); + gtkWindow.set_title(PrepareTitle(title)); } void SetMenuBar(MenuBarRef newMenuBar) override { @@ -982,6 +991,93 @@ WindowRef CreateWindow(Window::Kind kind, WindowRef parentWindow) { return window; } +//----------------------------------------------------------------------------- +// Message dialogs +//----------------------------------------------------------------------------- + +class MessageDialogImplGtk : public MessageDialog { +public: + Gtk::Image gtkImage; + Gtk::MessageDialog gtkDialog; + + MessageDialogImplGtk(Gtk::Window &parent) + : gtkDialog(parent, "", /*use_markup=*/false, Gtk::MESSAGE_INFO, + Gtk::BUTTONS_NONE, /*modal=*/true) + { + SetTitle("Message"); + } + + void SetType(Type type) override { + switch(type) { + case Type::INFORMATION: + gtkImage.set_from_icon_name("dialog-information", Gtk::ICON_SIZE_DIALOG); + break; + + case Type::QUESTION: + gtkImage.set_from_icon_name("dialog-question", Gtk::ICON_SIZE_DIALOG); + break; + + case Type::WARNING: + gtkImage.set_from_icon_name("dialog-warning", Gtk::ICON_SIZE_DIALOG); + break; + + case Type::ERROR: + gtkImage.set_from_icon_name("dialog-error", Gtk::ICON_SIZE_DIALOG); + break; + } + gtkDialog.set_image(gtkImage); + } + + void SetTitle(std::string title) override { + gtkDialog.set_title(PrepareTitle(title)); + } + + void SetMessage(std::string message) override { + gtkDialog.set_message(message); + } + + void SetDescription(std::string description) override { + gtkDialog.set_secondary_text(description); + } + + void AddButton(std::string name, Response response, bool isDefault) override { + int responseId; + switch(response) { + case Response::NONE: ssassert(false, "Invalid response"); + case Response::OK: responseId = Gtk::RESPONSE_OK; break; + case Response::YES: responseId = Gtk::RESPONSE_YES; break; + case Response::NO: responseId = Gtk::RESPONSE_NO; break; + case Response::CANCEL: responseId = Gtk::RESPONSE_CANCEL; break; + } + gtkDialog.add_button(PrepareMnemonics(name), responseId); + if(isDefault) { + gtkDialog.set_default_response(responseId); + } + } + + Response RunModal() override { + switch(gtkDialog.run()) { + case Gtk::RESPONSE_OK: return Response::OK; break; + case Gtk::RESPONSE_YES: return Response::YES; break; + case Gtk::RESPONSE_NO: return Response::NO; break; + case Gtk::RESPONSE_CANCEL: return Response::CANCEL; break; + + case Gtk::RESPONSE_NONE: + case Gtk::RESPONSE_CLOSE: + case Gtk::RESPONSE_DELETE_EVENT: + return Response::NONE; + break; + + default: ssassert(false, "Unexpected response"); + } + } +}; + +MessageDialogRef CreateMessageDialog(WindowRef parentWindow) { + return std::make_shared( + std::static_pointer_cast(parentWindow)->gtkWindow); +} + //----------------------------------------------------------------------------- // Application-wide APIs //----------------------------------------------------------------------------- diff --git a/src/platform/guimac.mm b/src/platform/guimac.mm index f4e292f..b6c0dff 100644 --- a/src/platform/guimac.mm +++ b/src/platform/guimac.mm @@ -3,8 +3,8 @@ // // Copyright 2018 whitequark //----------------------------------------------------------------------------- -#import #include "solvespace.h" +#import using namespace SolveSpace; @@ -52,6 +52,16 @@ static NSString* Wrap(const std::string &s) { namespace SolveSpace { namespace Platform { +//----------------------------------------------------------------------------- +// Utility functions +//----------------------------------------------------------------------------- + +static std::string PrepareMnemonics(std::string label) { + // OS X does not support mnemonics + label.erase(std::remove(label.begin(), label.end(), '&'), label.end()); + return label; +} + //----------------------------------------------------------------------------- // Fatal errors //----------------------------------------------------------------------------- @@ -184,12 +194,6 @@ TimerRef CreateTimer() { // Menus //----------------------------------------------------------------------------- -static std::string PrepareMenuLabel(std::string label) { - // OS X does not support mnemonics - label.erase(std::remove(label.begin(), label.end(), '&'), label.end()); - return label; -} - class MenuItemImplCocoa : public MenuItem { public: SSFunction *ssFunction; @@ -258,7 +262,7 @@ public: menuItems.push_back(menuItem); menuItem->onTrigger = onTrigger; - [menuItem->nsMenuItem setTitle:Wrap(PrepareMenuLabel(label))]; + [menuItem->nsMenuItem setTitle:Wrap(PrepareMnemonics(label))]; [nsMenu addItem:menuItem->nsMenuItem]; return menuItem; @@ -269,7 +273,7 @@ public: subMenus.push_back(subMenu); NSMenuItem *nsMenuItem = - [nsMenu addItemWithTitle:Wrap(PrepareMenuLabel(label)) action:nil keyEquivalent:@""]; + [nsMenu addItemWithTitle:Wrap(PrepareMnemonics(label)) action:nil keyEquivalent:@""]; [nsMenu setSubmenu:subMenu->nsMenu forItem:nsMenuItem]; return subMenu; @@ -309,7 +313,7 @@ public: subMenus.push_back(subMenu); NSMenuItem *nsMenuItem = [nsMenuBar addItemWithTitle:@"" action:nil keyEquivalent:@""]; - [subMenu->nsMenu setTitle:Wrap(PrepareMenuLabel(label))]; + [subMenu->nsMenu setTitle:Wrap(PrepareMnemonics(label))]; [nsMenuBar setSubmenu:subMenu->nsMenu forItem:nsMenuItem]; return subMenu; @@ -996,6 +1000,69 @@ WindowRef CreateWindow(Window::Kind kind, WindowRef parentWindow) { std::static_pointer_cast(parentWindow)); } +//----------------------------------------------------------------------------- +// Message dialogs +//----------------------------------------------------------------------------- + +class MessageDialogImplCocoa : public MessageDialog { +public: + NSAlert *nsAlert = [[NSAlert alloc] init]; + NSWindow *nsWindow; + + std::vector responses; + + void SetType(Type type) override { + switch(type) { + case Type::INFORMATION: + case Type::QUESTION: + nsAlert.alertStyle = NSInformationalAlertStyle; + break; + + case Type::WARNING: + case Type::ERROR: + nsAlert.alertStyle = NSWarningAlertStyle; + break; + } + } + + void SetTitle(std::string title) override { + [nsAlert.window setTitle:Wrap(title)]; + } + + void SetMessage(std::string message) override { + nsAlert.messageText = Wrap(message); + } + + void SetDescription(std::string description) override { + nsAlert.informativeText = Wrap(description); + } + + void AddButton(std::string name, Response response, bool isDefault) override { + NSButton *nsButton = [nsAlert addButtonWithTitle:Wrap(PrepareMnemonics(name))]; + if(!isDefault && [nsButton.keyEquivalent isEqualToString:@"\n"]) { + nsButton.keyEquivalent = @""; + } else if(response == Response::CANCEL) { + nsButton.keyEquivalent = @"\e"; + } + responses.push_back(response); + } + + Response RunModal() override { + // FIXME(platform/gui): figure out a way to run the alert as a sheet + NSModalResponse nsResponse = [nsAlert runModal]; + ssassert(nsResponse >= NSAlertFirstButtonReturn && + nsResponse <= NSAlertFirstButtonReturn + (long)responses.size(), + "Unexpected response"); + return responses[nsResponse - NSAlertFirstButtonReturn]; + } +}; + +MessageDialogRef CreateMessageDialog(WindowRef parentWindow) { + std::shared_ptr dialog = std::make_shared(); + dialog->nsWindow = std::static_pointer_cast(parentWindow)->nsWindow; + return dialog; +} + //----------------------------------------------------------------------------- // Application-wide APIs //----------------------------------------------------------------------------- diff --git a/src/platform/guinone.cpp b/src/platform/guinone.cpp index 1273d38..b4ca4da 100644 --- a/src/platform/guinone.cpp +++ b/src/platform/guinone.cpp @@ -113,6 +113,14 @@ WindowRef CreateWindow(Window::Kind kind, WindowRef parentWindow) { return std::shared_ptr(); } +//----------------------------------------------------------------------------- +// Dialogs +//----------------------------------------------------------------------------- + +MessageDialogRef CreateMessageDialog(WindowRef parentWindow) { + return std::shared_ptr(); +} + //----------------------------------------------------------------------------- // Application-wide APIs //----------------------------------------------------------------------------- @@ -135,20 +143,6 @@ bool GetSaveFile(Platform::Path *filename, const std::string &activeOrEmpty, const FileFilter filters[]) { ssassert(false, "Not implemented"); } -DialogChoice SaveFileYesNoCancel() { - ssassert(false, "Not implemented"); -} -DialogChoice LoadAutosaveYesNo() { - ssassert(false, "Not implemented"); -} -DialogChoice LocateImportedFileYesNoCancel(const Platform::Path &filename, - bool canCancel) { - ssassert(false, "Not implemented"); -} -void DoMessageBox(const char *message, int rows, int cols, bool error) { - dbp("%s box: %s", error ? "error" : "message", message); - ssassert(false, "Not implemented"); -} void OpenWebsite(const char *url) { ssassert(false, "Not implemented"); } diff --git a/src/platform/guiwin.cpp b/src/platform/guiwin.cpp index b42f9e9..c7760e5 100644 --- a/src/platform/guiwin.cpp +++ b/src/platform/guiwin.cpp @@ -15,8 +15,9 @@ #define WM_DPICHANGED 0x02E0 #endif -// We have our own CreateWindow. +// These interfere with our identifiers. #undef CreateWindow +#undef ERROR #if HAVE_OPENGL == 3 #define EGLAPI /*static linkage*/ @@ -95,7 +96,7 @@ BOOL ssAdjustWindowRectExForDpi(LPRECT lpRect, DWORD dwStyle, BOOL bMenu, // Utility functions //----------------------------------------------------------------------------- -std::wstring Title(const std::string &s) { +static std::wstring PrepareTitle(const std::string &s) { return Widen("SolveSpace - " + s); } @@ -1030,7 +1031,7 @@ public: } void SetTitle(const std::string &title) override { - sscheck(SetWindowTextW(hWindow, Title(title).c_str())); + sscheck(SetWindowTextW(hWindow, PrepareTitle(title).c_str())); } void SetMenuBar(MenuBarRef newMenuBar) override { @@ -1270,6 +1271,116 @@ WindowRef CreateWindow(Window::Kind kind, WindowRef parentWindow) { std::static_pointer_cast(parentWindow)); } +//----------------------------------------------------------------------------- +// Dialogs +//----------------------------------------------------------------------------- + +class MessageDialogImplWin32 : public MessageDialog { +public: + MSGBOXPARAMSW mbp = {}; + + int style; + + std::wstring titleW; + std::wstring messageW; + std::wstring descriptionW; + std::wstring textW; + + std::vector buttons; + int defaultButton; + + MessageDialogImplWin32() { + mbp.cbSize = sizeof(mbp); + SetTitle("Message"); + } + + void SetType(Type type) override { + switch(type) { + case Type::INFORMATION: + style = MB_ICONINFORMATION; + break; + + case Type::QUESTION: + style = MB_ICONQUESTION; + break; + + case Type::WARNING: + style = MB_ICONWARNING; + break; + + case Type::ERROR: + style = MB_ICONERROR; + break; + } + } + + void SetTitle(std::string title) override { + titleW = PrepareTitle(title); + mbp.lpszCaption = titleW.c_str(); + } + + void SetMessage(std::string message) override { + messageW = Widen(message); + UpdateText(); + } + + void SetDescription(std::string description) override { + descriptionW = Widen(description); + UpdateText(); + } + + void UpdateText() { + textW = messageW + L"\n\n" + descriptionW; + mbp.lpszText = textW.c_str(); + } + + void AddButton(std::string _name, Response response, bool isDefault) override { + int button; + switch(response) { + case Response::NONE: ssassert(false, "Invalid response"); + case Response::OK: button = IDOK; break; + case Response::YES: button = IDYES; break; + case Response::NO: button = IDNO; break; + case Response::CANCEL: button = IDCANCEL; break; + } + buttons.push_back(button); + if(isDefault) { + defaultButton = button; + } + } + + Response RunModal() override { + mbp.dwStyle = style; + + std::sort(buttons.begin(), buttons.end()); + if(buttons == std::vector({ IDOK })) { + mbp.dwStyle |= MB_OK; + } else if(buttons == std::vector({ IDOK, IDCANCEL })) { + mbp.dwStyle |= MB_OKCANCEL; + } else if(buttons == std::vector({ IDYES, IDNO })) { + mbp.dwStyle |= MB_YESNO; + } else if(buttons == std::vector({ IDCANCEL, IDYES, IDNO })) { + mbp.dwStyle |= MB_YESNOCANCEL; + } else { + ssassert(false, "Unexpected button set"); + } + + switch(MessageBoxIndirectW(&mbp)) { + case IDOK: return Response::OK; break; + case IDYES: return Response::YES; break; + case IDNO: return Response::NO; break; + case IDCANCEL: return Response::CANCEL; break; + default: ssassert(false, "Unexpected response"); + } + } +}; + +MessageDialogRef CreateMessageDialog(WindowRef parentWindow) { + std::shared_ptr dialog = std::make_shared(); + dialog->mbp.hwndOwner = std::static_pointer_cast(parentWindow)->hWindow; + return dialog; +} + //----------------------------------------------------------------------------- // Application-wide APIs //----------------------------------------------------------------------------- diff --git a/src/platform/w32main.cpp b/src/platform/w32main.cpp index b6b9ad6..2e4c6b7 100644 --- a/src/platform/w32main.cpp +++ b/src/platform/w32main.cpp @@ -25,8 +25,6 @@ using Platform::Narrow; using Platform::Widen; -HFONT FixedFont; - #ifdef HAVE_SPACEWARE // The 6-DOF input device. SiHdl SpaceNavigator = SI_NO_HANDLE; @@ -39,139 +37,6 @@ std::wstring Title(const std::string &s) { return Widen("SolveSpace - " + s); } -//----------------------------------------------------------------------------- -// Routines to display message boxes on screen. Do our own, instead of using -// MessageBox, because that is not consistent from version to version and -// there's word wrap problems. -//----------------------------------------------------------------------------- - -HWND MessageWnd, OkButton; -bool MessageDone; -int MessageWidth, MessageHeight; -const char *MessageString; - -static LRESULT CALLBACK MessageProc(HWND hwnd, UINT msg, WPARAM wParam, - LPARAM lParam) -{ - switch (msg) { - case WM_COMMAND: - if((HWND)lParam == OkButton && wParam == BN_CLICKED) { - MessageDone = true; - } - break; - - case WM_CLOSE: - case WM_DESTROY: - MessageDone = true; - break; - - case WM_PAINT: { - PAINTSTRUCT ps; - HDC hdc = BeginPaint(hwnd, &ps); - SelectObject(hdc, FixedFont); - SetTextColor(hdc, 0x000000); - SetBkMode(hdc, TRANSPARENT); - RECT rc; - SetRect(&rc, 10, 10, MessageWidth, MessageHeight); - std::wstring text = Widen(MessageString); - DrawText(hdc, text.c_str(), text.length(), &rc, DT_LEFT | DT_WORDBREAK); - EndPaint(hwnd, &ps); - break; - } - - default: - return DefWindowProc(hwnd, msg, wParam, lParam); - } - - return 1; -} - -HWND CreateWindowClient(DWORD exStyle, const wchar_t *className, const wchar_t *windowName, - DWORD style, int x, int y, int width, int height, HWND parent, - HMENU menu, HINSTANCE instance, void *param) -{ - HWND h = CreateWindowExW(exStyle, className, windowName, style, x, y, - width, height, parent, menu, instance, param); - - RECT r; - GetClientRect(h, &r); - width = width - (r.right - width); - height = height - (r.bottom - height); - - SetWindowPos(h, HWND_TOP, x, y, width, height, 0); - - return h; -} - -void SolveSpace::DoMessageBox(const char *str, int rows, int cols, bool error) -{ - EnableWindow((HWND)SS.GW.window->NativePtr(), FALSE); - EnableWindow((HWND)SS.TW.window->NativePtr(), FALSE); - - // Register the window class for our dialog. - WNDCLASSEX wc = {}; - wc.cbSize = sizeof(wc); - wc.style = CS_BYTEALIGNCLIENT | CS_BYTEALIGNWINDOW | CS_OWNDC; - wc.lpfnWndProc = (WNDPROC)MessageProc; - wc.hInstance = NULL; - wc.hbrBackground = (HBRUSH)COLOR_BTNSHADOW; - wc.lpszClassName = L"MessageWnd"; - wc.lpszMenuName = NULL; - wc.hCursor = LoadCursor(NULL, IDC_ARROW); - wc.hIcon = (HICON)LoadImage(NULL, MAKEINTRESOURCE(4000), - IMAGE_ICON, 32, 32, 0); - wc.hIconSm = (HICON)LoadImage(NULL, MAKEINTRESOURCE(4000), - IMAGE_ICON, 16, 16, 0); - RegisterClassEx(&wc); - - // Create the window. - MessageString = str; - RECT r; - GetWindowRect((HWND)SS.GW.window->NativePtr(), &r); - int width = cols*SS.TW.CHAR_WIDTH_ + 20, - height = rows*SS.TW.LINE_HEIGHT + 60; - MessageWidth = width; - MessageHeight = height; - MessageWnd = CreateWindowClient(0, L"MessageWnd", - Title(error ? C_("title", "Error") : C_("title", "Message")).c_str(), - WS_OVERLAPPED | WS_SYSMENU, - r.left + 100, r.top + 100, width, height, NULL, NULL, NULL, NULL); - - OkButton = CreateWindowExW(0, WC_BUTTON, Widen(C_("button", "OK")).c_str(), - WS_CHILD | WS_TABSTOP | WS_CLIPSIBLINGS | WS_VISIBLE | BS_DEFPUSHBUTTON, - (width - 70)/2, rows*SS.TW.LINE_HEIGHT + 20, - 70, 25, MessageWnd, NULL, NULL, NULL); - SendMessage(OkButton, WM_SETFONT, (WPARAM)FixedFont, true); - - ShowWindow(MessageWnd, true); - SetFocus(OkButton); - - MSG msg; - DWORD ret; - MessageDone = false; - while((ret = GetMessage(&msg, NULL, 0, 0)) != 0 && !MessageDone) { - if((msg.message == WM_KEYDOWN && - (msg.wParam == VK_RETURN || - msg.wParam == VK_ESCAPE)) || - (msg.message == WM_KEYUP && - (msg.wParam == VK_SPACE))) - { - MessageDone = true; - break; - } - - TranslateMessage(&msg); - DispatchMessage(&msg); - } - - MessageString = NULL; - DestroyWindow(MessageWnd); - - EnableWindow((HWND)SS.GW.window->NativePtr(), TRUE); - EnableWindow((HWND)SS.TW.window->NativePtr(), TRUE); - SetForegroundWindow((HWND)SS.GW.window->NativePtr()); -} - void SolveSpace::OpenWebsite(const char *url) { ShellExecuteW((HWND)SS.GW.window->NativePtr(), L"open", Widen(url).c_str(), NULL, NULL, SW_SHOWNORMAL); @@ -266,87 +131,6 @@ bool SolveSpace::GetSaveFile(Platform::Path *filename, const std::string &defExt return OpenSaveFile(/*isOpen=*/false, filename, defExtension, filters); } -DialogChoice SolveSpace::SaveFileYesNoCancel() -{ - EnableWindow((HWND)SS.GW.window->NativePtr(), FALSE); - EnableWindow((HWND)SS.TW.window->NativePtr(), FALSE); - - int r = MessageBoxW((HWND)SS.GW.window->NativePtr(), - Widen(_("The file has changed since it was last saved.\n\n" - "Do you want to save the changes?")).c_str(), - Title(C_("title", "Modified File")).c_str(), - MB_YESNOCANCEL | MB_ICONWARNING); - - EnableWindow((HWND)SS.GW.window->NativePtr(), TRUE); - EnableWindow((HWND)SS.TW.window->NativePtr(), TRUE); - SetForegroundWindow((HWND)SS.GW.window->NativePtr()); - - switch(r) { - case IDYES: - return DIALOG_YES; - case IDNO: - return DIALOG_NO; - case IDCANCEL: - default: - return DIALOG_CANCEL; - } -} - -DialogChoice SolveSpace::LoadAutosaveYesNo() -{ - EnableWindow((HWND)SS.GW.window->NativePtr(), FALSE); - EnableWindow((HWND)SS.TW.window->NativePtr(), FALSE); - - int r = MessageBoxW((HWND)SS.GW.window->NativePtr(), - Widen(_("An autosave file is available for this project.\n\n" - "Do you want to load the autosave file instead?")).c_str(), - Title(C_("title", "Autosave Available")).c_str(), - MB_YESNO | MB_ICONWARNING); - - EnableWindow((HWND)SS.GW.window->NativePtr(), TRUE); - EnableWindow((HWND)SS.TW.window->NativePtr(), TRUE); - SetForegroundWindow((HWND)SS.GW.window->NativePtr()); - - switch (r) { - case IDYES: - return DIALOG_YES; - case IDNO: - default: - return DIALOG_NO; - } -} - -DialogChoice SolveSpace::LocateImportedFileYesNoCancel(const Platform::Path &filename, - bool canCancel) { - EnableWindow((HWND)SS.GW.window->NativePtr(), FALSE); - EnableWindow((HWND)SS.TW.window->NativePtr(), FALSE); - - std::string message = - "The linked file " + filename.raw + " is not present.\n\n" - "Do you want to locate it manually?\n\n" - "If you select \"No\", any geometry that depends on " - "the missing file will be removed."; - - int r = MessageBoxW((HWND)SS.GW.window->NativePtr(), - Widen(message).c_str(), - Title(C_("title", "Missing File")).c_str(), - (canCancel ? MB_YESNOCANCEL : MB_YESNO) | MB_ICONWARNING); - - EnableWindow((HWND)SS.GW.window->NativePtr(), TRUE); - EnableWindow((HWND)SS.TW.window->NativePtr(), TRUE); - SetForegroundWindow((HWND)SS.GW.window->NativePtr()); - - switch(r) { - case IDYES: - return DIALOG_YES; - case IDNO: - return DIALOG_NO; - case IDCANCEL: - default: - return DIALOG_CANCEL; - } -} - std::vector SolveSpace::GetFontFiles() { std::vector fonts; @@ -414,14 +198,6 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, icc.dwICC = ICC_STANDARD_CLASSES|ICC_BAR_CLASSES; InitCommonControlsEx(&icc); - // A monospaced font - FixedFont = CreateFontW(SS.TW.CHAR_HEIGHT, SS.TW.CHAR_WIDTH_, 0, 0, - FW_REGULAR, false, - false, false, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, - DEFAULT_QUALITY, FF_DONTCARE, L"Lucida Console"); - if(!FixedFont) - FixedFont = (HFONT)GetStockObject(SYSTEM_FONT); - std::vector args = InitPlatform(0, NULL); #ifdef HAVE_SPACEWARE diff --git a/src/solvespace.cpp b/src/solvespace.cpp index 1884075..ae381f7 100644 --- a/src/solvespace.cpp +++ b/src/solvespace.cpp @@ -147,7 +147,18 @@ bool SolveSpaceUI::LoadAutosaveFor(const Platform::Path &filename) { return false; fclose(f); - if(LoadAutosaveYesNo() == DIALOG_YES) { + Platform::MessageDialogRef dialog = CreateMessageDialog(GW.window); + + using Platform::MessageDialog; + dialog->SetType(MessageDialog::Type::QUESTION); + dialog->SetTitle(C_("title", "Autosave Available")); + dialog->SetMessage(C_("dialog", "An autosave file is available for this sketch.")); + dialog->SetDescription(C_("dialog", "Do you want to load the autosave file instead?")); + dialog->AddButton(C_("button", "&Load autosave"), MessageDialog::Response::YES, + /*isDefault=*/true); + dialog->AddButton(C_("button", "Do&n't Load"), MessageDialog::Response::NO); + + if(dialog->RunModal() == MessageDialog::Response::YES) { unsaved = true; return LoadFromFile(autosaveFile, /*canCancel=*/true); } @@ -419,18 +430,35 @@ void SolveSpaceUI::RemoveAutosave() bool SolveSpaceUI::OkayToStartNewFile() { if(!unsaved) return true; - switch(SaveFileYesNoCancel()) { - case DIALOG_YES: + Platform::MessageDialogRef dialog = CreateMessageDialog(GW.window); + + using Platform::MessageDialog; + dialog->SetType(MessageDialog::Type::QUESTION); + dialog->SetTitle(C_("title", "Modified File")); + if(!SolveSpace::SS.saveFile.IsEmpty()) { + dialog->SetMessage(ssprintf(C_("dialog", "Do you want to save the changes you made to " + "the sketch “%s”?"), saveFile.raw.c_str())); + } else { + dialog->SetMessage(C_("dialog", "Do you want to save the changes you made to " + "the new sketch?")); + } + dialog->SetDescription(C_("dialog", "Your changes will be lost if you don't save them.")); + dialog->AddButton(C_("button", "&Save"), MessageDialog::Response::YES, + /*isDefault=*/true); + dialog->AddButton(C_("button", "Do&n't Save"), MessageDialog::Response::NO); + dialog->AddButton(C_("button", "&Cancel"), MessageDialog::Response::CANCEL); + + switch(dialog->RunModal()) { + case MessageDialog::Response::YES: return GetFilenameAndSave(/*saveAs=*/false); - case DIALOG_NO: + case MessageDialog::Response::NO: RemoveAutosave(); return true; - case DIALOG_CANCEL: + default: return false; } - ssassert(false, "Unexpected dialog choice"); } void SolveSpaceUI::UpdateWindowTitles() { diff --git a/src/solvespace.h b/src/solvespace.h index 66f3366..b722b2e 100644 --- a/src/solvespace.h +++ b/src/solvespace.h @@ -139,13 +139,6 @@ enum class Command : uint32_t; const size_t MAX_RECENT = 8; extern Platform::Path RecentFile[MAX_RECENT]; -void RefreshRecentMenus(); - -enum DialogChoice { DIALOG_YES = 1, DIALOG_NO = -1, DIALOG_CANCEL = 0 }; -DialogChoice SaveFileYesNoCancel(); -DialogChoice LoadAutosaveYesNo(); -DialogChoice LocateImportedFileYesNoCancel(const Platform::Path &filename, - bool canCancel); #define AUTOSAVE_EXT "slvs~" @@ -170,9 +163,6 @@ void dbp(const char *str, ...); dbp("tri: (%.3f %.3f %.3f) (%.3f %.3f %.3f) (%.3f %.3f %.3f)", \ CO((tri).a), CO((tri).b), CO((tri).c)) -void SetMousePointerToHand(bool yes); -void DoMessageBox(const char *str, int rows, int cols, bool error); - std::vector InitPlatform(int argc, char **argv); void *AllocTemporary(size_t n); @@ -254,8 +244,8 @@ void MakeMatrix(double *mat, double a11, double a12, double a13, double a14, void MultMatrix(double *mata, double *matb, double *matr); int64_t GetMilliseconds(); -void Message(const char *str, ...); -void Error(const char *str, ...); +void Message(const char *fmt, ...); +void Error(const char *fmt, ...); class System { public: diff --git a/src/util.cpp b/src/util.cpp index f4064f5..1c907a8 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -103,73 +103,71 @@ void SolveSpace::MultMatrix(double *mata, double *matb, double *matr) { // Word-wrap the string for our message box appropriately, and then display // that string. //----------------------------------------------------------------------------- -static void DoStringForMessageBox(const char *str, va_list f, bool error) +static void MessageBox(const char *fmt, va_list va, bool error) { - char inBuf[1024*50]; - vsprintf(inBuf, str, f); +#ifndef LIBRARY + va_list va_size; + va_copy(va_size, va); + int size = vsnprintf(NULL, 0, fmt, va_size); + ssassert(size >= 0, "vsnprintf could not encode string"); + va_end(va_size); - char outBuf[1024*50]; - int i = 0, j = 0, len = 0, longestLen = 47; - int rows = 0, cols = 0; + std::string text; + text.resize(size); - // Count the width of the longest line that starts with spaces; those - // are list items, that should not be split in the middle. - bool listLine = false; - while(inBuf[i]) { - if(inBuf[i] == '\r') { - // ignore these - } else if(inBuf[i] == ' ' && len == 0) { - listLine = true; - } else if(inBuf[i] == '\n') { - if(listLine) longestLen = max(longestLen, len); - len = 0; - } else { - len++; + vsnprintf(&text[0], size + 1, fmt, va); + + // Split message text using a heuristic for better presentation. + size_t separatorAt = 0; + while(separatorAt != std::string::npos) { + size_t dotAt = text.find('.', separatorAt + 1), + colonAt = text.find(':', separatorAt + 1); + separatorAt = min(dotAt, colonAt); + if(separatorAt == std::string::npos || + (separatorAt + 1 < text.size() && isspace(text[separatorAt + 1]))) { + break; } - i++; } - if(listLine) longestLen = max(longestLen, len); - - // Word wrap according to our target line length longestLen. - len = 0; - i = 0; - while(inBuf[i]) { - if(inBuf[i] == '\r') { - // ignore these - } else if(inBuf[i] == '\n') { - outBuf[j++] = '\n'; - if(len == 0) rows++; - len = 0; - } else if(inBuf[i] == ' ' && len > longestLen) { - outBuf[j++] = '\n'; - len = 0; - } else { - outBuf[j++] = inBuf[i]; - // Count rows when we draw the first character; so an empty - // row doesn't end up counting. - if(len == 0) rows++; - len++; - } - cols = max(cols, len); - i++; + std::string message = text.substr(0, separatorAt + 1); + std::string description; + if(separatorAt != std::string::npos && separatorAt + 1 < text.size()) { + description = text.substr(separatorAt + 1); } - outBuf[j++] = '\0'; - // And then display the text with our actual longest line length. - DoMessageBox(outBuf, rows, cols, error); + std::string::iterator it = description.begin(); + while(isspace(*it)) it++; + description = description.substr(it - description.begin()); + + Platform::MessageDialogRef dialog = CreateMessageDialog(SS.GW.window); + + using Platform::MessageDialog; + if(error) { + dialog->SetType(MessageDialog::Type::ERROR); + } else { + dialog->SetType(MessageDialog::Type::INFORMATION); + } + dialog->SetTitle(error ? C_("title", "Error") : C_("title", "Message")); + dialog->SetMessage(message); + if(!description.empty()) { + dialog->SetDescription(description); + } + dialog->AddButton(C_("button", "&OK"), MessageDialog::Response::OK, + /*isDefault=*/true); + dialog->RunModal(); +#endif } -void SolveSpace::Error(const char *str, ...) +void SolveSpace::Error(const char *fmt, ...) { va_list f; - va_start(f, str); - DoStringForMessageBox(str, f, /*error=*/true); + va_start(f, fmt); + MessageBox(fmt, f, /*error=*/true); va_end(f); } -void SolveSpace::Message(const char *str, ...) +void SolveSpace::Message(const char *fmt, ...) { va_list f; - va_start(f, str); - DoStringForMessageBox(str, f, /*error=*/false); + va_start(f, fmt); + MessageBox(fmt, f, /*error=*/false); va_end(f); }