From 136cd6ac51fe8d34f1acc1445ab42c92b4232922 Mon Sep 17 00:00:00 2001 From: James Turner Date: Wed, 6 Nov 2013 15:49:58 -0800 Subject: [PATCH] Message box support. This allows us to display a platform-native dialog for problems which occur early in startup (before we can show a PUI/Canvas dialog). In particular this improves feedback where FG_HOME, FG_DATA or aircraft selection is wrong, all of which happen very early in startup. --- src/GUI/CMakeLists.txt | 12 ++- src/GUI/CocoaAutoreleasePool.hxx | 24 +++++ src/GUI/CocoaMessageBox.mm | 42 +++++++++ src/GUI/CocoaMouseCursor.mm | 5 +- src/GUI/FGCocoaMenuBar.mm | 58 ++++++------ src/GUI/MessageBox.cxx | 148 +++++++++++++++++++++++++++++++ src/GUI/MessageBox.hxx | 26 ++++++ src/Main/bootstrap.cxx | 36 +++++--- src/Main/fg_init.cxx | 30 ++++--- src/Main/options.cxx | 33 ++++--- 10 files changed, 340 insertions(+), 74 deletions(-) create mode 100644 src/GUI/CocoaAutoreleasePool.hxx create mode 100644 src/GUI/CocoaMessageBox.mm create mode 100644 src/GUI/MessageBox.cxx create mode 100644 src/GUI/MessageBox.hxx diff --git a/src/GUI/CMakeLists.txt b/src/GUI/CMakeLists.txt index 6f36a1da2..573bbf452 100644 --- a/src/GUI/CMakeLists.txt +++ b/src/GUI/CMakeLists.txt @@ -21,6 +21,7 @@ set(SOURCES FileDialog.cxx PUIFileDialog.cxx MouseCursor.cxx + MessageBox.cxx ) set(HEADERS @@ -41,6 +42,7 @@ set(HEADERS FileDialog.hxx PUIFileDialog.hxx MouseCursor.hxx + MessageBox.hxx ) if(WIN32) @@ -51,8 +53,14 @@ if(WIN32) endif() if (APPLE) - list(APPEND HEADERS FGCocoaMenuBar.hxx CocoaFileDialog.hxx CocoaMouseCursor.hxx) - list(APPEND SOURCES FGCocoaMenuBar.mm CocoaFileDialog.mm CocoaMouseCursor.mm) + list(APPEND HEADERS FGCocoaMenuBar.hxx + CocoaFileDialog.hxx + CocoaMouseCursor.hxx + CocoaAutoreleasePool.hxx) + list(APPEND SOURCES FGCocoaMenuBar.mm + CocoaFileDialog.mm + CocoaMouseCursor.mm + CocoaMessageBox.mm) endif() flightgear_component(GUI "${SOURCES}" "${HEADERS}") diff --git a/src/GUI/CocoaAutoreleasePool.hxx b/src/GUI/CocoaAutoreleasePool.hxx new file mode 100644 index 000000000..bd283df50 --- /dev/null +++ b/src/GUI/CocoaAutoreleasePool.hxx @@ -0,0 +1,24 @@ +#ifndef FG_GUI_COCOA_AUTORELEASE_POOL_HXX +#define FG_GUI_COCOA_AUTORELEASE_POOL_HXX + +#include + +class CocoaAutoreleasePool +{ +public: + CocoaAutoreleasePool() + { + pool = [[NSAutoreleasePool alloc] init]; + } + + ~CocoaAutoreleasePool() + { + [pool release]; + } + +private: + NSAutoreleasePool* pool; +}; + + +#endif // of FG_GUI_COCOA_AUTORELEASE_POOL_HXX \ No newline at end of file diff --git a/src/GUI/CocoaMessageBox.mm b/src/GUI/CocoaMessageBox.mm new file mode 100644 index 000000000..cc521685c --- /dev/null +++ b/src/GUI/CocoaMessageBox.mm @@ -0,0 +1,42 @@ + +#include + +#include +#include +#include + +static NSString* stdStringToCocoa(const std::string& s) +{ + return [NSString stringWithUTF8String:s.c_str()]; +} + +flightgear::MessageBoxResult cocoaMessageBox(const std::string& msg, + const std::string& text) +{ + CocoaAutoreleasePool pool; + NSAlert* alert = [NSAlert alertWithMessageText:stdStringToCocoa(msg) + defaultButton:nil /* localized 'ok' */ + alternateButton:nil + otherButton:nil + informativeTextWithFormat:@"%@",stdStringToCocoa(text)]; + [[alert retain] autorelease]; + [alert runModal]; + return flightgear::MSG_BOX_OK; +} + + + +flightgear::MessageBoxResult cocoaFatalMessage(const std::string& msg, + const std::string& text) +{ + CocoaAutoreleasePool pool; + NSAlert* alert = [NSAlert alertWithMessageText:stdStringToCocoa(msg) + defaultButton:@"Quit FlightGear" + alternateButton:nil + otherButton:nil + informativeTextWithFormat:@"%@", stdStringToCocoa(text)]; + [[alert retain] autorelease]; + [alert runModal]; + return flightgear::MSG_BOX_OK; +} + diff --git a/src/GUI/CocoaMouseCursor.mm b/src/GUI/CocoaMouseCursor.mm index 8858ff52a..53dcccf00 100644 --- a/src/GUI/CocoaMouseCursor.mm +++ b/src/GUI/CocoaMouseCursor.mm @@ -23,6 +23,7 @@ #include #include
+#include class CocoaMouseCursor::CocoaMouseCursorPrivate { @@ -84,7 +85,8 @@ void CocoaMouseCursor::setCursor(Cursor aCursor) if (aCursor == d->activeCursorKey) { return; } - + + CocoaAutoreleasePool pool; d->activeCursorKey = aCursor; if (d->cursors.find(aCursor) == d->cursors.end()) { d->cursors[aCursor] = cocoaCursorForKey(aCursor); @@ -96,6 +98,7 @@ void CocoaMouseCursor::setCursor(Cursor aCursor) void CocoaMouseCursor::setCursorVisible(bool aVis) { + CocoaAutoreleasePool pool; if (aVis) { [NSCursor unhide]; } else { diff --git a/src/GUI/FGCocoaMenuBar.mm b/src/GUI/FGCocoaMenuBar.mm index 7bedc33d5..d70baa5d5 100644 --- a/src/GUI/FGCocoaMenuBar.mm +++ b/src/GUI/FGCocoaMenuBar.mm @@ -11,6 +11,7 @@ #include #include
+#include #include @@ -69,18 +70,29 @@ static NSString* stdStringToCocoa(const string& s) return [NSString stringWithUTF8String:s.c_str()]; } -static void setFunctionKeyShortcut(NSMenuItem* item, unichar shortcut) +static void setFunctionKeyShortcut(const std::string& shortcut, NSMenuItem* item) { + unichar shortcutChar = NSF1FunctionKey; + if (shortcut == "F11") { + shortcutChar = NSF11FunctionKey; + } else if (shortcut == "F12") { + shortcutChar = NSF12FunctionKey; + } else { + SG_LOG(SG_GENERAL, SG_WARN, "CocoaMenu:setFunctionKeyShortcut: unsupported:" << shortcut); + } + unichar ch[1]; - ch[0] = shortcut; + ch[0] = shortcutChar; [item setKeyEquivalentModifierMask:NSFunctionKeyMask]; [item setKeyEquivalent:[NSString stringWithCharacters:ch length:1]]; } + + static void setItemShortcutFromString(NSMenuItem* item, const string& s) { - const char* shortcut = ""; + std::string shortcut; bool hasCtrl = strutils::starts_with(s, "Ctrl-"); bool hasShift = strutils::starts_with(s, "Shift-"); @@ -91,21 +103,17 @@ static void setItemShortcutFromString(NSMenuItem* item, const string& s) if (hasCtrl) offset += 5; if (hasAlt) offset += 4; - shortcut = s.c_str() + offset; - if (!strcmp(shortcut, "Esc")) + shortcut = s.substr(offset); + if (shortcut == "Esc") shortcut = "\e"; - if (!strcmp(shortcut, "F11")) { - setFunctionKeyShortcut(item, NSF11FunctionKey); - return; - } - - if (!strcmp(shortcut, "F12")) { - setFunctionKeyShortcut(item, NSF12FunctionKey); - return; - } - - [item setKeyEquivalent:[NSString stringWithCString:shortcut encoding:NSUTF8StringEncoding]]; + if ((shortcut.length() >= 2) && (shortcut[0] == 'F') && isdigit(shortcut[1])) { + setFunctionKeyShortcut(shortcut, item); + return; + } + + simgear::strutils::lowercase(shortcut); + [item setKeyEquivalent:[NSString stringWithCString:shortcut.c_str() encoding:NSUTF8StringEncoding]]; NSUInteger modifiers = 0; if (hasCtrl) modifiers |= NSControlKeyMask; if (hasShift) modifiers |= NSShiftKeyMask; @@ -115,23 +123,7 @@ static void setItemShortcutFromString(NSMenuItem* item, const string& s) } namespace { - class CocoaAutoreleasePool - { - public: - CocoaAutoreleasePool() - { - pool = [[NSAutoreleasePool alloc] init]; - } - - ~CocoaAutoreleasePool() - { - [pool release]; - } - - private: - NSAutoreleasePool* pool; - }; - + class CocoaEnabledListener : public SGPropertyChangeListener { public: diff --git a/src/GUI/MessageBox.cxx b/src/GUI/MessageBox.cxx new file mode 100644 index 000000000..93563c66d --- /dev/null +++ b/src/GUI/MessageBox.cxx @@ -0,0 +1,148 @@ +#ifdef HAVE_CONFIG_H + #include "config.h" +#endif + +#include + +#include "MessageBox.hxx" + +#include
+#include + +#include + +#include + +#ifdef SG_WINDOWS + #include + +#include +#include +#endif + +#if defined(SG_MAC) + +// externs from CocoaMessageBox.mm +flightgear::MessageBoxResult +cocoaFatalMessage(const std::string& msg, const std::string& text); + +flightgear::MessageBoxResult +cocoaMessageBox(const std::string& msg, const std::string& text); + +#endif + +using namespace simgear::strutils; + +namespace { + +bool isCanvasImplementationRegistered() +{ + SGCommandMgr* cmd = globals->get_commands(); + return (cmd->getCommand("canvas-message-box") != NULL); +} + +#if defined(SG_WINDOWS) + +HWND getMainViewerHWND() +{ + osgViewer::Viewer::Windows windows; + if (!globals->get_renderer() || !globals->get_renderer()->getViewer()) { + return 0; + } + + globals->get_renderer()->getViewer()->getWindows(windows); + osgViewer::Viewer::Windows::const_iterator it = windows.begin(); + for(; it != windows.end(); ++it) { + if (strcmp((*it)->className(), "GraphicsWindowWin32")) { + continue; + } + + osgViewer::GraphicsWindowWin32* platformWin = + static_cast(*it); + return platformWin->getHWND(); + } + + return 0; +} + +flightgear::MessageBoxResult +win32MessageBox(const std::string& caption, + const std::string& msg, + const std::string& moreText) +{ + // during early startup (aircraft / fg-data validation) there is no + // osgViewer so no HWND. + HWND ownerWindow = getMainViewerHWND(); + std::string fullMsg(msg); + if (!moreText.empty()) { + fullMsg += "\n\n" + moreText; + } + + UINT mbType = MB_OK; + WCharVec wMsg(convertUtf8ToWString(fullMsg)), + wCap(convertUtf8ToWString(caption)); + wMsg.push_back(0); + wCap.push_back(0); + + ::MessageBoxExW(ownerWindow, wMsg.data(), wCap.data(), + mbType, 0 /* system lang */); + + return flightgear::MSG_BOX_OK; +} + +#endif + +} // anonymous namespace + +namespace flightgear +{ + +MessageBoxResult modalMessageBox(const std::string& caption, + const std::string& msg, + const std::string& moreText) +{ + // prefer canvas + if (isCanvasImplementationRegistered()) { + SGPropertyNode_ptr args(new SGPropertyNode); + args->setStringValue("caption", caption); + args->setStringValue("message", msg); + args->setStringValue("more", moreText); + globals->get_commands()->execute("canvas-message-box", args); + + // how to make it modal? + + return MSG_BOX_OK; + } + +#if defined(SG_WINDOWS) + return win32MessageBox(caption, msg, moreText); +#elif defined(SG_MAC) + return cocoaFatalMessage(msg, moreText); +#else + SG_LOG(SG_GENERAL, SG_ALERT, caption << ":" << msg); + if (!moreText.empty()) { + SG_LOG(SG_GENERAL, SG_ALERT, "(" << moreText << ")"); + } + return MSG_BOX_OK; +#endif +} + +MessageBoxResult fatalMessageBox(const std::string& caption, + const std::string& msg, + const std::string& moreText) +{ +#if defined(SG_WINDOWS) + return win32MessageBox(caption, msg, moreText); +#elif defined(SG_MAC) + return cocoaFatalMessage(msg, moreText); +#else + std::cerr << "FATAL:" << msg << "\n"; + if (!moreText.empty()) { + std::cerr << "(" << moreText << ")"; + } + std::cerr << std::endl; + return MSG_BOX_OK; +#endif +} + +} // of namespace flightgear diff --git a/src/GUI/MessageBox.hxx b/src/GUI/MessageBox.hxx new file mode 100644 index 000000000..505b8ccfb --- /dev/null +++ b/src/GUI/MessageBox.hxx @@ -0,0 +1,26 @@ +#ifndef FG_GUI_MESSAGE_BOX_HXX +#define FG_GUI_MESSAGE_BOX_HXX + +#include + +namespace flightgear +{ + +enum MessageBoxResult +{ + MSG_BOX_OK, + MSG_BOX_YES, + MSG_BOX_NO +}; + +MessageBoxResult modalMessageBox(const std::string& caption, + const std::string& msg, + const std::string& moreText = std::string()); + +MessageBoxResult fatalMessageBox(const std::string& caption, + const std::string& msg, + const std::string& moreText = std::string()); + +} // of namespace flightgear + +#endif // of FG_GUI_MESSAGE_BOX_HXX diff --git a/src/Main/bootstrap.cxx b/src/Main/bootstrap.cxx index 2aef4c2c0..2b5cb2b26 100644 --- a/src/Main/bootstrap.cxx +++ b/src/Main/bootstrap.cxx @@ -57,12 +57,16 @@ using std::endl; #include #include "main.hxx" -#include "globals.hxx" -#include "fg_props.hxx" - +#include
+#include
+#include #include "fg_os.hxx" +#if defined(SG_MAC) +#include +#endif + std::string homedir; std::string hostname; @@ -120,7 +124,7 @@ static void initFPE() } #endif -#if defined(_MSC_VER) || defined(_WIN32) +#if defined(SG_WINDOWS) int main ( int argc, char **argv ); int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { @@ -141,7 +145,7 @@ int _bootstrap_OSInit; // Main entry point; catch any exceptions that have made it this far. int main ( int argc, char **argv ) { -#if defined(_MSC_VER) || defined(_WIN32) +#if defined(SG_WINDOWS) // Don't show blocking "no disk in drive" error messages on Windows 7, // silently return errors to application instead. // See Microsoft MSDN #ms680621: "GUI apps should specify SEM_NOOPENFILEERRORBOX" @@ -157,6 +161,14 @@ int main ( int argc, char **argv ) signal(SIGPIPE, SIG_IGN); #endif +#if defined(SG_MAC) + // required so native messages boxes work prior to osgViewer init + // (only needed when not running as a bundled app) + ProcessSerialNumber sn = { 0, kCurrentProcess }; + TransformProcessType(&sn,kProcessTransformToForegroundApplication); + SetFrontProcess(&sn); +#endif + #ifdef PTW32_STATIC_LIB // Initialise static pthread win32 lib pthread_win32_process_attach_np (); @@ -208,19 +220,15 @@ int main ( int argc, char **argv ) } catch (const sg_throwable &t) { - // We must use cerr rather than - // logging, since logging may be - // disabled. - cerr << "Fatal error: " << t.getFormattedMessage() << endl; + std::string info; if (std::strlen(t.getOrigin()) != 0) - cerr << " (received from " << t.getOrigin() << ')' << endl; + info = std::string("received from ") + t.getOrigin(); + flightgear::fatalMessageBox("Fatal exception", t.getFormattedMessage(), info); } catch (const std::exception &e ) { - cerr << "Fatal error (std::exception): " << e.what() << endl; - + flightgear::fatalMessageBox("Fatal exception", e.what()); } catch (const std::string &s) { - cerr << "Fatal error (std::string): " << s << endl; - + flightgear::fatalMessageBox("Fatal exception", s); } catch (const char *s) { cerr << "Fatal error (const char*): " << s << endl; diff --git a/src/Main/fg_init.cxx b/src/Main/fg_init.cxx index 1c39233f4..8ad9803f9 100644 --- a/src/Main/fg_init.cxx +++ b/src/Main/fg_init.cxx @@ -76,6 +76,7 @@ #include #include #include +#include #include #include #include @@ -127,17 +128,13 @@ using namespace boost::algorithm; string fgBasePackageVersion() { SGPath base_path( globals->get_fg_root() ); base_path.append("version"); - + if (!base_path.exists()) { + return string(); + } + sg_gzifstream in( base_path.str() ); - if ( !in.is_open() ) { - SGPath old_path( globals->get_fg_root() ); - old_path.append( "Thanks" ); - sg_gzifstream old( old_path.str() ); - if ( !old.is_open() ) { - return "[none]"; - } else { - return "[old version]"; - } + if (!in.is_open()) { + return string(); } string version; @@ -219,6 +216,7 @@ public: { std::string aircraft = fgGetString( "/sim/aircraft", ""); if (aircraft.empty()) { + flightgear::fatalMessageBox("No aircraft", "No aircraft was specified"); SG_LOG(SG_GENERAL, SG_ALERT, "no aircraft specified"); return false; } @@ -236,6 +234,9 @@ public: readProperties(setFile.str(), globals->get_props()); } catch ( const sg_exception &e ) { SG_LOG(SG_INPUT, SG_ALERT, "Error reading aircraft: " << e.getFormattedMessage()); + flightgear::fatalMessageBox("Error reading aircraft", + "An error occured reading the requested aircraft (" + aircraft + ")", + e.getFormattedMessage()); return false; } @@ -243,6 +244,9 @@ public: } else { SG_LOG(SG_GENERAL, SG_ALERT, "aircraft '" << _searchAircraft << "' not found in specified dir:" << aircraftDir); + flightgear::fatalMessageBox("Aircraft not found", + "The requested aircraft '" + aircraft + "' could not be found in the specified location.", + aircraftDir); return false; } } @@ -262,6 +266,9 @@ public: if (_foundPath.str().empty()) { SG_LOG(SG_GENERAL, SG_ALERT, "Cannot find specified aircraft: " << aircraft ); + flightgear::fatalMessageBox("Aircraft not found", + "The requested aircraft '" + aircraft + "' could not be found in any of the search paths"); + return false; } @@ -276,6 +283,9 @@ public: readProperties(_foundPath.str(), globals->get_props()); } catch ( const sg_exception &e ) { SG_LOG(SG_INPUT, SG_ALERT, "Error reading aircraft: " << e.getFormattedMessage()); + flightgear::fatalMessageBox("Error reading aircraft", + "An error occured reading the requested aircraft (" + aircraft + ")", + e.getFormattedMessage()); return false; } diff --git a/src/Main/options.cxx b/src/Main/options.cxx index 036d3146a..306bd4fe2 100644 --- a/src/Main/options.cxx +++ b/src/Main/options.cxx @@ -50,8 +50,11 @@ #include #include #include + #include +#include +#include
#include "globals.hxx" #include "fg_init.hxx" #include "fg_props.hxx" @@ -2289,21 +2292,23 @@ void Options::setupRoot() // validate it static char required_version[] = FLIGHTGEAR_VERSION; string base_version = fgBasePackageVersion(); - if ( !(base_version == required_version) ) { - // tell the operator how to use this application + if (base_version.empty()) { + flightgear::fatalMessageBox("Base package not found", + "Required data files not found, check your installation.", + "Looking for base-package files at: '" + root + "'"); + + exit(-1); + } - simgear::requestConsole(); // ensure console is shown on Windows - - cerr << endl << "Base package check failed:" << endl \ - << " Version " << base_version << " found at: " \ - << globals->get_fg_root() << endl \ - << " Version " << required_version << " is required." << endl \ - << "Please upgrade/downgrade base package and set the path to your fgdata" << endl \ - << "with --fg-root=path_to_your_fgdata" << endl; -#ifdef _MSC_VER - cerr << "Hit a key to continue..." << endl; - cin.get(); -#endif + if (base_version != required_version) { + // tell the operator how to use this application + + flightgear::fatalMessageBox("Base package version mismatch", + "Version check failed: please check your installation.", + "Found data files for version '" + base_version + + "' at '" + globals->get_fg_root() + "', version '" + + required_version + "' is required."); + exit(-1); } } -- 2.39.5