Add a platform abstraction for file dialogs.

This commit merges all ad-hoc file dialog code, such as the feature
where dialogs remember last location and format, and exposes it
through a common interface.

This commit also significantly improves Gtk dialog handling code.
This commit is contained in:
whitequark 2018-07-17 18:51:00 +00:00
parent d7968978ad
commit 6b5db58971
17 changed files with 651 additions and 476 deletions

View File

@ -7,6 +7,7 @@
<objects> <objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSViewController"> <customObject id="-2" userLabel="File's Owner" customClass="NSViewController">
<connections> <connections>
<outlet property="textField" destination="z9Z-cA-QIW" id="w4z-a4-Khs"/>
<outlet property="button" destination="nNy-fR-AhK" id="w3z-a4-Khs"/> <outlet property="button" destination="nNy-fR-AhK" id="w3z-a4-Khs"/>
<outlet property="view" destination="c22-O7-iKe" id="w2z-a4-Khs"/> <outlet property="view" destination="c22-O7-iKe" id="w2z-a4-Khs"/>
</connections> </connections>

View File

@ -848,6 +848,8 @@ static Platform::MessageDialog::Response LocateImportedFile(const Platform::Path
} }
bool SolveSpaceUI::ReloadAllLinked(const Platform::Path &saveFile, bool canCancel) { bool SolveSpaceUI::ReloadAllLinked(const Platform::Path &saveFile, bool canCancel) {
Platform::SettingsRef settings = Platform::GetSettings();
std::map<Platform::Path, Platform::Path, Platform::PathLess> linkMap; std::map<Platform::Path, Platform::Path, Platform::PathLess> linkMap;
allConsistent = false; allConsistent = false;
@ -877,10 +879,13 @@ try_again:
// The file was moved; prompt the user for its new location. // The file was moved; prompt the user for its new location.
switch(LocateImportedFile(g.linkFile.RelativeTo(saveFile), canCancel)) { switch(LocateImportedFile(g.linkFile.RelativeTo(saveFile), canCancel)) {
case Platform::MessageDialog::Response::YES: { case Platform::MessageDialog::Response::YES: {
Platform::Path newLinkFile; Platform::FileDialogRef dialog = Platform::CreateOpenFileDialog(SS.GW.window);
if(GetOpenFile(&newLinkFile, "", SlvsFileFilter)) { dialog->AddFilters(Platform::SolveSpaceModelFileFilters);
linkMap[g.linkFile] = newLinkFile; dialog->ThawChoices(settings, "LinkSketch");
g.linkFile = newLinkFile; if(dialog->RunModal()) {
dialog->FreezeChoices(settings, "LinkSketch");
linkMap[g.linkFile] = dialog->GetFilename();
g.linkFile = dialog->GetFilename();
goto try_again; goto try_again;
} else { } else {
if(canCancel) return false; if(canCancel) return false;
@ -917,6 +922,8 @@ try_again:
bool SolveSpaceUI::ReloadLinkedImage(const Platform::Path &saveFile, bool SolveSpaceUI::ReloadLinkedImage(const Platform::Path &saveFile,
Platform::Path *filename, bool canCancel) { Platform::Path *filename, bool canCancel) {
Platform::SettingsRef settings = Platform::GetSettings();
std::shared_ptr<Pixmap> pixmap; std::shared_ptr<Pixmap> pixmap;
bool promptOpenFile = false; bool promptOpenFile = false;
if(filename->IsEmpty()) { if(filename->IsEmpty()) {
@ -948,7 +955,12 @@ bool SolveSpaceUI::ReloadLinkedImage(const Platform::Path &saveFile,
} }
if(promptOpenFile) { 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); pixmap = Pixmap::ReadPng(*filename);
if(pixmap == NULL) { if(pixmap == NULL) {
Error("The image '%s' is corrupted.", filename->raw.c_str()); Error("The image '%s' is corrupted.", filename->raw.c_str());

View File

@ -45,7 +45,7 @@ const MenuEntry Menu[] = {
{ 1, N_("&Save"), Command::SAVE, C|'s', KN, mFile }, { 1, N_("&Save"), Command::SAVE, C|'s', KN, mFile },
{ 1, N_("Save &As..."), Command::SAVE_AS, 0, KN, mFile }, { 1, N_("Save &As..."), Command::SAVE_AS, 0, KN, mFile },
{ 1, NULL, Command::NONE, 0, KN, NULL }, { 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 &View..."), Command::EXPORT_VIEW, 0, KN, mFile },
{ 1, N_("Export 2d &Section..."), Command::EXPORT_SECTION, 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 }, { 1, N_("Export 3d &Wireframe..."), Command::EXPORT_WIREFRAME, 0, KN, mFile },

View File

@ -74,6 +74,8 @@ void Group::MenuGroup(Command id) {
} }
void Group::MenuGroup(Command id, Platform::Path linkFile) { void Group::MenuGroup(Command id, Platform::Path linkFile) {
Platform::SettingsRef settings = Platform::GetSettings();
Group g = {}; Group g = {};
g.visible = true; g.visible = true;
g.color = RGBi(100, 100, 100); g.color = RGBi(100, 100, 100);
@ -229,7 +231,12 @@ void Group::MenuGroup(Command id, Platform::Path linkFile) {
g.type = Type::LINKED; g.type = Type::LINKED;
g.meshCombine = CombineAs::ASSEMBLE; g.meshCombine = CombineAs::ASSEMBLE;
if(g.linkFile.IsEmpty()) { 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 // Assign the default name of the group based on the name of

View File

@ -5,11 +5,6 @@
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
#include "solvespace.h" #include "solvespace.h"
namespace SolveSpace {
// These are defined in headless.cpp, and aren't exposed in solvespace.h.
extern std::shared_ptr<Pixmap> framebuffer;
}
static void ShowUsage(const std::string &cmd) { static void ShowUsage(const std::string &cmd) {
fprintf(stderr, "Usage: %s <command> <options> <filename> [filename...]", cmd.c_str()); fprintf(stderr, "Usage: %s <command> <options> <filename> [filename...]", cmd.c_str());
//-----------------------------------------------------------------------------> 80 col */ //-----------------------------------------------------------------------------> 80 col */
@ -50,21 +45,21 @@ Commands:
Reloads all imported files, regenerates the sketch, and saves it. Reloads all imported files, regenerates the sketch, and saves it.
)"); )");
auto FormatListFromFileFilter = [](const FileFilter *filter) { auto FormatListFromFileFilters = [](const std::vector<Platform::FileFilter> &filters) {
std::string descr; std::string descr;
while(filter->name) { for(auto filter : filters) {
descr += "\n "; descr += "\n ";
descr += filter->name; descr += filter.name;
descr += " ("; descr += " (";
const char *const *patterns = filter->patterns; bool first = true;
while(*patterns) { for(auto extension : filter.extensions) {
descr += *patterns; if(!first) {
if(*++patterns) {
descr += ", "; descr += ", ";
} }
descr += extension;
first = false;
} }
descr += ")"; descr += ")";
filter++;
} }
return descr; return descr;
}; };
@ -76,11 +71,11 @@ File formats:
export-wireframe:%s export-wireframe:%s
export-mesh:%s export-mesh:%s
export-surfaces:%s export-surfaces:%s
)", FormatListFromFileFilter(RasterFileFilter).c_str(), )", FormatListFromFileFilters(Platform::RasterFileFilters).c_str(),
FormatListFromFileFilter(VectorFileFilter).c_str(), FormatListFromFileFilters(Platform::VectorFileFilters).c_str(),
FormatListFromFileFilter(Vector3dFileFilter).c_str(), FormatListFromFileFilters(Platform::Vector3dFileFilters).c_str(),
FormatListFromFileFilter(MeshFileFilter).c_str(), FormatListFromFileFilters(Platform::MeshFileFilters).c_str(),
FormatListFromFileFilter(SurfaceFileFilter).c_str()); FormatListFromFileFilters(Platform::SurfaceFileFilters).c_str());
} }
static bool RunCommand(const std::vector<std::string> args) { static bool RunCommand(const std::vector<std::string> args) {

View File

@ -10,118 +10,6 @@
using SolveSpace::dbp; 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 */ /* Miscellanea */
void SolveSpace::OpenWebsite(const char *url) { void SolveSpace::OpenWebsite(const char *url) {

View File

@ -52,121 +52,6 @@
#endif #endif
namespace SolveSpace { 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<Gtk::FileFilter> 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) { void OpenWebsite(const char *url) {
gtk_show_uri(Gdk::Screen::get_default()->gobj(), url, GDK_CURRENT_TIME, NULL); gtk_show_uri(Gdk::Screen::get_default()->gobj(), url, GDK_CURRENT_TIME, NULL);

View File

@ -69,5 +69,60 @@ RgbaColor Settings::ThawColor(const std::string &key, RgbaColor defaultValue) {
return RgbaColor::FromPackedInt(ThawInt(key, defaultValue.ToPackedInt())); 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<FileFilter> &filters) {
for(auto filter : filters) AddFilter(filter);
}
std::vector<FileFilter> SolveSpaceModelFileFilters = {
{ CN_("file-type", "SolveSpace models"), { "slvs" } },
};
std::vector<FileFilter> RasterFileFilters = {
{ CN_("file-type", "PNG image"), { "png" } },
};
std::vector<FileFilter> 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<FileFilter> SurfaceFileFilters = {
{ CN_("file-type", "STEP file"), { "step", "stp" } },
};
std::vector<FileFilter> 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<FileFilter> Vector3dFileFilters = {
{ CN_("file-type", "STEP file"), { "step", "stp" } },
{ CN_("file-type", "DXF file (AutoCAD 2007)"), { "dxf" } },
};
std::vector<FileFilter> ImportFileFilters = {
{ CN_("file-type", "AutoCAD DXF and DWG files"), { "dxf", "dwg" } },
};
std::vector<FileFilter> CsvFileFilters = {
{ CN_("file-type", "Comma-separated values"), { "csv" } },
};
} }
} }

View File

@ -282,6 +282,53 @@ typedef std::shared_ptr<MessageDialog> MessageDialogRef;
MessageDialogRef CreateMessageDialog(WindowRef parentWindow); MessageDialogRef CreateMessageDialog(WindowRef parentWindow);
// A file filter.
struct FileFilter {
std::string name;
std::vector<std::string> extensions;
};
// SolveSpace's native file format
extern std::vector<FileFilter> SolveSpaceModelFileFilters;
// Raster image
extern std::vector<FileFilter> RasterFileFilters;
// Triangle mesh
extern std::vector<FileFilter> MeshFileFilters;
// NURBS surfaces
extern std::vector<FileFilter> SurfaceFileFilters;
// 2d vector (lines and curves) format
extern std::vector<FileFilter> VectorFileFilters;
// 3d vector (wireframe lines and curves) format
extern std::vector<FileFilter> Vector3dFileFilters;
// Any importable format
extern std::vector<FileFilter> ImportFileFilters;
// Comma-separated value, like a spreadsheet would use
extern std::vector<FileFilter> 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<std::string> extensions) = 0;
void AddFilter(const FileFilter &filter);
void AddFilters(const std::vector<FileFilter> &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<FileDialog> FileDialogRef;
FileDialogRef CreateOpenFileDialog(WindowRef parentWindow);
FileDialogRef CreateSaveFileDialog(WindowRef parentWindow);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// Application-wide APIs // Application-wide APIs
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------

View File

@ -13,15 +13,16 @@
#include <gtkmm/box.h> #include <gtkmm/box.h>
#include <gtkmm/checkmenuitem.h> #include <gtkmm/checkmenuitem.h>
#include <gtkmm/entry.h> #include <gtkmm/entry.h>
#include <gtkmm/filechooserdialog.h>
#include <gtkmm/fixed.h> #include <gtkmm/fixed.h>
#include <gtkmm/glarea.h> #include <gtkmm/glarea.h>
#include <gtkmm/main.h> #include <gtkmm/main.h>
#include <gtkmm/menu.h> #include <gtkmm/menu.h>
#include <gtkmm/menubar.h> #include <gtkmm/menubar.h>
#include <gtkmm/messagedialog.h>
#include <gtkmm/scrollbar.h> #include <gtkmm/scrollbar.h>
#include <gtkmm/separatormenuitem.h> #include <gtkmm/separatormenuitem.h>
#include <gtkmm/window.h> #include <gtkmm/window.h>
#include <gtkmm/messagedialog.h>
namespace SolveSpace { namespace SolveSpace {
namespace Platform { namespace Platform {
@ -1078,6 +1079,138 @@ MessageDialogRef CreateMessageDialog(WindowRef parentWindow) {
std::static_pointer_cast<WindowImplGtk>(parentWindow)->gtkWindow); std::static_pointer_cast<WindowImplGtk>(parentWindow)->gtkWindow);
} }
//-----------------------------------------------------------------------------
// File dialogs
//-----------------------------------------------------------------------------
class FileDialogImplGtk : public FileDialog {
public:
Gtk::FileChooserDialog gtkDialog;
std::vector<std::string> 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<std::string> extensions) override {
Glib::RefPtr<Gtk::FileFilter> 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 &gtkParent = std::static_pointer_cast<WindowImplGtk>(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<FileDialogImplGtk>(std::move(gtkDialog));
}
FileDialogRef CreateSaveFileDialog(WindowRef parentWindow) {
Gtk::Window &gtkParent = std::static_pointer_cast<WindowImplGtk>(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<FileDialogImplGtk>(std::move(gtkDialog));
}
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// Application-wide APIs // Application-wide APIs
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------

View File

@ -1063,6 +1063,180 @@ MessageDialogRef CreateMessageDialog(WindowRef parentWindow) {
return dialog; 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<std::string> 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<std::string> 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<OpenFileDialogImplCocoa> dialog = std::make_shared<OpenFileDialogImplCocoa>();
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<SaveFileDialogImplCocoa> dialog = std::make_shared<SaveFileDialogImplCocoa>();
dialog->nsPanel = nsPanel;
dialog->ssAccessory = ssAccessory;
ssAccessory.filters = dialog->nsFilters;
return dialog;
}
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// Application-wide APIs // Application-wide APIs
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------

View File

@ -114,13 +114,25 @@ WindowRef CreateWindow(Window::Kind kind, WindowRef parentWindow) {
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// Dialogs // Message dialogs
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
MessageDialogRef CreateMessageDialog(WindowRef parentWindow) { MessageDialogRef CreateMessageDialog(WindowRef parentWindow) {
return std::shared_ptr<MessageDialog>(); return std::shared_ptr<MessageDialog>();
} }
//-----------------------------------------------------------------------------
// File dialogs
//-----------------------------------------------------------------------------
FileDialogRef CreateOpenFileDialog(WindowRef parentWindow) {
return std::shared_ptr<FileDialog>();
}
FileDialogRef CreateSaveFileDialog(WindowRef parentWindow) {
return std::shared_ptr<FileDialog>();
}
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// Application-wide APIs // Application-wide APIs
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
@ -135,14 +147,6 @@ void Exit() {
// Dialogs // 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) { void OpenWebsite(const char *url) {
ssassert(false, "Not implemented"); ssassert(false, "Not implemented");
} }

View File

@ -9,6 +9,7 @@
#include <windows.h> #include <windows.h>
#include <windowsx.h> #include <windowsx.h>
#include <commctrl.h> #include <commctrl.h>
#include <commdlg.h>
#include <shellapi.h> #include <shellapi.h>
#ifndef WM_DPICHANGED #ifndef WM_DPICHANGED
@ -1272,7 +1273,7 @@ WindowRef CreateWindow(Window::Kind kind, WindowRef parentWindow) {
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// Dialogs // Message dialogs
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
class MessageDialogImplWin32 : public MessageDialog { class MessageDialogImplWin32 : public MessageDialog {
@ -1381,6 +1382,109 @@ MessageDialogRef CreateMessageDialog(WindowRef parentWindow) {
return dialog; 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<std::string> 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<FileDialogImplWin32> dialog = std::make_shared<FileDialogImplWin32>();
dialog->ofn.hwndOwner = std::static_pointer_cast<WindowImplWin32>(parentWindow)->hWindow;
dialog->isSaveDialog = false;
return dialog;
}
FileDialogRef CreateSaveFileDialog(WindowRef parentWindow) {
std::shared_ptr<FileDialogImplWin32> dialog = std::make_shared<FileDialogImplWin32>();
dialog->ofn.hwndOwner = std::static_pointer_cast<WindowImplWin32>(parentWindow)->hWindow;
dialog->isSaveDialog = true;
return dialog;
}
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// Application-wide APIs // Application-wide APIs
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------

View File

@ -33,104 +33,11 @@ SiHdl SpaceNavigator = SI_NO_HANDLE;
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// Utility routines // Utility routines
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
std::wstring Title(const std::string &s) {
return Widen("SolveSpace - " + s);
}
void SolveSpace::OpenWebsite(const char *url) { void SolveSpace::OpenWebsite(const char *url) {
ShellExecuteW((HWND)SS.GW.window->NativePtr(), ShellExecuteW((HWND)SS.GW.window->NativePtr(),
L"open", Widen(url).c_str(), NULL, NULL, SW_SHOWNORMAL); 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<Platform::Path> SolveSpace::GetFontFiles() { std::vector<Platform::Path> SolveSpace::GetFontFiles() {
std::vector<Platform::Path> fonts; std::vector<Platform::Path> fonts;

View File

@ -395,10 +395,22 @@ void SolveSpaceUI::AddToRecentList(const Platform::Path &filename) {
} }
bool SolveSpaceUI::GetFilenameAndSave(bool saveAs) { bool SolveSpaceUI::GetFilenameAndSave(bool saveAs) {
Platform::SettingsRef settings = Platform::GetSettings();
Platform::Path newSaveFile = saveFile; Platform::Path newSaveFile = saveFile;
if(saveAs || saveFile.IsEmpty()) { 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)) { if(SaveToFile(newSaveFile)) {
@ -490,9 +502,12 @@ void SolveSpaceUI::MenuFile(Command id) {
case Command::OPEN: { case Command::OPEN: {
if(!SS.OkayToStartNewFile()) break; if(!SS.OkayToStartNewFile()) break;
Platform::Path newFile; Platform::FileDialogRef dialog = Platform::CreateOpenFileDialog(SS.GW.window);
if(GetOpenFile(&newFile, "", SlvsFileFilter)) { dialog->AddFilters(Platform::SolveSpaceModelFileFilters);
SS.Load(newFile); dialog->ThawChoices(settings, "Sketch");
if(dialog->RunModal()) {
dialog->FreezeChoices(settings, "Sketch");
SS.Load(dialog->GetFilename());
} }
break; break;
} }
@ -505,24 +520,28 @@ void SolveSpaceUI::MenuFile(Command id) {
SS.GetFilenameAndSave(/*saveAs=*/true); SS.GetFilenameAndSave(/*saveAs=*/true);
break; break;
case Command::EXPORT_PNG: { case Command::EXPORT_IMAGE: {
Platform::Path exportFile = SS.saveFile; Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(SS.GW.window);
if(!GetSaveFile(&exportFile, "", RasterFileFilter)) break; dialog->AddFilters(Platform::RasterFileFilters);
SS.ExportAsPngTo(exportFile); dialog->ThawChoices(settings, "ExportImage");
if(dialog->RunModal()) {
dialog->FreezeChoices(settings, "ExportImage");
SS.ExportAsPngTo(dialog->GetFilename());
}
break; break;
} }
case Command::EXPORT_VIEW: { case Command::EXPORT_VIEW: {
Platform::Path exportFile = SS.saveFile; Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(SS.GW.window);
if(!GetSaveFile(&exportFile, dialog->AddFilters(Platform::VectorFileFilters);
Platform::GetSettings()->ThawString("ViewExportFormat"), dialog->ThawChoices(settings, "ExportView");
VectorFileFilter)) break; if(!dialog->RunModal()) break;
settings->FreezeString("ViewExportFormat", exportFile.Extension()); dialog->FreezeChoices(settings, "ExportView");
// If the user is exporting something where it would be // If the user is exporting something where it would be
// inappropriate to include the constraints, then warn. // inappropriate to include the constraints, then warn.
if(SS.GW.showConstraints && if(SS.GW.showConstraints &&
(exportFile.HasExtension("txt") || (dialog->GetFilename().HasExtension("txt") ||
fabs(SS.exportOffset) > LENGTH_EPS)) fabs(SS.exportOffset) > LENGTH_EPS))
{ {
Message(_("Constraints are currently shown, and will be exported " Message(_("Constraints are currently shown, and will be exported "
@ -531,62 +550,63 @@ void SolveSpaceUI::MenuFile(Command id) {
"text window.")); "text window."));
} }
SS.ExportViewOrWireframeTo(exportFile, /*exportWireframe*/false); SS.ExportViewOrWireframeTo(dialog->GetFilename(), /*exportWireframe=*/false);
break; break;
} }
case Command::EXPORT_WIREFRAME: { case Command::EXPORT_WIREFRAME: {
Platform::Path exportFile = SS.saveFile; Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(SS.GW.window);
if(!GetSaveFile(&exportFile, dialog->AddFilters(Platform::Vector3dFileFilters);
Platform::GetSettings()->ThawString("WireframeExportFormat"), dialog->ThawChoices(settings, "ExportWireframe");
Vector3dFileFilter)) break; if(!dialog->RunModal()) break;
settings->FreezeString("WireframeExportFormat", exportFile.Extension()); dialog->FreezeChoices(settings, "ExportWireframe");
SS.ExportViewOrWireframeTo(exportFile, /*exportWireframe*/true); SS.ExportViewOrWireframeTo(dialog->GetFilename(), /*exportWireframe*/true);
break; break;
} }
case Command::EXPORT_SECTION: { case Command::EXPORT_SECTION: {
Platform::Path exportFile = SS.saveFile; Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(SS.GW.window);
if(!GetSaveFile(&exportFile, dialog->AddFilters(Platform::VectorFileFilters);
Platform::GetSettings()->ThawString("SectionExportFormat"), dialog->ThawChoices(settings, "ExportSection");
VectorFileFilter)) break; if(!dialog->RunModal()) break;
settings->FreezeString("SectionExportFormat", exportFile.Extension()); dialog->FreezeChoices(settings, "ExportSection");
SS.ExportSectionTo(exportFile); SS.ExportSectionTo(dialog->GetFilename());
break; break;
} }
case Command::EXPORT_MESH: { case Command::EXPORT_MESH: {
Platform::Path exportFile = SS.saveFile; Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(SS.GW.window);
if(!GetSaveFile(&exportFile, dialog->AddFilters(Platform::MeshFileFilters);
Platform::GetSettings()->ThawString("MeshExportFormat"), dialog->ThawChoices(settings, "ExportMesh");
MeshFileFilter)) break; if(!dialog->RunModal()) break;
settings->FreezeString("MeshExportFormat", exportFile.Extension()); dialog->FreezeChoices(settings, "ExportMesh");
SS.ExportMeshTo(exportFile); SS.ExportMeshTo(dialog->GetFilename());
break; break;
} }
case Command::EXPORT_SURFACES: { case Command::EXPORT_SURFACES: {
Platform::Path exportFile = SS.saveFile; Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(SS.GW.window);
if(!GetSaveFile(&exportFile, dialog->AddFilters(Platform::SurfaceFileFilters);
Platform::GetSettings()->ThawString("SurfacesExportFormat"), dialog->ThawChoices(settings, "ExportSurfaces");
SurfaceFileFilter)) break; if(!dialog->RunModal()) break;
settings->FreezeString("SurfacesExportFormat", exportFile.Extension()); dialog->FreezeChoices(settings, "ExportSurfaces");
StepFileWriter sfw = {}; StepFileWriter sfw = {};
sfw.ExportSurfacesTo(exportFile); sfw.ExportSurfacesTo(dialog->GetFilename());
break; break;
} }
case Command::IMPORT: { case Command::IMPORT: {
Platform::Path importFile; Platform::FileDialogRef dialog = Platform::CreateOpenFileDialog(SS.GW.window);
if(!GetOpenFile(&importFile, dialog->AddFilters(Platform::ImportFileFilters);
Platform::GetSettings()->ThawString("ImportFormat"), dialog->ThawChoices(settings, "Import");
ImportableFileFilter)) break; if(!dialog->RunModal()) break;
settings->FreezeString("ImportFormat", importFile.Extension()); dialog->FreezeChoices(settings, "Import");
Platform::Path importFile = dialog->GetFilename();
if(importFile.HasExtension("dxf")) { if(importFile.HasExtension("dxf")) {
ImportDxf(importFile); ImportDxf(importFile);
} else if(importFile.HasExtension("dwg")) { } else if(importFile.HasExtension("dwg")) {
@ -613,6 +633,8 @@ void SolveSpaceUI::MenuFile(Command id) {
} }
void SolveSpaceUI::MenuAnalyze(Command id) { void SolveSpaceUI::MenuAnalyze(Command id) {
Platform::SettingsRef settings = Platform::GetSettings();
SS.GW.GroupSelection(); SS.GW.GroupSelection();
auto const &gs = SS.GW.gs; auto const &gs = SS.GW.gs;
@ -813,9 +835,13 @@ void SolveSpaceUI::MenuAnalyze(Command id) {
break; break;
case Command::STOP_TRACING: { case Command::STOP_TRACING: {
Platform::Path exportFile = SS.saveFile; Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(SS.GW.window);
if(GetSaveFile(&exportFile, "", CsvFileFilter)) { dialog->AddFilters(Platform::CsvFileFilters);
FILE *f = OpenFile(exportFile, "wb"); dialog->ThawChoices(settings, "Trace");
if(dialog->RunModal()) {
dialog->FreezeChoices(settings, "Trace");
FILE *f = OpenFile(dialog->GetFilename(), "wb");
if(f) { if(f) {
int i; int i;
SContour *sc = &(SS.traced.path); SContour *sc = &(SS.traced.path);
@ -827,7 +853,7 @@ void SolveSpaceUI::MenuAnalyze(Command id) {
} }
fclose(f); fclose(f);
} else { } 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 // Clear the trace, and stop tracing

View File

@ -142,18 +142,6 @@ extern Platform::Path RecentFile[MAX_RECENT];
#define AUTOSAVE_EXT "slvs~" #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<Platform::Path> GetFontFiles(); std::vector<Platform::Path> GetFontFiles();
void OpenWebsite(const char *url); void OpenWebsite(const char *url);
@ -172,11 +160,17 @@ void *MemAlloc(size_t n);
void MemFree(void *p); void MemFree(void *p);
void vl(); // debug function to validate heaps void vl(); // debug function to validate heaps
#include "resource.h"
// End of platform-specific functions // End of platform-specific functions
//================ //================
#include "resource.h"
enum class Unit : uint32_t {
MM = 0,
INCHES,
METERS
};
template<class T> template<class T>
struct CompareHandle { struct CompareHandle {
bool operator()(T lhs, T rhs) const { return lhs.v < rhs.v; } bool operator()(T lhs, T rhs) const { return lhs.v < rhs.v; }

View File

@ -58,63 +58,6 @@ inline const char *C_(const char *msgctxt, const char *msgid) {
} }
#endif #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. // This table describes the top-level menus in the graphics winodw.
enum class Command : uint32_t { enum class Command : uint32_t {
NONE = 0, NONE = 0,
@ -124,7 +67,7 @@ enum class Command : uint32_t {
OPEN_RECENT, OPEN_RECENT,
SAVE, SAVE,
SAVE_AS, SAVE_AS,
EXPORT_PNG, EXPORT_IMAGE,
EXPORT_MESH, EXPORT_MESH,
EXPORT_SURFACES, EXPORT_SURFACES,
EXPORT_VIEW, EXPORT_VIEW,