1 /* Reverse Engineer's Hex Editor
2 * Copyright (C) 2018 Daniel Collins <solemnwarning@solemnwarning.net>
3 *
4 * This program is free software; you can redistribute it and/or modify it
5 * under the terms of the GNU General Public License version 2 as published by
6 * the Free Software Foundation.
7 *
8 * This program is distributed in the hope that it will be useful, but WITHOUT
9 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
10 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
11 * more details.
12 *
13 * You should have received a copy of the GNU General Public License along with
14 * this program; if not, write to the Free Software Foundation, Inc., 51
15 * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 */
17
18 #include "platform.hpp"
19 #include <assert.h>
20 #include <wx/clipbrd.h>
21 #include <wx/dcbuffer.h>
22
23 #include "App.hpp"
24 #include "CodeCtrl.hpp"
25
26 enum {
27 ID_SELECT_TIMER = 1,
28 };
29
BEGIN_EVENT_TABLE(REHex::CodeCtrl,wxControl)30 BEGIN_EVENT_TABLE(REHex::CodeCtrl, wxControl)
31 EVT_PAINT(REHex::CodeCtrl::OnPaint)
32 EVT_ERASE_BACKGROUND(REHex::CodeCtrl::OnErase)
33 EVT_SIZE(REHex::CodeCtrl::OnSize)
34 EVT_SCROLLWIN(REHex::CodeCtrl::OnScroll)
35 EVT_MOUSEWHEEL(REHex::CodeCtrl::OnWheel)
36 EVT_CHAR(REHex::CodeCtrl::OnChar)
37 EVT_LEFT_DOWN(REHex::CodeCtrl::OnLeftDown)
38 EVT_LEFT_UP(REHex::CodeCtrl::OnLeftUp)
39 EVT_RIGHT_DOWN(REHex::CodeCtrl::OnRightDown)
40 EVT_MENU(wxID_COPY, REHex::CodeCtrl::OnCopy)
41 EVT_MENU(wxID_SELECTALL, REHex::CodeCtrl::OnSelectAll)
42 EVT_MOTION(REHex::CodeCtrl::OnMotion)
43 EVT_TIMER(ID_SELECT_TIMER, REHex::CodeCtrl::OnSelectTick)
44 END_EVENT_TABLE()
45
46 REHex::CodeCtrl::CodeCtrl(wxWindow *parent, wxWindowID id):
47 wxControl(parent, id, wxDefaultPosition, wxDefaultSize, (wxVSCROLL | wxHSCROLL | wxWANTS_CHARS)),
48 font(wxFontInfo().FaceName(wxGetApp().get_font_name())),
49 max_line_width(0),
50 offset_display_base(OFFSET_BASE_HEX),
51 offset_display_upper_bound(0xFFFFFFFF),
52 scroll_xoff(0), scroll_xoff_max(0),
53 scroll_yoff(0), scroll_yoff_max(0),
54 wheel_vert_accum(0),
55 wheel_horiz_accum(0),
56 mouse_selecting(false),
57 mouse_selecting_timer(this, ID_SELECT_TIMER),
58 selection_begin(-1, -1),
59 selection_end(-1, -1)
60 {
61 App &app = wxGetApp();
62
63 app.Bind(FONT_SIZE_ADJUSTMENT_CHANGED, &REHex::CodeCtrl::OnFontSizeAdjustmentChanged, this);
64
65 int font_size_adjustment = app.get_font_size_adjustment();
66
67 while(font_size_adjustment > 0) { font.MakeLarger(); --font_size_adjustment; }
68 while(font_size_adjustment < 0) { font.MakeSmaller(); ++font_size_adjustment; }
69
70 assert(font.IsFixedWidth());
71
72 wxClientDC dc(this);
73 dc.SetFont(font);
74
75 wxSize char_extent = dc.GetTextExtent("X");
76 font_width = char_extent.GetWidth();
77 font_height = char_extent.GetHeight();
78
79 std::string offset_str = format_offset(0, offset_display_base, offset_display_upper_bound);
80 code_xoff = dc.GetTextExtent(offset_str + " ").GetWidth();
81 }
82
~CodeCtrl()83 REHex::CodeCtrl::~CodeCtrl()
84 {
85 wxGetApp().Unbind(FONT_SIZE_ADJUSTMENT_CHANGED, &REHex::CodeCtrl::OnFontSizeAdjustmentChanged, this);
86 }
87
append_line(off_t offset,const std::string & text,bool active)88 void REHex::CodeCtrl::append_line(off_t offset, const std::string &text, bool active)
89 {
90 wxClientDC dc(this);
91 dc.SetFont(font);
92
93 /* GetTextExtent() doesn't seem to handle tabs correctly, so we expand
94 * them into spaces.
95 */
96
97 std::string text_no_tabs = text;
98 static const int TAB_WIDTH = 8;
99
100 for(size_t p = 0; (p = text_no_tabs.find('\t', p)) != std::string::npos;)
101 {
102 size_t n_spaces = TAB_WIDTH - (p % TAB_WIDTH);
103 text_no_tabs.replace(p, 1, n_spaces, ' ');
104 }
105
106 int line_width = code_xoff + dc.GetTextExtent(text_no_tabs).GetWidth();
107 if(max_line_width < line_width)
108 {
109 max_line_width = line_width;
110 }
111
112 lines.emplace_back(offset, text_no_tabs, active);
113
114 update_scrollbars();
115 Refresh();
116 }
117
clear()118 void REHex::CodeCtrl::clear()
119 {
120 selection_begin = CodeCharRef(-1, -1);
121 selection_end = CodeCharRef(-1, -1);
122
123 max_line_width = 0;
124 lines.clear();
125
126 update_scrollbars();
127 Refresh();
128 }
129
center_line(int line)130 void REHex::CodeCtrl::center_line(int line)
131 {
132 wxSize client_size = GetClientSize();
133
134 scroll_yoff = (line * font_height) - (client_size.GetHeight() / 2);
135
136 if(scroll_yoff < 0)
137 {
138 scroll_yoff = 0;
139 }
140 else if(scroll_yoff > scroll_yoff_max)
141 {
142 scroll_yoff = scroll_yoff_max;
143 }
144
145 scroll_xoff = 0;
146
147 update_scrollbars();
148 Refresh();
149 }
150
update_scrollbars()151 void REHex::CodeCtrl::update_scrollbars()
152 {
153 wxSize client_size = GetClientSize();
154
155 int virt_height = lines.size() * font_height;
156 if(virt_height > client_size.GetHeight())
157 {
158 scroll_yoff_max = virt_height - client_size.GetHeight();
159 if(scroll_yoff > scroll_yoff_max)
160 {
161 scroll_yoff = scroll_yoff_max;
162 }
163
164 SetScrollbar(wxVERTICAL, scroll_yoff, client_size.GetHeight(), virt_height);
165 }
166 else{
167 scroll_yoff_max = 0;
168 scroll_yoff = 0;
169
170 SetScrollbar(wxVERTICAL, 0, 0, 0);
171 }
172
173 if(max_line_width > client_size.GetWidth())
174 {
175 scroll_xoff_max = max_line_width - client_size.GetWidth();
176 if(scroll_xoff > scroll_xoff_max)
177 {
178 scroll_xoff = scroll_xoff_max;
179 }
180
181 SetScrollbar(wxHORIZONTAL, scroll_xoff, client_size.GetWidth(), max_line_width);
182 }
183 else{
184 scroll_xoff_max = 0;
185 scroll_xoff = 0;
186
187 SetScrollbar(wxHORIZONTAL, 0, 0, 0);
188 }
189 }
190
update_widths()191 void REHex::CodeCtrl::update_widths()
192 {
193 wxClientDC dc(this);
194 dc.SetFont(font);
195
196 std::string offset_str = format_offset(0, offset_display_base, offset_display_upper_bound);
197 code_xoff = dc.GetTextExtent(offset_str + " ").GetWidth();
198
199 max_line_width = 0;
200
201 for(auto l = lines.begin(); l != lines.end(); ++l)
202 {
203 int line_width = code_xoff + dc.GetTextExtent(l->text).GetWidth();
204 if(max_line_width < line_width)
205 {
206 max_line_width = line_width;
207 }
208 }
209 }
210
char_near_abs_xy(int abs_x,int abs_y)211 REHex::CodeCtrl::CodeCharRef REHex::CodeCtrl::char_near_abs_xy(int abs_x, int abs_y)
212 {
213 if(lines.empty())
214 {
215 return CodeCharRef(-1, -1);
216 }
217
218 int line_idx = std::min((abs_y / font_height), (int)(lines.size() - 1));
219 const Line &line = *(std::next(lines.begin(), line_idx));
220
221 int col = 0;
222
223 wxClientDC dc(this);
224 dc.SetFont(font);
225
226 while((code_xoff + dc.GetTextExtent(std::string((col + 1), 'X')).GetWidth()) < abs_x && col < (int)(line.text.length()))
227 {
228 ++col;
229 }
230
231 return CodeCharRef(line_idx, col);
232 }
233
char_near_rel_xy(int rel_x,int rel_y)234 REHex::CodeCtrl::CodeCharRef REHex::CodeCtrl::char_near_rel_xy(int rel_x, int rel_y)
235 {
236 return char_near_abs_xy((rel_x + scroll_xoff), (rel_y + scroll_yoff));
237 }
238
copy_selection()239 void REHex::CodeCtrl::copy_selection()
240 {
241 if(selection_end > selection_begin && wxTheClipboard->Open())
242 {
243 std::string copy_text;
244
245 for(int i = selection_begin.first; i <= selection_end.first; ++i)
246 {
247 assert(i >= 0);
248 assert((unsigned)(i) < lines.size());
249
250 const std::string &line_text = lines[i].text;
251
252 if(i > selection_begin.first)
253 {
254 copy_text += '\n';
255 }
256
257 int substr_off = (i == selection_begin.first ? selection_begin.second : 0);
258 assert(substr_off >= 0);
259 assert((unsigned)(substr_off) <= line_text.length());
260
261 int substr_len = (i == selection_end.first ? selection_end.second : line_text.length()) - substr_off;
262 assert(substr_len >= 0);
263 assert((unsigned)(substr_off + substr_len) <= line_text.length());
264
265 copy_text += line_text.substr(substr_off, substr_len);
266 }
267
268 wxTheClipboard->SetData(new wxTextDataObject(copy_text));
269 wxTheClipboard->Close();
270 }
271 }
272
select_all()273 void REHex::CodeCtrl::select_all()
274 {
275 if(!lines.empty())
276 {
277 selection_begin = CodeCharRef(0, 0);
278 selection_end = CodeCharRef((lines.size() - 1), lines.back().text.length());
279
280 Refresh();
281 }
282 }
283
set_offset_display(REHex::OffsetBase offset_display_base,off_t offset_display_upper_bound)284 void REHex::CodeCtrl::set_offset_display(REHex::OffsetBase offset_display_base, off_t offset_display_upper_bound)
285 {
286 this->offset_display_base = offset_display_base;
287 this->offset_display_upper_bound = offset_display_upper_bound;
288
289 update_widths();
290 update_scrollbars();
291 Refresh();
292 }
293
OnPaint(wxPaintEvent & event)294 void REHex::CodeCtrl::OnPaint(wxPaintEvent &event)
295 {
296 wxSize client_size = GetClientSize();
297
298 wxBufferedPaintDC dc(this);
299
300 dc.SetFont(font);
301 dc.SetBackground(*wxWHITE_BRUSH);
302 dc.SetBackgroundMode(wxTRANSPARENT);
303
304 dc.Clear();
305
306 int x = -scroll_xoff;
307 int y = -scroll_yoff;
308
309 int line_idx = 0;
310 for(auto line = lines.begin(); line != lines.end(); ++line, y += font_height, ++line_idx)
311 {
312 if((y + font_height) <= 0 || y >= client_size.GetHeight())
313 {
314 /* Line not visible, no point rendering it. */
315 continue;
316 }
317
318 wxColour fg_colour;
319 wxColour bg_colour;
320
321 int line_x = x + code_xoff;
322 std::string pending;
323
324 auto flush = [&dc, &line_x, &pending, &y]()
325 {
326 if(!pending.empty())
327 {
328 dc.DrawText(pending, line_x, y);
329 line_x += dc.GetTextExtent(pending).GetWidth();
330
331 pending.clear();
332 }
333 };
334
335 auto set = [&dc, &fg_colour, &bg_colour, &flush](const wxColour &fg, const wxColour &bg, bool force = false)
336 {
337 if(fg != fg_colour || bg != bg_colour || force)
338 {
339 flush();
340
341 dc.SetTextForeground(fg_colour = fg);
342 dc.SetTextBackground(bg_colour = bg);
343 dc.SetBackgroundMode(bg_colour == *wxWHITE ? wxTRANSPARENT : wxSOLID);
344 }
345 };
346
347 if(line->active)
348 {
349 set(*wxRED, *wxWHITE, true);
350 }
351 else{
352 set(*wxBLACK, *wxWHITE, true);
353 }
354
355 std::string offset_str = format_offset(line->offset, offset_display_base, offset_display_upper_bound);
356 dc.DrawText(offset_str.c_str(), x, y);
357
358 for(size_t c = 0; c < line->text.length(); ++c)
359 {
360 if(selection_begin <= CodeCharRef(line_idx, c) && selection_end > CodeCharRef(line_idx, c))
361 {
362 set(*wxWHITE, *wxBLUE);
363 }
364 else if(line->active)
365 {
366 set(*wxRED, *wxWHITE);
367 }
368 else{
369 set(*wxBLACK, *wxWHITE);
370 }
371
372 pending.push_back(line->text[c]);
373 }
374
375 flush();
376 }
377 }
378
OnErase(wxEraseEvent & event)379 void REHex::CodeCtrl::OnErase(wxEraseEvent& event)
380 {
381 // Left blank to disable erase
382 }
383
OnSize(wxSizeEvent & event)384 void REHex::CodeCtrl::OnSize(wxSizeEvent &event)
385 {
386 update_scrollbars();
387 }
388
OnFontSizeAdjustmentChanged(FontSizeAdjustmentEvent & event)389 void REHex::CodeCtrl::OnFontSizeAdjustmentChanged(FontSizeAdjustmentEvent &event)
390 {
391 font = wxFont(wxFontInfo().FaceName(wxGetApp().get_font_name()));
392
393 for(int i = 0; i < event.font_size_adjustment; ++i) { font.MakeLarger(); }
394 for(int i = 0; i > event.font_size_adjustment; --i) { font.MakeSmaller(); }
395
396 assert(font.IsFixedWidth());
397
398 wxClientDC dc(this);
399 dc.SetFont(font);
400
401 wxSize char_extent = dc.GetTextExtent("X");
402 font_width = char_extent.GetWidth();
403 font_height = char_extent.GetHeight();
404
405 update_widths();
406 update_scrollbars();
407 Refresh();
408
409 event.Skip();
410 }
411
OnScroll(wxScrollWinEvent & event)412 void REHex::CodeCtrl::OnScroll(wxScrollWinEvent &event)
413 {
414 wxEventType type = event.GetEventType();
415 int orientation = event.GetOrientation();
416
417 wxSize client_size = GetClientSize();
418
419 if(orientation == wxVERTICAL)
420 {
421 if(type == wxEVT_SCROLLWIN_THUMBTRACK || type == wxEVT_SCROLLWIN_THUMBRELEASE)
422 {
423 scroll_yoff = event.GetPosition();
424 }
425 else if(event.GetEventType() == wxEVT_SCROLLWIN_TOP)
426 {
427 scroll_yoff = 0;
428 }
429 else if(event.GetEventType() == wxEVT_SCROLLWIN_BOTTOM)
430 {
431 scroll_yoff = scroll_yoff_max;
432 }
433 else if(event.GetEventType() == wxEVT_SCROLLWIN_LINEUP)
434 {
435 --scroll_yoff;
436 }
437 else if(event.GetEventType() == wxEVT_SCROLLWIN_LINEDOWN)
438 {
439 ++scroll_yoff;
440 }
441 else if(event.GetEventType() == wxEVT_SCROLLWIN_PAGEUP)
442 {
443 scroll_yoff -= client_size.GetHeight();
444 }
445 else if(event.GetEventType() == wxEVT_SCROLLWIN_PAGEDOWN)
446 {
447 scroll_yoff += client_size.GetHeight();
448 }
449
450 if(scroll_yoff < 0)
451 {
452 scroll_yoff = 0;
453 }
454 else if(scroll_yoff > scroll_yoff_max)
455 {
456 scroll_yoff = scroll_yoff_max;
457 }
458
459 SetScrollPos(wxVERTICAL, scroll_yoff);
460 Refresh();
461 }
462 else if(orientation == wxHORIZONTAL)
463 {
464 if(type == wxEVT_SCROLLWIN_THUMBTRACK || type == wxEVT_SCROLLWIN_THUMBRELEASE)
465 {
466 scroll_xoff = event.GetPosition();
467 }
468 else if(event.GetEventType() == wxEVT_SCROLLWIN_TOP)
469 {
470 scroll_xoff = 0;
471 }
472 else if(event.GetEventType() == wxEVT_SCROLLWIN_BOTTOM)
473 {
474 scroll_xoff = scroll_xoff_max;
475 }
476 else if(event.GetEventType() == wxEVT_SCROLLWIN_LINEUP)
477 {
478 scroll_xoff -= font_width;
479 }
480 else if(event.GetEventType() == wxEVT_SCROLLWIN_LINEDOWN)
481 {
482 scroll_xoff += font_width;
483 }
484 else if(event.GetEventType() == wxEVT_SCROLLWIN_PAGEUP)
485 {
486 scroll_xoff -= client_size.GetWidth();
487 }
488 else if(event.GetEventType() == wxEVT_SCROLLWIN_PAGEDOWN)
489 {
490 scroll_xoff += client_size.GetWidth();
491 }
492
493 if(scroll_xoff < 0)
494 {
495 scroll_xoff = 0;
496 }
497 else if(scroll_xoff > scroll_xoff_max)
498 {
499 scroll_xoff = scroll_xoff_max;
500 }
501
502 SetScrollPos(wxHORIZONTAL, scroll_xoff);
503 Refresh();
504 }
505 }
506
OnWheel(wxMouseEvent & event)507 void REHex::CodeCtrl::OnWheel(wxMouseEvent &event)
508 {
509 wxMouseWheelAxis axis = event.GetWheelAxis();
510 int delta = event.GetWheelDelta();
511 int ticks_per_delta = event.GetLinesPerAction();
512
513 if(axis == wxMOUSE_WHEEL_VERTICAL)
514 {
515 ticks_per_delta *= font_height;
516
517 wheel_vert_accum += event.GetWheelRotation();
518
519 scroll_yoff -= (wheel_vert_accum / delta) * ticks_per_delta;
520
521 wheel_vert_accum = (wheel_vert_accum % delta);
522
523 if(scroll_yoff < 0)
524 {
525 scroll_yoff = 0;
526 }
527 else if(scroll_yoff > scroll_yoff_max)
528 {
529 scroll_yoff = scroll_yoff_max;
530 }
531
532 SetScrollPos(wxVERTICAL, scroll_yoff);
533 Refresh();
534 }
535 else if(axis == wxMOUSE_WHEEL_HORIZONTAL)
536 {
537 ticks_per_delta *= font_width;
538
539 wheel_horiz_accum += event.GetWheelRotation();
540
541 scroll_xoff += (wheel_horiz_accum / delta) * ticks_per_delta;
542
543 wheel_horiz_accum = (wheel_horiz_accum % delta);
544
545 if(scroll_xoff < 0)
546 {
547 scroll_xoff = 0;
548 }
549 else if(scroll_xoff > scroll_xoff_max)
550 {
551 scroll_xoff = scroll_xoff_max;
552 }
553
554 SetScrollPos(wxHORIZONTAL, scroll_xoff);
555 Refresh();
556 }
557 }
558
OnChar(wxKeyEvent & event)559 void REHex::CodeCtrl::OnChar(wxKeyEvent &event)
560 {
561 int key = event.GetKeyCode();
562 int modifiers = event.GetModifiers();
563
564 if((modifiers & wxMOD_CONTROL) && key == WXK_CONTROL_A)
565 {
566 select_all();
567 }
568 else if((modifiers & wxMOD_CONTROL) && key == WXK_CONTROL_C)
569 {
570 copy_selection();
571 }
572 else{
573 /* Not for us. Continue propagation. */
574 event.Skip();
575 }
576 }
577
OnLeftDown(wxMouseEvent & event)578 void REHex::CodeCtrl::OnLeftDown(wxMouseEvent &event)
579 {
580 int mouse_x = event.GetX();
581 int mouse_y = event.GetY();
582
583 mouse_selecting_from = char_near_rel_xy(mouse_x, mouse_y);
584 if(mouse_selecting_from.first >= 0)
585 {
586 mouse_selecting = true;
587
588 CaptureMouse();
589 mouse_selecting_timer.Start(MOUSE_SELECT_INTERVAL, wxTIMER_CONTINUOUS);
590
591 OnMotionTick(mouse_x, mouse_y);
592 }
593
594 Refresh();
595
596 /* We take focus when clicked. */
597 SetFocus();
598 }
599
OnLeftUp(wxMouseEvent & event)600 void REHex::CodeCtrl::OnLeftUp(wxMouseEvent &event)
601 {
602 if(mouse_selecting)
603 {
604 mouse_selecting_timer.Stop();
605 ReleaseMouse();
606
607 mouse_selecting = false;
608 }
609 }
610
OnRightDown(wxMouseEvent & event)611 void REHex::CodeCtrl::OnRightDown(wxMouseEvent &event)
612 {
613 /* If the user right clicks while selecting, and then releases the left button over the
614 * menu, we never receive the EVT_LEFT_UP event. Release the mouse and cancel the selection
615 * now, else we wind up keeping the mouse grabbed and stop it interacting with any other
616 * windows...
617 */
618
619 if(mouse_selecting)
620 {
621 mouse_selecting_timer.Stop();
622 ReleaseMouse();
623
624 mouse_selecting = false;
625 }
626
627 wxMenu menu;
628
629 menu.Append(wxID_COPY, "&Copy");
630 menu.Enable(wxID_COPY, (selection_begin < selection_end));
631
632 menu.AppendSeparator();
633
634 menu.Append(wxID_SELECTALL, "Select &All");
635
636 PopupMenu(&menu);
637
638 /* We take focus when clicked. */
639 SetFocus();
640 }
641
OnCopy(wxCommandEvent & event)642 void REHex::CodeCtrl::OnCopy(wxCommandEvent &event)
643 {
644 copy_selection();
645 }
646
OnSelectAll(wxCommandEvent & event)647 void REHex::CodeCtrl::OnSelectAll(wxCommandEvent &event)
648 {
649 select_all();
650 }
651
OnMotion(wxMouseEvent & event)652 void REHex::CodeCtrl::OnMotion(wxMouseEvent &event)
653 {
654 OnMotionTick(event.GetX(), event.GetY());
655 }
656
OnSelectTick(wxTimerEvent & event)657 void REHex::CodeCtrl::OnSelectTick(wxTimerEvent &event)
658 {
659 wxPoint window_pos = GetScreenPosition();
660 wxPoint mouse_pos = wxGetMousePosition();
661
662 OnMotionTick((mouse_pos.x - window_pos.x), (mouse_pos.y - window_pos.y));
663 }
664
OnMotionTick(int mouse_x,int mouse_y)665 void REHex::CodeCtrl::OnMotionTick(int mouse_x, int mouse_y)
666 {
667 if(mouse_selecting)
668 {
669 mouse_selecting_to = char_near_rel_xy(mouse_x, mouse_y);
670
671 if(mouse_selecting_to > mouse_selecting_from)
672 {
673 selection_begin = mouse_selecting_from;
674 selection_end = mouse_selecting_to;
675 }
676 else if(mouse_selecting_to < mouse_selecting_from)
677 {
678 selection_begin = mouse_selecting_to;
679 selection_end = mouse_selecting_from;
680 }
681 else{
682 selection_begin = CodeCharRef(-1, -1);
683 selection_end = CodeCharRef(-1, -1);
684 }
685
686 Refresh();
687 }
688 }
689