1//============================================================================
2//
3// SSSS tt lll lll
4// SS SS tt ll ll
5// SS tttttt eeee ll ll aaaa
6// SSSS tt ee ee ll ll aa
7// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator"
8// SS SS tt ee ll ll aa aa
9// SSSS ttt eeeee llll llll aaaaa
10//
11// Copyright (c) 1995-2019 by Bradford W. Mott, Stephen Anthony
12// and the Stella Team
13//
14// See the file "License.txt" for information on usage and redistribution of
15// this file, and for a DISCLAIMER OF ALL WARRANTIES.
16//============================================================================
17
18#include "bspf.hxx"
19#include "Debugger.hxx"
20#include "DiStella.hxx"
21#include "Widget.hxx"
22#include "StellaKeys.hxx"
23#include "FBSurface.hxx"
24#include "Font.hxx"
25#include "ScrollBarWidget.hxx"
26#include "RomListSettings.hxx"
27#include "RomListWidget.hxx"
28
29// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
30RomListWidget::RomListWidget(GuiObject* boss, const GUI::Font& lfont,
31 const GUI::Font& nfont,
32 int x, int y, int w, int h)
33 : EditableWidget(boss, nfont, x, y, 16, 16),
34 _rows(0),
35 _cols(0),
36 _currentPos(0),
37 _selectedItem(-1),
38 _highlightedItem(-1),
39 _editMode(false),
40 _currentKeyDown(KBDK_UNKNOWN),
41 _base(Common::Base::F_DEFAULT),
42 myDisasm(nullptr)//,
43{
44 _flags = Widget::FLAG_ENABLED | Widget::FLAG_CLEARBG | Widget::FLAG_RETAIN_FOCUS;
45 _bgcolor = kWidColor;
46 _bgcolorhi = kWidColor;
47 _textcolor = kTextColor;
48 _textcolorhi = kTextColor;
49
50 _cols = w / _fontWidth;
51 _rows = h / _fontHeight;
52
53 // Set real dimensions
54 _w = w - kScrollBarWidth;
55 _h = h + 2;
56
57 // Create scrollbar and attach to the list
58 myScrollBar = new ScrollBarWidget(boss, lfont, _x + _w, _y, kScrollBarWidth, _h);
59 myScrollBar->setTarget(this);
60
61 // Add settings menu
62 myMenu = make_unique<RomListSettings>(this, lfont);
63
64 // Take advantage of a wide debugger window when possible
65 const int fontWidth = lfont.getMaxCharWidth(),
66 numchars = w / fontWidth;
67
68 _labelWidth = std::max(14, int(0.45 * (numchars - 8 - 8 - 9 - 2))) * fontWidth - 1;
69 _bytesWidth = 9 * fontWidth;
70
71 ///////////////////////////////////////////////////////
72 // Add checkboxes
73 int ypos = _y + 2;
74
75 // rowheight is determined by largest item on a line,
76 // possibly meaning that number of rows will change
77 _fontHeight = std::max(_fontHeight, CheckboxWidget::boxSize());
78 _rows = h / _fontHeight;
79
80 // Create a CheckboxWidget for each row in the list
81 for(int i = 0; i < _rows; ++i)
82 {
83 CheckboxWidget* t = new CheckboxWidget(boss, lfont, _x + 2, ypos, "",
84 CheckboxWidget::kCheckActionCmd);
85 t->setTarget(this);
86 t->setID(i);
87 t->setFill(CheckboxWidget::FillType::Circle);
88 t->setTextColor(kTextColorEm);
89 ypos += _fontHeight;
90
91 myCheckList.push_back(t);
92 }
93
94 // Add filtering
95 EditableWidget::TextFilter f = [&](char c)
96 {
97 switch(_base)
98 {
99 case Common::Base::F_16:
100 case Common::Base::F_16_1:
101 case Common::Base::F_16_2:
102 case Common::Base::F_16_4:
103 case Common::Base::F_16_8:
104 return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || c == ' ';
105
106 case Common::Base::F_2:
107 case Common::Base::F_2_8:
108 case Common::Base::F_2_16:
109 return c == '0' || c == '1' || c == ' ';
110
111 case Common::Base::F_10:
112 return (c >= '0' && c <= '9') || c == ' ';
113
114 case Common::Base::F_DEFAULT:
115 default: // TODO - properly handle all other cases
116 return false;
117 }
118 };
119 setTextFilter(f);
120}
121
122// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
123void RomListWidget::setList(const CartDebug::Disassembly& disasm)
124{
125 myDisasm = &disasm;
126
127 // Enable all checkboxes
128 for(int i = 0; i < _rows; ++i)
129 myCheckList[i]->setFlags(Widget::FLAG_ENABLED);
130
131 // Then turn off any extras
132 if(int(myDisasm->list.size()) < _rows)
133 for(int i = int(myDisasm->list.size()); i < _rows; ++i)
134 myCheckList[i]->clearFlags(Widget::FLAG_ENABLED);
135
136 recalc();
137}
138
139// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
140void RomListWidget::setSelected(int item)
141{
142 if(item < -1 || item >= int(myDisasm->list.size()))
143 return;
144
145 if(isEnabled())
146 {
147 if(_editMode)
148 abortEditMode();
149
150 _currentPos = _selectedItem = item;
151 scrollToSelected();
152 }
153}
154
155// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
156void RomListWidget::setHighlighted(int item)
157{
158 if(item < -1 || item >= int(myDisasm->list.size()))
159 return;
160
161 if(isEnabled())
162 {
163 if(_editMode)
164 abortEditMode();
165
166 _highlightedItem = item;
167
168 // Only scroll the list if we're about to pass the page boundary
169 if (_highlightedItem < _currentPos)
170 {
171 _currentPos -= _rows;
172 if (_currentPos < 0)
173 _currentPos = 0;
174 }
175 else if(_highlightedItem == _currentPos + _rows)
176 _currentPos += _rows;
177
178 scrollToHighlighted();
179 }
180}
181
182// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
183int RomListWidget::findItem(int x, int y) const
184{
185 return (y - 1) / _fontHeight + _currentPos;
186}
187
188// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
189void RomListWidget::recalc()
190{
191 int size = int(myDisasm->list.size());
192
193 if (_currentPos >= size)
194 _currentPos = size - 1;
195 if (_currentPos < 0)
196 _currentPos = 0;
197
198 if(_selectedItem < 0 || _selectedItem >= size)
199 _selectedItem = 0;
200
201 _editMode = false;
202
203 myScrollBar->_numEntries = int(myDisasm->list.size());
204 myScrollBar->_entriesPerPage = _rows;
205
206 // Reset to normal data entry
207 abortEditMode();
208
209 setDirty();
210}
211
212// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
213void RomListWidget::scrollToCurrent(int item)
214{
215 // Only do something if the current item is not in our view port
216 if (item < _currentPos)
217 {
218 // it's above our view
219 _currentPos = item;
220 }
221 else if (item >= _currentPos + _rows )
222 {
223 // it's below our view
224 _currentPos = item - _rows + 1;
225 }
226
227 int size = int(myDisasm->list.size());
228 if (_currentPos < 0 || _rows > size)
229 _currentPos = 0;
230 else if (_currentPos + _rows > size)
231 _currentPos = size - _rows;
232
233 myScrollBar->_currentPos = _currentPos;
234 myScrollBar->recalc();
235
236 setDirty();
237}
238
239// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
240void RomListWidget::handleMouseDown(int x, int y, MouseButton b, int clickCount)
241{
242 if (!isEnabled())
243 return;
244
245 // Grab right mouse button for context menu, left for selection/edit mode
246 if(b == MouseButton::RIGHT)
247 {
248 // Set selected and add menu at current x,y mouse location
249 _selectedItem = findItem(x, y);
250 scrollToSelected();
251 myMenu->show(x + getAbsX(), y + getAbsY(),
252 dialog().surface().dstRect(), _selectedItem);
253 }
254 else
255 {
256 // First check whether the selection changed
257 int newSelectedItem;
258 newSelectedItem = findItem(x, y);
259 if (newSelectedItem > int(myDisasm->list.size()) - 1)
260 newSelectedItem = -1;
261
262 if (_selectedItem != newSelectedItem)
263 {
264 if (_editMode)
265 abortEditMode();
266 _selectedItem = newSelectedItem;
267 setDirty();
268 }
269 }
270}
271
272// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
273void RomListWidget::handleMouseUp(int x, int y, MouseButton b, int clickCount)
274{
275 // If this was a double click and the mouse is still over the selected item,
276 // send the double click command
277 if (clickCount == 2 && (_selectedItem == findItem(x, y)))
278 {
279 // Start edit mode
280 if(isEditable() && !_editMode)
281 startEditMode();
282 }
283}
284
285// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
286void RomListWidget::handleMouseWheel(int x, int y, int direction)
287{
288 myScrollBar->handleMouseWheel(x, y, direction);
289}
290
291// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
292void RomListWidget::handleMouseEntered()
293{
294 setFlags(Widget::FLAG_HILITED);
295 setDirty();
296}
297
298// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
299void RomListWidget::handleMouseLeft()
300{
301 clearFlags(Widget::FLAG_HILITED);
302 setDirty();
303}
304
305// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
306bool RomListWidget::handleText(char text)
307{
308 if(_editMode)
309 {
310 // Class EditableWidget handles all text editing related key presses for us
311 return EditableWidget::handleText(text);
312 }
313 return false;
314}
315
316// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
317bool RomListWidget::handleKeyDown(StellaKey key, StellaMod mod)
318{
319 // Ignore all Alt-mod keys
320 if(StellaModTest::isAlt(mod))
321 return true;
322
323 bool handled = true;
324 int oldSelectedItem = _selectedItem;
325
326 if (_editMode)
327 {
328 // Class EditableWidget handles all single-key presses for us
329 handled = EditableWidget::handleKeyDown(key, mod);
330 }
331 else
332 {
333 switch (key)
334 {
335 case KBDK_SPACE:
336 // Snap list back to currently highlighted line
337 if(_highlightedItem >= 0)
338 {
339 _currentPos = _highlightedItem;
340 scrollToHighlighted();
341 }
342 break;
343
344 default:
345 handled = false;
346 }
347 }
348
349 if (_selectedItem != oldSelectedItem)
350 {
351 myScrollBar->draw();
352 scrollToSelected();
353 }
354
355 _currentKeyDown = key;
356 return handled;
357}
358
359// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
360bool RomListWidget::handleKeyUp(StellaKey key, StellaMod mod)
361{
362 if (key == _currentKeyDown)
363 _currentKeyDown = KBDK_UNKNOWN;
364 return true;
365}
366
367// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
368bool RomListWidget::handleEvent(Event::Type e)
369{
370 if(!isEnabled() || _editMode)
371 return false;
372
373 bool handled = true;
374 int oldSelectedItem = _selectedItem;
375
376 switch(e)
377 {
378 case Event::UISelect:
379 if (_selectedItem >= 0)
380 {
381 if (isEditable())
382 startEditMode();
383 }
384 break;
385
386 case Event::UIUp:
387 if (_selectedItem > 0)
388 _selectedItem--;
389 break;
390
391 case Event::UIDown:
392 if (_selectedItem < int(myDisasm->list.size()) - 1)
393 _selectedItem++;
394 break;
395
396 case Event::UIPgUp:
397 _selectedItem -= _rows - 1;
398 if (_selectedItem < 0)
399 _selectedItem = 0;
400 break;
401
402 case Event::UIPgDown:
403 _selectedItem += _rows - 1;
404 if (_selectedItem >= int(myDisasm->list.size()))
405 _selectedItem = int(myDisasm->list.size()) - 1;
406 break;
407
408 case Event::UIHome:
409 _selectedItem = 0;
410 break;
411
412 case Event::UIEnd:
413 _selectedItem = int(myDisasm->list.size()) - 1;
414 break;
415
416 default:
417 handled = false;
418 }
419
420 if (_selectedItem != oldSelectedItem)
421 {
422 myScrollBar->draw();
423 scrollToSelected();
424 }
425
426 return handled;
427}
428
429// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
430void RomListWidget::handleCommand(CommandSender* sender, int cmd, int data, int id)
431{
432 switch (cmd)
433 {
434 case CheckboxWidget::kCheckActionCmd:
435 // We let the parent class handle this
436 // Pass it as a kRLBreakpointChangedCmd command, since that's the intent
437 sendCommand(RomListWidget::kBPointChangedCmd, _currentPos+id, 0);
438 break;
439
440 case GuiObject::kSetPositionCmd:
441 if (_currentPos != data)
442 {
443 _currentPos = data;
444 setDirty();
445 }
446 break;
447
448 default:
449 // Let the parent class handle all other commands directly
450 sendCommand(cmd, data, id);
451 }
452}
453
454// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
455void RomListWidget::lostFocusWidget()
456{
457 _editMode = false;
458
459 // Reset to normal data entry
460 abortEditMode();
461}
462
463// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
464void RomListWidget::drawWidget(bool hilite)
465{
466 FBSurface& s = _boss->dialog().surface();
467 bool onTop = _boss->dialog().isOnTop();
468 const CartDebug::DisassemblyList& dlist = myDisasm->list;
469 int i, pos, xpos, ypos, len = int(dlist.size());
470 ColorId textColor = onTop ? kTextColor : kColor;
471
472 const Common::Rect& r = getEditRect();
473 const Common::Rect& l = getLineRect();
474
475 // Draw a thin frame around the list and to separate columns
476 s.frameRect(_x, _y, _w + 1, _h, hilite ? kWidColorHi : kColor);
477 s.vLine(_x + CheckboxWidget::boxSize() + 5, _y, _y + _h - 1, kColor);
478
479 // Draw the list items
480 int cycleCountW = _fontWidth * 8,
481 noTypeDisasmW = _w - l.x() - _labelWidth,
482 noCodeDisasmW = noTypeDisasmW - r.w(),
483 codeDisasmW = noCodeDisasmW - cycleCountW,
484 actualWidth = myDisasm->fieldwidth * _fontWidth;
485 if(actualWidth < codeDisasmW)
486 codeDisasmW = actualWidth;
487
488 xpos = _x + CheckboxWidget::boxSize() + 10; ypos = _y + 2;
489 for (i = 0, pos = _currentPos; i < _rows && pos < len; i++, pos++, ypos += _fontHeight)
490 {
491 ColorId bytesColor = textColor;
492
493 // Draw checkboxes for correct lines (takes scrolling into account)
494 myCheckList[i]->setState(instance().debugger().
495 checkBreakPoint(dlist[pos].address,
496 instance().debugger().cartDebug().getBank(dlist[pos].address)));
497
498 myCheckList[i]->setDirty();
499 myCheckList[i]->draw();
500
501 // Draw highlighted item in a frame
502 if (_highlightedItem == pos)
503 s.frameRect(_x + l.x() - 3, ypos - 1, _w - l.x(), _fontHeight, onTop ? kWidColorHi : kBGColorLo);
504
505 // Draw the selected item inverted, on a highlighted background.
506 if(_selectedItem == pos && _hasFocus)
507 {
508 if(!_editMode)
509 {
510 s.fillRect(_x + r.x() - 3, ypos - 1, r.w(), _fontHeight, kTextColorHi);
511 bytesColor = kTextColorInv;
512 }
513 else
514 s.frameRect(_x + r.x() - 3, ypos - 1, r.w(), _fontHeight, kWidColorHi);
515 }
516
517 // Draw labels
518 s.drawString(_font, dlist[pos].label, xpos, ypos, _labelWidth,
519 dlist[pos].hllabel ? textColor : kColor);
520
521 // Bytes are only editable if they represent code, graphics, or accessible data
522 // Otherwise, the disassembly should get all remaining space
523 if(dlist[pos].type & (CartDebug::CODE|CartDebug::GFX|CartDebug::PGFX|CartDebug::DATA))
524 {
525 if(dlist[pos].type == CartDebug::CODE)
526 {
527 // Draw mnemonic
528 s.drawString(_font, dlist[pos].disasm.substr(0, 7), xpos + _labelWidth, ypos,
529 7 * _fontWidth, textColor);
530 // Draw operand
531 if (dlist[pos].disasm.length() > 8)
532 s.drawString(_font, dlist[pos].disasm.substr(8), xpos + _labelWidth + 7 * _fontWidth, ypos,
533 codeDisasmW - 7 * _fontWidth, textColor);
534 // Draw cycle count
535 s.drawString(_font, dlist[pos].ccount, xpos + _labelWidth + codeDisasmW, ypos,
536 cycleCountW, textColor);
537 }
538 else
539 {
540 // Draw disassembly only
541 s.drawString(_font, dlist[pos].disasm, xpos + _labelWidth, ypos,
542 noCodeDisasmW - 4, kTextColor);
543 }
544
545 // Draw separator
546 s.vLine(_x + r.x() - 7, ypos, ypos + _fontHeight - 1, kColor);
547
548 // Draw bytes
549 {
550 if (_selectedItem == pos && _editMode)
551 {
552 adjustOffset();
553 s.drawString(_font, editString(), _x + r.x(), ypos, r.w(), textColor,
554 TextAlign::Left, -_editScrollOffset, false);
555
556 drawCaret();
557 }
558 else
559 {
560 s.drawString(_font, dlist[pos].bytes, _x + r.x(), ypos, r.w(), bytesColor);
561 }
562 }
563 }
564 else
565 {
566 // Draw disassembly, giving it all remaining horizontal space
567 s.drawString(_font, dlist[pos].disasm, xpos + _labelWidth, ypos,
568 noTypeDisasmW, textColor);
569 }
570 }
571}
572
573// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
574Common::Rect RomListWidget::getLineRect() const
575{
576 const int yoffset = std::max(0, (_selectedItem - _currentPos) * _fontHeight),
577 xoffset = CheckboxWidget::boxSize() + 10;
578
579 return Common::Rect(2 + xoffset, 1 + yoffset,
580 _w - (xoffset - 15), _fontHeight + yoffset);
581}
582
583// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
584Common::Rect RomListWidget::getEditRect() const
585{
586 const int yoffset = std::max(0, (_selectedItem - _currentPos) * _fontHeight);
587
588 return Common::Rect(2 + _w - _bytesWidth, 1 + yoffset,
589 _w, _fontHeight + yoffset);
590}
591
592// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
593void RomListWidget::startEditMode()
594{
595 if (isEditable() && !_editMode && _selectedItem >= 0)
596 {
597 // Does this line represent an editable area?
598 if(myDisasm->list[_selectedItem].bytes == "")
599 return;
600
601 _editMode = true;
602 switch(myDisasm->list[_selectedItem].type)
603 {
604 case CartDebug::GFX:
605 case CartDebug::PGFX:
606 _base = DiStella::settings.gfxFormat;
607 break;
608 default:
609 _base = Common::Base::format();
610 }
611
612 // Widget gets raw data while editing
613 EditableWidget::startEditMode();
614 setText(myDisasm->list[_selectedItem].bytes);
615 }
616}
617
618// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
619void RomListWidget::endEditMode()
620{
621 if (!_editMode)
622 return;
623
624 // Send a message that editing finished with a return/enter key press
625 // The parent then calls getText() to get the newly entered data
626 _editMode = false;
627 sendCommand(RomListWidget::kRomChangedCmd, _selectedItem, _base);
628
629 // Reset to normal data entry
630 EditableWidget::endEditMode();
631}
632
633// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
634void RomListWidget::abortEditMode()
635{
636 // Undo any changes made
637 _editMode = false;
638
639 // Reset to normal data entry
640 EditableWidget::abortEditMode();
641}
642