add exporting
This commit is contained in:
parent
a6e0fd9e67
commit
fdf6001068
@ -90,6 +90,11 @@ pub fn build(b: *std.Build) !void {
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
const datetime_dep = b.dependency("zig-datetime", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
const profiler_dep = b.dependency("profiler.zig", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
@ -124,6 +129,7 @@ pub fn build(b: *std.Build) !void {
|
||||
exe.root_module.addImport("cute_aseprite", cute_aseprite_lib);
|
||||
exe.root_module.addImport("known-folders", known_folders);
|
||||
exe.root_module.addImport("ini", ini);
|
||||
exe.root_module.addImport("datetime", datetime_dep.module("zig-datetime"));
|
||||
|
||||
// TODO: Add flag to disable in release
|
||||
exe.root_module.addImport("profiler", profiler_dep.module("profiler"));
|
||||
@ -145,6 +151,8 @@ pub fn build(b: *std.Build) !void {
|
||||
exe.step.dependOn(&add_icon_step.step);
|
||||
|
||||
exe.linkSystemLibrary("Comdlg32");
|
||||
exe.linkSystemLibrary("ole32");
|
||||
exe.linkSystemLibrary("uuid");
|
||||
}
|
||||
|
||||
b.installArtifact(exe);
|
||||
|
424
src/app.zig
424
src/app.zig
@ -10,6 +10,7 @@ const MainScreen = @import("./screens/main_screen.zig");
|
||||
const ChannelFromDeviceScreen = @import("./screens/channel_from_device.zig");
|
||||
const Platform = @import("./platform/root.zig");
|
||||
const constants = @import("./constants.zig");
|
||||
const datetime = @import("datetime").datetime;
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const log = std.log.scoped(.app);
|
||||
@ -685,7 +686,6 @@ pub const Channel = struct {
|
||||
|
||||
// Persistent
|
||||
name: Name = .{},
|
||||
saved_collected_samples: ?[]u8 = null,
|
||||
|
||||
// Runtime
|
||||
device: Device = .{},
|
||||
@ -694,21 +694,14 @@ pub const Channel = struct {
|
||||
allowed_sample_rates: ?RangeF64 = null,
|
||||
write_pattern: std.ArrayListUnmanaged(f64) = .{},
|
||||
output_task: ?NIDaq.Task = null,
|
||||
last_sample_save_at: ?i128 = null,
|
||||
|
||||
pub fn deinit(self: *Channel, allocator: Allocator) void {
|
||||
self.clear(allocator);
|
||||
|
||||
if (self.saved_collected_samples) |str| {
|
||||
allocator.free(str);
|
||||
self.saved_collected_samples = null;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(self: *Channel, allocator: Allocator) void {
|
||||
self.allowed_sample_rates = null;
|
||||
self.allowed_sample_values = null;
|
||||
self.last_sample_save_at = null;
|
||||
|
||||
self.write_pattern.clearAndFree(allocator);
|
||||
if (self.output_task) |task| {
|
||||
@ -736,24 +729,6 @@ pub const Channel = struct {
|
||||
samples.appendAssumeCapacity(sample);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn isSavedSampleFileStale(self: *const Channel, dir: std.fs.Dir) !bool {
|
||||
const path = self.saved_collected_samples orelse return false;
|
||||
const last_sample_save_at = self.last_sample_save_at orelse return true;
|
||||
|
||||
const stat = dir.statFile(path) catch |e| {
|
||||
if (e == error.FileNotFound) {
|
||||
return true;
|
||||
}
|
||||
return e;
|
||||
};
|
||||
|
||||
return stat.mtime != last_sample_save_at;
|
||||
}
|
||||
|
||||
pub fn invalidateSavedSamples(self: *Channel) void {
|
||||
self.last_sample_save_at = null;
|
||||
}
|
||||
};
|
||||
|
||||
pub const File = struct {
|
||||
@ -922,12 +897,22 @@ pub const View = struct {
|
||||
};
|
||||
|
||||
pub const Project = struct {
|
||||
pub const Solution = struct {
|
||||
description: []u8,
|
||||
sample: u64
|
||||
};
|
||||
|
||||
const file_format_version: u8 = 0;
|
||||
const file_endian = std.builtin.Endian.big;
|
||||
|
||||
save_location: ?[]u8 = null,
|
||||
sample_rate: ?f64 = null,
|
||||
sample_rate: ?f64 = 5000,
|
||||
notes: std.ArrayListUnmanaged(u8) = .{},
|
||||
experiment_name: std.ArrayListUnmanaged(u8) = .{},
|
||||
pipete_solution: std.ArrayListUnmanaged(u8) = .{},
|
||||
statistic_points: std.ArrayListUnmanaged(u64) = .{},
|
||||
export_location: ?[]u8 = null,
|
||||
|
||||
solutions: std.ArrayListUnmanaged(Solution) = .{},
|
||||
|
||||
sample_lists: GenerationalArray(SampleList) = .{},
|
||||
channels: GenerationalArray(Channel) = .{},
|
||||
@ -1081,6 +1066,25 @@ pub const Project = struct {
|
||||
return marked_range;
|
||||
}
|
||||
|
||||
pub fn getSampleTimestamp(self: *Project) ?u64 {
|
||||
var channel_iter = self.channels.iterator();
|
||||
while (channel_iter.next()) |channel| {
|
||||
const collected_samples = self.sample_lists.get(channel.collected_samples_id) orelse continue;
|
||||
return collected_samples.getLength();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn appendSolution(self: *Project, allocator: std.mem.Allocator, solution: []u8) !void {
|
||||
const current_sample = self.getSampleTimestamp() orelse 0;
|
||||
|
||||
try self.solutions.append(allocator, Solution{
|
||||
.description = try allocator.dupe(u8, solution),
|
||||
.sample = current_sample
|
||||
});
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Project, allocator: Allocator) void {
|
||||
var file_iter = self.files.iterator();
|
||||
while (file_iter.next()) |file| {
|
||||
@ -1102,21 +1106,39 @@ pub const Project = struct {
|
||||
}
|
||||
self.sample_lists.clear();
|
||||
|
||||
if (self.save_location) |str| {
|
||||
allocator.free(str);
|
||||
self.save_location = null;
|
||||
|
||||
if (self.solutions.capacity > 0) {
|
||||
for (self.solutions.items) |solution| {
|
||||
allocator.free(solution.description);
|
||||
}
|
||||
self.solutions.deinit(allocator);
|
||||
}
|
||||
|
||||
if (self.pipete_solution.capacity > 0) {
|
||||
self.pipete_solution.deinit(allocator);
|
||||
}
|
||||
|
||||
if (self.notes.capacity > 0) {
|
||||
self.notes.deinit(allocator);
|
||||
}
|
||||
|
||||
if (self.export_location) |export_location| {
|
||||
allocator.free(export_location);
|
||||
}
|
||||
|
||||
if (self.statistic_points.items.len > 0) {
|
||||
self.statistic_points.deinit(allocator);
|
||||
}
|
||||
|
||||
self.experiment_name.deinit(allocator);
|
||||
}
|
||||
|
||||
// ------------------- Serialization ------------------ //
|
||||
|
||||
pub fn initFromFile(self: *Project, allocator: Allocator, save_location: []const u8) !void {
|
||||
pub fn initFromFile(self: *Project, allocator: Allocator, f: std.fs.File) !void {
|
||||
self.* = .{};
|
||||
errdefer self.deinit(allocator);
|
||||
|
||||
const f = try std.fs.cwd().openFile(save_location, .{});
|
||||
defer f.close();
|
||||
|
||||
const reader = f.reader();
|
||||
|
||||
const version = try readInt(reader, u8);
|
||||
@ -1129,6 +1151,21 @@ pub const Project = struct {
|
||||
self.sample_rate = null;
|
||||
}
|
||||
|
||||
const export_location = try readString(reader, allocator);
|
||||
if (export_location.len > 0) {
|
||||
self.export_location = export_location;
|
||||
} else {
|
||||
allocator.free(export_location);
|
||||
}
|
||||
|
||||
const experiment_name = try readString(reader, allocator);
|
||||
defer allocator.free(experiment_name);
|
||||
try self.experiment_name.insertSlice(allocator, 0, experiment_name);
|
||||
|
||||
const pipete_solution = try readString(reader, allocator);
|
||||
defer allocator.free(pipete_solution);
|
||||
try self.pipete_solution.insertSlice(allocator, 0, pipete_solution);
|
||||
|
||||
{ // Channels
|
||||
const channel_count = try readInt(reader, u32);
|
||||
for (0..channel_count) |_| {
|
||||
@ -1139,20 +1176,12 @@ pub const Project = struct {
|
||||
const channel_name = try readString(reader, allocator);
|
||||
defer allocator.free(channel_name);
|
||||
|
||||
var saved_collected_samples: ?[]u8 = try readString(reader, allocator);
|
||||
if (saved_collected_samples.?.len == 0) {
|
||||
allocator.free(saved_collected_samples.?);
|
||||
saved_collected_samples = null;
|
||||
}
|
||||
errdefer if (saved_collected_samples) |str| allocator.free(str);
|
||||
|
||||
const sample_list_id = try self.addSampleList(allocator);
|
||||
errdefer self.removeSampleList(sample_list_id);
|
||||
|
||||
const channel = self.channels.get(id).?;
|
||||
channel.* = Channel{
|
||||
.name = try utils.initBoundedStringZ(Channel.Name, channel_name),
|
||||
.saved_collected_samples = saved_collected_samples,
|
||||
.collected_samples_id = sample_list_id
|
||||
};
|
||||
}
|
||||
@ -1204,113 +1233,60 @@ pub const Project = struct {
|
||||
view.* = View{
|
||||
.reference = reference,
|
||||
};
|
||||
|
||||
view.graph_opts.x_range = try readRangeF64(reader);
|
||||
view.graph_opts.y_range = try readRangeF64(reader);
|
||||
const sync_controls = try readInt(reader, u8);
|
||||
view.sync_controls = sync_controls == 1;
|
||||
|
||||
const marked_ranges_count = try readInt(reader, u32);
|
||||
for (0..marked_ranges_count) |_| {
|
||||
try view.marked_ranges.append(View.MarkedRange{
|
||||
.axis = try readEnum(reader, u8, UI.Axis),
|
||||
.range = try readRangeF64(reader)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.save_location = try allocator.dupe(u8, save_location);
|
||||
}
|
||||
|
||||
pub fn save(self: *Project) !void {
|
||||
const save_location = self.save_location orelse return error.NoSaveLocation;
|
||||
pub fn save(self: *Project, f: std.fs.File) !void {
|
||||
const writer = f.writer();
|
||||
|
||||
var save_tmp_location: std.BoundedArray(u8, std.fs.max_path_bytes) = .{};
|
||||
save_tmp_location.appendSliceAssumeCapacity(save_location);
|
||||
save_tmp_location.appendSliceAssumeCapacity("-tmp");
|
||||
try writeInt(writer, u8, file_format_version);
|
||||
try writeFloat(writer, f64, self.sample_rate orelse 0);
|
||||
try writeString(writer, self.export_location orelse &[_]u8{});
|
||||
try writeString(writer, self.experiment_name.items);
|
||||
try writeString(writer, self.pipete_solution.items);
|
||||
|
||||
const dir = std.fs.cwd();
|
||||
{ // Channels
|
||||
try writeInt(writer, u32, @intCast(self.channels.count()));
|
||||
var channel_iter = self.channels.idIterator();
|
||||
while (channel_iter.next()) |channel_id| {
|
||||
const channel = self.channels.get(channel_id).?;
|
||||
const channel_name = utils.getBoundedStringZ(&channel.name);
|
||||
|
||||
{
|
||||
const f = try dir.createFile(save_tmp_location.slice(), .{});
|
||||
defer f.close();
|
||||
|
||||
const writer = f.writer();
|
||||
|
||||
try writeInt(writer, u8, file_format_version);
|
||||
try writeFloat(writer, f64, self.sample_rate orelse 0);
|
||||
|
||||
{ // Channels
|
||||
try writeInt(writer, u32, @intCast(self.channels.count()));
|
||||
var channel_iter = self.channels.idIterator();
|
||||
while (channel_iter.next()) |channel_id| {
|
||||
const channel = self.channels.get(channel_id).?;
|
||||
const channel_name = utils.getBoundedStringZ(&channel.name);
|
||||
|
||||
try writeId(writer, channel_id);
|
||||
try writeString(writer, channel_name);
|
||||
try writeString(writer, channel.saved_collected_samples orelse &[0]u8{});
|
||||
|
||||
if (channel.saved_collected_samples != null and try channel.isSavedSampleFileStale(dir)) {
|
||||
const path = channel.saved_collected_samples.?;
|
||||
|
||||
{
|
||||
const samples_file = try dir.createFile(path, .{});
|
||||
defer samples_file.close();
|
||||
|
||||
const sample_list = self.sample_lists.get(channel.collected_samples_id).?;
|
||||
try sample_list.writeToFile(samples_file);
|
||||
}
|
||||
|
||||
const stat = try dir.statFile(path);
|
||||
channel.last_sample_save_at = stat.mtime;
|
||||
}
|
||||
}
|
||||
try writeId(writer, channel_id);
|
||||
try writeString(writer, channel_name);
|
||||
}
|
||||
}
|
||||
|
||||
{ // Files
|
||||
try writeInt(writer, u32, @intCast(self.files.count()));
|
||||
var file_iter = self.files.idIterator();
|
||||
while (file_iter.next()) |file_id| {
|
||||
const file = self.files.get(file_id).?;
|
||||
{ // Files
|
||||
try writeInt(writer, u32, @intCast(self.files.count()));
|
||||
var file_iter = self.files.idIterator();
|
||||
while (file_iter.next()) |file_id| {
|
||||
const file = self.files.get(file_id).?;
|
||||
|
||||
try writeId(writer, file_id);
|
||||
try writeString(writer, file.path);
|
||||
}
|
||||
try writeId(writer, file_id);
|
||||
try writeString(writer, file.path);
|
||||
}
|
||||
}
|
||||
|
||||
{ // Views
|
||||
try writeInt(writer, u32, @intCast(self.views.count()));
|
||||
var view_iter = self.views.idIterator();
|
||||
while (view_iter.next()) |view_id| {
|
||||
const view = self.views.get(view_id).?;
|
||||
{ // Views
|
||||
try writeInt(writer, u32, @intCast(self.views.count()));
|
||||
var view_iter = self.views.idIterator();
|
||||
while (view_iter.next()) |view_id| {
|
||||
const view = self.views.get(view_id).?;
|
||||
|
||||
try writeId(writer, view_id);
|
||||
try writeInt(writer, u8, @intFromEnum(view.reference));
|
||||
switch (view.reference) {
|
||||
.channel => |channel_id| {
|
||||
try writeInt(writer, u32, channel_id.asInt());
|
||||
},
|
||||
.file => |file_id| {
|
||||
try writeInt(writer, u32, file_id.asInt());
|
||||
}
|
||||
}
|
||||
|
||||
try writeRangeF64(writer, view.graph_opts.x_range);
|
||||
try writeRangeF64(writer, view.graph_opts.y_range);
|
||||
try writeInt(writer, u8, @intFromBool(view.sync_controls));
|
||||
|
||||
try writeInt(writer, u32, view.marked_ranges.len);
|
||||
for (view.marked_ranges.constSlice()) |marked_range| {
|
||||
try writeEnum(writer, u8, UI.Axis, marked_range.axis);
|
||||
try writeRangeF64(writer, marked_range.range);
|
||||
try writeId(writer, view_id);
|
||||
try writeInt(writer, u8, @intFromEnum(view.reference));
|
||||
switch (view.reference) {
|
||||
.channel => |channel_id| {
|
||||
try writeInt(writer, u32, channel_id.asInt());
|
||||
},
|
||||
.file => |file_id| {
|
||||
try writeInt(writer, u32, file_id.asInt());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try std.fs.rename(dir, save_tmp_location.slice(), dir, save_location);
|
||||
}
|
||||
|
||||
fn writeRangeF64(writer: anytype, range: RangeF64) !void {
|
||||
@ -1395,8 +1371,7 @@ pub const Project = struct {
|
||||
pub const Command = union(enum) {
|
||||
start_collection,
|
||||
stop_collection,
|
||||
save_project,
|
||||
load_project,
|
||||
export_project,
|
||||
stop_output: Id, // Channel id
|
||||
start_output: Id, // Channel id
|
||||
add_file_from_picker,
|
||||
@ -1660,7 +1635,7 @@ fn deinitUI(self: *App) void {
|
||||
self.channel_from_device.deinit();
|
||||
}
|
||||
|
||||
fn loadProject(self: *App) !void {
|
||||
pub fn loadProject(self: *App, file: std.fs.File) !void {
|
||||
if (self.isNiDaqInUse()) {
|
||||
log.warn("Attempt to load while collection is still in progress. Loading canceled", .{});
|
||||
return;
|
||||
@ -1669,16 +1644,14 @@ fn loadProject(self: *App) !void {
|
||||
self.collection_mutex.lock();
|
||||
defer self.collection_mutex.unlock();
|
||||
|
||||
const save_location = self.project.save_location orelse return error.MissingSaveLocation;
|
||||
|
||||
log.info("Load project from: {s}", .{save_location});
|
||||
log.info("Load project", .{});
|
||||
|
||||
var loaded = try self.allocator.create(Project);
|
||||
defer self.allocator.destroy(loaded);
|
||||
|
||||
loaded.* = .{};
|
||||
errdefer loaded.deinit(self.allocator);
|
||||
try loaded.initFromFile(self.allocator, save_location);
|
||||
try loaded.initFromFile(self.allocator, file);
|
||||
|
||||
self.deinitUI();
|
||||
self.deinitProject();
|
||||
@ -1709,7 +1682,7 @@ fn loadProject(self: *App) !void {
|
||||
self.initUI() catch @panic("Failed to initialize UI, can't recover");
|
||||
}
|
||||
|
||||
fn saveProject(self: *App) !void {
|
||||
pub fn saveProject(self: *App, file: std.fs.File) !void {
|
||||
if (self.isNiDaqInUse()) {
|
||||
log.warn("Attempt to save while collection is still in progress. Saving canceled", .{});
|
||||
return;
|
||||
@ -1718,11 +1691,9 @@ fn saveProject(self: *App) !void {
|
||||
self.collection_mutex.lock();
|
||||
defer self.collection_mutex.unlock();
|
||||
|
||||
const save_location = self.project.save_location orelse return error.MissingSaveLocation;
|
||||
log.info("Save project", .{});
|
||||
|
||||
log.info("Save project to: {s}", .{save_location});
|
||||
|
||||
try self.project.save();
|
||||
try self.project.save(file);
|
||||
}
|
||||
|
||||
pub fn showUI(self: *App) !void {
|
||||
@ -1737,6 +1708,96 @@ pub fn showUI(self: *App) !void {
|
||||
}
|
||||
}
|
||||
|
||||
fn exportProject(self: *App) !void {
|
||||
log.debug("Export", .{});
|
||||
|
||||
const project = self.project;
|
||||
const export_location = project.export_location orelse return error.NoExportLocation;
|
||||
const experiment_name = project.experiment_name.items;
|
||||
if (experiment_name.len == 0) {
|
||||
return error.NoExperimentName;
|
||||
}
|
||||
|
||||
var export_dir = try std.fs.openDirAbsolute(export_location, .{});
|
||||
defer export_dir.close();
|
||||
|
||||
const now = datetime.Datetime.now();
|
||||
|
||||
var maybe_export_filename: ?[]u8 = null;
|
||||
defer if (maybe_export_filename) |str| self.allocator.free(str);
|
||||
|
||||
for (1..10000) |i| {
|
||||
const export_filename = try std.fmt.allocPrint(self.allocator, "{s}_{}-{}-{}_{d:0>4}", .{ experiment_name, now.date.day, now.date.month, now.date.year, i });
|
||||
|
||||
const file_exists: bool = if (export_dir.access(export_filename, .{})) true else |_| false;
|
||||
if (file_exists) {
|
||||
self.allocator.free(export_filename);
|
||||
continue;
|
||||
}
|
||||
|
||||
maybe_export_filename = export_filename;
|
||||
break;
|
||||
}
|
||||
|
||||
const export_filename = maybe_export_filename orelse return error.NoFilename;
|
||||
|
||||
const sample_rate = self.project.sample_rate orelse 1;
|
||||
|
||||
var file = try export_dir.createFile(export_filename, .{});
|
||||
defer file.close();
|
||||
const writer = file.writer();
|
||||
|
||||
{
|
||||
try writer.writeAll(export_filename);
|
||||
try writer.writeAll("\n\n");
|
||||
}
|
||||
|
||||
{
|
||||
try writer.writeAll("Time Solution\n");
|
||||
for (self.project.solutions.items) |solution| {
|
||||
const total_seconds = @as(f64, @floatFromInt(solution.sample)) / sample_rate;
|
||||
|
||||
const text = try std.fmt.allocPrint(self.allocator, "{d:<8.2} {s}\n", .{total_seconds, solution.description});
|
||||
defer self.allocator.free(text);
|
||||
|
||||
_ = try writer.writeAll(text);
|
||||
}
|
||||
try writer.writeAll("\n");
|
||||
}
|
||||
|
||||
{
|
||||
try writer.writeAll("Time 100/Gain\n");
|
||||
// TODO:
|
||||
try writer.writeAll("\n");
|
||||
}
|
||||
|
||||
{
|
||||
try writer.writeAll("Time of Statistic points\n");
|
||||
for (project.statistic_points.items) |sample_timestamp| {
|
||||
const total_seconds = @as(f64, @floatFromInt(sample_timestamp)) / sample_rate;
|
||||
|
||||
const text = try std.fmt.allocPrint(self.allocator, "{d:.2}\n", .{total_seconds});
|
||||
defer self.allocator.free(text);
|
||||
|
||||
_ = try writer.writeAll(text);
|
||||
|
||||
}
|
||||
try writer.writeAll("\n");
|
||||
}
|
||||
|
||||
{
|
||||
try writer.writeAll("Pipete Solution:\n");
|
||||
try writer.writeAll(self.project.pipete_solution.notes.items);
|
||||
try writer.writeAll("\n");
|
||||
}
|
||||
|
||||
{
|
||||
try writer.writeAll("Note:\n");
|
||||
try writer.writeAll(self.project.notes.items);
|
||||
try writer.writeAll("\n");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tick(self: *App) !void {
|
||||
var ui = &self.ui;
|
||||
self.command_queue.len = 0;
|
||||
@ -1753,14 +1814,6 @@ pub fn tick(self: *App) !void {
|
||||
ui.pullOsEvents();
|
||||
}
|
||||
|
||||
if (ui.isCtrlDown() and ui.isKeyboardPressed(.key_s)) {
|
||||
self.pushCommand(.save_project);
|
||||
}
|
||||
|
||||
if (ui.isCtrlDown() and ui.isKeyboardPressed(.key_l)) {
|
||||
self.pushCommand(.load_project);
|
||||
}
|
||||
|
||||
{
|
||||
self.collection_samples_mutex.lock();
|
||||
defer self.collection_samples_mutex.unlock();
|
||||
@ -1821,17 +1874,22 @@ pub fn tick(self: *App) !void {
|
||||
.stop_collection => {
|
||||
self.stopCollection();
|
||||
},
|
||||
.save_project => {
|
||||
self.saveProject() catch |e| {
|
||||
log.err("Failed to save project: {}", .{e});
|
||||
};
|
||||
},
|
||||
.load_project => {
|
||||
self.loadProject() catch |e| {
|
||||
log.err("Failed to load project: {}", .{e});
|
||||
utils.dumpErrorTrace();
|
||||
.export_project => {
|
||||
self.exportProject() catch |e| {
|
||||
log.err("Failed to export project: {}", .{e});
|
||||
};
|
||||
},
|
||||
// .save_project => {
|
||||
// self.saveProject() catch |e| {
|
||||
// log.err("Failed to save project: {}", .{e});
|
||||
// };
|
||||
// },
|
||||
// .load_project => {
|
||||
// self.loadProject() catch |e| {
|
||||
// log.err("Failed to load project: {}", .{e});
|
||||
// utils.dumpErrorTrace();
|
||||
// };
|
||||
// },
|
||||
.add_file_from_picker => {
|
||||
self.addFileFromPicker() catch |e| {
|
||||
log.err("Failed to add file from picker: {}", .{e});
|
||||
@ -2182,21 +2240,6 @@ pub fn addChannel(self: *App, channel_name: []const u8) !Id {
|
||||
.collected_samples_id = sample_list_id
|
||||
};
|
||||
|
||||
if (self.project.save_location) |project_file_path| {
|
||||
if (std.fs.path.dirname(project_file_path)) |project_dir| {
|
||||
var clean_channel_name_buff = channel.name;
|
||||
const clean_channel_name = utils.getBoundedStringZ(&clean_channel_name_buff);
|
||||
|
||||
// Sanitize the channel name, because it will be used as a filename
|
||||
std.mem.replaceScalar(u8, clean_channel_name, '/', '_');
|
||||
|
||||
const filename = try std.mem.concat(self.allocator, u8, &.{ clean_channel_name, ".bin" });
|
||||
defer self.allocator.free(filename);
|
||||
|
||||
channel.saved_collected_samples = try std.fs.path.join(self.allocator, &.{ project_dir, filename });
|
||||
}
|
||||
}
|
||||
|
||||
self.loadChannel(id) catch |e| {
|
||||
log.err("Failed to load channel: {}", .{ e });
|
||||
};
|
||||
@ -2239,29 +2282,6 @@ fn getAllowedSampleRate(self: *App, id: Id) !RangeF64 {
|
||||
return RangeF64.init(min_sample_rate, max_sample_rate);
|
||||
}
|
||||
|
||||
fn loadSavedSamples(self: *App, channel_id: Id) !void {
|
||||
const channel = self.getChannel(channel_id) orelse return;
|
||||
const saved_samples_location = channel.saved_collected_samples orelse return;
|
||||
|
||||
const dir = std.fs.cwd();
|
||||
|
||||
const samples_file = try dir.openFile(saved_samples_location, .{ });
|
||||
defer samples_file.close();
|
||||
|
||||
const byte_count = try samples_file.getEndPos();
|
||||
if (byte_count % 8 != 0) {
|
||||
return error.NotMultipleOf8;
|
||||
}
|
||||
|
||||
const sample_list = self.project.sample_lists.get(channel.collected_samples_id).?;
|
||||
sample_list.clear(self.allocator);
|
||||
|
||||
try self.project.readSamplesFromFile(self.allocator, channel.collected_samples_id, samples_file, @divExact(byte_count, 8));
|
||||
|
||||
const stat = try dir.statFile(saved_samples_location);
|
||||
channel.last_sample_save_at = stat.mtime;
|
||||
}
|
||||
|
||||
pub fn loadChannel(self: *App, id: Id) !void {
|
||||
const channel = self.getChannel(id) orelse return;
|
||||
channel.clear(self.allocator);
|
||||
@ -2279,10 +2299,6 @@ pub fn loadChannel(self: *App, id: Id) !void {
|
||||
log.err("Failed to get allowed sample rate: {}", .{e});
|
||||
break :blk null;
|
||||
};
|
||||
|
||||
self.loadSavedSamples(id) catch |e| {
|
||||
log.warn("Failed to load saved samples on channel: {}", .{e});
|
||||
};
|
||||
}
|
||||
|
||||
pub fn isChannelOutputing(self: *App, id: Id) bool {
|
||||
|
@ -167,7 +167,6 @@ last_applied_command: usize = 0,
|
||||
zoom_start: ?ViewAxisPosition = null,
|
||||
cursor: ?ViewAxisPosition = null,
|
||||
view_settings: ?Id = null, // View id
|
||||
view_fullscreen: ?Id = null, // View id
|
||||
view_protocol_modal: ?Id = null, // View id
|
||||
selected_tool: enum { move, select, marker } = .move,
|
||||
show_marked_range: ?struct {
|
||||
@ -329,14 +328,6 @@ pub fn isViewSettingsOpen(self: *System, view_id: Id) bool {
|
||||
return self.view_settings != null and self.view_settings.?.eql(view_id);
|
||||
}
|
||||
|
||||
pub fn toggleFullscreenView(self: *System, view_id: Id) void {
|
||||
if (self.view_fullscreen == null or !self.view_fullscreen.?.eql(view_id)) {
|
||||
self.view_fullscreen = view_id;
|
||||
} else {
|
||||
self.view_fullscreen = null;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setCursor(self: *System, view_id: Id, axis: UI.Axis, position: ?f64) void {
|
||||
return ViewAxisPosition.set(&self.cursor, view_id, axis, position);
|
||||
}
|
||||
|
@ -319,19 +319,11 @@ pub fn show(ctx: Context, view_id: Id, height: UI.Sizing) !Result {
|
||||
container.beginChildren();
|
||||
defer container.endChildren();
|
||||
|
||||
const fullscreen = ui.createBox(.{
|
||||
.key = ui.keyFromString("Fullscreen toggle"),
|
||||
_ = ui.createBox(.{
|
||||
.size_x = ruler_size,
|
||||
.size_y = ruler_size,
|
||||
.background = srcery.hard_black,
|
||||
.hot_cursor = .mouse_cursor_pointing_hand,
|
||||
.flags = &.{ .draw_hot, .draw_active, .clickable },
|
||||
.texture = Assets.fullscreen,
|
||||
.texture_size = .{ .x = 28, .y = 28 }
|
||||
});
|
||||
if (ui.signal(fullscreen).clicked()) {
|
||||
ctx.view_controls.toggleFullscreenView(view_id);
|
||||
}
|
||||
|
||||
x_ruler = UIViewRuler.createBox(ruler_ctx, ui.keyFromString("X ruler"), .X);
|
||||
}
|
||||
|
59
src/main.zig
59
src/main.zig
@ -5,6 +5,7 @@ const Application = @import("./app.zig");
|
||||
const Assets = @import("./assets.zig");
|
||||
const Profiler = @import("./my-profiler.zig");
|
||||
const Platform = @import("./platform/root.zig");
|
||||
const known_folders = @import("known-folders");
|
||||
const raylib_h = @cImport({
|
||||
@cInclude("stdio.h");
|
||||
@cInclude("raylib.h");
|
||||
@ -71,7 +72,7 @@ pub fn main() !void {
|
||||
const allocator = gpa.allocator();
|
||||
defer _ = gpa.deinit();
|
||||
|
||||
Platform.init(allocator);
|
||||
try Platform.init(allocator);
|
||||
defer Platform.deinit(allocator);
|
||||
|
||||
// TODO: Setup logging to a file
|
||||
@ -82,6 +83,7 @@ pub fn main() !void {
|
||||
var icon_image = rl.loadImageFromMemory(".png", icon_png);
|
||||
defer icon_image.unload();
|
||||
|
||||
|
||||
rl.initWindow(800, 600, "DAQ view");
|
||||
defer rl.closeWindow();
|
||||
rl.setWindowState(.{ .window_resizable = true, .vsync_hint = true });
|
||||
@ -102,39 +104,26 @@ pub fn main() !void {
|
||||
try Application.init(&app, allocator);
|
||||
defer app.deinit();
|
||||
|
||||
if (builtin.mode == .Debug and false) {
|
||||
var cwd_realpath_buff: [std.fs.MAX_PATH_BYTES]u8 = undefined;
|
||||
const cwd_realpath = try std.fs.cwd().realpath(".", &cwd_realpath_buff);
|
||||
var config_dir = (try known_folders.open(allocator, .roaming_configuration, .{})) orelse return error.ConfigDirNotFound;
|
||||
defer config_dir.close();
|
||||
|
||||
const save_location = try std.fs.path.join(allocator, &.{ cwd_realpath, "project.proj" });
|
||||
errdefer allocator.free(save_location);
|
||||
var app_config_dir = config_dir.openDir("daq-view", .{}) catch |e| switch (e) {
|
||||
error.FileNotFound => blk: {
|
||||
try config_dir.makeDir("daq-view");
|
||||
break :blk try config_dir.openDir("daq-view", .{});
|
||||
},
|
||||
else => return e
|
||||
};
|
||||
defer app_config_dir.close();
|
||||
|
||||
app.project.save_location = save_location;
|
||||
app.project.sample_rate = 5000;
|
||||
|
||||
// _ = try app.addView(.{
|
||||
// .channel = try app.addChannel("Dev1/ai0")
|
||||
// });
|
||||
|
||||
// _ = try app.addView(.{
|
||||
// .file = try app.addFile("./samples-5k.bin")
|
||||
// });
|
||||
|
||||
// _ = try app.addView(.{
|
||||
// .file = try app.addFile("./samples-50k.bin")
|
||||
// });
|
||||
|
||||
// _ = try app.addView(.{
|
||||
// .file = try app.addFile("./samples-300k.bin")
|
||||
// });
|
||||
|
||||
// _ = try app.addView(.{
|
||||
// .file = try app.addFile("./samples-9m.bin")
|
||||
// });
|
||||
|
||||
// _ = try app.addView(.{
|
||||
// .file = try app.addFile("./samples-18m.bin")
|
||||
// });
|
||||
if (app_config_dir.openFile("config.bin", .{})) |save_file| {
|
||||
defer save_file.close();
|
||||
app.loadProject(save_file) catch |e| {
|
||||
log.err("Failed to load project: {}", .{e});
|
||||
};
|
||||
} else |e| switch (e) {
|
||||
error.FileNotFound => {},
|
||||
else => return e
|
||||
}
|
||||
|
||||
var profiler: ?Profiler = null;
|
||||
@ -183,6 +172,12 @@ pub fn main() !void {
|
||||
|
||||
rl.endDrawing();
|
||||
}
|
||||
|
||||
{
|
||||
const save_file = try app_config_dir.createFile("config.bin", .{});
|
||||
defer save_file.close();
|
||||
try app.saveProject(save_file);
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
|
@ -21,12 +21,14 @@ pub const OpenFileOptions = struct {
|
||||
};
|
||||
|
||||
pub const Style = enum { open, save };
|
||||
pub const Kind = enum { file, folder };
|
||||
|
||||
filters: std.BoundedArray(Filter, 4) = .{},
|
||||
default_filter: ?usize = null,
|
||||
file_must_exist: bool = false,
|
||||
prompt_creation: bool = false,
|
||||
prompt_overwrite: bool = false,
|
||||
kind: Kind = .file,
|
||||
style: Style = .open,
|
||||
|
||||
pub fn appendFilter(self: *OpenFileOptions, name: []const u8, format: []const u8) !void {
|
||||
@ -94,8 +96,8 @@ pub fn waitUntilFilePickerDone(allocator: std.mem.Allocator, optional_id: *?File
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) void {
|
||||
impl.init(allocator);
|
||||
pub fn init(allocator: std.mem.Allocator) !void {
|
||||
try impl.init(allocator);
|
||||
}
|
||||
|
||||
pub fn deinit(allocator: std.mem.Allocator) void {
|
||||
|
@ -2,7 +2,7 @@ const std = @import("std");
|
||||
const rl = @import("raylib");
|
||||
const Platform = @import("root.zig");
|
||||
const windows_h = @cImport({
|
||||
@cDefine("_WIN32_WINNT", "0x0500");
|
||||
@cDefine("_WIN32_WINNT", "0x0600");
|
||||
@cInclude("windows.h");
|
||||
@cInclude("shobjidl.h");
|
||||
});
|
||||
@ -48,10 +48,18 @@ const OPENFILENAMEW = extern struct {
|
||||
extern fn GetOpenFileNameW([*c]OPENFILENAMEW) windows_h.WINBOOL;
|
||||
extern fn GetSaveFileNameW([*c]OPENFILENAMEW) windows_h.WINBOOL;
|
||||
|
||||
const FileDialogShow = *const fn ([*c]windows_h.IFileDialog, HWND) callconv(.C) windows_h.HRESULT;
|
||||
|
||||
const CLSCTX_INPROC_SERVER = 0x1;
|
||||
const FOS_PICKFOLDERS = 0x00000020;
|
||||
const FOS_FORCEFILESYSTEM = 0x00000040;
|
||||
const SIGDN_FILESYSPATH: c_uint = 0x80058000;
|
||||
|
||||
const OpenedFilePicker = struct {
|
||||
kind: OpenFileOptions.Kind,
|
||||
style: OpenFileOptions.Style,
|
||||
filename_w_buffer: [std.os.windows.PATH_MAX_WIDE]u16,
|
||||
lpstrFilter_utf16: [:0]u16,
|
||||
lpstrFilter_utf16: ?[:0]u16 = null,
|
||||
ofn: OPENFILENAMEW,
|
||||
thread: std.Thread,
|
||||
status: Platform.FilePickerStatus,
|
||||
@ -63,60 +71,64 @@ const OpenedFilePicker = struct {
|
||||
.ofn = std.mem.zeroes(OPENFILENAMEW),
|
||||
.style = opts.style,
|
||||
.filename_w_buffer = undefined,
|
||||
.lpstrFilter_utf16 = undefined,
|
||||
.thread = undefined,
|
||||
.status = Platform.FilePickerStatus.in_progress
|
||||
.status = Platform.FilePickerStatus.in_progress,
|
||||
.kind = opts.kind
|
||||
};
|
||||
|
||||
const hWnd: HWND = @alignCast(@ptrCast(rl.getWindowHandle()));
|
||||
assert(hWnd != null);
|
||||
if (opts.kind == .file) {
|
||||
const hWnd: HWND = @alignCast(@ptrCast(rl.getWindowHandle()));
|
||||
assert(hWnd != null);
|
||||
|
||||
var lpstrFilter_utf8: std.ArrayListUnmanaged(u8) = .{};
|
||||
defer lpstrFilter_utf8.deinit(allocator);
|
||||
var lpstrFilter_utf8: std.ArrayListUnmanaged(u8) = .{};
|
||||
defer lpstrFilter_utf8.deinit(allocator);
|
||||
|
||||
for (opts.filters.constSlice()) |filter| {
|
||||
try lpstrFilter_utf8.appendSlice(allocator, filter.name.slice());
|
||||
try lpstrFilter_utf8.append(allocator, 0);
|
||||
try lpstrFilter_utf8.appendSlice(allocator, filter.format.slice());
|
||||
try lpstrFilter_utf8.append(allocator, 0);
|
||||
for (opts.filters.constSlice()) |filter| {
|
||||
try lpstrFilter_utf8.appendSlice(allocator, filter.name.slice());
|
||||
try lpstrFilter_utf8.append(allocator, 0);
|
||||
try lpstrFilter_utf8.appendSlice(allocator, filter.format.slice());
|
||||
try lpstrFilter_utf8.append(allocator, 0);
|
||||
}
|
||||
|
||||
const lpstrFilter_utf16 = try std.unicode.utf8ToUtf16LeWithNull(allocator, lpstrFilter_utf8.items);
|
||||
errdefer allocator.free(lpstrFilter_utf16);
|
||||
self.lpstrFilter_utf16 = lpstrFilter_utf16;
|
||||
|
||||
var flags: c_ulong = 0;
|
||||
{
|
||||
flags |= windows_h.OFN_NOCHANGEDIR;
|
||||
flags |= windows_h.OFN_PATHMUSTEXIST;
|
||||
flags |= windows_h.OFN_EXPLORER;
|
||||
flags |= windows_h.OFN_LONGNAMES;
|
||||
flags |= windows_h.OFN_ENABLESIZING;
|
||||
|
||||
if (opts.file_must_exist) {
|
||||
flags |= windows_h.OFN_FILEMUSTEXIST;
|
||||
}
|
||||
if (opts.prompt_creation) {
|
||||
flags |= windows_h.OFN_CREATEPROMPT;
|
||||
}
|
||||
if (opts.prompt_overwrite) {
|
||||
flags |= windows_h.OFN_OVERWRITEPROMPT;
|
||||
}
|
||||
}
|
||||
|
||||
var nFilterIndex: c_ulong = 0;
|
||||
if (opts.default_filter) |default_filter| {
|
||||
assert(default_filter < opts.filters.len);
|
||||
nFilterIndex = @intCast(default_filter + 1);
|
||||
}
|
||||
|
||||
@memset(&self.filename_w_buffer, 0);
|
||||
self.ofn.lStructSize = @sizeOf(@TypeOf(self.ofn));
|
||||
self.ofn.hwndOwner = hWnd;
|
||||
self.ofn.lpstrFile = &self.filename_w_buffer;
|
||||
self.ofn.nMaxFile = self.filename_w_buffer.len;
|
||||
self.ofn.lpstrFilter = lpstrFilter_utf16;
|
||||
self.ofn.nFilterIndex = nFilterIndex;
|
||||
self.ofn.Flags = flags;
|
||||
}
|
||||
|
||||
self.lpstrFilter_utf16 = try std.unicode.utf8ToUtf16LeWithNull(allocator, lpstrFilter_utf8.items);
|
||||
errdefer allocator.free(self.lpstrFilter_utf16);
|
||||
|
||||
var flags: c_ulong = 0;
|
||||
{
|
||||
flags |= windows_h.OFN_NOCHANGEDIR;
|
||||
flags |= windows_h.OFN_PATHMUSTEXIST;
|
||||
flags |= windows_h.OFN_EXPLORER;
|
||||
flags |= windows_h.OFN_LONGNAMES;
|
||||
flags |= windows_h.OFN_ENABLESIZING;
|
||||
|
||||
if (opts.file_must_exist) {
|
||||
flags |= windows_h.OFN_FILEMUSTEXIST;
|
||||
}
|
||||
if (opts.prompt_creation) {
|
||||
flags |= windows_h.OFN_CREATEPROMPT;
|
||||
}
|
||||
if (opts.prompt_overwrite) {
|
||||
flags |= windows_h.OFN_OVERWRITEPROMPT;
|
||||
}
|
||||
}
|
||||
|
||||
var nFilterIndex: c_ulong = 0;
|
||||
if (opts.default_filter) |default_filter| {
|
||||
assert(default_filter < opts.filters.len);
|
||||
nFilterIndex = @intCast(default_filter + 1);
|
||||
}
|
||||
|
||||
@memset(&self.filename_w_buffer, 0);
|
||||
self.ofn.lStructSize = @sizeOf(@TypeOf(self.ofn));
|
||||
self.ofn.hwndOwner = hWnd;
|
||||
self.ofn.lpstrFile = &self.filename_w_buffer;
|
||||
self.ofn.nMaxFile = self.filename_w_buffer.len;
|
||||
self.ofn.lpstrFilter = self.lpstrFilter_utf16;
|
||||
self.ofn.nFilterIndex = nFilterIndex;
|
||||
self.ofn.Flags = flags;
|
||||
|
||||
// Creating this thread must be the last thing that this function does.
|
||||
// Because if an error occurs after a thread is spawn, it will not be joined/stopped
|
||||
@ -125,35 +137,95 @@ const OpenedFilePicker = struct {
|
||||
|
||||
fn deinit(self: *OpenedFilePicker, allocator: std.mem.Allocator) void {
|
||||
self.thread.join();
|
||||
allocator.free(self.lpstrFilter_utf16);
|
||||
if (self.lpstrFilter_utf16) |lpstrFilter_utf16| {
|
||||
allocator.free(lpstrFilter_utf16);
|
||||
}
|
||||
}
|
||||
|
||||
fn showDialog(self: *OpenedFilePicker) !void {
|
||||
const result = switch (self.style) {
|
||||
.open => GetOpenFileNameW(&self.ofn),
|
||||
.save => GetSaveFileNameW(&self.ofn)
|
||||
};
|
||||
if (self.kind == .file) {
|
||||
const result = switch (self.style) {
|
||||
.open => GetOpenFileNameW(&self.ofn),
|
||||
.save => GetSaveFileNameW(&self.ofn)
|
||||
};
|
||||
|
||||
if (result != windows_h.TRUE) {
|
||||
const err = windows_h.CommDlgExtendedError();
|
||||
if (err == 0) {
|
||||
if (result != windows_h.TRUE) {
|
||||
const err = windows_h.CommDlgExtendedError();
|
||||
if (err == 0) {
|
||||
return error.Canceled;
|
||||
}
|
||||
|
||||
// From the MSDN docs, these 3 error should only be able to occur
|
||||
// Source: https://learn.microsoft.com/en-us/windows/win32/api/commdlg/nf-commdlg-commdlgextendederror
|
||||
if (err == windows_h.FNERR_BUFFERTOOSMALL) {
|
||||
return error.BufferTooSmall;
|
||||
|
||||
} else if (err == windows_h.FNERR_INVALIDFILENAME) {
|
||||
return error.InvalidFilename;
|
||||
|
||||
} else if (err == windows_h.FNERR_SUBCLASSFAILURE) {
|
||||
return error.OutOfMemory;
|
||||
|
||||
} else {
|
||||
log.err("Unknown file picker error occured: {}", .{ err });
|
||||
return error.UnknownFileDialogError;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
var maybe_file_dialog: ?*windows_h.IFileDialog = null;
|
||||
const hr_create = windows_h.CoCreateInstance(
|
||||
&windows_h.CLSID_FileOpenDialog,
|
||||
null,
|
||||
CLSCTX_INPROC_SERVER,
|
||||
&windows_h.IID_IFileDialog,
|
||||
@ptrCast(&maybe_file_dialog),
|
||||
);
|
||||
|
||||
if (windows_h.FAILED(hr_create) or maybe_file_dialog == null) {
|
||||
log.err("Failed to create FileOpenDialog (HRESULT: {x})", .{hr_create});
|
||||
return error.CoCreateInstance;
|
||||
}
|
||||
|
||||
const file_dialog = maybe_file_dialog.?;
|
||||
const lpVtbl = maybe_file_dialog.?.lpVtbl[0];
|
||||
|
||||
defer _ = lpVtbl.Release.?(file_dialog);
|
||||
|
||||
var options: windows_h.DWORD = 0;
|
||||
if (windows_h.FAILED(lpVtbl.GetOptions.?(file_dialog, &options))) {
|
||||
log.err("Failed to get options", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
_ = lpVtbl.SetOptions.?(
|
||||
file_dialog,
|
||||
options | FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM,
|
||||
);
|
||||
|
||||
const hWnd: HWND = @alignCast(@ptrCast(rl.getWindowHandle()));
|
||||
assert(hWnd != null);
|
||||
|
||||
const lpVtblShow: FileDialogShow = @ptrCast(lpVtbl.Show.?);
|
||||
const hr_show = lpVtblShow(file_dialog, hWnd);
|
||||
if (!windows_h.SUCCEEDED(hr_show)) {
|
||||
log.warn("Dialog cancelled or failed (HRESULT: {x})", .{hr_show});
|
||||
return error.Canceled;
|
||||
}
|
||||
|
||||
// From the MSDN docs, these 3 error should only be able to occur
|
||||
// Source: https://learn.microsoft.com/en-us/windows/win32/api/commdlg/nf-commdlg-commdlgextendederror
|
||||
if (err == windows_h.FNERR_BUFFERTOOSMALL) {
|
||||
return error.BufferTooSmall;
|
||||
var item: ?*windows_h.IShellItem = null;
|
||||
const hr_result = lpVtbl.GetResult.?(file_dialog, &item);
|
||||
if (windows_h.SUCCEEDED(hr_result) and item != null) {
|
||||
defer _ = item.?.lpVtbl[0].Release.?(item.?);
|
||||
|
||||
} else if (err == windows_h.FNERR_INVALIDFILENAME) {
|
||||
return error.InvalidFilename;
|
||||
var path: windows_h.LPWSTR = null;
|
||||
const hr_display = item.?.lpVtbl[0].GetDisplayName.?(item.?, @bitCast(SIGDN_FILESYSPATH), &path);
|
||||
if (windows_h.SUCCEEDED(hr_display) and path != null) {
|
||||
defer windows_h.CoTaskMemFree(path);
|
||||
|
||||
} else if (err == windows_h.FNERR_SUBCLASSFAILURE) {
|
||||
return error.OutOfMemory;
|
||||
|
||||
} else {
|
||||
log.err("Unknown file picker error occured: {}", .{ err });
|
||||
return error.UnknownFileDialogError;
|
||||
const path_slice = std.mem.sliceTo(path, 0);
|
||||
@memcpy(self.filename_w_buffer[0..path_slice.len], path_slice);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -285,9 +357,16 @@ pub fn isFilePickerOpen(self: *Self) bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn init(self: *Self, allocator: std.mem.Allocator) void {
|
||||
pub fn init(self: *Self, allocator: std.mem.Allocator) !void {
|
||||
_ = windows_h.SetConsoleOutputCP(65001);
|
||||
|
||||
const COINIT_APARTMENTTHREADED = 0x2;
|
||||
|
||||
if (windows_h.CoInitializeEx(null, COINIT_APARTMENTTHREADED) != windows_h.S_OK) {
|
||||
log.err("Failed to initialize COM", .{});
|
||||
return error.CoInitialize;
|
||||
}
|
||||
|
||||
self.* = Self{
|
||||
.allocator = allocator
|
||||
};
|
||||
@ -300,4 +379,6 @@ pub fn deinit(self: *Self, allocator: std.mem.Allocator) void {
|
||||
maybe_opened_file_picker.* = null;
|
||||
}
|
||||
}
|
||||
|
||||
windows_h.CoUninitialize();
|
||||
}
|
@ -222,6 +222,9 @@ pub fn tick(self: *Screen) !void {
|
||||
.channel = try self.app.addChannel(channel)
|
||||
});
|
||||
}
|
||||
|
||||
_ = self.channel_names.reset(.free_all);
|
||||
self.selected_channels.len = 0;
|
||||
}
|
||||
|
||||
} else {
|
||||
|
@ -22,6 +22,8 @@ const Id = App.Id;
|
||||
|
||||
app: *App,
|
||||
view_controls: ViewControlsSystem,
|
||||
view_solutions: bool = false,
|
||||
view_statistics_points: bool = false,
|
||||
modal: ?union(enum){
|
||||
view_protocol: Id, // View id
|
||||
notes
|
||||
@ -39,7 +41,11 @@ preview_samples_y_range: RangeF64 = RangeF64.init(0, 0),
|
||||
notes_storage: UI.TextInputStorage,
|
||||
|
||||
// Project settings
|
||||
solution_input: UI.TextInputStorage,
|
||||
sample_rate_input: UI.TextInputStorage,
|
||||
experiment_name: UI.TextInputStorage,
|
||||
pipete_solution: UI.TextInputStorage,
|
||||
export_location_picker: ?Platform.FilePickerId = null,
|
||||
parsed_sample_rate: ?f64 = null,
|
||||
|
||||
// View settings
|
||||
@ -61,6 +67,9 @@ pub fn init(app: *App) !MainScreen {
|
||||
.amplitude_input = UI.TextInputStorage.init(allocator),
|
||||
.sample_rate_input = UI.TextInputStorage.init(allocator),
|
||||
.notes_storage = UI.TextInputStorage.init(allocator),
|
||||
.solution_input = UI.TextInputStorage.init(allocator),
|
||||
.experiment_name = UI.TextInputStorage.init(allocator),
|
||||
.pipete_solution = UI.TextInputStorage.init(allocator),
|
||||
.view_controls = ViewControlsSystem.init(&app.project),
|
||||
.transform_inputs = transform_inputs,
|
||||
.preview_sample_list_id = try app.project.addSampleList(allocator)
|
||||
@ -77,6 +86,9 @@ pub fn deinit(self: *MainScreen) void {
|
||||
self.amplitude_input.deinit();
|
||||
self.sample_rate_input.deinit();
|
||||
self.notes_storage.deinit();
|
||||
self.solution_input.deinit();
|
||||
self.experiment_name.deinit();
|
||||
self.pipete_solution.deinit();
|
||||
for (self.transform_inputs) |input| {
|
||||
input.deinit();
|
||||
}
|
||||
@ -276,13 +288,20 @@ fn showNotesModal(self: *MainScreen) !void {
|
||||
label.alignment.x = .center;
|
||||
label.size.x = UI.Sizing.initGrowFull();
|
||||
|
||||
_ = try ui.textInput(.{
|
||||
try ui.textInput(.{
|
||||
.key = ui.keyFromString("Notes"),
|
||||
.storage = &self.notes_storage,
|
||||
.size_x = UI.Sizing.initGrowFull(),
|
||||
.size_y = UI.Sizing.initGrowFull(),
|
||||
.single_line = false
|
||||
});
|
||||
|
||||
if (self.notes_storage.modified) {
|
||||
const project = &self.app.project;
|
||||
|
||||
project.notes.clearRetainingCapacity();
|
||||
try project.notes.insertSlice(self.app.allocator, 0, self.notes_storage.buffer.items);
|
||||
}
|
||||
}
|
||||
|
||||
fn setProtocolErrorMessage(self: *MainScreen, comptime fmt: []const u8, args: anytype) !void {
|
||||
@ -316,13 +335,120 @@ fn showProjectSettings(self: *MainScreen) !void {
|
||||
|
||||
_ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) });
|
||||
|
||||
{
|
||||
_ = ui.label("Experiment:", .{});
|
||||
try ui.textInput(.{
|
||||
.key = ui.keyFromString("Experiment name"),
|
||||
.storage = &self.experiment_name,
|
||||
.initial = project.experiment_name.items,
|
||||
.size_x = UI.Sizing.initGrowFull()
|
||||
});
|
||||
|
||||
if (self.experiment_name.modified) {
|
||||
project.experiment_name.clearRetainingCapacity();
|
||||
try project.experiment_name.insertSlice(self.app.allocator, 0, self.experiment_name.buffer.items);
|
||||
}
|
||||
}
|
||||
|
||||
_ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(0.5)) });
|
||||
|
||||
{
|
||||
_ = ui.label("Pipete solution:", .{});
|
||||
try ui.textInput(.{
|
||||
.key = ui.keyFromString("Pipete solution"),
|
||||
.storage = &self.pipete_solution,
|
||||
.initial = project.pipete_solution.items,
|
||||
.size_x = UI.Sizing.initGrowFull()
|
||||
});
|
||||
|
||||
if (self.pipete_solution.modified) {
|
||||
project.pipete_solution.clearRetainingCapacity();
|
||||
try project.pipete_solution.insertSlice(self.app.allocator, 0, self.pipete_solution.buffer.items);
|
||||
}
|
||||
}
|
||||
|
||||
_ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(0.5)) });
|
||||
|
||||
{
|
||||
_ = ui.label("Solution:", .{});
|
||||
|
||||
try ui.textInput(.{
|
||||
.key = ui.keyFromString("Solution input"),
|
||||
.storage = &self.solution_input,
|
||||
.size_x = UI.Sizing.initGrowFull()
|
||||
});
|
||||
|
||||
{
|
||||
const row = ui.createBox(.{
|
||||
.size_x = UI.Sizing.initFitChildren(),
|
||||
.size_y = UI.Sizing.initFitChildren(),
|
||||
.layout_direction = .left_to_right,
|
||||
.layout_gap = ui.rem(1)
|
||||
});
|
||||
row.beginChildren();
|
||||
defer row.endChildren();
|
||||
|
||||
{
|
||||
const btn = ui.textButton("Append solution");
|
||||
btn.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 });
|
||||
if (ui.signal(btn).clicked()) {
|
||||
if (self.solution_input.buffer.items.len > 0) {
|
||||
try project.appendSolution(self.app.allocator, self.solution_input.buffer.items);
|
||||
}
|
||||
self.solution_input.clear();
|
||||
}
|
||||
}
|
||||
|
||||
if (project.solutions.items.len > 0) {
|
||||
const btn = ui.textButton("View solutions");
|
||||
btn.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 });
|
||||
if (ui.signal(btn).clicked()) {
|
||||
self.view_solutions = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
_ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(0.5)) });
|
||||
|
||||
{
|
||||
const row = ui.createBox(.{
|
||||
.size_x = UI.Sizing.initFitChildren(),
|
||||
.size_y = UI.Sizing.initFitChildren(),
|
||||
.layout_direction = .left_to_right,
|
||||
.layout_gap = ui.rem(0.5)
|
||||
});
|
||||
row.beginChildren();
|
||||
defer row.endChildren();
|
||||
|
||||
{
|
||||
const btn = ui.textButton("Add statistic point");
|
||||
btn.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 });
|
||||
if (ui.signal(btn).clicked()) {
|
||||
const current_sample = project.getSampleTimestamp() orelse 0;
|
||||
try project.statistic_points.append(self.app.allocator, current_sample);
|
||||
}
|
||||
}
|
||||
|
||||
if (project.statistic_points.items.len > 0) {
|
||||
const btn = ui.textButton("View statistic points");
|
||||
btn.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 });
|
||||
if (ui.signal(btn).clicked()) {
|
||||
self.view_statistics_points = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(0.5)) });
|
||||
|
||||
{ // Sample rate
|
||||
var placeholder: ?[]const u8 = null;
|
||||
if (project.getDefaultSampleRate()) |default_sample_rate| {
|
||||
placeholder = try std.fmt.allocPrint(frame_allocator, "{d}", .{ default_sample_rate });
|
||||
}
|
||||
|
||||
_ = ui.label("Sample rate", .{});
|
||||
_ = ui.label("Sample rate:", .{});
|
||||
self.parsed_sample_rate = try ui.numberInput(f64, .{
|
||||
.key = ui.keyFromString("Sample rate input"),
|
||||
.storage = &self.sample_rate_input,
|
||||
@ -342,8 +468,43 @@ fn showProjectSettings(self: *MainScreen) !void {
|
||||
}
|
||||
}
|
||||
|
||||
if (ui.signal(ui.textButton("Open notes")).clicked()) {
|
||||
self.modal = .notes;
|
||||
_ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(0.5)) });
|
||||
|
||||
{
|
||||
const btn = ui.textButton("Open notes");
|
||||
btn.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 });
|
||||
if (ui.signal(btn).clicked()) {
|
||||
self.modal = .notes;
|
||||
}
|
||||
}
|
||||
|
||||
_ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(0.5)) });
|
||||
|
||||
{
|
||||
_ = ui.label("Export folder:", .{});
|
||||
if (ui.fileInput(.{
|
||||
.allocator = self.app.allocator,
|
||||
.key = ui.keyFromString("Export location"),
|
||||
.path = project.export_location,
|
||||
.file_picker = &self.export_location_picker,
|
||||
.open_dialog = true,
|
||||
.folder = true
|
||||
})) |path| {
|
||||
if (project.export_location) |str| {
|
||||
self.app.allocator.free(str);
|
||||
}
|
||||
project.export_location = path;
|
||||
}
|
||||
|
||||
const btn = ui.textButton("Export");
|
||||
if (self.app.isCollectionInProgress() or project.export_location == null) {
|
||||
btn.borders = UI.Borders.all(.{ .color = srcery.hard_black, .size = 4 });
|
||||
} else {
|
||||
btn.borders = UI.Borders.all(.{ .color = srcery.green, .size = 4 });
|
||||
if (ui.signal(btn).clicked()) {
|
||||
self.app.pushCommand(.export_project);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -383,20 +544,6 @@ fn showViewSettings(self: *MainScreen, view_id: Id) !void {
|
||||
} else {
|
||||
_ = ui.label("Type: unknown", .{ });
|
||||
}
|
||||
|
||||
if (ui.fileInput(.{
|
||||
.key = ui.keyFromString("Save location"),
|
||||
.allocator = self.app.allocator,
|
||||
.file_picker = &self.channel_save_file_picker,
|
||||
.path = channel.saved_collected_samples,
|
||||
.open_dialog = false
|
||||
})) |path| {
|
||||
if (channel.saved_collected_samples) |current_path| {
|
||||
self.app.allocator.free(current_path);
|
||||
}
|
||||
|
||||
channel.saved_collected_samples = path;
|
||||
}
|
||||
},
|
||||
.file => |file_id| {
|
||||
const file = project.files.get(file_id).?;
|
||||
@ -686,14 +833,6 @@ fn showToolbar(self: *MainScreen) void {
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var btn = ui.textButton("Save");
|
||||
btn.borders = UI.Borders.all(.{ .size = 4, .color = srcery.green });
|
||||
if (ui.signal(btn).clicked()) {
|
||||
self.app.pushCommand(.save_project);
|
||||
}
|
||||
}
|
||||
|
||||
_ = ui.createBox(.{ .size_x = UI.Sizing.initFixedPixels(ui.rem(1)) });
|
||||
|
||||
{
|
||||
@ -730,6 +869,102 @@ fn showToolbar(self: *MainScreen) void {
|
||||
}
|
||||
}
|
||||
|
||||
fn showSolutions(self: *MainScreen) !void {
|
||||
var ui = &self.app.ui;
|
||||
const project = &self.app.project;
|
||||
_ = &ui;
|
||||
|
||||
{
|
||||
const label = ui.label("Solutions", .{});
|
||||
label.borders.bottom = .{
|
||||
.color = srcery.white,
|
||||
.size = 1
|
||||
};
|
||||
|
||||
_ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) });
|
||||
}
|
||||
|
||||
const sample_rate = project.getSampleRate();
|
||||
|
||||
for (project.solutions.items) |solution| {
|
||||
const row = ui.createBox(.{
|
||||
.size_x = UI.Sizing.initGrowFull(),
|
||||
.size_y = UI.Sizing.initFitChildren(),
|
||||
.layout_direction = .left_to_right,
|
||||
.layout_gap = ui.rem(1)
|
||||
});
|
||||
row.beginChildren();
|
||||
defer row.endChildren();
|
||||
|
||||
if (sample_rate != null) {
|
||||
const seconds = @as(f64, @floatFromInt(solution.sample)) / sample_rate.?;
|
||||
_ = ui.label("{s}", .{ try utils.formatDuration(ui.frameAllocator(), seconds) });
|
||||
} else {
|
||||
_ = ui.label("{d}", .{ solution.sample });
|
||||
}
|
||||
|
||||
var description = ui.label("{s}", .{ solution.description });
|
||||
description.size.x = UI.Sizing.initGrowFull();
|
||||
description.flags.insert(.wrap_text);
|
||||
}
|
||||
|
||||
_ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) });
|
||||
|
||||
{
|
||||
const btn = ui.textButton("Close");
|
||||
btn.borders = UI.Borders.all(.{ .color = srcery.red, .size = 4 });
|
||||
if (ui.signal(btn).clicked()) {
|
||||
self.view_solutions = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn showStatisticsPoints(self: *MainScreen) !void {
|
||||
var ui = &self.app.ui;
|
||||
const project = &self.app.project;
|
||||
_ = &ui;
|
||||
|
||||
{
|
||||
const label = ui.label("Statistics points", .{});
|
||||
label.borders.bottom = .{
|
||||
.color = srcery.white,
|
||||
.size = 1
|
||||
};
|
||||
|
||||
_ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) });
|
||||
}
|
||||
|
||||
const sample_rate = project.getSampleRate();
|
||||
|
||||
for (project.statistic_points.items) |sample_timestamp| {
|
||||
const row = ui.createBox(.{
|
||||
.size_x = UI.Sizing.initGrowFull(),
|
||||
.size_y = UI.Sizing.initFitChildren(),
|
||||
.layout_direction = .left_to_right,
|
||||
.layout_gap = ui.rem(1)
|
||||
});
|
||||
row.beginChildren();
|
||||
defer row.endChildren();
|
||||
|
||||
if (sample_rate != null) {
|
||||
const seconds = @as(f64, @floatFromInt(sample_timestamp)) / sample_rate.?;
|
||||
_ = ui.label("{s}", .{ try utils.formatDuration(ui.frameAllocator(), seconds) });
|
||||
} else {
|
||||
_ = ui.label("{d}", .{ sample_timestamp });
|
||||
}
|
||||
}
|
||||
|
||||
_ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) });
|
||||
|
||||
{
|
||||
const btn = ui.textButton("Close");
|
||||
btn.borders = UI.Borders.all(.{ .color = srcery.red, .size = 4 });
|
||||
if (ui.signal(btn).clicked()) {
|
||||
self.view_statistics_points = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn showSidePanel(self: *MainScreen) !void {
|
||||
var ui = &self.app.ui;
|
||||
|
||||
@ -748,12 +983,16 @@ pub fn showSidePanel(self: *MainScreen) !void {
|
||||
|
||||
_ = ui.createBox(.{ .size_x = UI.Sizing.initFixedPixels(ui.rem(18)) });
|
||||
|
||||
if (self.view_controls.show_marker) |marker| {
|
||||
if (self.view_solutions) {
|
||||
try self.showSolutions();
|
||||
} else if (self.view_controls.show_marker) |marker| {
|
||||
self.showMarker(marker.view_id, marker.index);
|
||||
} else if (self.view_controls.show_marked_range) |show_marked_range| {
|
||||
self.showMarkedRange(show_marked_range.view_id, show_marked_range.index);
|
||||
} else if (self.view_controls.view_settings) |view_id| {
|
||||
try self.showViewSettings(view_id);
|
||||
} else if (self.view_statistics_points) {
|
||||
try self.showStatisticsPoints();
|
||||
} else {
|
||||
try self.showProjectSettings();
|
||||
}
|
||||
@ -819,10 +1058,7 @@ pub fn tick(self: *MainScreen) !void {
|
||||
.view_controls = &self.view_controls
|
||||
};
|
||||
|
||||
if (self.view_controls.view_fullscreen) |view_id| {
|
||||
_ = try UIView.show(ui_view_ctx, view_id, UI.Sizing.initGrowFull());
|
||||
|
||||
} else {
|
||||
{
|
||||
const container = ui.createBox(.{
|
||||
.size_x = UI.Sizing.initGrowFull(),
|
||||
.size_y = UI.Sizing.initGrowFull(),
|
||||
@ -877,12 +1113,14 @@ pub fn tick(self: *MainScreen) !void {
|
||||
if (ui.isKeyboardPressed(.key_escape)) {
|
||||
if (self.modal != null) {
|
||||
self.modal = null;
|
||||
} else if (self.view_controls.view_fullscreen != null) {
|
||||
self.view_controls.view_fullscreen = null;
|
||||
} else if (self.view_controls.view_settings != null) {
|
||||
self.view_controls.view_settings = null;
|
||||
} else if (self.view_controls.show_marked_range != null) {
|
||||
self.view_controls.show_marked_range = null;
|
||||
} else if (self.view_solutions) {
|
||||
self.view_solutions = false;
|
||||
} else if (self.view_statistics_points) {
|
||||
self.view_statistics_points = false;
|
||||
} else {
|
||||
self.app.should_close = true;
|
||||
}
|
||||
|
15
src/ui.zig
15
src/ui.zig
@ -2200,6 +2200,10 @@ pub const TextInputStorage = struct {
|
||||
self.buffer.deinit();
|
||||
}
|
||||
|
||||
pub fn clear(self: *TextInputStorage) void {
|
||||
self.deleteMany(0, self.buffer.items.len);
|
||||
}
|
||||
|
||||
pub fn setText(self: *TextInputStorage, text: []const u8) !void {
|
||||
self.modified = true;
|
||||
|
||||
@ -2497,6 +2501,7 @@ pub const FileInputOptions = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
file_picker: *?Platform.FilePickerId,
|
||||
open_dialog: bool = true,
|
||||
folder: bool = false,
|
||||
path: ?[]const u8 = null
|
||||
};
|
||||
|
||||
@ -3112,8 +3117,14 @@ pub fn fileInput(self: *UI, opts: FileInputOptions) ?[]u8 {
|
||||
file_open_options.style = .save;
|
||||
file_open_options.prompt_overwrite = true;
|
||||
}
|
||||
file_open_options.appendFilter("All", "*") catch unreachable;
|
||||
file_open_options.appendFilter("Binary", "*.bin") catch unreachable;
|
||||
|
||||
if (opts.folder) {
|
||||
file_open_options.kind = .folder;
|
||||
} else {
|
||||
file_open_options.kind = .file;
|
||||
file_open_options.appendFilter("All", "*") catch unreachable;
|
||||
file_open_options.appendFilter("Binary", "*.bin") catch unreachable;
|
||||
}
|
||||
|
||||
if (Platform.spawnFilePicker(&file_open_options)) |file_picker_id| {
|
||||
opts.file_picker.* = file_picker_id;
|
||||
|
Loading…
Reference in New Issue
Block a user