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:
parent
d7968978ad
commit
6b5db58971
@ -7,6 +7,7 @@
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="NSViewController">
|
||||
<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="view" destination="c22-O7-iKe" id="w2z-a4-Khs"/>
|
||||
</connections>
|
||||
|
22
src/file.cpp
22
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<Platform::Path, Platform::Path, Platform::PathLess> 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> 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());
|
||||
|
@ -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 },
|
||||
|
@ -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
|
||||
|
@ -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<Pixmap> framebuffer;
|
||||
}
|
||||
|
||||
static void ShowUsage(const std::string &cmd) {
|
||||
fprintf(stderr, "Usage: %s <command> <options> <filename> [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<Platform::FileFilter> &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<std::string> args) {
|
||||
|
@ -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) {
|
||||
|
@ -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<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) {
|
||||
gtk_show_uri(Gdk::Screen::get_default()->gobj(), url, GDK_CURRENT_TIME, NULL);
|
||||
|
@ -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<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" } },
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -282,6 +282,53 @@ typedef std::shared_ptr<MessageDialog> MessageDialogRef;
|
||||
|
||||
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
|
||||
//-----------------------------------------------------------------------------
|
||||
|
@ -13,15 +13,16 @@
|
||||
#include <gtkmm/box.h>
|
||||
#include <gtkmm/checkmenuitem.h>
|
||||
#include <gtkmm/entry.h>
|
||||
#include <gtkmm/filechooserdialog.h>
|
||||
#include <gtkmm/fixed.h>
|
||||
#include <gtkmm/glarea.h>
|
||||
#include <gtkmm/main.h>
|
||||
#include <gtkmm/menu.h>
|
||||
#include <gtkmm/menubar.h>
|
||||
#include <gtkmm/messagedialog.h>
|
||||
#include <gtkmm/scrollbar.h>
|
||||
#include <gtkmm/separatormenuitem.h>
|
||||
#include <gtkmm/window.h>
|
||||
#include <gtkmm/messagedialog.h>
|
||||
|
||||
namespace SolveSpace {
|
||||
namespace Platform {
|
||||
@ -1078,6 +1079,138 @@ MessageDialogRef CreateMessageDialog(WindowRef parentWindow) {
|
||||
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 >kParent = 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 >kParent = 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
|
||||
//-----------------------------------------------------------------------------
|
||||
|
@ -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<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
|
||||
//-----------------------------------------------------------------------------
|
||||
|
@ -114,13 +114,25 @@ WindowRef CreateWindow(Window::Kind kind, WindowRef parentWindow) {
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Dialogs
|
||||
// Message dialogs
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
MessageDialogRef CreateMessageDialog(WindowRef parentWindow) {
|
||||
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
|
||||
//-----------------------------------------------------------------------------
|
||||
@ -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");
|
||||
}
|
||||
|
@ -9,6 +9,7 @@
|
||||
#include <windows.h>
|
||||
#include <windowsx.h>
|
||||
#include <commctrl.h>
|
||||
#include <commdlg.h>
|
||||
#include <shellapi.h>
|
||||
|
||||
#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<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
|
||||
//-----------------------------------------------------------------------------
|
||||
|
@ -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<Platform::Path> SolveSpace::GetFontFiles() {
|
||||
std::vector<Platform::Path> fonts;
|
||||
|
||||
|
@ -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
|
||||
|
@ -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<Platform::Path> 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<class T>
|
||||
struct CompareHandle {
|
||||
bool operator()(T lhs, T rhs) const { return lhs.v < rhs.v; }
|
||||
|
59
src/ui.h
59
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,
|
||||
|
Loading…
Reference in New Issue
Block a user