diff --git a/res/cocoa/SaveFormatAccessory.xib b/res/cocoa/SaveFormatAccessory.xib index 3962b6a..3baf4c6 100644 --- a/res/cocoa/SaveFormatAccessory.xib +++ b/res/cocoa/SaveFormatAccessory.xib @@ -7,6 +7,7 @@ + diff --git a/src/file.cpp b/src/file.cpp index 24eb6c3..c3c8c29 100644 --- a/src/file.cpp +++ b/src/file.cpp @@ -848,6 +848,8 @@ static Platform::MessageDialog::Response LocateImportedFile(const Platform::Path } bool SolveSpaceUI::ReloadAllLinked(const Platform::Path &saveFile, bool canCancel) { + Platform::SettingsRef settings = Platform::GetSettings(); + std::map linkMap; allConsistent = false; @@ -877,10 +879,13 @@ try_again: // The file was moved; prompt the user for its new location. 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; + Platform::FileDialogRef dialog = Platform::CreateOpenFileDialog(SS.GW.window); + dialog->AddFilters(Platform::SolveSpaceModelFileFilters); + dialog->ThawChoices(settings, "LinkSketch"); + if(dialog->RunModal()) { + dialog->FreezeChoices(settings, "LinkSketch"); + linkMap[g.linkFile] = dialog->GetFilename(); + g.linkFile = dialog->GetFilename(); goto try_again; } else { if(canCancel) return false; @@ -917,6 +922,8 @@ try_again: bool SolveSpaceUI::ReloadLinkedImage(const Platform::Path &saveFile, Platform::Path *filename, bool canCancel) { + Platform::SettingsRef settings = Platform::GetSettings(); + std::shared_ptr pixmap; bool promptOpenFile = false; if(filename->IsEmpty()) { @@ -948,7 +955,12 @@ bool SolveSpaceUI::ReloadLinkedImage(const Platform::Path &saveFile, } if(promptOpenFile) { - if(GetOpenFile(filename, "", RasterFileFilter)) { + Platform::FileDialogRef dialog = Platform::CreateOpenFileDialog(SS.GW.window); + dialog->AddFilters(Platform::RasterFileFilters); + dialog->ThawChoices(settings, "LinkImage"); + if(dialog->RunModal()) { + dialog->FreezeChoices(settings, "LinkImage"); + *filename = dialog->GetFilename(); pixmap = Pixmap::ReadPng(*filename); if(pixmap == NULL) { Error("The image '%s' is corrupted.", filename->raw.c_str()); diff --git a/src/graphicswin.cpp b/src/graphicswin.cpp index f08eea7..b3e261e 100644 --- a/src/graphicswin.cpp +++ b/src/graphicswin.cpp @@ -45,7 +45,7 @@ const MenuEntry Menu[] = { { 1, N_("&Save"), Command::SAVE, C|'s', KN, mFile }, { 1, N_("Save &As..."), Command::SAVE_AS, 0, KN, mFile }, { 1, NULL, Command::NONE, 0, KN, NULL }, -{ 1, N_("Export &Image..."), Command::EXPORT_PNG, 0, KN, mFile }, +{ 1, N_("Export &Image..."), Command::EXPORT_IMAGE, 0, KN, mFile }, { 1, N_("Export 2d &View..."), Command::EXPORT_VIEW, 0, KN, mFile }, { 1, N_("Export 2d &Section..."), Command::EXPORT_SECTION, 0, KN, mFile }, { 1, N_("Export 3d &Wireframe..."), Command::EXPORT_WIREFRAME, 0, KN, mFile }, diff --git a/src/group.cpp b/src/group.cpp index 09a5568..b906069 100644 --- a/src/group.cpp +++ b/src/group.cpp @@ -74,6 +74,8 @@ void Group::MenuGroup(Command id) { } void Group::MenuGroup(Command id, Platform::Path linkFile) { + Platform::SettingsRef settings = Platform::GetSettings(); + Group g = {}; g.visible = true; g.color = RGBi(100, 100, 100); @@ -229,7 +231,12 @@ void Group::MenuGroup(Command id, Platform::Path linkFile) { g.type = Type::LINKED; g.meshCombine = CombineAs::ASSEMBLE; if(g.linkFile.IsEmpty()) { - if(!GetOpenFile(&g.linkFile, "", SlvsFileFilter)) return; + Platform::FileDialogRef dialog = Platform::CreateOpenFileDialog(SS.GW.window); + dialog->AddFilters(Platform::SolveSpaceModelFileFilters); + dialog->ThawChoices(settings, "LinkSketch"); + if(!dialog->RunModal()) return; + dialog->FreezeChoices(settings, "LinkSketch"); + g.linkFile = dialog->GetFilename(); } // Assign the default name of the group based on the name of diff --git a/src/platform/climain.cpp b/src/platform/climain.cpp index 5074de2..77ecc8c 100644 --- a/src/platform/climain.cpp +++ b/src/platform/climain.cpp @@ -5,11 +5,6 @@ //----------------------------------------------------------------------------- #include "solvespace.h" -namespace SolveSpace { - // These are defined in headless.cpp, and aren't exposed in solvespace.h. - extern std::shared_ptr framebuffer; -} - static void ShowUsage(const std::string &cmd) { fprintf(stderr, "Usage: %s [filename...]", cmd.c_str()); //-----------------------------------------------------------------------------> 80 col */ @@ -50,21 +45,21 @@ Commands: Reloads all imported files, regenerates the sketch, and saves it. )"); - auto FormatListFromFileFilter = [](const FileFilter *filter) { + auto FormatListFromFileFilters = [](const std::vector &filters) { std::string descr; - while(filter->name) { + for(auto filter : filters) { descr += "\n "; - descr += filter->name; + descr += filter.name; descr += " ("; - const char *const *patterns = filter->patterns; - while(*patterns) { - descr += *patterns; - if(*++patterns) { + bool first = true; + for(auto extension : filter.extensions) { + if(!first) { descr += ", "; } + descr += extension; + first = false; } descr += ")"; - filter++; } return descr; }; @@ -76,11 +71,11 @@ File formats: export-wireframe:%s export-mesh:%s export-surfaces:%s -)", FormatListFromFileFilter(RasterFileFilter).c_str(), - FormatListFromFileFilter(VectorFileFilter).c_str(), - FormatListFromFileFilter(Vector3dFileFilter).c_str(), - FormatListFromFileFilter(MeshFileFilter).c_str(), - FormatListFromFileFilter(SurfaceFileFilter).c_str()); +)", FormatListFromFileFilters(Platform::RasterFileFilters).c_str(), + FormatListFromFileFilters(Platform::VectorFileFilters).c_str(), + FormatListFromFileFilters(Platform::Vector3dFileFilters).c_str(), + FormatListFromFileFilters(Platform::MeshFileFilters).c_str(), + FormatListFromFileFilters(Platform::SurfaceFileFilters).c_str()); } static bool RunCommand(const std::vector args) { diff --git a/src/platform/cocoamain.mm b/src/platform/cocoamain.mm index 8640d43..65be7eb 100644 --- a/src/platform/cocoamain.mm +++ b/src/platform/cocoamain.mm @@ -10,118 +10,6 @@ using SolveSpace::dbp; -/* Utility functions */ - -static NSString* Wrap(const std::string &s) { - return [NSString stringWithUTF8String:s.c_str()]; -} - -/* Save/load */ - -bool SolveSpace::GetOpenFile(Platform::Path *filename, const std::string &defExtension, - const FileFilter ssFilters[]) { - NSOpenPanel *panel = [NSOpenPanel openPanel]; - NSMutableArray *filters = [[NSMutableArray alloc] init]; - for(const FileFilter *ssFilter = ssFilters; ssFilter->name; ssFilter++) { - for(const char *const *ssPattern = ssFilter->patterns; *ssPattern; ssPattern++) { - [filters addObject:[NSString stringWithUTF8String:*ssPattern]]; - } - } - [filters removeObjectIdenticalTo:@"*"]; - [panel setAllowedFileTypes:filters]; - - if([panel runModal] == NSFileHandlingPanelOKButton) { - *filename = Platform::Path::From( - [[NSFileManager defaultManager] - fileSystemRepresentationWithPath:[[panel URL] path]]); - return true; - } else { - return false; - } -} - -@interface SaveFormatController : NSViewController -@property NSSavePanel *panel; -@property NSArray *extensions; -@property (nonatomic) IBOutlet NSPopUpButton *button; -@property (nonatomic) NSInteger index; -@end - -@implementation SaveFormatController -@synthesize panel, extensions, button, index; -- (void)setIndex:(NSInteger)newIndex { - self->index = newIndex; - NSString *extension = [extensions objectAtIndex:newIndex]; - if(![extension isEqual:@"*"]) { - NSString *filename = [panel nameFieldStringValue]; - NSString *basename = [[filename componentsSeparatedByString:@"."] objectAtIndex:0]; - [panel setNameFieldStringValue:[basename stringByAppendingPathExtension:extension]]; - } -} -@end - -bool SolveSpace::GetSaveFile(Platform::Path *filename, const std::string &defExtension, - const FileFilter ssFilters[]) { - NSSavePanel *panel = [NSSavePanel savePanel]; - - SaveFormatController *controller = - [[SaveFormatController alloc] initWithNibName:@"SaveFormatAccessory" bundle:nil]; - [controller setPanel:panel]; - [panel setAccessoryView:[controller view]]; - - NSMutableArray *extensions = [[NSMutableArray alloc] init]; - - NSPopUpButton *button = [controller button]; - [button removeAllItems]; - for(const FileFilter *ssFilter = ssFilters; ssFilter->name; ssFilter++) { - std::string desc; - for(const char *const *ssPattern = ssFilter->patterns; *ssPattern; ssPattern++) { - if(desc == "") { - desc = *ssPattern; - } else { - desc += ", "; - desc += *ssPattern; - } - } - std::string title = Translate(ssFilter->name) + " (" + desc + ")"; - [button addItemWithTitle:Wrap(title)]; - [extensions addObject:[NSString stringWithUTF8String:ssFilter->patterns[0]]]; - } - [panel setAllowedFileTypes:extensions]; - [controller setExtensions:extensions]; - - int extensionIndex = 0; - if(defExtension != "") { - extensionIndex = [extensions indexOfObject:Wrap(defExtension)]; - if(extensionIndex == -1) { - extensionIndex = 0; - } - } - [button selectItemAtIndex:extensionIndex]; - - if(filename->IsEmpty()) { - [panel setNameFieldStringValue: - [Wrap(_("untitled")) - stringByAppendingPathExtension:[extensions objectAtIndex:extensionIndex]]]; - } else { - [panel setDirectoryURL: - [NSURL fileURLWithPath:Wrap(filename->Parent().raw) - isDirectory:NO]]; - [panel setNameFieldStringValue: - [Wrap(filename->FileStem()) - stringByAppendingPathExtension:[extensions objectAtIndex:extensionIndex]]]; - } - - if([panel runModal] == NSFileHandlingPanelOKButton) { - *filename = Platform::Path::From( - [[NSFileManager defaultManager] - fileSystemRepresentationWithPath:[[panel URL] path]]); - return true; - } else { - return false; - } -} - /* Miscellanea */ void SolveSpace::OpenWebsite(const char *url) { diff --git a/src/platform/gtkmain.cpp b/src/platform/gtkmain.cpp index 05e35bd..bf43340 100644 --- a/src/platform/gtkmain.cpp +++ b/src/platform/gtkmain.cpp @@ -52,121 +52,6 @@ #endif namespace SolveSpace { -/* Utility functions */ -std::string Title(const std::string &s) { - return "SolveSpace - " + s; -} - -/* Save/load */ - -static std::string ConvertFilters(std::string active, const FileFilter ssFilters[], - Gtk::FileChooser *chooser) { - for(const FileFilter *ssFilter = ssFilters; ssFilter->name; ssFilter++) { - Glib::RefPtr filter = Gtk::FileFilter::create(); - filter->set_name(Translate(ssFilter->name)); - - bool is_active = false; - std::string desc = ""; - for(const char *const *ssPattern = ssFilter->patterns; *ssPattern; ssPattern++) { - std::string pattern = "*." + std::string(*ssPattern); - filter->add_pattern(pattern); - filter->add_pattern(Glib::ustring(pattern).uppercase()); - if(active == "") - active = pattern.substr(2); - if("*." + active == pattern) - is_active = true; - if(desc == "") - desc = pattern; - else - desc += ", " + pattern; - } - filter->set_name(filter->get_name() + " (" + desc + ")"); - - chooser->add_filter(filter); - if(is_active) - chooser->set_filter(filter); - } - - return active; -} - -bool GetOpenFile(Platform::Path *filename, const std::string &activeOrEmpty, - const FileFilter filters[]) { - Gtk::FileChooserDialog chooser(*(Gtk::Window *)SS.GW.window->NativePtr(), - Title(C_("title", "Open File"))); - chooser.set_filename(filename->raw); - chooser.add_button(_("_Cancel"), Gtk::RESPONSE_CANCEL); - chooser.add_button(_("_Open"), Gtk::RESPONSE_OK); - chooser.set_current_folder(Platform::GetSettings()->ThawString("FileChooserPath")); - - ConvertFilters(activeOrEmpty, filters, &chooser); - - if(chooser.run() == Gtk::RESPONSE_OK) { - Platform::GetSettings()->FreezeString("FileChooserPath", chooser.get_current_folder()); - *filename = Platform::Path::From(chooser.get_filename()); - return true; - } else { - return false; - } -} - -static void ChooserFilterChanged(Gtk::FileChooserDialog *chooser) -{ - /* Extract the pattern from the filter. GtkFileFilter doesn't provide - any way to list the patterns, so we extract it from the filter name. - Gross. */ - std::string filter_name = chooser->get_filter()->get_name(); - int lparen = filter_name.rfind('(') + 1; - int rdelim = filter_name.find(',', lparen); - if(rdelim < 0) - rdelim = filter_name.find(')', lparen); - ssassert(lparen > 0 && rdelim > 0, "Expected to find a parenthesized extension"); - - std::string extension = filter_name.substr(lparen, rdelim - lparen); - if(extension == "*") - return; - - if(extension.length() > 2 && extension.substr(0, 2) == "*.") - extension = extension.substr(2, extension.length() - 2); - - Platform::Path path = Platform::Path::From(chooser->get_filename()); - chooser->set_current_name(path.WithExtension(extension).FileName()); -} - -bool GetSaveFile(Platform::Path *filename, const std::string &defExtension, - const FileFilter filters[]) { - Gtk::FileChooserDialog chooser(*(Gtk::Window *)SS.GW.window->NativePtr(), - Title(C_("title", "Save File")), - Gtk::FILE_CHOOSER_ACTION_SAVE); - chooser.set_do_overwrite_confirmation(true); - chooser.add_button(C_("button", "_Cancel"), Gtk::RESPONSE_CANCEL); - chooser.add_button(C_("button", "_Save"), Gtk::RESPONSE_OK); - - std::string activeExtension = ConvertFilters(defExtension, filters, &chooser); - - if(filename->IsEmpty()) { - chooser.set_current_folder(Platform::GetSettings()->ThawString("FileChooserPath")); - chooser.set_current_name(std::string(_("untitled")) + "." + activeExtension); - } else { - chooser.set_current_folder(filename->Parent().raw); - chooser.set_current_name(filename->WithExtension(activeExtension).FileName()); - } - - /* Gtk's dialog doesn't change the extension when you change the filter, - and makes it extremely hard to do so. Gtk is garbage. */ - chooser.property_filter().signal_changed(). - connect(sigc::bind(sigc::ptr_fun(&ChooserFilterChanged), &chooser)); - - if(chooser.run() == Gtk::RESPONSE_OK) { - Platform::GetSettings()->FreezeString("FileChooserPath", chooser.get_current_folder()); - *filename = Platform::Path::From(chooser.get_filename()); - return true; - } else { - return false; - } -} - -/* Miscellanea */ void OpenWebsite(const char *url) { gtk_show_uri(Gdk::Screen::get_default()->gobj(), url, GDK_CURRENT_TIME, NULL); diff --git a/src/platform/gui.cpp b/src/platform/gui.cpp index 0d7faca..5b5c65e 100644 --- a/src/platform/gui.cpp +++ b/src/platform/gui.cpp @@ -69,5 +69,60 @@ RgbaColor Settings::ThawColor(const std::string &key, RgbaColor defaultValue) { return RgbaColor::FromPackedInt(ThawInt(key, defaultValue.ToPackedInt())); } +//----------------------------------------------------------------------------- +// File dialogs +//----------------------------------------------------------------------------- + +void FileDialog::AddFilter(const FileFilter &filter) { + AddFilter(Translate("file-type", filter.name.c_str()), filter.extensions); +} + +void FileDialog::AddFilters(const std::vector &filters) { + for(auto filter : filters) AddFilter(filter); +} + +std::vector SolveSpaceModelFileFilters = { + { CN_("file-type", "SolveSpace models"), { "slvs" } }, +}; + +std::vector RasterFileFilters = { + { CN_("file-type", "PNG image"), { "png" } }, +}; + +std::vector MeshFileFilters = { + { CN_("file-type", "STL mesh"), { "stl" } }, + { CN_("file-type", "Wavefront OBJ mesh"), { "obj" } }, + { CN_("file-type", "Three.js-compatible mesh, with viewer"), { "html" } }, + { CN_("file-type", "Three.js-compatible mesh, mesh only"), { "js" } }, +}; + +std::vector SurfaceFileFilters = { + { CN_("file-type", "STEP file"), { "step", "stp" } }, +}; + +std::vector VectorFileFilters = { + { CN_("file-type", "PDF file"), { "pdf" } }, + { CN_("file-type", "Encapsulated PostScript"), { "eps", "ps" } }, + { CN_("file-type", "Scalable Vector Graphics"), { "svg" } }, + { CN_("file-type", "STEP file"), { "step", "stp" } }, + { CN_("file-type", "DXF file (AutoCAD 2007)"), { "dxf" } }, + { CN_("file-type", "HPGL file"), { "plt", "hpgl" } }, + { CN_("file-type", "G Code"), { "ngc", "txt" } }, +}; + +std::vector Vector3dFileFilters = { + { CN_("file-type", "STEP file"), { "step", "stp" } }, + { CN_("file-type", "DXF file (AutoCAD 2007)"), { "dxf" } }, +}; + +std::vector ImportFileFilters = { + { CN_("file-type", "AutoCAD DXF and DWG files"), { "dxf", "dwg" } }, +}; + +std::vector CsvFileFilters = { + { CN_("file-type", "Comma-separated values"), { "csv" } }, +}; + + } } diff --git a/src/platform/gui.h b/src/platform/gui.h index 63ff9b4..0796d3e 100644 --- a/src/platform/gui.h +++ b/src/platform/gui.h @@ -282,6 +282,53 @@ typedef std::shared_ptr MessageDialogRef; MessageDialogRef CreateMessageDialog(WindowRef parentWindow); +// A file filter. +struct FileFilter { + std::string name; + std::vector extensions; +}; + +// SolveSpace's native file format +extern std::vector SolveSpaceModelFileFilters; +// Raster image +extern std::vector RasterFileFilters; +// Triangle mesh +extern std::vector MeshFileFilters; +// NURBS surfaces +extern std::vector SurfaceFileFilters; +// 2d vector (lines and curves) format +extern std::vector VectorFileFilters; +// 3d vector (wireframe lines and curves) format +extern std::vector Vector3dFileFilters; +// Any importable format +extern std::vector ImportFileFilters; +// Comma-separated value, like a spreadsheet would use +extern std::vector CsvFileFilters; + +// A native dialog that asks to choose a file. +class FileDialog { +public: + virtual void SetTitle(std::string title) = 0; + virtual void SetCurrentName(std::string name) = 0; + + virtual Platform::Path GetFilename() = 0; + virtual void SetFilename(Platform::Path path) = 0; + + virtual void AddFilter(std::string name, std::vector extensions) = 0; + void AddFilter(const FileFilter &filter); + void AddFilters(const std::vector &filters); + + virtual void FreezeChoices(SettingsRef settings, const std::string &key) = 0; + virtual void ThawChoices(SettingsRef settings, const std::string &key) = 0; + + virtual bool RunModal() = 0; +}; + +typedef std::shared_ptr FileDialogRef; + +FileDialogRef CreateOpenFileDialog(WindowRef parentWindow); +FileDialogRef CreateSaveFileDialog(WindowRef parentWindow); + //----------------------------------------------------------------------------- // Application-wide APIs //----------------------------------------------------------------------------- diff --git a/src/platform/guigtk.cpp b/src/platform/guigtk.cpp index 0258702..6bfbd32 100644 --- a/src/platform/guigtk.cpp +++ b/src/platform/guigtk.cpp @@ -13,15 +13,16 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include -#include namespace SolveSpace { namespace Platform { @@ -1078,6 +1079,138 @@ MessageDialogRef CreateMessageDialog(WindowRef parentWindow) { std::static_pointer_cast(parentWindow)->gtkWindow); } +//----------------------------------------------------------------------------- +// File dialogs +//----------------------------------------------------------------------------- + +class FileDialogImplGtk : public FileDialog { +public: + Gtk::FileChooserDialog gtkDialog; + std::vector extensions; + + FileDialogImplGtk(Gtk::FileChooserDialog &&dialog) + : gtkDialog(std::move(dialog)) + { + gtkDialog.property_filter().signal_changed(). + connect(sigc::mem_fun(this, &FileDialogImplGtk::FilterChanged)); + } + + void SetTitle(std::string title) override { + gtkDialog.set_title(PrepareTitle(title)); + } + + void SetCurrentName(std::string name) override { + gtkDialog.set_current_name(name); + } + + Platform::Path GetFilename() override { + return Path::From(gtkDialog.get_filename()); + } + + void SetFilename(Platform::Path path) override { + gtkDialog.set_filename(path.raw); + } + + void AddFilter(std::string name, std::vector extensions) override { + Glib::RefPtr gtkFilter = Gtk::FileFilter::create(); + Glib::ustring desc; + for(auto extension : extensions) { + Glib::ustring pattern = "*"; + if(!extension.empty()) { + pattern = "*." + extension; + gtkFilter->add_pattern(pattern); + gtkFilter->add_pattern(Glib::ustring(pattern).uppercase()); + } + if(!desc.empty()) { + desc += ", "; + } + desc += pattern; + } + gtkFilter->set_name(name + " (" + desc + ")"); + + this->extensions.push_back(extensions.front()); + gtkDialog.add_filter(gtkFilter); + } + + std::string GetExtension() { + auto filters = gtkDialog.list_filters(); + size_t filterIndex = + std::find(filters.begin(), filters.end(), gtkDialog.get_filter()) - + filters.begin(); + if(filterIndex < extensions.size()) { + return extensions[filterIndex]; + } else { + return extensions.front(); + } + } + + void SetExtension(std::string extension) { + auto filters = gtkDialog.list_filters(); + size_t extensionIndex = + std::find(extensions.begin(), extensions.end(), extension) - + extensions.begin(); + if(extensionIndex < filters.size()) { + gtkDialog.set_filter(filters[extensionIndex]); + } else { + gtkDialog.set_filter(filters.front()); + } + } + + void FilterChanged() { + std::string extension = GetExtension(); + if(extension == "") return; + + Platform::Path path = GetFilename(); + SetCurrentName(path.WithExtension(extension).FileName()); + } + + void FreezeChoices(SettingsRef settings, const std::string &key) override { + settings->FreezeString("Dialog_" + key + "_Folder", + gtkDialog.get_current_folder()); + settings->FreezeString("Dialog_" + key + "_Filter", GetExtension()); + } + + void ThawChoices(SettingsRef settings, const std::string &key) override { + gtkDialog.set_current_folder(settings->ThawString("Dialog_" + key + "_Folder")); + SetExtension(settings->ThawString("Dialog_" + key + "_Filter")); + } + + bool RunModal() override { + if(gtkDialog.get_action() == Gtk::FILE_CHOOSER_ACTION_SAVE && + Path::From(gtkDialog.get_current_name()).FileStem().empty()) { + gtkDialog.set_current_name(std::string(_("untitled")) + "." + GetExtension()); + } + + if(gtkDialog.run() == Gtk::RESPONSE_OK) { + return true; + } else { + return false; + } + } +}; + +FileDialogRef CreateOpenFileDialog(WindowRef parentWindow) { + Gtk::Window >kParent = std::static_pointer_cast(parentWindow)->gtkWindow; + Gtk::FileChooserDialog gtkDialog(gtkParent, C_("title", "Open File"), + Gtk::FILE_CHOOSER_ACTION_OPEN); + gtkDialog.add_button(C_("button", "_Cancel"), Gtk::RESPONSE_CANCEL); + gtkDialog.add_button(C_("button", "_Open"), Gtk::RESPONSE_OK); + gtkDialog.set_default_response(Gtk::RESPONSE_OK); + return std::make_shared(std::move(gtkDialog)); + +} + +FileDialogRef CreateSaveFileDialog(WindowRef parentWindow) { + Gtk::Window >kParent = std::static_pointer_cast(parentWindow)->gtkWindow; + Gtk::FileChooserDialog gtkDialog(gtkParent, C_("title", "Save File"), + Gtk::FILE_CHOOSER_ACTION_SAVE); + gtkDialog.set_do_overwrite_confirmation(true); + gtkDialog.add_button(C_("button", "_Cancel"), Gtk::RESPONSE_CANCEL); + gtkDialog.add_button(C_("button", "_Save"), Gtk::RESPONSE_OK); + gtkDialog.set_default_response(Gtk::RESPONSE_OK); + return std::make_shared(std::move(gtkDialog)); +} + //----------------------------------------------------------------------------- // Application-wide APIs //----------------------------------------------------------------------------- diff --git a/src/platform/guimac.mm b/src/platform/guimac.mm index b6c0dff..6b5f34c 100644 --- a/src/platform/guimac.mm +++ b/src/platform/guimac.mm @@ -1063,6 +1063,180 @@ MessageDialogRef CreateMessageDialog(WindowRef parentWindow) { return dialog; } +//----------------------------------------------------------------------------- +// File dialogs +//----------------------------------------------------------------------------- + +} +} + +@interface SSSaveFormatAccessory : NSViewController +@property NSSavePanel *panel; +@property NSMutableArray *filters; + +@property(nonatomic) NSInteger index; +@property(nonatomic) IBOutlet NSTextField *textField; +@property(nonatomic) IBOutlet NSPopUpButton *button; +@end + +@implementation SSSaveFormatAccessory +@synthesize panel, filters, button; + +- (void)setIndex:(NSInteger)newIndex { + self->_index = newIndex; + NSMutableArray *filter = [filters objectAtIndex:newIndex]; + NSString *extension = [filter objectAtIndex:0]; + if(![extension isEqual:@"*"]) { + NSString *filename = panel.nameFieldStringValue; + NSString *basename = [[filename componentsSeparatedByString:@"."] objectAtIndex:0]; + panel.nameFieldStringValue = [basename stringByAppendingPathExtension:extension]; + } + [panel setAllowedFileTypes:filter]; +} +@end + +namespace SolveSpace { +namespace Platform { + +class FileDialogImplCocoa : public FileDialog { +public: + NSSavePanel *nsPanel = nil; + + void SetTitle(std::string title) override { + nsPanel.title = Wrap(title); + } + + void SetCurrentName(std::string name) override { + nsPanel.nameFieldStringValue = Wrap(name); + } + + Platform::Path GetFilename() override { + return Platform::Path::From(nsPanel.URL.fileSystemRepresentation); + } + + void SetFilename(Platform::Path path) override { + nsPanel.directoryURL = + [NSURL fileURLWithPath:Wrap(path.Parent().raw) isDirectory:YES]; + nsPanel.nameFieldStringValue = Wrap(path.FileStem()); + } + + void FreezeChoices(SettingsRef settings, const std::string &key) override { + settings->FreezeString("Dialog_" + key + "_Folder", + [nsPanel.directoryURL.absoluteString UTF8String]); + } + + void ThawChoices(SettingsRef settings, const std::string &key) override { + nsPanel.directoryURL = + [NSURL URLWithString:Wrap(settings->ThawString("Dialog_" + key + "_Folder", ""))]; + } + + bool RunModal() override { + if([nsPanel runModal] == NSFileHandlingPanelOKButton) { + return true; + } else { + return false; + } + } +}; + +class OpenFileDialogImplCocoa : public FileDialogImplCocoa { +public: + NSMutableArray *nsFilter = [[NSMutableArray alloc] init]; + + OpenFileDialogImplCocoa() { + SetTitle(C_("title", "Open File")); + } + + void AddFilter(std::string name, std::vector extensions) override { + for(auto extension : extensions) { + [nsFilter addObject:Wrap(extension)]; + } + [nsPanel setAllowedFileTypes:nsFilter]; + } +}; + +class SaveFileDialogImplCocoa : public FileDialogImplCocoa { +public: + NSMutableArray *nsFilters = [[NSMutableArray alloc] init]; + SSSaveFormatAccessory *ssAccessory = nil; + + SaveFileDialogImplCocoa() { + SetTitle(C_("title", "Save File")); + } + + void AddFilter(std::string name, std::vector extensions) override { + NSMutableArray *nsFilter = [[NSMutableArray alloc] init]; + for(auto extension : extensions) { + [nsFilter addObject:Wrap(extension)]; + } + if(nsFilters.count == 0) { + [nsPanel setAllowedFileTypes:nsFilter]; + } + [nsFilters addObject:nsFilter]; + + std::string desc; + for(auto extension : extensions) { + if(!desc.empty()) desc += ", "; + desc += extension; + } + std::string title = name + " (" + desc + ")"; + if(nsFilters.count == 1) { + [ssAccessory.button removeAllItems]; + } + [ssAccessory.button addItemWithTitle:Wrap(title)]; + [ssAccessory.button synchronizeTitleAndSelectedItem]; + } + + void FreezeChoices(SettingsRef settings, const std::string &key) override { + FileDialogImplCocoa::FreezeChoices(settings, key); + settings->FreezeInt("Dialog_" + key + "_Filter", ssAccessory.index); + } + + void ThawChoices(SettingsRef settings, const std::string &key) override { + FileDialogImplCocoa::ThawChoices(settings, key); + ssAccessory.index = settings->ThawInt("Dialog_" + key + "_Filter", 0); + } + + bool RunModal() override { + if(nsFilters.count == 1) { + nsPanel.accessoryView = nil; + } + + if(nsPanel.nameFieldStringValue.length == 0) { + nsPanel.nameFieldStringValue = Wrap(_("untitled")); + } + + return FileDialogImplCocoa::RunModal(); + } +}; + +FileDialogRef CreateOpenFileDialog(WindowRef parentWindow) { + NSOpenPanel *nsPanel = [NSOpenPanel openPanel]; + nsPanel.canSelectHiddenExtension = YES; + + std::shared_ptr dialog = std::make_shared(); + dialog->nsPanel = nsPanel; + + return dialog; +} + +FileDialogRef CreateSaveFileDialog(WindowRef parentWindow) { + NSSavePanel *nsPanel = [NSSavePanel savePanel]; + nsPanel.canSelectHiddenExtension = YES; + + SSSaveFormatAccessory *ssAccessory = + [[SSSaveFormatAccessory alloc] initWithNibName:@"SaveFormatAccessory" bundle:nil]; + ssAccessory.panel = nsPanel; + nsPanel.accessoryView = [ssAccessory view]; + + std::shared_ptr dialog = std::make_shared(); + dialog->nsPanel = nsPanel; + dialog->ssAccessory = ssAccessory; + ssAccessory.filters = dialog->nsFilters; + + return dialog; +} + //----------------------------------------------------------------------------- // Application-wide APIs //----------------------------------------------------------------------------- diff --git a/src/platform/guinone.cpp b/src/platform/guinone.cpp index b4ca4da..b6f26d5 100644 --- a/src/platform/guinone.cpp +++ b/src/platform/guinone.cpp @@ -114,13 +114,25 @@ WindowRef CreateWindow(Window::Kind kind, WindowRef parentWindow) { } //----------------------------------------------------------------------------- -// Dialogs +// Message dialogs //----------------------------------------------------------------------------- MessageDialogRef CreateMessageDialog(WindowRef parentWindow) { return std::shared_ptr(); } +//----------------------------------------------------------------------------- +// File dialogs +//----------------------------------------------------------------------------- + +FileDialogRef CreateOpenFileDialog(WindowRef parentWindow) { + return std::shared_ptr(); +} + +FileDialogRef CreateSaveFileDialog(WindowRef parentWindow) { + return std::shared_ptr(); +} + //----------------------------------------------------------------------------- // Application-wide APIs //----------------------------------------------------------------------------- @@ -135,14 +147,6 @@ void Exit() { // Dialogs //----------------------------------------------------------------------------- -bool GetOpenFile(Platform::Path *filename, const std::string &activeOrEmpty, - const FileFilter filters[]) { - ssassert(false, "Not implemented"); -} -bool GetSaveFile(Platform::Path *filename, const std::string &activeOrEmpty, - const FileFilter filters[]) { - 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 c7760e5..25b6082 100644 --- a/src/platform/guiwin.cpp +++ b/src/platform/guiwin.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #ifndef WM_DPICHANGED @@ -1272,7 +1273,7 @@ WindowRef CreateWindow(Window::Kind kind, WindowRef parentWindow) { } //----------------------------------------------------------------------------- -// Dialogs +// Message dialogs //----------------------------------------------------------------------------- class MessageDialogImplWin32 : public MessageDialog { @@ -1381,6 +1382,109 @@ MessageDialogRef CreateMessageDialog(WindowRef parentWindow) { return dialog; } +//----------------------------------------------------------------------------- +// File dialogs +//----------------------------------------------------------------------------- + +class FileDialogImplWin32 : public FileDialog { +public: + OPENFILENAMEW ofn = {}; + bool isSaveDialog; + std::wstring titleW; + std::wstring filtersW; + std::wstring defExtW; + std::wstring initialDirW; + // UNC paths may be as long as 32767 characters. + // Unfortunately, the Get*FileName API does not provide any way to use it + // except with a preallocated buffer of fixed size, so we use something + // reasonably large. + wchar_t filenameWC[32768] = {}; + + FileDialogImplWin32() { + ofn.lStructSize = sizeof(ofn); + ofn.lpstrFile = filenameWC; + ofn.nMaxFile = sizeof(filenameWC) / sizeof(wchar_t); + ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_HIDEREADONLY | + OFN_OVERWRITEPROMPT; + if(isSaveDialog) { + SetTitle(C_("title", "Save File")); + } else { + SetTitle(C_("title", "Open File")); + } + } + + void SetTitle(std::string title) override { + titleW = PrepareTitle(title); + ofn.lpstrTitle = titleW.c_str(); + } + + void SetCurrentName(std::string name) override { + SetFilename(GetFilename().Parent().Join(name)); + } + + Platform::Path GetFilename() override { + return Path::From(Narrow(filenameWC)); + } + + void SetFilename(Platform::Path path) override { + wcsncpy(filenameWC, Widen(path.raw).c_str(), sizeof(filenameWC) / sizeof(wchar_t) - 1); + } + + void AddFilter(std::string name, std::vector extensions) override { + std::string desc, patterns; + for(auto extension : extensions) { + std::string pattern = "*." + extension; + if(!desc.empty()) desc += ", "; + desc += pattern; + if(!patterns.empty()) patterns += ";"; + patterns += pattern; + } + filtersW += Widen(name + " (" + desc + ")" + '\0' + patterns + '\0'); + ofn.lpstrFilter = filtersW.c_str(); + if(ofn.lpstrDefExt == NULL) { + defExtW = Widen(extensions.front()); + ofn.lpstrDefExt = defExtW.c_str(); + } + } + + void FreezeChoices(SettingsRef settings, const std::string &key) override { + settings->FreezeString("Dialog_" + key + "_Folder", GetFilename().Parent().raw); + settings->FreezeInt("Dialog_" + key + "_Filter", ofn.nFilterIndex); + } + + void ThawChoices(SettingsRef settings, const std::string &key) override { + initialDirW = Widen(settings->ThawString("Dialog_" + key + "_Folder", "")); + ofn.lpstrInitialDir = initialDirW.c_str(); + ofn.nFilterIndex = settings->ThawInt("Dialog_" + key + "_Filter", 0); + } + + bool RunModal() override { + if(GetFilename().IsEmpty()) { + SetFilename(Path::From(_("untitled"))); + } + + if(isSaveDialog) { + return GetSaveFileNameW(&ofn); + } else { + return GetOpenFileNameW(&ofn); + } + } +}; + +FileDialogRef CreateOpenFileDialog(WindowRef parentWindow) { + std::shared_ptr dialog = std::make_shared(); + dialog->ofn.hwndOwner = std::static_pointer_cast(parentWindow)->hWindow; + dialog->isSaveDialog = false; + return dialog; +} + +FileDialogRef CreateSaveFileDialog(WindowRef parentWindow) { + std::shared_ptr dialog = std::make_shared(); + dialog->ofn.hwndOwner = std::static_pointer_cast(parentWindow)->hWindow; + dialog->isSaveDialog = true; + return dialog; +} + //----------------------------------------------------------------------------- // Application-wide APIs //----------------------------------------------------------------------------- diff --git a/src/platform/w32main.cpp b/src/platform/w32main.cpp index 2e4c6b7..b063254 100644 --- a/src/platform/w32main.cpp +++ b/src/platform/w32main.cpp @@ -33,104 +33,11 @@ SiHdl SpaceNavigator = SI_NO_HANDLE; //----------------------------------------------------------------------------- // Utility routines //----------------------------------------------------------------------------- -std::wstring Title(const std::string &s) { - return Widen("SolveSpace - " + s); -} - void SolveSpace::OpenWebsite(const char *url) { ShellExecuteW((HWND)SS.GW.window->NativePtr(), L"open", Widen(url).c_str(), NULL, NULL, SW_SHOWNORMAL); } -//----------------------------------------------------------------------------- -// Common dialog routines, to open or save a file. -//----------------------------------------------------------------------------- -static std::string ConvertFilters(const FileFilter ssFilters[]) { - std::string filter; - for(const FileFilter *ssFilter = ssFilters; ssFilter->name; ssFilter++) { - std::string desc, patterns; - for(const char *const *ssPattern = ssFilter->patterns; *ssPattern; ssPattern++) { - std::string pattern = "*." + std::string(*ssPattern); - if(desc == "") - desc = pattern; - else - desc += ", " + pattern; - if(patterns == "") - patterns = pattern; - else - patterns += ";" + pattern; - } - filter += std::string(ssFilter->name) + " (" + desc + ")" + '\0'; - filter += patterns + '\0'; - } - filter += '\0'; - return filter; -} - -static bool OpenSaveFile(bool isOpen, Platform::Path *filename, const std::string &defExtension, - const FileFilter filters[]) { - std::string activeExtension = defExtension; - if(activeExtension == "") { - activeExtension = filters[0].patterns[0]; - } - - std::wstring initialFilenameW; - if(filename->IsEmpty()) { - initialFilenameW = Widen("untitled"); - } else { - initialFilenameW = Widen(filename->Parent().Join(filename->FileStem()).raw); - } - std::wstring selPatternW = Widen(ConvertFilters(filters)); - std::wstring defExtensionW = Widen(defExtension); - - // UNC paths may be as long as 32767 characters. - // Unfortunately, the Get*FileName API does not provide any way to use it - // except with a preallocated buffer of fixed size, so we use something - // reasonably large. - const int len = 32768; - wchar_t filenameC[len] = {}; - wcsncpy(filenameC, initialFilenameW.c_str(), len - 1); - - OPENFILENAME ofn = {}; - ofn.lStructSize = sizeof(ofn); - ofn.hInstance = NULL; - ofn.hwndOwner = (HWND)SS.GW.window->NativePtr(); - ofn.lpstrFilter = selPatternW.c_str(); - ofn.lpstrDefExt = defExtensionW.c_str(); - ofn.lpstrFile = filenameC; - ofn.nMaxFile = len; - ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_HIDEREADONLY; - - EnableWindow((HWND)SS.GW.window->NativePtr(), FALSE); - EnableWindow((HWND)SS.TW.window->NativePtr(), FALSE); - - BOOL r; - if(isOpen) { - r = GetOpenFileNameW(&ofn); - } else { - r = GetSaveFileNameW(&ofn); - } - - EnableWindow((HWND)SS.GW.window->NativePtr(), TRUE); - EnableWindow((HWND)SS.TW.window->NativePtr(), TRUE); - SetForegroundWindow((HWND)SS.GW.window->NativePtr()); - - if(r) *filename = Platform::Path::From(Narrow(filenameC)); - return r ? true : false; -} - -bool SolveSpace::GetOpenFile(Platform::Path *filename, const std::string &defExtension, - const FileFilter filters[]) -{ - return OpenSaveFile(/*isOpen=*/true, filename, defExtension, filters); -} - -bool SolveSpace::GetSaveFile(Platform::Path *filename, const std::string &defExtension, - const FileFilter filters[]) -{ - return OpenSaveFile(/*isOpen=*/false, filename, defExtension, filters); -} - std::vector SolveSpace::GetFontFiles() { std::vector fonts; diff --git a/src/solvespace.cpp b/src/solvespace.cpp index ae381f7..cc26d92 100644 --- a/src/solvespace.cpp +++ b/src/solvespace.cpp @@ -395,10 +395,22 @@ void SolveSpaceUI::AddToRecentList(const Platform::Path &filename) { } bool SolveSpaceUI::GetFilenameAndSave(bool saveAs) { + Platform::SettingsRef settings = Platform::GetSettings(); Platform::Path newSaveFile = saveFile; if(saveAs || saveFile.IsEmpty()) { - if(!GetSaveFile(&newSaveFile, "", SlvsFileFilter)) return false; + Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(GW.window); + dialog->AddFilter(C_("file-type", "SolveSpace models"), { "slvs" }); + dialog->ThawChoices(settings, "Sketch"); + if(!newSaveFile.IsEmpty()) { + dialog->SetFilename(newSaveFile); + } + if(dialog->RunModal()) { + dialog->FreezeChoices(settings, "Sketch"); + newSaveFile = dialog->GetFilename(); + } else { + return false; + } } if(SaveToFile(newSaveFile)) { @@ -490,9 +502,12 @@ void SolveSpaceUI::MenuFile(Command id) { case Command::OPEN: { if(!SS.OkayToStartNewFile()) break; - Platform::Path newFile; - if(GetOpenFile(&newFile, "", SlvsFileFilter)) { - SS.Load(newFile); + Platform::FileDialogRef dialog = Platform::CreateOpenFileDialog(SS.GW.window); + dialog->AddFilters(Platform::SolveSpaceModelFileFilters); + dialog->ThawChoices(settings, "Sketch"); + if(dialog->RunModal()) { + dialog->FreezeChoices(settings, "Sketch"); + SS.Load(dialog->GetFilename()); } break; } @@ -505,24 +520,28 @@ void SolveSpaceUI::MenuFile(Command id) { SS.GetFilenameAndSave(/*saveAs=*/true); break; - case Command::EXPORT_PNG: { - Platform::Path exportFile = SS.saveFile; - if(!GetSaveFile(&exportFile, "", RasterFileFilter)) break; - SS.ExportAsPngTo(exportFile); + case Command::EXPORT_IMAGE: { + Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(SS.GW.window); + dialog->AddFilters(Platform::RasterFileFilters); + dialog->ThawChoices(settings, "ExportImage"); + if(dialog->RunModal()) { + dialog->FreezeChoices(settings, "ExportImage"); + SS.ExportAsPngTo(dialog->GetFilename()); + } break; } case Command::EXPORT_VIEW: { - Platform::Path exportFile = SS.saveFile; - if(!GetSaveFile(&exportFile, - Platform::GetSettings()->ThawString("ViewExportFormat"), - VectorFileFilter)) break; - settings->FreezeString("ViewExportFormat", exportFile.Extension()); + Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(SS.GW.window); + dialog->AddFilters(Platform::VectorFileFilters); + dialog->ThawChoices(settings, "ExportView"); + if(!dialog->RunModal()) break; + dialog->FreezeChoices(settings, "ExportView"); // If the user is exporting something where it would be // inappropriate to include the constraints, then warn. if(SS.GW.showConstraints && - (exportFile.HasExtension("txt") || + (dialog->GetFilename().HasExtension("txt") || fabs(SS.exportOffset) > LENGTH_EPS)) { Message(_("Constraints are currently shown, and will be exported " @@ -531,62 +550,63 @@ void SolveSpaceUI::MenuFile(Command id) { "text window.")); } - SS.ExportViewOrWireframeTo(exportFile, /*exportWireframe*/false); + SS.ExportViewOrWireframeTo(dialog->GetFilename(), /*exportWireframe=*/false); break; } case Command::EXPORT_WIREFRAME: { - Platform::Path exportFile = SS.saveFile; - if(!GetSaveFile(&exportFile, - Platform::GetSettings()->ThawString("WireframeExportFormat"), - Vector3dFileFilter)) break; - settings->FreezeString("WireframeExportFormat", exportFile.Extension()); + Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(SS.GW.window); + dialog->AddFilters(Platform::Vector3dFileFilters); + dialog->ThawChoices(settings, "ExportWireframe"); + if(!dialog->RunModal()) break; + dialog->FreezeChoices(settings, "ExportWireframe"); - SS.ExportViewOrWireframeTo(exportFile, /*exportWireframe*/true); + SS.ExportViewOrWireframeTo(dialog->GetFilename(), /*exportWireframe*/true); break; } case Command::EXPORT_SECTION: { - Platform::Path exportFile = SS.saveFile; - if(!GetSaveFile(&exportFile, - Platform::GetSettings()->ThawString("SectionExportFormat"), - VectorFileFilter)) break; - settings->FreezeString("SectionExportFormat", exportFile.Extension()); + Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(SS.GW.window); + dialog->AddFilters(Platform::VectorFileFilters); + dialog->ThawChoices(settings, "ExportSection"); + if(!dialog->RunModal()) break; + dialog->FreezeChoices(settings, "ExportSection"); - SS.ExportSectionTo(exportFile); + SS.ExportSectionTo(dialog->GetFilename()); break; } case Command::EXPORT_MESH: { - Platform::Path exportFile = SS.saveFile; - if(!GetSaveFile(&exportFile, - Platform::GetSettings()->ThawString("MeshExportFormat"), - MeshFileFilter)) break; - settings->FreezeString("MeshExportFormat", exportFile.Extension()); + Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(SS.GW.window); + dialog->AddFilters(Platform::MeshFileFilters); + dialog->ThawChoices(settings, "ExportMesh"); + if(!dialog->RunModal()) break; + dialog->FreezeChoices(settings, "ExportMesh"); - SS.ExportMeshTo(exportFile); + SS.ExportMeshTo(dialog->GetFilename()); break; } case Command::EXPORT_SURFACES: { - Platform::Path exportFile = SS.saveFile; - if(!GetSaveFile(&exportFile, - Platform::GetSettings()->ThawString("SurfacesExportFormat"), - SurfaceFileFilter)) break; - settings->FreezeString("SurfacesExportFormat", exportFile.Extension()); + Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(SS.GW.window); + dialog->AddFilters(Platform::SurfaceFileFilters); + dialog->ThawChoices(settings, "ExportSurfaces"); + if(!dialog->RunModal()) break; + dialog->FreezeChoices(settings, "ExportSurfaces"); StepFileWriter sfw = {}; - sfw.ExportSurfacesTo(exportFile); + sfw.ExportSurfacesTo(dialog->GetFilename()); break; } case Command::IMPORT: { - Platform::Path importFile; - if(!GetOpenFile(&importFile, - Platform::GetSettings()->ThawString("ImportFormat"), - ImportableFileFilter)) break; - settings->FreezeString("ImportFormat", importFile.Extension()); + Platform::FileDialogRef dialog = Platform::CreateOpenFileDialog(SS.GW.window); + dialog->AddFilters(Platform::ImportFileFilters); + dialog->ThawChoices(settings, "Import"); + if(!dialog->RunModal()) break; + dialog->FreezeChoices(settings, "Import"); + Platform::Path importFile = dialog->GetFilename(); if(importFile.HasExtension("dxf")) { ImportDxf(importFile); } else if(importFile.HasExtension("dwg")) { @@ -613,6 +633,8 @@ void SolveSpaceUI::MenuFile(Command id) { } void SolveSpaceUI::MenuAnalyze(Command id) { + Platform::SettingsRef settings = Platform::GetSettings(); + SS.GW.GroupSelection(); auto const &gs = SS.GW.gs; @@ -813,9 +835,13 @@ void SolveSpaceUI::MenuAnalyze(Command id) { break; case Command::STOP_TRACING: { - Platform::Path exportFile = SS.saveFile; - if(GetSaveFile(&exportFile, "", CsvFileFilter)) { - FILE *f = OpenFile(exportFile, "wb"); + Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(SS.GW.window); + dialog->AddFilters(Platform::CsvFileFilters); + dialog->ThawChoices(settings, "Trace"); + if(dialog->RunModal()) { + dialog->FreezeChoices(settings, "Trace"); + + FILE *f = OpenFile(dialog->GetFilename(), "wb"); if(f) { int i; SContour *sc = &(SS.traced.path); @@ -827,7 +853,7 @@ void SolveSpaceUI::MenuAnalyze(Command id) { } fclose(f); } else { - Error("Couldn't write to '%s'", exportFile.raw.c_str()); + Error("Couldn't write to '%s'", dialog->GetFilename().raw.c_str()); } } // Clear the trace, and stop tracing diff --git a/src/solvespace.h b/src/solvespace.h index b722b2e..72e1045 100644 --- a/src/solvespace.h +++ b/src/solvespace.h @@ -142,18 +142,6 @@ extern Platform::Path RecentFile[MAX_RECENT]; #define AUTOSAVE_EXT "slvs~" -enum class Unit : uint32_t { - MM = 0, - INCHES, - METERS -}; - -struct FileFilter; - -bool GetSaveFile(Platform::Path *filename, const std::string &defExtension, - const FileFilter filters[]); -bool GetOpenFile(Platform::Path *filename, const std::string &defExtension, - const FileFilter filters[]); std::vector GetFontFiles(); void OpenWebsite(const char *url); @@ -172,11 +160,17 @@ void *MemAlloc(size_t n); void MemFree(void *p); void vl(); // debug function to validate heaps -#include "resource.h" - // End of platform-specific functions //================ +#include "resource.h" + +enum class Unit : uint32_t { + MM = 0, + INCHES, + METERS +}; + template struct CompareHandle { bool operator()(T lhs, T rhs) const { return lhs.v < rhs.v; } diff --git a/src/ui.h b/src/ui.h index f5c002c..35218c0 100644 --- a/src/ui.h +++ b/src/ui.h @@ -58,63 +58,6 @@ inline const char *C_(const char *msgctxt, const char *msgid) { } #endif -// Filters for the file formats that we support. -struct FileFilter { - const char *name; - const char *patterns[3]; -}; - -// SolveSpace native file format -const FileFilter SlvsFileFilter[] = { - { N_("SolveSpace models"), { "slvs" } }, - { NULL, {} } -}; -// PNG format bitmap -const FileFilter RasterFileFilter[] = { - { N_("PNG file"), { "png" } }, - { NULL, {} } -}; -// Triangle mesh -const FileFilter MeshFileFilter[] = { - { N_("STL mesh"), { "stl" } }, - { N_("Wavefront OBJ mesh"), { "obj" } }, - { N_("Three.js-compatible mesh, with viewer"), { "html" } }, - { N_("Three.js-compatible mesh, mesh only"), { "js" } }, - { NULL, {} } -}; -// NURBS surfaces -const FileFilter SurfaceFileFilter[] = { - { N_("STEP file"), { "step", "stp" } }, - { NULL, {} } -}; -// 2d vector (lines and curves) format -const FileFilter VectorFileFilter[] = { - { N_("PDF file"), { "pdf" } }, - { N_("Encapsulated PostScript"), { "eps", "ps" } }, - { N_("Scalable Vector Graphics"), { "svg" } }, - { N_("STEP file"), { "step", "stp" } }, - { N_("DXF file (AutoCAD 2007)"), { "dxf" } }, - { N_("HPGL file"), { "plt", "hpgl" } }, - { N_("G Code"), { "ngc", "txt" } }, - { NULL, {} } -}; -// 3d vector (wireframe lines and curves) format -const FileFilter Vector3dFileFilter[] = { - { N_("STEP file"), { "step", "stp" } }, - { N_("DXF file (AutoCAD 2007)"), { "dxf" } }, - { NULL, {} } -}; -// All Importable formats -const FileFilter ImportableFileFilter[] = { - { N_("AutoCAD DXF and DWG files"), { "dxf", "dwg" } }, - { NULL, {} } -}; -// Comma-separated value, like a spreadsheet would use -const FileFilter CsvFileFilter[] = { - { N_("Comma-separated values"), { "csv" } }, - { NULL, {} } -}; - // This table describes the top-level menus in the graphics winodw. enum class Command : uint32_t { NONE = 0, @@ -124,7 +67,7 @@ enum class Command : uint32_t { OPEN_RECENT, SAVE, SAVE_AS, - EXPORT_PNG, + EXPORT_IMAGE, EXPORT_MESH, EXPORT_SURFACES, EXPORT_VIEW,