16 private const int OuterMargin = 18;
17 private const int HeaderMargin = 34;
18 private const int WellGap = 10;
19 private const int WellSize = 112;
20 private const int WellInnerPad = 8;
21 private const int SampleGap = 4;
22 private const int SampleMinSize = 16;
24 private readonly Builder _builder;
25 private readonly Dictionary<(
int row,
int col), BioImage.WellPlate.Well> _wellsByCell =
new();
26 private readonly List<BioImage.WellPlate.Well> _wells =
new();
27 private BioImage.WellPlate.Well _selectedWell;
28 private BioImage.WellPlate.Well.Sample _selectedSample;
33 private int _contentWidth;
34 private int _contentHeight;
36#pragma warning disable 649
38 private Gtk.DrawingArea pictureBox;
39#pragma warning restore 649
43 if (
ImageView.SelectedImage?.Plate ==
null ||
ImageView.SelectedImage.Type != BioImage.ImageType.well)
45 var dialog =
new MessageDialog(
50 "The current image does not contain a well plate.");
56 string gladePath = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(Environment.ProcessPath) ??
string.Empty,
"Glade",
"Plate.glade");
57 Builder builder =
new Builder(
new FileStream(gladePath, FileMode.Open, FileAccess.Read, FileShare.Read));
63 protected PlateTool(Builder builder, IntPtr handle) : base(handle)
66 builder.Autoconnect(
this);
69 SetDefaultSize(1200, 900);
71 pictureBox.CanFocus =
true;
72 pictureBox.Drawn += PictureBox_Drawn;
73 pictureBox.ButtonPressEvent += PictureBox_ButtonPressEvent;
74 pictureBox.KeyPressEvent += PictureBox_KeyPressEvent;
75 pictureBox.ScrollEvent += PictureBox_ScrollEvent;
76 pictureBox.AddEvents((
int)(EventMask.ButtonPressMask
77 | EventMask.ButtonReleaseMask
78 | EventMask.KeyPressMask
79 | EventMask.PointerMotionMask
80 | EventMask.ScrollMask));
82 App.ApplyStyles(
this);
86 private string BuildTitle()
88 var plate =
ImageView.SelectedImage?.Plate;
92 if (!
string.IsNullOrWhiteSpace(plate.Name))
93 return $
"Plate - {plate.Name}";
95 if (!
string.IsNullOrWhiteSpace(plate.ID))
96 return $
"Plate - {plate.ID}";
101 private void RebuildPlateLayout()
104 _wellsByCell.Clear();
106 var plate =
ImageView.SelectedImage?.Plate;
107 if (plate ==
null || plate.Wells ==
null || plate.Wells.Count == 0)
109 _minRow = _minCol = 0;
110 _maxRow = _maxCol = 0;
111 _contentWidth = _contentHeight = 0;
112 pictureBox.SetSizeRequest(400, 300);
113 pictureBox.QueueDraw();
117 _wells.AddRange(plate.Wells);
119 _minRow = _wells.Min(w => w.Row);
120 _maxRow = _wells.Max(w => w.Row);
121 _minCol = _wells.Min(w => w.Column);
122 _maxCol = _wells.Max(w => w.Column);
124 int cols = Math.Max(1, _maxCol - _minCol + 1);
125 int rows = Math.Max(1, _maxRow - _minRow + 1);
127 _contentWidth = OuterMargin + HeaderMargin + (cols * WellSize) + ((cols - 1) * WellGap) + OuterMargin;
128 _contentHeight = OuterMargin + HeaderMargin + (rows * WellSize) + ((rows - 1) * WellGap) + OuterMargin;
130 foreach (var well
in _wells)
131 _wellsByCell[(well.Row - _minRow, well.Column - _minCol)] = well;
133 pictureBox.SetSizeRequest(_contentWidth, _contentHeight);
134 SyncSelectionFromImage();
135 Title = BuildTitle();
136 pictureBox.QueueDraw();
139 private void SyncSelectionFromImage()
141 _selectedWell =
null;
142 _selectedSample =
null;
144 int activeIndex =
ImageView.SelectedImage?.WellIndex ?? -1;
148 foreach (var well
in _wells)
150 foreach (var sample
in well.Samples)
152 if (sample.Index == activeIndex)
154 _selectedWell = well;
155 _selectedSample = sample;
162 private void PictureBox_ButtonPressEvent(
object o, ButtonPressEventArgs args)
164 if (args.Event.Button != 1 ||
ImageView.SelectedImage?.Plate ==
null)
167 pictureBox.GrabFocus();
169 if (TryHitTest((
int)args.Event.X, (
int)args.Event.Y, out var well, out var sample))
171 SelectSample(well, sample);
172 pictureBox.QueueDraw();
176 private void PictureBox_ScrollEvent(
object o, ScrollEventArgs args)
178 if (
ImageView.SelectedImage?.Plate ==
null || _selectedWell ==
null)
181 if (args.Event.Direction == ScrollDirection.Up)
182 CycleSelectedSample(-1);
183 else if (args.Event.Direction == ScrollDirection.Down)
184 CycleSelectedSample(1);
186 pictureBox.QueueDraw();
189 private void PictureBox_KeyPressEvent(
object o, KeyPressEventArgs args)
191 if (
ImageView.SelectedImage?.Plate ==
null || _wells.Count == 0)
194 switch (args.Event.Key)
209 CycleSelectedSample(-1);
212 CycleSelectedSample(1);
224 pictureBox.QueueDraw();
227 private void NavigateWell(
int rowDelta,
int colDelta)
229 if (_selectedWell ==
null)
235 int targetRow = _selectedWell.Row + rowDelta;
236 int targetCol = _selectedWell.Column + colDelta;
237 if (_wellsByCell.TryGetValue((targetRow - _minRow, targetCol - _minCol), out var well))
239 SelectFirstSample(well);
245 .OrderBy(w => Math.Abs(w.Row - targetRow) + Math.Abs(w.Column - targetCol))
247 .ThenBy(w => w.Column)
250 SelectFirstSample(ordered);
253 private void CycleSelectedSample(
int direction)
255 if (_selectedWell ==
null || _selectedWell.Samples ==
null || _selectedWell.Samples.Count == 0)
258 int current = _selectedWell.Samples.FindIndex(s => s.Index ==
ImageView.SelectedImage.WellIndex);
262 int next = (current + direction) % _selectedWell.Samples.Count;
264 next += _selectedWell.Samples.Count;
266 SelectSample(_selectedWell, _selectedWell.Samples[next]);
269 private void SelectFirstWell()
271 var well = _wells.OrderBy(w => w.Row).ThenBy(w => w.Column).FirstOrDefault();
273 SelectFirstSample(well);
276 private void SelectLastWell()
278 var well = _wells.OrderByDescending(w => w.Row).ThenByDescending(w => w.Column).FirstOrDefault();
280 SelectFirstSample(well);
283 private void SelectFirstSample(BioImage.WellPlate.Well well)
288 var sample = well.Samples?.FirstOrDefault();
290 SelectSample(well, sample);
293 private void SelectSample(BioImage.WellPlate.Well well, BioImage.WellPlate.Well.Sample sample)
295 if (well ==
null || sample ==
null ||
ImageView.SelectedImage ==
null)
298 _selectedWell = well;
299 _selectedSample = sample;
301 if (
ImageView.SelectedImage.WellIndex != sample.Index)
303 ImageView.SelectedImage.WellIndex = sample.Index;
304 App.viewer?.ReinitializeWellField();
307 pictureBox.QueueDraw();
310 private bool TryHitTest(
int x,
int y, out BioImage.WellPlate.Well well, out BioImage.WellPlate.Well.Sample sample)
315 if (_wells.Count == 0)
318 int localX = x - OuterMargin - HeaderMargin;
319 int localY = y - OuterMargin - HeaderMargin;
320 if (localX < 0 || localY < 0)
323 int cellPitch = WellSize + WellGap;
324 int col = localX / cellPitch;
325 int row = localY / cellPitch;
326 int cellX = localX % cellPitch;
327 int cellY = localY % cellPitch;
329 if (col < 0 || row < 0)
332 if (!_wellsByCell.TryGetValue((row, col), out well))
336 if (cellX >= WellSize || cellY >= WellSize)
339 int sampleCount = well.Samples?.Count ?? 0;
340 if (sampleCount == 0)
343 var sampleRects = GetSampleRects(sampleCount,
new RectangleD(
344 OuterMargin + HeaderMargin + (col * cellPitch),
345 OuterMargin + HeaderMargin + (row * cellPitch),
346 WellSize, WellSize));
348 for (
int i = 0; i < sampleRects.Count && i < sampleCount; i++)
350 var rect = sampleRects[i];
351 if (x >= rect.X && x < rect.X + rect.W &&
352 y >= rect.Y && y < rect.Y + rect.H)
354 sample = well.Samples[i];
359 sample = well.Samples[0];
363 private void PictureBox_Drawn(
object o, DrawnArgs args)
368 if (
ImageView.SelectedImage?.Plate ==
null || _wells.Count == 0)
376 foreach (var well
in _wells)
380 private void DrawBackground(Context cr)
382 cr.SetSourceRGB(0.09, 0.09, 0.10);
386 private void DrawEmptyState(Context cr)
388 cr.SetSourceRGB(0.85, 0.85, 0.88);
389 cr.SelectFontFace(
"Sans", FontSlant.Normal, FontWeight.Normal);
391 DrawText(cr,
"No well plate metadata is available.", OuterMargin, OuterMargin + 30);
394 private void DrawHeaders(Context cr)
396 cr.SelectFontFace(
"Sans", FontSlant.Normal, FontWeight.Bold);
398 cr.SetSourceRGB(0.85, 0.85, 0.90);
400 int cellPitch = WellSize + WellGap;
401 int cols = Math.Max(1, _maxCol - _minCol + 1);
402 int rows = Math.Max(1, _maxRow - _minRow + 1);
404 for (
int c = 0; c < cols; c++)
406 string label = (c + 1).ToString();
407 double x = OuterMargin + HeaderMargin + (c * cellPitch) + (WellSize / 2.0);
408 DrawCenteredText(cr, label, x, OuterMargin + 14);
411 for (
int r = 0; r < rows; r++)
413 string label = ToPlateRowLabel(r);
414 double y = OuterMargin + HeaderMargin + (r * cellPitch) + (WellSize / 2.0);
415 DrawCenteredText(cr, label, OuterMargin + 14, y);
419 private void DrawWell(Context cr, BioImage.WellPlate.Well well)
421 int gridRow = well.Row - _minRow;
422 int gridCol = well.Column - _minCol;
423 int cellPitch = WellSize + WellGap;
424 double x = OuterMargin + HeaderMargin + (gridCol * cellPitch);
425 double y = OuterMargin + HeaderMargin + (gridRow * cellPitch);
427 var fill = ToCairoColor(well.Color);
428 cr.SetSourceRGBA(fill.R * 0.35, fill.G * 0.35, fill.B * 0.35, 0.75);
429 cr.Rectangle(x, y, WellSize, WellSize);
432 bool isSelectedWell = ReferenceEquals(well, _selectedWell);
433 double borderWidth = isSelectedWell ? 3.5 : 1.25;
436 double outlineLeft = x - 5.0;
437 double outlineTop = y - 2.0;
438 double outlineWidth = WellSize + 10.0;
439 double outlineHeight = WellSize + 6.0;
440 cr.Rectangle(outlineLeft, outlineTop, outlineWidth, outlineHeight);
441 cr.LineWidth = borderWidth;
442 cr.SetSourceRGB(1.0, 0.45, 0.15);
446 cr.LineWidth = borderWidth;
447 cr.SetSourceRGB(0.18, 0.18, 0.18);
448 cr.Rectangle(x, y, WellSize, WellSize);
451 DrawWellLabel(cr, well, x, y);
452 DrawSampleSlots(cr, well, x, y);
455 private void DrawWellLabel(Context cr, BioImage.WellPlate.Well well,
double x,
double y)
457 string label =
string.IsNullOrWhiteSpace(well.ID)
458 ? $
"{ToPlateRowLabel(well.Row - _minRow)}{(well.Column - _minCol) + 1}"
461 cr.SelectFontFace(
"Sans", FontSlant.Normal, FontWeight.Bold);
463 cr.SetSourceRGB(0.95, 0.95, 0.95);
464 DrawText(cr, label, x + 6, y + 14);
467 private void DrawSampleSlots(Context cr, BioImage.WellPlate.Well well,
double x,
double y)
469 int sampleCount = well.Samples?.Count ?? 0;
470 if (sampleCount <= 0)
473 var rects = GetSampleRects(sampleCount,
new RectangleD(x, y, WellSize, WellSize));
474 int activeIndex =
ImageView.SelectedImage?.WellIndex ?? -1;
476 for (
int i = 0; i < sampleCount && i < rects.Count; i++)
478 var sample = well.Samples[i];
480 bool isSelectedSample = sample.Index == activeIndex;
482 if (isSelectedSample)
483 cr.SetSourceRGB(0.98, 0.90, 0.15);
485 cr.SetSourceRGB(0.82, 0.82, 0.84);
487 cr.Rectangle(rect.X, rect.Y, rect.W, rect.H);
490 cr.LineWidth = isSelectedSample ? 2.0 : 1.0;
491 cr.SetSourceRGB(isSelectedSample ? 0.35 : 0.22, isSelectedSample ? 0.18 : 0.22, isSelectedSample ? 0.04 : 0.22);
494 if (rect.W >= 22 && rect.H >= 18)
496 string text = !
string.IsNullOrWhiteSpace(sample.ID) ? sample.ID : sample.Index.ToString();
497 cr.SelectFontFace(
"Sans", FontSlant.Normal, FontWeight.Bold);
498 cr.SetFontSize(Math.Max(8, Math.Min(11, rect.H * 0.35)));
499 cr.SetSourceRGB(0.10, 0.10, 0.10);
500 DrawCenteredText(cr, text, rect.X + rect.W / 2.0, rect.Y + rect.H / 2.0 + 1);
505 private static List<RectangleD> GetSampleRects(
int sampleCount, RectangleD wellRect)
507 var rects =
new List<RectangleD>();
508 if (sampleCount <= 0)
511 int columns = Math.Max(1, (
int)Math.Ceiling(Math.Sqrt(sampleCount)));
512 int rows = Math.Max(1, (
int)Math.Ceiling(sampleCount / (
double)columns));
513 double innerX = wellRect.X + WellInnerPad;
514 double innerY = wellRect.Y + 30;
515 double innerW = wellRect.W - (WellInnerPad * 2);
516 double innerH = wellRect.H - 38 - WellInnerPad;
517 double slotW = Math.Max(SampleMinSize, (innerW - ((columns - 1) * SampleGap)) / columns);
518 double slotH = Math.Max(SampleMinSize, (innerH - ((rows - 1) * SampleGap)) / rows);
520 double slot = Math.Min(slotW, slotH);
521 double totalW = (columns * slot) + ((columns - 1) * SampleGap);
522 double totalH = (rows * slot) + ((rows - 1) * SampleGap);
523 double startX = wellRect.X + (wellRect.W - totalW) / 2.0;
524 double startY = innerY + (innerH - totalH) / 2.0;
526 for (
int i = 0; i < sampleCount; i++)
530 rects.Add(
new RectangleD(
531 startX + c * (slot + SampleGap),
532 startY + r * (slot + SampleGap),
540 private static void DrawText(Context cr,
string text,
double x,
double y)
543 cr.ShowText(text ??
string.Empty);
546 private static void DrawCenteredText(Context cr,
string text,
double centerX,
double centerY)
548 if (
string.IsNullOrEmpty(text))
551 var ext = cr.TextExtents(text);
552 cr.MoveTo(centerX - (ext.Width / 2.0) - ext.XBearing,
553 centerY - (ext.Height / 2.0) - ext.YBearing);
557 private static string ToPlateRowLabel(
int rowIndex)
563 string label =
string.Empty;
564 int value = rowIndex;
567 label = (char)(
'A' + (value % 26)) + label;
568 value = (value / 26) - 1;
575 private static Cairo.Color ToCairoColor(System.Drawing.Color color)
578 return new Cairo.Color(0.20, 0.20, 0.20, 1.0);
580 return new Cairo.Color(color.R / 255.0, color.G / 255.0, color.B / 255.0, color.A / 255.0);