more graphical open file dialog on linux

This commit is contained in:
Adam D. Ruppe 2021-12-01 22:27:45 -05:00
parent 3751953018
commit d1f5de18d6
1 changed files with 315 additions and 93 deletions

408
minigui.d
View File

@ -502,6 +502,14 @@ class Widget : ReflectableProperties {
Added November 25, 2021
+/
int scaleWithDpi(int value, int assumedDpi = 96) {
// avoid potential overflow with common special values
if(value == int.max)
return int.max;
if(value == int.min)
return int.min;
if(value == 0)
return 0;
auto divide = (parentWindow && parentWindow.win) ? parentWindow.win.actualDpi : assumedDpi;
// for lower values it is something i don't really want changed anyway since it is an old monitor and you don't want to scale down.
// this also covers the case when actualDpi returns 0.
@ -4148,9 +4156,11 @@ class ListWidget : ListWidgetBase {
version(custom_widgets)
override void defaultEventHandler_click(ClickEvent event) {
this.focus();
auto y = (event.clientY - 4) / defaultLineHeight;
if(y >= 0 && y < options.length) {
setSelection(y);
if(event.button == MouseButton.left) {
auto y = (event.clientY - 4) / defaultLineHeight;
if(y >= 0 && y < options.length) {
setSelection(y);
}
}
super.defaultEventHandler_click(event);
}
@ -4228,6 +4238,7 @@ class ListWidget : ListWidgetBase {
{}
} else version(custom_widgets) {
scrollTo(Point(0, 0));
redraw();
}
}
@ -6422,12 +6433,45 @@ class VerticalLayout : Layout {
// most of this is intentionally blank - widget's default is vertical layout right now
///
this(Widget parent) { super(parent); }
/++
Sets a max width for the layout so you don't have to subclass. The max width
is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled.
History:
Added November 29, 2021 (dub v10.5)
+/
this(int maxWidth, Widget parent) {
this.mw = maxWidth;
super(parent);
}
private int mw = int.max;
override int maxWidth() { return scaleWithDpi(mw); }
}
/// Stacks the widgets horizontally, taking all the available height for each child.
class HorizontalLayout : Layout {
///
this(Widget parent) { super(parent); }
/++
Sets a max height for the layout so you don't have to subclass. The max height
is in device-independent pixels, meaning pixels at 96 dpi that are auto-scaled.
History:
Added November 29, 2021 (dub v10.5)
+/
this(int maxHeight, Widget parent) {
this.mh = maxHeight;
super(parent);
}
private int mh = 0;
override void recomputeChildLayout() {
.recomputeChildLayout!"width"(this);
}
@ -6447,6 +6491,9 @@ class HorizontalLayout : Layout {
}
override int maxHeight() {
if(mh != 0)
return mymax(minHeight, scaleWithDpi(mh));
int largest = 0;
int margins = 0;
int lastMargin = 0;
@ -7862,6 +7909,11 @@ class TableView : Widget {
}
+/
version(win32_widgets) {
CellStyle last;
COLORREF defaultColor;
COLORREF defaultBackground;
}
version(win32_widgets)
override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) {
@ -7884,18 +7936,30 @@ class TableView : Widget {
if(getCellStyle is null) // this SHOULD never happen...
return 0;
if(s.iSubItem == 0) {
// Windows resets it per row so we'll use item 0 as a chance
// to capture these for later
defaultColor = s.clrText;
defaultBackground = s.clrTextBk;
}
auto style = getCellStyle(cast(int) s.nmcd.dwItemSpec, cast(int) s.iSubItem);
if(style == CellStyle.init)
// if no special style and no reset needed...
if(style == CellStyle.init && (s.iSubItem == 0 || last == CellStyle.init))
return 0; // allow default processing to continue
last = style;
// might still need to reset or use the preference.
if(style.flags & CellStyle.Flags.textColorSet)
s.clrText = style.textColor.asWindowsColorRef;
else
s.clrText = 0; // reset in case it was set from last iteration not a fan
s.clrText = defaultColor; // reset in case it was set from last iteration not a fan
if(style.flags & CellStyle.Flags.backgroundColorSet)
s.clrTextBk = style.backgroundColor.asWindowsColorRef;
else
s.clrTextBk = Color.white.asWindowsColorRef; // need to reset it... not a fan of this
s.clrTextBk = defaultBackground; // need to reset it... not a fan of this
return CDRF_NEWFONT;
default:
@ -12471,7 +12535,7 @@ void getFileName(
onCancel();
}
} else version(custom_widgets) {
auto picker = new FilePicker(prefilledName);
auto picker = new FilePicker(prefilledName, filters);
picker.onOK = onOK;
picker.onCancel = onCancel;
picker.show();
@ -12484,10 +12548,194 @@ class FilePicker : Dialog {
void delegate(string) onOK;
void delegate() onCancel;
LineEdit lineEdit;
this(string prefilledName, Window owner = null) {
enum GetFilesResult {
success,
fileNotFound
}
static GetFilesResult getFiles(string cwd, scope void delegate(string name, bool isDirectory) dg) {
version(Windows) {
WIN32_FIND_DATA data;
WCharzBuffer search = WCharzBuffer(cwd ~ "/*");
auto handle = FindFirstFileW(search.ptr, &data);
scope(exit) if(handle !is INVALID_HANDLE_VALUE) FindClose(handle);
if(handle is INVALID_HANDLE_VALUE) {
if(GetLastError() == ERROR_FILE_NOT_FOUND)
return GetFilesResult.fileNotFound;
throw new WindowsApiException("FindFirstFileW");
}
try_more:
string name = makeUtf8StringFromWindowsString(data.cFileName[0 .. findIndexOfZero(data.cFileName[])]);
dg(name, (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? true : false);
auto ret = FindNextFileW(handle, &data);
if(ret == 0) {
if(GetLastError() == ERROR_NO_MORE_FILES)
return GetFilesResult.success;
throw new WindowsApiException("FindNextFileW");
}
goto try_more;
} else version(Posix) {
import core.sys.posix.dirent;
auto dir = opendir((cwd ~ "\0").ptr);
scope(exit)
if(dir) closedir(dir);
if(dir is null)
throw new ErrnoApiException("opendir [" ~ cwd ~ "]");
auto dirent = readdir(dir);
if(dirent is null)
return GetFilesResult.fileNotFound;
try_more:
string name = dirent.d_name[0 .. findIndexOfZero(dirent.d_name[])].idup;
dg(name, dirent.d_type == DT_DIR);
dirent = readdir(dir);
if(dirent is null)
return GetFilesResult.success;
goto try_more;
} else static assert(0);
}
// returns common prefix
string loadFiles(string cwd, string[] filters...) {
string[] files;
string[] dirs;
string commonPrefix;
getFiles(cwd, (string name, bool isDirectory) {
if(name == ".")
return; // skip this as unnecessary
if(isDirectory)
dirs ~= name;
else {
foreach(filter; filters)
if(
filter.length <= 1 ||
(filter[0] == '*' && name.endsWith(filter[1 .. $])) ||
(filter[$-1] == '*' && name.startsWith(filter[0 .. $ - 1]))
)
{
files ~= name;
if(filter.length > 0 && filter[$-1] == '*') {
if(commonPrefix is null) {
commonPrefix = name;
} else {
foreach(idx, char i; name) {
if(idx >= commonPrefix.length || i != commonPrefix[idx]) {
commonPrefix = commonPrefix[0 .. idx];
break;
}
}
}
}
break;
}
}
});
extern(C) static int comparator(scope const void* a, scope const void* b) {
auto sa = *cast(string*) a;
auto sb = *cast(string*) b;
for(int i = 0; i < sa.length; i++) {
if(i == sb.length)
return 1;
return sa[i] - sb[i];
}
return 0;
}
nonPhobosSort(files, &comparator);
nonPhobosSort(dirs, &comparator);
listWidget.clear();
dirWidget.clear();
foreach(name; dirs)
dirWidget.addOption(name);
foreach(name; files)
listWidget.addOption(name);
return commonPrefix;
}
ListWidget listWidget;
ListWidget dirWidget;
string currentDirectory;
string[] processedFilters;
//string[] filters = null, // format here is like ["Text files\0*.txt;*.text", "Image files\n*.png;*.jpg"]
this(string prefilledName, string[] filters, Window owner = null) {
super(300, 200, "Choose File..."); // owner);
auto listWidget = new ListWidget(this);
foreach(filter; filters) {
while(filter.length && filter[0] != 0) {
filter = filter[1 .. $];
}
if(filter.length)
filter = filter[1 .. $]; // trim off the 0
while(filter.length) {
int idx = 0;
while(idx < filter.length && filter[idx] != ';') {
idx++;
}
processedFilters ~= filter[0 .. idx];
if(idx < filter.length)
idx++; // skip the ;
filter = filter[idx .. $];
}
}
currentDirectory = ".";
{
auto hl = new HorizontalLayout(this);
dirWidget = new ListWidget(hl);
listWidget = new ListWidget(hl);
// double click events normally trigger something else but
// here user might be clicking kinda fast and we'd rather just
// keep it
dirWidget.addEventListener((scope DoubleClickEvent dev) {
auto ce = new ChangeEvent!void(dirWidget, () {});
ce.dispatch();
});
dirWidget.addEventListener((scope ChangeEvent!void sce) {
string v;
foreach(o; dirWidget.options)
if(o.selected) {
v = o.label;
break;
}
if(v.length) {
currentDirectory ~= "/" ~ v;
loadFiles(currentDirectory, processedFilters);
}
});
// double click here, on the other hand, selects the file
// and moves on
listWidget.addEventListener((scope DoubleClickEvent dev) {
OK();
});
}
lineEdit = new LineEdit(this);
lineEdit.focus();
@ -12502,105 +12750,32 @@ class FilePicker : Dialog {
lineEdit.content = o.label;
});
//version(none)
loadFiles(currentDirectory, processedFilters);
lineEdit.addEventListener((KeyDownEvent event) {
if(event.key == Key.Tab) {
listWidget.clear();
string commonPrefix;
auto cnt = lineEdit.content;
if(cnt.length >= 2 && cnt[0 ..2] == "./")
cnt = cnt[2 .. $];
auto current = lineEdit.content;
if(current.length >= 2 && current[0 ..2] == "./")
current = current[2 .. $];
version(Windows) {
WIN32_FIND_DATA data;
WCharzBuffer search = WCharzBuffer("./" ~ cnt ~ "*");
auto handle = FindFirstFileW(search.ptr, &data);
scope(exit) if(handle !is INVALID_HANDLE_VALUE) FindClose(handle);
if(handle is INVALID_HANDLE_VALUE) {
if(GetLastError() == ERROR_FILE_NOT_FOUND)
goto file_not_found;
throw new WindowsApiException("FindFirstFileW");
}
} else version(Posix) {
import core.sys.posix.dirent;
auto dir = opendir(".");
scope(exit)
if(dir) closedir(dir);
if(dir is null)
throw new ErrnoApiException("opendir");
auto commonPrefix = loadFiles(".", current ~ "*");
auto dirent = readdir(dir);
if(dirent is null)
goto file_not_found;
// filter those that don't start with it, since posix doesn't
// do the * thing itself
while(dirent.d_name[0 .. cnt.length] != cnt[]) {
dirent = readdir(dir);
if(dirent is null)
goto file_not_found;
}
} else static assert(0);
while(true) {
//foreach(string name; dirEntries(".", cnt ~ "*", SpanMode.shallow)) {
version(Windows) {
string name = makeUtf8StringFromWindowsString(data.cFileName[0 .. findIndexOfZero(data.cFileName[])]);
} else version(Posix) {
string name = dirent.d_name[0 .. findIndexOfZero(dirent.d_name[])].idup;
} else static assert(0);
listWidget.addOption(name);
if(commonPrefix is null)
commonPrefix = name;
else {
foreach(idx, char i; name) {
if(idx >= commonPrefix.length || i != commonPrefix[idx]) {
commonPrefix = commonPrefix[0 .. idx];
break;
}
}
}
version(Windows) {
auto ret = FindNextFileW(handle, &data);
if(ret == 0) {
if(GetLastError() == ERROR_NO_MORE_FILES)
break;
throw new WindowsApiException("FindNextFileW");
}
} else version(Posix) {
dirent = readdir(dir);
if(dirent is null)
break;
while(dirent.d_name[0 .. cnt.length] != cnt[]) {
dirent = readdir(dir);
if(dirent is null)
break;
}
if(dirent is null)
break;
} else static assert(0);
}
if(commonPrefix.length)
lineEdit.content = commonPrefix;
file_not_found:
// FIXME: if that is a directory, add the slash? or even go inside?
event.preventDefault();
}
});
lineEdit.content = prefilledName;
auto hl = new HorizontalLayout(this);
auto hl = new HorizontalLayout(60, this);
auto cancelButton = new Button("Cancel", hl);
auto okButton = new Button("OK", hl);
recomputeChildLayout(); // FIXME hack
cancelButton.addEventListener(EventType.triggered, &Cancel);
okButton.addEventListener(EventType.triggered, &OK);
@ -12616,8 +12791,26 @@ class FilePicker : Dialog {
}
override void OK() {
if(onOK)
onOK(lineEdit.content);
if(lineEdit.content.length) {
string accepted;
auto c = lineEdit.content;
if(c.length && c[0] == '/')
accepted = c;
else
accepted = currentDirectory ~ "/" ~ lineEdit.content;
if(isDir(accepted)) {
// FIXME: would be kinda nice to support ~ and collapse these paths too
// FIXME: would also be nice to actually show the "Looking in..." directory and maybe the filters but later.
currentDirectory = accepted;
loadFiles(currentDirectory, processedFilters);
lineEdit.content = "";
return;
}
if(onOK)
onOK(accepted);
}
close();
}
@ -12628,6 +12821,23 @@ class FilePicker : Dialog {
}
}
private bool isDir(string name) {
version(Windows) {
auto ws = WCharzBuffer(name);
auto ret = GetFileAttributesW(ws.ptr);
if(ret == INVALID_FILE_ATTRIBUTES)
return false;
return (ret & FILE_ATTRIBUTE_DIRECTORY) != 0;
} else version(Posix) {
import core.sys.posix.sys.stat;
stat_t buf;
auto ret = stat((name ~ '\0').ptr, &buf);
if(ret == -1)
return false; // I could probably check more specific errors tbh
return (buf.st_mode & S_IFMT) == S_IFDIR;
} else return false;
}
/*
http://msdn.microsoft.com/en-us/library/windows/desktop/bb775947%28v=vs.85%29.aspx#check_boxes
http://msdn.microsoft.com/en-us/library/windows/desktop/ms633574%28v=vs.85%29.aspx
@ -13313,6 +13523,18 @@ mixin template Observable(T, string name) {
}
private bool startsWith(string test, string thing) {
if(test.length < thing.length)
return false;
return test[0 .. thing.length] == thing;
}
private bool endsWith(string test, string thing) {
if(test.length < thing.length)
return false;
return test[$ - thing.length .. $] == thing;
}
// still do layout delegation
// and... split off Window from Widget.