mirror of
https://github.com/amyinspace/MagicSetEditor2.git
synced 2026-06-10 04:57:00 -04:00
554 lines
19 KiB
C++
554 lines
19 KiB
C++
//+----------------------------------------------------------------------------+
|
|
//| 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 <util/prec.hpp>
|
|
#include <gui/control/card_editor.hpp>
|
|
#include <gui/value/editor.hpp>
|
|
#include <gui/set/cards_panel.hpp>
|
|
#include <gui/util.hpp>
|
|
#include <data/field.hpp>
|
|
#include <data/stylesheet.hpp>
|
|
#include <data/settings.hpp>
|
|
#include <util/find_replace.hpp>
|
|
#include <util/window_id.hpp>
|
|
#include <wx/caret.h>
|
|
#include <boost/iterator/filter_iterator.hpp>
|
|
|
|
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> action) {
|
|
set->actions.addAction(move(action));
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------- : Algorithms
|
|
|
|
/// Swap the order of comparison, i.e. greater-than instead of less-than
|
|
template <typename Comp>
|
|
struct SwapCompare {
|
|
Comp comp;
|
|
SwapCompare(Comp comp) : comp(comp) {}
|
|
template <typename T, typename U> 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 <typename It, typename V, typename Comp>
|
|
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 <typename It, typename V, typename Comp>
|
|
It prev_element(It first, It last, V const& x, Comp comp) {
|
|
return next_element(first, last, x, SwapCompare<Comp>(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<ValueViewerP>::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 <typename Iterator>
|
|
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<ValueViewer*> init_search_order(vector<ValueViewerP> const& viewers) {
|
|
vector<ValueViewer*> 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<wxFrame*>( 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<CardsPanel*> (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<wxFrame*>( 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<CardsPanel*> (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<CardsPanel*> (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 ()
|