//+----------------------------------------------------------------------------+ //| Description: Magic Set Editor - Program to make Magic (tm) cards | //| Copyright: (C) Twan van Laarhoven and the other MSE developers | //| License: GNU General Public License 2 or later (see file COPYING) | //+----------------------------------------------------------------------------+ // ----------------------------------------------------------------------------- : Includes #include #include #include #include #include #include #include #include #include #include #include #include const bool draw_hover_borders = false; // ----------------------------------------------------------------------------- : DataEditor DataEditor::DataEditor(Window* parent, int id, long style) : CardViewer(parent, id, style | wxWANTS_CHARS) , next_in_tab_order(nullptr) , current_viewer(nullptr) , current_editor(nullptr) , hovered_viewer(nullptr) { // Create a caret SetCaret(new wxCaret(this,1,1)); } ValueViewerP DataEditor::makeViewer(const StyleP& style) { return style->makeEditor(*this); } // ----------------------------------------------------------------------------- : Utility for ValueViewers DrawWhat DataEditor::drawWhat(const ValueViewer* viewer) const { int what = DRAW_NORMAL | DRAW_ACTIVE * viewerIsCurrent(viewer) | DRAW_HOVER * (draw_hover_borders && viewer == hovered_viewer); if (nativeLook()) { what |= DRAW_BOXES | DRAW_EDITING | DRAW_NATIVELOOK | DRAW_ERRORS; } else { StyleSheetSettings& ss = settings.stylesheetSettingsFor(set->stylesheetFor(card)); what |= DRAW_BORDERS * ss.card_borders() | (DRAW_BOXES | DRAW_EDITING) * ss.card_draw_editing() | DRAW_ERRORS; } return (DrawWhat)what; } bool DataEditor::viewerIsCurrent(const ValueViewer* viewer) const { return viewer == current_viewer && FindFocus() == this; } void DataEditor::addAction(unique_ptr action) { set->actions.addAction(move(action)); } // ----------------------------------------------------------------------------- : Algorithms /// Swap the order of comparison, i.e. greater-than instead of less-than template struct SwapCompare { Comp comp; SwapCompare(Comp comp) : comp(comp) {} template inline bool operator () (T x, U y) { return comp(y,x); } }; // Return the next element in a collection after x, when using comp as ordering template It next_element(It first, It last, V const& x, Comp comp) { It best = last; for (It it = first ; it != last ; ++it) { if (*it != x && comp(x, *it)) { // this is a candidate if (best == last || comp(*it, *best)) { best = it; } } } return best; } template It prev_element(It first, It last, V const& x, Comp comp) { return next_element(first, last, x, SwapCompare(comp)); } // ----------------------------------------------------------------------------- : Selection bool DataEditor::AcceptsFocus() const { return wxWindow::AcceptsFocus(); } void DataEditor::select(ValueViewer* new_viewer) { ValueEditor* old_editor = current_editor; if (new_viewer) { current_viewer = new_viewer; current_editor = new_viewer->getEditor(); } else { current_viewer = nullptr; current_editor = nullptr; } if (current_editor != old_editor) { // selection has changed if (old_editor) old_editor->onLoseFocus(); if (current_editor) current_editor->onFocus(); onChange(); } } struct CompareTabOrder { bool operator() (ValueViewer const& a, ValueViewer const& b) { Style& as = *a.getStyle(), &bs = *b.getStyle(); // if tab_index differs, use that if (as.tab_index < bs.tab_index) return true; if (as.tab_index > bs.tab_index) return false; // otherwise look at the positions // To get a total order, we look at the viewer center. // Not completely (y,x), because for viewers that are almost at the same y we prefer to sort by x double ax = a.bounding_box.x + 0.25*a.bounding_box.width; // bias a bit to the left of the middle double bx = b.bounding_box.x + 0.25*b.bounding_box.width; double ay = a.bounding_box.top() + a.bounding_box.bottom() + 0.1*ax; // a bit of x, so that dominates when y is approximately equal double by = b.bounding_box.top() + b.bounding_box.bottom() + 0.1*bx; if (ay < by) return true; if (ay > by) return false; if (ax < bx) return true; if (ax > bx) return false; // arbitrary order otherwise return a.getField()->name < b.getField()->name; } bool operator() (ValueViewer* a, ValueViewer* b) { assert(a && b); return operator () (*a, *b); } bool operator() (ValueViewerP const& a, ValueViewer* b) { return operator () (*a, *b); } bool operator() (ValueViewer* a, ValueViewerP const& b) { return operator () (*a, *b); } bool operator() (ValueViewerP const& a, ValueViewerP const& b) { return operator () (*a, *b); } }; bool is_enabled(ValueViewerP const& v) { return v->getField()->editable && v->isVisible(); } bool DataEditor::selectWithTab(vector::iterator const& it) { if (it != viewers.end()) { select(it->get()); return true; } else { select(nullptr); return false; } } bool DataEditor::selectFirst() { // This would be nicer with boost::range, but filtered adaptor was only introduced in boost 1.42(?) return selectWithTab(std::min_element( boost::make_filter_iterator(is_enabled, viewers.begin(), viewers.end()), boost::make_filter_iterator(is_enabled, viewers.end(), viewers.end()), CompareTabOrder()).base()); } bool DataEditor::selectLast() { return selectWithTab(std::max_element( boost::make_filter_iterator(is_enabled, viewers.begin(), viewers.end()), boost::make_filter_iterator(is_enabled, viewers.end(), viewers.end()), CompareTabOrder()).base()); } bool DataEditor::selectNext() { if (!current_viewer) return selectFirst(); return selectWithTab(next_element( boost::make_filter_iterator(is_enabled, viewers.begin(), viewers.end()), boost::make_filter_iterator(is_enabled, viewers.end(), viewers.end()), current_viewer, CompareTabOrder()).base()); } bool DataEditor::selectPrevious() { if (!current_viewer) return selectLast(); return selectWithTab(prev_element( boost::make_filter_iterator(is_enabled, viewers.begin(), viewers.end()), boost::make_filter_iterator(is_enabled, viewers.end(), viewers.end()), current_viewer, CompareTabOrder()).base()); } void DataEditor::onInit() { current_viewer = nullptr; current_editor = nullptr; hovered_viewer = nullptr; viewers_in_search_order.clear(); // hide caret if it is shown wxCaret* caret = GetCaret(); if (caret->IsVisible()) caret->Hide(); } // ----------------------------------------------------------------------------- : Search / replace template bool DataEditor::search(Iterator it, Iterator end, FindInfo& find, bool from_start) { bool include = from_start || current_viewer == nullptr; for (;it != end; ++it) { ValueViewer* viewer = *it; if (viewer == current_viewer) include = true; if (include && viewer->getField()->editable && viewer->isVisible()) { ValueEditor* editor = viewer->getEditor(); if (editor) { if (editor && editor->search(find, from_start || viewer != current_viewer)) { selectViewer(viewer); return true; // done } } } } return false; } vector init_search_order(vector const& viewers) { vector in_order; for (auto const& v : viewers) { if (v->getEditor()) in_order.push_back(v.get()); } stable_sort(in_order.begin(), in_order.end(), CompareTabOrder()); return in_order; } bool DataEditor::search(FindInfo& find, bool from_start) { if (viewers_in_search_order.empty()) { viewers_in_search_order = init_search_order(viewers); } if (find.forward()) { return search(viewers_in_search_order.begin(), viewers_in_search_order.end(), find, from_start); } else { return search(viewers_in_search_order.rbegin(), viewers_in_search_order.rend(), find, from_start); } } // ----------------------------------------------------------------------------- : Clipboard & Formatting bool DataEditor::canCut() const { return current_editor && current_editor->canCut(); } bool DataEditor::canCopy() const { return current_editor && current_editor->canCopy(); } bool DataEditor::canPaste() const { return current_editor && current_editor->canPaste(); } bool DataEditor::canFormat(int type) const { return current_editor && current_editor->canFormat(type); } bool DataEditor::hasFormat(int type) const { return current_editor && current_editor->hasFormat(type); } bool DataEditor::canSelectAll() const { return current_editor && current_editor->canSelectAll(); } void DataEditor::doCut() { if (current_editor) current_editor->doCut(); } void DataEditor::doCopy() { if (current_editor) current_editor->doCopy(); } void DataEditor::doPaste() { if (current_editor) current_editor->doPaste(); } void DataEditor::doFormat(int type) { if (current_editor) current_editor->doFormat(type); } void DataEditor::doSelectAll() { if (current_editor) current_editor->doSelectAll(); } wxMenu* DataEditor::getMenu(int type) const { if (current_editor) { return current_editor->getMenu(type); } else { return nullptr; } } void DataEditor::onCommand(int id) { if (current_editor) { current_editor->onCommand(id); } } void DataEditor::insert(const String& text, const String& action_name) { if (current_editor) current_editor->insert(text, action_name); } // ----------------------------------------------------------------------------- : Mouse events void DataEditor::onLeftDown(wxMouseEvent& ev) { ev.Skip(); // for focus CaptureMouse(); // change selection? selectViewer(ev, &ValueEditor::onLeftDown); } void DataEditor::onLeftUp(wxMouseEvent& ev) { if (HasCapture()) ReleaseMouse(); if (current_editor && current_viewer) { RealPoint pos = mousePoint(ev, *current_viewer); if (current_viewer->containsPoint(pos)) { current_editor->onLeftUp(pos, ev); } } } void DataEditor::onLeftDClick(wxMouseEvent& ev) { if (current_editor && current_viewer) { RealPoint pos = mousePoint(ev, *current_viewer); if (current_viewer->containsPoint(pos)) { current_editor->onLeftDClick(pos, ev); } } } void DataEditor::onRightDown(wxMouseEvent& ev) { ev.Skip(); // for context menu // change selection? selectViewer(ev, &ValueEditor::onRightDown); } void DataEditor::onMouseWheel(wxMouseEvent& ev) { if (current_editor && current_viewer) { RealPoint pos = mousePoint(ev, *current_viewer); if (current_viewer->containsPoint(pos)) { if (current_editor->onMouseWheel(pos, ev)) return; } } ev.Skip(); } void DataEditor::onMotion(wxMouseEvent& ev) { if (current_editor && current_viewer) { RealPoint pos = mousePoint(ev, *current_viewer); current_editor->onMotion(pos, ev); } if (!HasCapture()) { // find editor under mouse ValueViewer* old_hovered_viewer = hovered_viewer; bool hovered_label = false; hovered_viewer = mousedOverViewer(ev, &hovered_label); if (old_hovered_viewer && hovered_viewer != old_hovered_viewer) { ValueEditor* e = old_hovered_viewer->getEditor(); RealPoint pos = mousePoint(ev, *old_hovered_viewer); if (e) e->onMouseLeave(pos, ev); if (draw_hover_borders) redraw(*old_hovered_viewer); } if (hovered_viewer && hovered_viewer != old_hovered_viewer) { if (draw_hover_borders) redraw(*hovered_viewer); } // change cursor and set status text if (hovered_viewer && !hovered_label) { ValueEditor* e = hovered_viewer->getEditor(); RealPoint pos = mousePoint(ev, *hovered_viewer); wxCursor c; if (e) c = e->cursor(pos); if (c.Ok()) { SetCursor(c); } else { SetCursor(wxCURSOR_ARROW); } } else { SetCursor(wxCURSOR_ARROW); } // set status text wxFrame* frame = dynamic_cast( wxGetTopLevelParent(this) ); if (frame) { frame->SetStatusText(hovered_viewer ? hovered_viewer->getField()->description.get() : String()); } } } void DataEditor::onMouseEnter(wxMouseEvent& ev) { ev.Skip(); if (GetId() == ID_CARD_LINK_EDITOR) { CardsPanel* panel = dynamic_cast (GetParent()); if (panel) { panel->refreshCard(card); } } } void DataEditor::onMouseLeave(wxMouseEvent& ev) { // on mouse leave for editor if (hovered_viewer) { ValueEditor* e = hovered_viewer->getEditor(); if (e) e->onMouseLeave(mousePoint(ev,*hovered_viewer), ev); if (draw_hover_borders && hovered_viewer) redraw(*hovered_viewer); hovered_viewer = nullptr; } // clear status text wxFrame* frame = dynamic_cast( wxGetTopLevelParent(this) ); if (frame) frame->SetStatusText(wxEmptyString); } bool DataEditor::selectViewer(ValueViewer* v) { if (!v) return false; ValueEditor* e = v->getEditor(); if (!e) return false; ValueEditor* old_editor = current_editor; current_editor = e; current_viewer = v; if (old_editor != current_editor) { // selection has changed, send focus events if (old_editor) old_editor->onLoseFocus(); if (current_editor) current_editor->onFocus(); return true; } return false; } void DataEditor::selectViewer(wxMouseEvent& ev, bool (ValueEditor::*event)(const RealPoint&, wxMouseEvent&)) { // change viewer/editor ValueViewer* viewer = mousedOverViewer(ev); bool changed = viewer && selectViewer(viewer); // pass event if (current_editor && current_viewer) { RealPoint pos = mousePoint(ev, *current_viewer); if (current_viewer->containsPoint(pos)) { (current_editor->*event)(pos, ev); } } // refresh? if (changed) { // selection has changed, refresh viewers // NOTE: after passing mouse down event, otherwise opening combo box produces flicker onChange(); } } RealPoint DataEditor::mousePoint(const wxMouseEvent& ev, const ValueViewer& viewer) const { Rotation rot = getRotation(); Rotater r(rot,viewer.getRotation()); return rot.trInv(RealPoint(ev.GetX(), ev.GetY())); } ValueViewer* DataEditor::mousedOverViewer(const wxMouseEvent& ev, bool* over_label_out) const { FOR_EACH_EDITOR_REVERSE{ // find high z index fields first if (v->getField()->editable) { if (v->containsPoint(mousePoint(ev,*v))) { if (over_label_out) *over_label_out = false; return v.get(); } else if (nativeLook()) { int y = ev.GetY() + GetScrollPos(wxVERTICAL); if (y >= v->getStyle()->top && y < v->getStyle()->bottom) { if (over_label_out) *over_label_out = true; return v.get(); } } } } return nullptr; } void DataEditor::onLoseCapture(wxMouseCaptureLostEvent&) { // We already test for wrong release with HasCapture() // but stupid wxwidget people decided to throw assertion failures } // ----------------------------------------------------------------------------- : Keyboard events void DataEditor::onChar(wxKeyEvent& ev) { if (ev.GetKeyCode() == WXK_TAB) { if (!ev.ShiftDown()) { // try to select the next editor if (selectNext()) return; // send a navigation event to our parent, to select another control wxNavigationKeyEvent evt; GetParent()->HandleWindowEvent(evt); } else { // try to select the previos editor if (selectPrevious()) return; // send a navigation event to our parent, to select another control wxNavigationKeyEvent evt; evt.SetDirection(false); GetParent()->HandleWindowEvent(evt); } } else if (current_editor) { if (!current_editor->onChar(ev)) { ev.Skip(); } } else { ev.Skip(); } if (GetId() == ID_CARD_LINK_EDITOR) { CardsPanel* panel = dynamic_cast (GetParent()); if (panel) { panel->refreshCard(card); } } } // ----------------------------------------------------------------------------- : Menu events void DataEditor::onContextMenu(wxContextMenuEvent& ev) { if (current_editor) { wxMenu m; add_menu_item_tr(&m, ID_EDIT_CUT, "cut", "cut"); add_menu_item_tr(&m, ID_EDIT_COPY, "copy", "copy"); add_menu_item_tr(&m, ID_EDIT_PASTE, "paste", "paste"); m.Enable(ID_EDIT_CUT, canCut()); m.Enable(ID_EDIT_COPY, canCopy()); m.Enable(ID_EDIT_PASTE, canPaste()); if (current_editor->onContextMenu(m, ev)) { PopupMenu(&m); } } } void DataEditor::onMenu(wxCommandEvent& ev) { if (current_editor) { if (!current_editor->onCommand(ev.GetId())) { ev.Skip(); } } else { ev.Skip(); } } // ----------------------------------------------------------------------------- : Focus events void DataEditor::onFocus(wxFocusEvent& ev) { if (current_editor) { current_editor->onFocus(); onChange(); } else { if (ev.GetWindow() && ev.GetWindow() == next_in_tab_order) { selectLast(); } else { selectFirst(); } } CardsPanel* panel = dynamic_cast (GetParent()); if (panel) { panel->setFocusedEditor(this); } } void DataEditor::onLoseFocus(wxFocusEvent& ev) { if (current_editor) { current_editor->onLoseFocus(); onChange(); } } // ----------------------------------------------------------------------------- : Event table BEGIN_EVENT_TABLE(DataEditor, CardViewer) EVT_LEFT_DOWN (DataEditor::onLeftDown) EVT_LEFT_UP (DataEditor::onLeftUp) EVT_LEFT_DCLICK (DataEditor::onLeftDClick) EVT_RIGHT_DOWN (DataEditor::onRightDown) EVT_MOTION (DataEditor::onMotion) EVT_MOUSEWHEEL (DataEditor::onMouseWheel) EVT_ENTER_WINDOW (DataEditor::onMouseEnter) EVT_LEAVE_WINDOW (DataEditor::onMouseLeave) EVT_CONTEXT_MENU (DataEditor::onContextMenu) EVT_MENU (wxID_ANY, DataEditor::onMenu) EVT_CHAR (DataEditor::onChar) EVT_SET_FOCUS (DataEditor::onFocus) EVT_KILL_FOCUS (DataEditor::onLoseFocus) EVT_MOUSE_CAPTURE_LOST(DataEditor::onLoseCapture) END_EVENT_TABLE ()