//+----------------------------------------------------------------------------+ //| Description: Magic Set Editor - Program to make Magic (tm) cards | //| Copyright: (C) 2001 - 2017 Twan van Laarhoven and Sean Hunt | //| License: GNU General Public License 2 or later (see file COPYING) | //+----------------------------------------------------------------------------+ // ----------------------------------------------------------------------------- : Includes #include #include #include #include #include #include #include #include #include DECLARE_TYPEOF_COLLECTION(SymbolPartP); // ----------------------------------------------------------------------------- : SymbolSelectEditor SymbolSelectEditor::SymbolSelectEditor(SymbolControl* control, bool rotate) : SymbolEditorBase(control) , click_mode(CLICK_NONE) , rotate(rotate) , cursorRotate(load_resource_cursor(_("rotate"))) , cursorShearX(load_resource_cursor(_("shear_x"))) , cursorShearY(load_resource_cursor(_("shear_y"))) { // Load resource images Image rot = load_resource_image(_("handle_rotate")); handleRotateTL = wxBitmap(rot); handleRotateTR = wxBitmap(rotate_image(rot,rad90)); handleRotateBR = wxBitmap(rotate_image(rot,rad180)); handleRotateBL = wxBitmap(rotate_image(rot,rad270)); Image shear = load_resource_image(_("handle_shear_x")); handleShearX = wxBitmap(shear); handleShearY = wxBitmap(rotate_image(shear,rad90)); handleCenter = wxBitmap(load_resource_image(_("handle_center"))); // Make sure all parts have updated bounds getSymbol()->updateBounds(); resetActions(); } // ----------------------------------------------------------------------------- : Drawing void SymbolSelectEditor::draw(DC& dc) { // highlight selected parts FOR_EACH(p, control.selected_parts.get()) { control.highlightPart(dc, *p, HIGHLIGHT_INTERIOR); } // highlight the part under the cursor if (highlightPart) { control.highlightPart(dc, *highlightPart, HIGHLIGHT_BORDER); } if (click_mode == CLICK_RECT) { // draw selection rectangle dc.SetBrush(*wxTRANSPARENT_BRUSH); dc.SetPen(wxPen(*wxCYAN,1,wxPENSTYLE_DOT)); //% TODO: use RotatedDC? RealRect rect = control.rotation.trRectToBB(RealRect(selection_rect_a, RealSize(selection_rect_b - selection_rect_a))); dc.DrawRectangle(rect); } else { // draw handles drawHandles(dc); } } void SymbolSelectEditor::drawHandles(DC& dc) { if (control.selected_parts.empty()) return; if (rotateAction) return; // not when rotating updateBoundingBox(); // Draw handles on all sides for (int dx = -1 ; dx <= 1 ; ++dx) { for (int dy = -1 ; dy <= 1 ; ++dy) { if (dx != 0 || dy != 0) { // no handle in the center drawHandle(dc, dx, dy); } } } // Draw rotation center? if (rotate) { drawRotationCenter(dc, center); } } void SymbolSelectEditor::drawHandle(DC& dc, int dx, int dy) { wxPoint p = control.rotation.tr(bounds.corner(dx, dy)); p.x += 4 * dx; p.y += 4 * dy; if (rotate) { // rotate or shear handle if (dx == 0) dc.DrawBitmap(handleShearX, p.x - 10, p.y - 3 - (dy < 0 ? 1 : 0)); if (dy == 0) dc.DrawBitmap(handleShearY, p.x - 3 - (dx < 0 ? 1 : 0), p.y - 10); else { // rotate if (dx == -1 && dy == -1) dc.DrawBitmap(handleRotateTL, p.x - 5, p.y - 5); if (dx == -1 && dy == 1) dc.DrawBitmap(handleRotateTR, p.x - 5, p.y - 11); if (dx == 1 && dy == -1) dc.DrawBitmap(handleRotateBL, p.x - 11, p.y - 5); if (dx == 1 && dy == 1) dc.DrawBitmap(handleRotateBR, p.x - 11, p.y - 11); } } else { // resize handle dc.SetBrush(*wxBLUE_BRUSH); dc.SetPen( *wxWHITE_PEN); dc.DrawRectangle(p.x - 3, p.y - 3, 6, 6); } } void SymbolSelectEditor::drawRotationCenter(DC& dc, const Vector2D& pos) { wxPoint p = control.rotation.tr(pos); dc.DrawBitmap(handleCenter, p.x - 9, p.y - 9); } // ----------------------------------------------------------------------------- : UI void SymbolSelectEditor::initUI(wxToolBar* tb, wxMenuBar* mb) { tb->AddSeparator(); tb->AddTool(ID_SYMBOL_COMBINE_MERGE, _TOOL_("merge"), load_resource_image(_("combine_or")), wxNullBitmap, wxITEM_CHECK, _TOOLTIP_("merge"), _HELP_("merge")); tb->AddTool(ID_SYMBOL_COMBINE_SUBTRACT, _TOOL_("subtract"), load_resource_image(_("combine_sub_dark")), wxNullBitmap, wxITEM_CHECK, _TOOLTIP_("subtract"), _HELP_("subtract")); tb->AddTool(ID_SYMBOL_COMBINE_INTERSECTION, _TOOL_("intersect"), load_resource_image(_("combine_and_dark")), wxNullBitmap, wxITEM_CHECK, _TOOLTIP_("intersect"), _HELP_("intersect")); tb->AddTool(ID_SYMBOL_COMBINE_DIFFERENCE, _TOOL_("difference"), load_resource_image(_("combine_xor")), wxNullBitmap, wxITEM_CHECK, _TOOLTIP_("difference"), _HELP_("difference")); tb->AddTool(ID_SYMBOL_COMBINE_OVERLAP, _TOOL_("overlap"), load_resource_image(_("combine_over")), wxNullBitmap, wxITEM_CHECK, _TOOLTIP_("overlap"), _HELP_("overlap")); tb->AddTool(ID_SYMBOL_COMBINE_BORDER, _TOOL_("border"), load_resource_image(_("combine_border")), wxNullBitmap, wxITEM_CHECK, _TOOLTIP_("border"), _HELP_("border")); tb->Realize(); } void SymbolSelectEditor::destroyUI(wxToolBar* tb, wxMenuBar* mb) { tb->DeleteTool(ID_SYMBOL_COMBINE_MERGE); tb->DeleteTool(ID_SYMBOL_COMBINE_SUBTRACT); tb->DeleteTool(ID_SYMBOL_COMBINE_INTERSECTION); tb->DeleteTool(ID_SYMBOL_COMBINE_DIFFERENCE); tb->DeleteTool(ID_SYMBOL_COMBINE_OVERLAP); tb->DeleteTool(ID_SYMBOL_COMBINE_BORDER); // HACK: hardcoded size of rest of toolbar tb->DeleteToolByPos(7); // delete separator } void SymbolSelectEditor::onUpdateUI(wxUpdateUIEvent& ev) { if (ev.GetId() >= ID_SYMBOL_COMBINE && ev.GetId() < ID_SYMBOL_COMBINE_MAX) { bool enable = false; bool check = true; FOR_EACH(p, control.selected_parts.get()) { if (SymbolShape* s = p->isSymbolShape()) { enable = true; if (s->combine != ev.GetId() - ID_SYMBOL_COMBINE) { check = false; break; } } // disable when symmetries are selected? } ev.Enable(enable); ev.Check(enable && check); } else if (ev.GetId() == ID_EDIT_DUPLICATE) { ev.Enable(!control.selected_parts.empty()); } else if (ev.GetId() == ID_EDIT_GROUP) { ev.Enable(control.selected_parts.size() >= 2); } else if (ev.GetId() == ID_EDIT_UNGROUP) { // is a group selected FOR_EACH(p, control.selected_parts.get()) { if (p->isSymbolGroup() && !p->isSymbolSymmetry()) { ev.Enable(true); return; } } ev.Enable(false); } else { ev.Enable(false); // we don't know about this item } } void SymbolSelectEditor::onCommand(int id) { if (id >= ID_SYMBOL_COMBINE && id < ID_SYMBOL_COMBINE_MAX) { // change combine mode addAction(make_unique( control.selected_parts.get(), static_cast(id - ID_SYMBOL_COMBINE) )); control.Refresh(false); } else if (id == ID_EDIT_DUPLICATE && !isEditing()) { // duplicate selection, not when dragging addAction(make_unique(*getSymbol(), control.selected_parts.get())); control.Refresh(false); } else if (id == ID_EDIT_GROUP && !isEditing()) { // group selection, not when dragging addAction(make_unique(*getSymbol(), control.selected_parts.get(), make_intrusive())); control.Refresh(false); } else if (id == ID_EDIT_UNGROUP && !isEditing()) { // ungroup selection, not when dragging addAction(make_unique(*getSymbol(), control.selected_parts.get())); control.Refresh(false); } } int SymbolSelectEditor::modeToolId() { return rotate ? ID_MODE_ROTATE : ID_MODE_SELECT; } // ----------------------------------------------------------------------------- : Mouse Events void SymbolSelectEditor::onLeftDown (const Vector2D& pos, wxMouseEvent& ev) { // change selection // Are we on a handle? int dx, dy; if (onAnyHandle(pos, &dx, &dy)) { click_mode = CLICK_HANDLE; return; // don't change the selection } // Select the part under the cursor SymbolPartP part = control.selected_parts.find(pos); if (part) { click_mode = control.selected_parts.select(part, ev.ShiftDown() ? SELECT_TOGGLE : SELECT_IF_OUTSIDE) ? (ev.ShiftDown() ? CLICK_NONE : CLICK_MOVE) : CLICK_TOGGLE; } else { // selection rectangle click_mode = CLICK_RECT; selection_rect_a = selection_rect_b = pos; if (!ev.ShiftDown()) { // select nothing control.selected_parts.clear(); } } // selection has changed updateBoundingBox(); control.signalSelectionChange(); control.Refresh(false); } void SymbolSelectEditor::onLeftUp (const Vector2D& pos, wxMouseEvent& ev) { if (isEditing()) { // stop editing resetActions(); } else { // mouse not moved -> change selection if (click_mode == CLICK_TOGGLE) { // switch between rotate and resize mode rotate = !rotate; } } click_mode = CLICK_NONE; control.Refresh(false); } void SymbolSelectEditor::onLeftDClick(const Vector2D& pos, wxMouseEvent& ev) { // start editing the points of the clicked part highlightPart = control.selected_parts.find(pos); if (highlightPart) { control.activatePart(highlightPart); } } void SymbolSelectEditor::onMouseMove (const Vector2D& from, const Vector2D& to, wxMouseEvent& ev) { // can we highlight a part? highlightPart = control.selected_parts.find(to); // are we on a handle? int dx, dy; if (!control.selected_parts.empty() && onAnyHandle(to, &dx, &dy)) { // we are on a handle, don't highlight highlightPart = SymbolPartP(); String shapes = control.selected_parts.size() > 1 ? _TYPE_("shapes") : _TYPE_("shape"); if (rotate) { // shear or rotating? if (dx == 0 || dy == 0) { SetStatusText(_HELP_1_("drag to shear", shapes)); control.SetCursor(dx == 0 ? cursorShearX : cursorShearY); } else { SetStatusText(_HELP_1_("drag to rotate", shapes)); control.SetCursor(cursorRotate); } } else { SetStatusText(_HELP_1_("drag to resize", shapes)); // what cursor to use? if (dx == dy) control.SetCursor(wxCURSOR_SIZENWSE); else if (dx == -dy) control.SetCursor(wxCURSOR_SIZENESW); else if (dx == 0) control.SetCursor(wxCURSOR_SIZENS); else if (dy == 0) control.SetCursor(wxCURSOR_SIZEWE); } } else { if (highlightPart) { SetStatusText(_HELP_("click to select shape")); } else { SetStatusText(_("")); } control.SetCursor(*wxSTANDARD_CURSOR); } control.Refresh(false); } template int snap(Event& ev) { return settings.symbol_grid_snap != ev.ShiftDown() ? settings.symbol_grid_size : 0; // shift toggles snap } void SymbolSelectEditor::onMouseDrag (const Vector2D& from, const Vector2D& to, wxMouseEvent& ev) { if (click_mode == CLICK_NONE) return; if (click_mode == CLICK_RECT) { // rectangle if (control.selected_parts.selectRect(selection_rect_a, selection_rect_b, to)) { control.signalSelectionChange(); } selection_rect_b = to; control.Refresh(false); return; } if (control.selected_parts.empty()) return; if (!isEditing()) { // we don't have an action yet, determine what to do // note: base it on the from position, which is the position where dragging started if (onAnyHandle(from, &scaleX, &scaleY)) { click_mode = CLICK_HANDLE; if (rotate) { if (scaleX == 0 || scaleY == 0) { // shear, center/fixed point on the opposite side auto action = make_unique(control.selected_parts.get(), bounds.corner(-scaleX, -scaleY)); shearAction = action.get(); addAction(std::move(action)); } else { // rotate auto action = make_unique(control.selected_parts.get(), center); rotateAction = action.get(); addAction(std::move(action)); startAngle = angleTo(to); } } else { // we are on a handle; start scaling auto action = make_unique(control.selected_parts.get(), scaleX, scaleY); scaleAction = action.get(); addAction(std::move(action)); } } else { // move click_mode = CLICK_MOVE; auto action = make_unique(control.selected_parts.get()); moveAction = action.get(); addAction(std::move(action)); } } // now update the action if (moveAction) { // move the selected parts moveAction->constrain = ev.ControlDown(); moveAction->snap = snap(ev); moveAction->move(to - from); } else if (scaleAction) { // scale the selected parts Vector2D delta = to-from; Vector2D dMin, dMax; if (scaleX == -1) dMin.x = delta.x; if (scaleX == 1) dMax.x = delta.x; if (scaleY == -1) dMin.y = delta.y; if (scaleY == 1) dMax.y = delta.y; // scaleAction->constrain = ev.ControlDown(); scaleAction->constrain = true; // always constrain diagonal scaling scaleAction->snap = snap(ev); scaleAction->move(dMin, dMax); } else if (rotateAction) { // rotate the selected parts Radians angle = angleTo(to); rotateAction->constrain = ev.ControlDown(); rotateAction->rotateTo(startAngle - angle); } else if (shearAction) { // shear the selected parts Vector2D delta = to-from; delta = delta.mul(Vector2D(scaleY, scaleX)); delta = delta.div(bounds.max - bounds.min); // shearAction->constrain = ev.ControlDown(); shearAction->snap = snap(ev); shearAction->move(delta); } control.Refresh(false); } // ----------------------------------------------------------------------------- : Key Events void SymbolSelectEditor::onKeyChange (wxKeyEvent& ev) { if (ev.GetKeyCode() == WXK_CONTROL || ev.GetKeyCode() == WXK_SHIFT) { // changed constrains if (moveAction) { moveAction->constrain = ev.ControlDown(); moveAction->snap = snap(ev); moveAction->move(Vector2D()); // apply constrains control.Refresh(false); } else if (scaleAction) { // only allow constrained scaling in diagonal direction // scaleAction->constrain = ev.ControlDown(); scaleAction->constrain = true; // always constrain diagonal scaling scaleAction->snap = snap(ev); scaleAction->update(); // apply constrains control.Refresh(false); } else if (rotateAction) { rotateAction->constrain = ev.ControlDown(); rotateAction->rotateBy(0); // apply constrains control.Refresh(false); } else if (shearAction) { shearAction->snap = snap(ev); shearAction->move(Vector2D()); // apply constrains control.Refresh(false); } } } void SymbolSelectEditor::onChar(wxKeyEvent& ev) { if (ev.GetKeyCode() == WXK_DELETE) { // delete selected parts addAction(make_unique(*getSymbol(), control.selected_parts.get())); if (control.selected_parts.selected(highlightPart)) highlightPart = SymbolPartP(); // deleted it control.selected_parts.clear(); resetActions(); control.Refresh(false); } else { // move selection using arrow keys double step = 1.0 / settings.symbol_grid_size; Vector2D delta; if (ev.GetKeyCode() == WXK_LEFT) delta = Vector2D(-step, 0); else if (ev.GetKeyCode() == WXK_RIGHT) delta = Vector2D( step, 0); else if (ev.GetKeyCode() == WXK_UP) delta = Vector2D(0, -step); else if (ev.GetKeyCode() == WXK_DOWN) delta = Vector2D(0, step); else { ev.Skip(); return; } addAction(make_unique(control.selected_parts.get(), delta)); } } bool SymbolSelectEditor::isEditing() { return moveAction || scaleAction || rotateAction || shearAction; } // ----------------------------------------------------------------------------- : Other bool SymbolSelectEditor::onHandle(const Vector2D& mpos, int dx, int dy) { wxPoint p = control.rotation.tr(bounds.corner(dx, dy)); wxPoint mp = control.rotation.tr(mpos); p.x = p.x + 4 * dx; p.y = p.y + 4 * dy; return mp.x >= p.x - 4 && mp.x < p.x + 4 && mp.y >= p.y - 4 && mp.y < p.y + 4; } bool SymbolSelectEditor::onAnyHandle(const Vector2D& mpos, int* dxOut, int* dyOut) { for (int dx = -1 ; dx <= 1 ; ++dx) { for (int dy = -1 ; dy <= 1 ; ++dy) { if ((dx != 0 || dy != 0) && onHandle(mpos, dx, dy)) { // (0,0) == center, not a handle *dxOut = dx; *dyOut = dy; return true; } } } return false; } double SymbolSelectEditor::angleTo(const Vector2D& pos) { return atan2(center.x - pos.x, center.y - pos.y); } void SymbolSelectEditor::updateBoundingBox() { // Find min and max coordinates bounds = Bounds(); FOR_EACH(p, control.selected_parts.get()) { bounds.update(p->bounds); } /* // Find rotation center center = Vector2D(0,0); FOR_EACH(p, control.selected_parts) { Vector2D size = p->max_pos - p->min_pos; size = size.mul(p->rotation_center); center += p->min_pos + size; } center /= control.selected_parts.size(); */ center = bounds.corner(0,0); } void SymbolSelectEditor::resetActions() { moveAction = nullptr; scaleAction = nullptr; rotateAction = nullptr; shearAction = nullptr; }